From 3652d0591dfa9fde71c1caf9409a394b05411aef Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 30 May 2018 17:05:15 +0200 Subject: [PATCH] MOBILE-2354 workshop: Sync provider --- src/addon/mod/workshop/providers/sync.ts | 565 +++++++++++++++++++++++ 1 file changed, 565 insertions(+) create mode 100644 src/addon/mod/workshop/providers/sync.ts diff --git a/src/addon/mod/workshop/providers/sync.ts b/src/addon/mod/workshop/providers/sync.ts new file mode 100644 index 000000000..8c1e6fb15 --- /dev/null +++ b/src/addon/mod/workshop/providers/sync.ts @@ -0,0 +1,565 @@ +// (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 { TranslateService } from '@ngx-translate/core'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonModWorkshopProvider } from './workshop'; +import { AddonModWorkshopHelperProvider } from './helper'; +import { AddonModWorkshopOfflineProvider } from './offline'; + +/** + * Service to sync workshops. + */ +@Injectable() +export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_mod_workshop_autom_synced'; + static MANUAL_SYNCED = 'addon_mod_workshop_manual_synced'; + static SYNC_TIME = 300000; + + protected componentTranslate: string; + + constructor(translate: TranslateService, + appProvider: CoreAppProvider, + courseProvider: CoreCourseProvider, + private eventsProvider: CoreEventsProvider, + loggerProvider: CoreLoggerProvider, + sitesProvider: CoreSitesProvider, + syncProvider: CoreSyncProvider, + textUtils: CoreTextUtilsProvider, + private utils: CoreUtilsProvider, + private workshopProvider: AddonModWorkshopProvider, + private workshopHelper: AddonModWorkshopHelperProvider, + private workshopOffline: AddonModWorkshopOfflineProvider) { + + super('AddonModWorkshopSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); + + this.componentTranslate = courseProvider.translateModuleName('workshop'); + } + + /** + * Check if an workshop has data to synchronize. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has data to sync, false otherwise. + */ + hasDataToSync(workshopId: number, siteId?: string): Promise { + return this.workshopOffline.hasWorkshopOfflineData(workshopId, siteId); + } + + /** + * Try to synchronize all workshops that need it and haven't been synchronized in a while. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved when the sync is done. + */ + syncAllWorkshops(siteId?: string): Promise { + return this.syncOnSites('all workshops', this.syncAllWorkshopsFunc.bind(this), [], siteId); + } + + /** + * Sync all workshops on a site. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncAllWorkshopsFunc(siteId?: string): Promise { + return this.workshopOffline.getAllWorkshops(siteId).then((workshopIds) => { + const promises = []; + + // Sync all workshops that haven't been synced for a while. + workshopIds.forEach((workshopId) => { + promises.push(this.syncWorkshopIfNeeded(workshopId, siteId).then((data) => { + if (data && data.updated) { + // Sync done. Send event. + this.eventsProvider.trigger(AddonModWorkshopSyncProvider.AUTO_SYNCED, { + workshopId: workshopId, + warnings: data.warnings + }, siteId); + } + })); + }); + + return Promise.all(promises); + }); + } + + /** + * Sync a workshop only if a certain time has passed since the last time. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the workshop is synced or if it doesn't need to be synced. + */ + syncWorkshopIfNeeded(workshopId: number, siteId?: string): Promise { + return this.isSyncNeeded(workshopId, siteId).then((needed) => { + if (needed) { + return this.syncWorkshop(workshopId, siteId); + } + }); + } + + /** + * Try to synchronize a workshop. + * + * @param {number} workshopId Workshop ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncWorkshop(workshopId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (this.isSyncing(workshopId, siteId)) { + // There's already a sync ongoing for this discussion, return the promise. + return this.getOngoingSync(workshopId, siteId); + } + + // Verify that workshop isn't blocked. + if (this.syncProvider.isBlocked(AddonModWorkshopProvider.COMPONENT, workshopId, siteId)) { + this.logger.debug('Cannot sync workshop ' + workshopId + ' because it is blocked.'); + + return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); + } + + this.logger.debug('Try to sync workshop ' + workshopId); + + const syncPromises = []; + + // Get offline submissions to be sent. + syncPromises.push(this.workshopOffline.getSubmissions(workshopId, siteId).catch(() => { + // No offline data found, return empty array. + return []; + })); + + // Get offline submission assessments to be sent. + syncPromises.push(this.workshopOffline.getAssessments(workshopId, siteId).catch(() => { + // No offline data found, return empty array. + return []; + })); + + // Get offline submission evaluations to be sent. + syncPromises.push(this.workshopOffline.getEvaluateSubmissions(workshopId, siteId).catch(() => { + // No offline data found, return empty array. + return []; + })); + + // Get offline assessment evaluations to be sent. + syncPromises.push(this.workshopOffline.getEvaluateAssessments(workshopId, siteId).catch(() => { + // No offline data found, return empty array. + return []; + })); + + const result = { + warnings: [], + updated: false + }; + + // Get offline submissions to be sent. + const syncPromise = Promise.all(syncPromises).then((syncs) => { + let courseId; + + // Get courseId from the first object + for (const x in syncs) { + if (syncs[x].length > 0 && syncs[x][0].courseid) { + courseId = syncs[x][0].courseid; + break; + } + } + + if (!courseId) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(null); + } + + return this.workshopProvider.getWorkshopById(courseId, workshopId, siteId).then((workshop) => { + const submissionsActions = syncs[0], + assessments = syncs[1], + submissionEvaluations = syncs[2], + assessmentEvaluations = syncs[3], + promises = [], + offlineSubmissions = {}; + + submissionsActions.forEach((action) => { + if (typeof offlineSubmissions[action.submissionid] == 'undefined') { + offlineSubmissions[action.submissionid] = []; + } + offlineSubmissions[action.submissionid].push(action); + }); + + Object.keys(offlineSubmissions).forEach((submissionId) => { + const submissionActions = offlineSubmissions[submissionId]; + promises.push(this.syncSubmission(workshop, submissionActions, result, siteId).then(() => { + result.updated = true; + })); + }); + + assessments.forEach((assessment) => { + promises.push(this.syncAssessment(workshop, assessment, result, siteId).then(() => { + result.updated = true; + })); + }); + + submissionEvaluations.forEach((evaluation) => { + promises.push(this.syncEvaluateSubmission(workshop, evaluation, result, siteId).then(() => { + result.updated = true; + })); + }); + + assessmentEvaluations.forEach((evaluation) => { + promises.push(this.syncEvaluateAssessment(workshop, evaluation, result, siteId).then(() => { + result.updated = true; + })); + }); + + return Promise.all(promises); + }).then(() => { + if (result.updated) { + // Data has been sent to server. Now invalidate the WS calls. + return this.workshopProvider.invalidateContentById(workshopId, courseId, siteId).catch(() => { + // Ignore errors. + }); + } + }); + }).then(() => { + // Sync finished, set sync time. + return this.setSyncTime(workshopId, siteId).catch(() => { + // Ignore errors. + }); + }).then(() => { + // All done, return the warnings. + return result; + }); + + return this.addOngoingSync(workshopId, syncPromise, siteId); + } + + /** + * Synchronize a submission. + * + * @param {any} workshop Workshop. + * @param {any[]} submissionActions Submission actions offline data. + * @param {any} result Object with the result of the sync. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + protected syncSubmission(workshop: any, submissionActions: any, result: any, siteId: string): Promise { + let discardError; + let editing = false; + + // Sort entries by timemodified. + submissionActions = submissionActions.sort((a, b) => { + return a.timemodified - b.timemodified; + }); + + let timePromise = null; + let submissionId = submissionActions[0].submissionid; + + if (submissionId > 0) { + editing = true; + timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, siteId).then((submission) => { + return submission.timemodified; + }).catch(() => { + return -1; + }); + } else { + timePromise = Promise.resolve(0); + } + + return timePromise.then((timemodified) => { + if (timemodified < 0 || timemodified >= submissionActions[0].timemodified) { + // The entry was not found in Moodle or the entry has been modified, discard the action. + result.updated = true; + discardError = this.translate.instant('addon.mod_workshop.warningsubmissionmodified'); + + return this.workshopOffline.deleteAllSubmissionActions(workshop.id, submissionId, siteId); + } + + let promise = Promise.resolve(); + + submissionActions.forEach((action) => { + promise = promise.then(() => { + submissionId = action.submissionid > 0 ? action.submissionid : submissionId; + + let fileProm; + // Upload attachments first if any. + if (action.attachmentsid) { + fileProm = this.workshopHelper.getSubmissionFilesFromOfflineFilesObject(action.attachmentsid, workshop.id, + submissionId, editing, siteId).then((files) => { + return this.workshopHelper.uploadOrStoreSubmissionFiles(workshop.id, submissionId, files, editing, + false, siteId); + }); + } else { + // Remove all files. + fileProm = this.workshopHelper.uploadOrStoreSubmissionFiles(workshop.id, submissionId, [], editing, false, + siteId); + } + + return fileProm.then((attachmentsId) => { + // Perform the action. + switch (action.action) { + case 'add': + return this.workshopProvider.addSubmissionOnline(workshop.id, action.title, action.content, + attachmentsId, siteId).then((newSubmissionId) => { + submissionId = newSubmissionId; + }); + case 'update': + return this.workshopProvider.updateSubmissionOnline(submissionId, action.title, action.content, + attachmentsId, siteId); + case 'delete': + return this.workshopProvider.deleteSubmissionOnline(submissionId, siteId); + default: + return Promise.resolve(); + } + }).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = error.message || error.error; + } else { + // Couldn't connect to server, reject. + return Promise.reject(error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.workshopOffline.deleteSubmissionAction(action.workshopid, action.submissionid, action.action, + siteId); + }); + }); + }); + + return promise.then(() => { + if (discardError) { + // Submission was discarded, add a warning. + const message = this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: workshop.name, + error: discardError + }); + + if (result.warnings.indexOf(message) == -1) { + result.warnings.push(message); + } + } + }); + }); + } + + /** + * Synchronize an assessment. + * + * @param {any} workshop Workshop. + * @param {any} assessment Assessment offline data. + * @param {any} result Object with the result of the sync. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + protected syncAssessment(workshop: any, assessmentData: any, result: any, siteId: string): Promise { + let discardError; + const assessmentId = assessmentData.assessmentid; + + const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, siteId).then((assessment) => { + return assessment.timemodified; + }).catch(() => { + return -1; + }); + + return timePromise.then((timemodified) => { + if (timemodified < 0 || timemodified >= assessmentData.timemodified) { + // The entry was not found in Moodle or the entry has been modified, discard the action. + result.updated = true; + discardError = this.translate.instant('addon.mod_workshop.warningassessmentmodified'); + + return this.workshopOffline.deleteAssessment(workshop.id, assessmentId, siteId); + } + + let fileProm; + const inputData = assessmentData.inputdata; + + // Upload attachments first if any. + if (inputData.feedbackauthorattachmentsid) { + fileProm = this.workshopHelper.getAssessmentFilesFromOfflineFilesObject(inputData.feedbackauthorattachmentsid, + workshop.id, assessmentId, siteId).then((files) => { + return this.workshopHelper.uploadOrStoreAssessmentFiles(workshop.id, assessmentId, files, false, siteId); + }); + } else { + // Remove all files. + fileProm = this.workshopHelper.uploadOrStoreAssessmentFiles(workshop.id, assessmentId, [], false, siteId); + } + + return fileProm.then((attachmentsId) => { + inputData.feedbackauthorattachmentsid = attachmentsId || 0; + + return this.workshopProvider.updateAssessmentOnline(assessmentId, inputData, siteId); + }).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = error.message || error.error; + } else { + // Couldn't connect to server, reject. + return Promise.reject(error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.workshopOffline.deleteAssessment(workshop.id, assessmentId, siteId); + }); + }).then(() => { + if (discardError) { + // Assessment was discarded, add a warning. + const message = this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: workshop.name, + error: discardError + }); + + if (result.warnings.indexOf(message) == -1) { + result.warnings.push(message); + } + } + }); + } + + /** + * Synchronize a submission evaluation. + * + * @param {any} workshop Workshop. + * @param {any} evaluate Submission evaluation offline data. + * @param {any} result Object with the result of the sync. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + protected syncEvaluateSubmission(workshop: any, evaluate: any, result: any, siteId: string): Promise { + let discardError; + const submissionId = evaluate.submissionid; + + const timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, siteId).then((submission) => { + return submission.timemodified; + }).catch(() => { + return -1; + }); + + return timePromise.then((timemodified) => { + if (timemodified < 0 || timemodified >= evaluate.timemodified) { + // The entry was not found in Moodle or the entry has been modified, discard the action. + result.updated = true; + discardError = this.translate.instant('addon.mod_workshop.warningsubmissionmodified'); + + return this.workshopOffline.deleteEvaluateSubmission(workshop.id, submissionId, siteId); + } + + return this.workshopProvider.evaluateSubmissionOnline(submissionId, evaluate.feedbacktext, evaluate.published, + evaluate.gradeover, siteId).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = error.message || error.error; + } else { + // Couldn't connect to server, reject. + return Promise.reject(error && error.error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.workshopOffline.deleteEvaluateSubmission(workshop.id, submissionId, siteId); + }); + }).then(() => { + if (discardError) { + // Assessment was discarded, add a warning. + const message = this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: workshop.name, + error: discardError + }); + + if (result.warnings.indexOf(message) == -1) { + result.warnings.push(message); + } + } + }); + } + + /** + * Synchronize a assessment evaluation. + * + * @param {any} workshop Workshop. + * @param {any} evaluate Assessment evaluation offline data. + * @param {any} result Object with the result of the sync. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + protected syncEvaluateAssessment(workshop: any, evaluate: any, result: any, siteId: string): Promise { + let discardError; + const assessmentId = evaluate.assessmentid; + + const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, siteId).then((assessment) => { + return assessment.timemodified; + }).catch(() => { + return -1; + }); + + return timePromise.then((timemodified) => { + if (timemodified < 0 || timemodified >= evaluate.timemodified) { + // The entry was not found in Moodle or the entry has been modified, discard the action. + result.updated = true; + discardError = this.translate.instant('addon.mod_workshop.warningassessmentmodified'); + + return this.workshopOffline.deleteEvaluateAssessment(workshop.id, assessmentId, siteId); + } + + return this.workshopProvider.evaluateAssessmentOnline(assessmentId, evaluate.feedbacktext, evaluate.weight, + evaluate.gradinggradeover, siteId).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = error.message || error.error; + } else { + // Couldn't connect to server, reject. + return Promise.reject(error && error.error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.workshopOffline.deleteEvaluateAssessment(workshop.id, assessmentId, siteId); + }); + }).then(() => { + if (discardError) { + // Assessment was discarded, add a warning. + const message = this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: workshop.name, + error: discardError + }); + + if (result.warnings.indexOf(message) == -1) { + result.warnings.push(message); + } + } + }); + } +}