diff --git a/src/addon/mod/quiz/pages/player/player.html b/src/addon/mod/quiz/pages/player/player.html new file mode 100644 index 000000000..581493ff5 --- /dev/null +++ b/src/addon/mod/quiz/pages/player/player.html @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + = 0" (click)="changePage(previousPage)" [title]="'core.previous' | translate"> + + + + + + + + = -1" (click)="changePage(nextPage)" [title]="'core.next' | translate"> + + + + + + + + + {{ 'addon.mod_quiz.startattempt' | translate }} + + + + + + + + + {{ 'core.question.questionno' | translate:{$a: question.number} }} + {{ 'core.question.information' | translate }} + + {{question.status}} + + + + + + + + + + + + + = 0" > + + + {{ 'core.previous' | translate }} + + + = -1"> + + {{ 'core.next' | translate }} + + + + + + + + + + {{ 'addon.mod_quiz.summaryofattempt' | translate }} + + + + + {{ 'addon.mod_quiz.question' | translate }} + {{ 'addon.mod_quiz.status' | translate }} + + + + + + + {{ question.number }} + {{ question.status }} + + + + + + {{ 'addon.mod_quiz.returnattempt' | translate }} + + + + {{ attempt.dueDateWarning }} + + + + + {{ 'addon.mod_quiz.cannotsubmitquizdueto' | translate }} + {{message}} + + + {{ 'core.openinbrowser' | translate }} + + + + + {{ 'addon.mod_quiz.submitallandfinish' | translate }} + + + + + + + {{ 'addon.mod_quiz.errorparsequestions' | translate }} + + + + + {{ 'core.openinbrowser' | translate }} + + + + + diff --git a/src/addon/mod/quiz/pages/player/player.module.ts b/src/addon/mod/quiz/pages/player/player.module.ts new file mode 100644 index 000000000..7275025c4 --- /dev/null +++ b/src/addon/mod/quiz/pages/player/player.module.ts @@ -0,0 +1,35 @@ +// (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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreQuestionComponentsModule } from '@core/question/components/components.module'; +import { AddonModQuizPlayerPage } from './player'; + +@NgModule({ + declarations: [ + AddonModQuizPlayerPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CoreQuestionComponentsModule, + IonicPageModule.forChild(AddonModQuizPlayerPage), + TranslateModule.forChild() + ], +}) +export class AddonModQuizPlayerPageModule {} diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts new file mode 100644 index 000000000..eb0c26a33 --- /dev/null +++ b/src/addon/mod/quiz/pages/player/player.ts @@ -0,0 +1,522 @@ +// (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 { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; +import { IonicPage, NavParams, Content } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; +import { AddonModQuizProvider } from '../../providers/quiz'; +import { AddonModQuizSyncProvider } from '../../providers/quiz-sync'; +import { AddonModQuizHelperProvider } from '../../providers/helper'; + +/** + * Page that allows attempting a quiz. + */ +@IonicPage({ segment: 'addon-mod-quiz-player' }) +@Component({ + selector: 'page-addon-mod-quiz-player', + templateUrl: 'player.html', +}) +export class AddonModQuizPlayerPage implements OnInit, OnDestroy { + @ViewChild(Content) content: Content; + + quiz: any; // The quiz the attempt belongs to. + attempt: any; // The attempt being attempted. + moduleUrl: string; // URL to the module in the site. + component = AddonModQuizProvider.COMPONENT; // Component to link the files to. + loaded: boolean; // Whether data has been loaded. + quizAborted: boolean; // Whether the quiz was aborted due to an error. + preflightData: any = {}; // Preflight data to attempt the quiz. + offline: boolean; // Whether the quiz is being attempted in offline mode. + toc: any[]; // TOC to navigate the questions. + questions: any[]; // Questions of the current page. + nextPage: number; // Next page. + previousPage: number; // Previous page. + showSummary: boolean; // Whether the attempt summary should be displayed. + summaryQuestions: any[]; // The questions to display in the summary. + canReturn: boolean; // Whether the user can return to a page after seeing the summary. + preventSubmitMessages: string[]; // List of messages explaining why the quiz cannot be submitted. + endTime: number; // The time when the attempt must be finished. + + protected element: HTMLElement; // Host element of the page. + protected courseId: number; // The course ID the quiz belongs to. + protected quizId: number; // Quiz ID to attempt. + protected isTimed: boolean; // Whether the quiz has a time limit. + protected quizAccessInfo: any; // Quiz access information. + protected attemptAccessInfo: any; // Attempt access info. + protected lastAttempt: any; // Last user attempt before a new one is created (if needed). + protected newAttempt: boolean; // Whether the user is starting a new attempt. + protected quizDataLoaded: boolean; // Whether the quiz data has been loaded. + protected timeUpCalled: boolean; // Whether the time up function has been called. + + constructor(navParams: NavParams, element: ElementRef, protected translate: TranslateService, + protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider, + protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, + protected timeUtils: CoreTimeUtilsProvider, protected quizProvider: AddonModQuizProvider, + protected quizHelper: AddonModQuizHelperProvider, protected quizSync: AddonModQuizSyncProvider, + protected questionHelper: CoreQuestionHelperProvider) { + + this.quizId = navParams.get('quizId'); + this.courseId = navParams.get('courseId'); + this.moduleUrl = navParams.get('moduleUrl'); + + // Block the quiz so it cannot be synced. + this.syncProvider.blockOperation(AddonModQuizProvider.COMPONENT, this.quizId); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Start the player when the page is loaded. + this.start(); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + // Stop auto save. + // @todo $mmaModQuizAutoSave.stopAutoSaving(); + // @todo $mmaModQuizAutoSave.stopCheckChangesProcess(); + + // Unblock the quiz so it can be synced. + this.syncProvider.unblockOperation(AddonModQuizProvider.COMPONENT, this.quizId); + } + + /** + * Abort the quiz. + */ + abortQuiz(): void { + this.quizAborted = true; + } + + /** + * A behaviour button in a question was clicked (Check, Redo, ...). + * + * @param {any} button Clicked button. + */ + behaviourButtonClicked(button: any): void { + // Confirm that the user really wants to do it. + this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => { + const modal = this.domUtils.showModalLoading('core.sending', true), + answers = this.getAnswers(); + + // Add the clicked button data. + answers[button.name] = button.value; + + // Behaviour checks are always in online. + this.quizProvider.processAttempt(this.quiz, this.attempt, answers, this.preflightData).then(() => { + // Reload the current page. + const scrollElement = this.content.getScrollElement(), + scrollTop = scrollElement.scrollTop || 0, + scrollLeft = scrollElement.scrollLeft || 0; + + this.loaded = false; + this.content.scrollToTop(); // Scroll top so the spinner is seen. + + return this.loadPage(this.attempt.currentpage).finally(() => { + this.loaded = true; + this.content.scrollTo(scrollLeft, scrollTop); + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error performing action.'); + }).finally(() => { + modal.dismiss(); + }); + }); + } + + /** + * Change the current page. If slot is supplied, try to scroll to that question. + * + * @param {number} page Page to load. -1 means summary. + * @param {boolean} [fromToc] Whether the page was selected using the TOC. + * @param {number} [slot] Slot of the question to scroll to. + */ + changePage(page: number, fromToc?: boolean, slot?: number): void { + if (page != -1 && (this.attempt.state == AddonModQuizProvider.ATTEMPT_OVERDUE || this.attempt.finishedOffline)) { + // We can't load a page if overdue or the local attempt is finished. + return; + } else if (page == this.attempt.currentpage && !this.showSummary && typeof slot != 'undefined') { + // Navigating to a question in the current page. + this.scrollToQuestion(slot); + + return; + } else if ((page == this.attempt.currentpage && !this.showSummary) || (fromToc && this.quiz.isSequential && page != -1)) { + // If the user is navigating to the current page we do nothing. + // Also, in sequential quizzes we don't allow navigating using the TOC except for finishing the quiz (summary). + return; + } else if (page === -1 && this.showSummary) { + // Summary already shown. + return; + } + + this.loaded = false; + this.content.scrollToTop(); + + // First try to save the attempt data. We only save it if we're not seeing the summary. + const promise = this.showSummary ? Promise.resolve() : this.processAttempt(false, false); + promise.then(() => { + // Attempt data successfully saved, load the page or summary. + + if (page === -1) { + return this.loadSummary(); + } else { + // @todo $mmaModQuizAutoSave.stopCheckChangesProcess(); // Stop checking for changes during page change. + + return this.loadPage(page).catch((error) => { + // @todo $mmaModQuizAutoSave.startCheckChangesProcess($scope, quiz, attempt); // Start the check again. + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true); + }); + } + }, (error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorsaveattempt', true); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true); + }).finally(() => { + this.loaded = true; + + if (typeof slot != 'undefined') { + // Scroll to the question. Give some time to the questions to render. + setTimeout(() => { + this.scrollToQuestion(slot); + }, 2000); + } + }); + } + + /** + * Convenience function to get the quiz data. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchData(): Promise { + // Wait for any ongoing sync to finish. We won't sync a quiz while it's being played. + return this.quizSync.waitForSync(this.quizId).then(() => { + // Sync finished, now get the quiz. + return this.quizProvider.getQuizById(this.courseId, this.quizId); + }).then((quizData) => { + this.quiz = quizData; + this.quiz.isSequential = this.quizProvider.isNavigationSequential(this.quiz); + + if (this.quizProvider.isQuizOffline(this.quiz)) { + // Quiz supports offline. + return true; + } else { + // Quiz doesn't support offline right now, but maybe it did and then the setting was changed. + // If we have an unfinished offline attempt then we'll use offline mode. + return this.quizProvider.isLastAttemptOfflineUnfinished(this.quiz); + } + }).then((offlineMode) => { + this.offline = offlineMode; + + if (this.quiz.timelimit > 0) { + this.isTimed = true; + this.quiz.readableTimeLimit = this.timeUtils.formatTime(this.quiz.timelimit); + } + + // Get access information for the quiz. + return this.quizProvider.getQuizAccessInformation(this.quiz.id, this.offline, true); + }).then((info) => { + this.quizAccessInfo = info; + + // Get user attempts to determine last attempt. + return this.quizProvider.getUserAttempts(this.quiz.id, 'all', true, this.offline, true); + }).then((attempts) => { + if (!attempts.length) { + // There are no attempts, start a new one. + this.newAttempt = true; + } else { + const promises = []; + + // Get the last attempt. If it's finished, start a new one. + this.lastAttempt = attempts[attempts.length - 1]; + this.newAttempt = this.quizProvider.isAttemptFinished(this.lastAttempt.state); + + // Load quiz last sync time. + promises.push(this.quizSync.getSyncTime(this.quiz.id).then((time) => { + this.quiz.syncTime = time; + this.quiz.syncTimeReadable = this.quizSync.getReadableTimeFromTimestamp(time); + })); + + // Load flag to show if attempts are finished but not synced. + promises.push(this.quizProvider.loadFinishedOfflineData(attempts)); + + return Promise.all(promises); + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true); + }); + } + + /** + * Finish an attempt, either by timeup or because the user clicked to finish it. + * + * @param {boolean} [userFinish] Whether the user clicked to finish the attempt. + * @param {boolean} [timeUp] Whether the quiz time is up. + * @return {Promise} Promise resolved when done. + */ + finishAttempt(userFinish?: boolean, timeUp?: boolean): Promise { + let promise; + + // Show confirm if the user clicked the finish button and the quiz is in progress. + if (!timeUp && this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { + promise = this.domUtils.showConfirm(this.translate.instant('addon.mod_quiz.confirmclose')); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + const modal = this.domUtils.showModalLoading('core.sending', true); + + return this.processAttempt(userFinish, timeUp).then(() => { + // Trigger an event to notify the attempt was finished. + this.eventsProvider.trigger(AddonModQuizProvider.ATTEMPT_FINISHED_EVENT, { + quizId: this.quizId, + attemptId: this.attempt.id, + synced: !this.offline + }, this.sitesProvider.getCurrentSiteId()); + + // Leave the player. + // @todo blockData && blockData.back(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorsaveattempt', true); + }).finally(() => { + modal.dismiss(); + }); + }); + } + + /** + * Get the input answers. + * + * @return {any} Object with the answers. + */ + protected getAnswers(): any { + return this.questionHelper.getAnswersFromForm(document.forms['addon-mod_quiz-player-form']); + } + + /** + * Initializes the timer if enabled. + */ + protected initTimer(): void { + if (this.attemptAccessInfo.endtime > 0) { + // Quiz has an end time. Check if time left should be shown. + if (this.quizProvider.shouldShowTimeLeft(this.quizAccessInfo.activerulenames, this.attempt, + this.attemptAccessInfo.endtime)) { + this.endTime = this.attemptAccessInfo.endtime; + } else { + delete this.endTime; + } + } + } + + /** + * Load a page questions. + * + * @param {number} page The page to load. + * @return {Promise} Promise resolved when done. + */ + protected loadPage(page: number): Promise { + return this.quizProvider.getAttemptData(this.attempt.id, page, this.preflightData, this.offline, true).then((data) => { + // Update attempt, status could change during the execution. + this.attempt = data.attempt; + this.attempt.currentpage = page; + + this.questions = data.questions; + this.nextPage = data.nextpage; + this.previousPage = this.quiz.isSequential ? -1 : page - 1; + this.showSummary = false; + + this.questions.forEach((question) => { + // Get the readable mark for each question. + question.readableMark = this.quizHelper.getQuestionMarkFromHtml(question.html); + + // Extract the question info box. + this.questionHelper.extractQuestionInfoBox(question, '.info'); + + // Set the preferred behaviour. + question.preferredBehaviour = this.quiz.preferredbehaviour; + + // Check if the question is blocked. If it is, treat it as a description question. + if (this.quizProvider.isQuestionBlocked(question)) { + question.type = 'description'; + } + }); + + // Mark the page as viewed. We'll ignore errors in this call. + this.quizProvider.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline); + + // Start looking for changes. + // @todo $mmaModQuizAutoSave.startCheckChangesProcess($scope, quiz, attempt); + }); + } + + /** + * Load attempt summary. + * + * @return {Promise} Promise resolved when done. + */ + protected loadSummary(): Promise { + this.summaryQuestions = []; + + return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline, true, true).then((qs) => { + this.showSummary = true; + this.summaryQuestions = qs; + + this.canReturn = this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS && !this.attempt.finishedOffline; + this.preventSubmitMessages = this.quizProvider.getPreventSubmitMessages(this.summaryQuestions); + + this.attempt.dueDateWarning = this.quizProvider.getAttemptDueDateWarning(this.quiz, this.attempt); + + // Log summary as viewed. + this.quizProvider.logViewAttemptSummary(this.attempt.id, this.preflightData); + }); + } + + /** + * Load TOC to navigate the questions. + * + * @return {Promise} Promise resolved when done. + */ + protected loadToc(): Promise { + // We use the attempt summary to build the TOC because it contains all the questions. + return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline).then((questions) => { + this.toc = questions; + }); + } + + // Prepare the answers to be sent for the attempt. + protected prepareAnswers(): Promise { + return this.questionHelper.prepareAnswers(this.questions, this.getAnswers(), this.offline); + } + + /** + * Process attempt. + * + * @param {boolean} [userFinish] Whether the user clicked to finish the attempt. + * @param {boolean} [timeUp] Whether the quiz time is up. + * @return {Promise} Promise resolved when done. + */ + protected processAttempt(userFinish?: boolean, timeUp?: boolean): Promise { + // Get the answers to send. + return this.prepareAnswers().then((answers) => { + // Send the answers. + return this.quizProvider.processAttempt(this.quiz, this.attempt, answers, this.preflightData, userFinish, timeUp, + this.offline); + }).then(() => { + // Answers saved, cancel auto save. + // @todo $mmaModQuizAutoSave.cancelAutoSave(); + // @todo $mmaModQuizAutoSave.hideAutoSaveError($scope); + }); + } + + /** + * Scroll to a certain question. + * + * @param {number} slot Slot of the question to scroll to. + */ + protected scrollToQuestion(slot: number): void { + this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_quiz-question-' + slot); + } + + /** + * Show connection error. + * + * @param {Event} ev Click event. + */ + showConnectionError(ev: Event): void { + // @todo + } + + /** + * Convenience function to start the player. + */ + start(): void { + let promise; + this.loaded = false; + + if (this.quizDataLoaded) { + // Quiz data has been loaded, try to start or continue. + promise = this.startOrContinueAttempt(); + } else { + // Fetch data. + promise = this.fetchData().then(() => { + this.quizDataLoaded = true; + + return this.startOrContinueAttempt(); + }); + } + + promise.finally(() => { + this.loaded = true; + }); + } + + /** + * Start or continue an attempt. + * + * @return {Promise} [description] + */ + protected startOrContinueAttempt(): Promise { + const attempt = this.newAttempt ? undefined : this.lastAttempt; + + // Get the preflight data and start attempt if needed. + return this.quizHelper.getAndCheckPreflightData(this.quiz, this.quizAccessInfo, this.preflightData, attempt, this.offline, + false, 'addon.mod_quiz.startattempt').then((attempt) => { + + // Re-fetch attempt access information with the right attempt (might have changed because a new attempt was created). + return this.quizProvider.getAttemptAccessInformation(this.quiz.id, attempt.id, this.offline, true).then((info) => { + this.attemptAccessInfo = info; + this.attempt = attempt; + + return this.loadToc(); + }).then(() => { + if (this.attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !this.attempt.finishedOffline) { + // Attempt not overdue and not finished in offline, load page. + return this.loadPage(this.attempt.currentpage).then(() => { + this.initTimer(); + }); + } else { + // Attempt is overdue or finished in offline, we can only load the summary. + return this.loadSummary(); + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true); + }); + }).catch((error) => { + if (error) { + this.domUtils.showErrorModal(error); + } + }); + } + + /** + * Quiz time has finished. + */ + timeUp(): void { + if (this.timeUpCalled) { + return; + } + + this.timeUpCalled = true; + this.finishAttempt(false, true); + } +} diff --git a/src/addon/qtype/multichoice/component/multichoice.html b/src/addon/qtype/multichoice/component/multichoice.html index ee734dd99..b9271c852 100644 --- a/src/addon/qtype/multichoice/component/multichoice.html +++ b/src/addon/qtype/multichoice/component/multichoice.html @@ -15,9 +15,9 @@ - + - + diff --git a/src/core/question/components/question/question.html b/src/core/question/components/question/question.html index 33fc72525..a8d9f363a 100644 --- a/src/core/question/components/question/question.html +++ b/src/core/question/components/question/question.html @@ -18,7 +18,7 @@ - {{ button.value }} + {{ button.value }} diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts index 38d575562..bd4aec3f2 100644 --- a/src/core/question/providers/helper.ts +++ b/src/core/question/providers/helper.ts @@ -19,7 +19,9 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreQuestionProvider } from './question'; +import { CoreQuestionDelegate } from './delegate'; /** * Service with some common functions to handle questions. @@ -31,8 +33,8 @@ export class CoreQuestionHelperProvider { constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, private questionProvider: CoreQuestionProvider, private sitesProvider: CoreSitesProvider, - private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider, - private filepoolProvider: CoreFilepoolProvider) { } + private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider, private utils: CoreUtilsProvider, + private filepoolProvider: CoreFilepoolProvider, private questionDelegate: CoreQuestionDelegate) { } /** * Add a behaviour button to the question's "behaviourButtons" property. @@ -301,6 +303,44 @@ export class CoreQuestionHelperProvider { return answers; } + /** + * Retrieve the answers entered in a form. + * We don't use ngModel because it doesn't detect changes done by JavaScript and some questions might do that. + * + * @param {HTMLFormElement} form Form. + * @return {any} Object with the answers. + */ + getAnswersFromForm(form: HTMLFormElement): any { + if (!form || !form.elements) { + return {}; + } + + const answers = {}, + elements = Array.from(form.elements); + + elements.forEach((element: HTMLInputElement) => { + const name = element.name || ''; + + // Ignore flag and submit inputs. + if (!name || name.match(/_:flagged$/) || element.type == 'submit' || element.tagName == 'BUTTON') { + return; + } + + // Get the value. + if (element.type == 'checkbox') { + answers[name] = !!element.checked; + } else if (element.type == 'radio') { + if (element.checked) { + answers[name] = element.value; + } + } else { + answers[name] = element.value; + } + }); + + return answers; + } + /** * Given an HTML code with list of attachments, returns the list of attached files (filename and fileurl). * Please take into account that this function will treat all the anchors in the HTML, you should provide @@ -468,6 +508,27 @@ export class CoreQuestionHelperProvider { }); } + /** + * Prepare and return the answers. + * + * @param {any[]} questions The list of questions. + * @param {any} answers The input data. + * @param {boolean} offline True if data should be saved in offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with answers to send to server. + */ + prepareAnswers(questions: any[], answers: any, offline?: boolean, siteId?: string): Promise { + const promises = []; + + questions.forEach((question) => { + promises.push(this.questionDelegate.prepareAnswersForQuestion(question, answers, offline, siteId)); + }); + + return this.utils.allPromises(promises).then(() => { + return answers; + }); + } + /** * Replace Moodle's correct/incorrect classes with the Mobile ones. *
{{question.status}}
{{ 'addon.mod_quiz.cannotsubmitquizdueto' | translate }}
{{message}}
{{ 'addon.mod_quiz.errorparsequestions' | translate }}