Merge pull request #1743 from dpalou/MOBILE-2114

Mobile 2114
main
Juan Leyva 2019-01-30 12:36:01 +01:00 committed by GitHub
commit 92419e2799
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 170 additions and 18 deletions

View File

@ -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<any>} Promise resolved when done.
*/
protected sync(): Promise<any> {
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;
});
}
}

View File

@ -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<any>} Promise resolved when the user data is retrieved.
*/
getScormUserData(scormId: number, attempt: number, scos: any[], siteId?: string, userId?: number): Promise<any> {
scos = scos || [];
let fullName = '',
userName = '';

View File

@ -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<any>} Promise resolved when done.
*/
prefetchAfterUpdate(module: any, courseId: number, siteId?: string): Promise<any> {
// 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.
*

View File

@ -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;

View File

@ -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<any>} Promise resolved with the updates.
*/
getModuleUpdates(module: any, courseId: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
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<any>} Promise resolved when data is invalidated.
*/
invalidateModuleUpdates(courseId: number, moduleId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getModuleUpdatesCacheKey(courseId, moduleId));
});
}
/**
* Check if a list of modules is being downloaded.
*