From c6b51873f1251ccd108ee8052bad6e72116709e9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 28 Feb 2019 15:10:17 +0100 Subject: [PATCH] MOBILE-2850 quiz: Prefetch data after syncing quiz --- .../mod/quiz/providers/prefetch-handler.ts | 83 ++++++++++++++++--- src/addon/mod/quiz/providers/quiz-sync.ts | 57 +++++++++++-- src/addon/mod/quiz/providers/quiz.ts | 20 +++-- src/core/course/classes/activity-sync.ts | 2 +- 4 files changed, 137 insertions(+), 25 deletions(-) diff --git a/src/addon/mod/quiz/providers/prefetch-handler.ts b/src/addon/mod/quiz/providers/prefetch-handler.ts index 794cd9c5b..d292dc958 100644 --- a/src/addon/mod/quiz/providers/prefetch-handler.ts +++ b/src/addon/mod/quiz/providers/prefetch-handler.ts @@ -50,6 +50,21 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils); } + /** + * Download the module. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {string} [dirPath] Path of the directory where to store all the content files. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {boolean} [canStart=true] If true, start a new attempt if needed. + * @return {Promise} Promise resolved when all content is downloaded. + */ + download(module: any, courseId: number, dirPath?: string, single?: boolean, canStart: boolean = true): Promise { + // Same implementation for download and prefetch. + return this.prefetch(module, courseId, single, dirPath, canStart); + } + /** * Get list of files. If not defined, we'll assume they're in module.contents. * @@ -190,7 +205,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl const siteId = this.sitesProvider.getCurrentSiteId(); - return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quiz) => { + return this.quizProvider.getQuiz(courseId, module.id, false, false, siteId).then((quiz) => { if (quiz.allowofflineattempts !== 1 || quiz.hasquestions === 0) { return false; } @@ -220,10 +235,11 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl * @param {number} courseId Course ID the module belongs to. * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. * @param {string} [dirPath] Path of the directory where to store all the content files. + * @param {boolean} [canStart=true] If true, start a new attempt if needed. * @return {Promise} Promise resolved when done. */ - prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { - return this.prefetchPackage(module, courseId, single, this.prefetchQuiz.bind(this)); + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string, canStart: boolean = true): Promise { + return this.prefetchPackage(module, courseId, single, this.prefetchQuiz.bind(this), undefined, canStart); } /** @@ -233,9 +249,10 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl * @param {number} courseId Course ID the module belongs to. * @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section. * @param {String} siteId Site ID. + * @param {boolean} canStart If true, start a new attempt if needed. * @return {Promise} Promise resolved when done. */ - protected prefetchQuiz(module: any, courseId: number, single: boolean, siteId: string): Promise { + protected prefetchQuiz(module: any, courseId: number, single: boolean, siteId: string, canStart: boolean): Promise { let attempts: any[], startAttempt = false, quiz, @@ -244,7 +261,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl preflightData; // Get quiz. - return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quizData) => { + return this.quizProvider.getQuiz(courseId, module.id, false, true, siteId).then((quizData) => { quiz = quizData; const promises = [], @@ -272,7 +289,13 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl }).then(() => { // Check if we need to start a new attempt. let attempt = attempts[attempts.length - 1]; - if (!attempt || this.quizProvider.isAttemptFinished(attempt.state)) { + + if (!canStart && !attempt) { + // No attempts and we won't start a new one, so we don't need preflight data. + return; + } + + if (canStart && (!attempt || this.quizProvider.isAttemptFinished(attempt.state))) { // Check if the user can attempt the quiz. if (attemptAccessInfo.preventnewattemptreasons.length) { return Promise.reject(this.textUtils.buildMessage(attemptAccessInfo.preventnewattemptreasons)); @@ -331,6 +354,11 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl return Promise.all(promises); }).then(() => { + if (!canStart) { + // Nothing else to do. + return; + } + // If there's nothing to send, mark the quiz as synchronized. // We don't return the promises because it should be fast and we don't want to block the user for this. if (!this.syncProvider) { @@ -477,14 +505,49 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl return this.prefetchAttempt(quiz, lastAttempt, preflightData, siteId); } }).then(() => { - // Prefetch finished, get current status to determine if we need to change it. + // Prefetch finished, set the right status. + return this.setStatusAfterPrefetch(quiz, attempts, true, false, siteId); + }); + } + + /** + * Set the right status to a quiz after prefetching. + * If the last attempt is finished or there isn't one, set it as not downloaded to show download icon. + * + * @param {any} quiz Quiz. + * @param {any[]} [attempts] List of attempts. If not provided, they will be calculated. + * @param {boolean} [forceCache] Whether it should always return cached data. Only if attempts is undefined. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). Only if + * attempts is undefined. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + setStatusAfterPrefetch(quiz: any, attempts?: any[], forceCache?: boolean, ignoreCache?: boolean, siteId?: string) + : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + let status; + + if (!attempts) { + // Get the attempts. + promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, forceCache, ignoreCache, siteId).then((atts) => { + attempts = atts; + })); + } + + // Check the current status of the quiz. + promises.push(this.filepoolProvider.getPackageStatus(siteId, this.component, quiz.coursemodule).then((stat) => { + status = stat; + })); + + return Promise.all(promises).then(() => { - return this.filepoolProvider.getPackageStatus(siteId, this.component, quiz.coursemodule); - }).then((status) => { if (status !== CoreConstants.NOT_DOWNLOADED) { // Quiz was downloaded, set the new status. // If no attempts or last is finished we'll mark it as not downloaded to show download icon. - const isLastFinished = !lastAttempt || this.quizProvider.isAttemptFinished(lastAttempt.state), + const lastAttempt = attempts[attempts.length - 1], + isLastFinished = !lastAttempt || this.quizProvider.isAttemptFinished(lastAttempt.state), newStatus = isLastFinished ? CoreConstants.NOT_DOWNLOADED : CoreConstants.DOWNLOADED; return this.filepoolProvider.storePackageStatus(siteId, newStatus, this.component, quiz.coursemodule); diff --git a/src/addon/mod/quiz/providers/quiz-sync.ts b/src/addon/mod/quiz/providers/quiz-sync.ts index f70b29109..854fb8a83 100644 --- a/src/addon/mod/quiz/providers/quiz-sync.ts +++ b/src/addon/mod/quiz/providers/quiz-sync.ts @@ -23,9 +23,10 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreQuestionProvider } from '@core/question/providers/question'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { AddonModQuizProvider } from './quiz'; import { AddonModQuizOfflineProvider } from './quiz-offline'; import { AddonModQuizPrefetchHandler } from './prefetch-handler'; @@ -51,7 +52,7 @@ export interface AddonModQuizSyncResult { * Service to sync quizzes. */ @Injectable() -export class AddonModQuizSyncProvider extends CoreSyncBaseProvider { +export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_quiz_autom_synced'; @@ -59,13 +60,14 @@ export class AddonModQuizSyncProvider 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 quizProvider: AddonModQuizProvider, private quizOfflineProvider: AddonModQuizOfflineProvider, - private prefetchHandler: AddonModQuizPrefetchHandler, private questionProvider: CoreQuestionProvider, - private questionDelegate: CoreQuestionDelegate, private logHelper: CoreCourseLogHelperProvider) { + protected prefetchHandler: AddonModQuizPrefetchHandler, private questionProvider: CoreQuestionProvider, + private questionDelegate: CoreQuestionDelegate, private logHelper: CoreCourseLogHelperProvider, + prefetchDelegate: CoreCourseModulePrefetchDelegate, private courseProvider: CoreCourseProvider) { super('AddonModQuizSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('quiz'); } @@ -97,7 +99,11 @@ export class AddonModQuizSyncProvider extends CoreSyncBaseProvider { }).then(() => { if (updated) { // Data has been sent. Update prefetched data. - return this.prefetchHandler.prefetchQuizAndLastAttempt(quiz, false, siteId); + return this.courseProvider.getModuleBasicInfoByInstance(quiz.id, 'quiz', siteId).then((module) => { + return this.prefetchAfterUpdateQuiz(module, quiz, courseId, undefined, siteId); + }).catch(() => { + // Ignore errors. + }); } }).then(() => { return this.setSyncTime(quiz.id, siteId).catch(() => { @@ -145,6 +151,41 @@ export class AddonModQuizSyncProvider extends CoreSyncBaseProvider { }); } + /** + * Conveniece function to prefetch data after an update. + * + * @param {any} module Module. + * @param {any} quiz Quiz. + * @param {number} courseId Course ID. + * @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. + */ + prefetchAfterUpdateQuiz(module: any, quiz: any, courseId: number, regex?: RegExp, siteId?: string): Promise { + regex = regex || /^.*files$/; + + let shouldDownload; + + // Get the module updates to check if the data was updated or not. + return this.prefetchDelegate.getModuleUpdates(module, courseId, true, siteId).then((result) => { + + if (result && result.updates && result.updates.length > 0) { + // Only prefetch if files haven't changed. + shouldDownload = !result.updates.find((entry) => { + return entry.name.match(regex); + }); + + if (shouldDownload) { + return this.prefetchHandler.download(module, courseId, undefined, false, false); + } + } + + }).then(() => { + // Prefetch finished or not needed, set the right status. + return this.prefetchHandler.setStatusAfterPrefetch(quiz, undefined, shouldDownload, false, siteId); + }); + } + /** * Try to synchronize all the quizzes in a certain site or in all sites. * @@ -185,7 +226,7 @@ export class AddonModQuizSyncProvider extends CoreSyncBaseProvider { if (!this.syncProvider.isBlocked(AddonModQuizProvider.COMPONENT, quiz.id, siteId)) { // Quiz not blocked, try to synchronize it. - promises.push(this.quizProvider.getQuizById(quiz.courseid, quiz.id, false, siteId).then((quiz) => { + promises.push(this.quizProvider.getQuizById(quiz.courseid, quiz.id, false, false, siteId).then((quiz) => { return this.syncQuizIfNeeded(quiz, false, siteId).then((data) => { if (data && data.warnings && data.warnings.length) { // Store the warnings to show them when the user opens the quiz. diff --git a/src/addon/mod/quiz/providers/quiz.ts b/src/addon/mod/quiz/providers/quiz.ts index 551675b22..8861537f6 100644 --- a/src/addon/mod/quiz/providers/quiz.ts +++ b/src/addon/mod/quiz/providers/quiz.ts @@ -672,10 +672,13 @@ export class AddonModQuizProvider { * @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 Quiz is retrieved. */ - protected getQuizByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string): Promise { + protected getQuizByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] @@ -686,6 +689,9 @@ export class AddonModQuizProvider { if (forceCache) { preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; } return site.read('mod_quiz_get_quizzes_by_courses', params, preSets).then((response) => { @@ -710,11 +716,12 @@ export class AddonModQuizProvider { * @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 quiz is retrieved. */ - getQuiz(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise { - return this.getQuizByField(courseId, 'coursemodule', cmId, forceCache, siteId); + getQuiz(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.getQuizByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); } /** @@ -723,11 +730,12 @@ export class AddonModQuizProvider { * @param {number} courseId Course ID. * @param {number} id Quiz 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 quiz is retrieved. */ - getQuizById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise { - return this.getQuizByField(courseId, 'id', id, forceCache, siteId); + getQuizById(courseId: number, id: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.getQuizByField(courseId, 'id', id, forceCache, ignoreCache, siteId); } /** @@ -1223,7 +1231,7 @@ export class AddonModQuizProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Get required data to call the invalidate functions. - return this.getQuiz(courseId, moduleId, false, siteId).then((quiz) => { + return this.getQuiz(courseId, moduleId, true, false, siteId).then((quiz) => { return this.getUserAttempts(quiz.id, 'all', true, false, false, siteId).then((attempts) => { // Now invalidate it. const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined; diff --git a/src/core/course/classes/activity-sync.ts b/src/core/course/classes/activity-sync.ts index 3a4cb3e8d..8d7333963 100644 --- a/src/core/course/classes/activity-sync.ts +++ b/src/core/course/classes/activity-sync.ts @@ -38,7 +38,7 @@ export class CoreCourseActivitySyncBaseProvider extends CoreSyncBaseProvider { } /** - * Conveniece function to refetch data after an update. + * Conveniece function to prefetch data after an update. * * @param {any} module Module. * @param {number} courseId Course ID.