535 lines
21 KiB
TypeScript
535 lines
21 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 { Injectable } from '@angular/core';
|
|
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
|
|
import { makeSingleton } from '@singletons';
|
|
import {
|
|
AddonModAssign,
|
|
AddonModAssignAssign,
|
|
AddonModAssignProvider,
|
|
AddonModAssignSubmission,
|
|
AddonModAssignSubmissionStatusOptions,
|
|
} from '../assign';
|
|
import { AddonModAssignSubmissionDelegate } from '../submission-delegate';
|
|
import { AddonModAssignFeedbackDelegate } from '../feedback-delegate';
|
|
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
|
|
import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
|
import { CoreWSFile } from '@services/ws';
|
|
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../assign-helper';
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
import { CoreFilepool } from '@services/filepool';
|
|
import { CoreGroups } from '@services/groups';
|
|
import { AddonModAssignSync, AddonModAssignSyncResult } from '../assign-sync';
|
|
import { CoreUser } from '@features/user/services/user';
|
|
import { CoreGradesHelper } from '@features/grades/services/grades-helper';
|
|
import { CoreCourses } from '@features/courses/services/courses';
|
|
import { multilangString } from '@services/lang';
|
|
|
|
/**
|
|
* Handler to prefetch assigns.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
|
|
|
|
name = 'AddonModAssign';
|
|
modName = 'assign';
|
|
component = AddonModAssignProvider.COMPONENT;
|
|
updatesNames = /^configuration$|^.*files$|^submissions$|^grades$|^gradeitems$|^outcomes$|^comments$/;
|
|
|
|
/**
|
|
* Check if a certain module can use core_course_check_updates to check if it has updates.
|
|
* If not defined, it will assume all modules can be checked.
|
|
* The modules that return false will always be shown as outdated when they're downloaded.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @returns Whether the module can use check_updates. The promise should never be rejected.
|
|
*/
|
|
async canUseCheckUpdates(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
|
|
// Teachers cannot use the WS because it doesn't check student submissions.
|
|
try {
|
|
const assign = await AddonModAssign.getAssignment(courseId, module.id);
|
|
|
|
const data = await AddonModAssign.getSubmissions(assign.id, { cmId: module.id });
|
|
if (data.canviewsubmissions) {
|
|
return false;
|
|
}
|
|
|
|
// Check if the user can view their own submission.
|
|
await AddonModAssign.getSubmissionStatus(assign.id, { cmId: module.id });
|
|
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get list of files. If not defined, we'll assume they're in module.contents.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @returns Promise resolved with the list of files.
|
|
*/
|
|
async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSFile[]> {
|
|
const siteId = CoreSites.getCurrentSiteId();
|
|
|
|
try {
|
|
const assign = await AddonModAssign.getAssignment(courseId, module.id, { siteId });
|
|
// Get intro files, attachments and activity files.
|
|
let files: CoreWSFile[] = assign.introattachments || [];
|
|
files = files.concat(this.getIntroFilesFromInstance(module, assign));
|
|
files = files.concat(assign.activityattachments || []);
|
|
|
|
// Now get the files in the submissions.
|
|
const submissionData = await AddonModAssign.getSubmissions(assign.id, { cmId: module.id, siteId });
|
|
|
|
if (submissionData.canviewsubmissions) {
|
|
// Teacher, get all submissions.
|
|
const submissions =
|
|
await AddonModAssignHelper.getSubmissionsUserData(assign, submissionData.submissions, 0, { siteId });
|
|
|
|
// Get all the files in the submissions.
|
|
const promises = submissions.map(async (submission) => {
|
|
try {
|
|
const submissionFiles = await this.getSubmissionFiles(
|
|
assign,
|
|
submission.submitid!,
|
|
!!submission.blindid,
|
|
true,
|
|
siteId,
|
|
);
|
|
|
|
files = files.concat(submissionFiles);
|
|
} catch (error) {
|
|
if (error && error.errorcode == 'nopermission') {
|
|
// The user does not have persmission to view this submission, ignore it.
|
|
return;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
} else {
|
|
// Student, get only his/her submissions.
|
|
const userId = CoreSites.getCurrentSiteUserId();
|
|
const blindMarking = !!assign.blindmarking && !assign.revealidentities;
|
|
|
|
const submissionFiles = await this.getSubmissionFiles(assign, userId, blindMarking, false, siteId);
|
|
files = files.concat(submissionFiles);
|
|
}
|
|
|
|
return files;
|
|
} catch {
|
|
// Error getting data, return empty list.
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get submission files.
|
|
*
|
|
* @param assign Assign.
|
|
* @param submitId User ID of the submission to get.
|
|
* @param blindMarking True if blind marking, false otherwise.
|
|
* @param canViewAllSubmissions Whether the user can view all submissions.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved with array of files.
|
|
*/
|
|
protected async getSubmissionFiles(
|
|
assign: AddonModAssignAssign,
|
|
submitId: number,
|
|
blindMarking: boolean,
|
|
canViewAllSubmissions: boolean,
|
|
siteId?: string,
|
|
): Promise<CoreWSFile[]> {
|
|
|
|
const submissionStatus = await AddonModAssign.getSubmissionStatusWithRetry(assign, {
|
|
userId: submitId,
|
|
isBlind: blindMarking,
|
|
siteId,
|
|
});
|
|
const userSubmission = AddonModAssign.getSubmissionObjectFromAttempt(assign, submissionStatus.lastattempt);
|
|
|
|
// Get intro and activity files from the submission status if it's a student.
|
|
// It's ok if they were already obtained from the assignment instance, they won't be downloaded twice.
|
|
const files = canViewAllSubmissions ?
|
|
[] :
|
|
(submissionStatus.assignmentdata?.attachments?.intro || [])
|
|
.concat(submissionStatus.assignmentdata?.attachments?.activity || []);
|
|
|
|
if (!submissionStatus.lastattempt || !userSubmission) {
|
|
return files;
|
|
}
|
|
|
|
const promises: Promise<CoreWSFile[]>[] = [];
|
|
|
|
if (userSubmission.plugins) {
|
|
// Add submission plugin files.
|
|
userSubmission.plugins.forEach((plugin) => {
|
|
promises.push(AddonModAssignSubmissionDelegate.getPluginFiles(assign, userSubmission, plugin, siteId));
|
|
});
|
|
}
|
|
|
|
if (submissionStatus.feedback && submissionStatus.feedback.plugins) {
|
|
// Add feedback plugin files.
|
|
submissionStatus.feedback.plugins.forEach((plugin) => {
|
|
promises.push(AddonModAssignFeedbackDelegate.getPluginFiles(assign, userSubmission, plugin, siteId));
|
|
});
|
|
}
|
|
|
|
const filesLists = await Promise.all(promises);
|
|
|
|
return files.concat.apply(files, filesLists);
|
|
}
|
|
|
|
/**
|
|
* Invalidate the prefetched content.
|
|
*
|
|
* @param moduleId The module ID.
|
|
* @param courseId The course ID the module belongs to.
|
|
* @returns Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
|
await AddonModAssign.invalidateContent(moduleId, courseId);
|
|
}
|
|
|
|
/**
|
|
* Invalidate WS calls needed to determine module status.
|
|
*
|
|
* @param module Module.
|
|
* @returns Promise resolved when invalidated.
|
|
*/
|
|
async invalidateModule(module: CoreCourseAnyModuleData): Promise<void> {
|
|
return CoreCourse.invalidateModule(module.id);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
prefetch(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
|
|
return this.prefetchPackage(module, courseId, (siteId) => this.prefetchAssign(module, courseId, siteId));
|
|
}
|
|
|
|
/**
|
|
* Prefetch an assignment.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param siteId Site ID.
|
|
* @returns Promise resolved when done.
|
|
*/
|
|
protected async prefetchAssign(module: CoreCourseAnyModuleData, courseId: number, siteId: string): Promise<void> {
|
|
const userId = CoreSites.getCurrentSiteUserId();
|
|
|
|
const options: CoreSitesCommonWSOptions = {
|
|
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
|
|
siteId,
|
|
};
|
|
|
|
const modOptions: CoreCourseCommonModWSOptions = {
|
|
cmId: module.id,
|
|
...options,
|
|
};
|
|
|
|
// Get assignment to retrieve all its submissions.
|
|
const assign = await AddonModAssign.getAssignment(courseId, module.id, options);
|
|
const promises: Promise<unknown>[] = [];
|
|
const blindMarking = assign.blindmarking && !assign.revealidentities;
|
|
|
|
if (blindMarking) {
|
|
promises.push(
|
|
CoreUtils.ignoreErrors(AddonModAssign.getAssignmentUserMappings(assign.id, -1, modOptions)),
|
|
);
|
|
}
|
|
|
|
promises.push(this.prefetchSubmissions(assign, courseId, module.id, userId, siteId));
|
|
|
|
promises.push(CoreCourse.getModuleBasicInfoByInstance(assign.id, 'assign', { siteId }));
|
|
// Get course data, needed to determine upload max size if it's configured to be course limit.
|
|
promises.push(CoreUtils.ignoreErrors(CoreCourses.getCourseByField('id', courseId, siteId)));
|
|
|
|
// Download intro files and attachments. Do not call getFiles because it'd call some WS twice.
|
|
let files: CoreWSFile[] = assign.introattachments || [];
|
|
files = files.concat(this.getIntroFilesFromInstance(module, assign));
|
|
|
|
promises.push(CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id));
|
|
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Prefetch assign submissions.
|
|
*
|
|
* @param assign Assign.
|
|
* @param courseId Course ID.
|
|
* @param moduleId Module ID.
|
|
* @param userId User ID. If not defined, site's current user.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved when prefetched, rejected otherwise.
|
|
*/
|
|
protected async prefetchSubmissions(
|
|
assign: AddonModAssignAssign,
|
|
courseId: number,
|
|
moduleId: number,
|
|
userId: number,
|
|
siteId: string,
|
|
): Promise<void> {
|
|
const modOptions: CoreCourseCommonModWSOptions = {
|
|
cmId: moduleId,
|
|
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
|
|
siteId,
|
|
};
|
|
|
|
// Get submissions.
|
|
const submissions = await AddonModAssign.getSubmissions(assign.id, modOptions);
|
|
const promises: Promise<unknown>[] = [];
|
|
|
|
promises.push(this.prefetchParticipantSubmissions(
|
|
assign,
|
|
submissions.canviewsubmissions,
|
|
submissions.submissions,
|
|
moduleId,
|
|
courseId,
|
|
userId,
|
|
siteId,
|
|
));
|
|
|
|
// Prefetch own submission, we need to do this for teachers too so the response with error is cached.
|
|
promises.push(
|
|
this.prefetchSubmission(
|
|
assign,
|
|
courseId,
|
|
moduleId,
|
|
{
|
|
userId,
|
|
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
|
|
siteId,
|
|
},
|
|
true,
|
|
),
|
|
);
|
|
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
protected async prefetchParticipantSubmissions(
|
|
assign: AddonModAssignAssign,
|
|
canviewsubmissions: boolean,
|
|
submissions: AddonModAssignSubmission[] = [],
|
|
moduleId: number,
|
|
courseId: number,
|
|
userId: number,
|
|
siteId: string,
|
|
): Promise<void> {
|
|
|
|
const options: CoreSitesCommonWSOptions = {
|
|
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
|
|
siteId,
|
|
};
|
|
|
|
const modOptions: CoreCourseCommonModWSOptions = {
|
|
cmId: moduleId,
|
|
...options,
|
|
};
|
|
|
|
// Always prefetch groupInfo.
|
|
const groupInfo = await CoreGroups.getActivityGroupInfo(assign.cmid, false, undefined, siteId);
|
|
if (!canviewsubmissions) {
|
|
|
|
return;
|
|
}
|
|
|
|
// Teacher, prefetch all submissions.
|
|
if (!groupInfo.groups || groupInfo.groups.length == 0) {
|
|
groupInfo.groups = [{ id: 0, name: multilangString() }];
|
|
}
|
|
|
|
const promises = groupInfo.groups.map((group) =>
|
|
AddonModAssignHelper.getSubmissionsUserData(assign, submissions, group.id, options)
|
|
.then((submissions: AddonModAssignSubmissionFormatted[]) => {
|
|
|
|
const subPromises: Promise<unknown>[] = submissions.map((submission) => {
|
|
const submissionOptions = {
|
|
userId: submission.submitid,
|
|
groupId: group.id,
|
|
isBlind: !!submission.blindid,
|
|
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
|
|
siteId,
|
|
};
|
|
|
|
return this.prefetchSubmission(assign, courseId, moduleId, submissionOptions, true);
|
|
});
|
|
|
|
subPromises.push(AddonModAssign.getAssignmentGrades(assign.id, modOptions));
|
|
|
|
// Prefetch the submission of the current user even if it does not exist, this will be create it.
|
|
if (!submissions || !submissions.find((subm: AddonModAssignSubmissionFormatted) => subm.submitid == userId)) {
|
|
const submissionOptions = {
|
|
userId,
|
|
groupId: group.id,
|
|
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
|
|
siteId,
|
|
};
|
|
|
|
subPromises.push(this.prefetchSubmission(assign, courseId, moduleId, submissionOptions));
|
|
}
|
|
|
|
return Promise.all(subPromises);
|
|
}).then(async () => {
|
|
// Participiants already fetched, we don't need to ignore cache now.
|
|
const participants = await AddonModAssignHelper.getParticipants(assign, group.id, { siteId });
|
|
|
|
await CoreUser.prefetchUserAvatars(participants, 'profileimageurl', siteId);
|
|
|
|
return;
|
|
}));
|
|
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Prefetch a submission.
|
|
*
|
|
* @param assign Assign.
|
|
* @param courseId Course ID.
|
|
* @param moduleId Module ID.
|
|
* @param options Other options, see getSubmissionStatusWithRetry.
|
|
* @param resolveOnNoPermission If true, will avoid throwing if a nopermission error is raised.
|
|
* @returns Promise resolved when prefetched, rejected otherwise.
|
|
*/
|
|
protected async prefetchSubmission(
|
|
assign: AddonModAssignAssign,
|
|
courseId: number,
|
|
moduleId: number,
|
|
options: AddonModAssignSubmissionStatusOptions = {},
|
|
resolveOnNoPermission = false,
|
|
): Promise<void> {
|
|
const submission = await AddonModAssign.getSubmissionStatusWithRetry(assign, options);
|
|
const siteId = options.siteId!;
|
|
const userId = options.userId;
|
|
|
|
try {
|
|
const promises: Promise<unknown>[] = [];
|
|
const blindMarking = !!assign.blindmarking && !assign.revealidentities;
|
|
let userIds: number[] = [];
|
|
const userSubmission = AddonModAssign.getSubmissionObjectFromAttempt(assign, submission.lastattempt);
|
|
|
|
if (submission.lastattempt) {
|
|
// Get IDs of the members who need to submit.
|
|
if (!blindMarking && submission.lastattempt.submissiongroupmemberswhoneedtosubmit) {
|
|
userIds = userIds.concat(submission.lastattempt.submissiongroupmemberswhoneedtosubmit);
|
|
}
|
|
|
|
if (userSubmission && userSubmission.id) {
|
|
// Prefetch submission plugins data.
|
|
if (userSubmission.plugins) {
|
|
userSubmission.plugins.forEach((plugin) => {
|
|
// Prefetch the plugin WS data.
|
|
promises.push(
|
|
AddonModAssignSubmissionDelegate.prefetch(assign, userSubmission, plugin, siteId),
|
|
);
|
|
|
|
// Prefetch the plugin files.
|
|
promises.push(
|
|
AddonModAssignSubmissionDelegate.getPluginFiles(assign, userSubmission, plugin, siteId)
|
|
.then((files) =>
|
|
CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id))
|
|
.catch(() => {
|
|
// Ignore errors.
|
|
}),
|
|
);
|
|
});
|
|
}
|
|
|
|
// Get ID of the user who did the submission.
|
|
if (userSubmission.userid) {
|
|
userIds.push(userSubmission.userid);
|
|
}
|
|
}
|
|
|
|
if (assign.teamsubmission && submission.lastattempt.submissiongroup) {
|
|
// Prefetch group info.
|
|
promises.push(CoreGroups.getActivityAllowedGroups(assign.cmid));
|
|
}
|
|
}
|
|
|
|
// Prefetch grade items.
|
|
if (userId) {
|
|
promises.push(CoreCourse.getModuleBasicGradeInfo(moduleId, siteId).then((gradeInfo) => {
|
|
if (gradeInfo) {
|
|
promises.push(
|
|
CoreGradesHelper.getGradeModuleItems(courseId, moduleId, userId, undefined, siteId, true),
|
|
);
|
|
}
|
|
|
|
return;
|
|
}));
|
|
}
|
|
|
|
// Prefetch feedback.
|
|
if (submission.feedback) {
|
|
// Get profile and image of the grader.
|
|
if (submission.feedback.grade && submission.feedback.grade.grader > 0) {
|
|
userIds.push(submission.feedback.grade.grader);
|
|
}
|
|
|
|
// Prefetch feedback plugins data.
|
|
if (submission.feedback.plugins && userSubmission && userSubmission.id) {
|
|
submission.feedback.plugins.forEach((plugin) => {
|
|
// Prefetch the plugin WS data.
|
|
promises.push(AddonModAssignFeedbackDelegate.prefetch(assign, userSubmission, plugin, siteId));
|
|
|
|
// Prefetch the plugin files.
|
|
promises.push(
|
|
AddonModAssignFeedbackDelegate.getPluginFiles(assign, userSubmission, plugin, siteId)
|
|
.then((files) => CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id))
|
|
.catch(() => {
|
|
// Ignore errors.
|
|
}),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Prefetch user profiles.
|
|
promises.push(CoreUser.prefetchProfiles(userIds, courseId, siteId));
|
|
|
|
await Promise.all(promises);
|
|
} catch (error) {
|
|
// Ignore if the user can't view their own submission.
|
|
if (resolveOnNoPermission && error.errorcode != 'nopermission') {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync a module.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved when done.
|
|
*/
|
|
sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
|
|
return AddonModAssignSync.syncAssign(module.instance, siteId);
|
|
}
|
|
|
|
}
|
|
export const AddonModAssignPrefetchHandler = makeSingleton(AddonModAssignPrefetchHandlerService);
|