MOBILE-2061 course: Don't sync completion if modified in the site

main
dpalou 2018-10-05 10:10:33 +02:00
parent 1c6618b42a
commit 83ef6467c8
5 changed files with 87 additions and 64 deletions

View File

@ -1113,6 +1113,7 @@
"core.course.overriddennotice": "grades", "core.course.overriddennotice": "grades",
"core.course.sections": "moodle", "core.course.sections": "moodle",
"core.course.useactivityonbrowser": "local_moodlemobileapp", "core.course.useactivityonbrowser": "local_moodlemobileapp",
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
"core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp",
"core.coursedetails": "moodle", "core.coursedetails": "moodle",
"core.courses.allowguests": "enrol_guest", "core.courses.allowguests": "enrol_guest",

View File

@ -24,5 +24,6 @@
"refreshcourse": "Refresh course", "refreshcourse": "Refresh course",
"sections": "Sections", "sections": "Sections",
"useactivityonbrowser": "You can still use it using your device's web browser.", "useactivityonbrowser": "You can still use it using your device's web browser.",
"warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
"warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}" "warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}"
} }

View File

@ -45,7 +45,7 @@ export class CoreCourseOfflineProvider {
type: 'TEXT' type: 'TEXT'
}, },
{ {
name: 'timecreated', name: 'timecompleted',
type: 'INTEGER' type: 'INTEGER'
} }
] ]
@ -131,7 +131,7 @@ export class CoreCourseOfflineProvider {
completed: completed, completed: completed,
courseid: courseId, courseid: courseId,
coursename: courseName || '', coursename: courseName || '',
timecreated: Date.now() timecompleted: Date.now()
}; };
return site.getDb().insertRecord(CoreCourseOfflineProvider.MANUAL_COMPLETION_TABLE, entry); return site.getDb().insertRecord(CoreCourseOfflineProvider.MANUAL_COMPLETION_TABLE, entry);

View File

@ -121,9 +121,14 @@ export class CoreCourseProvider {
* @param {number} courseId Course ID. * @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined, current user. * @param {number} [userId] User ID. If not defined, current user.
* @param {boolean} [forceCache] True if it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
* @param {boolean} [includeOffline=true] True if it should load offline data in the completion status.
* @return {Promise<any>} Promise resolved with the completion statuses: object where the key is module ID. * @return {Promise<any>} Promise resolved with the completion statuses: object where the key is module ID.
*/ */
getActivitiesCompletionStatus(courseId: number, siteId?: string, userId?: number): Promise<any> { getActivitiesCompletionStatus(courseId: number, siteId?: string, userId?: number, forceCache: boolean = false,
ignoreCache: boolean = false, includeOffline: boolean = true): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId(); userId = userId || site.getUserId();
@ -133,10 +138,17 @@ export class CoreCourseProvider {
courseid: courseId, courseid: courseId,
userid: userId userid: userId
}, },
preSets = { preSets: CoreSiteWSPreSets = {
cacheKey: this.getActivitiesCompletionCacheKey(courseId, userId) cacheKey: this.getActivitiesCompletionCacheKey(courseId, userId)
}; };
if (forceCache) {
preSets.omitExpires = true;
} else if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('core_completion_get_activities_completion_status', params, preSets).then((data) => { return site.read('core_completion_get_activities_completion_status', params, preSets).then((data) => {
if (data && data.statuses) { if (data && data.statuses) {
return this.utils.arrayToObject(data.statuses, 'cmid'); return this.utils.arrayToObject(data.statuses, 'cmid');
@ -144,6 +156,10 @@ export class CoreCourseProvider {
return Promise.reject(null); return Promise.reject(null);
}).then((completionStatus) => { }).then((completionStatus) => {
if (!includeOffline) {
return completionStatus;
}
// Now get the offline completion (if any). // Now get the offline completion (if any).
return this.courseOffline.getCourseManualCompletions(courseId, site.id).then((offlineCompletions) => { return this.courseOffline.getCourseManualCompletions(courseId, site.id).then((offlineCompletions) => {
offlineCompletions.forEach((offlineCompletion) => { offlineCompletions.forEach((offlineCompletion) => {
@ -686,51 +702,33 @@ export class CoreCourseProvider {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Convenience function to store a message to be synchronized later. // Convenience function to store a completion to be synchronized later.
const storeOffline = (): Promise<any> => { const storeOffline = (): Promise<any> => {
return this.courseOffline.markCompletedManually(cmId, completed, courseId, courseName, siteId); return this.courseOffline.markCompletedManually(cmId, completed, courseId, courseName, siteId);
}; };
// Check if we already have a completion stored. // The offline function requires a courseId and it could be missing because it's a calculated field.
return this.courseOffline.getManualCompletion(cmId, siteId).catch(() => { if (!this.appProvider.isOnline() && courseId) {
// No completion stored. // App is offline, store the action.
}).then((entry) => { return storeOffline();
}
if (entry && completed != entry.completed) { // Try to send it to server.
// It has changed, this means that the offline data can be deleted because the action was undone. return this.markCompletedManuallyOnline(cmId, completed, siteId).then((result) => {
return this.courseOffline.deleteManualCompletion(cmId, siteId).then(() => { // Data sent to server, if there is some offline data delete it now.
return { return this.courseOffline.deleteManualCompletion(cmId, siteId).catch(() => {
status: true, // Ignore errors, shouldn't happen.
offline: true }).then(() => {
}; return result;
}); });
} }).catch((error) => {
if (this.utils.isWebServiceError(error) || !courseId) {
if (!this.appProvider.isOnline() && courseId) { // The WebService has thrown an error, this means that responses cannot be submitted.
// App is offline, store the action. return Promise.reject(error);
} else {
// Couldn't connect to server, store it offline.
return storeOffline(); return storeOffline();
} }
return this.markCompletedManuallyOnline(cmId, completed, siteId).then((result) => {
// Data sent to server, if there is some offline data delete it now.
if (entry) {
return this.courseOffline.deleteManualCompletion(cmId, siteId).catch(() => {
// Ignore errors, shouldn't happen.
}).then(() => {
return result;
});
}
return result;
}).catch((error) => {
if (this.utils.isWebServiceError(error) || !courseId) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
} else {
// Couldn't connect to server, store it offline.
return storeOffline();
}
});
}); });
} }

View File

@ -128,34 +128,57 @@ export class CoreCourseSyncProvider extends CoreSyncBaseProvider {
return Promise.reject(null); return Promise.reject(null);
} }
const promises = []; // Get the current completion status to check if any completion was modified in web.
return this.courseProvider.getActivitiesCompletionStatus(courseId, siteId, undefined, false, true, false)
.then((onlineCompletions) => {
// Send all the completions. const promises = [];
completions.forEach((entry) => {
promises.push(this.courseProvider.markCompletedManuallyOnline(entry.cmid, entry.completed, siteId).then(() => {
result.updated = true;
return this.courseOffline.deleteManualCompletion(entry.cmid, siteId); // Send all the completions.
}).catch((error) => { completions.forEach((entry) => {
if (this.utils.isWebServiceError(error)) { const onlineComp = onlineCompletions[entry.cmid];
// The WebService has thrown an error, this means that the completion cannot be submitted. Delete it.
result.updated = true;
return this.courseOffline.deleteManualCompletion(entry.cmid, siteId).then(() => { // Check if the completion was modified in online. If so, discard it.
// Responses deleted, add a warning. if (onlineComp && onlineComp.timecompleted * 1000 > entry.timecompleted) {
result.warnings.push(this.translate.instant('core.course.warningofflinemanualcompletiondeleted', { promises.push(this.courseOffline.deleteManualCompletion(entry.cmid, siteId).then(() => {
name: entry.coursename || courseId,
error: this.textUtils.getErrorMessageFromError(error) // Completion deleted, add a warning if the completion status doesn't match.
})); if (onlineComp.state != entry.completed) {
}); result.warnings.push(this.translate.instant('core.course.warningofflinemanualcompletiondeleted', {
name: entry.coursename || courseId,
error: this.translate.instant('core.course.warningmanualcompletionmodified')
}));
}
}));
return;
} }
// Couldn't connect to server, reject. promises.push(this.courseProvider.markCompletedManuallyOnline(entry.cmid, entry.completed, siteId).then(() => {
return Promise.reject(error); result.updated = true;
}));
});
return Promise.all(promises); return this.courseOffline.deleteManualCompletion(entry.cmid, siteId);
}).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that the completion cannot be submitted. Delete it.
result.updated = true;
return this.courseOffline.deleteManualCompletion(entry.cmid, siteId).then(() => {
// Completion deleted, add a warning.
result.warnings.push(this.translate.instant('core.course.warningofflinemanualcompletiondeleted', {
name: entry.coursename || courseId,
error: this.textUtils.getErrorMessageFromError(error)
}));
});
}
// Couldn't connect to server, reject.
return Promise.reject(error);
}));
});
return Promise.all(promises);
});
}).then(() => { }).then(() => {
if (result.updated) { if (result.updated) {
// Update data. // Update data.