MOBILE-2348 quiz: Implement prefetch handler
This commit is contained in:
		
							parent
							
								
									02f25f50ac
								
							
						
					
					
						commit
						32c66f222f
					
				
							
								
								
									
										448
									
								
								src/addon/mod/quiz/providers/prefetch-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										448
									
								
								src/addon/mod/quiz/providers/prefetch-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,448 @@ | |||||||
|  | // (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, Injector } from '@angular/core'; | ||||||
|  | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
|  | import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; | ||||||
|  | import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; | ||||||
|  | import { AddonModQuizProvider } from './quiz'; | ||||||
|  | import { AddonModQuizHelperProvider } from './helper'; | ||||||
|  | import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate'; | ||||||
|  | import { AddonModQuizSyncProvider } from './quiz-sync'; | ||||||
|  | import { CoreConstants } from '@core/constants'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handler to prefetch quizzes. | ||||||
|  |  */ | ||||||
|  | @Injectable() | ||||||
|  | export class AddonModQuizPrefetchHandler extends CoreCourseModulePrefetchHandlerBase { | ||||||
|  |     name = 'AddonModQuiz'; | ||||||
|  |     modName = 'quiz'; | ||||||
|  |     component = AddonModQuizProvider.COMPONENT; | ||||||
|  |     updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^questions$|^attempts$/; | ||||||
|  | 
 | ||||||
|  |     protected syncProvider: AddonModQuizSyncProvider; // It will be injected later to prevent circular dependencies.
 | ||||||
|  | 
 | ||||||
|  |     constructor(protected injector: Injector, protected quizProvider: AddonModQuizProvider, | ||||||
|  |             protected textUtils: CoreTextUtilsProvider, protected quizHelper: AddonModQuizHelperProvider, | ||||||
|  |             protected accessRuleDelegate: AddonModQuizAccessRuleDelegate, protected questionHelper: CoreQuestionHelperProvider) { | ||||||
|  |         super(injector); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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. @see downloadOrPrefetch. | ||||||
|  |      * @return {Promise<any>} Promise resolved when all content is downloaded. | ||||||
|  |      */ | ||||||
|  |     download(module: any, courseId: number, dirPath?: string): Promise<any> { | ||||||
|  |         // Same implementation for download or prefetch.
 | ||||||
|  |         return this.prefetch(module, courseId, false, dirPath); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the download size of a module. | ||||||
|  |      * | ||||||
|  |      * @param {any} module Module. | ||||||
|  |      * @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. | ||||||
|  |      * @return {Promise<{size: number, total: boolean}>} Promise resolved with the size and a boolean indicating if it was able | ||||||
|  |      *                                                   to calculate the total size. | ||||||
|  |      */ | ||||||
|  |     getDownloadSize(module: any, courseId: any, single?: boolean): Promise<{ size: number, total: boolean }> { | ||||||
|  |         return Promise.resolve({ | ||||||
|  |             size: -1, | ||||||
|  |             total: false | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get list of files. If not defined, we'll assume they're in module.contents. | ||||||
|  |      * | ||||||
|  |      * @param {any} module Module. | ||||||
|  |      * @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. | ||||||
|  |      * @return {Promise<any[]>} Promise resolved with the list of files. | ||||||
|  |      */ | ||||||
|  |     getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> { | ||||||
|  |         return Promise.resolve([]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gather some preflight data for an attempt. This function will start a new attempt if needed. | ||||||
|  |      * | ||||||
|  |      * @param {any} quiz Quiz. | ||||||
|  |      * @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation. | ||||||
|  |      * @param {any} [attempt] Attempt to continue. Don't pass any value if the user needs to start a new attempt. | ||||||
|  |      * @param {boolean} [askPreflight] Whether it should ask for preflight data if needed. | ||||||
|  |      * @param {string} [modalTitle] Lang key of the title to set to preflight modal (e.g. 'addon.mod_quiz.startattempt'). | ||||||
|  |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|  |      * @return {Promise<any>} Promise resolved with the preflight data. | ||||||
|  |      */ | ||||||
|  |     getPreflightData(quiz: any, accessInfo: any, attempt?: any, askPreflight?: boolean, title?: string, siteId?: string) | ||||||
|  |             : Promise<any> { | ||||||
|  |         const preflightData = {}; | ||||||
|  |         let promise; | ||||||
|  | 
 | ||||||
|  |         if (askPreflight) { | ||||||
|  |             // We can ask preflight, check if it's needed and get the data.
 | ||||||
|  |             promise = this.quizHelper.getAndCheckPreflightData( | ||||||
|  |                     quiz, accessInfo, preflightData, attempt, false, true, title, siteId); | ||||||
|  |         } else { | ||||||
|  |             // Get some fixed preflight data from access rules (data that doesn't require user interaction).
 | ||||||
|  |             const rules = accessInfo.activerulenames; | ||||||
|  | 
 | ||||||
|  |             promise = this.accessRuleDelegate.getFixedPreflightData(rules, quiz, preflightData, attempt, true, siteId).then(() => { | ||||||
|  |                 if (!attempt) { | ||||||
|  |                     // We need to create a new attempt.
 | ||||||
|  |                     return this.quizProvider.startAttempt(quiz.id, preflightData, false, siteId); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return promise.then(() => { | ||||||
|  |             return preflightData; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Invalidate the prefetched content. | ||||||
|  |      * | ||||||
|  |      * @param {number} moduleId The module ID. | ||||||
|  |      * @param {number} courseId The course ID the module belongs to. | ||||||
|  |      * @return {Promise<any>} Promise resolved when the data is invalidated. | ||||||
|  |      */ | ||||||
|  |     invalidateContent(moduleId: number, courseId: number): Promise<any> { | ||||||
|  |         return this.quizProvider.invalidateContent(moduleId, courseId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Invalidate WS calls needed to determine module status. | ||||||
|  |      * | ||||||
|  |      * @param {any} module Module. | ||||||
|  |      * @param {number} courseId Course ID the module belongs to. | ||||||
|  |      * @return {Promise<any>} Promise resolved when invalidated. | ||||||
|  |      */ | ||||||
|  |     invalidateModule(module: any, courseId: number): Promise<any> { | ||||||
|  |         // Invalidate the calls required to check if a quiz is downloadable.
 | ||||||
|  |         const promises = []; | ||||||
|  | 
 | ||||||
|  |         promises.push(this.quizProvider.invalidateQuizData(courseId)); | ||||||
|  |         promises.push(this.quizProvider.invalidateUserAttemptsForUser(module.instance)); | ||||||
|  | 
 | ||||||
|  |         return Promise.all(promises); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable. | ||||||
|  |      * | ||||||
|  |      * @param {any} module Module. | ||||||
|  |      * @param {number} courseId Course ID the module belongs to. | ||||||
|  |      * @return {boolean|Promise<boolean>} Whether the module can be downloaded. The promise should never be rejected. | ||||||
|  |      */ | ||||||
|  |     isDownloadable(module: any, courseId: number): boolean | Promise<boolean> { | ||||||
|  |         const siteId = this.sitesProvider.getCurrentSiteId(); | ||||||
|  | 
 | ||||||
|  |         return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quiz) => { | ||||||
|  |             if (quiz.allowofflineattempts !== 1 || quiz.hasquestions === 0) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Not downloadable if we reached max attempts or the quiz has an unfinished attempt.
 | ||||||
|  |             return this.quizProvider.getUserAttempts(quiz.id, undefined, true, false, false, siteId).then((attempts) => { | ||||||
|  |                 const isLastFinished = !attempts.length || this.quizProvider.isAttemptFinished(attempts[attempts.length - 1].state); | ||||||
|  | 
 | ||||||
|  |                 return quiz.attempts === 0 || quiz.attempts > attempts.length || !isLastFinished; | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Whether or not the handler is enabled on a site level. | ||||||
|  |      * | ||||||
|  |      * @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. | ||||||
|  |      */ | ||||||
|  |     isEnabled(): boolean | Promise<boolean> { | ||||||
|  |         return this.quizProvider.isPluginEnabled(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Prefetch a module. | ||||||
|  |      * | ||||||
|  |      * @param {any} module Module. | ||||||
|  |      * @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. @see downloadOrPrefetch. | ||||||
|  |      * @return {Promise<any>} Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> { | ||||||
|  |         return this.prefetchPackage(module, courseId, single, this.prefetchQuiz.bind(this)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Prefetch a quiz. | ||||||
|  |      * | ||||||
|  |      * @param {any} module Module. | ||||||
|  |      * @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. | ||||||
|  |      * @return {Promise<any>} Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     protected prefetchQuiz(module: any, courseId: number, single: boolean, siteId: string): Promise<any> { | ||||||
|  |         let attempts: any[], | ||||||
|  |             startAttempt = false, | ||||||
|  |             quiz, | ||||||
|  |             quizAccessInfo, | ||||||
|  |             attemptAccessInfo, | ||||||
|  |             preflightData; | ||||||
|  | 
 | ||||||
|  |         // Get quiz.
 | ||||||
|  |         return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quizData) => { | ||||||
|  |             quiz = quizData; | ||||||
|  | 
 | ||||||
|  |             const promises = [], | ||||||
|  |                 introFiles = this.getIntroFilesFromInstance(module, quiz); | ||||||
|  | 
 | ||||||
|  |             // Prefetch some quiz data.
 | ||||||
|  |             promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { | ||||||
|  |                 quizAccessInfo = info; | ||||||
|  |             })); | ||||||
|  |             promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, true, siteId)); | ||||||
|  |             promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { | ||||||
|  |                 attempts = atts; | ||||||
|  |             })); | ||||||
|  |             promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId).then((info) => { | ||||||
|  |                 attemptAccessInfo = info; | ||||||
|  |             })); | ||||||
|  | 
 | ||||||
|  |             promises.push(this.filepoolProvider.addFilesToQueue(siteId, introFiles, AddonModQuizProvider.COMPONENT, module.id)); | ||||||
|  | 
 | ||||||
|  |             return Promise.all(promises); | ||||||
|  |         }).then(() => { | ||||||
|  |             // Check if we need to start a new attempt.
 | ||||||
|  |             const attempt = attempts[attempts.length - 1]; | ||||||
|  |             if (!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)); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 startAttempt = true; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Get the preflight data. This function will also start a new attempt if needed.
 | ||||||
|  |             return this.getPreflightData(quiz, quizAccessInfo, attempt, single, 'core.download', siteId); | ||||||
|  | 
 | ||||||
|  |         }).then((data) => { | ||||||
|  |             preflightData = data; | ||||||
|  | 
 | ||||||
|  |             const promises = []; | ||||||
|  | 
 | ||||||
|  |             if (startAttempt) { | ||||||
|  |                 // Re-fetch user attempts since we created a new one.
 | ||||||
|  |                 promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { | ||||||
|  |                     attempts = atts; | ||||||
|  |                 })); | ||||||
|  | 
 | ||||||
|  |                 // Update the download time to prevent detecting the new attempt as an update.
 | ||||||
|  |                 promises.push(this.filepoolProvider.updatePackageDownloadTime(siteId, AddonModQuizProvider.COMPONENT, module.id) | ||||||
|  |                         .catch(() => { | ||||||
|  |                     // Ignore errors.
 | ||||||
|  |                 })); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Fetch attempt related data.
 | ||||||
|  |             promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, true, siteId)); | ||||||
|  |             promises.push(this.quizProvider.getUserBestGrade(quiz.id, true, siteId)); | ||||||
|  |             promises.push(this.quizProvider.getGradeFromGradebook(courseId, module.id, true, siteId).then((gradebookData) => { | ||||||
|  |                 if (typeof gradebookData.graderaw != 'undefined') { | ||||||
|  |                     return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId); | ||||||
|  |                 } | ||||||
|  |             }).catch(() => { | ||||||
|  |                 // Ignore errors.
 | ||||||
|  |             })); | ||||||
|  |             promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt.
 | ||||||
|  | 
 | ||||||
|  |             return Promise.all(promises); | ||||||
|  |         }).then(() => { | ||||||
|  |             // We have quiz data, now we'll get specific data for each attempt.
 | ||||||
|  |             const promises = []; | ||||||
|  | 
 | ||||||
|  |             attempts.forEach((attempt) => { | ||||||
|  |                 promises.push(this.prefetchAttempt(quiz, attempt, preflightData, siteId)); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             return Promise.all(promises); | ||||||
|  |         }).then(() => { | ||||||
|  |             // 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) { | ||||||
|  |                 this.syncProvider = this.injector.get(AddonModQuizSyncProvider); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             this.syncProvider.hasDataToSync(quiz.id, siteId).then((hasData) => { | ||||||
|  |                 if (!hasData) { | ||||||
|  |                     this.syncProvider.setSyncTime(quiz.id, siteId); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Prefetch all WS data for an attempt. | ||||||
|  |      * | ||||||
|  |      * @param {any} quiz Quiz. | ||||||
|  |      * @param {any} attempt Attempt. | ||||||
|  |      * @param {any} preflightData Preflight required data (like password). | ||||||
|  |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|  |      * @return {Promise<any>} Promise resolved when the prefetch is finished. Data returned is not reliable. | ||||||
|  |      */ | ||||||
|  |     prefetchAttempt(quiz: any, attempt: any, preflightData: any, siteId?: string): Promise<any> { | ||||||
|  |         const pages = this.quizProvider.getPagesFromLayout(attempt.layout), | ||||||
|  |             promises = [], | ||||||
|  |             isSequential = this.quizProvider.isNavigationSequential(quiz); | ||||||
|  | 
 | ||||||
|  |         if (this.quizProvider.isAttemptFinished(attempt.state)) { | ||||||
|  |             // Attempt is finished, get feedback and review data.
 | ||||||
|  | 
 | ||||||
|  |             const attemptGrade = this.quizProvider.rescaleGrade(attempt.sumgrades, quiz, false); | ||||||
|  |             if (typeof attemptGrade != 'undefined') { | ||||||
|  |                 promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), true, siteId)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Get the review for each page.
 | ||||||
|  |             pages.forEach((page) => { | ||||||
|  |                 promises.push(this.quizProvider.getAttemptReview(attempt.id, page, true, siteId).catch(() => { | ||||||
|  |                     // Ignore failures, maybe the user can't review the attempt.
 | ||||||
|  |                 })); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |              // Get the review for all questions in same page.
 | ||||||
|  |             promises.push(this.quizProvider.getAttemptReview(attempt.id, -1, true, siteId).then((data) => { | ||||||
|  |                 // Download the files inside the questions.
 | ||||||
|  |                 const questionPromises = []; | ||||||
|  | 
 | ||||||
|  |                 data.questions.forEach((question) => { | ||||||
|  |                     questionPromises.push(this.questionHelper.prefetchQuestionFiles( | ||||||
|  |                             question, this.component, quiz.coursemodule, siteId)); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 return Promise.all(questionPromises); | ||||||
|  |             }, () => { | ||||||
|  |                 // Ignore failures, maybe the user can't review the attempt.
 | ||||||
|  |             })); | ||||||
|  |         } else { | ||||||
|  | 
 | ||||||
|  |             // Attempt not finished, get data needed to continue the attempt.
 | ||||||
|  |             promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, attempt.id, false, true, siteId)); | ||||||
|  |             promises.push(this.quizProvider.getAttemptSummary(attempt.id, preflightData, false, true, false, siteId)); | ||||||
|  | 
 | ||||||
|  |             if (attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { | ||||||
|  |                 // Get data for each page.
 | ||||||
|  |                 pages.forEach((page) => { | ||||||
|  |                     if (isSequential && page < attempt.currentpage) { | ||||||
|  |                         // Sequential quiz, cannot get pages before the current one.
 | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     promises.push(this.quizProvider.getAttemptData(attempt.id, page, preflightData, false, true, siteId) | ||||||
|  |                             .then((data) => { | ||||||
|  |                         // Download the files inside the questions.
 | ||||||
|  |                         const questionPromises = []; | ||||||
|  | 
 | ||||||
|  |                         data.questions.forEach((question) => { | ||||||
|  |                             questionPromises.push(this.questionHelper.prefetchQuestionFiles( | ||||||
|  |                                     question, this.component, quiz.coursemodule, siteId)); | ||||||
|  |                         }); | ||||||
|  | 
 | ||||||
|  |                         return Promise.all(questionPromises); | ||||||
|  |                     })); | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return Promise.all(promises); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Prefetches some data for a quiz and its last attempt. | ||||||
|  |      * This function will NOT start a new attempt, it only reads data for the quiz and the last attempt. | ||||||
|  |      * | ||||||
|  |      * @param {any} quiz Quiz. | ||||||
|  |      * @param {boolean} [askPreflight] Whether it should ask for preflight data if needed. | ||||||
|  |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|  |      * @return {Promise<any>} Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     prefetchQuizAndLastAttempt(quiz: any, askPreflight?: boolean, siteId?: string): Promise<any> { | ||||||
|  |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
|  | 
 | ||||||
|  |         const promises = []; | ||||||
|  |         let attempts, | ||||||
|  |             quizAccessInfo, | ||||||
|  |             preflightData, | ||||||
|  |             lastAttempt; | ||||||
|  | 
 | ||||||
|  |         // Get quiz data.
 | ||||||
|  |         promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { | ||||||
|  |             quizAccessInfo = info; | ||||||
|  |         })); | ||||||
|  |         promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, true, siteId)); | ||||||
|  |         promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, true, siteId)); | ||||||
|  |         promises.push(this.quizProvider.getUserBestGrade(quiz.id, true, siteId)); | ||||||
|  |         promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { | ||||||
|  |             attempts = atts; | ||||||
|  |         })); | ||||||
|  |         promises.push(this.quizProvider.getGradeFromGradebook(quiz.course, quiz.coursemodule, true, siteId) | ||||||
|  |                 .then((gradebookData) => { | ||||||
|  |             if (typeof gradebookData.grade != 'undefined') { | ||||||
|  |                 return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId); | ||||||
|  |             } | ||||||
|  |         })); | ||||||
|  |         promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt.
 | ||||||
|  | 
 | ||||||
|  |         return Promise.all(promises).then(() => { | ||||||
|  |             lastAttempt = attempts[attempts.length - 1]; | ||||||
|  |             if (!lastAttempt) { | ||||||
|  |                 // No need to get attempt data, we don't need preflight data.
 | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Get the preflight data.
 | ||||||
|  |             return this.getPreflightData(quiz, quizAccessInfo, lastAttempt, askPreflight, 'core.download', siteId); | ||||||
|  | 
 | ||||||
|  |         }).then((data) => { | ||||||
|  |             preflightData = data; | ||||||
|  | 
 | ||||||
|  |             if (lastAttempt) { | ||||||
|  |                 // Get data for last attempt.
 | ||||||
|  |                 return this.prefetchAttempt(quiz, lastAttempt, preflightData, siteId); | ||||||
|  |             } | ||||||
|  |         }).then(() => { | ||||||
|  |             // Prefetch finished, get current status to determine if we need to change it.
 | ||||||
|  | 
 | ||||||
|  |             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), | ||||||
|  |                     newStatus = isLastFinished ? CoreConstants.NOT_DOWNLOADED : CoreConstants.DOWNLOADED; | ||||||
|  | 
 | ||||||
|  |                 return this.filepoolProvider.storePackageStatus(siteId, newStatus, this.component, quiz.coursemodule); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -13,9 +13,15 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||||
|  | import { CoreCronDelegate } from '@providers/cron'; | ||||||
|  | import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; | ||||||
| import { AddonModQuizAccessRuleDelegate } from './providers/access-rules-delegate'; | import { AddonModQuizAccessRuleDelegate } from './providers/access-rules-delegate'; | ||||||
| import { AddonModQuizProvider } from './providers/quiz'; | import { AddonModQuizProvider } from './providers/quiz'; | ||||||
| import { AddonModQuizOfflineProvider } from './providers/quiz-offline'; | import { AddonModQuizOfflineProvider } from './providers/quiz-offline'; | ||||||
|  | import { AddonModQuizHelperProvider } from './providers/helper'; | ||||||
|  | import { AddonModQuizSyncProvider } from './providers/quiz-sync'; | ||||||
|  | import { AddonModQuizPrefetchHandler } from './providers/prefetch-handler'; | ||||||
|  | import { AddonModQuizSyncCronHandler } from './providers/sync-cron-handler'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
| @ -25,7 +31,18 @@ import { AddonModQuizOfflineProvider } from './providers/quiz-offline'; | |||||||
|     providers: [ |     providers: [ | ||||||
|         AddonModQuizAccessRuleDelegate, |         AddonModQuizAccessRuleDelegate, | ||||||
|         AddonModQuizProvider, |         AddonModQuizProvider, | ||||||
|         AddonModQuizOfflineProvider |         AddonModQuizOfflineProvider, | ||||||
|  |         AddonModQuizHelperProvider, | ||||||
|  |         AddonModQuizSyncProvider, | ||||||
|  |         AddonModQuizPrefetchHandler, | ||||||
|  |         AddonModQuizSyncCronHandler | ||||||
|     ] |     ] | ||||||
| }) | }) | ||||||
| export class AddonModQuizModule { } | export class AddonModQuizModule { | ||||||
|  |     constructor(prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModQuizPrefetchHandler, | ||||||
|  |             cronDelegate: CoreCronDelegate, syncHandler: AddonModQuizSyncCronHandler) { | ||||||
|  | 
 | ||||||
|  |         prefetchDelegate.registerHandler(prefetchHandler); | ||||||
|  |         cronDelegate.register(syncHandler); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -14,9 +14,11 @@ | |||||||
| 
 | 
 | ||||||
| import { Injectable, EventEmitter } from '@angular/core'; | import { Injectable, EventEmitter } from '@angular/core'; | ||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
|  | import { CoreFilepoolProvider } from '@providers/filepool'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider } from '@providers/sites'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
|  | import { CoreUrlUtilsProvider } from '@providers/utils/url'; | ||||||
| import { CoreQuestionProvider } from './question'; | import { CoreQuestionProvider } from './question'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -29,7 +31,8 @@ export class CoreQuestionHelperProvider { | |||||||
| 
 | 
 | ||||||
|     constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, |     constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, | ||||||
|         private questionProvider: CoreQuestionProvider, private sitesProvider: CoreSitesProvider, |         private questionProvider: CoreQuestionProvider, private sitesProvider: CoreSitesProvider, | ||||||
|         private translate: TranslateService) { } |         private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider, | ||||||
|  |         private filepoolProvider: CoreFilepoolProvider) { } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Add a behaviour button to the question's "behaviourButtons" property. |      * Add a behaviour button to the question's "behaviourButtons" property. | ||||||
| @ -428,6 +431,43 @@ export class CoreQuestionHelperProvider { | |||||||
|         question.html = form.innerHTML; |         question.html = form.innerHTML; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Prefetch the files in a question HTML. | ||||||
|  |      * | ||||||
|  |      * @param {any} question Question. | ||||||
|  |      * @param {string} [component] The component to link the files to. If not defined, question component. | ||||||
|  |      * @param {string|number} [componentId] An ID to use in conjunction with the component. If not defined, question ID. | ||||||
|  |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|  |      * @return {Promise<any>} Promise resolved when all the files have been downloaded. | ||||||
|  |      */ | ||||||
|  |     prefetchQuestionFiles(question: any, component?: string, componentId?: string | number, siteId?: string): Promise<any> { | ||||||
|  |         const urls = this.domUtils.extractDownloadableFilesFromHtml(question.html); | ||||||
|  | 
 | ||||||
|  |         if (!component) { | ||||||
|  |             component = CoreQuestionProvider.COMPONENT; | ||||||
|  |             componentId = question.id; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|  |             const promises = []; | ||||||
|  | 
 | ||||||
|  |             urls.forEach((url) => { | ||||||
|  |                 if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(url)) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (url.indexOf('theme/image.php') > -1 && url.indexOf('flagged') > -1) { | ||||||
|  |                     // Ignore flag images.
 | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 promises.push(this.filepoolProvider.addToQueueByUrl(siteId, url, component, componentId)); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             return Promise.all(promises); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Replace Moodle's correct/incorrect classes with the Mobile ones. |      * Replace Moodle's correct/incorrect classes with the Mobile ones. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -58,6 +58,8 @@ export interface CoreQuestionState { | |||||||
|  */ |  */ | ||||||
| @Injectable() | @Injectable() | ||||||
| export class CoreQuestionProvider { | export class CoreQuestionProvider { | ||||||
|  |     static COMPONENT = 'mmQuestion'; | ||||||
|  | 
 | ||||||
|     // Variables for database.
 |     // Variables for database.
 | ||||||
|     protected QUESTION_TABLE = 'questions'; |     protected QUESTION_TABLE = 'questions'; | ||||||
|     protected QUESTION_ANSWERS_TABLE = 'question_answers'; |     protected QUESTION_ANSWERS_TABLE = 'question_answers'; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user