// (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 { CoreTimeUtilsProvider } from '@providers/utils/time'; 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, timeUtils: CoreTimeUtilsProvider) { super('CoreCourseSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); } /** * 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); } // Get the current completion status to check if any completion was modified in web. // This can be retrieved on core_course_get_contents since 3.6 but this is an easy way to get them. return this.courseProvider.getActivitiesCompletionStatus(courseId, siteId, undefined, false, true, false) .then((onlineCompletions) => { const promises = []; // Send all the completions. completions.forEach((entry) => { const onlineComp = onlineCompletions[entry.cmid]; // 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; } 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(() => { // 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. return this.courseProvider.invalidateSections(courseId, siteId).then(() => { if (this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.6')) { return this.courseProvider.getSections(courseId, false, true, undefined, siteId); } else { return this.courseProvider.getActivitiesCompletionStatus(courseId, siteId); } }).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); } }