From be209e9cb5dd3d72d328ef42a0ad130829a9773c Mon Sep 17 00:00:00 2001 From: dpalou Date: Tue, 2 Oct 2018 08:55:06 +0200 Subject: [PATCH 1/5] MOBILE-2061 course: Save manual completion in offline --- .../module-completion/module-completion.ts | 16 +-- .../components/module/core-course-module.html | 9 +- src/core/course/course.module.ts | 5 +- src/core/course/lang/en.json | 1 + src/core/course/providers/course-offline.ts | 126 ++++++++++++++++++ src/core/course/providers/course.ts | 105 ++++++++++++++- 6 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 src/core/course/providers/course-offline.ts diff --git a/src/core/course/components/module-completion/module-completion.ts b/src/core/course/components/module-completion/module-completion.ts index 25e3165d8..5faf673b0 100644 --- a/src/core/course/components/module-completion/module-completion.ts +++ b/src/core/course/components/module-completion/module-completion.ts @@ -14,10 +14,10 @@ import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreCourseProvider } from '../../providers/course'; /** * Component to handle activity completion. It shows a checkbox with the current status, and allows manually changing @@ -41,7 +41,8 @@ export class CoreCourseModuleCompletionComponent implements OnChanges { completionDescription: string; constructor(private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider, - private translate: TranslateService, private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider) { + private translate: TranslateService, private courseProvider: CoreCourseProvider, + private userProvider: CoreUserProvider) { this.completionChanged = new EventEmitter(); } @@ -68,14 +69,11 @@ export class CoreCourseModuleCompletionComponent implements OnChanges { e.preventDefault(); e.stopPropagation(); - const modal = this.domUtils.showModalLoading(), - params = { - cmid: this.completion.cmid, - completed: this.completion.state === 1 ? 0 : 1 - }, - currentSite = this.sitesProvider.getCurrentSite(); + const modal = this.domUtils.showModalLoading(); + + this.courseProvider.markCompletedManually(this.completion.cmid, this.completion.state === 1 ? 0 : 1, + this.completion.courseId).then((response) => { - currentSite.write('core_completion_update_activity_completion_status_manually', params).then((response) => { if (!response.status) { return Promise.reject(null); } diff --git a/src/core/course/components/module/core-course-module.html b/src/core/course/components/module/core-course-module.html index d5c91e4d1..1f815d2ed 100644 --- a/src/core/course/components/module/core-course-module.html +++ b/src/core/course/components/module/core-course-module.html @@ -30,13 +30,14 @@ -
+
- {{ 'core.course.hiddenfromstudents' | translate }} - {{ 'core.course.hiddenoncoursepage' | translate }} - + {{ 'core.course.hiddenfromstudents' | translate }} + {{ 'core.course.hiddenoncoursepage' | translate }} + + {{ 'core.course.manualcompletionnotsynced' | translate }}
\ No newline at end of file diff --git a/src/core/course/course.module.ts b/src/core/course/course.module.ts index 2522e66f0..a4ebf855c 100644 --- a/src/core/course/course.module.ts +++ b/src/core/course/course.module.ts @@ -17,6 +17,7 @@ import { CoreCourseProvider } from './providers/course'; import { CoreCourseHelperProvider } from './providers/helper'; import { CoreCourseFormatDelegate } from './providers/format-delegate'; import { CoreCourseModuleDelegate } from './providers/module-delegate'; +import { CoreCourseOfflineProvider } from './providers/course-offline'; import { CoreCourseModulePrefetchDelegate } from './providers/module-prefetch-delegate'; import { CoreCourseOptionsDelegate } from './providers/options-delegate'; import { CoreCourseFormatDefaultHandler } from './providers/default-format'; @@ -33,7 +34,8 @@ export const CORE_COURSE_PROVIDERS: any[] = [ CoreCourseFormatDelegate, CoreCourseModuleDelegate, CoreCourseModulePrefetchDelegate, - CoreCourseOptionsDelegate + CoreCourseOptionsDelegate, + CoreCourseOfflineProvider ]; @NgModule({ @@ -51,6 +53,7 @@ export const CORE_COURSE_PROVIDERS: any[] = [ CoreCourseModuleDelegate, CoreCourseModulePrefetchDelegate, CoreCourseOptionsDelegate, + CoreCourseOfflineProvider, CoreCourseFormatDefaultHandler, CoreCourseModuleDefaultHandler ], diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json index e21a91a95..02e85ef9e 100644 --- a/src/core/course/lang/en.json +++ b/src/core/course/lang/en.json @@ -18,6 +18,7 @@ "errorgetmodule": "Error getting activity data.", "hiddenfromstudents": "Hidden from students", "hiddenoncoursepage": "Available but not shown on course page", + "manualcompletionnotsynced": "Manual completion not synchronised.", "nocontentavailable": "No content available at the moment.", "overriddennotice": "Your final grade from this activity was manually adjusted.", "refreshcourse": "Refresh course", diff --git a/src/core/course/providers/course-offline.ts b/src/core/course/providers/course-offline.ts new file mode 100644 index 000000000..774532a45 --- /dev/null +++ b/src/core/course/providers/course-offline.ts @@ -0,0 +1,126 @@ +// (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 { CoreSitesProvider } from '@providers/sites'; + +/** + * Service to handle offline data for courses. + */ +@Injectable() +export class CoreCourseOfflineProvider { + + // Variables for database. + static MANUAL_COMPLETION_TABLE = 'course_manual_completion'; + protected tablesSchema = [ + { + name: CoreCourseOfflineProvider.MANUAL_COMPLETION_TABLE, + columns: [ + { + name: 'cmid', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'completed', + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'timecreated', + type: 'INTEGER' + } + ] + } + ]; + + constructor(private sitesProvider: CoreSitesProvider) { + this.sitesProvider.createTablesFromSchema(this.tablesSchema); + } + + /** + * Delete a manual completion stored. + * + * @param {number} cmId The module ID to remove the completion. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted, rejected if failure. + */ + deleteManualCompletion(cmId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + + return site.getDb().deleteRecords(CoreCourseOfflineProvider.MANUAL_COMPLETION_TABLE, {cmid: cmId}); + }); + } + + /** + * Get all offline manual completions for a certain course. + * + * @param {number} courseId Course ID the module belongs to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the list of completions. + */ + getCourseManualCompletions(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + + return site.getDb().getRecords(CoreCourseOfflineProvider.MANUAL_COMPLETION_TABLE, {courseid: courseId}); + }); + } + + /** + * Get the offline manual completion for a certain module. + * + * @param {number} cmId The module ID to remove the completion. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the completion, rejected if failure or not found. + */ + getManualCompletion(cmId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + + return site.getDb().getRecord(CoreCourseOfflineProvider.MANUAL_COMPLETION_TABLE, {cmid: cmId}); + }); + } + + /** + * Offline version for manually marking a module as completed. + * + * @param {number} cmId The module ID to store the completion. + * @param {number} completed Whether the module is completed or not. + * @param {number} courseId Course ID the module belongs to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise<{status: boolean, offline: boolean}>} Promise resolved when completion is successfully stored. + */ + markCompletedManually(cmId: number, completed: number, courseId: number, siteId?: string) + : Promise<{status: boolean, offline: boolean}> { + + // Store the offline data. + return this.sitesProvider.getSite(siteId).then((site) => { + const entry = { + cmid: cmId, + completed: completed, + courseid: courseId, + timecreated: Date.now() + }; + + return site.getDb().insertRecord(CoreCourseOfflineProvider.MANUAL_COMPLETION_TABLE, entry); + }).then(() => { + return { + status: true, + offline: true + }; + }); + } +} diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index efdbf618e..bbe338cd4 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; @@ -21,6 +22,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSiteWSPreSets } from '@classes/site'; import { CoreConstants } from '../../constants'; +import { CoreCourseOfflineProvider } from './course-offline'; /** * Service that provides some features regarding a course. @@ -75,7 +77,8 @@ export class CoreCourseProvider { ]; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider, - private utils: CoreUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private translate: TranslateService) { + private utils: CoreUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private translate: TranslateService, + private courseOffline: CoreCourseOfflineProvider, private appProvider: CoreAppProvider) { this.logger = logger.getInstance('CoreCourseProvider'); this.sitesProvider.createTableFromSchema(this.courseStatusTableSchema); @@ -140,6 +143,27 @@ export class CoreCourseProvider { } return Promise.reject(null); + }).then((completionStatus) => { + // Now get the offline completion (if any). + return this.courseOffline.getCourseManualCompletions(courseId, site.id).then((offlineCompletions) => { + offlineCompletions.forEach((offlineCompletion) => { + + if (offlineCompletion && typeof completionStatus[offlineCompletion.cmid] != 'undefined') { + const onlineCompletion = completionStatus[offlineCompletion.cmid]; + + // If the activity uses manual completion, override the value with the offline one. + if (onlineCompletion.tracking === 1) { + onlineCompletion.state = offlineCompletion.completed; + onlineCompletion.offline = true; + } + } + }); + + return completionStatus; + }).catch(() => { + // Ignore errors. + return completionStatus; + }); }); }); } @@ -647,6 +671,85 @@ export class CoreCourseProvider { }); } + /** + * Offline version for manually marking a module as completed. + * + * @param {number} cmId The module ID. + * @param {number} completed Whether the module is completed or not. + * @param {number} courseId Course ID the module belongs to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when completion is successfully sent or stored. + */ + markCompletedManually(cmId: number, completed: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a message to be synchronized later. + const storeOffline = (): Promise => { + return this.courseOffline.markCompletedManually(cmId, completed, courseId, siteId); + }; + + // Check if we already have a completion stored. + return this.courseOffline.getManualCompletion(cmId, siteId).catch(() => { + // No completion stored. + }).then((entry) => { + + if (entry && completed != entry.completed) { + // It has changed, this means that the offline data can be deleted because the action was undone. + return this.courseOffline.deleteManualCompletion(cmId, siteId).then(() => { + return { + status: true, + offline: true + }; + }); + } + + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + 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)) { + // 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(); + } + }); + }); + } + + /** + * Offline version for manually marking a module as completed. + * + * @param {number} cmId The module ID. + * @param {number} completed Whether the module is completed or not. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when completion is successfully sent. + */ + markCompletedManuallyOnline(cmId: number, completed: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + cmid: cmId, + completed: completed + }; + + return site.write('core_completion_update_activity_completion_status_manually', params); + }); + } + /** * Change the course status, setting it to the previous status. * From e4248a8a44066e56dba7d6dcc7cd2e65f8734df3 Mon Sep 17 00:00:00 2001 From: dpalou Date: Tue, 2 Oct 2018 13:02:11 +0200 Subject: [PATCH 2/5] MOBILE-2061 course: Synchronize offline manual completion --- scripts/langindex.json | 2 + .../module-completion/module-completion.ts | 2 +- src/core/course/course.module.ts | 16 +- src/core/course/lang/en.json | 3 +- src/core/course/pages/section/section.ts | 53 ++++-- src/core/course/providers/course-offline.ts | 21 ++- src/core/course/providers/course.ts | 11 +- src/core/course/providers/helper.ts | 4 +- .../course/providers/sync-cron-handler.ts | 47 +++++ src/core/course/providers/sync.ts | 178 ++++++++++++++++++ 10 files changed, 314 insertions(+), 23 deletions(-) create mode 100644 src/core/course/providers/sync-cron-handler.ts create mode 100644 src/core/course/providers/sync.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index dc7ab8aa1..0c767ab98 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1108,10 +1108,12 @@ "core.course.errorgetmodule": "local_moodlemobileapp", "core.course.hiddenfromstudents": "moodle", "core.course.hiddenoncoursepage": "moodle", + "core.course.manualcompletionnotsynced": "local_moodlemobileapp", "core.course.nocontentavailable": "local_moodlemobileapp", "core.course.overriddennotice": "grades", "core.course.sections": "moodle", "core.course.useactivityonbrowser": "local_moodlemobileapp", + "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", "core.coursedetails": "moodle", "core.courses.allowguests": "enrol_guest", "core.courses.availablecourses": "moodle", diff --git a/src/core/course/components/module-completion/module-completion.ts b/src/core/course/components/module-completion/module-completion.ts index 5faf673b0..c383c41ca 100644 --- a/src/core/course/components/module-completion/module-completion.ts +++ b/src/core/course/components/module-completion/module-completion.ts @@ -72,7 +72,7 @@ export class CoreCourseModuleCompletionComponent implements OnChanges { const modal = this.domUtils.showModalLoading(); this.courseProvider.markCompletedManually(this.completion.cmid, this.completion.state === 1 ? 0 : 1, - this.completion.courseId).then((response) => { + this.completion.courseId, this.completion.courseName).then((response) => { if (!response.status) { return Promise.reject(null); diff --git a/src/core/course/course.module.ts b/src/core/course/course.module.ts index a4ebf855c..541c4333b 100644 --- a/src/core/course/course.module.ts +++ b/src/core/course/course.module.ts @@ -13,6 +13,7 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { CoreCronDelegate } from '@providers/cron'; import { CoreCourseProvider } from './providers/course'; import { CoreCourseHelperProvider } from './providers/helper'; import { CoreCourseFormatDelegate } from './providers/format-delegate'; @@ -26,6 +27,8 @@ import { CoreCourseFormatSingleActivityModule } from './formats/singleactivity/s import { CoreCourseFormatSocialModule } from './formats/social/social.module'; import { CoreCourseFormatTopicsModule } from './formats/topics/topics.module'; import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module'; +import { CoreCourseSyncProvider } from './providers/sync'; +import { CoreCourseSyncCronHandler } from './providers/sync-cron-handler'; // List of providers (without handlers). export const CORE_COURSE_PROVIDERS: any[] = [ @@ -35,7 +38,8 @@ export const CORE_COURSE_PROVIDERS: any[] = [ CoreCourseModuleDelegate, CoreCourseModulePrefetchDelegate, CoreCourseOptionsDelegate, - CoreCourseOfflineProvider + CoreCourseOfflineProvider, + CoreCourseSyncProvider ]; @NgModule({ @@ -54,9 +58,15 @@ export const CORE_COURSE_PROVIDERS: any[] = [ CoreCourseModulePrefetchDelegate, CoreCourseOptionsDelegate, CoreCourseOfflineProvider, + CoreCourseSyncProvider, CoreCourseFormatDefaultHandler, - CoreCourseModuleDefaultHandler + CoreCourseModuleDefaultHandler, + CoreCourseSyncCronHandler ], exports: [] }) -export class CoreCourseModule {} +export class CoreCourseModule { + constructor(cronDelegate: CoreCronDelegate, syncHandler: CoreCourseSyncCronHandler) { + cronDelegate.register(syncHandler); + } +} diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json index 02e85ef9e..3b9028eba 100644 --- a/src/core/course/lang/en.json +++ b/src/core/course/lang/en.json @@ -23,5 +23,6 @@ "overriddennotice": "Your final grade from this activity was manually adjusted.", "refreshcourse": "Refresh course", "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.", + "warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}" } \ No newline at end of file diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 4184c17ac..a1f902e16 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -24,6 +24,7 @@ import { CoreCourseHelperProvider } from '../../providers/helper'; import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; import { CoreCourseModulePrefetchDelegate } from '../../providers/module-prefetch-delegate'; import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay } from '../../providers/options-delegate'; +import { CoreCourseSyncProvider } from '../../providers/sync'; import { CoreCourseFormatComponent } from '../../components/format/format'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreTabsComponent } from '@components/tabs/tabs'; @@ -62,6 +63,7 @@ export class CoreCourseSectionPage implements OnDestroy { protected module: any; protected completionObserver; protected courseStatusObserver; + protected syncObserver; protected firstTabName: string; protected isDestroyed = false; @@ -70,7 +72,7 @@ export class CoreCourseSectionPage implements OnDestroy { private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider, sitesProvider: CoreSitesProvider, private navCtrl: NavController, private injector: Injector, - private prefetchDelegate: CoreCourseModulePrefetchDelegate) { + private prefetchDelegate: CoreCourseModulePrefetchDelegate, private syncProvider: CoreCourseSyncProvider) { this.course = navParams.get('course'); this.sectionId = navParams.get('sectionId'); this.sectionNumber = navParams.get('sectionNumber'); @@ -87,7 +89,17 @@ export class CoreCourseSectionPage implements OnDestroy { if (shouldRefresh) { this.completionObserver = eventsProvider.on(CoreEventsProvider.COMPLETION_MODULE_VIEWED, (data) => { if (data && data.courseId == this.course.id) { - this.refreshAfterCompletionChange(); + this.refreshAfterCompletionChange(true); + } + }); + + this.syncObserver = eventsProvider.on(CoreCourseSyncProvider.AUTO_SYNCED, (data) => { + if (data && data.courseId == this.course.id) { + this.refreshAfterCompletionChange(false); + + if (data.warnings && data.warnings[0]) { + this.domUtils.showErrorModal(data.warnings[0]); + } } }); } @@ -113,7 +125,7 @@ export class CoreCourseSectionPage implements OnDestroy { this.courseHelper.openModule(this.navCtrl, this.module, this.course.id, this.sectionId); } - this.loadData().finally(() => { + this.loadData(false, true).finally(() => { this.dataLoaded = true; if (!this.downloadCourseEnabled) { @@ -146,19 +158,34 @@ export class CoreCourseSectionPage implements OnDestroy { /** * Fetch and load all the data required for the view. + * + * @param {boolean} [refresh] If it's refreshing content. + * @param {boolean} [sync] If the refresh is needs syncing. + * @return {Promise} Promise resolved when done. */ - protected loadData(refresh?: boolean): Promise { + protected loadData(refresh?: boolean, sync?: boolean): Promise { // First of all, get the course because the data might have changed. return this.coursesProvider.getUserCourse(this.course.id).catch(() => { // Error getting the course, probably guest access. }).then((course) => { - const promises = []; - let promise; - if (course) { this.course = course; } + if (sync) { + // Try to synchronize the course data. + return this.syncProvider.syncCourse(this.course.id).then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); + } + }).catch(() => { + // For now we don't allow manual syncing, so ignore errors. + }); + } + }).then(() => { + const promises = []; + let promise; + // Get the completion status. if (this.course.enablecompletion === false) { // Completion not enabled. @@ -185,7 +212,7 @@ export class CoreCourseSectionPage implements OnDestroy { } }).then((sections) => { - this.courseHelper.addHandlerDataForModules(sections, this.course.id, completionStatus); + this.courseHelper.addHandlerDataForModules(sections, this.course.id, completionStatus, this.course.fullname); // Format the name of each section and check if it has content. this.sections = sections.map((section) => { @@ -268,7 +295,7 @@ export class CoreCourseSectionPage implements OnDestroy { */ doRefresh(refresher?: any): Promise { return this.invalidateData().finally(() => { - return this.loadData(true).finally(() => { + return this.loadData(true, true).finally(() => { /* Do not call doRefresh on the format component if the refresher is defined in the format component to prevent an inifinite loop. */ let promise; @@ -290,7 +317,7 @@ export class CoreCourseSectionPage implements OnDestroy { */ onCompletionChange(): void { this.invalidateData().finally(() => { - this.refreshAfterCompletionChange(); + this.refreshAfterCompletionChange(true); }); } @@ -314,8 +341,10 @@ export class CoreCourseSectionPage implements OnDestroy { /** * Refresh list after a completion change since there could be new activities. + * + * @param {boolean} [sync] If the refresh is needs syncing. */ - protected refreshAfterCompletionChange(): void { + protected refreshAfterCompletionChange(sync?: boolean): void { // Save scroll position to restore it once done. const scrollElement = this.content.getScrollElement(), scrollTop = scrollElement.scrollTop || 0, @@ -324,7 +353,7 @@ export class CoreCourseSectionPage implements OnDestroy { this.dataLoaded = false; this.domUtils.scrollToTop(this.content); // Scroll top so the spinner is seen. - this.loadData().then(() => { + this.loadData(true, sync).then(() => { return this.formatComponent.doRefresh(undefined, undefined, true); }).finally(() => { this.dataLoaded = true; diff --git a/src/core/course/providers/course-offline.ts b/src/core/course/providers/course-offline.ts index 774532a45..869161e8e 100644 --- a/src/core/course/providers/course-offline.ts +++ b/src/core/course/providers/course-offline.ts @@ -40,6 +40,10 @@ export class CoreCourseOfflineProvider { name: 'courseid', type: 'INTEGER' }, + { + name: 'coursename', + type: 'TEXT' + }, { name: 'timecreated', type: 'INTEGER' @@ -66,6 +70,19 @@ export class CoreCourseOfflineProvider { }); } + /** + * Get all offline manual completions for a certain course. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the list of completions. + */ + getAllManualCompletions(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + + return site.getDb().getRecords(CoreCourseOfflineProvider.MANUAL_COMPLETION_TABLE); + }); + } + /** * Get all offline manual completions for a certain course. * @@ -100,10 +117,11 @@ export class CoreCourseOfflineProvider { * @param {number} cmId The module ID to store the completion. * @param {number} completed Whether the module is completed or not. * @param {number} courseId Course ID the module belongs to. + * @param {string} [courseName] Course name. Recommended, it is used to display a better warning message. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise<{status: boolean, offline: boolean}>} Promise resolved when completion is successfully stored. */ - markCompletedManually(cmId: number, completed: number, courseId: number, siteId?: string) + markCompletedManually(cmId: number, completed: number, courseId: number, courseName?: string, siteId?: string) : Promise<{status: boolean, offline: boolean}> { // Store the offline data. @@ -112,6 +130,7 @@ export class CoreCourseOfflineProvider { cmid: cmId, completed: completed, courseid: courseId, + coursename: courseName || '', timecreated: Date.now() }; diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index bbe338cd4..4e30f65fb 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -677,15 +677,18 @@ export class CoreCourseProvider { * @param {number} cmId The module ID. * @param {number} completed Whether the module is completed or not. * @param {number} courseId Course ID the module belongs to. + * @param {string} [courseName] Course name. Recommended, it is used to display a better warning message. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when completion is successfully sent or stored. */ - markCompletedManually(cmId: number, completed: number, courseId: number, siteId?: string): Promise { + markCompletedManually(cmId: number, completed: number, courseId: number, courseName?: string, siteId?: string) + : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a message to be synchronized later. const storeOffline = (): Promise => { - return this.courseOffline.markCompletedManually(cmId, completed, courseId, siteId); + return this.courseOffline.markCompletedManually(cmId, completed, courseId, courseName, siteId); }; // Check if we already have a completion stored. @@ -703,7 +706,7 @@ export class CoreCourseProvider { }); } - if (!this.appProvider.isOnline()) { + if (!this.appProvider.isOnline() && courseId) { // App is offline, store the action. return storeOffline(); } @@ -720,7 +723,7 @@ export class CoreCourseProvider { return result; }).catch((error) => { - if (this.utils.isWebServiceError(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 { diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 88c8f433f..968887a3e 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -131,9 +131,10 @@ export class CoreCourseHelperProvider { * @param {any[]} sections List of sections to treat modules. * @param {number} courseId Course ID of the modules. * @param {any[]} [completionStatus] List of completion status. + * @param {string} [courseName] Course name. Recommended if completionStatus is supplied. * @return {boolean} Whether the sections have content. */ - addHandlerDataForModules(sections: any[], courseId: number, completionStatus?: any): boolean { + addHandlerDataForModules(sections: any[], courseId: number, completionStatus?: any, courseName?: string): boolean { let hasContent = false; sections.forEach((section) => { @@ -150,6 +151,7 @@ export class CoreCourseHelperProvider { // Check if activity has completions and if it's marked. module.completionstatus = completionStatus[module.id]; module.completionstatus.courseId = courseId; + module.completionstatus.courseName = courseName; } // Check if the module is stealth. diff --git a/src/core/course/providers/sync-cron-handler.ts b/src/core/course/providers/sync-cron-handler.ts new file mode 100644 index 000000000..f7bd6cdba --- /dev/null +++ b/src/core/course/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 { CoreCourseSyncProvider } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class CoreCourseSyncCronHandler implements CoreCronHandler { + name = 'CoreCourseSyncCronHandler'; + + constructor(private courseSync: CoreCourseSyncProvider) {} + + /** + * 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.courseSync.syncAllCourses(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return this.courseSync.syncInterval; + } +} diff --git a/src/core/course/providers/sync.ts b/src/core/course/providers/sync.ts new file mode 100644 index 000000000..670040881 --- /dev/null +++ b/src/core/course/providers/sync.ts @@ -0,0 +1,178 @@ +// (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 { CoreLoggerProvider } from '@providers/logger'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreCourseOfflineProvider } from './course-offline'; +import { CoreCourseProvider } from './course'; +import { CoreEventsProvider } from '@providers/events'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSyncProvider } from '@providers/sync'; + +/** + * Service to sync course offline data. This only syncs the offline data of the course itself, not the offline data of + * the activities in the course. + */ +@Injectable() +export class CoreCourseSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'core_course_autom_synced'; + + constructor(protected sitesProvider: CoreSitesProvider, loggerProvider: CoreLoggerProvider, + protected appProvider: CoreAppProvider, private courseOffline: CoreCourseOfflineProvider, + private eventsProvider: CoreEventsProvider, private courseProvider: CoreCourseProvider, + translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider, + syncProvider: CoreSyncProvider) { + + super('CoreCourseSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); + } + + /** + * Try to synchronize all the courses in a certain site or in all sites. + * + * @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. + */ + syncAllCourses(siteId?: string): Promise { + return this.syncOnSites('courses', this.syncAllCoursesFunc.bind(this), undefined, siteId); + } + + /** + * Sync all courses 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 syncAllCoursesFunc(siteId?: string): Promise { + return this.courseOffline.getAllManualCompletions(siteId).then((completions) => { + const promises = []; + + // Sync all courses. + completions.forEach((completion) => { + promises.push(this.syncCourseIfNeeded(completion.courseid, siteId).then((result) => { + if (result && result.updated) { + // Sync successful, send event. + this.eventsProvider.trigger(CoreCourseSyncProvider.AUTO_SYNCED, { + courseId: completion.courseid, + warnings: result.warnings + }, siteId); + } + })); + }); + }); + } + + /** + * Sync a course if it's needed. + * + * @param {number} courseId Course ID to be synced. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the course is synced or it doesn't need to be synced. + */ + syncCourseIfNeeded(courseId: number, siteId?: string): Promise { + // Usually we call isSyncNeeded to check if a certain time has passed. + // However, since we barely send data for now just sync the course. + return this.syncCourse(courseId, siteId); + } + + /** + * Synchronize a course. + * + * @param {number} courseId Course ID to be synced. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncCourse(courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (this.isSyncing(courseId, siteId)) { + // There's already a sync ongoing for this discussion, return the promise. + return this.getOngoingSync(courseId, siteId); + } + + this.logger.debug(`Try to sync course '${courseId}'`); + + const result = { + warnings: [], + updated: false + }; + + // Get offline responses to be sent. + const syncPromise = this.courseOffline.getCourseManualCompletions(courseId, siteId).catch(() => { + // No offline data found, return empty list. + return []; + }).then((completions) => { + if (!completions || !completions.length) { + // Nothing to sync. + return; + } + + if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(null); + } + + const promises = []; + + // Send all the completions. + completions.forEach((entry) => { + promises.push(this.courseProvider.markCompletedManuallyOnline(entry.cmid, entry.completed, siteId).then(() => { + result.updated = true; + + 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(() => { + // Responses deleted, add a warning. + result.warnings.push(this.translate.instant('core.course.warningofflinemanualcompletiondeleted', { + name: entry.coursename || courseId, + error: error.error + })); + }); + } + + // Couldn't connect to server, reject. + return Promise.reject(error); + })); + }); + + return Promise.all(promises); + }).then(() => { + if (result.updated) { + // Update data. + return this.courseProvider.invalidateSections(courseId, siteId).then(() => { + return this.courseProvider.getActivitiesCompletionStatus(courseId); + }).catch(() => { + // Ignore errors. + }); + } + }).then(() => { + // Sync finished, set sync time. + return this.setSyncTime(courseId, siteId); + }).then(() => { + // All done, return the data. + return result; + }); + + return this.addOngoingSync(courseId, syncPromise, siteId); + } +} From 15b4b6b8d12540d9975c673d6f18bc720aa67af8 Mon Sep 17 00:00:00 2001 From: dpalou Date: Wed, 3 Oct 2018 08:42:05 +0200 Subject: [PATCH 3/5] MOBILE-2061 sync: Fix errors displayed while syncing --- src/addon/mod/assign/providers/assign-sync.ts | 4 ++-- src/addon/mod/choice/providers/sync.ts | 2 +- src/addon/mod/data/providers/sync.ts | 4 ++-- src/addon/mod/feedback/providers/sync.ts | 4 ++-- src/addon/mod/forum/providers/sync.ts | 4 ++-- src/addon/mod/glossary/providers/sync.ts | 2 +- src/addon/mod/lesson/providers/lesson-sync.ts | 4 ++-- src/addon/mod/survey/providers/sync.ts | 4 ++-- src/addon/mod/wiki/providers/wiki-sync.ts | 2 +- src/addon/mod/workshop/providers/sync.ts | 12 ++++++------ src/core/course/providers/sync.ts | 2 +- src/core/courses/providers/course-link-handler.ts | 8 +++++--- src/core/login/pages/email-signup/email-signup.ts | 2 +- .../forgotten-password/forgotten-password.ts | 2 +- src/core/login/pages/site-policy/site-policy.ts | 4 ++-- src/core/login/providers/helper.ts | 4 ++-- src/providers/sites.ts | 2 +- src/providers/utils/dom.ts | 15 +++++---------- src/providers/utils/text.ts | 10 ++++++++++ 19 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/addon/mod/assign/providers/assign-sync.ts b/src/addon/mod/assign/providers/assign-sync.ts index ae518eb4b..368eba7de 100644 --- a/src/addon/mod/assign/providers/assign-sync.ts +++ b/src/addon/mod/assign/providers/assign-sync.ts @@ -308,7 +308,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { }).catch((error) => { if (error && this.utils.isWebServiceError(error)) { // A WebService has thrown an error, this means it cannot be submitted. Discard the submission. - discardError = error.message || error.error || error.content || error.body; + discardError = this.textUtils.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. return Promise.reject(error); @@ -402,7 +402,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { }).catch((error) => { if (error && this.utils.isWebServiceError(error)) { // The WebService has thrown an error, this means it cannot be submitted. Discard the offline data. - discardError = error.message || error.error || error.content || error.body; + discardError = this.textUtils.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. return Promise.reject(error); diff --git a/src/addon/mod/choice/providers/sync.ts b/src/addon/mod/choice/providers/sync.ts index 03d72e5cd..48efe6663 100644 --- a/src/addon/mod/choice/providers/sync.ts +++ b/src/addon/mod/choice/providers/sync.ts @@ -176,7 +176,7 @@ export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { component: this.componentTranslate, name: data.name, - error: error.error + error: this.textUtils.getErrorMessageFromError(error) })); }); } diff --git a/src/addon/mod/data/providers/sync.ts b/src/addon/mod/data/providers/sync.ts index e29f29ab6..37ac1e714 100644 --- a/src/addon/mod/data/providers/sync.ts +++ b/src/addon/mod/data/providers/sync.ts @@ -295,10 +295,10 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { promises.push(actionPromise.catch((error) => { if (error && error.wserror) { // The WebService has thrown an error, this means it cannot be performed. Discard. - discardError = error.error; + discardError = this.textUtils.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. - return Promise.reject(error && error.error); + return Promise.reject(error); } }).then(() => { // Delete the offline data. diff --git a/src/addon/mod/feedback/providers/sync.ts b/src/addon/mod/feedback/providers/sync.ts index 469090738..21d3b34f8 100644 --- a/src/addon/mod/feedback/providers/sync.ts +++ b/src/addon/mod/feedback/providers/sync.ts @@ -251,12 +251,12 @@ export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { component: this.componentTranslate, name: feedback.name, - error: error.error + error: this.textUtils.getErrorMessageFromError(error) })); }); } else { // Couldn't connect to server, reject. - return Promise.reject(error && error.error); + return Promise.reject(error); } }); } diff --git a/src/addon/mod/forum/providers/sync.ts b/src/addon/mod/forum/providers/sync.ts index 64b95f369..127843475 100644 --- a/src/addon/mod/forum/providers/sync.ts +++ b/src/addon/mod/forum/providers/sync.ts @@ -227,7 +227,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider { result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { component: this.componentTranslate, name: data.name, - error: error.error + error: this.textUtils.getErrorMessageFromError(error) })); }); } else { @@ -399,7 +399,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider { result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { component: this.componentTranslate, name: data.name, - error: error.error + error: this.textUtils.getErrorMessageFromError(error) })); }); } else { diff --git a/src/addon/mod/glossary/providers/sync.ts b/src/addon/mod/glossary/providers/sync.ts index c78e8920e..00350ef75 100644 --- a/src/addon/mod/glossary/providers/sync.ts +++ b/src/addon/mod/glossary/providers/sync.ts @@ -198,7 +198,7 @@ export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider { result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { component: this.componentTranslate, name: data.concept, - error: error.error + error: this.textUtils.getErrorMessageFromError(error) })); }); } else { diff --git a/src/addon/mod/lesson/providers/lesson-sync.ts b/src/addon/mod/lesson/providers/lesson-sync.ts index acefccfe8..5afb4f829 100644 --- a/src/addon/mod/lesson/providers/lesson-sync.ts +++ b/src/addon/mod/lesson/providers/lesson-sync.ts @@ -401,7 +401,7 @@ export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { component: this.componentTranslate, name: lesson.name, - error: error + error: this.textUtils.getErrorMessageFromError(error) })); }); } else { @@ -485,7 +485,7 @@ export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { component: this.componentTranslate, name: lesson.name, - error: error + error: this.textUtils.getErrorMessageFromError(error) })); }); } else { diff --git a/src/addon/mod/survey/providers/sync.ts b/src/addon/mod/survey/providers/sync.ts index 74b1f160d..ac3601d15 100644 --- a/src/addon/mod/survey/providers/sync.ts +++ b/src/addon/mod/survey/providers/sync.ts @@ -173,13 +173,13 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { component: this.componentTranslate, name: data.name, - error: error.error + error: this.textUtils.getErrorMessageFromError(error) })); }); } // Couldn't connect to server, reject. - return Promise.reject(error && error.error); + return Promise.reject(error); }); }).then(() => { if (courseId) { diff --git a/src/addon/mod/wiki/providers/wiki-sync.ts b/src/addon/mod/wiki/providers/wiki-sync.ts index e402174ae..a042a04c9 100644 --- a/src/addon/mod/wiki/providers/wiki-sync.ts +++ b/src/addon/mod/wiki/providers/wiki-sync.ts @@ -291,7 +291,7 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider { const warning = this.translate.instant('core.warningofflinedatadeleted', { component: this.translate.instant('addon.mod_wiki.wikipage'), name: page.title, - error: error + error: this.textUtils.getErrorMessageFromError(error) }); result.discarded.push({ diff --git a/src/addon/mod/workshop/providers/sync.ts b/src/addon/mod/workshop/providers/sync.ts index 050ead192..e0f37b92c 100644 --- a/src/addon/mod/workshop/providers/sync.ts +++ b/src/addon/mod/workshop/providers/sync.ts @@ -340,7 +340,7 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { }).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; + discardError = this.textUtils.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. return Promise.reject(error); @@ -421,7 +421,7 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { }).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; + discardError = this.textUtils.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. return Promise.reject(error); @@ -480,10 +480,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { 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; + discardError = this.textUtils.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. - return Promise.reject(error && error.error); + return Promise.reject(error); } }).then(() => { // Delete the offline data. @@ -539,10 +539,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { 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; + discardError = this.textUtils.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. - return Promise.reject(error && error.error); + return Promise.reject(error); } }).then(() => { // Delete the offline data. diff --git a/src/core/course/providers/sync.ts b/src/core/course/providers/sync.ts index 670040881..8a5334677 100644 --- a/src/core/course/providers/sync.ts +++ b/src/core/course/providers/sync.ts @@ -145,7 +145,7 @@ export class CoreCourseSyncProvider extends CoreSyncBaseProvider { // Responses deleted, add a warning. result.warnings.push(this.translate.instant('core.course.warningofflinemanualcompletiondeleted', { name: entry.coursename || courseId, - error: error.error + error: this.textUtils.getErrorMessageFromError(error) })); }); } diff --git a/src/core/courses/providers/course-link-handler.ts b/src/core/courses/providers/course-link-handler.ts index 4fa899f8f..092ac130d 100644 --- a/src/core/courses/providers/course-link-handler.ts +++ b/src/core/courses/providers/course-link-handler.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; import { CoreLoginHelperProvider } from '@core/login/providers/helper'; @@ -34,7 +35,8 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { constructor(private sitesProvider: CoreSitesProvider, private coursesProvider: CoreCoursesProvider, private loginHelper: CoreLoginHelperProvider, private domUtils: CoreDomUtilsProvider, - private translate: TranslateService, private courseProvider: CoreCourseProvider) { + private translate: TranslateService, private courseProvider: CoreCourseProvider, + private textUtils: CoreTextUtilsProvider) { super(); } @@ -150,7 +152,7 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { modal.dismiss(); if (error) { - error = error.message || error.error || error.content || error.body || error; + error = this.textUtils.getErrorMessageFromError(error) || error; } if (!error) { error = this.translate.instant('core.courses.notenroled'); @@ -232,7 +234,7 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { if (typeof password != 'undefined') { // The user attempted a password. Show an error message. - this.domUtils.showErrorModal(error.message); + this.domUtils.showErrorModal(error); } return this.domUtils.showPrompt(body, title, placeholder).then((password) => { diff --git a/src/core/login/pages/email-signup/email-signup.ts b/src/core/login/pages/email-signup/email-signup.ts index d48fdc609..314f38cb4 100644 --- a/src/core/login/pages/email-signup/email-signup.ts +++ b/src/core/login/pages/email-signup/email-signup.ts @@ -276,7 +276,7 @@ export class CoreLoginEmailSignupPage { } }); }).catch((error) => { - this.domUtils.showErrorModalDefault(error && error.error, 'core.login.usernotaddederror', true); + this.domUtils.showErrorModalDefault(error, 'core.login.usernotaddederror', true); }).finally(() => { modal.dismiss(); }); diff --git a/src/core/login/pages/forgotten-password/forgotten-password.ts b/src/core/login/pages/forgotten-password/forgotten-password.ts index 3aec2d353..2659c8126 100644 --- a/src/core/login/pages/forgotten-password/forgotten-password.ts +++ b/src/core/login/pages/forgotten-password/forgotten-password.ts @@ -70,7 +70,7 @@ export class CoreLoginForgottenPasswordPage { this.navCtrl.pop(); } }).catch((error) => { - this.domUtils.showErrorModal(error.error); + this.domUtils.showErrorModal(error); }).finally(() => { modal.dismiss(); }); diff --git a/src/core/login/pages/site-policy/site-policy.ts b/src/core/login/pages/site-policy/site-policy.ts index 30b56e58f..e97d54be4 100644 --- a/src/core/login/pages/site-policy/site-policy.ts +++ b/src/core/login/pages/site-policy/site-policy.ts @@ -88,7 +88,7 @@ export class CoreLoginSitePolicyPage { this.policyLoaded = true; }); }).catch((error) => { - this.domUtils.showErrorModalDefault(error && error.error, 'Error getting site policy.'); + this.domUtils.showErrorModalDefault(error, 'Error getting site policy.'); this.cancel(); }); } @@ -118,7 +118,7 @@ export class CoreLoginSitePolicyPage { return this.loginHelper.goToSiteInitialPage(); }); }).catch((error) => { - this.domUtils.showErrorModalDefault(error.message, 'Error accepting site policy.'); + this.domUtils.showErrorModalDefault(error, 'Error accepting site policy.'); }).finally(() => { modal.dismiss(); }); diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index a690bb6ec..079536ad1 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -1001,9 +1001,9 @@ export class CoreLoginHelperProvider { */ treatUserTokenError(siteUrl: string, error: any): void { if (error.errorcode == 'forcepasswordchangenotice') { - this.openChangePassword(siteUrl, error.error || error.message || error.body || error.content); + this.openChangePassword(siteUrl, this.textUtils.getErrorMessageFromError(error)); } else if (error.errorcode == 'legacymoodleversion') { - this.showLegacyNoticeModal(error.error); + this.showLegacyNoticeModal(this.textUtils.getErrorMessageFromError(error)); } else { this.domUtils.showErrorModal(error); } diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 01495727e..8ceaffac4 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -388,7 +388,7 @@ export class CoreSitesProvider { const promise = this.http.post(siteUrl + '/login/token.php', data).timeout(CoreConstants.WS_TIMEOUT).toPromise(); return promise.catch((error) => { - return Promise.reject(error.message); + return Promise.reject(error); }).then((data: any) => { if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) { return Promise.reject({ errorcode: data.errorcode, error: data.error }); diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index b64ec82fc..4ed2f7bee 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -1005,15 +1005,10 @@ export class CoreDomUtilsProvider { if (error.coreCanceled) { // It's a canceled error, don't display an error. return; - } else if (typeof error.content != 'undefined') { - error = error.content; - } else if (typeof error.body != 'undefined') { - error = error.body; - } else if (typeof error.message != 'undefined') { - error = error.message; - } else if (typeof error.error != 'undefined') { - error = error.error; - } else { + } + + error = this.textUtils.getErrorMessageFromError(error); + if (!error) { // No common properties found, just stringify it. error = JSON.stringify(error); extraInfo = ''; // No need to add extra info because it's already in the error. @@ -1058,7 +1053,7 @@ export class CoreDomUtilsProvider { let errorMessage = error; if (error && typeof error != 'string') { - errorMessage = error.message || error.error || error.content || error.body; + errorMessage = this.textUtils.getErrorMessageFromError(error); } return this.showErrorModal(typeof errorMessage == 'string' ? error : defaultError, needsTranslate, autocloseTime); diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts index 3ee32224e..7ae4cecbf 100644 --- a/src/providers/utils/text.ts +++ b/src/providers/utils/text.ts @@ -400,6 +400,16 @@ export class CoreTextUtilsProvider { }); } + /** + * Get the error message from an error object. + * + * @param {any} error Error object. + * @return {string} Error message, undefined if not found. + */ + getErrorMessageFromError(error: any): string { + return error && (error.message || error.error || error.content || error.body); + } + /** * Get the pluginfile URL to replace @@PLUGINFILE@@ wildcards. * From 1c6618b42aa56af62a5ae1b4bc214f96f040a2e8 Mon Sep 17 00:00:00 2001 From: dpalou Date: Thu, 4 Oct 2018 17:20:54 +0200 Subject: [PATCH 4/5] MOBILE-2061 core: Fix sync param JSDoc --- src/addon/mod/assign/components/index/index.ts | 2 +- src/addon/mod/chat/components/index/index.ts | 2 +- src/addon/mod/choice/components/index/index.ts | 2 +- src/addon/mod/data/components/index/index.ts | 2 +- src/addon/mod/feedback/components/index/index.ts | 2 +- src/addon/mod/lesson/components/index/index.ts | 2 +- src/addon/mod/lti/components/index/index.ts | 2 +- src/addon/mod/quiz/components/index/index.ts | 2 +- src/addon/mod/scorm/components/index/index.ts | 2 +- src/addon/mod/survey/components/index/index.ts | 2 +- src/addon/mod/wiki/components/index/index.ts | 2 +- src/addon/mod/workshop/components/index/index.ts | 2 +- src/core/course/pages/section/section.ts | 4 ++-- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/addon/mod/assign/components/index/index.ts b/src/addon/mod/assign/components/index/index.ts index d832848b2..d5945f7cc 100644 --- a/src/addon/mod/assign/components/index/index.ts +++ b/src/addon/mod/assign/components/index/index.ts @@ -136,7 +136,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo * Get assignment data. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/chat/components/index/index.ts b/src/addon/mod/chat/components/index/index.ts index 15f382a00..881411996 100644 --- a/src/addon/mod/chat/components/index/index.ts +++ b/src/addon/mod/chat/components/index/index.ts @@ -57,7 +57,7 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp * Download chat. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/choice/components/index/index.ts b/src/addon/mod/choice/components/index/index.ts index ae899d407..e8880dcbc 100644 --- a/src/addon/mod/choice/components/index/index.ts +++ b/src/addon/mod/choice/components/index/index.ts @@ -112,7 +112,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo * Download choice contents. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index f5007ca5a..d259527a6 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -154,7 +154,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp * Download data contents. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/feedback/components/index/index.ts b/src/addon/mod/feedback/components/index/index.ts index 74ac6e14d..16ad4b2f1 100644 --- a/src/addon/mod/feedback/components/index/index.ts +++ b/src/addon/mod/feedback/components/index/index.ts @@ -147,7 +147,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity * Download feedback contents. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/lesson/components/index/index.ts b/src/addon/mod/lesson/components/index/index.ts index f29f1a4ce..7bd329b8e 100644 --- a/src/addon/mod/lesson/components/index/index.ts +++ b/src/addon/mod/lesson/components/index/index.ts @@ -106,7 +106,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo * Get the lesson data. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/lti/components/index/index.ts b/src/addon/mod/lti/components/index/index.ts index d2520d35b..0f4e3d4ca 100644 --- a/src/addon/mod/lti/components/index/index.ts +++ b/src/addon/mod/lti/components/index/index.ts @@ -58,7 +58,7 @@ export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityCompo * Get the LTI data. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/quiz/components/index/index.ts b/src/addon/mod/quiz/components/index/index.ts index d0cb002a8..3ce3124c9 100644 --- a/src/addon/mod/quiz/components/index/index.ts +++ b/src/addon/mod/quiz/components/index/index.ts @@ -148,7 +148,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp * Get the quiz data. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/scorm/components/index/index.ts b/src/addon/mod/scorm/components/index/index.ts index fcb892d5b..85d1e567c 100644 --- a/src/addon/mod/scorm/components/index/index.ts +++ b/src/addon/mod/scorm/components/index/index.ts @@ -137,7 +137,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom * Get the SCORM data. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index ba18bbcff..bddf70a1b 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -93,7 +93,7 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo * Download survey contents. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/wiki/components/index/index.ts b/src/addon/mod/wiki/components/index/index.ts index b030bbb16..bca61171c 100644 --- a/src/addon/mod/wiki/components/index/index.ts +++ b/src/addon/mod/wiki/components/index/index.ts @@ -213,7 +213,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp * Get the wiki data. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts index 97f3a0df3..0d586f426 100644 --- a/src/addon/mod/workshop/components/index/index.ts +++ b/src/addon/mod/workshop/components/index/index.ts @@ -177,7 +177,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity * Download feedback contents. * * @param {boolean} [refresh=false] If it's refreshing content. - * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [sync=false] If it should try to sync. * @param {boolean} [showErrors=false] If show errors to the user of hide them. * @return {Promise} Promise resolved when done. */ diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index a1f902e16..991a67d93 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -160,7 +160,7 @@ export class CoreCourseSectionPage implements OnDestroy { * Fetch and load all the data required for the view. * * @param {boolean} [refresh] If it's refreshing content. - * @param {boolean} [sync] If the refresh is needs syncing. + * @param {boolean} [sync] If it should try to sync. * @return {Promise} Promise resolved when done. */ protected loadData(refresh?: boolean, sync?: boolean): Promise { @@ -342,7 +342,7 @@ export class CoreCourseSectionPage implements OnDestroy { /** * Refresh list after a completion change since there could be new activities. * - * @param {boolean} [sync] If the refresh is needs syncing. + * @param {boolean} [sync] If it should try to sync. */ protected refreshAfterCompletionChange(sync?: boolean): void { // Save scroll position to restore it once done. From 83ef6467c89726c765a85dc5c72dc25892c86ceb Mon Sep 17 00:00:00 2001 From: dpalou Date: Fri, 5 Oct 2018 10:10:33 +0200 Subject: [PATCH 5/5] MOBILE-2061 course: Don't sync completion if modified in the site --- scripts/langindex.json | 1 + src/core/course/lang/en.json | 1 + src/core/course/providers/course-offline.ts | 4 +- src/core/course/providers/course.ts | 78 ++++++++++----------- src/core/course/providers/sync.ts | 67 ++++++++++++------ 5 files changed, 87 insertions(+), 64 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 0c767ab98..db7b45180 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1113,6 +1113,7 @@ "core.course.overriddennotice": "grades", "core.course.sections": "moodle", "core.course.useactivityonbrowser": "local_moodlemobileapp", + "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", "core.coursedetails": "moodle", "core.courses.allowguests": "enrol_guest", diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json index 3b9028eba..0a2229b22 100644 --- a/src/core/course/lang/en.json +++ b/src/core/course/lang/en.json @@ -24,5 +24,6 @@ "refreshcourse": "Refresh course", "sections": "Sections", "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}}" } \ No newline at end of file diff --git a/src/core/course/providers/course-offline.ts b/src/core/course/providers/course-offline.ts index 869161e8e..fadb65463 100644 --- a/src/core/course/providers/course-offline.ts +++ b/src/core/course/providers/course-offline.ts @@ -45,7 +45,7 @@ export class CoreCourseOfflineProvider { type: 'TEXT' }, { - name: 'timecreated', + name: 'timecompleted', type: 'INTEGER' } ] @@ -131,7 +131,7 @@ export class CoreCourseOfflineProvider { completed: completed, courseid: courseId, coursename: courseName || '', - timecreated: Date.now() + timecompleted: Date.now() }; return site.getDb().insertRecord(CoreCourseOfflineProvider.MANUAL_COMPLETION_TABLE, entry); diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index 4e30f65fb..13ab87bd1 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -121,9 +121,14 @@ export class CoreCourseProvider { * @param {number} courseId Course ID. * @param {string} [siteId] Site ID. If not defined, current site. * @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} Promise resolved with the completion statuses: object where the key is module ID. */ - getActivitiesCompletionStatus(courseId: number, siteId?: string, userId?: number): Promise { + getActivitiesCompletionStatus(courseId: number, siteId?: string, userId?: number, forceCache: boolean = false, + ignoreCache: boolean = false, includeOffline: boolean = true): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); @@ -133,10 +138,17 @@ export class CoreCourseProvider { courseid: courseId, userid: userId }, - preSets = { + preSets: CoreSiteWSPreSets = { 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) => { if (data && data.statuses) { return this.utils.arrayToObject(data.statuses, 'cmid'); @@ -144,6 +156,10 @@ export class CoreCourseProvider { return Promise.reject(null); }).then((completionStatus) => { + if (!includeOffline) { + return completionStatus; + } + // Now get the offline completion (if any). return this.courseOffline.getCourseManualCompletions(courseId, site.id).then((offlineCompletions) => { offlineCompletions.forEach((offlineCompletion) => { @@ -686,51 +702,33 @@ export class CoreCourseProvider { 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 => { return this.courseOffline.markCompletedManually(cmId, completed, courseId, courseName, siteId); }; - // Check if we already have a completion stored. - return this.courseOffline.getManualCompletion(cmId, siteId).catch(() => { - // No completion stored. - }).then((entry) => { + // The offline function requires a courseId and it could be missing because it's a calculated field. + if (!this.appProvider.isOnline() && courseId) { + // App is offline, store the action. + return storeOffline(); + } - if (entry && completed != entry.completed) { - // It has changed, this means that the offline data can be deleted because the action was undone. - return this.courseOffline.deleteManualCompletion(cmId, siteId).then(() => { - return { - status: true, - offline: true - }; - }); - } - - if (!this.appProvider.isOnline() && courseId) { - // App is offline, store the action. + // Try to send it to server. + return this.markCompletedManuallyOnline(cmId, completed, siteId).then((result) => { + // Data sent to server, if there is some offline data delete it now. + return this.courseOffline.deleteManualCompletion(cmId, siteId).catch(() => { + // Ignore errors, shouldn't happen. + }).then(() => { + 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(); } - - 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(); - } - }); }); } diff --git a/src/core/course/providers/sync.ts b/src/core/course/providers/sync.ts index 8a5334677..87ebc23d8 100644 --- a/src/core/course/providers/sync.ts +++ b/src/core/course/providers/sync.ts @@ -128,34 +128,57 @@ export class CoreCourseSyncProvider extends CoreSyncBaseProvider { 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. - completions.forEach((entry) => { - promises.push(this.courseProvider.markCompletedManuallyOnline(entry.cmid, entry.completed, siteId).then(() => { - result.updated = true; + const 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; + // Send all the completions. + completions.forEach((entry) => { + const onlineComp = onlineCompletions[entry.cmid]; - return this.courseOffline.deleteManualCompletion(entry.cmid, siteId).then(() => { - // Responses deleted, add a warning. - result.warnings.push(this.translate.instant('core.course.warningofflinemanualcompletiondeleted', { - name: entry.coursename || courseId, - error: this.textUtils.getErrorMessageFromError(error) - })); - }); + // Check if the completion was modified in online. If so, discard it. + if (onlineComp && onlineComp.timecompleted * 1000 > entry.timecompleted) { + promises.push(this.courseOffline.deleteManualCompletion(entry.cmid, siteId).then(() => { + + // 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. - return Promise.reject(error); - })); - }); + promises.push(this.courseProvider.markCompletedManuallyOnline(entry.cmid, entry.completed, siteId).then(() => { + 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(() => { if (result.updated) { // Update data.