2019-02-08 12:02:32 +01:00

1827 lines
70 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 { TranslateService } from '@ngx-translate/core';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSiteWSPreSets } from '@classes/site';
import { CoreGradesHelperProvider } from '@core/grades/providers/helper';
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
import { AddonModQuizOfflineProvider } from './quiz-offline';
/**
* Service that provides some features for quiz.
*/
@Injectable()
export class AddonModQuizProvider {
static COMPONENT = 'mmaModQuiz';
static ATTEMPT_FINISHED_EVENT = 'addon_mod_quiz_attempt_finished';
// Grade methods.
static GRADEHIGHEST = 1;
static GRADEAVERAGE = 2;
static ATTEMPTFIRST = 3;
static ATTEMPTLAST = 4;
// Question options.
static QUESTION_OPTIONS_MAX_ONLY = 1;
static QUESTION_OPTIONS_MARK_AND_MAX = 2;
// Attempt state.
static ATTEMPT_IN_PROGRESS = 'inprogress';
static ATTEMPT_OVERDUE = 'overdue';
static ATTEMPT_FINISHED = 'finished';
static ATTEMPT_ABANDONED = 'abandoned';
// Show the countdown timer if there is less than this amount of time left before the the quiz close date.
static QUIZ_SHOW_TIME_BEFORE_DEADLINE = 3600;
protected ROOT_CACHE_KEY = 'mmaModQuiz:';
protected logger;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
private translate: TranslateService, private textUtils: CoreTextUtilsProvider,
private gradesHelper: CoreGradesHelperProvider, private questionDelegate: CoreQuestionDelegate,
private filepoolProvider: CoreFilepoolProvider, private timeUtils: CoreTimeUtilsProvider,
private accessRulesDelegate: AddonModQuizAccessRuleDelegate, private quizOfflineProvider: AddonModQuizOfflineProvider,
private domUtils: CoreDomUtilsProvider, private logHelper: CoreCourseLogHelperProvider) {
this.logger = logger.getInstance('AddonModQuizProvider');
}
/**
* Formats a grade to be displayed.
*
* @param {number} grade Grade.
* @param {number} decimals Decimals to use.
* @return {string} Grade to display.
*/
formatGrade(grade: number, decimals: number): string {
if (typeof grade == 'undefined' || grade == -1 || grade === null) {
return this.translate.instant('addon.mod_quiz.notyetgraded');
}
return this.utils.formatFloat(this.textUtils.roundToDecimals(grade, decimals));
}
/**
* Get attempt questions. Returns all of them or just the ones in certain pages.
*
* @param {any} quiz Quiz.
* @param {any} attempt Attempt.
* @param {any} preflightData Preflight required data (like password).
* @param {number[]} [pages] List of pages to get. If not defined, all pages.
* @param {boolean} [offline] Whether it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the questions.
*/
getAllQuestionsData(quiz: any, attempt: any, preflightData: any, pages?: number[], offline?: boolean, ignoreCache?: boolean,
siteId?: string): Promise<any> {
const promises = [],
questions = {},
isSequential = this.isNavigationSequential(quiz);
if (!pages) {
pages = this.getPagesFromLayout(attempt.layout);
}
pages.forEach((page) => {
if (isSequential && page < attempt.currentpage) {
// Sequential quiz, cannot get pages before the current one.
return;
}
// Get the questions in the page.
promises.push(this.getAttemptData(attempt.id, page, preflightData, offline, ignoreCache, siteId).then((data) => {
// Add the questions to the result object.
data.questions.forEach((question) => {
questions[question.slot] = question;
});
}));
});
return Promise.all(promises).then(() => {
return questions;
});
}
/**
* Get cache key for get attempt access information WS calls.
*
* @param {number} quizId Quiz ID.
* @param {number} attemptId Attempt ID.
* @return {string} Cache key.
*/
protected getAttemptAccessInformationCacheKey(quizId: number, attemptId: number): string {
return this.getAttemptAccessInformationCommonCacheKey(quizId) + ':' + attemptId;
}
/**
* Get common cache key for get attempt access information WS calls.
*
* @param {number} quizId Quiz ID.
* @return {string} Cache key.
*/
protected getAttemptAccessInformationCommonCacheKey(quizId: number): string {
return this.ROOT_CACHE_KEY + 'attemptAccessInformation:' + quizId;
}
/**
* Get access information for an attempt.
*
* @param {number} quizId Quiz ID.
* @param {number} attemptId Attempt ID. 0 for user's last attempt.
* @param {boolean} offline Whether it should return cached data. Has priority over ignoreCache.
* @param {boolean} ignoreCache Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the access information.
*/
getAttemptAccessInformation(quizId: number, attemptId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string)
: Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
quizid: quizId,
attemptid: attemptId
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getAttemptAccessInformationCacheKey(quizId, attemptId)
};
if (offline) {
preSets.omitExpires = true;
} else if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_quiz_get_attempt_access_information', params, preSets);
});
}
/**
* Get cache key for get attempt data WS calls.
*
* @param {number} attemptId Attempt ID.
* @param {number} page Page.
* @return {string} Cache key.
*/
protected getAttemptDataCacheKey(attemptId: number, page: number): string {
return this.getAttemptDataCommonCacheKey(attemptId) + ':' + page;
}
/**
* Get common cache key for get attempt data WS calls.
*
* @param {number} attemptId Attempt ID.
* @return {string} Cache key.
*/
protected getAttemptDataCommonCacheKey(attemptId: number): string {
return this.ROOT_CACHE_KEY + 'attemptData:' + attemptId;
}
/**
* Get an attempt's data.
*
* @param {number} attemptId Attempt ID.
* @param {number} page Page number.
* @param {any} preflightData Preflight required data (like password).
* @param {boolean} [offline] Whether it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the attempt data.
*/
getAttemptData(attemptId: number, page: number, preflightData: any, offline?: boolean, ignoreCache?: boolean, siteId?: string)
: Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
attemptid: attemptId,
page: page,
preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true)
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getAttemptDataCacheKey(attemptId, page)
};
if (offline) {
preSets.omitExpires = true;
} else if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_quiz_get_attempt_data', params, preSets);
});
}
/**
* Get an attempt's due date.
*
* @param {any} quiz Quiz.
* @param {any} attempt Attempt.
* @return {number} Attempt's due date, 0 if no due date or invalid data.
*/
getAttemptDueDate(quiz: any, attempt: any): number {
const deadlines = [];
if (quiz.timelimit) {
deadlines.push(parseInt(attempt.timestart, 10) + parseInt(quiz.timelimit, 10));
}
if (quiz.timeclose) {
deadlines.push(parseInt(quiz.timeclose, 10));
}
if (!deadlines.length) {
return 0;
}
// Get min due date.
const dueDate = Math.min.apply(null, deadlines);
if (!dueDate) {
return 0;
}
switch (attempt.state) {
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
return dueDate * 1000;
case AddonModQuizProvider.ATTEMPT_OVERDUE:
return (dueDate + parseInt(quiz.graceperiod, 10)) * 1000;
default:
this.logger.warn('Unexpected state when getting due date: ' + attempt.state);
return 0;
}
}
/**
* Get an attempt's warning because of due date.
*
* @param {any} quiz Quiz.
* @param {any} attempt Attempt.
* @return {string} Attempt's warning, undefined if no due date.
*/
getAttemptDueDateWarning(quiz: any, attempt: any): string {
const dueDate = this.getAttemptDueDate(quiz, attempt);
if (attempt.state === AddonModQuizProvider.ATTEMPT_OVERDUE) {
return this.translate.instant('addon.mod_quiz.overduemustbesubmittedby', {$a: this.timeUtils.userDate(dueDate)});
} else if (dueDate) {
return this.translate.instant('addon.mod_quiz.mustbesubmittedby', {$a: this.timeUtils.userDate(dueDate)});
}
}
/**
* Turn attempt's state into a readable state, including some extra data depending on the state.
*
* @param {any} quiz Quiz.
* @param {any} attempt Attempt.
* @return {string[]} List of state sentences.
*/
getAttemptReadableState(quiz: any, attempt: any): string[] {
if (attempt.finishedOffline) {
return [this.translate.instant('addon.mod_quiz.finishnotsynced')];
}
switch (attempt.state) {
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
return [this.translate.instant('addon.mod_quiz.stateinprogress')];
case AddonModQuizProvider.ATTEMPT_OVERDUE:
const sentences = [],
dueDate = this.getAttemptDueDate(quiz, attempt);
sentences.push(this.translate.instant('addon.mod_quiz.stateoverdue'));
if (dueDate) {
sentences.push(this.translate.instant('addon.mod_quiz.stateoverduedetails',
{$a: this.timeUtils.userDate(dueDate)}));
}
return sentences;
case AddonModQuizProvider.ATTEMPT_FINISHED:
return [
this.translate.instant('addon.mod_quiz.statefinished'),
this.translate.instant('addon.mod_quiz.statefinisheddetails',
{$a: this.timeUtils.userDate(attempt.timefinish * 1000)})
];
case AddonModQuizProvider.ATTEMPT_ABANDONED:
return [this.translate.instant('addon.mod_quiz.stateabandoned')];
default:
return [];
}
}
/**
* Turn attempt's state into a readable state name, without any more data.
*
* @param {string} state State.
* @return {string} Readable state name.
*/
getAttemptReadableStateName(state: string): string {
switch (state) {
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
return this.translate.instant('addon.mod_quiz.stateinprogress');
case AddonModQuizProvider.ATTEMPT_OVERDUE:
return this.translate.instant('addon.mod_quiz.stateoverdue');
case AddonModQuizProvider.ATTEMPT_FINISHED:
return this.translate.instant('addon.mod_quiz.statefinished');
case AddonModQuizProvider.ATTEMPT_ABANDONED:
return this.translate.instant('addon.mod_quiz.stateabandoned');
default:
return '';
}
}
/**
* Get cache key for get attempt review WS calls.
*
* @param {number} attemptId Attempt ID.
* @param {number} page Page.
* @return {string} Cache key.
*/
protected getAttemptReviewCacheKey(attemptId: number, page: number): string {
return this.getAttemptReviewCommonCacheKey(attemptId) + ':' + page;
}
/**
* Get common cache key for get attempt review WS calls.
*
* @param {number} attemptId Attempt ID.
* @return {string} Cache key.
*/
protected getAttemptReviewCommonCacheKey(attemptId: number): string {
return this.ROOT_CACHE_KEY + 'attemptReview:' + attemptId;
}
/**
* Get an attempt's review.
*
* @param {number} attemptId Attempt ID.
* @param {number} [page] Page number. If not defined, return all the questions in all the pages.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the attempt review.
*/
getAttemptReview(attemptId: number, page?: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
if (typeof page == 'undefined') {
page = -1;
}
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
attemptid: attemptId,
page: page
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getAttemptReviewCacheKey(attemptId, page)
};
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_quiz_get_attempt_review', params, preSets);
});
}
/**
* Get cache key for get attempt summary WS calls.
*
* @param {number} attemptId Attempt ID.
* @return {string} Cache key.
*/
protected getAttemptSummaryCacheKey(attemptId: number): string {
return this.ROOT_CACHE_KEY + 'attemptSummary:' + attemptId;
}
/**
* Get an attempt's summary.
*
* @param {number} attemptId Attempt ID.
* @param {any} preflightData Preflight required data (like password).
* @param {boolean} [offline] Whether it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {boolean} [loadLocal] Whether it should load local state for each question. Only applicable if offline=true.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with the list of questions for the attempt summary.
*/
getAttemptSummary(attemptId: number, preflightData: any, offline?: boolean, ignoreCache?: boolean, loadLocal?: boolean,
siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
attemptid: attemptId,
preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true)
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getAttemptSummaryCacheKey(attemptId)
};
if (offline) {
preSets.omitExpires = true;
} else if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_quiz_get_attempt_summary', params, preSets).then((response) => {
if (response && response.questions) {
if (offline && loadLocal) {
return this.quizOfflineProvider.loadQuestionsLocalStates(attemptId, response.questions, site.getId());
}
return response.questions;
}
return Promise.reject(null);
});
});
}
/**
* Get cache key for get combined review options WS calls.
*
* @param {number} quizId Quiz ID.
* @param {number} userId User ID.
* @return {string} Cache key.
*/
protected getCombinedReviewOptionsCacheKey(quizId: number, userId: number): string {
return this.getCombinedReviewOptionsCommonCacheKey(quizId) + ':' + userId;
}
/**
* Get common cache key for get combined review options WS calls.
*
* @param {number} quizId Quiz ID.
* @return {string} Cache key.
*/
protected getCombinedReviewOptionsCommonCacheKey(quizId: number): string {
return this.ROOT_CACHE_KEY + 'combinedReviewOptions:' + quizId;
}
/**
* Get a quiz combined review options.
*
* @param {number} quizId Quiz ID.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined use site's current user.
* @return {Promise<any>}Promise resolved with the combined review options.
*/
getCombinedReviewOptions(quizId: number, ignoreCache?: boolean, siteId?: string, userId?: number): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
const params = {
quizid: quizId,
userid: userId
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getCombinedReviewOptionsCacheKey(quizId, userId)
};
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_quiz_get_combined_review_options', params, preSets).then((response) => {
if (response && response.someoptions && response.alloptions) {
// Convert the arrays to objects with name -> value.
response.someoptions = this.utils.objectToKeyValueMap(response.someoptions, 'name', 'value');
response.alloptions = this.utils.objectToKeyValueMap(response.alloptions, 'name', 'value');
return response;
}
return Promise.reject(null);
});
});
}
/**
* Get cache key for get feedback for grade WS calls.
*
* @param {number} quizId Quiz ID.
* @param {number} grade Grade.
* @return {string} Cache key.
*/
protected getFeedbackForGradeCacheKey(quizId: number, grade: number): string {
return this.getFeedbackForGradeCommonCacheKey(quizId) + ':' + grade;
}
/**
* Get common cache key for get feedback for grade WS calls.
*
* @param {number} quizId Quiz ID.
* @return {string} Cache key.
*/
protected getFeedbackForGradeCommonCacheKey(quizId: number): string {
return this.ROOT_CACHE_KEY + 'feedbackForGrade:' + quizId;
}
/**
* Get the feedback for a certain grade.
*
* @param {number} quizId Quiz ID.
* @param {number} grade Grade.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the feedback.
*/
getFeedbackForGrade(quizId: number, grade: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
quizid: quizId,
grade: grade
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getFeedbackForGradeCacheKey(quizId, grade)
};
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_quiz_get_quiz_feedback_for_grade', params, preSets);
});
}
/**
* Determine the correct number of decimal places required to format a grade.
* Based on Moodle's quiz_get_grade_format.
*
* @param {any} quiz Quiz.
* @return {number} Number of decimals.
*/
getGradeDecimals(quiz: any): number {
if (typeof quiz.questiondecimalpoints == 'undefined') {
quiz.questiondecimalpoints = -1;
}
if (quiz.questiondecimalpoints == -1) {
return quiz.decimalpoints;
}
return quiz.questiondecimalpoints;
}
/**
* Gets a quiz grade and feedback from the gradebook.
*
* @param {number} courseId Course ID.
* @param {number} moduleId Quiz module ID.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined use site's current user.
* @return {Promise<any>} Promise resolved with an object containing the grade and the feedback.
*/
getGradeFromGradebook(courseId: number, moduleId: number, ignoreCache?: boolean, siteId?: string, userId?: number)
: Promise<any> {
return this.gradesHelper.getGradeModuleItems(courseId, moduleId, userId, null, siteId, ignoreCache).then((items) => {
return items.shift();
});
}
/**
* Given a list of attempts, returns the last finished attempt.
*
* @param {any[]} attempts Attempts.
* @return {any} Last finished attempt.
*/
getLastFinishedAttemptFromList(attempts: any[]): any {
if (attempts && attempts.length) {
for (let i = attempts.length - 1; i >= 0; i--) {
const attempt = attempts[i];
if (this.isAttemptFinished(attempt.state)) {
return attempt;
}
}
}
}
/**
* Given a list of questions, check if the quiz can be submitted.
* Will return an array with the messages to prevent the submit. Empty array if quiz can be submitted.
*
* @param {any[]} questions Questions.
* @return {string[]} List of prevent submit messages. Empty array if quiz can be submitted.
*/
getPreventSubmitMessages(questions: any[]): string[] {
const messages = [];
questions.forEach((question) => {
let message = this.questionDelegate.getPreventSubmitMessage(question);
if (message) {
message = this.translate.instant(message);
messages.push(this.translate.instant('core.question.questionmessage', {$a: question.slot, $b: message}));
}
});
return messages;
}
/**
* Get cache key for quiz data WS calls.
*
* @param {number} courseId Course ID.
* @return {string} Cache key.
*/
protected getQuizDataCacheKey(courseId: number): string {
return this.ROOT_CACHE_KEY + 'quiz:' + courseId;
}
/**
* Get a Quiz with key=value. If more than one is found, only the first will be returned.
*
* @param {number} courseId Course ID.
* @param {string} key Name of the property to check.
* @param {any} value Value to search.
* @param {boolean} [forceCache] Whether it should always return cached data.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the Quiz is retrieved.
*/
protected getQuizByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
courseids: [courseId]
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getQuizDataCacheKey(courseId)
};
if (forceCache) {
preSets.omitExpires = true;
}
return site.read('mod_quiz_get_quizzes_by_courses', params, preSets).then((response) => {
if (response && response.quizzes) {
// Search the quiz.
for (const i in response.quizzes) {
const quiz = response.quizzes[i];
if (quiz[key] == value) {
return quiz;
}
}
}
return Promise.reject(null);
});
});
}
/**
* Get a quiz by module ID.
*
* @param {number} courseId Course ID.
* @param {number} cmId Course module ID.
* @param {boolean} [forceCache] Whether it should always return cached data.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the quiz is retrieved.
*/
getQuiz(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise<any> {
return this.getQuizByField(courseId, 'coursemodule', cmId, forceCache, siteId);
}
/**
* Get a quiz by quiz ID.
*
* @param {number} courseId Course ID.
* @param {number} id Quiz ID.
* @param {boolean} [forceCache] Whether it should always return cached data.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the quiz is retrieved.
*/
getQuizById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise<any> {
return this.getQuizByField(courseId, 'id', id, forceCache, siteId);
}
/**
* Get cache key for get quiz access information WS calls.
*
* @param {number} quizId Quiz ID.
* @return {string} Cache key.
*/
protected getQuizAccessInformationCacheKey(quizId: number): string {
return this.ROOT_CACHE_KEY + 'quizAccessInformation:' + quizId;
}
/**
* Get access information for an attempt.
*
* @param {number} quizId Quiz ID.
* @param {boolean} [offline] Whether it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the access information.
*/
getQuizAccessInformation(quizId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
quizid: quizId
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getQuizAccessInformationCacheKey(quizId)
};
if (offline) {
preSets.omitExpires = true;
} else if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_quiz_get_quiz_access_information', params, preSets);
});
}
/**
* Get a readable Quiz grade method.
*
* @param {number|string} method Grading method.
* @return {string} Readable grading method.
*/
getQuizGradeMethod(method: number | string): string {
if (typeof method == 'string') {
method = parseInt(method, 10);
}
switch (method) {
case AddonModQuizProvider.GRADEHIGHEST:
return this.translate.instant('addon.mod_quiz.gradehighest');
case AddonModQuizProvider.GRADEAVERAGE:
return this.translate.instant('addon.mod_quiz.gradeaverage');
case AddonModQuizProvider.ATTEMPTFIRST:
return this.translate.instant('addon.mod_quiz.attemptfirst');
case AddonModQuizProvider.ATTEMPTLAST:
return this.translate.instant('addon.mod_quiz.attemptlast');
default:
return '';
}
}
/**
* Get cache key for get quiz required qtypes WS calls.
*
* @param {number} quizId Quiz ID.
* @return {string} Cache key.
*/
protected getQuizRequiredQtypesCacheKey(quizId: number): string {
return this.ROOT_CACHE_KEY + 'quizRequiredQtypes:' + quizId;
}
/**
* Get the potential question types that would be required for a given quiz.
*
* @param {number} quizId Quiz ID.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the access information.
*/
getQuizRequiredQtypes(quizId: number, ignoreCache?: boolean, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
quizid: quizId
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getQuizRequiredQtypesCacheKey(quizId)
};
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_quiz_get_quiz_required_qtypes', params, preSets).then((response) => {
if (response && response.questiontypes) {
return response.questiontypes;
}
return Promise.reject(null);
});
});
}
/**
* Given an attempt's layout, return the list of pages.
*
* @param {string} layout Attempt's layout.
* @return {number[]} Pages.
* @description
* An attempt's layout is a string with the question numbers separated by commas. A 0 indicates a change of page.
* Example: 1,2,3,0,4,5,6,0
* In the example above, first page has questions 1, 2 and 3. Second page has questions 4, 5 and 6.
*
* This function returns a list of pages.
*/
getPagesFromLayout(layout: string): number[] {
const split = layout.split(','),
pages: number[] = [];
let page = 0;
for (let i = 0; i < split.length; i++) {
if (split[i] == '0') {
pages.push(page);
page++;
}
}
return pages;
}
/**
* Given an attempt's layout and a list of questions identified by question slot,
* return the list of pages that have at least 1 of the questions.
*
* @param {string} layout Attempt's layout.
* @param {any} questions List of questions. It needs to be an object where the keys are question slot.
* @return {number[]} Pages.
* @description
* An attempt's layout is a string with the question numbers separated by commas. A 0 indicates a change of page.
* Example: 1,2,3,0,4,5,6,0
* In the example above, first page has questions 1, 2 and 3. Second page has questions 4, 5 and 6.
*
* This function returns a list of pages.
*/
getPagesFromLayoutAndQuestions(layout: string, questions: any): number[] {
const split = layout.split(','),
pages: number[] = [];
let page = 0,
pageAdded = false;
for (let i = 0; i < split.length; i++) {
const value = Number(split[i]);
if (value == 0) {
page++;
pageAdded = false;
} else if (!pageAdded && questions[value]) {
pages.push(page);
pageAdded = true;
}
}
return pages;
}
/**
* Given a list of question types, returns the types that aren't supported.
*
* @param {string[]} questionTypes Question types to check.
* @return {string[]} Not supported question types.
*/
getUnsupportedQuestions(questionTypes: string[]): string[] {
const notSupported = [];
questionTypes.forEach((type) => {
if (type != 'random' && !this.questionDelegate.isQuestionSupported(type)) {
notSupported.push(type);
}
});
return notSupported;
}
/**
* Given a list of access rules names, returns the rules that aren't supported.
*
* @param {string[]} rulesNames Rules to check.
* @return {string[]} Not supported rules names.
*/
getUnsupportedRules(rulesNames: string[]): string[] {
const notSupported = [];
rulesNames.forEach((name) => {
if (!this.accessRulesDelegate.isAccessRuleSupported(name)) {
notSupported.push(name);
}
});
return notSupported;
}
/**
* Get cache key for get user attempts WS calls.
*
* @param {number} quizId Quiz ID.
* @param {number} userId User ID.
* @return {string} Cache key.
*/
protected getUserAttemptsCacheKey(quizId: number, userId: number): string {
return this.getUserAttemptsCommonCacheKey(quizId) + ':' + userId;
}
/**
* Get common cache key for get user attempts WS calls.
*
* @param {number} quizId Quiz ID.
* @return {string} Cache key.
*/
protected getUserAttemptsCommonCacheKey(quizId: number): string {
return this.ROOT_CACHE_KEY + 'userAttempts:' + quizId;
}
/**
* Get quiz attempts for a certain user.
*
* @param {number} quizId Quiz ID.
* @param {number} [status=all] Status of the attempts to get. By default, 'all'.
* @param {boolean} [includePreviews=true] Whether to include previews. Defaults to true.
* @param {boolean} [offline] Whether it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined use site's current user.
* @return {Promise<any[]>} Promise resolved with the attempts.
*/
getUserAttempts(quizId: number, status: string = 'all', includePreviews: boolean = true, offline?: boolean,
ignoreCache?: boolean, siteId?: string, userId?: number): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
const params = {
quizid: quizId,
userid: userId,
status: status,
includepreviews: includePreviews ? 1 : 0
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getUserAttemptsCacheKey(quizId, userId)
};
if (offline) {
preSets.omitExpires = true;
} else if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_quiz_get_user_attempts', params, preSets).then((response) => {
if (response && response.attempts) {
return response.attempts;
}
return Promise.reject(null);
});
});
}
/**
* Get cache key for get user best grade WS calls.
*
* @param {number} quizId Quiz ID.
* @param {number} userId User ID.
* @return {string} Cache key.
*/
protected getUserBestGradeCacheKey(quizId: number, userId: number): string {
return this.getUserBestGradeCommonCacheKey(quizId) + ':' + userId;
}
/**
* Get common cache key for get user best grade WS calls.
*
* @param {number} quizId Quiz ID.
* @return {string} Cache key.
*/
protected getUserBestGradeCommonCacheKey(quizId: number): string {
return this.ROOT_CACHE_KEY + 'userBestGrade:' + quizId;
}
/**
* Get best grade in a quiz for a certain user.
*
* @param {number} quizId Quiz ID.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined use site's current user.
* @return {Promise<any>} Promise resolved with the best grade data.
*/
getUserBestGrade(quizId: number, ignoreCache?: boolean, siteId?: string, userId?: number): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
const params = {
quizid: quizId,
userid: userId
},
preSets: CoreSiteWSPreSets = {
cacheKey: this.getUserBestGradeCacheKey(quizId, userId)
};
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('mod_quiz_get_user_best_grade', params, preSets);
});
}
/**
* Invalidates all the data related to a certain quiz.
*
* @param {number} quizId Quiz ID.
* @param {number} [courseId] Course ID.
* @param {number} [attemptId] Attempt ID to invalidate some WS calls.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined use site's current user.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateAllQuizData(quizId: number, courseId?: number, attemptId?: number, siteId?: string, userId?: number): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const promises = [];
promises.push(this.invalidateAttemptAccessInformation(quizId, siteId));
promises.push(this.invalidateCombinedReviewOptionsForUser(quizId, siteId, userId));
promises.push(this.invalidateFeedback(quizId, siteId));
promises.push(this.invalidateQuizAccessInformation(quizId, siteId));
promises.push(this.invalidateQuizRequiredQtypes(quizId, siteId));
promises.push(this.invalidateUserAttemptsForUser(quizId, siteId, userId));
promises.push(this.invalidateUserBestGradeForUser(quizId, siteId, userId));
if (attemptId) {
promises.push(this.invalidateAttemptData(attemptId, siteId));
promises.push(this.invalidateAttemptReview(attemptId, siteId));
promises.push(this.invalidateAttemptSummary(attemptId, siteId));
}
if (courseId) {
promises.push(this.invalidateGradeFromGradebook(courseId, siteId, userId));
}
return Promise.all(promises);
}
/**
* Invalidates attempt access information for all attempts in a quiz.
*
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateAttemptAccessInformation(quizId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKeyStartingWith(this.getAttemptAccessInformationCommonCacheKey(quizId));
});
}
/**
* Invalidates attempt access information for an attempt.
*
* @param {number} quizId Quiz ID.
* @param {number} attemptId Attempt ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateAttemptAccessInformationForAttempt(quizId: number, attemptId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getAttemptAccessInformationCacheKey(quizId, attemptId));
});
}
/**
* Invalidates attempt data for all pages.
*
* @param {number} attemptId Attempt ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateAttemptData(attemptId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKeyStartingWith(this.getAttemptDataCommonCacheKey(attemptId));
});
}
/**
* Invalidates attempt data for a certain page.
*
* @param {number} attemptId Attempt ID.
* @param {number} page Page.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateAttemptDataForPage(attemptId: number, page: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getAttemptDataCacheKey(attemptId, page));
});
}
/**
* Invalidates attempt review for all pages.
*
* @param {number} attemptId Attempt ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateAttemptReview(attemptId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKeyStartingWith(this.getAttemptReviewCommonCacheKey(attemptId));
});
}
/**
* Invalidates attempt review for a certain page.
*
* @param {number} attemptId Attempt ID.
* @param {number} page Page.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateAttemptReviewForPage(attemptId: number, page: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getAttemptReviewCacheKey(attemptId, page));
});
}
/**
* Invalidates attempt summary.
*
* @param {number} attemptId Attempt ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateAttemptSummary(attemptId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getAttemptSummaryCacheKey(attemptId));
});
}
/**
* Invalidates combined review options for all users.
*
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateCombinedReviewOptions(quizId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKeyStartingWith(this.getCombinedReviewOptionsCommonCacheKey(quizId));
});
}
/**
* Invalidates combined review options for a certain user.
*
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined use site's current user.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateCombinedReviewOptionsForUser(quizId: number, siteId?: string, userId?: number): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
return site.invalidateWsCacheForKey(this.getCombinedReviewOptionsCacheKey(quizId, userId));
});
}
/**
* Invalidate the prefetched content except files.
* To invalidate files, use AddonModQuizProvider.invalidateFiles.
*
* @param {number} moduleId The module ID.
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Get required data to call the invalidate functions.
return this.getQuiz(courseId, moduleId, false, siteId).then((quiz) => {
return this.getUserAttempts(quiz.id, 'all', true, false, false, siteId).then((attempts) => {
// Now invalidate it.
const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined;
return this.invalidateAllQuizData(quiz.id, courseId, lastAttemptId, siteId);
});
});
}
/**
* Invalidates feedback for all grades of a quiz.
*
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateFeedback(quizId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKeyStartingWith(this.getFeedbackForGradeCommonCacheKey(quizId));
});
}
/**
* Invalidates feedback for a certain grade.
*
* @param {number} quizId Quiz ID.
* @param {number} grade Grade.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateFeedbackForGrade(quizId: number, grade: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getFeedbackForGradeCacheKey(quizId, grade));
});
}
/**
* Invalidate the prefetched files.
*
* @param {number} moduleId The module ID.
* @return {Promise<any>} Promise resolved when the files are invalidated.
*/
invalidateFiles(moduleId: number): Promise<any> {
return this.filepoolProvider.invalidateFilesByComponent(this.sitesProvider.getCurrentSiteId(),
AddonModQuizProvider.COMPONENT, moduleId);
}
/**
* Invalidates grade from gradebook for a certain user.
*
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined use site's current user.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateGradeFromGradebook(courseId: number, siteId?: string, userId?: number): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
return this.gradesHelper.invalidateGradeModuleItems(courseId, userId, null, siteId);
});
}
/**
* Invalidates quiz access information for a quiz.
*
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateQuizAccessInformation(quizId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getQuizAccessInformationCacheKey(quizId));
});
}
/**
* Invalidates required qtypes for a quiz.
*
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateQuizRequiredQtypes(quizId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getQuizRequiredQtypesCacheKey(quizId));
});
}
/**
* Invalidates user attempts for all users.
*
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateUserAttempts(quizId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKeyStartingWith(this.getUserAttemptsCommonCacheKey(quizId));
});
}
/**
* Invalidates user attempts for a certain user.
*
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined use site's current user.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateUserAttemptsForUser(quizId: number, siteId?: string, userId?: number): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
return site.invalidateWsCacheForKey(this.getUserAttemptsCacheKey(quizId, userId));
});
}
/**
* Invalidates user best grade for all users.
*
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateUserBestGrade(quizId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKeyStartingWith(this.getUserBestGradeCommonCacheKey(quizId));
});
}
/**
* Invalidates user best grade for a certain user.
*
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined use site's current user.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateUserBestGradeForUser(quizId: number, siteId?: string, userId?: number): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
return site.invalidateWsCacheForKey(this.getUserBestGradeCacheKey(quizId, userId));
});
}
/**
* Invalidates quiz data.
*
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateQuizData(courseId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getQuizDataCacheKey(courseId));
});
}
/**
* Check if an attempt is finished based on its state.
*
* @param {string} state Attempt's state.
* @return {boolean} Whether it's finished.
*/
isAttemptFinished(state: string): boolean {
return state == AddonModQuizProvider.ATTEMPT_FINISHED || state == AddonModQuizProvider.ATTEMPT_ABANDONED;
}
/**
* Check if an attempt is finished in offline but not synced.
*
* @param {number} attemptId Attempt ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: true if finished in offline but not synced, false otherwise.
*/
isAttemptFinishedOffline(attemptId: number, siteId?: string): Promise<boolean> {
return this.quizOfflineProvider.getAttemptById(attemptId, siteId).then((attempt) => {
return !!attempt.finished;
}).catch(() => {
return false;
});
}
/**
* Check if an attempt is nearly over. We consider an attempt nearly over or over if:
* - Is not in progress
* OR
* - It finished before autosaveperiod passes.
*
* @param {any} quiz Quiz.
* @param {any} attempt Attempt.
* @return {boolean} Whether it's nearly over or over.
*/
isAttemptTimeNearlyOver(quiz: any, attempt: any): boolean {
if (attempt.state != AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
// Attempt not in progress, return true.
return true;
}
const dueDate = this.getAttemptDueDate(quiz, attempt),
autoSavePeriod = quiz.autosaveperiod || 0;
if (dueDate > 0 && Date.now() + autoSavePeriod >= dueDate) {
return true;
}
return false;
}
/**
* Check if last attempt is offline and unfinished.
*
* @param {number} attemptId Attempt ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined, user current site's user.
* @return {Promise<boolean>} Promise resolved with boolean: true if last offline attempt is unfinished, false otherwise.
*/
isLastAttemptOfflineUnfinished(quiz: any, siteId?: string, userId?: number): Promise<boolean> {
return this.quizOfflineProvider.getQuizAttempts(quiz.id, siteId, userId).then((attempts) => {
const last = attempts.pop();
return last && !last.finished;
}).catch(() => {
return false;
});
}
/**
* Check if a quiz navigation is sequential.
*
* @param {any} quiz Quiz.
* @return {boolean} Whether navigation is sequential.
*/
isNavigationSequential(quiz: any): boolean {
return quiz.navmethod == 'sequential';
}
/**
* Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the quiz WS are available.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {boolean} Whether the plugin is enabled.
*/
isPluginEnabled(siteId?: string): boolean {
// Quiz WebServices were introduced in 3.1, it will always be enabled.
return true;
}
/**
* Check if a question is blocked.
*
* @param {any} question Question.
* @return {boolean} Whether it's blocked.
*/
isQuestionBlocked(question: any): boolean {
const element = this.domUtils.convertToElement(question.html);
return !!element.querySelector('.mod_quiz-blocked_question_warning');
}
/**
* Check if a quiz is enabled to be used in offline.
*
* @param {any} quiz Quiz.
* @return {boolean} Whether offline is enabled.
*/
isQuizOffline(quiz: any): boolean {
// Don't allow downloading the quiz if offline is disabled to prevent wasting a lot of data when opening it.
return !!quiz.allowofflineattempts && !this.sitesProvider.getCurrentSite().isOfflineDisabled();
}
/**
* Given a list of attempts, add finishedOffline=true to those attempts that are finished in offline but not synced.
*
* @param {any[]} attempts List of attempts.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<void>} Promise resolved when done.
*/
loadFinishedOfflineData(attempts: any[], siteId?: string): Promise<void> {
if (attempts.length) {
// We only need to check the last attempt because the user can only have 1 local attempt.
const lastAttempt = attempts[attempts.length - 1];
return this.isAttemptFinishedOffline(lastAttempt.id, siteId).then((finished) => {
lastAttempt.finishedOffline = finished;
});
}
return Promise.resolve();
}
/**
* Report an attempt as being viewed. It did not store logs offline because order of the log is important.
*
* @param {number} attemptId Attempt ID.
* @param {number} [page=0] Page number.
* @param {any} [preflightData] Preflight required data (like password).
* @param {boolean} [offline] Whether attempt is offline.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
logViewAttempt(attemptId: number, page: number = 0, preflightData: any = {}, offline?: boolean, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
attemptid: attemptId,
page: page,
preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true)
},
promises = [];
promises.push(site.write('mod_quiz_view_attempt', params));
if (offline) {
promises.push(this.quizOfflineProvider.setAttemptCurrentPage(attemptId, page, site.getId()));
}
return Promise.all(promises);
});
}
/**
* Report an attempt's review as being viewed.
*
* @param {number} attemptId Attempt ID.
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
logViewAttemptReview(attemptId: number, quizId: number, siteId?: string): Promise<any> {
const params = {
attemptid: attemptId
};
return this.logHelper.log('mod_quiz_view_attempt_review', params, AddonModQuizProvider.COMPONENT, quizId, siteId);
}
/**
* Report an attempt's summary as being viewed.
*
* @param {number} attemptId Attempt ID.
* @param {any} preflightData Preflight required data (like password).
* @param {number} quizId Quiz ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
logViewAttemptSummary(attemptId: number, preflightData: any, quizId: number, siteId?: string): Promise<any> {
const params = {
attemptid: attemptId,
preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true)
};
return this.logHelper.log('mod_quiz_view_attempt_summary', params, AddonModQuizProvider.COMPONENT, quizId, siteId);
}
/**
* Report a quiz as being viewed.
*
* @param {number} id Module ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
logViewQuiz(id: number, siteId?: string): Promise<any> {
const params = {
quizid: id
};
return this.logHelper.log('mod_quiz_view_quiz', params, AddonModQuizProvider.COMPONENT, id, siteId);
}
/**
* Process an attempt, saving its data.
*
* @param {any} quiz Quiz.
* @param {any} attempt Attempt.
* @param {any} data Data to save.
* @param {any} preflightData Preflight required data (like password).
* @param {boolean} [finish] Whether to finish the quiz.
* @param {boolean} [timeUp] Whether the quiz time is up, false otherwise.
* @param {boolean} [offline] Whether the attempt is offline.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
*/
processAttempt(quiz: any, attempt: any, data: any, preflightData: any, finish?: boolean, timeUp?: boolean, offline?: boolean,
siteId?: string): Promise<any> {
if (offline) {
return this.processAttemptOffline(quiz, attempt, data, preflightData, finish, siteId);
}
return this.processAttemptOnline(attempt.id, data, preflightData, finish, timeUp, siteId);
}
/**
* Process an online attempt, saving its data.
*
* @param {number} attemptId Attempt ID.
* @param {any} data Data to save.
* @param {any} preflightData Preflight required data (like password).
* @param {boolean} [finish] Whether to finish the quiz.
* @param {boolean} [timeUp] Whether the quiz time is up, false otherwise.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
*/
protected processAttemptOnline(attemptId: number, data: any, preflightData: any, finish?: boolean, timeUp?: boolean,
siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
attemptid: attemptId,
data: this.utils.objectToArrayOfObjects(data, 'name', 'value'),
finishattempt: finish ? 1 : 0,
timeup: timeUp ? 1 : 0,
preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value')
};
return site.write('mod_quiz_process_attempt', params).then((response) => {
if (response && response.warnings && response.warnings.length) {
// Reject with the first warning.
return Promise.reject(response.warnings[0]);
} else if (response && response.state) {
return response.state;
}
return Promise.reject(null);
});
});
}
/**
* Process an offline attempt, saving its data.
*
* @param {any} quiz Quiz.
* @param {any} attempt Attempt.
* @param {any} data Data to save.
* @param {any} preflightData Preflight required data (like password).
* @param {boolean} [finish] Whether to finish the quiz.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
*/
protected processAttemptOffline(quiz: any, attempt: any, data: any, preflightData: any, finish?: boolean, siteId?: string)
: Promise<any> {
// Get attempt summary to have the list of questions.
return this.getAttemptSummary(attempt.id, preflightData, true, false, true, siteId).then((questionArray) => {
// Convert the question array to an object.
const questions = this.utils.arrayToObject(questionArray, 'slot');
return this.quizOfflineProvider.processAttempt(quiz, attempt, questions, data, finish, siteId);
});
}
/**
* Check if it's a graded quiz. Based on Moodle's quiz_has_grades.
*
* @param {any} quiz Quiz.
* @return {boolean} Whether quiz is graded.
*/
quizHasGrades(quiz: any): boolean {
return quiz.grade >= 0.000005 && quiz.sumgrades >= 0.000005;
}
/**
* Convert the raw grade into a grade out of the maximum grade for this quiz.
* Based on Moodle's quiz_rescale_grade.
*
* @param {string} rawGrade The unadjusted grade, for example attempt.sumgrades.
* @param {any} quiz Quiz.
* @param {boolean|string} format True to format the results for display, 'question' to format a question grade
* (different number of decimal places), false to not format it.
* @return {string} Grade to display.
*/
rescaleGrade(rawGrade: string, quiz: any, format: boolean | string = true): string {
let grade: number;
const rawGradeNum = parseFloat(rawGrade);
if (!isNaN(rawGradeNum)) {
if (quiz.sumgrades >= 0.000005) {
grade = rawGradeNum * quiz.grade / quiz.sumgrades;
} else {
grade = 0;
}
}
if (format === 'question') {
return this.formatGrade(grade, this.getGradeDecimals(quiz));
} else if (format) {
return this.formatGrade(grade, quiz.decimalpoints);
}
if (grade === null) {
return null;
} else if (typeof grade == 'undefined') {
return undefined;
}
return String(grade);
}
/**
* Save an attempt data.
*
* @param {any} quiz Quiz.
* @param {any} attempt Attempt.
* @param {any} data Data to save.
* @param {any} preflightData Preflight required data (like password).
* @param {boolean} [offline] Whether attempt is offline.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
*/
saveAttempt(quiz: any, attempt: any, data: any, preflightData: any, offline?: boolean, siteId?: string): Promise<any> {
try {
if (offline) {
return this.processAttemptOffline(quiz, attempt, data, preflightData, false, siteId);
}
return this.saveAttemptOnline(attempt.id, data, preflightData, siteId);
} catch (ex) {
this.logger.error(ex);
return Promise.reject(null);
}
}
/**
* Save an attempt data.
*
* @param {number} attemptId Attempt ID.
* @param {any} data Data to save.
* @param {any} preflightData Preflight required data (like password).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<void>} Promise resolved in success, rejected otherwise.
*/
protected saveAttemptOnline(attemptId: number, data: any, preflightData: any, siteId?: string): Promise<void> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
attemptid: attemptId,
data: this.utils.objectToArrayOfObjects(data, 'name', 'value'),
preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value')
};
return site.write('mod_quiz_save_attempt', params).then((response) => {
if (response && response.warnings && response.warnings.length) {
// Reject with the first warning.
return Promise.reject(response.warnings[0]);
} else if (!response || !response.status) {
return Promise.reject(null);
}
});
});
}
/**
* Check if time left should be shown.
*
* @param {string[]} rules List of active rules names.
* @param {any} attempt Attempt.
* @param {number} endTime The attempt end time (in seconds).
* @return {boolean} Whether time left should be displayed.
*/
shouldShowTimeLeft(rules: string[], attempt: any, endTime: number): boolean {
const timeNow = this.timeUtils.timestamp();
if (attempt.state != AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
return false;
}
return this.accessRulesDelegate.shouldShowTimeLeft(rules, attempt, endTime, timeNow);
}
/**
* Start an attempt.
*
* @param {number} quizId Quiz ID.
* @param {any} preflightData Preflight required data (like password).
* @param {boolean} [forceNew] Whether to force a new attempt or not.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the attempt data.
*/
startAttempt(quizId: number, preflightData: any, forceNew?: boolean, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
quizid: quizId,
preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true),
forcenew: forceNew ? 1 : 0
};
return site.write('mod_quiz_start_attempt', params).then((response) => {
if (response && response.warnings && response.warnings.length) {
// Reject with the first warning.
return Promise.reject(response.warnings[0]);
} else if (response && response.attempt) {
return response.attempt;
}
return Promise.reject(null);
});
});
}
}