532 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 { CoreCourseHelper } from '@features/course/services/course-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';
/**
* 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.
* @return 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.
* @return 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 and attachments.
let files: CoreWSFile[] = assign.introattachments || [];
files = files.concat(this.getIntroFilesFromInstance(module, assign));
// 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((submission) =>
this.getSubmissionFiles(assign, submission.submitid!, !!submission.blindid, siteId).then((submissionFiles) => {
files = files.concat(submissionFiles);
return;
}).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, 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 siteId Site ID. If not defined, current site.
* @return Promise resolved with array of files.
*/
protected async getSubmissionFiles(
assign: AddonModAssignAssign,
submitId: number,
blindMarking: boolean,
siteId?: string,
): Promise<CoreWSFile[]> {
const submissionStatus = await AddonModAssign.getSubmissionStatusWithRetry(assign, {
userId: submitId,
isBlind: blindMarking,
siteId,
});
const userSubmission = AddonModAssign.getSubmissionObjectFromAttempt(assign, submissionStatus.lastattempt);
if (!submissionStatus.lastattempt || !userSubmission) {
return [];
}
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 [].concat.apply([], filesLists);
}
/**
* Invalidate the prefetched content.
*
* @param moduleId The module ID.
* @param courseId The course ID the module belongs to.
* @return 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.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when invalidated.
*/
async invalidateModule(module: CoreCourseAnyModuleData): Promise<void> {
return CoreCourse.invalidateModule(module.id);
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
*/
async isEnabled(): Promise<boolean> {
return AddonModAssign.isPluginEnabled();
}
/**
* Prefetch a module.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when done.
*/
prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
return this.prefetchPackage(module, courseId, this.prefetchAssign.bind(this, module, courseId));
}
/**
* Prefetch an assignment.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when done.
*/
protected async prefetchAssign(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
const userId = CoreSites.getCurrentSiteUserId();
courseId = courseId || module.course || CoreSites.getCurrentSiteHomeId();
const siteId = CoreSites.getCurrentSiteId();
const options: CoreSitesCommonWSOptions = {
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
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(CoreCourseHelper.getModuleCourseIdByInstance(assign.id, 'assign', 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.
* @return 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.OnlyNetwork,
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.OnlyNetwork,
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.OnlyNetwork,
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: '' }];
}
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.OnlyNetwork,
siteId,
};
return this.prefetchSubmission(assign, courseId, moduleId, submissionOptions, true);
});
if (!assign.markingworkflow) {
// Get assignment grades only if workflow is not enabled to check grading date.
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.OnlyNetwork,
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 });
// Fail silently (Moodle < 3.2).
await CoreUtils.ignoreErrors(
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.
* @return 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);
}
}
}
// 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.
* @return Promise resolved when done.
*/
sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
return AddonModAssignSync.syncAssign(module.instance!, siteId);
}
}
export const AddonModAssignPrefetchHandler = makeSingleton(AddonModAssignPrefetchHandlerService);