From afc2fc1f643a142417a4c4334ad632efe55537c3 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 31 May 2018 13:45:31 +0200 Subject: [PATCH] MOBILE-2354 workshop: Prefech and sync cron handlers --- .../workshop/providers/prefetch-handler.ts | 368 ++++++++++++++++++ .../workshop/providers/sync-cron-handler.ts | 47 +++ src/addon/mod/workshop/workshop.module.ts | 14 +- 3 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 src/addon/mod/workshop/providers/prefetch-handler.ts create mode 100644 src/addon/mod/workshop/providers/sync-cron-handler.ts diff --git a/src/addon/mod/workshop/providers/prefetch-handler.ts b/src/addon/mod/workshop/providers/prefetch-handler.ts new file mode 100644 index 000000000..1c641fb57 --- /dev/null +++ b/src/addon/mod/workshop/providers/prefetch-handler.ts @@ -0,0 +1,368 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; +import { CoreGroupsProvider } from '@providers/groups'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { AddonModWorkshopProvider } from './workshop'; +import { AddonModWorkshopHelperProvider } from './helper'; + +/** + * Handler to prefetch workshops. + */ +@Injectable() +export class AddonModWorkshopPrefetchHandler extends CoreCourseModulePrefetchHandlerBase { + name = 'AddonModWorkshop'; + modName = 'workshop'; + component = AddonModWorkshopProvider.COMPONENT; + updatesNames = new RegExp('^configuration$|^.*files$|^completion|^gradeitems$|^outcomes$|^submissions$|^assessments$' + + '|^assessmentgrades$|^usersubmissions$|^userassessments$|^userassessmentgrades$|^userassessmentgrades$'); + + constructor(injector: Injector, + private groupsProvider: CoreGroupsProvider, + private userProvider: CoreUserProvider, + private workshopProvider: AddonModWorkshopProvider, + private workshopHelper: AddonModWorkshopHelperProvider) { + super(injector); + } + + /** + * Download the module. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when all content is downloaded. + */ + download(module: any, courseId: number, dirPath?: string): Promise { + // Workshop cannot be downloaded right away, only prefetched. + return this.prefetch(module, courseId, false, dirPath); + } + + /** + * Get list of files. If not defined, we'll assume they're in module.contents. + * + * @param {any} module Module. + * @param {Number} courseId Course ID the module belongs to. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @return {Promise} Promise resolved with the list of files. + */ + getFiles(module: any, courseId: number, single?: boolean): Promise { + return this.getWorkshopInfoHelper(module, courseId, true).then((info) => { + return info.files; + }); + } + + /** + * Helper function to get all workshop info just once. + * + * @param {any} module Module to get the files. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} [omitFail=false] True to always return even if fails. Default false. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the info fetched. + */ + protected getWorkshopInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false, + ignoreCache: boolean = false, siteId?: string): Promise { + let workshop, + groups = [], + files = [], + access; + + return this.sitesProvider.getSite(siteId).then((site) => { + const userId = site.getUserId(); + + return this.workshopProvider.getWorkshop(courseId, module.id, siteId, forceCache).then((data) => { + files = this.getIntroFilesFromInstance(module, data); + files = files.concat(data.instructauthorsfiles).concat(data.instructreviewersfiles); + workshop = data; + + return this.workshopProvider.getWorkshopAccessInformation(workshop.id, false, true, siteId).then((accessData) => { + access = accessData; + if (access.canviewallsubmissions) { + return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId).then((groupInfo) => { + if (!groupInfo.groups || groupInfo.groups.length == 0) { + groupInfo.groups = [{id: 0}]; + } + groups = groupInfo.groups; + }); + } + }); + }).then(() => { + return this.workshopProvider.getUserPlanPhases(workshop.id, false, true, siteId).then((phases) => { + // Get submission phase info. + const submissionPhase = phases[AddonModWorkshopProvider.PHASE_SUBMISSION], + canSubmit = this.workshopHelper.canSubmit(workshop, access, submissionPhase.tasks), + canAssess = this.workshopHelper.canAssess(workshop, access), + promises = []; + + if (canSubmit) { + promises.push(this.workshopHelper.getUserSubmission(workshop.id, userId).then((submission) => { + if (submission) { + files = files.concat(submission.contentfiles).concat(submission.attachmentfiles); + } + })); + } + + if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) { + promises.push(this.workshopProvider.getSubmissions(workshop.id).then((submissions) => { + const promises2 = []; + submissions.forEach((submission) => { + files = files.concat(submission.contentfiles).concat(submission.attachmentfiles); + promises2.push(this.workshopProvider.getSubmissionAssessments(workshop.id, submission.id) + .then((assessments) => { + assessments.forEach((assessment) => { + files = files.concat(assessment.feedbackattachmentfiles) + .concat(assessment.feedbackcontentfiles); + }); + })); + }); + + return Promise.all(promises2); + })); + } + + // Get assessment files. + if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) { + promises.push(this.workshopHelper.getReviewerAssessments(workshop.id).then((assessments) => { + assessments.forEach((assessment) => { + files = files.concat(assessment.feedbackattachmentfiles).concat(assessment.feedbackcontentfiles); + }); + })); + } + + return Promise.all(promises); + }); + }); + }).then(() => { + return { + workshop: workshop, + groups: groups, + files: files.filter((file) => typeof file !== 'undefined') + }; + }).catch((message) => { + if (omitFail) { + // Any error, return the info we have. + return Promise.resolve({ + workshop: workshop, + groups: groups, + files: files.filter((file) => typeof file !== 'undefined') + }); + } + + return Promise.reject(message); + }); + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId The course ID the module belongs to. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return this.workshopProvider.invalidateContent(moduleId, courseId); + } + + /** + * Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @return {boolean|Promise} Whether the module can be downloaded. The promise should never be rejected. + */ + isDownloadable(module: any, courseId: number): boolean | Promise { + return this.workshopProvider.getWorkshop(courseId, module.id, undefined, true).then((workshop) => { + return this.workshopProvider.getWorkshopAccessInformation(workshop.id).then((accessData) => { + // Check if workshop is setup by phase. + return accessData.canswitchphase || workshop.phase > AddonModWorkshopProvider.PHASE_SETUP; + }); + }); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + return this.workshopProvider.isPluginEnabled(); + } + + /** + * Prefetch a module. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when done. + */ + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { + return this.prefetchPackage(module, courseId, single, this.prefetchWorkshop.bind(this)); + } + + /** + * Retrieves all the grades reports for all the groups and then returns only unique grades. + * + * @param {number} workshopId Workshop ID. + * @param {any[]} groups Array of groups in the activity. + * @param {string} siteId Site ID. If not defined, current site. + * @return {Promise} All unique entries. + */ + protected getAllGradesReport(workshopId: number, groups: any[], siteId: string): Promise { + const promises = []; + + groups.forEach((group) => { + promises.push(this.workshopProvider.fetchAllGradeReports( + workshopId, group.id, undefined, false, false, siteId)); + }); + + return Promise.all(promises).then((grades) => { + const uniqueGrades = {}; + + grades.forEach((groupGrades) => { + groupGrades.forEach((grade) => { + if (grade.submissionid) { + uniqueGrades[grade.submissionid] = grade; + } + }); + }); + + return uniqueGrades; + }); + } + + /** + * Prefetch a workshop. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved when done. + */ + protected prefetchWorkshop(module: any, courseId: number, single: boolean, siteId: string): Promise { + const userIds = []; + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.sitesProvider.getSite(siteId).then((site) => { + const currentUserId = site.getUserId(); + + // Prefetch the workshop data. + return this.getWorkshopInfoHelper(module, courseId, false, false, true, siteId).then((info) => { + const workshop = info.workshop, + promises = [], + assessments = []; + + promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id)); + promises.push(this.workshopProvider.getWorkshopAccessInformation(workshop.id, false, true, siteId) + .then((access) => { + return this.workshopProvider.getUserPlanPhases(workshop.id, false, true, siteId).then((phases) => { + + // Get submission phase info. + const submissionPhase = phases[AddonModWorkshopProvider.PHASE_SUBMISSION], + canSubmit = this.workshopHelper.canSubmit(workshop, access, submissionPhase.tasks), + canAssess = this.workshopHelper.canAssess(workshop, access), + promises2 = []; + + if (canSubmit) { + promises2.push(this.workshopProvider.getSubmissions(workshop.id)); + // Add userId to the profiles to prefetch. + userIds.push(currentUserId); + } + + let reportPromise = Promise.resolve(); + if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) { + reportPromise = this.getAllGradesReport(workshop.id, info.groups, siteId) + .then((grades) => { + grades.forEach((grade) => { + userIds.push(grade.userid); + userIds.push(grade.gradeoverby); + + grade.reviewedby.forEach((assessment) => { + userIds.push(assessment.userid); + userIds.push(assessment.gradinggradeoverby); + assessments[assessment.assessmentid] = assessment; + }); + + grade.reviewerof.forEach((assessment) => { + userIds.push(assessment.userid); + userIds.push(assessment.gradinggradeoverby); + assessments[assessment.assessmentid] = assessment; + }); + }); + }); + } + + if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) { + // Wait the report promise to finish to override assessments array if needed. + reportPromise = reportPromise.finally(() => { + return this.workshopHelper.getReviewerAssessments(workshop.id, currentUserId, undefined, + undefined, siteId).then((revAssessments) => { + let p = Promise.resolve(); + revAssessments.forEach((assessment) => { + if (assessment.submission.authorid == currentUserId) { + p = this.workshopProvider.getAssessment(workshop.id, assessment.id); + } + userIds.push(assessment.reviewerid); + userIds.push(assessment.gradinggradeoverby); + assessments[assessment.id] = assessment; + }); + + return p; + }); + }); + } + + if (assessments.length > 0) { + reportPromise = reportPromise.finally(() => { + const promises3 = []; + assessments.forEach((assessment, id) => { + promises3.push(this.workshopProvider.getAssessmentForm(workshop.id, id, undefined, undefined, + undefined, siteId)); + }); + + return Promise.all(promises3); + }); + } + promises2.push(reportPromise); + + if (workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { + promises2.push(this.workshopProvider.getGrades(workshop.id)); + if (access.canviewpublishedsubmissions) { + promises2.push(this.workshopProvider.getSubmissions(workshop.id)); + } + } + + return Promise.all(promises2); + }); + })); + // Add Basic Info to manage links. + promises.push(this.courseProvider.getModuleBasicInfoByInstance(workshop.id, 'workshop', siteId)); + promises.push(this.courseProvider.getModuleBasicGradeInfo(module.id, siteId)); + + return Promise.all(promises); + }); + }).then(() => { + // Prefetch user profiles. + return this.userProvider.prefetchProfiles(userIds, courseId, siteId); + }); + } +} diff --git a/src/addon/mod/workshop/providers/sync-cron-handler.ts b/src/addon/mod/workshop/providers/sync-cron-handler.ts new file mode 100644 index 000000000..86bf07017 --- /dev/null +++ b/src/addon/mod/workshop/providers/sync-cron-handler.ts @@ -0,0 +1,47 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreCronHandler } from '@providers/cron'; +import { AddonModWorkshopSyncProvider } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonModWorkshopSyncCronHandler implements CoreCronHandler { + name = 'AddonModWorkshopSyncCronHandler'; + + constructor(private workshopSync: AddonModWorkshopSyncProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + return this.workshopSync.syncAllWorkshops(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return AddonModWorkshopSyncProvider.SYNC_TIME; + } +} diff --git a/src/addon/mod/workshop/workshop.module.ts b/src/addon/mod/workshop/workshop.module.ts index 2653be857..2c1c864e9 100644 --- a/src/addon/mod/workshop/workshop.module.ts +++ b/src/addon/mod/workshop/workshop.module.ts @@ -13,8 +13,10 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { CoreCronDelegate } from '@providers/cron'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { AddonModWorkshopComponentsModule } from './components/components.module'; import { AddonModWorkshopModuleHandler } from './providers/module-handler'; import { AddonModWorkshopProvider } from './providers/workshop'; @@ -23,6 +25,8 @@ import { AddonModWorkshopOfflineProvider } from './providers/offline'; import { AddonModWorkshopSyncProvider } from './providers/sync'; import { AddonModWorkshopHelperProvider } from './providers/helper'; import { AddonWorkshopAssessmentStrategyDelegate } from './providers/assessment-strategy-delegate'; +import { AddonModWorkshopPrefetchHandler } from './providers/prefetch-handler'; +import { AddonModWorkshopSyncCronHandler } from './providers/sync-cron-handler'; @NgModule({ declarations: [ @@ -37,13 +41,19 @@ import { AddonWorkshopAssessmentStrategyDelegate } from './providers/assessment- AddonModWorkshopOfflineProvider, AddonModWorkshopSyncProvider, AddonModWorkshopHelperProvider, - AddonWorkshopAssessmentStrategyDelegate + AddonWorkshopAssessmentStrategyDelegate, + AddonModWorkshopPrefetchHandler, + AddonModWorkshopSyncCronHandler ] }) export class AddonModWorkshopModule { constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModWorkshopModuleHandler, - contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModWorkshopLinkHandler) { + contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModWorkshopLinkHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModWorkshopPrefetchHandler, + cronDelegate: CoreCronDelegate, syncHandler: AddonModWorkshopSyncCronHandler) { moduleDelegate.registerHandler(moduleHandler); contentLinksDelegate.registerHandler(linkHandler); + prefetchDelegate.registerHandler(prefetchHandler); + cronDelegate.register(syncHandler); } }