forked from EVOgeek/Vmeda.Online
344 lines
16 KiB
TypeScript
344 lines
16 KiB
TypeScript
// (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, NavController } from 'ionic-angular';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
import { CoreSitesProvider } from '@providers/sites';
|
|
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';
|
|
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
|
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
|
|
|
/**
|
|
* Helper service that provides some features for quiz.
|
|
*/
|
|
@Injectable()
|
|
export class AddonModQuizHelperProvider {
|
|
|
|
constructor(private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
|
|
private accessRuleDelegate: AddonModQuizAccessRuleDelegate, private quizProvider: AddonModQuizProvider,
|
|
private modalCtrl: ModalController, private quizOfflineProvider: AddonModQuizOfflineProvider,
|
|
private courseHelper: CoreCourseHelperProvider, private sitesProvider: CoreSitesProvider,
|
|
private linkHelper: CoreContentLinksHelperProvider) { }
|
|
|
|
/**
|
|
* 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.
|
|
// This means there's something wrong with some access rule or user is offline and data isn't cached.
|
|
// 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(this.domUtils.createCanceledError());
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
const element = this.domUtils.convertToElement(html);
|
|
|
|
return this.domUtils.getContentsOfElement(element, '.grade');
|
|
}
|
|
|
|
/**
|
|
* Get a quiz ID by attempt ID.
|
|
*
|
|
* @param {number} attemptId Attempt ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<number>} Promise resolved with the quiz ID.
|
|
*/
|
|
getQuizIdByAttemptId(attemptId: number, siteId?: string): Promise<number> {
|
|
// Use getAttemptReview to retrieve the quiz ID.
|
|
return this.quizProvider.getAttemptReview(attemptId, undefined, false, siteId).then((reviewData) => {
|
|
if (reviewData.attempt && reviewData.attempt.quiz) {
|
|
return reviewData.attempt.quiz;
|
|
}
|
|
|
|
return Promise.reject(null);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle a review link.
|
|
*
|
|
* @param {NavController} navCtrl Nav controller, can be undefined/null.
|
|
* @param {number} attemptId Attempt ID.
|
|
* @param {number} [page] Page to load, -1 to all questions in same page.
|
|
* @param {number} [courseId] Course ID.
|
|
* @param {number} [quizId] Quiz ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when done.
|
|
*/
|
|
handleReviewLink(navCtrl: NavController, attemptId: number, page?: number, courseId?: number, quizId?: number,
|
|
siteId?: string): Promise<any> {
|
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
|
|
|
const modal = this.domUtils.showModalLoading();
|
|
let promise;
|
|
|
|
if (quizId) {
|
|
promise = Promise.resolve(quizId);
|
|
} else {
|
|
// Retrieve the quiz ID using the attempt ID.
|
|
promise = this.getQuizIdByAttemptId(attemptId);
|
|
}
|
|
|
|
return promise.then((id) => {
|
|
quizId = id;
|
|
|
|
// Get the courseId if we don't have it.
|
|
if (courseId) {
|
|
return courseId;
|
|
} else {
|
|
return this.courseHelper.getModuleCourseIdByInstance(quizId, 'quiz', siteId);
|
|
}
|
|
}).then((courseId) => {
|
|
// Go to the review page.
|
|
const pageParams = {
|
|
quizId: quizId,
|
|
attemptId: attemptId,
|
|
courseId: courseId,
|
|
page: isNaN(page) ? -1 : page
|
|
};
|
|
|
|
return this.linkHelper.goInSite(navCtrl, 'AddonModQuizReviewPage', pageParams, siteId);
|
|
}).catch((error) => {
|
|
|
|
this.domUtils.showErrorModalDefault(error, 'An error occurred while loading the required data.');
|
|
}).finally(() => {
|
|
modal.dismiss();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
});
|
|
}
|
|
}
|