diff --git a/src/addon/mod/scorm/components/index/index.ts b/src/addon/mod/scorm/components/index/index.ts index ec881b25d..de725b272 100644 --- a/src/addon/mod/scorm/components/index/index.ts +++ b/src/addon/mod/scorm/components/index/index.ts @@ -61,7 +61,8 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom protected lastAttempt: number; // Last attempt. protected lastIsOffline: boolean; // Whether the last attempt is offline. protected hasPlayed = false; // Whether the user has opened the player page. - protected syncDueToPlayerLeft = false; // Whether a sync was due to the user leaving the player. + protected dataSentObserver; // To detect data sent to server. + protected dataSent = false; // Whether some data was sent to server while playing the SCORM. constructor(injector: Injector, protected scormProvider: AddonModScormProvider, @Optional() protected content: Content, protected scormHelper: AddonModScormHelperProvider, protected scormOffline: AddonModScormOfflineProvider, @@ -330,13 +331,12 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom * @return {boolean} If suceed or not. */ protected hasSyncSucceed(result: any): boolean { - if (result.updated || this.syncDueToPlayerLeft) { - // Check completion status if something was sent or the user just left the player. - // If the user plays the SCORM in online we don't know if he sent data or not, so always check completion. + if (result.updated || this.dataSent) { + // Check completion status if something was sent. this.checkCompletion(); } - this.syncDueToPlayerLeft = false; + this.dataSent = false; return true; } @@ -349,11 +349,13 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom if (this.hasPlayed) { this.hasPlayed = false; - this.syncDueToPlayerLeft = true; this.scormOptions.newAttempt = false; // Uncheck new attempt. // Add a delay to make sure the player has started the last writing calls so we can detect conflicts. setTimeout(() => { + this.dataSentObserver && this.dataSentObserver.off(); // Stop listening for changes. + this.dataSentObserver = undefined; + // Refresh data. this.showLoadingAndRefresh(true, false); }, 500); @@ -368,6 +370,15 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom if (this.navCtrl.getActive().component.name == 'AddonModScormPlayerPage') { this.hasPlayed = true; + + // Detect if anything was sent to server. + this.dataSentObserver && this.dataSentObserver.off(); + + this.dataSentObserver = this.eventsProvider.on(AddonModScormProvider.DATA_SENT_EVENT, (data) => { + if (data.scormId === this.scorm.id) { + this.dataSent = true; + } + }, this.siteId); } } @@ -533,6 +544,17 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom * @return {Promise} Promise resolved when done. */ protected sync(): Promise { - return this.scormSync.syncScorm(this.scorm); + return this.scormSync.syncScorm(this.scorm).then((result) => { + if (!result.updated && this.dataSent) { + // The user sent data to server, but not in the sync process. Check if we need to fetch data. + return this.scormSync.prefetchAfterUpdate(this.module, this.courseId).catch(() => { + // Ignore errors. + }).then(() => { + return result; + }); + } + + return result; + }); } } diff --git a/src/addon/mod/scorm/providers/scorm-offline.ts b/src/addon/mod/scorm/providers/scorm-offline.ts index 910628d46..b8cdc1d43 100644 --- a/src/addon/mod/scorm/providers/scorm-offline.ts +++ b/src/addon/mod/scorm/providers/scorm-offline.ts @@ -430,6 +430,8 @@ export class AddonModScormOfflineProvider { * @return {{[scoId: number]: string}} Launch URLs indexed by SCO ID. */ protected getLaunchUrlsFromScos(scos: any[]): {[scoId: number]: string} { + scos = scos || []; + const response = {}; scos.forEach((sco) => { @@ -487,12 +489,15 @@ export class AddonModScormOfflineProvider { * * @param {number} scormId SCORM ID. * @param {number} attempt Attempt number. - * @param {any[]} scos SCOs returned by AddonModScormProvider.getScos. + * @param {any[]} scos SCOs returned by AddonModScormProvider.getScos. If not supplied, this function will only return the + * SCOs that have something stored and cmi.launch_data will be undefined. * @param {string} [siteId] Site ID. If not defined, current site. * @param {number} [userId] User ID. If not defined use site's current user. * @return {Promise} Promise resolved when the user data is retrieved. */ getScormUserData(scormId: number, attempt: number, scos: any[], siteId?: string, userId?: number): Promise { + scos = scos || []; + let fullName = '', userName = ''; diff --git a/src/addon/mod/scorm/providers/scorm-sync.ts b/src/addon/mod/scorm/providers/scorm-sync.ts index 9a5d06ae7..e9b5014fb 100644 --- a/src/addon/mod/scorm/providers/scorm-sync.ts +++ b/src/addon/mod/scorm/providers/scorm-sync.ts @@ -23,6 +23,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreSyncBaseProvider } from '@classes/base-sync'; import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm'; import { AddonModScormOfflineProvider } from './scorm-offline'; @@ -63,9 +64,10 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, - courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, timeUtils: CoreTimeUtilsProvider, + private eventsProvider: CoreEventsProvider, timeUtils: CoreTimeUtilsProvider, private scormProvider: AddonModScormProvider, private scormOfflineProvider: AddonModScormOfflineProvider, - private prefetchHandler: AddonModScormPrefetchHandler, private utils: CoreUtilsProvider) { + private prefetchHandler: AddonModScormPrefetchHandler, private utils: CoreUtilsProvider, + private prefetchDelegate: CoreCourseModulePrefetchDelegate, private courseProvider: CoreCourseProvider) { super('AddonModScormSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); @@ -190,11 +192,11 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { let promise; if (updated) { - // Update the WS data. - promise = this.scormProvider.invalidateAllScormData(scorm.id, siteId).catch(() => { + // Update downloaded data. + promise = this.courseProvider.getModuleBasicInfoByInstance(scorm.id, 'scorm', siteId).then((module) => { + return this.prefetchAfterUpdate(module, scorm.course); + }).catch(() => { // Ignore errors. - }).then(() => { - return this.prefetchHandler.fetchWSData(scorm, siteId); }); } else { promise = Promise.resolve(); @@ -357,6 +359,31 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { }); } + /** + * Prefetch data after an update. It won't prefetch the data if the package file was updated. + * + * @param {any} module Module. + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetchAfterUpdate(module: any, courseId: number, siteId?: string): Promise { + // Get the module updates to check if the package was updated or not. + return this.prefetchDelegate.getModuleUpdates(module, courseId, true, siteId).then((result) => { + + if (result && result.updates) { + // Only prefetch if the package file hasn't changed. + const fileChanged = !!result.updates.find((entry) => { + return entry.name == 'packagefiles'; + }); + + if (!fileChanged) { + return this.prefetchHandler.download(module, courseId); + } + } + }); + } + /** * Save a snapshot from a synchronization. * diff --git a/src/addon/mod/scorm/providers/scorm.ts b/src/addon/mod/scorm/providers/scorm.ts index 6c3a7a151..201bdeaee 100644 --- a/src/addon/mod/scorm/providers/scorm.ts +++ b/src/addon/mod/scorm/providers/scorm.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; @@ -86,6 +87,7 @@ export class AddonModScormProvider { static LAUNCH_PREV_SCO_EVENT = 'addon_mod_scorm_launch_prev_sco'; static UPDATE_TOC_EVENT = 'addon_mod_scorm_update_toc'; static GO_OFFLINE_EVENT = 'addon_mod_scorm_go_offline'; + static DATA_SENT_EVENT = 'addon_mod_scorm_data_sent'; // Protected constants. protected VALID_STATUSES = ['notattempted', 'passed', 'completed', 'failed', 'incomplete', 'browsed', 'suspend']; @@ -110,7 +112,8 @@ export class AddonModScormProvider { constructor(logger: CoreLoggerProvider, private translate: TranslateService, private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private scormOfflineProvider: AddonModScormOfflineProvider, - private timeUtils: CoreTimeUtilsProvider, private syncProvider: CoreSyncProvider) { + private timeUtils: CoreTimeUtilsProvider, private syncProvider: CoreSyncProvider, + private eventsProvider: CoreEventsProvider) { this.logger = logger.getInstance('AddonModScormProvider'); } @@ -1483,6 +1486,12 @@ export class AddonModScormProvider { return this.saveTracksOnline(scorm.id, scoId, attempt, tracks, siteId).then(() => { // Tracks have been saved, update cached user data. this.updateUserDataAfterSave(scorm.id, attempt, tracks, siteId); + + this.eventsProvider.trigger(AddonModScormProvider.DATA_SENT_EVENT, { + scormId: scorm.id, + scoId: scoId, + attempt: attempt + }, this.sitesProvider.getCurrentSiteId()); }); } } @@ -1546,6 +1555,12 @@ export class AddonModScormProvider { if (success) { // Tracks have been saved, update cached user data. this.updateUserDataAfterSave(scorm.id, attempt, tracks); + + this.eventsProvider.trigger(AddonModScormProvider.DATA_SENT_EVENT, { + scormId: scorm.id, + scoId: scoId, + attempt: attempt + }, this.sitesProvider.getCurrentSiteId()); } return success; diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 17a8d9f3c..e69b04036 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -826,7 +826,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { } /** - * Get a module status and download time. It will only return the download time if the module is downloaded and not outdated. + * Get a module status and download time. It will only return the download time if the module is downloaded or outdated. * * @param {any} module Module. * @param {number} courseId Course ID the module belongs to. @@ -841,8 +841,8 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { const packageId = this.filepoolProvider.getPackageId(handler.component, module.id), status = this.statusCache.getValue(packageId, 'status'); - if (typeof status != 'undefined' && status != CoreConstants.DOWNLOADED) { - // Status is different than downloaded, just return the status. + if (typeof status != 'undefined' && status != CoreConstants.DOWNLOADED && status != CoreConstants.OUTDATED) { + // Module isn't downloaded, just return the status. return Promise.resolve({ status: status }); @@ -872,6 +872,75 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { }); } + /** + * Get updates for a certain module. + * It will only return the updates if the module can use check updates and it's downloaded or outdated. + * + * @param {any} module Module to check. + * @param {number} courseId Course the module belongs to. + * @param {boolean} [ignoreCache] True if 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 with the updates. + */ + getModuleUpdates(module: any, courseId: number, ignoreCache?: boolean, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + // Get the status and download time of the module. + return this.getModuleStatusAndDownloadTime(module, courseId).then((data) => { + if (data.status != CoreConstants.DOWNLOADED && data.status != CoreConstants.OUTDATED) { + // Not downloaded, no updates. + return {}; + } + + // Module is downloaded. Check if it can check updates. + return this.canModuleUseCheckUpdates(module, courseId).then((canUse) => { + if (!canUse) { + // Can't use check updates, no updates. + return {}; + } + + const params = { + courseid: courseId, + tocheck: [ + { + contextlevel: 'module', + id: module.id, + since: data.downloadTime || 0 + } + ] + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getModuleUpdatesCacheKey(courseId, module.id), + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('core_course_check_updates', params, preSets).then((response) => { + if (!response || !response.instances || !response.instances[0]) { + return Promise.reject(null); + } + + return response.instances[0]; + }); + }); + }); + }); + } + + /** + * Get cache key for module updates WS calls. + * + * @param {number} courseId Course ID. + * @param {number} moduleId Module ID. + * @return {string} Cache key. + */ + protected getModuleUpdatesCacheKey(courseId: number, moduleId: number): string { + return this.getCourseUpdatesCacheKey(courseId) + ':' + moduleId; + } + /** * Get a prefetch handler. * @@ -933,6 +1002,20 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { } } + /** + * Invalidate check updates WS call for a certain module. + * + * @param {number} courseId Course ID. + * @param {number} moduleId Module ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when data is invalidated. + */ + invalidateModuleUpdates(courseId: number, moduleId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getModuleUpdatesCacheKey(courseId, moduleId)); + }); + } + /** * Check if a list of modules is being downloaded. *