2021-02-18 15:34:13 +01:00
|
|
|
// (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.
|
|
|
|
|
2021-02-22 15:58:52 +01:00
|
|
|
import { Component, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
2021-03-17 13:20:15 +01:00
|
|
|
import { Params } from '@angular/router';
|
2021-02-22 15:58:52 +01:00
|
|
|
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
|
|
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
2021-02-18 15:34:13 +01:00
|
|
|
import { IonRefresher } 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 { CoreUtils } from '@services/utils/utils';
|
|
|
|
import { Translate } from '@singletons';
|
|
|
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
|
|
|
import {
|
|
|
|
AddonModAssignAssign,
|
|
|
|
AddonModAssignSubmission,
|
|
|
|
AddonModAssignProvider,
|
|
|
|
AddonModAssign,
|
2021-06-30 08:12:19 +02:00
|
|
|
AddonModAssignGrade,
|
2021-02-18 15:34:13 +01:00
|
|
|
} from '../../services/assign';
|
|
|
|
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../../services/assign-helper';
|
|
|
|
import { AddonModAssignOffline } from '../../services/assign-offline';
|
|
|
|
import {
|
|
|
|
AddonModAssignSyncProvider,
|
|
|
|
AddonModAssignSync,
|
|
|
|
AddonModAssignManualSyncData,
|
|
|
|
AddonModAssignAutoSyncData,
|
|
|
|
} from '../../services/assign-sync';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Page that displays a list of submissions of an assignment.
|
|
|
|
*/
|
|
|
|
@Component({
|
|
|
|
selector: 'page-addon-mod-assign-submission-list',
|
|
|
|
templateUrl: 'submission-list.html',
|
|
|
|
})
|
2021-02-22 15:58:52 +01:00
|
|
|
export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestroy {
|
2021-02-18 15:34:13 +01:00
|
|
|
|
2021-02-22 15:58:52 +01:00
|
|
|
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
2021-02-18 15:34:13 +01:00
|
|
|
|
|
|
|
title = ''; // Title to display.
|
|
|
|
assign?: AddonModAssignAssign; // Assignment.
|
2021-02-22 15:58:52 +01:00
|
|
|
submissions: AddonModAssignSubmissionListManager; // List of submissions
|
2021-02-18 15:34:13 +01:00
|
|
|
loaded = false; // Whether data has been loaded.
|
|
|
|
groupId = 0; // Group ID to show.
|
|
|
|
courseId!: number; // Course ID the assignment belongs to.
|
|
|
|
moduleId!: number; // Module ID the submission belongs to.
|
|
|
|
|
|
|
|
groupInfo: CoreGroupInfo = {
|
|
|
|
groups: [],
|
|
|
|
separateGroups: false,
|
|
|
|
visibleGroups: false,
|
|
|
|
defaultGroupId: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
protected selectedStatus?: string; // The status to see.
|
|
|
|
protected gradedObserver: CoreEventObserver; // Observer to refresh data when a grade changes.
|
|
|
|
protected syncObserver: CoreEventObserver; // Observer to refresh data when the async is synchronized.
|
|
|
|
protected submissionsData: { canviewsubmissions: boolean; submissions?: AddonModAssignSubmission[] } = {
|
|
|
|
canviewsubmissions: false,
|
|
|
|
};
|
|
|
|
|
2021-03-11 15:55:14 +01:00
|
|
|
constructor() {
|
2021-02-22 15:58:52 +01:00
|
|
|
this.submissions = new AddonModAssignSubmissionListManager(AddonModAssignSubmissionListPage);
|
|
|
|
|
2021-02-18 15:34:13 +01:00
|
|
|
// Update data if some grade changes.
|
2021-03-10 12:30:18 +01:00
|
|
|
this.gradedObserver = CoreEvents.on(
|
2021-02-18 15:34:13 +01:00
|
|
|
AddonModAssignProvider.GRADED_EVENT,
|
|
|
|
(data) => {
|
|
|
|
if (
|
|
|
|
this.loaded &&
|
|
|
|
this.assign &&
|
|
|
|
data.assignmentId == this.assign.id &&
|
2021-03-02 11:41:04 +01:00
|
|
|
data.userId == CoreSites.getCurrentSiteUserId()
|
2021-02-18 15:34:13 +01:00
|
|
|
) {
|
|
|
|
// Grade changed, refresh the data.
|
|
|
|
this.loaded = false;
|
|
|
|
|
|
|
|
this.refreshAllData(true).finally(() => {
|
|
|
|
this.loaded = true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreSites.getCurrentSiteId(),
|
2021-02-18 15:34:13 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
// Refresh data if this assign is synchronized.
|
|
|
|
const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED];
|
|
|
|
this.syncObserver = CoreEvents.onMultiple<AddonModAssignAutoSyncData | AddonModAssignManualSyncData>(
|
|
|
|
events,
|
|
|
|
(data) => {
|
|
|
|
if (!this.loaded || ('context' in data && data.context == 'submission-list')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.loaded = false;
|
|
|
|
|
|
|
|
this.refreshAllData(false).finally(() => {
|
|
|
|
this.loaded = true;
|
|
|
|
});
|
|
|
|
},
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreSites.getCurrentSiteId(),
|
2021-02-18 15:34:13 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component being initialized.
|
|
|
|
*/
|
2021-02-22 15:58:52 +01:00
|
|
|
ngAfterViewInit(): void {
|
2021-09-09 16:38:38 +02:00
|
|
|
try {
|
|
|
|
this.moduleId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
|
|
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
|
|
|
this.groupId = CoreNavigator.getRouteNumberParam('groupId') || 0;
|
|
|
|
this.selectedStatus = CoreNavigator.getRouteParam('status');
|
|
|
|
} catch (error) {
|
|
|
|
CoreDomUtils.showErrorModal(error);
|
|
|
|
|
|
|
|
CoreNavigator.back();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
2021-02-18 15:34:13 +01:00
|
|
|
|
2021-02-22 15:58:52 +01:00
|
|
|
if (this.selectedStatus) {
|
|
|
|
if (this.selectedStatus == AddonModAssignProvider.NEED_GRADING) {
|
2021-03-02 11:41:04 +01:00
|
|
|
this.title = Translate.instant('addon.mod_assign.numberofsubmissionsneedgrading');
|
2021-02-18 15:34:13 +01:00
|
|
|
} else {
|
2021-03-02 11:41:04 +01:00
|
|
|
this.title = Translate.instant('addon.mod_assign.submissionstatus_' + this.selectedStatus);
|
2021-02-18 15:34:13 +01:00
|
|
|
}
|
2021-02-22 15:58:52 +01:00
|
|
|
} else {
|
2021-03-02 11:41:04 +01:00
|
|
|
this.title = Translate.instant('addon.mod_assign.numberofparticipants');
|
2021-02-22 15:58:52 +01:00
|
|
|
}
|
|
|
|
this.fetchAssignment(true).finally(() => {
|
|
|
|
this.loaded = true;
|
|
|
|
this.submissions.start(this.splitView);
|
2021-02-18 15:34:13 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch assignment data.
|
|
|
|
*
|
|
|
|
* @param sync Whether to try to synchronize data.
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
2021-02-19 12:41:30 +01:00
|
|
|
protected async fetchAssignment(sync = false): Promise<void> {
|
2021-02-18 15:34:13 +01:00
|
|
|
try {
|
|
|
|
// Get assignment data.
|
2021-03-02 11:41:04 +01:00
|
|
|
this.assign = await AddonModAssign.getAssignment(this.courseId, this.moduleId);
|
2021-02-18 15:34:13 +01:00
|
|
|
|
|
|
|
this.title = this.assign.name || this.title;
|
|
|
|
|
|
|
|
if (sync) {
|
|
|
|
try {
|
|
|
|
// Try to synchronize data.
|
2021-03-02 11:41:04 +01:00
|
|
|
const result = await AddonModAssignSync.syncAssign(this.assign.id);
|
2021-02-18 15:34:13 +01:00
|
|
|
|
|
|
|
if (result && result.updated) {
|
2021-03-10 12:30:18 +01:00
|
|
|
CoreEvents.trigger(
|
2021-02-18 15:34:13 +01:00
|
|
|
AddonModAssignSyncProvider.MANUAL_SYNCED,
|
|
|
|
{
|
|
|
|
assignId: this.assign.id,
|
|
|
|
warnings: result.warnings,
|
|
|
|
gradesBlocked: result.gradesBlocked,
|
|
|
|
context: 'submission-list',
|
|
|
|
},
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreSites.getCurrentSiteId(),
|
2021-02-18 15:34:13 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
// Ignore errors, probably user is offline or sync is blocked.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get assignment submissions.
|
2021-03-02 11:41:04 +01:00
|
|
|
this.submissionsData = await AddonModAssign.getSubmissions(this.assign.id, { cmId: this.assign.cmid });
|
2021-02-18 15:34:13 +01:00
|
|
|
|
|
|
|
if (!this.submissionsData.canviewsubmissions) {
|
|
|
|
// User shouldn't be able to reach here.
|
|
|
|
throw new Error('Cannot view submissions.');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if groupmode is enabled to avoid showing wrong numbers.
|
2021-03-02 11:41:04 +01:00
|
|
|
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.assign.cmid, false);
|
2021-02-18 15:34:13 +01:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
await this.setGroup(CoreGroups.validateGroupId(this.groupId, this.groupInfo));
|
2021-02-18 15:34:13 +01:00
|
|
|
} catch (error) {
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreDomUtils.showErrorModalDefault(error, 'Error getting assigment data.');
|
2021-02-18 15:34:13 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set group to see the summary.
|
|
|
|
*
|
|
|
|
* @param groupId Group ID.
|
|
|
|
* @return Resolved when done.
|
|
|
|
*/
|
|
|
|
async setGroup(groupId: number): Promise<void> {
|
|
|
|
this.groupId = groupId;
|
|
|
|
|
|
|
|
// Fetch submissions and grades.
|
|
|
|
const submissions =
|
2021-03-02 11:41:04 +01:00
|
|
|
await AddonModAssignHelper.getSubmissionsUserData(
|
2021-02-18 15:34:13 +01:00
|
|
|
this.assign!,
|
|
|
|
this.submissionsData.submissions,
|
|
|
|
this.groupId,
|
|
|
|
);
|
|
|
|
// Get assignment grades only if workflow is not enabled to check grading date.
|
|
|
|
const grades = !this.assign!.markingworkflow
|
2021-03-02 11:41:04 +01:00
|
|
|
? await AddonModAssign.getAssignmentGrades(this.assign!.id, { cmId: this.assign!.cmid })
|
2021-02-18 15:34:13 +01:00
|
|
|
: [];
|
|
|
|
|
|
|
|
// Filter the submissions to get only the ones with the right status and add some extra data.
|
|
|
|
const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING;
|
|
|
|
const searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus;
|
|
|
|
|
|
|
|
const promises: Promise<void>[] = [];
|
|
|
|
const showSubmissions: AddonModAssignSubmissionForList[] = [];
|
|
|
|
|
|
|
|
submissions.forEach((submission: AddonModAssignSubmissionForList) => {
|
|
|
|
if (!searchStatus || searchStatus == submission.status) {
|
|
|
|
promises.push(
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreUtils.ignoreErrors(
|
|
|
|
AddonModAssignOffline.getSubmissionGrade(this.assign!.id, submission.userid),
|
2021-02-18 15:34:13 +01:00
|
|
|
).then(async (data) => {
|
|
|
|
if (getNeedGrading) {
|
|
|
|
// Only show the submissions that need to be graded.
|
2021-03-02 11:41:04 +01:00
|
|
|
const add = await AddonModAssign.needsSubmissionToBeGraded(submission, this.assign!.id);
|
2021-02-18 15:34:13 +01:00
|
|
|
|
|
|
|
if (!add) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load offline grades.
|
|
|
|
const notSynced = !!data && submission.timemodified < data.timemodified;
|
|
|
|
|
|
|
|
if (submission.gradingstatus == 'graded' && !this.assign!.markingworkflow) {
|
|
|
|
// Get the last grade of the submission.
|
|
|
|
const grade = grades
|
|
|
|
.filter((grade) => grade.userid == submission.userid)
|
2021-06-30 08:12:19 +02:00
|
|
|
.reduce(
|
|
|
|
(a, b) => (a && a.timemodified > b.timemodified ? a : b),
|
|
|
|
<AddonModAssignGrade | undefined> undefined,
|
|
|
|
);
|
2021-02-18 15:34:13 +01:00
|
|
|
|
|
|
|
if (grade && grade.timemodified < submission.timemodified) {
|
|
|
|
submission.gradingstatus = AddonModAssignProvider.GRADED_FOLLOWUP_SUBMIT;
|
|
|
|
}
|
|
|
|
}
|
2021-03-02 11:41:04 +01:00
|
|
|
submission.statusColor = AddonModAssign.getSubmissionStatusColor(submission.status);
|
|
|
|
submission.gradingColor = AddonModAssign.getSubmissionGradingStatusColor(
|
2021-02-18 15:34:13 +01:00
|
|
|
submission.gradingstatus,
|
|
|
|
);
|
|
|
|
|
|
|
|
// Show submission status if not submitted for grading.
|
|
|
|
if (submission.statusColor != 'success' || !submission.gradingstatus) {
|
2021-03-02 11:41:04 +01:00
|
|
|
submission.statusTranslated = Translate.instant(
|
2021-02-18 15:34:13 +01:00
|
|
|
'addon.mod_assign.submissionstatus_' + submission.status,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
submission.statusTranslated = '';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (notSynced) {
|
|
|
|
submission.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
|
|
|
|
submission.gradingColor = '';
|
|
|
|
} else if (submission.statusColor != 'danger' || submission.gradingColor != 'danger') {
|
|
|
|
// Show grading status if one of the statuses is not done.
|
2021-03-02 11:41:04 +01:00
|
|
|
submission.gradingStatusTranslationId = AddonModAssign.getSubmissionGradingStatusTranslationId(
|
2021-02-18 15:34:13 +01:00
|
|
|
submission.gradingstatus,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
submission.gradingStatusTranslationId = '';
|
|
|
|
}
|
|
|
|
|
|
|
|
showSubmissions.push(submission);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
2021-02-22 15:58:52 +01:00
|
|
|
this.submissions.setItems(showSubmissions);
|
2021-02-18 15:34:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Refresh all the data.
|
|
|
|
*
|
|
|
|
* @param sync Whether to try to synchronize data.
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
|
|
|
protected async refreshAllData(sync?: boolean): Promise<void> {
|
|
|
|
const promises: Promise<void>[] = [];
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
promises.push(AddonModAssign.invalidateAssignmentData(this.courseId));
|
2021-02-18 15:34:13 +01:00
|
|
|
if (this.assign) {
|
2021-03-02 11:41:04 +01:00
|
|
|
promises.push(AddonModAssign.invalidateAllSubmissionData(this.assign.id));
|
|
|
|
promises.push(AddonModAssign.invalidateAssignmentUserMappingsData(this.assign.id));
|
|
|
|
promises.push(AddonModAssign.invalidateAssignmentGradesData(this.assign.id));
|
|
|
|
promises.push(AddonModAssign.invalidateListParticipantsData(this.assign.id));
|
2021-02-18 15:34:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await Promise.all(promises);
|
|
|
|
} finally {
|
|
|
|
this.fetchAssignment(sync);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Refresh the list.
|
|
|
|
*
|
|
|
|
* @param refresher Refresher.
|
|
|
|
*/
|
2021-03-12 12:22:55 +01:00
|
|
|
refreshList(refresher?: IonRefresher): void {
|
2021-02-18 15:34:13 +01:00
|
|
|
this.refreshAllData(true).finally(() => {
|
2021-03-12 12:22:55 +01:00
|
|
|
refresher?.complete();
|
2021-02-18 15:34:13 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component being destroyed.
|
|
|
|
*/
|
|
|
|
ngOnDestroy(): void {
|
|
|
|
this.gradedObserver?.off();
|
|
|
|
this.syncObserver?.off();
|
2021-02-22 15:58:52 +01:00
|
|
|
this.submissions.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper class to manage submissions.
|
|
|
|
*/
|
|
|
|
class AddonModAssignSubmissionListManager extends CorePageItemsListManager<AddonModAssignSubmissionForList> {
|
|
|
|
|
|
|
|
constructor(pageComponent: unknown) {
|
|
|
|
super(pageComponent);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
protected getItemPath(submission: AddonModAssignSubmissionForList): string {
|
|
|
|
return String(submission.submitid);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
protected getItemQueryParams(submission: AddonModAssignSubmissionForList): Params {
|
2021-02-25 14:44:06 +01:00
|
|
|
return {
|
2021-02-22 15:58:52 +01:00
|
|
|
blindId: submission.blindid,
|
2021-02-25 14:44:06 +01:00
|
|
|
};
|
2021-02-22 15:58:52 +01:00
|
|
|
}
|
|
|
|
|
2021-02-18 15:34:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculated data for an assign submission.
|
|
|
|
*/
|
|
|
|
type AddonModAssignSubmissionForList = AddonModAssignSubmissionFormatted & {
|
|
|
|
statusColor?: string; // Calculated in the app. Color of the submission status.
|
|
|
|
gradingColor?: string; // Calculated in the app. Color of the submission grading status.
|
|
|
|
statusTranslated?: string; // Calculated in the app. Translated text of the submission status.
|
|
|
|
gradingStatusTranslationId?: string; // Calculated in the app. Key of the text of the submission grading status.
|
|
|
|
};
|