// (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 { // 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 { 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 { 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[] = []; 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 { 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 { 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 { 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 { 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 { 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[] = []; 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 { const modOptions: CoreCourseCommonModWSOptions = { cmId: moduleId, readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, siteId, }; // Get submissions. const submissions = await AddonModAssign.getSubmissions(assign.id, modOptions); const promises: Promise[] = []; 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 { 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[] = 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 { const submission = await AddonModAssign.getSubmissionStatusWithRetry(assign, options); const siteId = options.siteId!; const userId = options.userId; try { const promises: Promise[] = []; 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 { return AddonModAssignSync.syncAssign(module.instance!, siteId); } } export const AddonModAssignPrefetchHandler = makeSingleton(AddonModAssignPrefetchHandlerService);