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. *