421 lines
15 KiB
TypeScript

// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Optional, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Params } from '@angular/router';
import { CoreSite } from '@classes/site';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
AddonModAssign,
AddonModAssignAssign,
AddonModAssignProvider,
AddonModAssignSubmissionGradingSummary,
} from '../../services/assign';
import { AddonModAssignOffline } from '../../services/assign-offline';
import {
AddonModAssignAutoSyncData,
AddonModAssignSync,
AddonModAssignSyncProvider,
AddonModAssignSyncResult,
} from '../../services/assign-sync';
import { AddonModAssignSubmissionComponent } from '../submission/submission';
/**
* Component that displays an assignment.
*/
@Component({
selector: 'addon-mod-assign-index',
templateUrl: 'addon-mod-assign-index.html',
})
export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
@ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
component = AddonModAssignProvider.COMPONENT;
moduleName = 'assign';
assign?: AddonModAssignAssign; // The assign object.
canViewAllSubmissions = false; // Whether the user can view all submissions.
canViewOwnSubmission = false; // Whether the user can view their own submission.
timeRemaining?: string; // Message about time remaining to submit.
lateSubmissions?: string; // Message about late submissions.
showNumbers = true; // Whether to show number of submissions with each status.
summary?: AddonModAssignSubmissionGradingSummary; // The grading summary.
needsGradingAvalaible = false; // Whether we can see the submissions that need grading.
groupInfo: CoreGroupInfo = {
groups: [],
separateGroups: false,
visibleGroups: false,
defaultGroupId: 0,
};
// Status.
submissionStatusSubmitted = AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED;
submissionStatusDraft = AddonModAssignProvider.SUBMISSION_STATUS_DRAFT;
needGrading = AddonModAssignProvider.NEED_GRADING;
protected currentUserId?: number; // Current user ID.
protected currentSite?: CoreSite; // Current user ID.
protected syncEventName = AddonModAssignSyncProvider.AUTO_SYNCED;
// Observers.
protected savedObserver?: CoreEventObserver;
protected submittedObserver?: CoreEventObserver;
protected gradedObserver?: CoreEventObserver;
constructor(
protected content?: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage,
) {
super('AddonModLessonIndexComponent', content, courseContentsPage);
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
super.ngOnInit();
this.currentUserId = CoreSites.getCurrentSiteUserId();
this.currentSite = CoreSites.getCurrentSite();
// Listen to events.
this.savedObserver = CoreEvents.on(
AddonModAssignProvider.SUBMISSION_SAVED_EVENT,
(data) => {
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
// Assignment submission saved, refresh data.
this.showLoadingAndRefresh(true, false);
}
},
this.siteId,
);
this.submittedObserver = CoreEvents.on(
AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT,
(data) => {
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
// Assignment submitted, check completion.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
// Reload data since it can have offline data now.
this.showLoadingAndRefresh(true, false);
}
},
this.siteId,
);
this.gradedObserver = CoreEvents.on(AddonModAssignProvider.GRADED_EVENT, (data) => {
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
// Assignment graded, refresh data.
this.showLoadingAndRefresh(true, false);
}
}, this.siteId);
await this.loadContent(false, true);
try {
await AddonModAssign.logView(this.assign!.id, this.assign!.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors. Just don't check Module completion.
}
if (this.canViewAllSubmissions) {
// User can see all submissions, log grading view.
CoreUtils.ignoreErrors(AddonModAssign.logGradingView(this.assign!.id, this.assign!.name));
} else if (this.canViewOwnSubmission) {
// User can only see their own submission, log view the user submission.
CoreUtils.ignoreErrors(AddonModAssign.logSubmissionView(this.assign!.id, this.assign!.name));
}
}
/**
* Expand the description.
*/
expandDescription(ev?: Event): void {
ev?.preventDefault();
ev?.stopPropagation();
if (this.assign && (this.description || this.assign.introattachments)) {
CoreTextUtils.viewText(Translate.instant('core.description'), this.description || '', {
component: this.component,
componentId: this.module.id,
files: this.assign.introattachments,
filter: true,
contextLevel: 'module',
instanceId: this.module.id,
courseId: this.courseId,
});
}
}
/**
* Get assignment data.
*
* @param refresh If it's refreshing content.
* @param sync If it should try to sync.
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> {
// Get assignment data.
try {
this.assign = await AddonModAssign.getAssignment(this.courseId, this.module.id);
this.dataRetrieved.emit(this.assign);
this.description = this.assign.intro;
if (sync) {
// Try to synchronize the assign.
await CoreUtils.ignoreErrors(this.syncActivity(showErrors));
}
// Check if there's any offline data for this assign.
this.hasOffline = await AddonModAssignOffline.hasAssignOfflineData(this.assign.id);
// Get assignment submissions.
const submissions = await AddonModAssign.getSubmissions(this.assign.id, { cmId: this.module.id });
const time = CoreTimeUtils.timestamp();
this.canViewAllSubmissions = submissions.canviewsubmissions;
if (submissions.canviewsubmissions) {
// Calculate the messages to display about time remaining and late submissions.
if (this.assign.duedate > 0) {
if (this.assign.duedate - time <= 0) {
this.timeRemaining = Translate.instant('addon.mod_assign.assignmentisdue');
} else {
this.timeRemaining = CoreTimeUtils.formatDuration(this.assign.duedate - time, 3);
if (this.assign.cutoffdate) {
if (this.assign.cutoffdate > time) {
this.lateSubmissions = Translate.instant(
'addon.mod_assign.latesubmissionsaccepted',
{ $a: CoreTimeUtils.userDate(this.assign.cutoffdate * 1000) },
);
} else {
this.lateSubmissions = Translate.instant('addon.mod_assign.nomoresubmissionsaccepted');
}
} else {
this.lateSubmissions = '';
}
}
} else {
this.timeRemaining = '';
this.lateSubmissions = '';
}
// Check if groupmode is enabled to avoid showing wrong numbers.
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.assign.cmid, false);
this.showNumbers = (this.groupInfo.groups && this.groupInfo.groups.length == 0) ||
this.currentSite!.isVersionGreaterEqualThan('3.5');
await this.setGroup(CoreGroups.validateGroupId(this.group, this.groupInfo));
return;
}
try {
// Check if the user can view their own submission.
await AddonModAssign.getSubmissionStatus(this.assign.id, { cmId: this.module.id });
this.canViewOwnSubmission = true;
} catch (error) {
this.canViewOwnSubmission = false;
if (error.errorcode !== 'nopermission') {
throw error;
}
}
} finally {
this.fillContextMenu(refresh);
}
}
/**
* Set group to see the summary.
*
* @param groupId Group ID.
* @return Resolved when done.
*/
async setGroup(groupId = 0): Promise<void> {
this.group = groupId;
const submissionStatus = await AddonModAssign.getSubmissionStatus(this.assign!.id, {
groupId: this.group,
cmId: this.module.id,
});
this.summary = submissionStatus.gradingsummary;
if (!this.summary) {
this.needsGradingAvalaible = false;
return;
}
if (this.summary?.warnofungroupedusers === true) {
this.summary.warnofungroupedusers = 'ungroupedusers';
} else {
switch (this.summary?.warnofungroupedusers) {
case AddonModAssignProvider.WARN_GROUPS_REQUIRED:
this.summary.warnofungroupedusers = 'ungroupedusers';
break;
case AddonModAssignProvider.WARN_GROUPS_OPTIONAL:
this.summary.warnofungroupedusers = 'ungroupedusersoptional';
break;
default:
this.summary.warnofungroupedusers = '';
break;
}
}
this.needsGradingAvalaible =
(submissionStatus.gradingsummary?.submissionsneedgradingcount || 0) > 0 &&
this.currentSite!.isVersionGreaterEqualThan('3.2');
}
/**
* Go to view a list of submissions.
*
* @param status Status to see.
* @param hasSubmissions If the status has any submission.
*/
goToSubmissionList(status?: string, hasSubmissions = false): void {
if (typeof status != 'undefined' && !hasSubmissions && this.showNumbers) {
return;
}
const params: Params = {
groupId: this.group || 0,
moduleName: this.moduleName,
};
if (typeof status != 'undefined') {
params.status = status;
}
CoreNavigator.navigate('submission', {
params,
});
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned by the sync function.
* @return If succeed or not.
*/
protected hasSyncSucceed(result: AddonModAssignSyncResult): boolean {
if (result.updated) {
this.submissionComponent?.invalidateAndRefresh(false);
}
return result.updated;
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModAssign.invalidateAssignmentData(this.courseId));
if (this.assign) {
promises.push(AddonModAssign.invalidateAllSubmissionData(this.assign.id));
if (this.canViewAllSubmissions) {
promises.push(AddonModAssign.invalidateSubmissionStatusData(this.assign.id, undefined, this.group));
}
}
await Promise.all(promises).finally(() => {
this.submissionComponent?.invalidateAndRefresh(true);
});
}
/**
* User entered the page that contains the component.
*/
ionViewDidEnter(): void {
super.ionViewDidEnter();
this.submissionComponent?.ionViewDidEnter();
}
/**
* User left the page that contains the component.
*/
ionViewDidLeave(): void {
super.ionViewDidLeave();
this.submissionComponent?.ionViewDidLeave();
}
/**
* Compares sync event data with current data to check if refresh content is needed.
*
* @param syncEventData Data receiven on sync observer.
* @return True if refresh is needed, false otherwise.
*/
protected isRefreshSyncNeeded(syncEventData: AddonModAssignAutoSyncData): boolean {
if (this.assign && syncEventData.assignId == this.assign.id) {
if (syncEventData.warnings && syncEventData.warnings.length) {
// Show warnings.
CoreDomUtils.showErrorModal(syncEventData.warnings[0]);
}
return true;
}
return false;
}
/**
* Performs the sync of the activity.
*
* @return Promise resolved when done.
*/
protected async sync(): Promise<void> {
await AddonModAssignSync.syncAssign(this.assign!.id);
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
super.ngOnDestroy();
this.savedObserver?.off();
this.submittedObserver?.off();
this.gradedObserver?.off();
}
}