2402 lines
83 KiB
TypeScript

// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreError } from '@classes/errors/error';
import { CoreWSError } from '@classes/errors/wserror';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreGradesFormattedItem, CoreGradesHelper } from '@features/grades/services/grades-helper';
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
import {
CoreQuestion,
CoreQuestionQuestionParsed,
CoreQuestionQuestionWSData,
CoreQuestionsAnswers,
} from '@features/question/services/question';
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
import { makeSingleton, Translate } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
import { AddonModQuizAttempt } from './quiz-helper';
import { AddonModQuizOffline, AddonModQuizQuestionsWithAnswers } from './quiz-offline';
import { AddonModQuizAutoSyncData, AddonModQuizSyncProvider } from './quiz-sync';
const ROOT_CACHE_KEY = 'mmaModQuiz:';
declare module '@singletons/events' {
/**
* Augment CoreEventsData interface with events specific to this service.
*
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CoreEventsData {
[AddonModQuizProvider.ATTEMPT_FINISHED_EVENT]: AddonModQuizAttemptFinishedData;
[AddonModQuizSyncProvider.AUTO_SYNCED]: AddonModQuizAutoSyncData;
}
}
/**
* Service that provides some features for quiz.
*/
@Injectable({ providedIn: 'root' })
export class AddonModQuizProvider {
static readonly COMPONENT = 'mmaModQuiz';
static readonly ATTEMPT_FINISHED_EVENT = 'addon_mod_quiz_attempt_finished';
// Grade methods.
static readonly GRADEHIGHEST = 1;
static readonly GRADEAVERAGE = 2;
static readonly ATTEMPTFIRST = 3;
static readonly ATTEMPTLAST = 4;
// Question options.
static readonly QUESTION_OPTIONS_MAX_ONLY = 1;
static readonly QUESTION_OPTIONS_MARK_AND_MAX = 2;
// Attempt state.
static readonly ATTEMPT_IN_PROGRESS = 'inprogress';
static readonly ATTEMPT_OVERDUE = 'overdue';
static readonly ATTEMPT_FINISHED = 'finished';
static readonly ATTEMPT_ABANDONED = 'abandoned';
// Show the countdown timer if there is less than this amount of time left before the the quiz close date.
static readonly QUIZ_SHOW_TIME_BEFORE_DEADLINE = 3600;
protected logger: CoreLogger;
constructor() {
this.logger = CoreLogger.getInstance('AddonModQuizProvider');
}
/**
* Formats a grade to be displayed.
*
* @param grade Grade.
* @param decimals Decimals to use.
* @return Grade to display.
*/
formatGrade(grade?: number | null, decimals?: number): string {
if (grade === undefined || grade == -1 || grade === null || isNaN(grade)) {
return Translate.instant('addon.mod_quiz.notyetgraded');
}
return CoreUtils.formatFloat(CoreTextUtils.roundToDecimals(grade, decimals));
}
/**
* Get attempt questions. Returns all of them or just the ones in certain pages.
*
* @param quiz Quiz.
* @param attempt Attempt.
* @param preflightData Preflight required data (like password).
* @param options Other options.
* @return Promise resolved with the questions.
*/
async getAllQuestionsData(
quiz: AddonModQuizQuizWSData,
attempt: AddonModQuizAttemptWSData,
preflightData: Record<string, string>,
options: AddonModQuizAllQuestionsDataOptions = {},
): Promise<Record<number, CoreQuestionQuestionParsed>> {
const questions: Record<number, CoreQuestionQuestionParsed> = {};
const isSequential = this.isNavigationSequential(quiz);
const pages = options.pages || this.getPagesFromLayout(attempt.layout);
await Promise.all(pages.map(async (page) => {
if (isSequential && page < (attempt.currentpage || 0)) {
// Sequential quiz, cannot get pages before the current one.
return;
}
// Get the questions in the page.
const data = await this.getAttemptData(attempt.id, page, preflightData, options);
// Add the questions to the result object.
data.questions.forEach((question) => {
questions[question.slot] = question;
});
}));
return questions;
}
/**
* Get cache key for get attempt access information WS calls.
*
* @param quizId Quiz ID.
* @param attemptId Attempt ID.
* @return 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 quizId Quiz ID.
* @return Cache key.
*/
protected getAttemptAccessInformationCommonCacheKey(quizId: number): string {
return ROOT_CACHE_KEY + 'attemptAccessInformation:' + quizId;
}
/**
* Get access information for an attempt.
*
* @param quizId Quiz ID.
* @param attemptId Attempt ID. 0 for user's last attempt.
* @param options Other options.
* @return Promise resolved with the access information.
*/
async getAttemptAccessInformation(
quizId: number,
attemptId: number,
options: CoreCourseCommonModWSOptions = {},
): Promise<AddonModQuizGetAttemptAccessInformationWSResponse> {
const site = await CoreSites.getSite(options.siteId);
const params: AddonModQuizGetAttemptAccessInformationWSParams = {
quizid: quizId,
attemptid: attemptId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getAttemptAccessInformationCacheKey(quizId, attemptId),
component: AddonModQuizProvider.COMPONENT,
componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
return site.read('mod_quiz_get_attempt_access_information', params, preSets);
}
/**
* Get cache key for get attempt data WS calls.
*
* @param attemptId Attempt ID.
* @param page Page.
* @return 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 attemptId Attempt ID.
* @return Cache key.
*/
protected getAttemptDataCommonCacheKey(attemptId: number): string {
return ROOT_CACHE_KEY + 'attemptData:' + attemptId;
}
/**
* Get an attempt's data.
*
* @param attemptId Attempt ID.
* @param page Page number.
* @param preflightData Preflight required data (like password).
* @param options Other options.
* @return Promise resolved with the attempt data.
*/
async getAttemptData(
attemptId: number,
page: number,
preflightData: Record<string, string>,
options: CoreCourseCommonModWSOptions = {},
): Promise<AddonModQuizGetAttemptDataResponse> {
const site = await CoreSites.getSite(options.siteId);
const params: AddonModQuizGetAttemptDataWSParams = {
attemptid: attemptId,
page: page,
preflightdata: CoreUtils.objectToArrayOfObjects<AddonModQuizPreflightDataWSParam>(
preflightData,
'name',
'value',
true,
),
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getAttemptDataCacheKey(attemptId, page),
component: AddonModQuizProvider.COMPONENT,
componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
const result = await site.read<AddonModQuizGetAttemptDataWSResponse>('mod_quiz_get_attempt_data', params, preSets);
result.questions = CoreQuestion.parseQuestions(result.questions);
return result;
}
/**
* Get an attempt's due date.
*
* @param quiz Quiz.
* @param attempt Attempt.
* @return Attempt's due date, 0 if no due date or invalid data.
*/
getAttemptDueDate(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttemptWSData): number {
const deadlines: number[] = [];
if (quiz.timelimit && attempt.timestart) {
deadlines.push(attempt.timestart + quiz.timelimit);
}
if (quiz.timeclose) {
deadlines.push(quiz.timeclose);
}
if (!deadlines.length) {
return 0;
}
// Get min due date.
const dueDate: number = 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 + quiz.graceperiod!) * 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 quiz Quiz.
* @param attempt Attempt.
* @return Attempt's warning, undefined if no due date.
*/
getAttemptDueDateWarning(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttemptWSData): string | undefined {
const dueDate = this.getAttemptDueDate(quiz, attempt);
if (attempt.state === AddonModQuizProvider.ATTEMPT_OVERDUE) {
return Translate.instant(
'addon.mod_quiz.overduemustbesubmittedby',
{ $a: CoreTimeUtils.userDate(dueDate) },
);
} else if (dueDate) {
return Translate.instant('addon.mod_quiz.mustbesubmittedby', { $a: CoreTimeUtils.userDate(dueDate) });
}
}
/**
* Turn attempt's state into a readable state, including some extra data depending on the state.
*
* @param quiz Quiz.
* @param attempt Attempt.
* @return List of state sentences.
*/
getAttemptReadableState(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttempt): string[] {
if (attempt.finishedOffline) {
return [Translate.instant('addon.mod_quiz.finishnotsynced')];
}
switch (attempt.state) {
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
return [Translate.instant('addon.mod_quiz.stateinprogress')];
case AddonModQuizProvider.ATTEMPT_OVERDUE: {
const sentences: string[] = [];
const dueDate = this.getAttemptDueDate(quiz, attempt);
sentences.push(Translate.instant('addon.mod_quiz.stateoverdue'));
if (dueDate) {
sentences.push(Translate.instant(
'addon.mod_quiz.stateoverduedetails',
{ $a: CoreTimeUtils.userDate(dueDate) },
));
}
return sentences;
}
case AddonModQuizProvider.ATTEMPT_FINISHED:
return [
Translate.instant('addon.mod_quiz.statefinished'),
Translate.instant(
'addon.mod_quiz.statefinisheddetails',
{ $a: CoreTimeUtils.userDate(attempt.timefinish! * 1000) },
),
];
case AddonModQuizProvider.ATTEMPT_ABANDONED:
return [Translate.instant('addon.mod_quiz.stateabandoned')];
default:
return [];
}
}
/**
* Turn attempt's state into a readable state name, without any more data.
*
* @param state State.
* @return Readable state name.
*/
getAttemptReadableStateName(state: string): string {
switch (state) {
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
return Translate.instant('addon.mod_quiz.stateinprogress');
case AddonModQuizProvider.ATTEMPT_OVERDUE:
return Translate.instant('addon.mod_quiz.stateoverdue');
case AddonModQuizProvider.ATTEMPT_FINISHED:
return Translate.instant('addon.mod_quiz.statefinished');
case AddonModQuizProvider.ATTEMPT_ABANDONED:
return Translate.instant('addon.mod_quiz.stateabandoned');
default:
return '';
}
}
/**
* Get cache key for get attempt review WS calls.
*
* @param attemptId Attempt ID.
* @param page Page.
* @return 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 attemptId Attempt ID.
* @return Cache key.
*/
protected getAttemptReviewCommonCacheKey(attemptId: number): string {
return ROOT_CACHE_KEY + 'attemptReview:' + attemptId;
}
/**
* Get an attempt's review.
*
* @param attemptId Attempt ID.
* @param options Other options.
* @return Promise resolved with the attempt review.
*/
async getAttemptReview(
attemptId: number,
options: AddonModQuizGetAttemptReviewOptions = {},
): Promise<AddonModQuizGetAttemptReviewResponse> {
const page = typeof options.page == 'undefined' ? -1 : options.page;
const site = await CoreSites.getSite(options.siteId);
const params = {
attemptid: attemptId,
page: page,
};
const preSets = {
cacheKey: this.getAttemptReviewCacheKey(attemptId, page),
cacheErrors: ['noreview'],
component: AddonModQuizProvider.COMPONENT,
componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
const result = await site.read<AddonModQuizGetAttemptReviewWSResponse>('mod_quiz_get_attempt_review', params, preSets);
result.questions = CoreQuestion.parseQuestions(result.questions);
return result;
}
/**
* Get cache key for get attempt summary WS calls.
*
* @param attemptId Attempt ID.
* @return Cache key.
*/
protected getAttemptSummaryCacheKey(attemptId: number): string {
return ROOT_CACHE_KEY + 'attemptSummary:' + attemptId;
}
/**
* Get an attempt's summary.
*
* @param attemptId Attempt ID.
* @param preflightData Preflight required data (like password).
* @param options Other options.
* @return Promise resolved with the list of questions for the attempt summary.
*/
async getAttemptSummary(
attemptId: number,
preflightData: Record<string, string>,
options: AddonModQuizGetAttemptSummaryOptions = {},
): Promise<CoreQuestionQuestionParsed[]> {
const site = await CoreSites.getSite(options.siteId);
const params: AddonModQuizGetAttemptSummaryWSParams = {
attemptid: attemptId,
preflightdata: CoreUtils.objectToArrayOfObjects<AddonModQuizPreflightDataWSParam>(
preflightData,
'name',
'value',
true,
),
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getAttemptSummaryCacheKey(attemptId),
component: AddonModQuizProvider.COMPONENT,
componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
const response = await site.read<AddonModQuizGetAttemptSummaryWSResponse>('mod_quiz_get_attempt_summary', params, preSets);
const questions = CoreQuestion.parseQuestions(response.questions);
if (options.loadLocal) {
return AddonModQuizOffline.loadQuestionsLocalStates(attemptId, questions, site.getId());
}
return questions;
}
/**
* Get cache key for get combined review options WS calls.
*
* @param quizId Quiz ID.
* @param userId User ID.
* @return 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 quizId Quiz ID.
* @return Cache key.
*/
protected getCombinedReviewOptionsCommonCacheKey(quizId: number): string {
return ROOT_CACHE_KEY + 'combinedReviewOptions:' + quizId;
}
/**
* Get a quiz combined review options.
*
* @param quizId Quiz ID.
* @param options Other options.
* @return Promise resolved with the combined review options.
*/
async getCombinedReviewOptions(
quizId: number,
options: AddonModQuizUserOptions = {},
): Promise<AddonModQuizCombinedReviewOptions> {
const site = await CoreSites.getSite(options.siteId);
const userId = options.userId || site.getUserId();
const params: AddonModQuizGetCombinedReviewOptionsWSParams = {
quizid: quizId,
userid: userId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getCombinedReviewOptionsCacheKey(quizId, userId),
component: AddonModQuizProvider.COMPONENT,
componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
const response = await site.read<AddonModQuizGetCombinedReviewOptionsWSResponse>(
'mod_quiz_get_combined_review_options',
params,
preSets,
);
// Convert the arrays to objects with name -> value.
return {
someoptions: <Record<string, number>> CoreUtils.objectToKeyValueMap(response.someoptions, 'name', 'value'),
alloptions: <Record<string, number>> CoreUtils.objectToKeyValueMap(response.alloptions, 'name', 'value'),
warnings: response.warnings,
};
}
/**
* Get cache key for get feedback for grade WS calls.
*
* @param quizId Quiz ID.
* @param grade Grade.
* @return 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 quizId Quiz ID.
* @return Cache key.
*/
protected getFeedbackForGradeCommonCacheKey(quizId: number): string {
return ROOT_CACHE_KEY + 'feedbackForGrade:' + quizId;
}
/**
* Get the feedback for a certain grade.
*
* @param quizId Quiz ID.
* @param grade Grade.
* @param options Other options.
* @return Promise resolved with the feedback.
*/
async getFeedbackForGrade(
quizId: number,
grade: number,
options: CoreCourseCommonModWSOptions = {},
): Promise<AddonModQuizGetQuizFeedbackForGradeWSResponse> {
const site = await CoreSites.getSite(options.siteId);
const params: AddonModQuizGetQuizFeedbackForGradeWSParams = {
quizid: quizId,
grade: grade,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getFeedbackForGradeCacheKey(quizId, grade),
updateFrequency: CoreSite.FREQUENCY_RARELY,
component: AddonModQuizProvider.COMPONENT,
componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
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 quiz Quiz.
* @return Number of decimals.
*/
getGradeDecimals(quiz: AddonModQuizQuizWSData): 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 courseId Course ID.
* @param moduleId Quiz module ID.
* @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down).
* @param siteId Site ID. If not defined, current site.
* @param userId User ID. If not defined use site's current user.
* @return Promise resolved with an object containing the grade and the feedback.
*/
async getGradeFromGradebook(
courseId: number,
moduleId: number,
ignoreCache?: boolean,
siteId?: string,
userId?: number,
): Promise<CoreGradesFormattedItem | undefined> {
const items = await CoreGradesHelper.getGradeModuleItems(
courseId,
moduleId,
userId,
undefined,
siteId,
ignoreCache,
);
return items.shift();
}
/**
* Given a list of attempts, returns the last finished attempt.
*
* @param attempts Attempts sorted. First attempt should be the first on the list.
* @return Last finished attempt.
*/
getLastFinishedAttemptFromList(attempts?: AddonModQuizAttemptWSData[]): AddonModQuizAttemptWSData | undefined {
if (!attempts) {
return;
}
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 questions Questions.
* @return List of prevent submit messages. Empty array if quiz can be submitted.
*/
getPreventSubmitMessages(questions: CoreQuestionQuestionParsed[]): string[] {
const messages: string[] = [];
questions.forEach((question) => {
if (question.type != 'random' && !CoreQuestionDelegate.isQuestionSupported(question.type)) {
// The question isn't supported.
messages.push(Translate.instant('core.question.questionmessage', {
$a: question.slot,
$b: Translate.instant('core.question.errorquestionnotsupported', { $a: question.type }),
}));
} else {
let message = CoreQuestionDelegate.getPreventSubmitMessage(question);
if (message) {
message = Translate.instant(message);
messages.push(Translate.instant('core.question.questionmessage', { $a: question.slot, $b: message }));
}
}
});
return messages;
}
/**
* Get cache key for quiz data WS calls.
*
* @param courseId Course ID.
* @return Cache key.
*/
protected getQuizDataCacheKey(courseId: number): string {
return ROOT_CACHE_KEY + 'quiz:' + courseId;
}
/**
* Get a Quiz with key=value. If more than one is found, only the first will be returned.
*
* @param courseId Course ID.
* @param key Name of the property to check.
* @param value Value to search.
* @param options Other options.
* @return Promise resolved when the Quiz is retrieved.
*/
protected async getQuizByField(
courseId: number,
key: string,
value: unknown,
options: CoreSitesCommonWSOptions = {},
): Promise<AddonModQuizQuizWSData> {
const site = await CoreSites.getSite(options.siteId);
const params: AddonModQuizGetQuizzesByCoursesWSParams = {
courseids: [courseId],
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getQuizDataCacheKey(courseId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
component: AddonModQuizProvider.COMPONENT,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
const response = await site.read<AddonModQuizGetQuizzesByCoursesWSResponse>(
'mod_quiz_get_quizzes_by_courses',
params,
preSets,
);
// Search the quiz.
const quiz = response.quizzes.find(quiz => quiz[key] == value);
if (!quiz) {
throw new CoreError(Translate.instant('core.course.modulenotfound'));
}
return quiz;
}
/**
* Get a quiz by module ID.
*
* @param courseId Course ID.
* @param cmId Course module ID.
* @param options Other options.
* @return Promise resolved when the quiz is retrieved.
*/
getQuiz(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModQuizQuizWSData> {
return this.getQuizByField(courseId, 'coursemodule', cmId, options);
}
/**
* Get a quiz by quiz ID.
*
* @param courseId Course ID.
* @param id Quiz ID.
* @param options Other options.
* @return Promise resolved when the quiz is retrieved.
*/
getQuizById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModQuizQuizWSData> {
return this.getQuizByField(courseId, 'id', id, options);
}
/**
* Get cache key for get quiz access information WS calls.
*
* @param quizId Quiz ID.
* @return Cache key.
*/
protected getQuizAccessInformationCacheKey(quizId: number): string {
return ROOT_CACHE_KEY + 'quizAccessInformation:' + quizId;
}
/**
* Get access information for an attempt.
*
* @param quizId Quiz ID.
* @param options Other options.
* @return Promise resolved with the access information.
*/
async getQuizAccessInformation(
quizId: number,
options: CoreCourseCommonModWSOptions = {},
): Promise<AddonModQuizGetQuizAccessInformationWSResponse> {
const site = await CoreSites.getSite(options.siteId);
const params: AddonModQuizGetQuizAccessInformationWSParams = {
quizid: quizId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getQuizAccessInformationCacheKey(quizId),
component: AddonModQuizProvider.COMPONENT,
componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
return site.read('mod_quiz_get_quiz_access_information', params, preSets);
}
/**
* Get a readable Quiz grade method.
*
* @param method Grading method.
* @return Readable grading method.
*/
getQuizGradeMethod(method?: number | string): string {
if (method === undefined) {
return '';
}
if (typeof method == 'string') {
method = parseInt(method, 10);
}
switch (method) {
case AddonModQuizProvider.GRADEHIGHEST:
return Translate.instant('addon.mod_quiz.gradehighest');
case AddonModQuizProvider.GRADEAVERAGE:
return Translate.instant('addon.mod_quiz.gradeaverage');
case AddonModQuizProvider.ATTEMPTFIRST:
return Translate.instant('addon.mod_quiz.attemptfirst');
case AddonModQuizProvider.ATTEMPTLAST:
return Translate.instant('addon.mod_quiz.attemptlast');
default:
return '';
}
}
/**
* Get cache key for get quiz required qtypes WS calls.
*
* @param quizId Quiz ID.
* @return Cache key.
*/
protected getQuizRequiredQtypesCacheKey(quizId: number): string {
return ROOT_CACHE_KEY + 'quizRequiredQtypes:' + quizId;
}
/**
* Get the potential question types that would be required for a given quiz.
*
* @param quizId Quiz ID.
* @param options Other options.
* @return Promise resolved with the access information.
*/
async getQuizRequiredQtypes(quizId: number, options: CoreCourseCommonModWSOptions = {}): Promise<string[]> {
const site = await CoreSites.getSite(options.siteId);
const params: AddonModQuizGetQuizRequiredQtypesWSParams = {
quizid: quizId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getQuizRequiredQtypesCacheKey(quizId),
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
component: AddonModQuizProvider.COMPONENT,
componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
const response = await site.read<AddonModQuizGetQuizRequiredQtypesWSResponse>(
'mod_quiz_get_quiz_required_qtypes',
params,
preSets,
);
return response.questiontypes;
}
/**
* Given an attempt's layout, return the list of pages.
*
* @param layout Attempt's layout.
* @return 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[] {
if (!layout) {
return [];
}
const split = layout.split(',');
const 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 layout Attempt's layout.
* @param questions List of questions. It needs to be an object where the keys are question slot.
* @return 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: AddonModQuizQuestionsWithAnswers): number[] {
const split = layout.split(',');
const pages: number[] = [];
let page = 0;
let 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 questionTypes Question types to check.
* @return Not supported question types.
*/
getUnsupportedQuestions(questionTypes: string[]): string[] {
const notSupported: string[] = [];
questionTypes.forEach((type) => {
if (type != 'random' && !CoreQuestionDelegate.isQuestionSupported(type)) {
notSupported.push(type);
}
});
return notSupported;
}
/**
* Given a list of access rules names, returns the rules that aren't supported.
*
* @param rulesNames Rules to check.
* @return Not supported rules names.
*/
getUnsupportedRules(rulesNames: string[]): string[] {
const notSupported: string[] = [];
rulesNames.forEach((name) => {
if (!AddonModQuizAccessRuleDelegate.isAccessRuleSupported(name)) {
notSupported.push(name);
}
});
return notSupported;
}
/**
* Get cache key for get user attempts WS calls.
*
* @param quizId Quiz ID.
* @param userId User ID.
* @return 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 quizId Quiz ID.
* @return Cache key.
*/
protected getUserAttemptsCommonCacheKey(quizId: number): string {
return ROOT_CACHE_KEY + 'userAttempts:' + quizId;
}
/**
* Get quiz attempts for a certain user.
*
* @param quizId Quiz ID.
* @param options Other options.
* @return Promise resolved with the attempts.
*/
async getUserAttempts(
quizId: number,
options: AddonModQuizGetUserAttemptsOptions = {},
): Promise<AddonModQuizAttemptWSData[]> {
const status = options.status || 'all';
const includePreviews = typeof options.includePreviews == 'undefined' ? true : options.includePreviews;
const site = await CoreSites.getSite(options.siteId);
const userId = options.userId || site.getUserId();
const params: AddonModQuizGetUserAttemptsWSParams = {
quizid: quizId,
userid: userId,
status: status,
includepreviews: !!includePreviews,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getUserAttemptsCacheKey(quizId, userId),
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
component: AddonModQuizProvider.COMPONENT,
componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
const response = await site.read<AddonModQuizGetUserAttemptsWSResponse>('mod_quiz_get_user_attempts', params, preSets);
return response.attempts;
}
/**
* Get cache key for get user best grade WS calls.
*
* @param quizId Quiz ID.
* @param userId User ID.
* @return 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 quizId Quiz ID.
* @return Cache key.
*/
protected getUserBestGradeCommonCacheKey(quizId: number): string {
return ROOT_CACHE_KEY + 'userBestGrade:' + quizId;
}
/**
* Get best grade in a quiz for a certain user.
*
* @param quizId Quiz ID.
* @param options Other options.
* @return Promise resolved with the best grade data.
*/
async getUserBestGrade(quizId: number, options: AddonModQuizUserOptions = {}): Promise<AddonModQuizGetUserBestGradeWSResponse> {
const site = await CoreSites.getSite(options.siteId);
const userId = options.userId || site.getUserId();
const params: AddonModQuizGetUserBestGradeWSParams = {
quizid: quizId,
userid: userId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getUserBestGradeCacheKey(quizId, userId),
component: AddonModQuizProvider.COMPONENT,
componentId: options.cmId,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
return site.read('mod_quiz_get_user_best_grade', params, preSets);
}
/**
* Invalidates all the data related to a certain quiz.
*
* @param quizId Quiz ID.
* @param courseId Course ID.
* @param attemptId Attempt ID to invalidate some WS calls.
* @param siteId Site ID. If not defined, current site.
* @param userId User ID. If not defined use site's current user.
* @return Promise resolved when the data is invalidated.
*/
async invalidateAllQuizData(
quizId: number,
courseId?: number,
attemptId?: number,
siteId?: string,
userId?: number,
): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId();
const promises: Promise<void>[] = [];
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));
}
await Promise.all(promises);
}
/**
* Invalidates attempt access information for all attempts in a quiz.
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateAttemptAccessInformation(quizId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getAttemptAccessInformationCommonCacheKey(quizId));
}
/**
* Invalidates attempt access information for an attempt.
*
* @param quizId Quiz ID.
* @param attemptId Attempt ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateAttemptAccessInformationForAttempt(quizId: number, attemptId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getAttemptAccessInformationCacheKey(quizId, attemptId));
}
/**
* Invalidates attempt data for all pages.
*
* @param attemptId Attempt ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateAttemptData(attemptId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getAttemptDataCommonCacheKey(attemptId));
}
/**
* Invalidates attempt data for a certain page.
*
* @param attemptId Attempt ID.
* @param page Page.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateAttemptDataForPage(attemptId: number, page: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getAttemptDataCacheKey(attemptId, page));
}
/**
* Invalidates attempt review for all pages.
*
* @param attemptId Attempt ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateAttemptReview(attemptId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getAttemptReviewCommonCacheKey(attemptId));
}
/**
* Invalidates attempt review for a certain page.
*
* @param attemptId Attempt ID.
* @param page Page.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateAttemptReviewForPage(attemptId: number, page: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getAttemptReviewCacheKey(attemptId, page));
}
/**
* Invalidates attempt summary.
*
* @param attemptId Attempt ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateAttemptSummary(attemptId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getAttemptSummaryCacheKey(attemptId));
}
/**
* Invalidates combined review options for all users.
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateCombinedReviewOptions(quizId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getCombinedReviewOptionsCommonCacheKey(quizId));
}
/**
* Invalidates combined review options for a certain user.
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
* @param userId User ID. If not defined use site's current user.
* @return Promise resolved when the data is invalidated.
*/
async invalidateCombinedReviewOptionsForUser(quizId: number, siteId?: string, userId?: number): Promise<void> {
const site = await CoreSites.getSite(siteId);
return site.invalidateWsCacheForKey(this.getCombinedReviewOptionsCacheKey(quizId, userId || site.getUserId()));
}
/**
* Invalidate the prefetched content except files.
*
* @param moduleId The module ID.
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId();
// Get required data to call the invalidate functions.
const quiz = await this.getQuiz(courseId, moduleId, {
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
siteId,
});
const attempts = await this.getUserAttempts(quiz.id, { cmId: moduleId, siteId });
// Now invalidate it.
const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined;
await this.invalidateAllQuizData(quiz.id, courseId, lastAttemptId, siteId);
}
/**
* Invalidates feedback for all grades of a quiz.
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateFeedback(quizId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getFeedbackForGradeCommonCacheKey(quizId));
}
/**
* Invalidates feedback for a certain grade.
*
* @param quizId Quiz ID.
* @param grade Grade.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateFeedbackForGrade(quizId: number, grade: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getFeedbackForGradeCacheKey(quizId, grade));
}
/**
* Invalidates grade from gradebook for a certain user.
*
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @param userId User ID. If not defined use site's current user.
* @return Promise resolved when the data is invalidated.
*/
async invalidateGradeFromGradebook(courseId: number, siteId?: string, userId?: number): Promise<void> {
const site = await CoreSites.getSite(siteId);
await CoreGradesHelper.invalidateGradeModuleItems(courseId, userId || site.getUserId(), undefined, siteId);
}
/**
* Invalidates quiz access information for a quiz.
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateQuizAccessInformation(quizId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getQuizAccessInformationCacheKey(quizId));
}
/**
* Invalidates required qtypes for a quiz.
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateQuizRequiredQtypes(quizId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getQuizRequiredQtypesCacheKey(quizId));
}
/**
* Invalidates user attempts for all users.
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateUserAttempts(quizId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getUserAttemptsCommonCacheKey(quizId));
}
/**
* Invalidates user attempts for a certain user.
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
* @param userId User ID. If not defined use site's current user.
* @return Promise resolved when the data is invalidated.
*/
async invalidateUserAttemptsForUser(quizId: number, siteId?: string, userId?: number): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getUserAttemptsCacheKey(quizId, userId || site.getUserId()));
}
/**
* Invalidates user best grade for all users.
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateUserBestGrade(quizId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getUserBestGradeCommonCacheKey(quizId));
}
/**
* Invalidates user best grade for a certain user.
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
* @param userId User ID. If not defined use site's current user.
* @return Promise resolved when the data is invalidated.
*/
async invalidateUserBestGradeForUser(quizId: number, siteId?: string, userId?: number): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getUserBestGradeCacheKey(quizId, userId || site.getUserId()));
}
/**
* Invalidates quiz data.
*
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateQuizData(courseId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getQuizDataCacheKey(courseId));
}
/**
* Check if an attempt is finished based on its state.
*
* @param state Attempt's state.
* @return 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 attemptId Attempt ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: true if finished in offline but not synced, false otherwise.
*/
async isAttemptFinishedOffline(attemptId: number, siteId?: string): Promise<boolean> {
try {
const attempt = await AddonModQuizOffline.getAttemptById(attemptId, siteId);
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 quiz Quiz.
* @param attempt Attempt.
* @return Whether it's nearly over or over.
*/
isAttemptTimeNearlyOver(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttemptWSData): boolean {
if (attempt.state != AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
// Attempt not in progress, return true.
return true;
}
const dueDate = this.getAttemptDueDate(quiz, attempt);
const autoSavePeriod = quiz.autosaveperiod || 0;
if (dueDate > 0 && Date.now() + autoSavePeriod >= dueDate) {
return true;
}
return false;
}
/**
* Check if last attempt is offline and unfinished.
*
* @param attemptId Attempt ID.
* @param siteId Site ID. If not defined, current site.
* @param userId User ID. If not defined, user current site's user.
* @return Promise resolved with boolean: true if last offline attempt is unfinished, false otherwise.
*/
async isLastAttemptOfflineUnfinished(quiz: AddonModQuizQuizWSData, siteId?: string, userId?: number): Promise<boolean> {
try {
const attempts = await AddonModQuizOffline.getQuizAttempts(quiz.id, siteId, userId);
const last = attempts.pop();
return !!last && !last.finished;
} catch {
return false;
}
}
/**
* Check if a quiz navigation is sequential.
*
* @param quiz Quiz.
* @return Whether navigation is sequential.
*/
isNavigationSequential(quiz: AddonModQuizQuizWSData): boolean {
return quiz.navmethod == 'sequential';
}
/**
* Check if a question is blocked.
*
* @param question Question.
* @return Whether it's blocked.
*/
isQuestionBlocked(question: CoreQuestionQuestionParsed): boolean {
const element = CoreDomUtils.convertToElement(question.html);
return !!element.querySelector('.mod_quiz-blocked_question_warning');
}
/**
* Check if a quiz is enabled to be used in offline.
*
* @param quiz Quiz.
* @return Whether offline is enabled.
*/
isQuizOffline(quiz: AddonModQuizQuizWSData): boolean {
// Don't allow downloading the quiz if offline is disabled to prevent wasting a lot of data when opening it.
return !!quiz.allowofflineattempts && !CoreSites.getCurrentSite()?.isOfflineDisabled();
}
/**
* Report an attempt as being viewed. It did not store logs offline because order of the log is important.
*
* @param attemptId Attempt ID.
* @param page Page number.
* @param preflightData Preflight required data (like password).
* @param offline Whether attempt is offline.
* @param quiz Quiz instance. If set, a Firebase event will be stored.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the WS call is successful.
*/
async logViewAttempt(
attemptId: number,
page: number = 0,
preflightData: Record<string, string> = {},
offline?: boolean,
quiz?: AddonModQuizQuizWSData,
siteId?: string,
): Promise<void> {
const site = await CoreSites.getSite(siteId);
const params: AddonModQuizViewAttemptWSParams = {
attemptid: attemptId,
page: page,
preflightdata: CoreUtils.objectToArrayOfObjects<AddonModQuizPreflightDataWSParam>(
preflightData,
'name',
'value',
),
};
const promises: Promise<unknown>[] = [];
promises.push(site.write('mod_quiz_view_attempt', params));
if (offline) {
promises.push(AddonModQuizOffline.setAttemptCurrentPage(attemptId, page, site.getId()));
}
if (quiz) {
CorePushNotifications.logViewEvent(
quiz.id,
quiz.name,
'quiz',
'mod_quiz_view_attempt',
{ attemptid: attemptId, page },
siteId,
);
}
await Promise.all(promises);
}
/**
* Report an attempt's review as being viewed.
*
* @param attemptId Attempt ID.
* @param quizId Quiz ID.
* @param name Name of the quiz.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the WS call is successful.
*/
logViewAttemptReview(attemptId: number, quizId: number, name?: string, siteId?: string): Promise<void> {
const params: AddonModQuizViewAttemptReviewWSParams = {
attemptid: attemptId,
};
return CoreCourseLogHelper.logSingle(
'mod_quiz_view_attempt_review',
params,
AddonModQuizProvider.COMPONENT,
quizId,
name,
'quiz',
params,
siteId,
);
}
/**
* Report an attempt's summary as being viewed.
*
* @param attemptId Attempt ID.
* @param preflightData Preflight required data (like password).
* @param quizId Quiz ID.
* @param name Name of the quiz.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the WS call is successful.
*/
logViewAttemptSummary(
attemptId: number,
preflightData: Record<string, string>,
quizId: number,
name?: string,
siteId?: string,
): Promise<void> {
const params: AddonModQuizViewAttemptSummaryWSParams = {
attemptid: attemptId,
preflightdata: CoreUtils.objectToArrayOfObjects<AddonModQuizPreflightDataWSParam>(
preflightData,
'name',
'value',
),
};
return CoreCourseLogHelper.logSingle(
'mod_quiz_view_attempt_summary',
params,
AddonModQuizProvider.COMPONENT,
quizId,
name,
'quiz',
{ attemptid: attemptId },
siteId,
);
}
/**
* Report a quiz as being viewed.
*
* @param id Module ID.
* @param name Name of the quiz.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the WS call is successful.
*/
logViewQuiz(id: number, name?: string, siteId?: string): Promise<void> {
const params: AddonModQuizViewQuizWSParams = {
quizid: id,
};
return CoreCourseLogHelper.logSingle(
'mod_quiz_view_quiz',
params,
AddonModQuizProvider.COMPONENT,
id,
name,
'quiz',
{},
siteId,
);
}
/**
* Process an attempt, saving its data.
*
* @param quiz Quiz.
* @param attempt Attempt.
* @param data Data to save.
* @param preflightData Preflight required data (like password).
* @param finish Whether to finish the quiz.
* @param timeUp Whether the quiz time is up, false otherwise.
* @param offline Whether the attempt is offline.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved in success, rejected otherwise.
*/
async processAttempt(
quiz: AddonModQuizQuizWSData,
attempt: AddonModQuizAttemptWSData,
data: CoreQuestionsAnswers,
preflightData: Record<string, string>,
finish?: boolean,
timeUp?: boolean,
offline?: boolean,
siteId?: string,
): Promise<void> {
if (offline) {
return this.processAttemptOffline(quiz, attempt, data, preflightData, finish, siteId);
}
await this.processAttemptOnline(attempt.id, data, preflightData, finish, timeUp, siteId);
}
/**
* Process an online attempt, saving its data.
*
* @param attemptId Attempt ID.
* @param data Data to save.
* @param preflightData Preflight required data (like password).
* @param finish Whether to finish the quiz.
* @param timeUp Whether the quiz time is up, false otherwise.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved in success, rejected otherwise.
*/
protected async processAttemptOnline(
attemptId: number,
data: CoreQuestionsAnswers,
preflightData: Record<string, string>,
finish?: boolean,
timeUp?: boolean,
siteId?: string,
): Promise<string> {
const site = await CoreSites.getSite(siteId);
const params: AddonModQuizProcessAttemptWSParams = {
attemptid: attemptId,
data: CoreUtils.objectToArrayOfObjects(data, 'name', 'value'),
finishattempt: !!finish,
timeup: !!timeUp,
preflightdata: CoreUtils.objectToArrayOfObjects<AddonModQuizPreflightDataWSParam>(
preflightData,
'name',
'value',
),
};
const response = await site.write<AddonModQuizProcessAttemptWSResponse>('mod_quiz_process_attempt', params);
if (response.warnings?.length) {
// Reject with the first warning.
throw new CoreWSError(response.warnings[0]);
}
return response.state;
}
/**
* Process an offline attempt, saving its data.
*
* @param quiz Quiz.
* @param attempt Attempt.
* @param data Data to save.
* @param preflightData Preflight required data (like password).
* @param finish Whether to finish the quiz.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved in success, rejected otherwise.
*/
protected async processAttemptOffline(
quiz: AddonModQuizQuizWSData,
attempt: AddonModQuizAttemptWSData,
data: CoreQuestionsAnswers,
preflightData: Record<string, string>,
finish?: boolean,
siteId?: string,
): Promise<void> {
// Get attempt summary to have the list of questions.
const questionsArray = await this.getAttemptSummary(attempt.id, preflightData, {
cmId: quiz.coursemodule,
loadLocal: true,
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
siteId,
});
// Convert the question array to an object.
const questions = CoreUtils.arrayToObject(questionsArray, 'slot');
return AddonModQuizOffline.processAttempt(quiz, attempt, questions, data, finish, siteId);
}
/**
* Check if it's a graded quiz. Based on Moodle's quiz_has_grades.
*
* @param quiz Quiz.
* @return Whether quiz is graded.
*/
quizHasGrades(quiz: AddonModQuizQuizWSData): 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 rawGrade The unadjusted grade, for example attempt.sumgrades.
* @param quiz Quiz.
* @param 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 Grade to display.
*/
rescaleGrade(
rawGrade: string | number | undefined | null,
quiz: AddonModQuizQuizWSData,
format: boolean | string = true,
): string | undefined {
let grade: number | undefined;
const rawGradeNum = typeof rawGrade == 'string' ? parseFloat(rawGrade) : rawGrade;
if (rawGradeNum !== undefined && rawGradeNum !== null && !isNaN(rawGradeNum)) {
if (quiz.sumgrades! >= 0.000005) {
grade = rawGradeNum * quiz.grade! / quiz.sumgrades!;
} else {
grade = 0;
}
}
if (grade === null || grade === undefined) {
return;
}
if (format === 'question') {
return this.formatGrade(grade, this.getGradeDecimals(quiz));
} else if (format) {
return this.formatGrade(grade, quiz.decimalpoints!);
}
return String(grade);
}
/**
* Save an attempt data.
*
* @param quiz Quiz.
* @param attempt Attempt.
* @param data Data to save.
* @param preflightData Preflight required data (like password).
* @param offline Whether attempt is offline.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved in success, rejected otherwise.
*/
async saveAttempt(
quiz: AddonModQuizQuizWSData,
attempt: AddonModQuizAttemptWSData,
data: CoreQuestionsAnswers,
preflightData: Record<string, string>,
offline?: boolean,
siteId?: string,
): Promise<void> {
try {
if (offline) {
return await this.processAttemptOffline(quiz, attempt, data, preflightData, false, siteId);
}
await this.saveAttemptOnline(attempt.id, data, preflightData, siteId);
} catch (error) {
this.logger.error(error);
throw error;
}
}
/**
* Save an attempt data.
*
* @param attemptId Attempt ID.
* @param data Data to save.
* @param preflightData Preflight required data (like password).
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved in success, rejected otherwise.
*/
protected async saveAttemptOnline(
attemptId: number,
data: CoreQuestionsAnswers,
preflightData: Record<string, string>,
siteId?: string,
): Promise<void> {
const site = await CoreSites.getSite(siteId);
const params: AddonModQuizSaveAttemptWSParams = {
attemptid: attemptId,
data: CoreUtils.objectToArrayOfObjects(data, 'name', 'value'),
preflightdata: CoreUtils.objectToArrayOfObjects<AddonModQuizPreflightDataWSParam>(
preflightData,
'name',
'value',
),
};
const response = await site.write<CoreStatusWithWarningsWSResponse>('mod_quiz_save_attempt', params);
if (response.warnings?.length) {
// Reject with the first warning.
throw new CoreWSError(response.warnings[0]);
} else if (!response.status) {
// It shouldn't happen that status is false and no warnings were returned.
throw new CoreError('Cannot save data.');
}
}
/**
* Check if time left should be shown.
*
* @param rules List of active rules names.
* @param attempt Attempt.
* @param endTime The attempt end time (in seconds).
* @return Whether time left should be displayed.
*/
shouldShowTimeLeft(rules: string[], attempt: AddonModQuizAttemptWSData, endTime: number): boolean {
const timeNow = CoreTimeUtils.timestamp();
if (attempt.state != AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
return false;
}
return AddonModQuizAccessRuleDelegate.shouldShowTimeLeft(rules, attempt, endTime, timeNow);
}
/**
* Start an attempt.
*
* @param quizId Quiz ID.
* @param preflightData Preflight required data (like password).
* @param forceNew Whether to force a new attempt or not.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the attempt data.
*/
async startAttempt(
quizId: number,
preflightData: Record<string, string>,
forceNew?: boolean,
siteId?: string,
): Promise<AddonModQuizAttemptWSData> {
const site = await CoreSites.getSite(siteId);
const params: AddonModQuizStartAttemptWSParams = {
quizid: quizId,
preflightdata: CoreUtils.objectToArrayOfObjects<AddonModQuizPreflightDataWSParam>(
preflightData,
'name',
'value',
),
forcenew: !!forceNew,
};
const response = await site.write<AddonModQuizStartAttemptWSResponse>('mod_quiz_start_attempt', params);
if (response.warnings?.length) {
// Reject with the first warning.
throw new CoreWSError(response.warnings[0]);
}
return response.attempt;
}
}
export const AddonModQuiz = makeSingleton(AddonModQuizProvider);
/**
* Common options with user ID.
*/
export type AddonModQuizUserOptions = CoreCourseCommonModWSOptions & {
userId?: number; // User ID. If not defined use site's current user.
};
/**
* Options to pass to getAllQuestionsData.
*/
export type AddonModQuizAllQuestionsDataOptions = CoreCourseCommonModWSOptions & {
pages?: number[]; // List of pages to get. If not defined, all pages.
};
/**
* Options to pass to getAttemptReview.
*/
export type AddonModQuizGetAttemptReviewOptions = CoreCourseCommonModWSOptions & {
page?: number; // List of pages to get. If not defined, all pages.
};
/**
* Options to pass to getAttemptSummary.
*/
export type AddonModQuizGetAttemptSummaryOptions = CoreCourseCommonModWSOptions & {
loadLocal?: boolean; // Whether it should load local state for each question.
};
/**
* Options to pass to getUserAttempts.
*/
export type AddonModQuizGetUserAttemptsOptions = CoreCourseCommonModWSOptions & {
status?: string; // Status of the attempts to get. By default, 'all'.
includePreviews?: boolean; // Whether to include previews. Defaults to true.
userId?: number; // User ID. If not defined use site's current user.
};
/**
* Preflight data in the format accepted by the WebServices.
*/
type AddonModQuizPreflightDataWSParam = {
name: string; // Data name.
value: string; // Data value.
};
/**
* Params of mod_quiz_get_attempt_access_information WS.
*/
export type AddonModQuizGetAttemptAccessInformationWSParams = {
quizid: number; // Quiz instance id.
attemptid?: number; // Attempt id, 0 for the user last attempt if exists.
};
/**
* Data returned by mod_quiz_get_attempt_access_information WS.
*/
export type AddonModQuizGetAttemptAccessInformationWSResponse = {
endtime?: number; // When the attempt must be submitted (determined by rules).
isfinished: boolean; // Whether there is no way the user will ever be allowed to attempt.
ispreflightcheckrequired?: boolean; // Whether a check is required before the user starts/continues his attempt.
preventnewattemptreasons: string[]; // List of reasons.
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_quiz_get_attempt_data WS.
*/
export type AddonModQuizGetAttemptDataWSParams = {
attemptid: number; // Attempt id.
page: number; // Page number.
preflightdata?: AddonModQuizPreflightDataWSParam[]; // Preflight required data (like passwords).
};
/**
* Data returned by mod_quiz_get_attempt_data WS.
*/
export type AddonModQuizGetAttemptDataWSResponse = {
attempt: AddonModQuizAttemptWSData;
messages: string[]; // Access messages, will only be returned for users with mod/quiz:preview capability.
nextpage: number; // Next page number.
questions: CoreQuestionQuestionWSData[];
warnings?: CoreWSExternalWarning[];
};
/**
* Attempt data returned by several WebServices.
*/
export type AddonModQuizAttemptWSData = {
id: number; // Attempt id.
quiz?: number; // Foreign key reference to the quiz that was attempted.
userid?: number; // Foreign key reference to the user whose attempt this is.
attempt?: number; // Sequentially numbers this students attempts at this quiz.
uniqueid?: number; // Foreign key reference to the question_usage that holds the details of the the question_attempts.
layout?: string; // Attempt layout.
currentpage?: number; // Attempt current page.
preview?: number; // Whether is a preview attempt or not.
state?: string; // The current state of the attempts. 'inprogress', 'overdue', 'finished' or 'abandoned'.
timestart?: number; // Time when the attempt was started.
timefinish?: number; // Time when the attempt was submitted. 0 if the attempt has not been submitted yet.
timemodified?: number; // Last modified time.
timemodifiedoffline?: number; // Last modified time via webservices.
timecheckstate?: number; // Next time quiz cron should check attempt for state changes. NULL means never check.
sumgrades?: number | null; // Total marks for this attempt.
};
/**
* Get attempt data response with parsed questions.
*/
export type AddonModQuizGetAttemptDataResponse = Omit<AddonModQuizGetAttemptDataWSResponse, 'questions'> & {
questions: CoreQuestionQuestionParsed[];
};
/**
* Params of mod_quiz_get_attempt_review WS.
*/
export type AddonModQuizGetAttemptReviewWSParams = {
attemptid: number; // Attempt id.
page?: number; // Page number, empty for all the questions in all the pages.
};
/**
* Data returned by mod_quiz_get_attempt_review WS.
*/
export type AddonModQuizGetAttemptReviewWSResponse = {
grade: string; // Grade for the quiz (or empty or "notyetgraded").
attempt: AddonModQuizAttemptWSData;
additionaldata: AddonModQuizWSAdditionalData[];
questions: CoreQuestionQuestionWSData[];
warnings?: CoreWSExternalWarning[];
};
/**
* Additional data returned by mod_quiz_get_attempt_review WS.
*/
export type AddonModQuizWSAdditionalData = {
id: string; // Id of the data.
title: string; // Data title.
content: string; // Data content.
};
/**
* Get attempt review response with parsed questions.
*/
export type AddonModQuizGetAttemptReviewResponse = Omit<AddonModQuizGetAttemptReviewWSResponse, 'questions'> & {
questions: CoreQuestionQuestionParsed[];
};
/**
* Params of mod_quiz_get_attempt_summary WS.
*/
export type AddonModQuizGetAttemptSummaryWSParams = {
attemptid: number; // Attempt id.
preflightdata?: AddonModQuizPreflightDataWSParam[]; // Preflight required data (like passwords).
};
/**
* Data returned by mod_quiz_get_attempt_summary WS.
*/
export type AddonModQuizGetAttemptSummaryWSResponse = {
questions: CoreQuestionQuestionWSData[];
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_quiz_get_combined_review_options WS.
*/
export type AddonModQuizGetCombinedReviewOptionsWSParams = {
quizid: number; // Quiz instance id.
userid?: number; // User id (empty for current user).
};
/**
* Data returned by mod_quiz_get_combined_review_options WS.
*/
export type AddonModQuizGetCombinedReviewOptionsWSResponse = {
someoptions: AddonModQuizWSReviewOption[];
alloptions: AddonModQuizWSReviewOption[];
warnings?: CoreWSExternalWarning[];
};
/**
* Option data returned by mod_quiz_get_combined_review_options.
*/
export type AddonModQuizWSReviewOption = {
name: string; // Option name.
value: number; // Option value.
};
/**
* Data returned by mod_quiz_get_combined_review_options WS, formatted to convert the options to objects.
*/
export type AddonModQuizCombinedReviewOptions = Omit<AddonModQuizGetCombinedReviewOptionsWSResponse, 'alloptions'|'someoptions'> & {
someoptions: Record<string, number>;
alloptions: Record<string, number>;
};
/**
* Params of mod_quiz_get_quiz_feedback_for_grade WS.
*/
export type AddonModQuizGetQuizFeedbackForGradeWSParams = {
quizid: number; // Quiz instance id.
grade: number; // The grade to check.
};
/**
* Data returned by mod_quiz_get_quiz_feedback_for_grade WS.
*/
export type AddonModQuizGetQuizFeedbackForGradeWSResponse = {
feedbacktext: string; // The comment that corresponds to this grade (empty for none).
feedbacktextformat?: number; // Feedbacktext format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
feedbackinlinefiles?: CoreWSExternalFile[];
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_quiz_get_quizzes_by_courses WS.
*/
export type AddonModQuizGetQuizzesByCoursesWSParams = {
courseids?: number[]; // Array of course ids.
};
/**
* Data returned by mod_quiz_get_quizzes_by_courses WS.
*/
export type AddonModQuizGetQuizzesByCoursesWSResponse = {
quizzes: AddonModQuizQuizWSData[];
warnings?: CoreWSExternalWarning[];
};
/**
* Quiz data returned by mod_quiz_get_quizzes_by_courses WS.
*/
export type AddonModQuizQuizWSData = {
id: number; // Standard Moodle primary key.
course: number; // Foreign key reference to the course this quiz is part of.
coursemodule: number; // Course module id.
name: string; // Quiz name.
intro?: string; // Quiz introduction text.
introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
introfiles?: CoreWSExternalFile[];
timeopen?: number; // The time when this quiz opens. (0 = no restriction.).
timeclose?: number; // The time when this quiz closes. (0 = no restriction.).
timelimit?: number; // The time limit for quiz attempts, in seconds.
overduehandling?: string; // The method used to handle overdue attempts. 'autosubmit', 'graceperiod' or 'autoabandon'.
graceperiod?: number; // The amount of time (in seconds) after time limit during which attempts can still be submitted.
preferredbehaviour?: string; // The behaviour to ask questions to use.
canredoquestions?: number; // Allows students to redo any completed question within a quiz attempt.
attempts?: number; // The maximum number of attempts a student is allowed.
attemptonlast?: number; // Whether subsequent attempts start from the answer to the previous attempt (1) or start blank (0).
grademethod?: number; // One of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.
decimalpoints?: number; // Number of decimal points to use when displaying grades.
questiondecimalpoints?: number; // Number of decimal points to use when displaying question grades.
reviewattempt?: number; // Whether users are allowed to review their quiz attempts at various times.
reviewcorrectness?: number; // Whether users are allowed to review their quiz attempts at various times.
reviewmarks?: number; // Whether users are allowed to review their quiz attempts at various times.
reviewspecificfeedback?: number; // Whether users are allowed to review their quiz attempts at various times.
reviewgeneralfeedback?: number; // Whether users are allowed to review their quiz attempts at various times.
reviewrightanswer?: number; // Whether users are allowed to review their quiz attempts at various times.
reviewoverallfeedback?: number; // Whether users are allowed to review their quiz attempts at various times.
questionsperpage?: number; // How often to insert a page break when editing the quiz, or when shuffling the question order.
navmethod?: string; // Any constraints on how the user is allowed to navigate around the quiz.
shuffleanswers?: number; // Whether the parts of the question should be shuffled, in those question types that support it.
sumgrades?: number | null; // The total of all the question instance maxmarks.
grade?: number; // The total that the quiz overall grade is scaled to be out of.
timecreated?: number; // The time when the quiz was added to the course.
timemodified?: number; // Last modified time.
password?: string; // A password that the student must enter before starting or continuing a quiz attempt.
subnet?: string; // Used to restrict the IP addresses from which this quiz can be attempted.
browsersecurity?: string; // Restriciton on the browser the student must use. E.g. 'securewindow'.
delay1?: number; // Delay that must be left between the first and second attempt, in seconds.
delay2?: number; // Delay that must be left between the second and subsequent attempt, in seconds.
showuserpicture?: number; // Option to show the user's picture during the attempt and on the review page.
showblocks?: number; // Whether blocks should be shown on the attempt.php and review.php pages.
completionattemptsexhausted?: number; // Mark quiz complete when the student has exhausted the maximum number of attempts.
completionpass?: number; // Whether to require passing grade.
allowofflineattempts?: number; // Whether to allow the quiz to be attempted offline in the mobile app.
autosaveperiod?: number; // Auto-save delay.
hasfeedback?: number; // Whether the quiz has any non-blank feedback text.
hasquestions?: number; // Whether the quiz has questions.
section?: number; // Course section id.
visible?: number; // Module visibility.
groupmode?: number; // Group mode.
groupingid?: number; // Grouping id.
};
/**
* Params of mod_quiz_get_quiz_access_information WS.
*/
export type AddonModQuizGetQuizAccessInformationWSParams = {
quizid: number; // Quiz instance id.
};
/**
* Data returned by mod_quiz_get_quiz_access_information WS.
*/
export type AddonModQuizGetQuizAccessInformationWSResponse = {
canattempt: boolean; // Whether the user can do the quiz or not.
canmanage: boolean; // Whether the user can edit the quiz settings or not.
canpreview: boolean; // Whether the user can preview the quiz or not.
canreviewmyattempts: boolean; // Whether the users can review their previous attempts or not.
canviewreports: boolean; // Whether the user can view the quiz reports or not.
accessrules: string[]; // List of rules.
activerulenames: string[]; // List of active rules.
preventaccessreasons: string[]; // List of reasons.
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_quiz_get_quiz_required_qtypes WS.
*/
export type AddonModQuizGetQuizRequiredQtypesWSParams = {
quizid: number; // Quiz instance id.
};
/**
* Data returned by mod_quiz_get_quiz_required_qtypes WS.
*/
export type AddonModQuizGetQuizRequiredQtypesWSResponse = {
questiontypes: string[]; // List of question types used in the quiz.
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_quiz_get_user_attempts WS.
*/
export type AddonModQuizGetUserAttemptsWSParams = {
quizid: number; // Quiz instance id.
userid?: number; // User id, empty for current user.
status?: string; // Quiz status: all, finished or unfinished.
includepreviews?: boolean; // Whether to include previews or not.
};
/**
* Data returned by mod_quiz_get_user_attempts WS.
*/
export type AddonModQuizGetUserAttemptsWSResponse = {
attempts: AddonModQuizAttemptWSData[];
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_quiz_get_user_best_grade WS.
*/
export type AddonModQuizGetUserBestGradeWSParams = {
quizid: number; // Quiz instance id.
userid?: number; // User id.
};
/**
* Data returned by mod_quiz_get_user_best_grade WS.
*/
export type AddonModQuizGetUserBestGradeWSResponse = {
hasgrade: boolean; // Whether the user has a grade on the given quiz.
grade?: number; // The grade (only if the user has a grade).
gradetopass?: number; // @since 3.11. The grade to pass the quiz (only if set).
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_quiz_view_attempt WS.
*/
export type AddonModQuizViewAttemptWSParams = {
attemptid: number; // Attempt id.
page: number; // Page number.
preflightdata?: AddonModQuizPreflightDataWSParam[]; // Preflight required data (like passwords).
};
/**
* Params of mod_quiz_process_attempt WS.
*/
export type AddonModQuizProcessAttemptWSParams = {
attemptid: number; // Attempt id.
data?: { // The data to be saved.
name: string; // Data name.
value: string; // Data value.
}[];
finishattempt?: boolean; // Whether to finish or not the attempt.
timeup?: boolean; // Whether the WS was called by a timer when the time is up.
preflightdata?: AddonModQuizPreflightDataWSParam[]; // Preflight required data (like passwords).
};
/**
* Data returned by mod_quiz_process_attempt WS.
*/
export type AddonModQuizProcessAttemptWSResponse = {
state: string; // The new attempt state: inprogress, finished, overdue, abandoned.
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_quiz_save_attempt WS.
*/
export type AddonModQuizSaveAttemptWSParams = {
attemptid: number; // Attempt id.
data: { // The data to be saved.
name: string; // Data name.
value: string; // Data value.
}[];
preflightdata?: AddonModQuizPreflightDataWSParam[]; // Preflight required data (like passwords).
};
/**
* Params of mod_quiz_start_attempt WS.
*/
export type AddonModQuizStartAttemptWSParams = {
quizid: number; // Quiz instance id.
preflightdata?: AddonModQuizPreflightDataWSParam[]; // Preflight required data (like passwords).
forcenew?: boolean; // Whether to force a new attempt or not.
};
/**
* Data returned by mod_quiz_start_attempt WS.
*/
export type AddonModQuizStartAttemptWSResponse = {
attempt: AddonModQuizAttemptWSData;
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_quiz_view_attempt_review WS.
*/
export type AddonModQuizViewAttemptReviewWSParams = {
attemptid: number; // Attempt id.
};
/**
* Params of mod_quiz_view_attempt_summary WS.
*/
export type AddonModQuizViewAttemptSummaryWSParams = {
attemptid: number; // Attempt id.
preflightdata?: AddonModQuizPreflightDataWSParam[]; // Preflight required data (like passwords).
};
/**
* Params of mod_quiz_view_quiz WS.
*/
export type AddonModQuizViewQuizWSParams = {
quizid: number; // Quiz instance id.
};
/**
* Data passed to ATTEMPT_FINISHED_EVENT event.
*/
export type AddonModQuizAttemptFinishedData = {
quizId: number;
attemptId: number;
synced: boolean;
};