diff --git a/src/addon/mod/feedback/providers/sync.ts b/src/addon/mod/feedback/providers/sync.ts index b555caa1f..dc447d944 100644 --- a/src/addon/mod/feedback/providers/sync.ts +++ b/src/addon/mod/feedback/providers/sync.ts @@ -53,6 +53,21 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv this.componentTranslate = courseProvider.translateModuleName('feedback'); } + /** + * Conveniece function to prefetch data after an update. + * + * @param {any} module Module. + * @param {number} courseId Course ID. + * @param {RegExp} [regex] If regex matches, don't download the data. Defaults to check files and timers. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetchAfterUpdate(module: any, courseId: number, regex?: RegExp, siteId?: string): Promise { + regex = regex || /^.*files$|^timers/; + + return super.prefetchAfterUpdate(module, courseId, regex, siteId); + } + /** * Try to synchronize all the feedbacks in a certain site or in all sites. * diff --git a/src/addon/mod/lesson/components/index/index.ts b/src/addon/mod/lesson/components/index/index.ts index ada4eded7..0f98b9444 100644 --- a/src/addon/mod/lesson/components/index/index.ts +++ b/src/addon/mod/lesson/components/index/index.ts @@ -61,6 +61,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo protected accessInfo: any; // Lesson access info. protected password: string; // The password for the lesson. protected hasPlayed: boolean; // Whether the user has gone to the lesson player (attempted). + protected dataSentObserver; // To detect data sent to server. + protected dataSent = false; // Whether some data was sent to server while playing the lesson. constructor(injector: Injector, protected lessonProvider: AddonModLessonProvider, @Optional() content: Content, protected groupsProvider: CoreGroupsProvider, protected lessonOffline: AddonModLessonOfflineProvider, @@ -228,6 +230,13 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo * @return {boolean} If suceed or not. */ protected hasSyncSucceed(result: any): boolean { + if (result.updated || this.dataSent) { + // Check completion status if something was sent. + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); + } + + this.dataSent = false; + return result.updated; } @@ -243,6 +252,10 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo if (this.hasPlayed) { this.hasPlayed = false; + this.dataSentObserver && this.dataSentObserver.off(); // Stop listening for changes. + this.dataSentObserver = undefined; + + // Refresh data. this.showLoadingAndRefresh(true, false); } } @@ -257,6 +270,16 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo if (this.navCtrl.getActive().component.name == 'AddonModLessonPlayerPage') { this.hasPlayed = true; + + // Detect if anything was sent to server. + this.dataSentObserver && this.dataSentObserver.off(); + + this.dataSentObserver = this.eventsProvider.on(AddonModLessonProvider.DATA_SENT_EVENT, (data) => { + // Ignore launch sending because it only affects timers. + if (data.lessonId === this.lesson.id && data.type != 'launch') { + this.dataSent = true; + } + }, this.siteId); } } @@ -556,7 +579,18 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo * @return {Promise} Promise resolved when done. */ protected sync(): Promise { - return this.lessonSync.syncLesson(this.lesson.id, true); + return this.lessonSync.syncLesson(this.lesson.id, true).then((result) => { + if (!result.updated && this.dataSent && this.isPrefetched()) { + // The user sent data to server, but not in the sync process. Check if we need to fetch data. + return this.lessonSync.prefetchAfterUpdate(this.module, this.courseId).catch(() => { + // Ignore errors. + }).then(() => { + return result; + }); + } + + return result; + }); } /** @@ -575,4 +609,13 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo return Promise.reject(error); }); } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + + this.dataSentObserver && this.dataSentObserver.off(); + } } diff --git a/src/addon/mod/lesson/providers/lesson-sync.ts b/src/addon/mod/lesson/providers/lesson-sync.ts index 68b741f94..d4c183285 100644 --- a/src/addon/mod/lesson/providers/lesson-sync.ts +++ b/src/addon/mod/lesson/providers/lesson-sync.ts @@ -25,7 +25,8 @@ import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { AddonModLessonProvider } from './lesson'; import { AddonModLessonOfflineProvider } from './lesson-offline'; import { AddonModLessonPrefetchHandler } from './prefetch-handler'; @@ -51,7 +52,7 @@ export interface AddonModLessonSyncResult { * Service to sync lesson. */ @Injectable() -export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { +export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_lesson_autom_synced'; @@ -92,12 +93,12 @@ export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, private lessonProvider: AddonModLessonProvider, private lessonOfflineProvider: AddonModLessonOfflineProvider, - private prefetchHandler: AddonModLessonPrefetchHandler, timeUtils: CoreTimeUtilsProvider, + protected prefetchHandler: AddonModLessonPrefetchHandler, timeUtils: CoreTimeUtilsProvider, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, - private logHelper: CoreCourseLogHelperProvider) { + private logHelper: CoreCourseLogHelperProvider, prefetchDelegate: CoreCourseModulePrefetchDelegate) { super('AddonModLessonSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('lesson'); @@ -288,7 +289,7 @@ export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { courseId = attempts[0].courseid; // Get the info, access info and the lesson password if needed. - return this.lessonProvider.getLessonById(courseId, lessonId, false, siteId).then((lessonData) => { + return this.lessonProvider.getLessonById(courseId, lessonId, false, false, siteId).then((lessonData) => { lesson = lessonData; return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId); @@ -364,7 +365,7 @@ export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { // Data already retrieved when syncing attempts. promise = Promise.resolve(); } else { - promise = this.lessonProvider.getLessonById(courseId, lessonId, false, siteId).then((lessonData) => { + promise = this.lessonProvider.getLessonById(courseId, lessonId, false, false, siteId).then((lessonData) => { lesson = lessonData; return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId); @@ -429,31 +430,9 @@ export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { }); }).then(() => { if (result.updated && courseId) { - // Data has been sent to server. Now invalidate the WS calls. - const promises = []; - - promises.push(this.lessonProvider.invalidateAccessInformation(lessonId, siteId)); - promises.push(this.lessonProvider.invalidateContentPagesViewed(lessonId, siteId)); - promises.push(this.lessonProvider.invalidateQuestionsAttempts(lessonId, siteId)); - promises.push(this.lessonProvider.invalidatePagesPossibleJumps(lessonId, siteId)); - promises.push(this.lessonProvider.invalidateTimers(lessonId, siteId)); - - return this.utils.allPromises(promises).catch(() => { + // Data has been sent to server, update data. + return this.prefetchAfterUpdate(module, courseId, undefined, siteId).catch(() => { // Ignore errors. - }).then(() => { - // Sync successful, update some data that might have been modified. - return this.lessonProvider.getAccessInformation(lessonId, false, false, siteId).then((info) => { - const promises = [], - retake = info.attemptscount; - - promises.push(this.lessonProvider.getContentPagesViewedOnline(lessonId, retake, false, false, siteId)); - promises.push(this.lessonProvider.getQuestionsAttemptsOnline(lessonId, retake, false, undefined, false, - false, siteId)); - - return Promise.all(promises); - }).catch(() => { - // Ignore errors. - }); }); } }).then(() => { diff --git a/src/addon/mod/lesson/providers/lesson.ts b/src/addon/mod/lesson/providers/lesson.ts index e39b119cf..5441b3f3a 100644 --- a/src/addon/mod/lesson/providers/lesson.ts +++ b/src/addon/mod/lesson/providers/lesson.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -113,6 +114,7 @@ export interface AddonModLessonGrade { @Injectable() export class AddonModLessonProvider { static COMPONENT = 'mmaModLesson'; + static DATA_SENT_EVENT = 'addon_mod_lesson_data_sent'; // This page. static LESSON_THISPAGE = 0; @@ -186,7 +188,8 @@ export class AddonModLessonProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider, - private lessonOfflineProvider: AddonModLessonOfflineProvider, private logHelper: CoreCourseLogHelperProvider) { + private lessonOfflineProvider: AddonModLessonOfflineProvider, private logHelper: CoreCourseLogHelperProvider, + private eventsProvider: CoreEventsProvider) { this.logger = logger.getInstance('AddonModLessonProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); @@ -1087,7 +1090,17 @@ export class AddonModLessonProvider { }); } - return this.finishRetakeOnline(lesson.id, password, outOfTime, review, siteId); + return this.finishRetakeOnline(lesson.id, password, outOfTime, review, siteId).then((response) => { + this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, { + lessonId: lesson.id, + type: 'finish', + courseId: courseId, + outOfTime: outOfTime, + review: review + }, this.sitesProvider.getCurrentSiteId()); + + return response; + }); } /** @@ -1363,11 +1376,12 @@ export class AddonModLessonProvider { * @param {number} courseId Course ID. * @param {number} cmid Course module ID. * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the lesson is retrieved. */ - getLesson(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise { - return this.getLessonByField(courseId, 'coursemodule', cmId, forceCache, siteId); + getLesson(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.getLessonByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); } /** @@ -1377,10 +1391,12 @@ export class AddonModLessonProvider { * @param {string} key Name of the property to check. * @param {any} value Value to search. * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the lesson is retrieved. */ - protected getLessonByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string): Promise { + protected getLessonByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { @@ -1392,6 +1408,9 @@ export class AddonModLessonProvider { if (forceCache) { preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; } return site.read('mod_lesson_get_lessons_by_courses', params, preSets).then((response) => { @@ -1416,11 +1435,12 @@ export class AddonModLessonProvider { * @param {number} courseId Course ID. * @param {number} id Lesson ID. * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the lesson is retrieved. */ - getLessonById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise { - return this.getLessonByField(courseId, 'id', id, forceCache, siteId); + getLessonById(courseId: number, id: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.getLessonByField(courseId, 'id', id, forceCache, ignoreCache, siteId); } /** @@ -2758,7 +2778,14 @@ export class AddonModLessonProvider { params.pageid = pageId; } - return site.write('mod_lesson_launch_attempt', params); + return site.write('mod_lesson_launch_attempt', params).then((response) => { + this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, { + lessonId: id, + type: 'launch' + }, this.sitesProvider.getCurrentSiteId()); + + return response; + }); }); } @@ -3028,7 +3055,17 @@ export class AddonModLessonProvider { }); } - return this.processPageOnline(lesson.id, pageId, data, password, review, siteId); + return this.processPageOnline(lesson.id, pageId, data, password, review, siteId).then((response) => { + this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, { + lessonId: lesson.id, + type: 'process', + courseId: courseId, + pageId: pageId, + review: review + }, this.sitesProvider.getCurrentSiteId()); + + return response; + }); } /** diff --git a/src/addon/mod/lesson/providers/prefetch-handler.ts b/src/addon/mod/lesson/providers/prefetch-handler.ts index 17de0a1dd..3660a90cd 100644 --- a/src/addon/mod/lesson/providers/prefetch-handler.ts +++ b/src/addon/mod/lesson/providers/prefetch-handler.ts @@ -83,7 +83,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan password, result; - return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lessonData) => { + return this.lessonProvider.getLesson(courseId, module.id, false, false, siteId).then((lessonData) => { lesson = lessonData; // Get the lesson password if it's needed. @@ -190,7 +190,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan const siteId = this.sitesProvider.getCurrentSiteId(); // Invalidate data to determine if module is downloadable. - return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lesson) => { + return this.lessonProvider.getLesson(courseId, module.id, true, false, siteId).then((lesson) => { const promises = []; promises.push(this.lessonProvider.invalidateLessonData(courseId, siteId)); @@ -210,7 +210,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan isDownloadable(module: any, courseId: number): boolean | Promise { const siteId = this.sitesProvider.getCurrentSiteId(); - return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lesson) => { + return this.lessonProvider.getLesson(courseId, module.id, false, false, siteId).then((lesson) => { if (!this.lessonProvider.isLessonOffline(lesson)) { return false; } @@ -260,7 +260,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan password, accessInfo; - return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lessonData) => { + return this.lessonProvider.getLesson(courseId, module.id, false, true, siteId).then((lessonData) => { lesson = lessonData; // Get the lesson password if it's needed. @@ -360,7 +360,8 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan if (accessInfo.canviewreports) { // Prefetch reports data. - promises.push(this.groupsProvider.getActivityAllowedGroupsIfEnabled(module.id, undefined, siteId).then((groups) => { + promises.push(this.groupsProvider.getActivityAllowedGroupsIfEnabled(module.id, undefined, siteId, true) + .then((groups) => { const subPromises = []; groups.forEach((group) => { diff --git a/src/core/course/classes/activity-sync.ts b/src/core/course/classes/activity-sync.ts index 287706f9b..3a4cb3e8d 100644 --- a/src/core/course/classes/activity-sync.ts +++ b/src/core/course/classes/activity-sync.ts @@ -42,7 +42,7 @@ export class CoreCourseActivitySyncBaseProvider extends CoreSyncBaseProvider { * * @param {any} module Module. * @param {number} courseId Course ID. - * @param {RegExp} [regex] RegExp to check if it should download data. Defaults to check files. + * @param {RegExp} [regex] If regex matches, don't download the data. Defaults to check files. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when done. */ @@ -54,11 +54,11 @@ export class CoreCourseActivitySyncBaseProvider extends CoreSyncBaseProvider { if (result && result.updates && result.updates.length > 0) { // Only prefetch if files haven't changed. - const fileChanged = !!result.updates.find((entry) => { + const shouldDownload = !result.updates.find((entry) => { return entry.name.match(regex); }); - if (!fileChanged) { + if (shouldDownload) { return this.prefetchHandler.download(module, courseId); } } diff --git a/src/providers/groups.ts b/src/providers/groups.ts index e5bbea314..f2ce0b146 100644 --- a/src/providers/groups.ts +++ b/src/providers/groups.ts @@ -124,16 +124,17 @@ export class CoreGroupsProvider { * @param {number} cmId Course module ID. * @param {number} [userId] User ID. If not defined, use current user. * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the groups are retrieved. If not allowed, empty array will be returned. */ - getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string): Promise { + getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Get real groupmode, in case it's forced by the course. - return this.activityHasGroups(cmId, siteId).then((hasGroups) => { + return this.activityHasGroups(cmId, siteId, ignoreCache).then((hasGroups) => { if (hasGroups) { // Get the groups available for the user. - return this.getActivityAllowedGroups(cmId, userId, siteId); + return this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache); } return []; @@ -147,7 +148,7 @@ export class CoreGroupsProvider { * @param {boolean} [addAllParts=true] Whether to add the all participants option. Always true for visible groups. * @param {number} [userId] User ID. If not defined, use current user. * @param {string} [siteId] Site ID. If not defined, current site. - * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved with the group info. */ getActivityGroupInfo(cmId: number, addAllParts: boolean = true, userId?: number, siteId?: string, ignoreCache?: boolean)