MOBILE-2348 quiz: Create functions and page to get preflight data

main
Dani Palou 2018-03-26 14:29:30 +02:00
parent fd41274d9e
commit 5a2e57796d
5 changed files with 483 additions and 11 deletions

View File

@ -0,0 +1,27 @@
<ion-header>
<ion-navbar>
<ion-title>{{ title | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding class="addon-mod_quiz-preflight-modal">
<core-loading [hideUntil]="loaded">
<form ion-list [formGroup]="preflightForm" (ngSubmit)="sendData()">
<!-- Access rules. -->
<ng-container *ngFor="let componentClass of accessRulesComponent; let last = last">
<core-dynamic-component [component]="componentClass" [data]="data">
<p padding>Couldn't find the directive to render this access rule.</p>
</core-dynamic-component>
<ion-item-divider color="light" *ngIf="!last"></ion-item-divider>
</ng-container>
<button ion-button block type="submit">
{{ title | translate }}
</button>
</form>
</core-loading>
</ion-content>

View File

@ -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 {}

View File

@ -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();
}
}

View File

@ -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<boolean>} Whether the rule requires a preflight check.
*/
isPreflightCheckRequired(quiz: any, attempt: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean>;
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean>;
/**
* 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<any>} Promise resolved when done if async, void if it's synchronous.
*/
getFixedPreflightData?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string): void | Promise<any>;
getFixedPreflightData?(quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string): void | Promise<any>;
/**
* 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<any>} 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<any> {
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<any>} Promise resolved with the component to use, undefined if not found.
*/
getPreflightComponent(rule: string, injector: Injector): Promise<any> {
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<boolean>} 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<boolean>} Promise resolved with boolean: whether it's required.
*/
isPreflightCheckRequiredForRule(rule: string, quiz: any, attempt: any, prefetch?: boolean, siteId?: string): Promise<boolean> {
return Promise.resolve(this.executeFunctionOnEnabled(rule, 'isPreflightCheckRequired', [quiz, attempt, prefetch, siteId]));
}
/**
* Notify all rules that the preflight check has passed.
*

View File

@ -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<any>} 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<any> {
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<any>} Promise resolved with the preflight data. Rejected if user cancels.
*/
getPreflightData(quiz: any, accessInfo: any, attempt: any, prefetch?: boolean, title?: string, siteId?: string): Promise<any> {
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<any>} Promise resolved when the preflight data is validated.
*/
validatePreflightData(quiz: any, accessInfo: any, preflightData: any, attempt: any, offline?: boolean, prefetch?: boolean,
siteId?: string): Promise<any> {
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);
});
}
}