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