// (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 { CoreTimeUtilsProvider } from '@providers/utils/time'; 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'; protected componentTranslate: string; constructor(translate: TranslateService, appProvider: CoreAppProvider, courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, timeUtils: CoreTimeUtilsProvider, private utils: CoreUtilsProvider, private workshopProvider: AddonModWorkshopProvider, private workshopHelper: AddonModWorkshopHelperProvider, private workshopOffline: AddonModWorkshopOfflineProvider) { super('AddonModWorkshopSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); 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) => { if (workshop.submissiontypefile == AddonModWorkshopProvider.SUBMISSION_TYPE_DISABLED) { attachmentsId = null; } // 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 = this.textUtils.getErrorMessageFromError(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 = this.textUtils.getErrorMessageFromError(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 = this.textUtils.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. return Promise.reject(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 = this.textUtils.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. return Promise.reject(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); } } }); } }