MOBILE-2354 workshop: Sync provider

main
Albert Gasset 2018-05-30 17:05:15 +02:00
parent d000b3da36
commit 3652d0591d
1 changed files with 565 additions and 0 deletions

View File

@ -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<any>} Promise resolved with boolean: true if has data to sync, false otherwise.
*/
hasDataToSync(workshopId: number, siteId?: string): Promise<any> {
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<any>} Promise resolved when the sync is done.
*/
syncAllWorkshops(siteId?: string): Promise<any> {
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<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
protected syncAllWorkshopsFunc(siteId?: string): Promise<any> {
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<any>} Promise resolved when the workshop is synced or if it doesn't need to be synced.
*/
syncWorkshopIfNeeded(workshopId: number, siteId?: string): Promise<any> {
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<any>} Promise resolved if sync is successful, rejected otherwise.
*/
syncWorkshop(workshopId: number, siteId?: string): Promise<any> {
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<any>} Promise resolved if success, rejected otherwise.
*/
protected syncSubmission(workshop: any, submissionActions: any, result: any, siteId: string): Promise<any> {
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<any>} Promise resolved if success, rejected otherwise.
*/
protected syncAssessment(workshop: any, assessmentData: any, result: any, siteId: string): Promise<any> {
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<any>} Promise resolved if success, rejected otherwise.
*/
protected syncEvaluateSubmission(workshop: any, evaluate: any, result: any, siteId: string): Promise<any> {
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<any>} Promise resolved if success, rejected otherwise.
*/
protected syncEvaluateAssessment(workshop: any, evaluate: any, result: any, siteId: string): Promise<any> {
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);
}
}
});
}
}