diff --git a/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.html b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.html
new file mode 100644
index 000000000..db49a795e
--- /dev/null
+++ b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.html
@@ -0,0 +1,27 @@
+
+
+ {{ title | translate }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.module.ts b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.module.ts
new file mode 100644
index 000000000..740c8f325
--- /dev/null
+++ b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.module.ts
@@ -0,0 +1,31 @@
+// (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 { AddonModQuizPreflightModalPage } from './preflight-modal';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '@components/components.module';
+
+@NgModule({
+ declarations: [
+ AddonModQuizPreflightModalPage
+ ],
+ imports: [
+ CoreComponentsModule,
+ IonicPageModule.forChild(AddonModQuizPreflightModalPage),
+ TranslateModule.forChild()
+ ]
+})
+export class AddonModQuizPreflightModalModule {}
diff --git a/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.ts b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.ts
new file mode 100644
index 000000000..48e704f54
--- /dev/null
+++ b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.ts
@@ -0,0 +1,122 @@
+// (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, Injector, ViewChild } from '@angular/core';
+import { IonicPage, ViewController, NavParams, Content } from 'ionic-angular';
+import { TranslateService } from '@ngx-translate/core';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
+
+/**
+ * Modal that renders the access rules for a quiz.
+ */
+@IonicPage({ segment: 'addon-mod-quiz-preflight-modal' })
+@Component({
+ selector: 'page-addon-mod-quiz-preflight-modal',
+ templateUrl: 'preflight-modal.html',
+})
+export class AddonModQuizPreflightModalPage implements OnInit {
+
+ @ViewChild(Content) content: Content;
+
+ preflightForm: FormGroup;
+ title: string;
+ accessRulesComponent: any[] = [];
+ data: any;
+ loaded: boolean;
+
+ protected quiz: any;
+ protected attempt: any;
+ protected prefetch: boolean;
+ protected siteId: string;
+ protected rules: string[];
+ protected renderedRules: string[] = [];
+
+ constructor(params: NavParams, fb: FormBuilder, translate: TranslateService, sitesProvider: CoreSitesProvider,
+ protected viewCtrl: ViewController, protected accessRuleDelegate: AddonModQuizAccessRuleDelegate,
+ protected injector: Injector, protected domUtils: CoreDomUtilsProvider) {
+
+ this.title = params.get('title') || translate.instant('addon.mod_quiz.startattempt');
+ this.quiz = params.get('quiz');
+ this.attempt = params.get('attempt');
+ this.prefetch = params.get('prefetch');
+ this.siteId = params.get('siteId') || sitesProvider.getCurrentSiteId();
+ this.rules = params.get('rules') || [];
+
+ // Create an empty form group. The controls will be added by the access rules components.
+ this.preflightForm = fb.group({});
+
+ // Create the data to pass to the access rules components.
+ this.data = {
+ quiz: this.quiz,
+ attempt: this.attempt,
+ prefetch: this.prefetch,
+ form: this.preflightForm,
+ siteId: this.siteId
+ };
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ const promises = [];
+
+ this.rules.forEach((rule) => {
+ // Check if preflight is required for rule and, if so, get the component to render it.
+ promises.push(this.accessRuleDelegate.isPreflightCheckRequiredForRule(rule, this.quiz, this.attempt, this.prefetch,
+ this.siteId).then((required) => {
+
+ if (required) {
+ return this.accessRuleDelegate.getPreflightComponent(rule, this.injector).then((component) => {
+ if (component) {
+ this.renderedRules.push(rule);
+ this.accessRulesComponent.push(component);
+ }
+ });
+ }
+ }));
+ });
+
+ Promise.all(promises).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error loading rules');
+ }).finally(() => {
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * Check that the data is valid and send it back.
+ */
+ sendData(): void {
+ if (!this.preflightForm.valid) {
+ // Form not valid. Scroll to the first element with errors.
+ if (!this.domUtils.scrollToInputError(this.content)) {
+ // Input not found, show an error modal.
+ this.domUtils.showErrorModal('core.errorinvalidform', true);
+ }
+ } else {
+ this.viewCtrl.dismiss(this.preflightForm.value);
+ }
+ }
+
+ /**
+ * Close modal.
+ */
+ closeModal(): void {
+ this.viewCtrl.dismiss();
+ }
+}
diff --git a/src/addon/mod/quiz/providers/access-rules-delegate.ts b/src/addon/mod/quiz/providers/access-rules-delegate.ts
index 730023106..ace7c6c69 100644
--- a/src/addon/mod/quiz/providers/access-rules-delegate.ts
+++ b/src/addon/mod/quiz/providers/access-rules-delegate.ts
@@ -34,24 +34,24 @@ export interface AddonModQuizAccessRuleHandler extends CoreDelegateHandler {
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
*
* @param {any} quiz The quiz the rule belongs to.
- * @param {any} attempt The attempt started/continued.
+ * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {boolean|Promise} Whether the rule requires a preflight check.
*/
- isPreflightCheckRequired(quiz: any, attempt: any, prefetch?: boolean, siteId?: string): boolean | Promise;
+ isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise;
/**
* Add preflight data that doesn't require user interaction. The data should be added to the preflightData param.
*
* @param {any} quiz The quiz the rule belongs to.
- * @param {any} attempt The attempt started/continued.
* @param {any} preflightData Object where to add the preflight data.
+ * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {void|Promise} Promise resolved when done if async, void if it's synchronous.
*/
- getFixedPreflightData?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string): void | Promise;
+ getFixedPreflightData?(quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string): void | Promise;
/**
* Return the Component to use to display the access rule preflight.
@@ -128,20 +128,20 @@ export class AddonModQuizAccessRuleDelegate extends CoreDelegate {
*
* @param {string[]} rules List of active rules names.
* @param {any} quiz Quiz.
- * @param {any} attempt Attempt.
* @param {any} preflightData Object where to store the preflight data.
+ * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise resolved when all the data has been gathered.
*/
- getFixedPreflightData(rules: string[], quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string)
+ getFixedPreflightData(rules: string[], quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string)
: Promise {
rules = rules || [];
const promises = [];
rules.forEach((rule) => {
promises.push(Promise.resolve(
- this.executeFunctionOnEnabled(rule, 'getFixedPreflightData', [quiz, attempt, preflightData, prefetch, siteId])
+ this.executeFunctionOnEnabled(rule, 'getFixedPreflightData', [quiz, preflightData, attempt, prefetch, siteId])
));
});
@@ -150,6 +150,16 @@ export class AddonModQuizAccessRuleDelegate extends CoreDelegate {
});
}
+ /**
+ * Get the Component to use to display the access rule preflight.
+ *
+ * @param {Injector} injector Injector.
+ * @return {Promise} Promise resolved with the component to use, undefined if not found.
+ */
+ getPreflightComponent(rule: string, injector: Injector): Promise {
+ return Promise.resolve(this.executeFunctionOnEnabled(rule, 'getPreflightComponent', [injector]));
+ }
+
/**
* Check if an access rule is supported.
*
@@ -165,7 +175,7 @@ export class AddonModQuizAccessRuleDelegate extends CoreDelegate {
*
* @param {string[]} rules List of active rules names.
* @param {any} quiz Quiz.
- * @param {any} attempt Attempt.
+ * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise resolved with boolean: whether it's required.
@@ -177,9 +187,7 @@ export class AddonModQuizAccessRuleDelegate extends CoreDelegate {
let isRequired = false;
rules.forEach((rule) => {
- promises.push(Promise.resolve(
- this.executeFunctionOnEnabled(rule, 'isPreflightCheckRequired', [quiz, attempt, prefetch, siteId])
- ).then((required) => {
+ promises.push(this.isPreflightCheckRequiredForRule(rule, quiz, attempt, prefetch, siteId).then((required) => {
if (required) {
isRequired = true;
}
@@ -194,6 +202,20 @@ export class AddonModQuizAccessRuleDelegate extends CoreDelegate {
});
}
+ /**
+ * Check if preflight check is required for a certain rule.
+ *
+ * @param {string} rule Rule name.
+ * @param {any} quiz Quiz.
+ * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
+ * @param {boolean} [prefetch] Whether the user is prefetching the quiz.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with boolean: whether it's required.
+ */
+ isPreflightCheckRequiredForRule(rule: string, quiz: any, attempt: any, prefetch?: boolean, siteId?: string): Promise {
+ return Promise.resolve(this.executeFunctionOnEnabled(rule, 'isPreflightCheckRequired', [quiz, attempt, prefetch, siteId]));
+ }
+
/**
* Notify all rules that the preflight check has passed.
*
diff --git a/src/addon/mod/quiz/providers/helper.ts b/src/addon/mod/quiz/providers/helper.ts
new file mode 100644
index 000000000..4404a6703
--- /dev/null
+++ b/src/addon/mod/quiz/providers/helper.ts
@@ -0,0 +1,270 @@
+// (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 } from '@angular/core';
+import { ModalController } from 'ionic-angular';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+import { AddonModQuizProvider } from './quiz';
+import { AddonModQuizOfflineProvider } from './quiz-offline';
+import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
+
+/**
+ * Helper service that provides some features for quiz.
+ */
+@Injectable()
+export class AddonModQuizHelperProvider {
+
+ protected div = document.createElement('div'); // A div element to search in HTML code.
+
+ constructor(private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
+ private accessRuleDelegate: AddonModQuizAccessRuleDelegate, private quizProvider: AddonModQuizProvider,
+ private modalCtrl: ModalController, private quizOfflineProvider: AddonModQuizOfflineProvider) { }
+
+ /**
+ * Validate a preflight data or show a modal to input the preflight data if required.
+ * It calls AddonModQuizProvider.startAttempt if a new attempt is needed.
+ *
+ * @param {any} quiz Quiz.
+ * @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation.
+ * @param {any} preflightData Object where to store the preflight data.
+ * @param {any} [attempt] Attempt to continue. Don't pass any value if the user needs to start a new attempt.
+ * @param {boolean} [offline] Whether the attempt is offline.
+ * @param {boolean} [prefetch] Whether user is prefetching.
+ * @param {string} [title] The title to display in the modal and in the submit button.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @param {boolean} [retrying] Whether we're retrying after a failure.
+ * @return {Promise} Promise resolved when the preflight data is validated. The resolve param is the attempt.
+ */
+ getAndCheckPreflightData(quiz: any, accessInfo: any, preflightData: any, attempt: any, offline?: boolean, prefetch?: boolean,
+ title?: string, siteId?: string, retrying?: boolean): Promise {
+
+ const rules = accessInfo.activerulenames;
+ let isPreflightCheckRequired = false;
+
+ // Check if the user needs to input preflight data.
+ return this.accessRuleDelegate.isPreflightCheckRequired(rules, quiz, attempt, prefetch, siteId).then((required) => {
+ isPreflightCheckRequired = required;
+
+ if (required) {
+ // Preflight check is required but no preflightData has been sent. Show a modal with the preflight form.
+ return this.getPreflightData(quiz, accessInfo, attempt, prefetch, title, siteId).then((data) => {
+ // Data entered by the user, add it to preflight data and check it again.
+ Object.assign(preflightData, data);
+ });
+ }
+ }).then(() => {
+ // Get some fixed preflight data from access rules (data that doesn't require user interaction).
+ return this.accessRuleDelegate.getFixedPreflightData(rules, quiz, preflightData, attempt, prefetch, siteId);
+ }).then(() => {
+
+ // All the preflight data is gathered, now validate it.
+ return this.validatePreflightData(quiz, accessInfo, preflightData, attempt, offline, prefetch, siteId)
+ .catch((error) => {
+
+ if (prefetch) {
+ return Promise.reject(error);
+ } else if (retrying && !isPreflightCheckRequired) {
+ // We're retrying after a failure, but the preflight check wasn't required.
+ // If this happens it means there's something wrong with some access rule.
+ // Don't retry again because it would lead to an infinite loop.
+ return Promise.reject(error);
+ } else {
+ // Show error and ask for the preflight again.
+ // Wait to show the error because we want it to be shown over the preflight modal.
+ setTimeout(() => {
+ this.domUtils.showErrorModalDefault(error, 'core.error', true);
+ }, 100);
+
+ return this.getAndCheckPreflightData(quiz, accessInfo, preflightData, attempt, offline, prefetch,
+ title, siteId, true);
+ }
+ });
+ });
+ }
+
+ /**
+ * Get the preflight data from the user using a modal.
+ *
+ * @param {any} quiz Quiz.
+ * @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation.
+ * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
+ * @param {boolean} [prefetch] Whether the user is prefetching the quiz.
+ * @param {string} [title] The title to display in the modal and in the submit button.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the preflight data. Rejected if user cancels.
+ */
+ getPreflightData(quiz: any, accessInfo: any, attempt: any, prefetch?: boolean, title?: string, siteId?: string): Promise {
+ const notSupported: string[] = [];
+
+ // Check if there is any unsupported rule.
+ accessInfo.activerulenames.forEach((rule) => {
+ if (!this.accessRuleDelegate.isAccessRuleSupported(rule)) {
+ notSupported.push(rule);
+ }
+ });
+
+ if (notSupported.length) {
+ return Promise.reject(this.translate.instant('addon.mod_quiz.errorrulesnotsupported') + ' ' +
+ JSON.stringify(notSupported));
+ }
+
+ // Create and show the modal.
+ const modal = this.modalCtrl.create('AddonModQuizPreflightModalPage', {
+ title: title,
+ quiz: quiz,
+ attempt: attempt,
+ prefetch: !!prefetch,
+ siteId: siteId,
+ rules: accessInfo.activerulenames
+ });
+
+ modal.present();
+
+ // Wait for modal to be dismissed.
+ return new Promise((resolve, reject): void => {
+ modal.onDidDismiss((data) => {
+ if (typeof data != 'undefined') {
+ resolve(data);
+ } else {
+ reject(null);
+ }
+ });
+ });
+ }
+
+ /**
+ * Gets the mark string from a question HTML.
+ * Example result: "Marked out of 1.00".
+ *
+ * @param {string} html Question's HTML.
+ * @return {string} Question's mark.
+ */
+ getQuestionMarkFromHtml(html: string): string {
+ this.div.innerHTML = html;
+
+ return this.domUtils.getContentsOfElement(this.div, '.grade');
+ }
+
+ /**
+ * Add some calculated data to the attempt.
+ *
+ * @param {any} quiz Quiz.
+ * @param {any} attempt Attempt.
+ * @param {boolean} highlight Whether we should check if attempt should be highlighted.
+ * @param {number} [bestGrade] Quiz's best grade (formatted). Required if highlight=true.
+ */
+ setAttemptCalculatedData(quiz: any, attempt: any, highlight?: boolean, bestGrade?: string): void {
+
+ attempt.rescaledGrade = this.quizProvider.rescaleGrade(attempt.sumgrades, quiz, false);
+ attempt.finished = this.quizProvider.isAttemptFinished(attempt.state);
+ attempt.readableState = this.quizProvider.getAttemptReadableState(quiz, attempt);
+
+ if (quiz.showMarkColumn && attempt.finished) {
+ attempt.readableMark = this.quizProvider.formatGrade(attempt.sumgrades, quiz.decimalpoints);
+ } else {
+ attempt.readableMark = '';
+ }
+
+ if (quiz.showGradeColumn && attempt.finished) {
+ attempt.readableGrade = this.quizProvider.formatGrade(attempt.rescaledGrade, quiz.decimalpoints);
+
+ // Highlight the highest grade if appropriate.
+ attempt.highlightGrade = highlight && !attempt.preview && attempt.state == AddonModQuizProvider.ATTEMPT_FINISHED &&
+ attempt.readableGrade == bestGrade;
+ } else {
+ attempt.readableGrade = '';
+ }
+ }
+
+ /**
+ * Add some calculated data to the quiz.
+ *
+ * @param {any} quiz Quiz.
+ * @param {any} options Options returned by AddonModQuizProvider.getCombinedReviewOptions.
+ */
+ setQuizCalculatedData(quiz: any, options: any): void {
+ quiz.sumGradesFormatted = this.quizProvider.formatGrade(quiz.sumgrades, quiz.decimalpoints);
+ quiz.gradeFormatted = this.quizProvider.formatGrade(quiz.grade, quiz.decimalpoints);
+
+ quiz.showAttemptColumn = quiz.attempts != 1;
+ quiz.showGradeColumn = options.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX &&
+ this.quizProvider.quizHasGrades(quiz);
+ quiz.showMarkColumn = quiz.showGradeColumn && quiz.grade != quiz.sumgrades;
+ quiz.showFeedbackColumn = quiz.hasfeedback && options.alloptions.overallfeedback;
+ }
+
+ /**
+ * Validate the preflight data. It calls AddonModQuizProvider.startAttempt if a new attempt is needed.
+ *
+ * @param {any} quiz Quiz.
+ * @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation.
+ * @param {any} preflightData Object where to store the preflight data.
+ * @param {any} [attempt] Attempt to continue. Don't pass any value if the user needs to start a new attempt.
+ * @param {boolean} [offline] Whether the attempt is offline.
+ * @param {boolean} [sent] Whether preflight data has been entered by the user.
+ * @param {boolean} [prefetch] Whether user is prefetching.
+ * @param {string} [title] The title to display in the modal and in the submit button.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the preflight data is validated.
+ */
+ validatePreflightData(quiz: any, accessInfo: any, preflightData: any, attempt: any, offline?: boolean, prefetch?: boolean,
+ siteId?: string): Promise {
+
+ const rules = accessInfo.activerulenames;
+ let promise;
+
+ if (attempt) {
+ if (attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !attempt.finishedOffline) {
+ // We're continuing an attempt. Call getAttemptData to validate the preflight data.
+ const page = attempt.currentpage;
+
+ promise = this.quizProvider.getAttemptData(attempt.id, page, preflightData, offline, true, siteId).then(() => {
+ if (offline) {
+ // Get current page stored in local.
+ return this.quizOfflineProvider.getAttemptById(attempt.id).then((localAttempt) => {
+ attempt.currentpage = localAttempt.currentpage;
+ }).catch(() => {
+ // No local data.
+ });
+ }
+ });
+ } else {
+ // Attempt is overdue or finished in offline, we can only see the summary.
+ // Call getAttemptSummary to validate the preflight data.
+ promise = this.quizProvider.getAttemptSummary(attempt.id, preflightData, offline, true, false, siteId);
+ }
+ } else {
+ // We're starting a new attempt, call startAttempt.
+ promise = this.quizProvider.startAttempt(quiz.id, preflightData, false, siteId).then((att) => {
+ attempt = att;
+ });
+ }
+
+ return promise.then(() => {
+ // Preflight data validated.
+ this.accessRuleDelegate.notifyPreflightCheckPassed(rules, quiz, attempt, preflightData, prefetch, siteId);
+
+ return attempt;
+ }).catch((error) => {
+ if (this.utils.isWebServiceError(error)) {
+ // The WebService returned an error, assume the preflight failed.
+ this.accessRuleDelegate.notifyPreflightCheckFailed(rules, quiz, attempt, preflightData, prefetch, siteId);
+ }
+
+ return Promise.reject(error);
+ });
+ }
+}