forked from EVOgeek/Vmeda.Online
4237 lines
151 KiB
TypeScript
4237 lines
151 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 { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
|
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
|
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
|
import { CoreGradesProvider } from '@features/grades/services/grades';
|
|
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
|
|
import { CoreDomUtils } from '@services/utils/dom';
|
|
import { CoreTextUtils } from '@services/utils/text';
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
|
import { makeSingleton, Translate } from '@singletons';
|
|
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
|
|
import { AddonModLessonPasswordDBRecord, PASSWORD_TABLE_NAME } from './database/lesson';
|
|
import { AddonModLessonOffline, AddonModLessonPageAttemptRecord } from './lesson-offline';
|
|
|
|
const ROOT_CACHE_KEY = 'mmaModLesson:';
|
|
|
|
/**
|
|
* Service that provides some features for lesson.
|
|
*
|
|
* Lesson terminology is a bit confusing and ambiguous in Moodle. For that reason, in the app it has been decided to use
|
|
* the following terminology:
|
|
* - Retake: An attempt in a lesson. In Moodle it's sometimes called "attempt", "try" or "retry".
|
|
* - Attempt: An attempt in a page inside a retake. In the app, this includes content pages.
|
|
* - Content page: A page with only content (no question). In Moodle it's sometimes called "branch table".
|
|
* - Page answers: List of possible answers for a page (configured by the teacher). NOT the student answer for the page.
|
|
*
|
|
* This terminology sometimes won't match with WebServices names, params or responses.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class AddonModLessonProvider {
|
|
|
|
static readonly COMPONENT = 'mmaModLesson';
|
|
static readonly DATA_SENT_EVENT = 'addon_mod_lesson_data_sent';
|
|
|
|
static readonly LESSON_THISPAGE = 0; // This page.
|
|
static readonly LESSON_UNSEENPAGE = 1; // Next page -> any page not seen before.
|
|
static readonly LESSON_UNANSWEREDPAGE = 2; // Next page -> any page not answered correctly.
|
|
static readonly LESSON_NEXTPAGE = -1; // Jump to Next Page.
|
|
static readonly LESSON_EOL = -9; // End of Lesson.
|
|
static readonly LESSON_UNSEENBRANCHPAGE = -50; // Jump to an unseen page within a branch and end of branch or end of lesson.
|
|
static readonly LESSON_RANDOMPAGE = -60; // Jump to a random page within a branch and end of branch or end of lesson.
|
|
static readonly LESSON_RANDOMBRANCH = -70; // Jump to a random Branch.
|
|
static readonly LESSON_CLUSTERJUMP = -80; // Cluster Jump.
|
|
|
|
// Type of page: question or structure (content).
|
|
static readonly TYPE_QUESTION = 0;
|
|
static readonly TYPE_STRUCTURE = 1;
|
|
|
|
// Type of question pages.
|
|
static readonly LESSON_PAGE_SHORTANSWER = 1;
|
|
static readonly LESSON_PAGE_TRUEFALSE = 2;
|
|
static readonly LESSON_PAGE_MULTICHOICE = 3;
|
|
static readonly LESSON_PAGE_MATCHING = 5;
|
|
static readonly LESSON_PAGE_NUMERICAL = 8;
|
|
static readonly LESSON_PAGE_ESSAY = 10;
|
|
static readonly LESSON_PAGE_BRANCHTABLE = 20; // Content page.
|
|
static readonly LESSON_PAGE_ENDOFBRANCH = 21;
|
|
static readonly LESSON_PAGE_CLUSTER = 30;
|
|
static readonly LESSON_PAGE_ENDOFCLUSTER = 31;
|
|
|
|
static readonly MULTIANSWER_DELIMITER = '@^#|'; // Constant used as a delimiter when parsing multianswer questions.
|
|
static readonly LESSON_OTHER_ANSWERS = '@#wronganswer#@';
|
|
|
|
/**
|
|
* Add an answer and its response to a feedback string (HTML).
|
|
*
|
|
* @param feedback The current feedback.
|
|
* @param answer Student answer.
|
|
* @param answerFormat Answer format.
|
|
* @param response Response.
|
|
* @param className Class to add to the response.
|
|
* @return New feedback.
|
|
*/
|
|
protected addAnswerAndResponseToFeedback(
|
|
feedback: string,
|
|
answer: string,
|
|
answerFormat: number,
|
|
response: string,
|
|
className: string,
|
|
): string {
|
|
// Add a table row containing the answer.
|
|
feedback += '<tr><td class="cell c0 lastcol">' + (answerFormat ? answer : CoreTextUtils.cleanTags(answer)) +
|
|
'</td></tr>';
|
|
|
|
// If the response exists, add a table row containing the response. If not, add en empty row.
|
|
if (response?.trim()) {
|
|
feedback += '<tr><td class="cell c0 lastcol ' + className + '"><em>' +
|
|
Translate.instant('addon.mod_lesson.response') + '</em>: <br/>' +
|
|
response + '</td></tr>';
|
|
} else {
|
|
feedback += '<tr><td class="cell c0 lastcol"></td></tr>';
|
|
}
|
|
|
|
return feedback;
|
|
}
|
|
|
|
/**
|
|
* Add a message to a list of messages, following the format of the messages returned by WS.
|
|
*
|
|
* @param messages List of messages where to add the message.
|
|
* @param stringId The ID of the message to be translated. E.g. 'addon.mod_lesson.numberofpagesviewednotice'.
|
|
* @param stringParams The params of the message (if any).
|
|
*/
|
|
protected addMessage(messages: AddonModLessonMessageWSData[], stringId: string, stringParams?: Record<string, unknown>): void {
|
|
messages.push({
|
|
message: Translate.instant(stringId, stringParams),
|
|
type: '',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a property to the result of the "process EOL page" simulation in offline.
|
|
*
|
|
* @param result Result where to add the value.
|
|
* @param name Name of the property.
|
|
* @param value Value to add.
|
|
* @param addMessage Whether to add a message related to the value.
|
|
*/
|
|
protected addResultValueEolPage(
|
|
result: AddonModLessonFinishRetakeResponse,
|
|
name: string,
|
|
value: unknown,
|
|
addMessage?: boolean,
|
|
): void {
|
|
let message = '';
|
|
|
|
if (addMessage) {
|
|
const params = typeof value != 'boolean' ? { $a: value } : undefined;
|
|
message = Translate.instant('addon.mod_lesson.' + name, params);
|
|
}
|
|
|
|
result.data[name] = {
|
|
name: name,
|
|
value: value,
|
|
message: message,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if an answer page (from getUserRetake) is a content page.
|
|
*
|
|
* @param page Answer page.
|
|
* @return Whether it's a content page.
|
|
*/
|
|
answerPageIsContent(page: AddonModLessonUserAttemptAnswerPageWSData): boolean {
|
|
// The page doesn't have any reliable field to use for checking this. Check qtype first (translated string).
|
|
if (page.qtype == Translate.instant('addon.mod_lesson.branchtable')) {
|
|
return true;
|
|
}
|
|
|
|
// The qtype doesn't match, but that doesn't mean it's not a content page, maybe the language is different.
|
|
// Check it's not a question page.
|
|
if (page.answerdata && !this.answerPageIsQuestion(page)) {
|
|
// It isn't a question page, but it can be an end of branch, etc. Check if the first answer has a button.
|
|
if (page.answerdata.answers && page.answerdata.answers[0]) {
|
|
const element = CoreDomUtils.convertToElement(page.answerdata.answers[0][0]);
|
|
|
|
return !!element.querySelector('input[type="button"]');
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if an answer page (from getUserRetake) is a question page.
|
|
*
|
|
* @param page Answer page.
|
|
* @return Whether it's a question page.
|
|
*/
|
|
answerPageIsQuestion(page: AddonModLessonUserAttemptAnswerPageWSData): boolean {
|
|
if (!page.answerdata) {
|
|
return false;
|
|
}
|
|
|
|
if (page.answerdata.score) {
|
|
// Only question pages have a score.
|
|
return true;
|
|
}
|
|
|
|
if (page.answerdata.answers) {
|
|
for (let i = 0; i < page.answerdata.answers.length; i++) {
|
|
const answer = page.answerdata.answers[i];
|
|
if (answer[1]) {
|
|
// Only question pages have a statistic.
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Calculate some offline data like progress and ongoingscore.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the data.
|
|
*/
|
|
protected async calculateOfflineData(
|
|
lesson: AddonModLessonLessonWSData,
|
|
options: AddonModLessonCalculateOfflineDataOptions = {},
|
|
): Promise<{reviewmode: boolean; progress?: number; ongoingscore: string}> {
|
|
|
|
const reviewMode = !!(options.review || options.accessInfo?.reviewmode);
|
|
let ongoingMessage = '';
|
|
let progress: number | undefined;
|
|
|
|
if (options.accessInfo && !options.accessInfo.canmanage) {
|
|
if (lesson.ongoing && !reviewMode) {
|
|
ongoingMessage = await this.getOngoingScoreMessage(lesson, options.accessInfo, options);
|
|
}
|
|
|
|
if (lesson.progressbar) {
|
|
const modOptions = {
|
|
cmId: lesson.coursemodule,
|
|
...options, // Include all options.
|
|
};
|
|
|
|
progress = await this.calculateProgress(lesson.id, options.accessInfo, modOptions);
|
|
}
|
|
}
|
|
|
|
return {
|
|
reviewmode: reviewMode,
|
|
progress,
|
|
ongoingscore: ongoingMessage,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate the progress of the current user in the lesson.
|
|
* Based on Moodle's calculate_progress.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param accessInfo Access info.
|
|
* @param options Other options.
|
|
* @return Promise resolved with a number: the progress (scale 0-100).
|
|
*/
|
|
async calculateProgress(
|
|
lessonId: number,
|
|
accessInfo: AddonModLessonGetAccessInformationWSResponse,
|
|
options: AddonModLessonCalculateProgressOptions = {},
|
|
): Promise<number> {
|
|
options.siteId = options.siteId || CoreSites.getCurrentSiteId();
|
|
|
|
// Check if the user is reviewing the attempt.
|
|
if (options.review) {
|
|
return 100;
|
|
}
|
|
|
|
const retake = accessInfo.attemptscount;
|
|
const commonOptions = {
|
|
cmId: options.cmId,
|
|
siteId: options.siteId,
|
|
};
|
|
|
|
if (!options.pageIndex) {
|
|
// Retrieve the index.
|
|
const pages = await this.getPages(lessonId, {
|
|
cmId: options.cmId,
|
|
password: options.password,
|
|
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
|
siteId: options.siteId,
|
|
});
|
|
|
|
options.pageIndex = this.createPagesIndex(pages);
|
|
}
|
|
|
|
// Get the list of question pages attempted.
|
|
let viewedPagesIds = await this.getPagesIdsWithQuestionAttempts(lessonId, retake, commonOptions);
|
|
|
|
// Get the list of viewed content pages.
|
|
const viewedContentPagesIds = await this.getContentPagesViewedIds(lessonId, retake, commonOptions);
|
|
|
|
const validPages = {};
|
|
let pageId = accessInfo.firstpageid;
|
|
|
|
viewedPagesIds = CoreUtils.mergeArraysWithoutDuplicates(viewedPagesIds, viewedContentPagesIds);
|
|
|
|
// Filter out the following pages:
|
|
// - End of Cluster
|
|
// - End of Branch
|
|
// - Pages found inside of Clusters
|
|
// Do not filter out Cluster Page(s) because we count a cluster as one.
|
|
// By keeping the cluster page, we get our 1.
|
|
while (pageId) {
|
|
pageId = this.validPageAndView(options.pageIndex, options.pageIndex[pageId], validPages, viewedPagesIds);
|
|
}
|
|
|
|
// Progress calculation as a percent.
|
|
return CoreTextUtils.roundToDecimals(viewedPagesIds.length / Object.keys(validPages).length, 2) * 100;
|
|
}
|
|
|
|
/**
|
|
* Check if the answer provided by the user is correct or not and return the result object.
|
|
* This method is based on the check_answer implementation of all page types (Moodle).
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageData Page data.
|
|
* @param data Data containing the user answer.
|
|
* @param jumps Possible jumps.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @return Result.
|
|
*/
|
|
protected checkAnswer(
|
|
lesson: AddonModLessonLessonWSData,
|
|
pageData: AddonModLessonGetPageDataWSResponse,
|
|
data: Record<string, unknown>,
|
|
jumps: AddonModLessonPossibleJumps,
|
|
pageIndex: Record<number, AddonModLessonPageWSData>,
|
|
): AddonModLessonCheckAnswerResult {
|
|
// Default result.
|
|
const result: AddonModLessonCheckAnswerResult = {
|
|
answerid: 0,
|
|
noanswer: false,
|
|
correctanswer: false,
|
|
isessayquestion: false,
|
|
response: '',
|
|
newpageid: 0,
|
|
studentanswer: '',
|
|
userresponse: '',
|
|
feedback: '',
|
|
nodefaultresponse: false,
|
|
inmediatejump: false,
|
|
};
|
|
|
|
switch (pageData.page!.qtype) {
|
|
case AddonModLessonProvider.LESSON_PAGE_BRANCHTABLE:
|
|
// Load the new page immediately.
|
|
result.inmediatejump = true;
|
|
result.newpageid = this.getNewPageId(pageData.page!.id, <number> data.jumpto, jumps);
|
|
break;
|
|
|
|
case AddonModLessonProvider.LESSON_PAGE_ESSAY:
|
|
this.checkAnswerEssay(pageData, data, result);
|
|
break;
|
|
|
|
case AddonModLessonProvider.LESSON_PAGE_MATCHING:
|
|
this.checkAnswerMatching(pageData, data, result);
|
|
break;
|
|
|
|
case AddonModLessonProvider.LESSON_PAGE_MULTICHOICE:
|
|
this.checkAnswerMultichoice(lesson, pageData, data, pageIndex, result);
|
|
break;
|
|
|
|
case AddonModLessonProvider.LESSON_PAGE_NUMERICAL:
|
|
this.checkAnswerNumerical(lesson, pageData, data, pageIndex, result);
|
|
break;
|
|
|
|
case AddonModLessonProvider.LESSON_PAGE_SHORTANSWER:
|
|
this.checkAnswerShort(lesson, pageData, data, pageIndex, result);
|
|
break;
|
|
|
|
case AddonModLessonProvider.LESSON_PAGE_TRUEFALSE:
|
|
this.checkAnswerTruefalse(lesson, pageData, data, pageIndex, result);
|
|
break;
|
|
default:
|
|
// Nothing to do.
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Check an essay answer.
|
|
*
|
|
* @param pageData Page data.
|
|
* @param data Data containing the user answer.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkAnswerEssay(
|
|
pageData: AddonModLessonGetPageDataWSResponse,
|
|
data: Record<string, unknown>,
|
|
result: AddonModLessonCheckAnswerResult,
|
|
): void {
|
|
let studentAnswer;
|
|
|
|
result.isessayquestion = true;
|
|
|
|
if (!data) {
|
|
result.inmediatejump = true;
|
|
result.newpageid = pageData.page!.id;
|
|
|
|
return;
|
|
}
|
|
|
|
// The name was changed to "answer_editor" in 3.7. Before it was just "answer". Support both cases.
|
|
if (typeof data['answer_editor[text]'] != 'undefined') {
|
|
studentAnswer = data['answer_editor[text]'];
|
|
} else if (typeof data.answer_editor == 'object') {
|
|
studentAnswer = (<{text: string}> data.answer_editor).text;
|
|
} else if (typeof data['answer[text]'] != 'undefined') {
|
|
studentAnswer = data['answer[text]'];
|
|
} else if (typeof data.answer == 'object') {
|
|
studentAnswer = (<{text: string}> data.answer).text;
|
|
} else {
|
|
studentAnswer = data.answer;
|
|
}
|
|
|
|
if (!studentAnswer || studentAnswer.trim() === '') {
|
|
result.noanswer = true;
|
|
|
|
return;
|
|
}
|
|
|
|
// Essay pages should only have 1 possible answer.
|
|
pageData.answers.forEach((answer) => {
|
|
result.answerid = answer.id;
|
|
result.newpageid = answer.jumpto || 0;
|
|
});
|
|
|
|
result.userresponse = {
|
|
sent: 0,
|
|
graded: 0,
|
|
score: 0,
|
|
answer: studentAnswer,
|
|
answerformat: 1,
|
|
response: '',
|
|
responseformat: 1,
|
|
};
|
|
result.studentanswerformat = 1;
|
|
result.studentanswer = studentAnswer;
|
|
}
|
|
|
|
/**
|
|
* Check a matching answer.
|
|
*
|
|
* @param pageData Page data for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkAnswerMatching(
|
|
pageData: AddonModLessonGetPageDataWSResponse,
|
|
data: Record<string, unknown>,
|
|
result: AddonModLessonCheckAnswerResult,
|
|
): void {
|
|
if (!data) {
|
|
result.inmediatejump = true;
|
|
result.newpageid = pageData.page!.id;
|
|
|
|
return;
|
|
}
|
|
|
|
const response = this.getUserResponseMatching(data);
|
|
const getAnswers = CoreUtils.clone(pageData.answers);
|
|
const correct = getAnswers.shift();
|
|
const wrong = getAnswers.shift();
|
|
const answers: Record<number, AddonModLessonPageAnswerWSData> = {};
|
|
|
|
getAnswers.forEach((answer) => {
|
|
if (answer.answer !== '' || answer.response !== '') {
|
|
answers[answer.id] = answer;
|
|
}
|
|
});
|
|
|
|
// Get the user's exact responses for record keeping.
|
|
const userResponse: string[] = [];
|
|
let hits = 0;
|
|
|
|
result.studentanswer = '';
|
|
result.studentanswerformat = 1;
|
|
|
|
for (const id in response) {
|
|
let value = response[id];
|
|
|
|
if (!value) {
|
|
result.noanswer = true;
|
|
|
|
return;
|
|
}
|
|
|
|
value = CoreTextUtils.decodeHTML(value);
|
|
userResponse.push(value);
|
|
|
|
if (typeof answers[id] != 'undefined') {
|
|
const answer = answers[id];
|
|
|
|
result.studentanswer += '<br />' + answer.answer + ' = ' + value;
|
|
if (answer.response && answer.response.trim() == value.trim()) {
|
|
hits++;
|
|
}
|
|
}
|
|
}
|
|
|
|
result.userresponse = userResponse.join(',');
|
|
|
|
if (hits == Object.keys(answers).length) {
|
|
result.correctanswer = true;
|
|
result.response = correct!.answer || '';
|
|
result.answerid = correct!.id;
|
|
result.newpageid = correct!.jumpto || 0;
|
|
} else {
|
|
result.correctanswer = false;
|
|
result.response = wrong!.answer || '';
|
|
result.answerid = wrong!.id;
|
|
result.newpageid = wrong!.jumpto || 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check a multichoice answer.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageData Page data for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkAnswerMultichoice(
|
|
lesson: AddonModLessonLessonWSData,
|
|
pageData: AddonModLessonGetPageDataWSResponse,
|
|
data: Record<string, unknown>,
|
|
pageIndex: Record<number, AddonModLessonPageWSData>,
|
|
result: AddonModLessonCheckAnswerResult,
|
|
): void {
|
|
|
|
if (!data) {
|
|
result.inmediatejump = true;
|
|
result.newpageid = pageData.page!.id;
|
|
|
|
return;
|
|
}
|
|
|
|
const answers = this.getUsedAnswersMultichoice(pageData);
|
|
|
|
if (pageData.page!.qoption) {
|
|
// Multianswer allowed, user's answer is an array.
|
|
const studentAnswers = this.getUserResponseMultichoice(data);
|
|
|
|
if (!studentAnswers || !Array.isArray(studentAnswers)) {
|
|
result.noanswer = true;
|
|
|
|
return;
|
|
}
|
|
|
|
// Get what the user answered.
|
|
result.userresponse = studentAnswers.join(',');
|
|
|
|
// Get the answers in a set order, the id order.
|
|
const studentAswersArray: string[] = [];
|
|
const responses: string[] = [];
|
|
let nHits = 0;
|
|
let nCorrect = 0;
|
|
let correctAnswerId = 0;
|
|
let wrongAnswerId = 0;
|
|
let correctPageId: number | undefined;
|
|
let wrongPageId: number | undefined;
|
|
|
|
// Store student's answers for displaying on feedback page.
|
|
result.studentanswer = '';
|
|
result.studentanswerformat = 1;
|
|
answers.forEach((answer) => {
|
|
for (const i in studentAnswers) {
|
|
const answerId = studentAnswers[i];
|
|
|
|
if (answerId == answer.id) {
|
|
studentAswersArray.push(answer.answer!);
|
|
responses.push(answer.response || '');
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
result.studentanswer = studentAswersArray.join(AddonModLessonProvider.MULTIANSWER_DELIMITER);
|
|
|
|
// Iterate over all the possible answers.
|
|
answers.forEach((answer) => {
|
|
const correctAnswer = this.isAnswerCorrect(lesson, pageData.page!.id, answer, pageIndex);
|
|
|
|
// Iterate over all the student answers to check if he selected the current possible answer.
|
|
studentAnswers.forEach((answerId) => {
|
|
if (answerId == answer.id) {
|
|
if (correctAnswer) {
|
|
nHits++;
|
|
} else {
|
|
// Always use the first student wrong answer.
|
|
if (typeof wrongPageId == 'undefined') {
|
|
wrongPageId = answer.jumpto;
|
|
}
|
|
// Save the answer id for scoring.
|
|
if (!wrongAnswerId) {
|
|
wrongAnswerId = answer.id;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (correctAnswer) {
|
|
nCorrect++;
|
|
|
|
// Save the first jumpto.
|
|
if (typeof correctPageId == 'undefined') {
|
|
correctPageId = answer.jumpto;
|
|
}
|
|
// Save the answer id for scoring.
|
|
if (!correctAnswerId) {
|
|
correctAnswerId = answer.id;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (studentAnswers.length == nCorrect && nHits == nCorrect) {
|
|
result.correctanswer = true;
|
|
result.response = responses.join(AddonModLessonProvider.MULTIANSWER_DELIMITER);
|
|
result.newpageid = correctPageId || 0;
|
|
result.answerid = correctAnswerId;
|
|
} else {
|
|
result.correctanswer = false;
|
|
result.response = responses.join(AddonModLessonProvider.MULTIANSWER_DELIMITER);
|
|
result.newpageid = wrongPageId || 0;
|
|
result.answerid = wrongAnswerId;
|
|
}
|
|
} else {
|
|
// Only one answer allowed.
|
|
if (typeof data.answerid == 'undefined' || (!data.answerid && Number(data.answerid) !== 0)) {
|
|
result.noanswer = true;
|
|
|
|
return;
|
|
}
|
|
|
|
result.answerid = <number> data.answerid;
|
|
|
|
// Search the answer.
|
|
for (const i in pageData.answers) {
|
|
const answer = pageData.answers[i];
|
|
if (answer.id == data.answerid) {
|
|
result.correctanswer = this.isAnswerCorrect(lesson, pageData.page!.id, answer, pageIndex);
|
|
result.newpageid = answer.jumpto || 0;
|
|
result.response = answer.response || '';
|
|
result.userresponse = result.studentanswer = answer.answer || '';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check a numerical answer.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageData Page data for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkAnswerNumerical(
|
|
lesson: AddonModLessonLessonWSData,
|
|
pageData: AddonModLessonGetPageDataWSResponse,
|
|
data: Record<string, unknown>,
|
|
pageIndex: Record<number, AddonModLessonPageWSData>,
|
|
result: AddonModLessonCheckAnswerResult,
|
|
): void {
|
|
|
|
const parsedAnswer = parseFloat(<string> data.answer);
|
|
|
|
// Set defaults.
|
|
result.response = '';
|
|
result.newpageid = 0;
|
|
|
|
if (!data.answer || isNaN(parsedAnswer)) {
|
|
result.noanswer = true;
|
|
|
|
return;
|
|
}
|
|
|
|
result.useranswer = parsedAnswer;
|
|
result.studentanswer = result.userresponse = String(result.useranswer);
|
|
|
|
// Find the answer.
|
|
for (const i in pageData.answers) {
|
|
const answer = pageData.answers[i];
|
|
let max: number;
|
|
let min: number;
|
|
|
|
if (answer.answer && answer.answer.indexOf(':') != -1) {
|
|
// There's a pair of values.
|
|
const split = answer.answer.split(':');
|
|
min = parseFloat(split[0]);
|
|
max = parseFloat(split[1]);
|
|
} else {
|
|
// Only one value.
|
|
min = parseFloat(answer.answer || '');
|
|
max = min;
|
|
}
|
|
|
|
if (parsedAnswer >= min && parsedAnswer <= max) {
|
|
result.newpageid = answer.jumpto || 0;
|
|
result.response = answer.response || '';
|
|
result.correctanswer = this.isAnswerCorrect(lesson, pageData.page!.id, answer, pageIndex);
|
|
result.answerid = answer.id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.checkOtherAnswers(lesson, pageData, result);
|
|
}
|
|
|
|
/**
|
|
* Check a short answer.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageData Page data for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkAnswerShort(
|
|
lesson: AddonModLessonLessonWSData,
|
|
pageData: AddonModLessonGetPageDataWSResponse,
|
|
data: Record<string, unknown>,
|
|
pageIndex: Record<number, AddonModLessonPageWSData>,
|
|
result: AddonModLessonCheckAnswerResult,
|
|
): void {
|
|
|
|
let studentAnswer = typeof data.answer == 'string' ? data.answer.trim() : false;
|
|
if (!studentAnswer) {
|
|
result.noanswer = true;
|
|
|
|
return;
|
|
}
|
|
|
|
// Search the answer in the list of possible answers.
|
|
for (const i in pageData.answers) {
|
|
const answer = pageData.answers[i];
|
|
const useRegExp = pageData.page!.qoption;
|
|
let expectedAnswer = answer.answer || '';
|
|
let isMatch = false;
|
|
let ignoreCase = '';
|
|
|
|
if (useRegExp) {
|
|
if (expectedAnswer.substr(-2) == '/i') {
|
|
expectedAnswer = expectedAnswer.substr(0, expectedAnswer.length - 2);
|
|
ignoreCase = 'i';
|
|
}
|
|
} else {
|
|
expectedAnswer = expectedAnswer.replace('*', '#####');
|
|
expectedAnswer = CoreTextUtils.escapeForRegex(expectedAnswer);
|
|
expectedAnswer = expectedAnswer.replace('#####', '.*');
|
|
}
|
|
|
|
// See if user typed in any of the correct answers.
|
|
if (this.isAnswerCorrect(lesson, pageData.page!.id, answer, pageIndex)) {
|
|
if (!useRegExp) { // We are using 'normal analysis', which ignores case.
|
|
if (studentAnswer.match(new RegExp('^' + expectedAnswer + '$', 'i'))) {
|
|
isMatch = true;
|
|
}
|
|
} else {
|
|
if (studentAnswer.match(new RegExp('^' + expectedAnswer + '$', ignoreCase))) {
|
|
isMatch = true;
|
|
}
|
|
}
|
|
if (isMatch) {
|
|
result.correctanswer = true;
|
|
}
|
|
} else {
|
|
if (!useRegExp) {
|
|
// We are using 'normal analysis'.
|
|
// See if user typed in any of the wrong answers; don't worry about case.
|
|
if (studentAnswer.match(new RegExp('^' + expectedAnswer + '$', 'i'))) {
|
|
isMatch = true;
|
|
}
|
|
} else { // We are using regular expressions analysis.
|
|
const startCode = expectedAnswer.substr(0, 2);
|
|
|
|
switch (startCode){
|
|
// 1- Check for absence of required string in studentAnswer (coded by initial '--').
|
|
case '--':
|
|
expectedAnswer = expectedAnswer.substr(2);
|
|
if (!studentAnswer.match(new RegExp('^' + expectedAnswer + '$', ignoreCase))) {
|
|
isMatch = true;
|
|
}
|
|
break;
|
|
|
|
// 2- Check for code for marking wrong strings (coded by initial '++').
|
|
case '++': {
|
|
expectedAnswer = expectedAnswer.substr(2);
|
|
|
|
// Check for one or several matches.
|
|
const matches = studentAnswer.match(new RegExp(expectedAnswer, 'g' + ignoreCase));
|
|
if (matches) {
|
|
isMatch = true;
|
|
const nb = matches[0].length;
|
|
const original: string[] = [];
|
|
const marked: string[] = [];
|
|
|
|
for (let j = 0; j < nb; j++) {
|
|
original.push(matches[0][j]);
|
|
marked.push('<span class="incorrect matches">' + matches[0][j] + '</span>');
|
|
}
|
|
|
|
for (let j = 0; j < original.length; j++) {
|
|
studentAnswer = studentAnswer.replace(original[j], marked[j]);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
// 3- Check for wrong answers belonging neither to -- nor to ++ categories.
|
|
default:
|
|
if (studentAnswer.match(new RegExp('^' + expectedAnswer + '$', ignoreCase))) {
|
|
isMatch = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
result.correctanswer = false;
|
|
}
|
|
}
|
|
|
|
if (isMatch) {
|
|
result.newpageid = answer.jumpto || 0;
|
|
result.response = answer.response || '';
|
|
result.answerid = answer.id;
|
|
break; // Quit answer analysis immediately after a match has been found.
|
|
}
|
|
}
|
|
|
|
this.checkOtherAnswers(lesson, pageData, result);
|
|
|
|
result.userresponse = studentAnswer;
|
|
result.studentanswer = CoreTextUtils.s(studentAnswer); // Clean student answer as it goes to output.
|
|
}
|
|
|
|
/**
|
|
* Check a truefalse answer.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageData Page data for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkAnswerTruefalse(
|
|
lesson: AddonModLessonLessonWSData,
|
|
pageData: AddonModLessonGetPageDataWSResponse,
|
|
data: Record<string, unknown>,
|
|
pageIndex: Record<number, AddonModLessonPageWSData>,
|
|
result: AddonModLessonCheckAnswerResult,
|
|
): void {
|
|
|
|
if (!data.answerid) {
|
|
result.noanswer = true;
|
|
|
|
return;
|
|
}
|
|
|
|
result.answerid = <number> data.answerid;
|
|
|
|
// Get the answer.
|
|
for (const i in pageData.answers) {
|
|
const answer = pageData.answers[i];
|
|
if (answer.id == data.answerid) {
|
|
// Answer found.
|
|
result.correctanswer = this.isAnswerCorrect(lesson, pageData.page!.id, answer, pageIndex);
|
|
result.newpageid = answer.jumpto || 0;
|
|
result.response = answer.response || '';
|
|
result.studentanswer = result.userresponse = answer.answer || '';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check the "other answers" value.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageData Page data for the page to process.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkOtherAnswers(
|
|
lesson: AddonModLessonLessonWSData,
|
|
pageData: AddonModLessonGetPageDataWSResponse,
|
|
result: AddonModLessonCheckAnswerResult,
|
|
): void {
|
|
// We could check here to see if we have a wrong answer jump to use.
|
|
if (result.answerid == 0) {
|
|
// Use the all other answers jump details if it is set up.
|
|
const lastAnswer = pageData.answers[pageData.answers.length - 1] || {};
|
|
|
|
// Double check that this is the OTHER_ANSWERS answer.
|
|
if (typeof lastAnswer.answer == 'string' &&
|
|
lastAnswer.answer.indexOf(AddonModLessonProvider.LESSON_OTHER_ANSWERS) != -1) {
|
|
result.newpageid = lastAnswer.jumpto || 0;
|
|
result.response = lastAnswer.response || '';
|
|
|
|
if (lesson.custom) {
|
|
result.correctanswer = !!(lastAnswer.score && lastAnswer.score > 0);
|
|
}
|
|
result.answerid = lastAnswer.id;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a list of pages indexed by page ID based on a list of pages.
|
|
*
|
|
* @param pageList List of pages.
|
|
* @return Pages index.
|
|
*/
|
|
protected createPagesIndex(pageList: AddonModLessonGetPagesPageWSData[]): Record<number, AddonModLessonPageWSData> {
|
|
// Index the pages by page ID.
|
|
const pages: Record<number, AddonModLessonPageWSData> = {};
|
|
|
|
pageList.forEach((pageData) => {
|
|
pages[pageData.page.id] = pageData.page;
|
|
});
|
|
|
|
return pages;
|
|
}
|
|
|
|
/**
|
|
* Finishes a retake.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param courseId Course ID the lesson belongs to.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the result.
|
|
*/
|
|
async finishRetake(
|
|
lesson: AddonModLessonLessonWSData,
|
|
courseId: number,
|
|
options: AddonModLessonFinishRetakeOptions = {},
|
|
): Promise<AddonModLessonFinishRetakeResponse> {
|
|
|
|
if (options.offline) {
|
|
return this.finishRetakeOffline(lesson, courseId, options);
|
|
}
|
|
|
|
const response = await this.finishRetakeOnline(lesson.id, options);
|
|
|
|
CoreEvents.trigger<AddonModLessonDataSentData>(AddonModLessonProvider.DATA_SENT_EVENT, {
|
|
lessonId: lesson.id,
|
|
type: 'finish',
|
|
courseId: courseId,
|
|
outOfTime: options.outOfTime,
|
|
review: options.review,
|
|
}, CoreSites.getCurrentSiteId());
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Finishes a retake. It will fail if offline or cannot connect.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the result.
|
|
*/
|
|
async finishRetakeOnline(
|
|
lessonId: number,
|
|
options: AddonModLessonFinishRetakeOnlineOptions = {},
|
|
): Promise<AddonModLessonFinishRetakeResponse> {
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: AddonModLessonFinishAttemptWSParams = {
|
|
lessonid: lessonId,
|
|
outoftime: !!options.outOfTime,
|
|
review: !!options.review,
|
|
};
|
|
if (typeof options.password == 'string') {
|
|
params.password = options.password;
|
|
}
|
|
|
|
const response = await site.write<AddonModLessonFinishAttemptWSResponse>('mod_lesson_finish_attempt', params);
|
|
|
|
// Convert the data array into an object and decode the values.
|
|
const map: Record<string, AddonModLessonEOLPageDataEntry> = {};
|
|
|
|
response.data.forEach((entry) => {
|
|
if (entry.value && typeof entry.value == 'string' && entry.value !== '1') {
|
|
// It's a JSON encoded object. Try to decode it.
|
|
entry.value = CoreTextUtils.parseJSON(entry.value);
|
|
}
|
|
|
|
map[entry.name] = entry;
|
|
});
|
|
|
|
return Object.assign(response, { data: map });
|
|
}
|
|
|
|
/**
|
|
* Finishes a retake in offline.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param courseId Course ID the lesson belongs to.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the result.
|
|
*/
|
|
protected async finishRetakeOffline(
|
|
lesson: AddonModLessonLessonWSData,
|
|
courseId: number,
|
|
options: AddonModLessonFinishRetakeOptions = {},
|
|
): Promise<AddonModLessonFinishRetakeResponse> {
|
|
// First finish the retake offline.
|
|
const retake = options.accessInfo!.attemptscount;
|
|
|
|
await AddonModLessonOffline.finishRetake(lesson.id, courseId, retake, true, options.outOfTime, options.siteId);
|
|
|
|
// Get the lesson grade.
|
|
const newOptions = {
|
|
cmId: lesson.coursemodule,
|
|
password: options.password,
|
|
review: options.review,
|
|
siteId: options.siteId,
|
|
};
|
|
|
|
const gradeInfo = await CoreUtils.ignoreErrors(this.lessonGrade(lesson, retake, newOptions));
|
|
|
|
// Retake marked, now return the response.
|
|
return this.processEolPage(lesson, courseId, options, gradeInfo);
|
|
}
|
|
|
|
/**
|
|
* Create the data returned by finishRetakeOffline, to display the EOL page. It won't return all the possible data.
|
|
* This code is based in Moodle's process_eol_page.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param courseId Course ID the lesson belongs to.
|
|
* @param options Other options.
|
|
* @param gradeInfo Lesson grade info.
|
|
* @return Promise resolved with the data.
|
|
*/
|
|
protected async processEolPage(
|
|
lesson: AddonModLessonLessonWSData,
|
|
courseId: number,
|
|
options: AddonModLessonFinishRetakeOptions = {},
|
|
gradeInfo: AddonModLessonGrade | undefined,
|
|
): Promise<AddonModLessonFinishRetakeResponse> {
|
|
if (!options.accessInfo) {
|
|
throw new CoreError('Access info not supplied to finishRetake.');
|
|
}
|
|
|
|
// This code is based in Moodle's process_eol_page.
|
|
const result: AddonModLessonFinishRetakeResponse = {
|
|
data: {},
|
|
messages: [],
|
|
warnings: [],
|
|
};
|
|
let gradeLesson = true;
|
|
|
|
this.addResultValueEolPage(result, 'offline', true); // Mark the result as offline.
|
|
this.addResultValueEolPage(result, 'gradeinfo', gradeInfo);
|
|
|
|
if (lesson.custom && !options.accessInfo.canmanage) {
|
|
/* Before we calculate the custom score make sure they answered the minimum number of questions.
|
|
We only need to do this for custom scoring as we can not get the miniumum score the user should achieve.
|
|
If we are not using custom scoring (so all questions are valued as 1) then we simply check if they
|
|
answered more than the minimum questions, if not, we mark it out of the number specified in the minimum
|
|
questions setting - which is done in lesson_grade(). */
|
|
|
|
// Get the number of answers given.
|
|
if (gradeInfo && lesson.minquestions && gradeInfo.nquestions < lesson.minquestions) {
|
|
gradeLesson = false;
|
|
this.addMessage(result.messages, 'addon.mod_lesson.numberofpagesviewednotice', {
|
|
$a: {
|
|
nquestions: gradeInfo.nquestions,
|
|
minquestions: lesson.minquestions,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!options.accessInfo.canmanage) {
|
|
if (gradeLesson) {
|
|
const progress = await this.calculateProgress(lesson.id, options.accessInfo, {
|
|
cmId: lesson.coursemodule,
|
|
password: options.password,
|
|
review: options.review,
|
|
siteId: options.siteId,
|
|
});
|
|
|
|
this.addResultValueEolPage(result, 'progresscompleted', progress);
|
|
|
|
if (gradeInfo?.attempts) {
|
|
// User has answered questions.
|
|
if (!lesson.custom) {
|
|
this.addResultValueEolPage(result, 'numberofpagesviewed', gradeInfo.nquestions, true);
|
|
if (lesson.minquestions) {
|
|
if (gradeInfo.nquestions < lesson.minquestions) {
|
|
this.addResultValueEolPage(result, 'youshouldview', lesson.minquestions, true);
|
|
}
|
|
}
|
|
this.addResultValueEolPage(result, 'numberofcorrectanswers', gradeInfo.earned, true);
|
|
}
|
|
|
|
const entryData: Record<string, number> = {
|
|
score: gradeInfo.earned,
|
|
grade: gradeInfo.total,
|
|
};
|
|
if (gradeInfo.nmanual) {
|
|
entryData.tempmaxgrade = gradeInfo.total - gradeInfo.manualpoints;
|
|
entryData.essayquestions = gradeInfo.nmanual;
|
|
this.addResultValueEolPage(result, 'displayscorewithessays', entryData, true);
|
|
} else {
|
|
this.addResultValueEolPage(result, 'displayscorewithoutessays', entryData, true);
|
|
}
|
|
|
|
if (lesson.grade !== undefined && lesson.grade != CoreGradesProvider.TYPE_NONE) {
|
|
entryData.grade = CoreTextUtils.roundToDecimals(gradeInfo.grade * lesson.grade / 100, 1);
|
|
entryData.total = lesson.grade;
|
|
this.addResultValueEolPage(result, 'yourcurrentgradeisoutof', entryData, true);
|
|
}
|
|
|
|
} else {
|
|
// User hasn't answered any question, only content pages.
|
|
if (lesson.timelimit) {
|
|
if (options.outOfTime) {
|
|
this.addResultValueEolPage(result, 'eolstudentoutoftimenoanswers', true, true);
|
|
}
|
|
} else {
|
|
this.addResultValueEolPage(result, 'welldone', true, true);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Display for teacher.
|
|
if (lesson.grade != CoreGradesProvider.TYPE_NONE) {
|
|
this.addResultValueEolPage(result, 'displayofgrade', true, true);
|
|
}
|
|
}
|
|
|
|
if (lesson.modattempts && options.accessInfo.canmanage) {
|
|
this.addResultValueEolPage(result, 'modattemptsnoteacher', true, true);
|
|
}
|
|
|
|
if (gradeLesson) {
|
|
this.addResultValueEolPage(result, 'gradelesson', 1);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get the access information of a certain lesson.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the access information.
|
|
*/
|
|
async getAccessInformation(
|
|
lessonId: number,
|
|
options: CoreCourseCommonModWSOptions = {},
|
|
): Promise<AddonModLessonGetAccessInformationWSResponse> {
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: AddonModLessonGetAccessInformationWSParams = {
|
|
lessonid: lessonId,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getAccessInformationCacheKey(lessonId),
|
|
updateFrequency: CoreSite.FREQUENCY_OFTEN,
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
return site.read('mod_lesson_get_lesson_access_information', params, preSets);
|
|
}
|
|
|
|
/**
|
|
* Get cache key for access information WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getAccessInformationCacheKey(lessonId: number): string {
|
|
return ROOT_CACHE_KEY + 'accessInfo:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get content pages viewed in online and offline.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with an object with the online and offline viewed pages.
|
|
*/
|
|
async getContentPagesViewed(
|
|
lessonId: number,
|
|
retake: number,
|
|
options: CoreCourseCommonModWSOptions = {},
|
|
): Promise<{online: AddonModLessonWSContentPageViewed[]; offline: AddonModLessonPageAttemptRecord[]}> {
|
|
const type = AddonModLessonProvider.TYPE_STRUCTURE;
|
|
|
|
const [online, offline] = await Promise.all([
|
|
this.getContentPagesViewedOnline(lessonId, retake, options),
|
|
CoreUtils.ignoreErrors(
|
|
AddonModLessonOffline.getRetakeAttemptsForType(lessonId, retake, type, options.siteId),
|
|
),
|
|
]);
|
|
|
|
return {
|
|
online,
|
|
offline: offline || [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get content pages viewed WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @return Cache key.
|
|
*/
|
|
protected getContentPagesViewedCacheKey(lessonId: number, retake: number): string {
|
|
return this.getContentPagesViewedCommonCacheKey(lessonId) + ':' + retake;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get content pages viewed WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getContentPagesViewedCommonCacheKey(lessonId: number): string {
|
|
return ROOT_CACHE_KEY + 'contentPagesViewed:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get IDS of content pages viewed in online and offline.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with list of IDs.
|
|
*/
|
|
async getContentPagesViewedIds(
|
|
lessonId: number,
|
|
retake: number,
|
|
options: CoreCourseCommonModWSOptions = {},
|
|
): Promise<number[]> {
|
|
const result = await this.getContentPagesViewed(lessonId, retake, options);
|
|
|
|
const ids: Record<number, boolean> = {};
|
|
const pages = (<(AddonModLessonContentPageOrRecord)[]> result.online).concat(result.offline);
|
|
|
|
pages.forEach((page) => {
|
|
if (!ids[page.pageid]) {
|
|
ids[page.pageid] = true;
|
|
}
|
|
});
|
|
|
|
return Object.keys(ids).map((id) => Number(id));
|
|
}
|
|
|
|
/**
|
|
* Get the list of content pages viewed in the site for a certain retake.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the viewed pages.
|
|
*/
|
|
async getContentPagesViewedOnline(
|
|
lessonId: number,
|
|
retake: number,
|
|
options: CoreCourseCommonModWSOptions = {},
|
|
): Promise<AddonModLessonWSContentPageViewed[]> {
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: AddonModLessonGetContentPagesViewedWSParams = {
|
|
lessonid: lessonId,
|
|
lessonattempt: retake,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getContentPagesViewedCacheKey(lessonId, retake),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
const result = await site.read<AddonModLessonGetContentPagesViewedWSResponse>(
|
|
'mod_lesson_get_content_pages_viewed',
|
|
params,
|
|
preSets,
|
|
);
|
|
|
|
return result.pages;
|
|
}
|
|
|
|
/**
|
|
* Get the last content page viewed.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the last content page viewed.
|
|
*/
|
|
async getLastContentPageViewed(
|
|
lessonId: number,
|
|
retake: number,
|
|
options: CoreCourseCommonModWSOptions = {},
|
|
): Promise<AddonModLessonContentPageOrRecord | undefined> {
|
|
try {
|
|
const data = await this.getContentPagesViewed(lessonId, retake, options);
|
|
|
|
let lastPage: AddonModLessonContentPageOrRecord | undefined;
|
|
let maxTime = 0;
|
|
|
|
data.online.forEach((page) => {
|
|
if (page.timeseen > maxTime) {
|
|
lastPage = page;
|
|
maxTime = page.timeseen;
|
|
}
|
|
});
|
|
|
|
data.offline.forEach((page) => {
|
|
if (page.timemodified > maxTime) {
|
|
lastPage = page;
|
|
maxTime = page.timemodified;
|
|
}
|
|
});
|
|
|
|
return lastPage;
|
|
} catch {
|
|
// Error getting last page, don't return anything.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the last page seen.
|
|
* Based on Moodle's get_last_page_seen.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the last page seen.
|
|
*/
|
|
async getLastPageSeen(
|
|
lessonId: number,
|
|
retake: number,
|
|
options: CoreCourseCommonModWSOptions = {},
|
|
): Promise<number | undefined> {
|
|
options.siteId = options.siteId || CoreSites.getCurrentSiteId();
|
|
|
|
let lastPageSeen: number | undefined;
|
|
|
|
// Get the last question answered.
|
|
const answer = await AddonModLessonOffline.getLastQuestionPageAttempt(lessonId, retake, options.siteId);
|
|
|
|
if (answer) {
|
|
lastPageSeen = answer.newpageid;
|
|
}
|
|
|
|
// Now get the last content page viewed.
|
|
const page = await this.getLastContentPageViewed(lessonId, retake, options);
|
|
|
|
if (page) {
|
|
if (answer) {
|
|
const pageTime = 'timeseen' in page ? page.timeseen : page.timemodified;
|
|
if (pageTime > answer.timemodified) {
|
|
// This content page was viewed more recently than the question page.
|
|
lastPageSeen = (<AddonModLessonPageAttemptRecord> page).newpageid || page.pageid;
|
|
}
|
|
} else {
|
|
// Has not answered any questions but has viewed a content page.
|
|
lastPageSeen = (<AddonModLessonPageAttemptRecord> page).newpageid || page.pageid;
|
|
}
|
|
}
|
|
|
|
return lastPageSeen;
|
|
}
|
|
|
|
/**
|
|
* Get a Lesson by module ID.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param cmid Course module ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved when the lesson is retrieved.
|
|
*/
|
|
getLesson(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModLessonLessonWSData> {
|
|
return this.getLessonByField(courseId, 'coursemodule', cmId, options);
|
|
}
|
|
|
|
/**
|
|
* Get a Lesson 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 lesson is retrieved.
|
|
*/
|
|
protected async getLessonByField(
|
|
courseId: number,
|
|
key: string,
|
|
value: number,
|
|
options: CoreSitesCommonWSOptions = {},
|
|
): Promise<AddonModLessonLessonWSData> {
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: AddonModLessonGetLessonsByCoursesWSParams = {
|
|
courseids: [courseId],
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getLessonDataCacheKey(courseId),
|
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
const response = await site.read<AddonModLessonGetLessonsByCoursesWSResponse>(
|
|
'mod_lesson_get_lessons_by_courses',
|
|
params,
|
|
preSets,
|
|
);
|
|
|
|
const currentLesson = response.lessons.find((lesson) => lesson[key] == value);
|
|
|
|
if (currentLesson) {
|
|
return currentLesson;
|
|
}
|
|
|
|
throw new CoreError('Lesson not found.');
|
|
}
|
|
|
|
/**
|
|
* Get a Lesson by lesson ID.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param id Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved when the lesson is retrieved.
|
|
*/
|
|
getLessonById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModLessonLessonWSData> {
|
|
return this.getLessonByField(courseId, 'id', id, options);
|
|
}
|
|
|
|
/**
|
|
* Get cache key for Lesson data WS calls.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getLessonDataCacheKey(courseId: number): string {
|
|
return ROOT_CACHE_KEY + 'lesson:' + courseId;
|
|
}
|
|
|
|
/**
|
|
* Get a lesson protected with password.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the lesson.
|
|
*/
|
|
async getLessonWithPassword(
|
|
lessonId: number,
|
|
options: AddonModLessonGetWithPasswordOptions = {},
|
|
): Promise<AddonModLessonLessonWSData> {
|
|
const validatePassword = options.validatePassword ?? true;
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: AddonModLessonGetLessonWSParams = {
|
|
lessonid: lessonId,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getLessonWithPasswordCacheKey(lessonId),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
if (typeof options.password == 'string') {
|
|
params.password = options.password;
|
|
}
|
|
|
|
const response = await site.read<AddonModLessonGetLessonWSResponse>('mod_lesson_get_lesson', params, preSets);
|
|
|
|
if (typeof response.lesson.ongoing == 'undefined') {
|
|
// Basic data not received, password is wrong. Remove stored password.
|
|
this.removeStoredPassword(lessonId, site.id);
|
|
|
|
if (validatePassword) {
|
|
// Invalidate the data and reject.
|
|
await CoreUtils.ignoreErrors(this.invalidateLessonWithPassword(lessonId, site.id));
|
|
|
|
throw new CoreError(Translate.instant('addon.mod_lesson.loginfail'));
|
|
}
|
|
}
|
|
|
|
return response.lesson;
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get lesson with password WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getLessonWithPasswordCacheKey(lessonId: number): string {
|
|
return ROOT_CACHE_KEY + 'lessonWithPswrd:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Given a page ID, a jumpto and all the possible jumps, calcualate the new page ID.
|
|
*
|
|
* @param pageId Current page ID.
|
|
* @param jumpTo The jumpto.
|
|
* @param jumps Possible jumps.
|
|
* @return New page ID.
|
|
*/
|
|
protected getNewPageId(pageId: number, jumpTo: number, jumps: AddonModLessonPossibleJumps): number {
|
|
// If jump not found, return current jumpTo.
|
|
if (jumps && jumps[pageId] && jumps[pageId][jumpTo]) {
|
|
return jumps[pageId][jumpTo].calculatedjump;
|
|
} else if (!jumpTo) {
|
|
// Return current page.
|
|
return pageId;
|
|
}
|
|
|
|
return jumpTo;
|
|
}
|
|
|
|
/**
|
|
* Get the ongoing score message for the user (depending on the user permission and lesson settings).
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param accessInfo Access info.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the ongoing score message.
|
|
*/
|
|
getOngoingScoreMessage(
|
|
lesson: AddonModLessonLessonWSData,
|
|
accessInfo: AddonModLessonGetAccessInformationWSResponse,
|
|
options: AddonModLessonGradeOptions = {},
|
|
): Promise<string> {
|
|
|
|
if (accessInfo.canmanage) {
|
|
return Promise.resolve(Translate.instant('addon.mod_lesson.teacherongoingwarning'));
|
|
} else {
|
|
let retake = accessInfo.attemptscount;
|
|
if (options.review) {
|
|
retake--;
|
|
}
|
|
|
|
return this.lessonGrade(lesson, retake, options).then((gradeInfo) => {
|
|
if (lesson.custom) {
|
|
return Translate.instant(
|
|
'addon.mod_lesson.ongoingcustom',
|
|
{ $a: { score: gradeInfo.earned, currenthigh: gradeInfo.total } },
|
|
);
|
|
} else {
|
|
return Translate.instant(
|
|
'addon.mod_lesson.ongoingnormal',
|
|
{ $a: { correct: gradeInfo.earned, viewed: gradeInfo.attempts } },
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the possible answers from a page.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageId Page ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the list of possible answers.
|
|
*/
|
|
protected async getPageAnswers(
|
|
lesson: AddonModLessonLessonWSData,
|
|
pageId: number,
|
|
options: AddonModLessonPwdReviewOptions = {},
|
|
): Promise<AddonModLessonPageAnswerWSData[]> {
|
|
const data = await this.getPageData(lesson, pageId, {
|
|
includeContents: true,
|
|
...options, // Include all options.
|
|
readingStrategy: options.readingStrategy || CoreSitesReadingStrategy.PreferCache,
|
|
includeOfflineData: false,
|
|
});
|
|
|
|
return data.answers;
|
|
}
|
|
|
|
/**
|
|
* Get all the possible answers from a list of pages, indexed by answerId.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageIds List of page IDs.
|
|
* @param options Other options.
|
|
* @return Promise resolved with an object containing the answers.
|
|
*/
|
|
protected async getPagesAnswers(
|
|
lesson: AddonModLessonLessonWSData,
|
|
pageIds: number[],
|
|
options: AddonModLessonPwdReviewOptions = {},
|
|
): Promise<Record<number, AddonModLessonPageAnswerData>> {
|
|
|
|
const answers: Record<number, AddonModLessonPageAnswerData> = {};
|
|
|
|
await Promise.all(pageIds.map(async (pageId) => {
|
|
const pageAnswers = await this.getPageAnswers(lesson, pageId, options);
|
|
|
|
pageAnswers.forEach((answer) => {
|
|
// Include the pageid in each answer and add them to the final list.
|
|
answers[answer.id] = Object.assign(answer, { pageid: pageId });
|
|
});
|
|
}));
|
|
|
|
return answers;
|
|
}
|
|
|
|
/**
|
|
* Get page data.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageId Page ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the page data.
|
|
*/
|
|
async getPageData(
|
|
lesson: AddonModLessonLessonWSData,
|
|
pageId: number,
|
|
options: AddonModLessonGetPageDataOptions = {},
|
|
): Promise<AddonModLessonGetPageDataWSResponse> {
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: AddonModLessonGetPageDataWSParams = {
|
|
lessonid: lesson.id,
|
|
pageid: Number(pageId),
|
|
review: !!options.review,
|
|
returncontents: !!options.includeContents,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getPageDataCacheKey(lesson.id, pageId),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
if (typeof options.password == 'string') {
|
|
params.password = options.password;
|
|
}
|
|
|
|
if (options.review) {
|
|
// Force online mode in review.
|
|
preSets.getFromCache = false;
|
|
preSets.saveToCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
const response = await site.read<AddonModLessonGetPageDataWSResponse>('mod_lesson_get_page_data', params, preSets);
|
|
|
|
if (preSets.omitExpires && options.includeOfflineData && response.page && options.accessInfo && options.jumps) {
|
|
// Offline mode and valid page. Calculate the data that might be affected.
|
|
const calcData = await this.calculateOfflineData(lesson, options);
|
|
|
|
Object.assign(response, calcData);
|
|
|
|
response.messages = await this.getPageViewMessages(lesson, options.accessInfo, response.page, options.jumps, {
|
|
password: options.password,
|
|
siteId: options.siteId,
|
|
});
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get page data WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param pageId Page ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getPageDataCacheKey(lessonId: number, pageId: number): string {
|
|
return this.getPageDataCommonCacheKey(lessonId) + ':' + pageId;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get page data WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getPageDataCommonCacheKey(lessonId: number): string {
|
|
return ROOT_CACHE_KEY + 'pageData:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get lesson pages.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the pages.
|
|
*/
|
|
async getPages(lessonId: number, options: AddonModLessonPwdReviewOptions = {}): Promise<AddonModLessonGetPagesPageWSData[]> {
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: AddonModLessonGetPagesWSParams = {
|
|
lessonid: lessonId,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getPagesCacheKey(lessonId),
|
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
if (typeof options.password == 'string') {
|
|
params.password = options.password;
|
|
}
|
|
|
|
const response = await site.read<AddonModLessonGetPagesWSResponse>('mod_lesson_get_pages', params, preSets);
|
|
|
|
return response.pages;
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get pages WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getPagesCacheKey(lessonId: number): string {
|
|
return ROOT_CACHE_KEY + 'pages:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get possible jumps for a lesson.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the jumps.
|
|
*/
|
|
async getPagesPossibleJumps(
|
|
lessonId: number,
|
|
options: CoreCourseCommonModWSOptions = {},
|
|
): Promise<AddonModLessonPossibleJumps> {
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: AddonModLessonGetPagesPossibleJumpsWSParams = {
|
|
lessonid: lessonId,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getPagesPossibleJumpsCacheKey(lessonId),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
const response = await site.read<AddonModLessonGetPagesPossibleJumpsWSResponse>(
|
|
'mod_lesson_get_pages_possible_jumps',
|
|
params,
|
|
preSets,
|
|
);
|
|
|
|
// Index the jumps by page and jumpto.
|
|
const jumps: AddonModLessonPossibleJumps = {};
|
|
|
|
response.jumps.forEach((jump) => {
|
|
if (typeof jumps[jump.pageid] == 'undefined') {
|
|
jumps[jump.pageid] = {};
|
|
}
|
|
jumps[jump.pageid][jump.jumpto] = jump;
|
|
});
|
|
|
|
return jumps;
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get pages possible jumps WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getPagesPossibleJumpsCacheKey(lessonId: number): string {
|
|
return ROOT_CACHE_KEY + 'pagesJumps:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get different informative messages when processing a lesson page.
|
|
* Please try to use WS response messages instead of this function if possible.
|
|
* Based on Moodle's add_messages_on_page_process.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param accessInfo Access info.
|
|
* @param result Result of process page.
|
|
* @param review If the user wants to review just after finishing (1 hour margin).
|
|
* @param jumps Possible jumps.
|
|
* @return Array with the messages.
|
|
*/
|
|
getPageProcessMessages(
|
|
lesson: AddonModLessonLessonWSData,
|
|
accessInfo: AddonModLessonGetAccessInformationWSResponse,
|
|
result: AddonModLessonProcessPageResponse,
|
|
review: boolean,
|
|
jumps: AddonModLessonPossibleJumps,
|
|
): AddonModLessonMessageWSData[] {
|
|
const messages = [];
|
|
|
|
if (accessInfo.canmanage) {
|
|
// Warning for teachers to inform them that cluster and unseen does not work while logged in as a teacher.
|
|
if (this.lessonDisplayTeacherWarning(jumps)) {
|
|
this.addMessage(messages, 'addon.mod_lesson.teacherjumpwarning', {
|
|
$a: {
|
|
cluster: Translate.instant('addon.mod_lesson.clusterjump'),
|
|
unseen: Translate.instant('addon.mod_lesson.unseenpageinbranch'),
|
|
},
|
|
});
|
|
}
|
|
|
|
// Inform teacher that s/he will not see the timer.
|
|
if (lesson.timelimit) {
|
|
this.addMessage(messages, 'addon.mod_lesson.teachertimerwarning');
|
|
}
|
|
}
|
|
// Report attempts remaining.
|
|
if (result.attemptsremaining && result.attemptsremaining > 0 && lesson.review && !review) {
|
|
this.addMessage(messages, 'addon.mod_lesson.attemptsremaining', { $a: result.attemptsremaining });
|
|
}
|
|
|
|
return messages;
|
|
}
|
|
|
|
/**
|
|
* Get the IDs of all the pages that have at least 1 question attempt.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the IDs.
|
|
*/
|
|
async getPagesIdsWithQuestionAttempts(
|
|
lessonId: number,
|
|
retake: number,
|
|
options: AddonModLessonGetPagesIdsWithAttemptsOptions = {},
|
|
): Promise<number[]> {
|
|
|
|
const result = await this.getQuestionsAttempts(lessonId, retake, options);
|
|
|
|
const ids: Record<number, boolean> = {};
|
|
const attempts = (<AddonModLessonAnyAttemptData[]> result.online).concat(result.offline);
|
|
|
|
attempts.forEach((attempt) => {
|
|
if (!ids[attempt.pageid]) {
|
|
ids[attempt.pageid] = true;
|
|
}
|
|
});
|
|
|
|
return Object.keys(ids).map((id) => Number(id));
|
|
}
|
|
|
|
/**
|
|
* Get different informative messages when viewing a lesson page.
|
|
* Please try to use WS response messages instead of this function if possible.
|
|
* Based on Moodle's add_messages_on_page_view.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param accessInfo Access info. Required if offline is true.
|
|
* @param page Page loaded.
|
|
* @param jumps Possible jumps.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the list of messages.
|
|
*/
|
|
async getPageViewMessages(
|
|
lesson: AddonModLessonLessonWSData,
|
|
accessInfo: AddonModLessonGetAccessInformationWSResponse,
|
|
page: AddonModLessonPageWSData,
|
|
jumps: AddonModLessonPossibleJumps,
|
|
options: AddonModLessonGetPageViewMessagesOptions = {},
|
|
): Promise<AddonModLessonMessageWSData[]> {
|
|
|
|
const messages: AddonModLessonMessageWSData[] = [];
|
|
|
|
if (!accessInfo.canmanage) {
|
|
if (page.qtype == AddonModLessonProvider.LESSON_PAGE_BRANCHTABLE && lesson.minquestions) {
|
|
// Tell student how many questions they have seen, how many are required and their grade.
|
|
const retake = accessInfo.attemptscount;
|
|
|
|
const gradeInfo = await CoreUtils.ignoreErrors(this.lessonGrade(lesson, retake, options));
|
|
if (gradeInfo?.attempts) {
|
|
if (gradeInfo.nquestions < lesson.minquestions) {
|
|
this.addMessage(messages, 'addon.mod_lesson.numberofpagesviewednotice', {
|
|
$a: {
|
|
nquestions: gradeInfo.nquestions,
|
|
minquestions: lesson.minquestions,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (!options.review && !lesson.retake) {
|
|
this.addMessage(messages, 'addon.mod_lesson.numberofcorrectanswers', { $a: gradeInfo.earned });
|
|
|
|
if (lesson.grade !== undefined && lesson.grade != CoreGradesProvider.TYPE_NONE) {
|
|
this.addMessage(messages, 'addon.mod_lesson.yourcurrentgradeisoutof', { $a: {
|
|
grade: CoreTextUtils.roundToDecimals(gradeInfo.grade * lesson.grade / 100, 1),
|
|
total: lesson.grade,
|
|
} });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (lesson.timelimit) {
|
|
this.addMessage(messages, 'addon.mod_lesson.teachertimerwarning');
|
|
}
|
|
|
|
if (this.lessonDisplayTeacherWarning(jumps)) {
|
|
// Warning for teachers to inform them that cluster and unseen does not work while logged in as a teacher.
|
|
this.addMessage(messages, 'addon.mod_lesson.teacherjumpwarning', {
|
|
$a: {
|
|
cluster: Translate.instant('addon.mod_lesson.clusterjump'),
|
|
unseen: Translate.instant('addon.mod_lesson.unseenpageinbranch'),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
return messages;
|
|
}
|
|
|
|
/**
|
|
* Get questions attempts, including offline attempts.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the questions attempts.
|
|
*/
|
|
async getQuestionsAttempts(
|
|
lessonId: number,
|
|
retake: number,
|
|
options: AddonModLessonGetQuestionsAttemptsOptions = {},
|
|
): Promise<{online: AddonModLessonQuestionAttemptWSData[]; offline: AddonModLessonPageAttemptRecord[]}> {
|
|
|
|
const [online, offline] = await Promise.all([
|
|
this.getQuestionsAttemptsOnline(lessonId, retake, options),
|
|
CoreUtils.ignoreErrors(AddonModLessonOffline.getQuestionsAttempts(
|
|
lessonId,
|
|
retake,
|
|
options.correct,
|
|
options.pageId,
|
|
options.siteId,
|
|
)),
|
|
]);
|
|
|
|
return {
|
|
online,
|
|
offline: offline || [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get questions attempts WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param userId User ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getQuestionsAttemptsCacheKey(lessonId: number, retake: number, userId: number): string {
|
|
return this.getQuestionsAttemptsCommonCacheKey(lessonId) + ':' + userId + ':' + retake;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get questions attempts WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getQuestionsAttemptsCommonCacheKey(lessonId: number): string {
|
|
return ROOT_CACHE_KEY + 'questionsAttempts:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get questions attempts from the site.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the questions attempts.
|
|
*/
|
|
async getQuestionsAttemptsOnline(
|
|
lessonId: number,
|
|
retake: number,
|
|
options: AddonModLessonGetQuestionsAttemptsOptions = {},
|
|
): Promise<AddonModLessonQuestionAttemptWSData[]> {
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const userId = options.userId || site.getUserId();
|
|
|
|
// Don't pass "pageId" and "correct" params, they will be filtered locally.
|
|
const params: AddonModLessonGetQuestionsAttemptsWSParams = {
|
|
lessonid: lessonId,
|
|
attempt: retake,
|
|
userid: userId,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getQuestionsAttemptsCacheKey(lessonId, retake, userId),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
const response = await site.read<AddonModLessonGetQuestionsAttemptsWSResponse>(
|
|
'mod_lesson_get_questions_attempts',
|
|
params,
|
|
preSets,
|
|
);
|
|
|
|
if (!options.pageId && !options.correct) {
|
|
return response.attempts;
|
|
}
|
|
|
|
// Filter the attempts.
|
|
return response.attempts.filter((attempt) => {
|
|
if (options.correct && !attempt.correct) {
|
|
return false;
|
|
}
|
|
|
|
if (options.pageId && attempt.pageid != options.pageId) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the overview of retakes in a lesson (named "attempts overview" in Moodle).
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the retakes overview, undefined if no attempts.
|
|
*/
|
|
async getRetakesOverview(
|
|
lessonId: number,
|
|
options: AddonModLessonGroupOptions = {},
|
|
): Promise<AddonModLessonAttemptsOverviewWSData | undefined> {
|
|
const groupId = options.groupId || 0;
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: AddonModLessonGetAttemptsOverviewWSParams = {
|
|
lessonid: lessonId,
|
|
groupid: groupId,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getRetakesOverviewCacheKey(lessonId, groupId),
|
|
updateFrequency: CoreSite.FREQUENCY_OFTEN,
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
const response = await site.read<AddonModLessonGetAttemptsOverviewWSResponse>(
|
|
'mod_lesson_get_attempts_overview',
|
|
params,
|
|
preSets,
|
|
);
|
|
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get retakes overview WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param groupId Group ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getRetakesOverviewCacheKey(lessonId: number, groupId: number): string {
|
|
return this.getRetakesOverviewCommonCacheKey(lessonId) + ':' + groupId;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get retakes overview WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getRetakesOverviewCommonCacheKey(lessonId: number): string {
|
|
return ROOT_CACHE_KEY + 'retakesOverview:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get a password stored in DB.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with password on success, rejected otherwise.
|
|
*/
|
|
async getStoredPassword(lessonId: number, siteId?: string): Promise<string> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
const entry = await site.getDb().getRecord<AddonModLessonPasswordDBRecord>(PASSWORD_TABLE_NAME, { lessonid: lessonId });
|
|
|
|
return entry.password;
|
|
}
|
|
|
|
/**
|
|
* Finds all pages that appear to be a subtype of the provided pageId until an end point specified within "ends" is
|
|
* encountered or no more pages exist.
|
|
* Based on Moodle's get_sub_pages_of.
|
|
*
|
|
* @param pages Index of lesson pages, indexed by page ID. See createPagesIndex.
|
|
* @param pageId Page ID to get subpages of.
|
|
* @param end An array of LESSON_PAGE_* types that signify an end of the subtype.
|
|
* @return List of subpages.
|
|
*/
|
|
getSubpagesOf(pages: Record<number, AddonModLessonPageWSData>, pageId: number, ends: number[]): AddonModLessonPageWSData[] {
|
|
const subPages: AddonModLessonPageWSData[] = [];
|
|
|
|
pageId = pages[pageId].nextpageid; // Move to the first page after the given page.
|
|
ends = ends || [];
|
|
|
|
// Search until there are no more pages or it reaches a page of the searched types.
|
|
while (pageId && ends.indexOf(pages[pageId].qtype) == -1) {
|
|
subPages.push(pages[pageId]);
|
|
pageId = pages[pageId].nextpageid;
|
|
}
|
|
|
|
return subPages;
|
|
}
|
|
|
|
/**
|
|
* Get lesson timers.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the pages.
|
|
*/
|
|
async getTimers(lessonId: number, options: AddonModLessonUserOptions = {}): Promise<AddonModLessonUserTimerWSData[]> {
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const userId = options.userId || site.getUserId();
|
|
const params: ModLessonGetUserTimersWSParams = {
|
|
lessonid: lessonId,
|
|
userid: userId,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getTimersCacheKey(lessonId, userId),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
const response = await site.read<ModLessonGetUserTimersWSResponse>('mod_lesson_get_user_timers', params, preSets);
|
|
|
|
return response.timers;
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get timers WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param userId User ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getTimersCacheKey(lessonId: number, userId: number): string {
|
|
return this.getTimersCommonCacheKey(lessonId) + ':' + userId;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get timers WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getTimersCommonCacheKey(lessonId: number): string {
|
|
return ROOT_CACHE_KEY + 'timers:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get the list of used answers (with valid answer) in a multichoice question page.
|
|
*
|
|
* @param pageData Page data for the page to process.
|
|
* @return List of used answers.
|
|
*/
|
|
protected getUsedAnswersMultichoice(pageData: AddonModLessonGetPageDataWSResponse): AddonModLessonPageAnswerWSData[] {
|
|
const answers = CoreUtils.clone(pageData.answers);
|
|
|
|
return answers.filter((entry) => entry.answer !== undefined && entry.answer !== '');
|
|
}
|
|
|
|
/**
|
|
* Get the user's response in a matching question page.
|
|
*
|
|
* @param data Data containing the user answer.
|
|
* @return User response.
|
|
*/
|
|
protected getUserResponseMatching(data: Record<string, unknown>): Record<string, string> {
|
|
if (data.response) {
|
|
// The data is already stored as expected. Return it.
|
|
return <Record<string, string>> data.response;
|
|
}
|
|
|
|
// Data is stored in properties like 'response[379]'. Recreate the response object.
|
|
const response: Record<string, string> = {};
|
|
|
|
for (const key in data) {
|
|
const match = key.match(/^response\[(\d+)\]/);
|
|
|
|
if (match && match.length > 1) {
|
|
response[match[1]] = <string> data[key];
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Get the user's response in a multichoice page if multiple answers are allowed.
|
|
*
|
|
* @param data Data containing the user answer.
|
|
* @return User response.
|
|
*/
|
|
protected getUserResponseMultichoice(data: Record<string, unknown>): number[] | undefined {
|
|
if (data.answer) {
|
|
// The data is already stored as expected. If it's valid, parse the values to int.
|
|
if (Array.isArray(data.answer)) {
|
|
return data.answer.map((value) => parseInt(value, 10));
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// Data is stored in properties like 'answer[379]'. Recreate the answer array.
|
|
const answer: number[] = [];
|
|
for (const key in data) {
|
|
const match = key.match(/^answer\[(\d+)\]/);
|
|
if (match && match.length > 1) {
|
|
answer.push(parseInt(match[1], 10));
|
|
}
|
|
}
|
|
|
|
return answer;
|
|
}
|
|
|
|
/**
|
|
* Get a user's retake.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number
|
|
* @param options Other options.
|
|
* @return Promise resolved with the retake data.
|
|
*/
|
|
async getUserRetake(
|
|
lessonId: number,
|
|
retake: number,
|
|
options: AddonModLessonUserOptions = {},
|
|
): Promise<AddonModLessonGetUserAttemptWSResponse> {
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const userId = options.userId || site.getUserId();
|
|
const params: AddonModLessonGetUserAttemptWSParams = {
|
|
lessonid: lessonId,
|
|
userid: userId,
|
|
lessonattempt: retake,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getUserRetakeCacheKey(lessonId, userId, retake),
|
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
return site.read('mod_lesson_get_user_attempt', params, preSets);
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get user retake WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param userId User ID.
|
|
* @param retake Retake number
|
|
* @return Cache key.
|
|
*/
|
|
protected getUserRetakeCacheKey(lessonId: number, userId: number, retake: number): string {
|
|
return this.getUserRetakeUserCacheKey(lessonId, userId) + ':' + retake;
|
|
}
|
|
|
|
/**
|
|
* Get user cache key for get user retake WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param userId User ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getUserRetakeUserCacheKey(lessonId: number, userId: number): string {
|
|
return this.getUserRetakeLessonCacheKey(lessonId) + ':' + userId;
|
|
}
|
|
|
|
/**
|
|
* Get lesson cache key for get user retake WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getUserRetakeLessonCacheKey(lessonId: number): string {
|
|
return ROOT_CACHE_KEY + 'userRetake:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get the prevent access reason to display for a certain lesson.
|
|
*
|
|
* @param info Lesson access info.
|
|
* @param ignorePassword Whether password protected reason should be ignored (user already entered the password).
|
|
* @param isReview Whether user is reviewing a retake.
|
|
* @return Prevent access reason.
|
|
*/
|
|
getPreventAccessReason(
|
|
info: AddonModLessonGetAccessInformationWSResponse,
|
|
ignorePassword?: boolean,
|
|
isReview?: boolean,
|
|
): AddonModLessonPreventAccessReason | undefined {
|
|
if (!info?.preventaccessreasons) {
|
|
return;
|
|
}
|
|
|
|
let reason: AddonModLessonPreventAccessReason | undefined;
|
|
for (let i = 0; i < info.preventaccessreasons.length; i++) {
|
|
const entry = info.preventaccessreasons[i];
|
|
|
|
if (entry.reason == 'lessonopen' || entry.reason == 'lessonclosed') {
|
|
// Time restrictions are the most prioritary, return it.
|
|
return entry;
|
|
} else if (entry.reason == 'passwordprotectedlesson') {
|
|
if (!ignorePassword) {
|
|
// Treat password before all other reasons.
|
|
reason = entry;
|
|
}
|
|
} else if (entry.reason == 'noretake' && isReview) {
|
|
// Ignore noretake error when reviewing.
|
|
} else if (!reason) {
|
|
// Rest of cases, just return any of them.
|
|
reason = entry;
|
|
}
|
|
}
|
|
|
|
return reason;
|
|
}
|
|
|
|
/**
|
|
* Check if a jump is correct.
|
|
* Based in Moodle's jumpto_is_correct.
|
|
*
|
|
* @param pageId ID of the page from which you are jumping from.
|
|
* @param jumpTo The jumpto number.
|
|
* @param pageIndex Object containing all the pages indexed by ID. See createPagesIndex.
|
|
* @return Whether jump is correct.
|
|
*/
|
|
jumptoIsCorrect(pageId: number, jumpTo: number, pageIndex: Record<number, AddonModLessonPageWSData>): boolean {
|
|
// First test the special values.
|
|
if (!jumpTo) {
|
|
// Same page
|
|
return false;
|
|
} else if (jumpTo == AddonModLessonProvider.LESSON_NEXTPAGE) {
|
|
return true;
|
|
} else if (jumpTo == AddonModLessonProvider.LESSON_UNSEENBRANCHPAGE) {
|
|
return true;
|
|
} else if (jumpTo == AddonModLessonProvider.LESSON_RANDOMPAGE) {
|
|
return true;
|
|
} else if (jumpTo == AddonModLessonProvider.LESSON_CLUSTERJUMP) {
|
|
return true;
|
|
} else if (jumpTo == AddonModLessonProvider.LESSON_EOL) {
|
|
return true;
|
|
}
|
|
|
|
let aPageId = pageIndex[pageId].nextpageid;
|
|
while (aPageId) {
|
|
if (jumpTo == aPageId) {
|
|
return true;
|
|
}
|
|
|
|
aPageId = pageIndex[aPageId].nextpageid;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Invalidates Lesson data.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateAccessInformation(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(lessonId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates content pages viewed for all retakes.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateContentPagesViewed(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKeyStartingWith(this.getContentPagesViewedCommonCacheKey(lessonId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates content pages viewed for a certain retake.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateContentPagesViewedForRetake(lessonId: number, retake: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getContentPagesViewedCacheKey(lessonId, retake));
|
|
}
|
|
|
|
/**
|
|
* Invalidates Lesson data.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateLessonData(courseId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getLessonDataCacheKey(courseId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates lesson with password.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateLessonWithPassword(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getLessonWithPasswordCacheKey(lessonId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates page data for all pages.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidatePageData(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKeyStartingWith(this.getPageDataCommonCacheKey(lessonId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates page data for a certain page.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param pageId Page ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidatePageDataForPage(lessonId: number, pageId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getPageDataCacheKey(lessonId, pageId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates pages.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidatePages(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getPagesCacheKey(lessonId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates pages possible jumps.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidatePagesPossibleJumps(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getPagesPossibleJumpsCacheKey(lessonId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates questions attempts for all retakes.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateQuestionsAttempts(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKeyStartingWith(this.getQuestionsAttemptsCommonCacheKey(lessonId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates question attempts for a certain retake and user.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param siteId Site ID. If not defined, current site..
|
|
* @param userId User ID. If not defined, site's user.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateQuestionsAttemptsForRetake(lessonId: number, retake: number, siteId?: string, userId?: number): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getQuestionsAttemptsCacheKey(lessonId, retake, userId || site.getUserId()));
|
|
}
|
|
|
|
/**
|
|
* Invalidates retakes overview for all groups in a lesson.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateRetakesOverview(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKeyStartingWith(this.getRetakesOverviewCommonCacheKey(lessonId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates retakes overview for a certain group in a lesson.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param groupId Group ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateRetakesOverviewForGroup(lessonId: number, groupId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getRetakesOverviewCacheKey(lessonId, groupId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates timers for all users in a lesson.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateTimers(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKeyStartingWith(this.getTimersCommonCacheKey(lessonId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates timers for a certain user.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @param userId User ID. If not defined, site's current user.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateTimersForUser(lessonId: number, siteId?: string, userId?: number): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getTimersCacheKey(lessonId, userId || site.getUserId()));
|
|
}
|
|
|
|
/**
|
|
* Invalidates a certain retake for a certain user.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param userId User ID. Undefined for current user.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateUserRetake(lessonId: number, retake: number, userId?: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getUserRetakeCacheKey(lessonId, userId || site.getUserId(), retake));
|
|
}
|
|
|
|
/**
|
|
* Invalidates all retakes for all users in a lesson.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateUserRetakesForLesson(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKeyStartingWith(this.getUserRetakeLessonCacheKey(lessonId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates all retakes for a certain user in a lesson.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param userId User ID. Undefined for current user.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateUserRetakesForUser(lessonId: number, userId?: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKeyStartingWith(this.getUserRetakeUserCacheKey(lessonId, userId || site.getUserId()));
|
|
}
|
|
|
|
/**
|
|
* Check if a page answer is correct.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageId The page ID.
|
|
* @param answer The answer to check.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @return Whether the answer is correct.
|
|
*/
|
|
protected isAnswerCorrect(
|
|
lesson: AddonModLessonLessonWSData,
|
|
pageId: number,
|
|
answer: AddonModLessonPageAnswerWSData,
|
|
pageIndex: Record<number, AddonModLessonPageWSData>,
|
|
): boolean {
|
|
if (lesson.custom) {
|
|
// Custom scores. If score on answer is positive, it is correct.
|
|
return !!(answer.score && answer.score > 0);
|
|
} else {
|
|
return this.jumptoIsCorrect(pageId, answer.jumpto || 0, pageIndex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a lesson is enabled to be used in offline.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @return Whether offline is enabled.
|
|
*/
|
|
isLessonOffline(lesson: AddonModLessonLessonWSData): boolean {
|
|
return !!lesson.allowofflineattempts;
|
|
}
|
|
|
|
/**
|
|
* Check if a lesson is password protected based in the access info.
|
|
*
|
|
* @param info Lesson access info.
|
|
* @return Whether the lesson is password protected.
|
|
*/
|
|
isPasswordProtected(info: AddonModLessonGetAccessInformationWSResponse): boolean {
|
|
if (!info || !info.preventaccessreasons) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < info.preventaccessreasons.length; i++) {
|
|
const entry = info.preventaccessreasons[i];
|
|
|
|
if (entry.reason == 'passwordprotectedlesson') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the lesson WS are available.
|
|
*
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
|
|
*/
|
|
async isPluginEnabled(siteId?: string): Promise<boolean> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
// All WS were introduced at the same time so checking one is enough.
|
|
return site.wsAvailable('mod_lesson_get_lesson_access_information');
|
|
}
|
|
|
|
/**
|
|
* Check if a page is a question page or a content page.
|
|
*
|
|
* @param type Type of the page.
|
|
* @return True if question page, false if content page.
|
|
*/
|
|
isQuestionPage(type: number): boolean {
|
|
return type == AddonModLessonProvider.TYPE_QUESTION;
|
|
}
|
|
|
|
/**
|
|
* Start or continue a retake.
|
|
*
|
|
* @param id Lesson ID.
|
|
* @param password Lesson password (if any).
|
|
* @param pageId Page id to continue from (only when continuing a retake).
|
|
* @param review If the user wants to review just after finishing (1 hour margin).
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the WS call is successful.
|
|
*/
|
|
async launchRetake(
|
|
id: number,
|
|
password?: string,
|
|
pageId?: number,
|
|
review?: boolean,
|
|
siteId?: string,
|
|
): Promise<AddonModLessonLaunchAttemptWSResponse> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
const params: AddonModLessonLaunchAttemptWSParams = {
|
|
lessonid: id,
|
|
review: !!review,
|
|
};
|
|
if (typeof password == 'string') {
|
|
params.password = password;
|
|
}
|
|
if (typeof pageId == 'number') {
|
|
params.pageid = pageId;
|
|
}
|
|
|
|
const response = await site.write<AddonModLessonLaunchAttemptWSResponse>('mod_lesson_launch_attempt', params);
|
|
|
|
CoreEvents.trigger<AddonModLessonDataSentData>(AddonModLessonProvider.DATA_SENT_EVENT, {
|
|
lessonId: id,
|
|
type: 'launch',
|
|
}, CoreSites.getCurrentSiteId());
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Check if the user left during a timed session.
|
|
*
|
|
* @param info Lesson access info.
|
|
* @return True if left during timed, false otherwise.
|
|
*/
|
|
leftDuringTimed(info?: AddonModLessonGetAccessInformationWSResponse): boolean {
|
|
return !!(info?.lastpageseen && info.lastpageseen != AddonModLessonProvider.LESSON_EOL && info.leftduringtimedsession);
|
|
}
|
|
|
|
/**
|
|
* Checks to see if a LESSON_CLUSTERJUMP or a LESSON_UNSEENBRANCHPAGE is used in a lesson.
|
|
* Based on Moodle's lesson_display_teacher_warning.
|
|
*
|
|
* @param jumps Possible jumps.
|
|
* @return Whether the lesson uses one of those jumps.
|
|
*/
|
|
lessonDisplayTeacherWarning(jumps: AddonModLessonPossibleJumps): boolean {
|
|
if (!jumps) {
|
|
return false;
|
|
}
|
|
|
|
// Check if any jump is to cluster or unseen content page.
|
|
for (const pageId in jumps) {
|
|
for (const jumpto in jumps[pageId]) {
|
|
const jumptoNum = Number(jumpto);
|
|
|
|
if (jumptoNum == AddonModLessonProvider.LESSON_CLUSTERJUMP ||
|
|
jumptoNum == AddonModLessonProvider.LESSON_UNSEENBRANCHPAGE) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Calculates a user's grade for a lesson.
|
|
* Based on Moodle's lesson_grade.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the grade data.
|
|
*/
|
|
async lessonGrade(
|
|
lesson: AddonModLessonLessonWSData,
|
|
retake: number,
|
|
options: AddonModLessonGradeOptions = {},
|
|
): Promise<AddonModLessonGrade> {
|
|
|
|
const result: AddonModLessonGrade = {
|
|
nquestions: 0,
|
|
attempts: 0,
|
|
total: 0,
|
|
earned: 0,
|
|
grade: 0,
|
|
nmanual: 0,
|
|
manualpoints: 0,
|
|
};
|
|
|
|
// Get the questions attempts for the user.
|
|
const attemptsData = await this.getQuestionsAttempts(lesson.id, retake, {
|
|
cmId: lesson.coursemodule,
|
|
siteId: options.siteId,
|
|
userId: options.userId,
|
|
});
|
|
|
|
const attempts = (<AddonModLessonAnyAttemptData[]> attemptsData.online).concat(attemptsData.offline);
|
|
|
|
if (!attempts.length) {
|
|
// No attempts.
|
|
return result;
|
|
}
|
|
|
|
// Create the pageIndex if it isn't provided.
|
|
if (!options.pageIndex) {
|
|
const pages = await this.getPages(lesson.id, {
|
|
password: options.password,
|
|
cmId: lesson.coursemodule,
|
|
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
|
siteId: options.siteId,
|
|
});
|
|
|
|
options.pageIndex = this.createPagesIndex(pages);
|
|
}
|
|
|
|
const attemptSet: Record<number, AddonModLessonAnyAttemptData[]> = {};
|
|
const pageIds: number[] = [];
|
|
|
|
// Group each try with its page.
|
|
attempts.forEach((attempt) => {
|
|
if (!attemptSet[attempt.pageid]) {
|
|
attemptSet[attempt.pageid] = [];
|
|
pageIds.push(attempt.pageid);
|
|
}
|
|
attemptSet[attempt.pageid].push(attempt);
|
|
});
|
|
|
|
if (lesson.maxattempts && lesson.maxattempts > 0) {
|
|
// Drop all attempts that go beyond max attempts for the lesson.
|
|
for (const pageId in attemptSet) {
|
|
// Sort the list by time in ascending order.
|
|
const attempts = attemptSet[pageId].sort((a, b) =>
|
|
('timeseen' in a ? a.timeseen : a.timemodified) - ('timeseen' in b ? b.timeseen : b.timemodified));
|
|
|
|
attemptSet[pageId] = attempts.slice(0, lesson.maxattempts);
|
|
}
|
|
}
|
|
|
|
// Get all the answers from the pages the user answered.
|
|
const answers = await this.getPagesAnswers(lesson, pageIds, options);
|
|
|
|
// Number of pages answered.
|
|
result.nquestions = Object.keys(attemptSet).length;
|
|
|
|
for (const pageId in attemptSet) {
|
|
const attempts = attemptSet[pageId];
|
|
const lastAttempt = attempts[attempts.length - 1];
|
|
|
|
if (lesson.custom) {
|
|
// If essay question, handle it, otherwise add to score.
|
|
if (options.pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const score: number | undefined = (<any> lastAttempt.useranswer)?.score;
|
|
if (typeof score != 'undefined') {
|
|
result.earned += score;
|
|
}
|
|
result.nmanual++;
|
|
result.manualpoints += answers[lastAttempt.answerid!].score || 0;
|
|
} else if (lastAttempt.answerid) {
|
|
result.earned += answers[lastAttempt.answerid!].score || 0;
|
|
}
|
|
} else {
|
|
attempts.forEach((attempt) => {
|
|
result.earned += attempt.correct ? 1 : 0;
|
|
});
|
|
|
|
// If essay question, increase numbers.
|
|
if (options.pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) {
|
|
result.nmanual++;
|
|
result.manualpoints++;
|
|
}
|
|
}
|
|
|
|
// Number of times answered.
|
|
result.attempts += attempts.length;
|
|
}
|
|
|
|
if (lesson.custom) {
|
|
const bestScores: Record<number, number> = {};
|
|
|
|
// Find the highest possible score per page to get our total.
|
|
for (const answerId in answers) {
|
|
const answer = answers[answerId];
|
|
|
|
if (typeof bestScores[answer.pageid] == 'undefined') {
|
|
bestScores[answer.pageid] = answer.score || 0;
|
|
} else if (bestScores[answer.pageid] < (answer.score || 0)) {
|
|
bestScores[answer.pageid] = answer.score || 0;
|
|
}
|
|
}
|
|
|
|
// Sum all the scores.
|
|
for (const pageId in bestScores) {
|
|
result.total += bestScores[pageId];
|
|
}
|
|
} else {
|
|
// Check to make sure the student has answered the minimum questions.
|
|
if (lesson.minquestions && result.nquestions < lesson.minquestions) {
|
|
// Nope, increase number viewed by the amount of unanswered questions.
|
|
result.total = result.attempts + (lesson.minquestions - result.nquestions);
|
|
} else {
|
|
result.total = result.attempts;
|
|
}
|
|
}
|
|
|
|
if (result.total) { // Not zero.
|
|
result.grade = CoreTextUtils.roundToDecimals(result.earned * 100 / result.total, 5);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Report a lesson as being viewed.
|
|
*
|
|
* @param id Module ID.
|
|
* @param password Lesson password (if any).
|
|
* @param name Name of the assign.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the WS call is successful.
|
|
*/
|
|
async logViewLesson(id: number, password?: string, name?: string, siteId?: string): Promise<void> {
|
|
const params: AddonModLessonViewLessonWSParams = {
|
|
lessonid: id,
|
|
};
|
|
|
|
if (typeof password == 'string') {
|
|
params.password = password;
|
|
}
|
|
|
|
await CoreCourseLogHelper.logSingle(
|
|
'mod_lesson_view_lesson',
|
|
params,
|
|
AddonModLessonProvider.COMPONENT,
|
|
id,
|
|
name,
|
|
'lesson',
|
|
{},
|
|
siteId,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Process a lesson page, saving its data.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param courseId Course ID the lesson belongs to.
|
|
* @param pageData Page data for the page to process.
|
|
* @param data Data to save.
|
|
* @param options Other options.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
async processPage(
|
|
lesson: AddonModLessonLessonWSData,
|
|
courseId: number,
|
|
pageData: AddonModLessonGetPageDataWSResponse,
|
|
data: Record<string, unknown>,
|
|
options: AddonModLessonProcessPageOptions = {},
|
|
): Promise<AddonModLessonProcessPageResponse> {
|
|
|
|
options.siteId = options.siteId || CoreSites.getCurrentSiteId();
|
|
if (!pageData.page) {
|
|
throw new CoreError('Page data not supplied.');
|
|
}
|
|
|
|
const page = pageData.page;
|
|
const pageId = page.id;
|
|
|
|
if (!options.offline) {
|
|
const response = <AddonModLessonProcessPageResponse> await this.processPageOnline(lesson.id, pageId, data, options);
|
|
|
|
CoreEvents.trigger<AddonModLessonDataSentData>(AddonModLessonProvider.DATA_SENT_EVENT, {
|
|
lessonId: lesson.id,
|
|
type: 'process',
|
|
courseId: courseId,
|
|
pageId: pageId,
|
|
review: options.review,
|
|
}, CoreSites.getCurrentSiteId());
|
|
|
|
response.sent = true;
|
|
|
|
return response;
|
|
}
|
|
|
|
if (!options.accessInfo || !options.jumps) {
|
|
throw new CoreError('Access info or jumps not supplied to processPage.');
|
|
}
|
|
|
|
// Get the list of pages of the lesson.
|
|
const pages = await this.getPages(lesson.id, {
|
|
cmId: lesson.coursemodule,
|
|
password: options.password,
|
|
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
|
siteId: options.siteId,
|
|
});
|
|
|
|
const pageIndex = this.createPagesIndex(pages);
|
|
const result: AddonModLessonProcessPageResponse = {
|
|
newpageid: <number> data.newpageid,
|
|
inmediatejump: false,
|
|
nodefaultresponse: false,
|
|
feedback: '',
|
|
attemptsremaining: null,
|
|
correctanswer: false,
|
|
noanswer: false,
|
|
isessayquestion: false,
|
|
maxattemptsreached: false,
|
|
response: '',
|
|
studentanswer: '',
|
|
userresponse: '',
|
|
reviewmode: false,
|
|
ongoingscore: '',
|
|
progress: null,
|
|
displaymenu: false,
|
|
messages: [],
|
|
};
|
|
|
|
if (pageData.answers.length) {
|
|
const recordAttemptResult = await this.recordAttempt(
|
|
lesson,
|
|
courseId,
|
|
pageData,
|
|
data,
|
|
!!options.review,
|
|
options.accessInfo,
|
|
options.jumps,
|
|
pageIndex,
|
|
options.siteId,
|
|
);
|
|
|
|
Object.assign(result, recordAttemptResult);
|
|
} else {
|
|
// If no answers, progress to the next page (as set by newpageid).
|
|
result.nodefaultresponse = true;
|
|
}
|
|
|
|
result.newpageid = this.getNewPageId(pageData.page.id, result.newpageid, options.jumps);
|
|
|
|
// Calculate some needed offline data.
|
|
const calculatedData = await this.calculateOfflineData(lesson, {
|
|
accessInfo: options.accessInfo,
|
|
password: options.password,
|
|
review: options.review,
|
|
pageIndex,
|
|
siteId: options.siteId,
|
|
});
|
|
|
|
// Add some default data to match the WS response.
|
|
return {
|
|
...result,
|
|
...calculatedData,
|
|
displaymenu: pageData.displaymenu, // Keep the same value since we can't calculate it in offline.
|
|
messages: this.getPageProcessMessages(lesson, options.accessInfo, result, !!options.review, options.jumps),
|
|
warnings: [],
|
|
sent: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Process a lesson page, saving its data. It will fail if offline or cannot connect.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param pageId Page ID.
|
|
* @param data Data to save.
|
|
* @param options Other options.
|
|
* @return Promise resolved in success, rejected otherwise.
|
|
*/
|
|
async processPageOnline(
|
|
lessonId: number,
|
|
pageId: number,
|
|
data: Record<string, unknown>,
|
|
options: AddonModLessonProcessPageOnlineOptions = {},
|
|
): Promise<AddonModLessonProcessPageWSResponse> {
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: AddonModLessonProcessPageWSParams = {
|
|
lessonid: lessonId,
|
|
pageid: pageId,
|
|
data: CoreUtils.objectToArrayOfObjects<ProcessPageData>(data, 'name', 'value', true),
|
|
review: !!options.review,
|
|
};
|
|
|
|
if (typeof options.password == 'string') {
|
|
params.password = options.password;
|
|
}
|
|
|
|
return site.write('mod_lesson_process_page', params);
|
|
}
|
|
|
|
/**
|
|
* Records an attempt on a certain page.
|
|
* Based on Moodle's record_attempt.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param courseId Course ID the lesson belongs to.
|
|
* @param pageData Page data for the page to process.
|
|
* @param data Data to save.
|
|
* @param review If the user wants to review just after finishing (1 hour margin).
|
|
* @param accessInfo Access info.
|
|
* @param jumps Possible jumps.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with the result.
|
|
*/
|
|
protected async recordAttempt(
|
|
lesson: AddonModLessonLessonWSData,
|
|
courseId: number,
|
|
pageData: AddonModLessonGetPageDataWSResponse,
|
|
data: Record<string, unknown>,
|
|
review: boolean,
|
|
accessInfo: AddonModLessonGetAccessInformationWSResponse,
|
|
jumps: AddonModLessonPossibleJumps,
|
|
pageIndex: Record<number, AddonModLessonPageWSData>,
|
|
siteId?: string,
|
|
): Promise<AddonModLessonRecordAttemptResult> {
|
|
|
|
if (!pageData.page) {
|
|
throw new CoreError('Page data not supplied.');
|
|
}
|
|
|
|
// Check the user answer. Each page type has its own implementation.
|
|
const result: AddonModLessonRecordAttemptResult = this.checkAnswer(lesson, pageData, data, jumps, pageIndex);
|
|
const retake = accessInfo.attemptscount;
|
|
|
|
// Processes inmediate jumps.
|
|
if (result.inmediatejump) {
|
|
if (pageData.page?.qtype == AddonModLessonProvider.LESSON_PAGE_BRANCHTABLE) {
|
|
// Store the content page data. In Moodle this is stored in a separate table, during checkAnswer.
|
|
await AddonModLessonOffline.processPage(
|
|
lesson.id,
|
|
courseId,
|
|
retake,
|
|
pageData.page,
|
|
data,
|
|
result.newpageid,
|
|
result.answerid,
|
|
false,
|
|
result.userresponse,
|
|
siteId,
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
let nAttempts: number | undefined;
|
|
result.attemptsremaining = 0;
|
|
result.maxattemptsreached = false;
|
|
|
|
if (result.noanswer) {
|
|
result.newpageid = pageData.page.id; // Display same page again.
|
|
result.feedback = Translate.instant('addon.mod_lesson.noanswer');
|
|
|
|
return result;
|
|
}
|
|
|
|
if (!accessInfo.canmanage) {
|
|
// Get the number of attempts that have been made on this question for this student and retake.
|
|
const attempts = await this.getQuestionsAttempts(lesson.id, retake, {
|
|
cmId: lesson.coursemodule,
|
|
pageId: pageData.page.id,
|
|
siteId,
|
|
});
|
|
|
|
nAttempts = attempts.online.length + attempts.offline.length;
|
|
|
|
// Check if they have reached (or exceeded) the maximum number of attempts allowed.
|
|
if (lesson.maxattempts && lesson.maxattempts > 0 && nAttempts >= lesson.maxattempts) {
|
|
result.maxattemptsreached = true;
|
|
result.feedback = Translate.instant('addon.mod_lesson.maximumnumberofattemptsreached');
|
|
result.newpageid = AddonModLessonProvider.LESSON_NEXTPAGE;
|
|
|
|
return result;
|
|
}
|
|
|
|
// Only insert a record if we are not reviewing the lesson.
|
|
if (!review && (lesson.retake || (!lesson.retake && !retake))) {
|
|
// Store the student's attempt and increase the number of attempts made.
|
|
// Calculate and store the new page ID to prevent having to recalculate it later.
|
|
const newPageId = this.getNewPageId(pageData.page.id, result.newpageid, jumps);
|
|
|
|
await AddonModLessonOffline.processPage(
|
|
lesson.id,
|
|
courseId,
|
|
retake,
|
|
pageData.page,
|
|
data,
|
|
newPageId,
|
|
result.answerid,
|
|
result.correctanswer,
|
|
result.userresponse,
|
|
siteId,
|
|
);
|
|
|
|
nAttempts++;
|
|
}
|
|
|
|
// Check if "number of attempts remaining" message is needed.
|
|
if (!result.correctanswer && !result.newpageid) {
|
|
// Retreive the number of attempts left counter.
|
|
if (lesson.maxattempts && lesson.maxattempts > 0 && nAttempts >= lesson.maxattempts) {
|
|
if (lesson.maxattempts > 1) { // Don't bother with message if only one attempt.
|
|
result.maxattemptsreached = true;
|
|
}
|
|
result.newpageid = AddonModLessonProvider.LESSON_NEXTPAGE;
|
|
} else if (lesson.maxattempts && lesson.maxattempts > 1) { // Don't show message if only one attempt or unlimited.
|
|
result.attemptsremaining = lesson.maxattempts - nAttempts;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine default feedback if necessary.
|
|
if (!result.response) {
|
|
if (!lesson.feedback && !result.noanswer && !(lesson.review && !result.correctanswer && !result.isessayquestion)) {
|
|
// These conditions have been met:
|
|
// 1. The lesson manager has not supplied feedback to the student.
|
|
// 2. Not displaying default feedback.
|
|
// 3. The user did provide an answer.
|
|
// 4. We are not reviewing with an incorrect answer (and not reviewing an essay question).
|
|
result.nodefaultresponse = true;
|
|
} else if (result.isessayquestion) {
|
|
result.response = Translate.instant('addon.mod_lesson.defaultessayresponse');
|
|
} else if (result.correctanswer) {
|
|
result.response = Translate.instant('addon.mod_lesson.thatsthecorrectanswer');
|
|
} else {
|
|
result.response = Translate.instant('addon.mod_lesson.thatsthewronganswer');
|
|
}
|
|
}
|
|
|
|
if (!result.response) {
|
|
return result;
|
|
}
|
|
|
|
if (lesson.review && !result.correctanswer && !result.isessayquestion) {
|
|
// Calculate the number of question attempt in the page if it isn't calculated already.
|
|
if (typeof nAttempts == 'undefined') {
|
|
const result = await this.getQuestionsAttempts(lesson.id, retake, {
|
|
cmId: lesson.coursemodule,
|
|
pageId: pageData.page.id,
|
|
siteId,
|
|
});
|
|
|
|
nAttempts = result.online.length + result.offline.length;
|
|
}
|
|
|
|
const messageId = nAttempts == 1 ? 'firstwrong' : 'secondpluswrong';
|
|
|
|
result.feedback = '<div class="box feedback">' + Translate.instant('addon.mod_lesson.' + messageId) + '</div>';
|
|
} else {
|
|
result.feedback = '';
|
|
}
|
|
|
|
let className = 'response';
|
|
if (result.correctanswer) {
|
|
className += ' correct';
|
|
} else if (!result.isessayquestion) {
|
|
className += ' incorrect';
|
|
}
|
|
|
|
result.feedback += '<div class="box generalbox boxaligncenter p-y-1">' + pageData.page.contents + '</div>';
|
|
result.feedback += '<div class="correctanswer generalbox"><em>' +
|
|
Translate.instant('addon.mod_lesson.youranswer') + '</em> : ' +
|
|
'<div class="studentanswer m-t-2 m-b-2"><table class="generaltable"><tbody>';
|
|
|
|
// Create a table containing the answers and responses.
|
|
if (pageData.page.qoption) {
|
|
// Multianswer allowed.
|
|
const studentAnswerArray = result.studentanswer ?
|
|
result.studentanswer.split(AddonModLessonProvider.MULTIANSWER_DELIMITER) : [];
|
|
const responseArray = result.response ? result.response.split(AddonModLessonProvider.MULTIANSWER_DELIMITER) : [];
|
|
|
|
// Add answers and responses to the table.
|
|
for (let i = 0; i < studentAnswerArray.length; i++) {
|
|
result.feedback = this.addAnswerAndResponseToFeedback(
|
|
result.feedback,
|
|
studentAnswerArray[i],
|
|
result.studentanswerformat || 1,
|
|
responseArray[i],
|
|
className,
|
|
);
|
|
}
|
|
} else {
|
|
// Only 1 answer, add it to the table.
|
|
result.feedback = this.addAnswerAndResponseToFeedback(
|
|
result.feedback,
|
|
result.studentanswer,
|
|
result.studentanswerformat || 1,
|
|
result.response,
|
|
className,
|
|
);
|
|
}
|
|
|
|
result.feedback += '</tbody></table></div></div>';
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Remove a password stored in DB.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when removed.
|
|
*/
|
|
async removeStoredPassword(lessonId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.getDb().deleteRecords(PASSWORD_TABLE_NAME, { lessonid: lessonId });
|
|
}
|
|
|
|
/**
|
|
* Store a password in DB.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param password Password to store.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when stored.
|
|
*/
|
|
async storePassword(lessonId: number, password: string, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
const entry: AddonModLessonPasswordDBRecord = {
|
|
lessonid: lessonId,
|
|
password: password,
|
|
timemodified: Date.now(),
|
|
};
|
|
|
|
await site.getDb().insertRecord(PASSWORD_TABLE_NAME, entry);
|
|
}
|
|
|
|
/**
|
|
* Function to determine if a page is a valid page. It will add the page to validPages if valid. It can also
|
|
* modify the list of viewedPagesIds for cluster pages.
|
|
* Based on Moodle's valid_page_and_view.
|
|
*
|
|
* @param pages Index of lesson pages, indexed by page ID. See createPagesIndex.
|
|
* @param page Page to check.
|
|
* @param validPages Valid pages, indexed by page ID.
|
|
* @param viewedPagesIds List of viewed pages IDs.
|
|
* @return Next page ID.
|
|
*/
|
|
validPageAndView(
|
|
pages: Record<number, AddonModLessonPageWSData>,
|
|
page: AddonModLessonPageWSData,
|
|
validPages: Record<number, number>,
|
|
viewedPagesIds: number[],
|
|
): number {
|
|
|
|
if (page.qtype != AddonModLessonProvider.LESSON_PAGE_ENDOFCLUSTER &&
|
|
page.qtype != AddonModLessonProvider.LESSON_PAGE_ENDOFBRANCH) {
|
|
// Add this page as a valid page.
|
|
validPages[page.id] = 1;
|
|
}
|
|
|
|
if (page.qtype == AddonModLessonProvider.LESSON_PAGE_CLUSTER) {
|
|
// Get list of pages in the cluster.
|
|
const subPages = this.getSubpagesOf(pages, page.id, [AddonModLessonProvider.LESSON_PAGE_ENDOFCLUSTER]);
|
|
|
|
subPages.forEach((subPage) => {
|
|
const position = viewedPagesIds.indexOf(subPage.id);
|
|
if (position == -1) {
|
|
return;
|
|
}
|
|
|
|
delete viewedPagesIds[position]; // Remove it.
|
|
|
|
// Since the user did see one page in the cluster, add the cluster pageid to the viewedPagesIds.
|
|
if (viewedPagesIds.indexOf(page.id) == -1) {
|
|
viewedPagesIds.push(page.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
return page.nextpageid;
|
|
}
|
|
|
|
}
|
|
|
|
export const AddonModLesson = makeSingleton(AddonModLessonProvider);
|
|
|
|
/**
|
|
* Result of check answer.
|
|
*/
|
|
export type AddonModLessonCheckAnswerResult = {
|
|
answerid: number;
|
|
noanswer: boolean;
|
|
correctanswer: boolean;
|
|
isessayquestion: boolean;
|
|
response: string;
|
|
newpageid: number;
|
|
studentanswer: string;
|
|
userresponse: unknown;
|
|
feedback?: string;
|
|
nodefaultresponse?: boolean;
|
|
inmediatejump?: boolean;
|
|
studentanswerformat?: number;
|
|
useranswer?: unknown;
|
|
};
|
|
|
|
/**
|
|
* Result of record attempt.
|
|
*/
|
|
export type AddonModLessonRecordAttemptResult = AddonModLessonCheckAnswerResult & {
|
|
attemptsremaining?: number;
|
|
maxattemptsreached?: boolean;
|
|
};
|
|
|
|
/**
|
|
* Result of lesson grade.
|
|
*/
|
|
export type AddonModLessonGrade = {
|
|
/**
|
|
* Number of questions answered.
|
|
*/
|
|
nquestions: number;
|
|
|
|
/**
|
|
* Number of question attempts.
|
|
*/
|
|
attempts: number;
|
|
|
|
/**
|
|
* Max points possible.
|
|
*/
|
|
total: number;
|
|
|
|
/**
|
|
* Points earned by the student.
|
|
*/
|
|
earned: number;
|
|
|
|
/**
|
|
* Calculated percentage grade.
|
|
*/
|
|
grade: number;
|
|
|
|
/**
|
|
* Numer of manually graded questions.
|
|
*/
|
|
nmanual: number;
|
|
|
|
/**
|
|
* Point value for manually graded questions.
|
|
*/
|
|
manualpoints: number;
|
|
};
|
|
|
|
/**
|
|
* Common options including a group ID.
|
|
*/
|
|
export type AddonModLessonGroupOptions = CoreCourseCommonModWSOptions & {
|
|
groupId?: number; // The group to get. If not defined, all participants.
|
|
};
|
|
|
|
/**
|
|
* Common options including a group ID.
|
|
*/
|
|
export type AddonModLessonUserOptions = CoreCourseCommonModWSOptions & {
|
|
userId?: number; // User ID. If not defined, site's current user.
|
|
};
|
|
|
|
/**
|
|
* Common options including a password.
|
|
*/
|
|
export type AddonModLessonPasswordOptions = CoreCourseCommonModWSOptions & {
|
|
password?: string; // Lesson password (if any).
|
|
};
|
|
|
|
/**
|
|
* Common options including password and review.
|
|
*/
|
|
export type AddonModLessonPwdReviewOptions = AddonModLessonPasswordOptions & {
|
|
review?: boolean; // If the user wants to review just after finishing (1 hour margin).
|
|
};
|
|
|
|
/**
|
|
* Options to pass to get lesson with password.
|
|
*/
|
|
export type AddonModLessonGetWithPasswordOptions = AddonModLessonPasswordOptions & {
|
|
validatePassword?: boolean; // Defauls to true. If true, the function will fail if the password is wrong.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to calculateProgress.
|
|
*/
|
|
export type AddonModLessonCalculateProgressBasicOptions = {
|
|
password?: string; // Lesson password (if any).
|
|
review?: boolean; // If the user wants to review just after finishing (1 hour margin).
|
|
pageIndex?: Record<number, AddonModLessonPageWSData>; // Page index. If not provided, it will be calculated.
|
|
siteId?: string; // Site ID. If not defined, current site.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to calculateProgress.
|
|
*/
|
|
export type AddonModLessonCalculateProgressOptions = AddonModLessonCalculateProgressBasicOptions & {
|
|
cmId?: number; // Module ID.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to lessonGrade.
|
|
*/
|
|
export type AddonModLessonGradeOptions = AddonModLessonCalculateProgressBasicOptions & {
|
|
userId?: number; // User ID. If not defined, site's user.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to calculateOfflineData.
|
|
*/
|
|
export type AddonModLessonCalculateOfflineDataOptions = AddonModLessonCalculateProgressBasicOptions & {
|
|
accessInfo?: AddonModLessonGetAccessInformationWSResponse; // Access info.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to get page data.
|
|
*/
|
|
export type AddonModLessonGetPageDataOptions = AddonModLessonPwdReviewOptions & {
|
|
includeContents?: boolean; // Include the page rendered contents.
|
|
includeOfflineData?: boolean; // Whether to include calculated offline data. Only when ignoring cache.
|
|
accessInfo?: AddonModLessonGetAccessInformationWSResponse; // Access info. Required if includeOfflineData is true.
|
|
jumps?: AddonModLessonPossibleJumps; // Possible jumps. Required if includeOfflineData is true.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to get page data.
|
|
*/
|
|
export type AddonModLessonGetPageViewMessagesOptions = {
|
|
password?: string; // Lesson password (if any).
|
|
review?: boolean; // If the user wants to review just after finishing (1 hour margin).
|
|
siteId?: string; // Site ID. If not defined, current site.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to get questions attempts.
|
|
*/
|
|
export type AddonModLessonGetQuestionsAttemptsOptions = CoreCourseCommonModWSOptions & {
|
|
correct?: boolean; // True to only fetch correct attempts, false to get them all.
|
|
pageId?: number; // If defined, only get attempts on this page.
|
|
userId?: number; // User ID. If not defined, site's user.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to getPagesIdsWithQuestionAttempts.
|
|
*/
|
|
export type AddonModLessonGetPagesIdsWithAttemptsOptions = CoreCourseCommonModWSOptions & {
|
|
correct?: boolean; // True to only fetch correct attempts, false to get them all.
|
|
userId?: number; // User ID. If not defined, site's user.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to processPageOnline.
|
|
*/
|
|
export type AddonModLessonProcessPageOnlineOptions = {
|
|
password?: string; // Lesson password (if any).
|
|
review?: boolean; // If the user wants to review just after finishing (1 hour margin).
|
|
siteId?: string; // Site ID. If not defined, current site.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to processPage.
|
|
*/
|
|
export type AddonModLessonProcessPageOptions = AddonModLessonProcessPageOnlineOptions & {
|
|
offline?: boolean; // Whether it's offline mode.
|
|
accessInfo?: AddonModLessonGetAccessInformationWSResponse; // Access info. Required if offline is true.
|
|
jumps?: AddonModLessonPossibleJumps; // Possible jumps. Required if offline is true.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to finishRetakeOnline.
|
|
*/
|
|
export type AddonModLessonFinishRetakeOnlineOptions = {
|
|
password?: string; // Lesson password (if any).
|
|
outOfTime?: boolean; // Whether the user ran out of time.
|
|
review?: boolean; // If the user wants to review just after finishing (1 hour margin).
|
|
siteId?: string; // Site ID. If not defined, current site.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to finishRetake.
|
|
*/
|
|
export type AddonModLessonFinishRetakeOptions = AddonModLessonFinishRetakeOnlineOptions & {
|
|
offline?: boolean; // Whether it's offline mode.
|
|
accessInfo?: AddonModLessonGetAccessInformationWSResponse; // Access info. Required if offline is true.
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_get_lesson_access_information WS.
|
|
*/
|
|
export type AddonModLessonGetAccessInformationWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_lesson_access_information WS.
|
|
*/
|
|
export type AddonModLessonGetAccessInformationWSResponse = {
|
|
canmanage: boolean; // Whether the user can manage the lesson or not.
|
|
cangrade: boolean; // Whether the user can grade the lesson or not.
|
|
canviewreports: boolean; // Whether the user can view the lesson reports or not.
|
|
reviewmode: boolean; // Whether the lesson is in review mode for the current user.
|
|
attemptscount: number; // The number of attempts done by the user.
|
|
lastpageseen: number; // The last page seen id.
|
|
leftduringtimedsession: boolean; // Whether the user left during a timed session.
|
|
firstpageid: number; // The lesson first page id.
|
|
preventaccessreasons: AddonModLessonPreventAccessReason[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Prevent access reason returned by mod_lesson_get_lesson_access_information.
|
|
*/
|
|
export type AddonModLessonPreventAccessReason = {
|
|
reason: string; // Reason lang string code.
|
|
data: string; // Additional data.
|
|
message: string; // Complete html message.
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_get_content_pages_viewed WS.
|
|
*/
|
|
export type AddonModLessonGetContentPagesViewedWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
lessonattempt: number; // Lesson attempt number.
|
|
userid?: number; // The user id (empty for current user).
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_content_pages_viewed WS.
|
|
*/
|
|
export type AddonModLessonGetContentPagesViewedWSResponse = {
|
|
pages: AddonModLessonWSContentPageViewed[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Page data returned in mod_lesson_get_content_pages_viewed WS.
|
|
*/
|
|
export type AddonModLessonWSContentPageViewed = {
|
|
id: number; // The attempt id.
|
|
lessonid: number; // The lesson id.
|
|
pageid: number; // The page id.
|
|
userid: number; // The user who viewed the page.
|
|
retry: number; // The lesson attempt number.
|
|
flag: number; // 1 if the next page was calculated randomly.
|
|
timeseen: number; // The time the page was seen.
|
|
nextpageid: number; // The next page chosen id.
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_get_lessons_by_courses WS.
|
|
*/
|
|
export type AddonModLessonGetLessonsByCoursesWSParams = {
|
|
courseids?: number[]; // Array of course ids.
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_lessons_by_courses WS.
|
|
*/
|
|
export type AddonModLessonGetLessonsByCoursesWSResponse = {
|
|
lessons: AddonModLessonLessonWSData[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Lesson data returned by WS.
|
|
*/
|
|
export type AddonModLessonLessonWSData = {
|
|
id: number; // Standard Moodle primary key.
|
|
course: number; // Foreign key reference to the course this lesson is part of.
|
|
coursemodule: number; // Course module id.
|
|
name: string; // Lesson name.
|
|
intro?: string; // Lesson introduction text.
|
|
introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
|
practice?: boolean; // Practice lesson?.
|
|
modattempts?: boolean; // Allow student review?.
|
|
usepassword?: boolean; // Password protected lesson?.
|
|
password?: string; // Password.
|
|
dependency?: number; // Dependent on (another lesson id).
|
|
conditions?: string; // Conditions to enable the lesson.
|
|
grade?: number; // The total that the grade is scaled to be out of.
|
|
custom?: boolean; // Custom scoring?.
|
|
ongoing?: boolean; // Display ongoing score?.
|
|
usemaxgrade?: number; // How to calculate the final grade.
|
|
maxanswers?: number; // Maximum answers per page.
|
|
maxattempts?: number; // Maximum attempts.
|
|
review?: boolean; // Provide option to try a question again.
|
|
nextpagedefault?: number; // Action for a correct answer.
|
|
feedback?: boolean; // Display default feedback.
|
|
minquestions?: number; // Minimum number of questions.
|
|
maxpages?: number; // Number of pages to show.
|
|
timelimit?: number; // Time limit.
|
|
retake?: boolean; // Re-takes allowed.
|
|
activitylink?: number; // Id of the next activity to be linked once the lesson is completed.
|
|
mediafile?: string; // Local file path or full external URL.
|
|
mediaheight?: number; // Popup for media file height.
|
|
mediawidth?: number; // Popup for media with.
|
|
mediaclose?: number; // Display a close button in the popup?.
|
|
slideshow?: boolean; // Display lesson as slideshow.
|
|
width?: number; // Slideshow width.
|
|
height?: number; // Slideshow height.
|
|
bgcolor?: string; // Slideshow bgcolor.
|
|
displayleft?: boolean; // Display left pages menu?.
|
|
displayleftif?: number; // Minimum grade to display menu.
|
|
progressbar?: boolean; // Display progress bar?.
|
|
available?: number; // Available from.
|
|
deadline?: number; // Available until.
|
|
timemodified?: number; // Last time settings were updated.
|
|
completionendreached?: number; // Require end reached for completion?.
|
|
completiontimespent?: number; // Student must do this activity at least for.
|
|
allowofflineattempts: boolean; // Whether to allow the lesson to be attempted offline in the mobile app.
|
|
introfiles?: { // Introfiles.
|
|
filename?: string; // File name.
|
|
filepath?: string; // File path.
|
|
filesize?: number; // File size.
|
|
fileurl: string; // Downloadable file url.
|
|
timemodified?: number; // Time modified.
|
|
mimetype?: string; // File mime type.
|
|
isexternalfile?: number; // Whether is an external file.
|
|
repositorytype?: string; // The repository type for the external files.
|
|
}[];
|
|
mediafiles?: { // Mediafiles.
|
|
filename?: string; // File name.
|
|
filepath?: string; // File path.
|
|
filesize?: number; // File size.
|
|
fileurl: string; // Downloadable file url.
|
|
timemodified?: number; // Time modified.
|
|
mimetype?: string; // File mime type.
|
|
isexternalfile?: number; // Whether is an external file.
|
|
repositorytype?: string; // The repository type for the external files.
|
|
}[];
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_get_lesson WS.
|
|
*/
|
|
export type AddonModLessonGetLessonWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
password?: string; // Lesson password.
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_lesson WS.
|
|
*/
|
|
export type AddonModLessonGetLessonWSResponse = {
|
|
lesson: AddonModLessonLessonWSData;
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_get_page_data WS.
|
|
*/
|
|
export type AddonModLessonGetPageDataWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
pageid: number; // The page id.
|
|
password?: string; // Optional password (the lesson may be protected).
|
|
review?: boolean; // If we want to review just after finishing (1 hour margin).
|
|
returncontents?: boolean; // If we must return the complete page contents once rendered.
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_page_data WS.
|
|
*/
|
|
export type AddonModLessonGetPageDataWSResponse = {
|
|
page?: AddonModLessonPageWSData; // Page fields.
|
|
newpageid: number; // New page id (if a jump was made).
|
|
pagecontent?: string; // Page html content.
|
|
ongoingscore: string; // The ongoing score message.
|
|
progress: number; // Progress percentage in the lesson.
|
|
contentfiles: CoreWSExternalFile[];
|
|
answers: AddonModLessonPageAnswerWSData[];
|
|
messages: AddonModLessonMessageWSData[];
|
|
displaymenu: boolean; // Whether we should display the menu or not in this page.
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Page data returned by several WS.
|
|
*/
|
|
export type AddonModLessonPageWSData = {
|
|
id: number; // The id of this lesson page.
|
|
lessonid: number; // The id of the lesson this page belongs to.
|
|
prevpageid: number; // The id of the page before this one.
|
|
nextpageid: number; // The id of the next page in the page sequence.
|
|
qtype: number; // Identifies the page type of this page.
|
|
qoption: number; // Used to record page type specific options.
|
|
layout: number; // Used to record page specific layout selections.
|
|
display: number; // Used to record page specific display selections.
|
|
timecreated: number; // Timestamp for when the page was created.
|
|
timemodified: number; // Timestamp for when the page was last modified.
|
|
title?: string; // The title of this page.
|
|
contents?: string; // The contents of this page.
|
|
contentsformat?: number; // Contents format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
|
displayinmenublock: boolean; // Toggles display in the left menu block.
|
|
type: number; // The type of the page [question | structure].
|
|
typeid: number; // The unique identifier for the page type.
|
|
typestring: string; // The string that describes this page type.
|
|
};
|
|
|
|
/**
|
|
* Page answer data returned by mod_lesson_get_page_data.
|
|
*/
|
|
export type AddonModLessonPageAnswerWSData = {
|
|
id: number; // The ID of this answer in the database.
|
|
answerfiles: CoreWSExternalFile[];
|
|
responsefiles: CoreWSExternalFile[];
|
|
jumpto?: number; // Identifies where the user goes upon completing a page with this answer.
|
|
grade?: number; // The grade this answer is worth.
|
|
score?: number; // The score this answer will give.
|
|
flags?: number; // Used to store options for the answer.
|
|
timecreated?: number; // A timestamp of when the answer was created.
|
|
timemodified?: number; // A timestamp of when the answer was modified.
|
|
answer?: string; // Possible answer text.
|
|
answerformat?: number; // Answer format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
|
response?: string; // Response text for the answer.
|
|
responseformat?: number; // Response format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
|
};
|
|
|
|
/**
|
|
* Page answer data with some calculated data.
|
|
*/
|
|
export type AddonModLessonPageAnswerData = AddonModLessonPageAnswerWSData & {
|
|
pageid: number;
|
|
};
|
|
|
|
/**
|
|
* Message data returned by several WS.
|
|
*/
|
|
export type AddonModLessonMessageWSData = {
|
|
message: string; // Message.
|
|
type: string; // Message type: usually a CSS identifier like: success, info, warning, error, ...
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_get_pages WS.
|
|
*/
|
|
export type AddonModLessonGetPagesWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
password?: string; // Optional password (the lesson may be protected).
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_pages WS.
|
|
*/
|
|
export type AddonModLessonGetPagesWSResponse = {
|
|
pages: AddonModLessonGetPagesPageWSData[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Data for each page returned by mod_lesson_get_pages WS.
|
|
*/
|
|
export type AddonModLessonGetPagesPageWSData = {
|
|
page: AddonModLessonPageWSData; // Page fields.
|
|
answerids: number[]; // List of answers ids (empty for content pages in Moodle 1.9).
|
|
jumps: number[]; // List of possible page jumps.
|
|
filescount: number; // The total number of files attached to the page.
|
|
filessizetotal: number; // The total size of the files.
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_get_pages_possible_jumps WS.
|
|
*/
|
|
export type AddonModLessonGetPagesPossibleJumpsWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_pages_possible_jumps WS.
|
|
*/
|
|
export type AddonModLessonGetPagesPossibleJumpsWSResponse = {
|
|
jumps: AddonModLessonPossibleJumpWSData[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Data for each jump returned by mod_lesson_get_pages_possible_jumps WS.
|
|
*/
|
|
export type AddonModLessonPossibleJumpWSData = {
|
|
pageid: number; // The page id.
|
|
answerid: number; // The answer id.
|
|
jumpto: number; // The jump (page id or type of jump).
|
|
calculatedjump: number; // The real page id (or EOL) to jump.
|
|
};
|
|
|
|
/**
|
|
* Lesson possible jumps, indexed by page and jumpto.
|
|
*/
|
|
export type AddonModLessonPossibleJumps = Record<number, Record<number, AddonModLessonPossibleJumpWSData>>;
|
|
|
|
/**
|
|
* Params of mod_lesson_get_questions_attempts WS.
|
|
*/
|
|
export type AddonModLessonGetQuestionsAttemptsWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
attempt: number; // Lesson attempt number.
|
|
correct?: boolean; // Only fetch correct attempts.
|
|
pageid?: number; // Only fetch attempts at the given page.
|
|
userid?: number; // Only fetch attempts of the given user.
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_questions_attempts WS.
|
|
*/
|
|
export type AddonModLessonGetQuestionsAttemptsWSResponse = {
|
|
attempts: AddonModLessonQuestionAttemptWSData[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Data for each attempt returned by mod_lesson_get_questions_attempts WS.
|
|
*/
|
|
export type AddonModLessonQuestionAttemptWSData = {
|
|
id: number; // The attempt id.
|
|
lessonid: number; // The attempt lessonid.
|
|
pageid: number; // The attempt pageid.
|
|
userid: number; // The user who did the attempt.
|
|
answerid: number; // The attempt answerid.
|
|
retry: number; // The lesson attempt number.
|
|
correct: number; // If it was the correct answer.
|
|
useranswer: string; // The complete user answer.
|
|
timeseen: number; // The time the question was seen.
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_get_attempts_overview WS.
|
|
*/
|
|
export type AddonModLessonGetAttemptsOverviewWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
groupid?: number; // Group id, 0 means that the function will determine the user group.
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_attempts_overview WS.
|
|
*/
|
|
export type AddonModLessonGetAttemptsOverviewWSResponse = {
|
|
data?: AddonModLessonAttemptsOverviewWSData; // Attempts overview data (empty for no attemps).
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Overview data returned by mod_lesson_get_attempts_overview WS.
|
|
*/
|
|
export type AddonModLessonAttemptsOverviewWSData = {
|
|
lessonscored: boolean; // True if the lesson was scored.
|
|
numofattempts: number; // Number of attempts.
|
|
avescore: number | null; // Average score.
|
|
highscore: number | null; // High score.
|
|
lowscore: number | null; // Low score.
|
|
avetime: number | null; // Average time (spent in taking the lesson).
|
|
hightime: number | null; // High time.
|
|
lowtime: number | null; // Low time.
|
|
students?: AddonModLessonAttemptsOverviewsStudentWSData[]; // Students data, including attempts.
|
|
};
|
|
|
|
/**
|
|
* Student data returned by mod_lesson_get_attempts_overview WS.
|
|
*/
|
|
export type AddonModLessonAttemptsOverviewsStudentWSData = {
|
|
id: number; // User id.
|
|
fullname: string; // User full name.
|
|
bestgrade: number; // Best grade.
|
|
attempts: AddonModLessonAttemptsOverviewsAttemptWSData[];
|
|
};
|
|
|
|
/**
|
|
* Attempt data returned by mod_lesson_get_attempts_overview WS.
|
|
*/
|
|
export type AddonModLessonAttemptsOverviewsAttemptWSData = {
|
|
try: number; // Attempt number.
|
|
grade: number | null; // Attempt grade.
|
|
timestart: number; // Attempt time started.
|
|
timeend: number; // Attempt last time continued.
|
|
end: number; // Attempt time ended.
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_get_user_timers WS.
|
|
*/
|
|
export type ModLessonGetUserTimersWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
userid?: number; // The user id (empty for current user).
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_user_timers WS.
|
|
*/
|
|
export type ModLessonGetUserTimersWSResponse = {
|
|
timers: AddonModLessonUserTimerWSData[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Data for each timer returned by mod_lesson_get_user_timers WS.
|
|
*/
|
|
export type AddonModLessonUserTimerWSData = {
|
|
id: number; // The attempt id.
|
|
lessonid: number; // The lesson id.
|
|
userid: number; // The user id.
|
|
starttime: number; // First access time for a new timer session.
|
|
lessontime: number; // Last access time to the lesson during the timer session.
|
|
completed: number; // If the lesson for this timer was completed.
|
|
timemodifiedoffline: number; // Last modified time via webservices.
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_get_user_attempt WS.
|
|
*/
|
|
export type AddonModLessonGetUserAttemptWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
userid: number; // The user id. 0 for current user.
|
|
lessonattempt: number; // The attempt number.
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_get_user_attempt WS.
|
|
*/
|
|
export type AddonModLessonGetUserAttemptWSResponse = {
|
|
answerpages: AddonModLessonUserAttemptAnswerPageWSData[];
|
|
userstats: {
|
|
grade: number; // Attempt final grade.
|
|
completed: number; // Time completed.
|
|
timetotake: number; // Time taken.
|
|
gradeinfo?: AddonModLessonAttemptGradeWSData; // Attempt grade.
|
|
};
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Answer page data returned by mod_lesson_get_user_attempt.
|
|
*/
|
|
export type AddonModLessonUserAttemptAnswerPageWSData = {
|
|
page?: AddonModLessonPageWSData; // Page fields.
|
|
title: string; // Page title.
|
|
contents: string; // Page contents.
|
|
qtype: string; // Identifies the page type of this page.
|
|
grayout: number; // If is required to apply a grayout.
|
|
answerdata?: AddonModLessonUserAttemptAnswerData; // Answer data (empty in content pages created in Moodle 1.x).
|
|
};
|
|
|
|
/**
|
|
* Answer data of a user attempt answer page.
|
|
*/
|
|
export type AddonModLessonUserAttemptAnswerData = {
|
|
score: string; // The score (text version).
|
|
response: string; // The response text.
|
|
responseformat: number; // Response. format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
|
answers?: string[][]; // User answers.
|
|
};
|
|
|
|
/**
|
|
* Attempt grade returned by several WS.
|
|
*/
|
|
export type AddonModLessonAttemptGradeWSData = {
|
|
nquestions: number; // Number of questions answered.
|
|
attempts: number; // Number of question attempts.
|
|
total: number; // Max points possible.
|
|
earned: number; // Points earned by student.
|
|
grade: number; // Calculated percentage grade.
|
|
nmanual: number; // Number of manually graded questions.
|
|
manualpoints: number; // Point value for manually graded questions.
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_finish_attempt WS.
|
|
*/
|
|
export type AddonModLessonFinishAttemptWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
password?: string; // Optional password (the lesson may be protected).
|
|
outoftime?: boolean; // If the user run out of time.
|
|
review?: boolean; // If we want to review just after finishing (1 hour margin).
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_finish_attempt WS.
|
|
*/
|
|
export type AddonModLessonFinishAttemptWSResponse = {
|
|
data: AddonModLessonEOLPageWSDataEntry[]; // The EOL page information data.
|
|
messages: AddonModLessonMessageWSData[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* EOL page data entry returned by mod_lesson_finish_attempt WS.
|
|
*/
|
|
export type AddonModLessonEOLPageWSDataEntry = {
|
|
name: string; // Data name.
|
|
value: string; // Data value.
|
|
message: string; // Data message (translated string).
|
|
};
|
|
|
|
/**
|
|
* Finish retake response.
|
|
*/
|
|
export type AddonModLessonFinishRetakeResponse = Omit<AddonModLessonFinishAttemptWSResponse, 'data'> & {
|
|
data: Record<string, AddonModLessonEOLPageDataEntry>;
|
|
};
|
|
|
|
/**
|
|
* Parsed EOL page data.
|
|
*/
|
|
export type AddonModLessonEOLPageDataEntry = {
|
|
name: string; // Data name.
|
|
value: unknown; // Data value.
|
|
message: string; // Data message (translated string).
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_view_lesson WS.
|
|
*/
|
|
export type AddonModLessonViewLessonWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
password?: string; // Lesson password.
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_process_page WS.
|
|
*/
|
|
export type AddonModLessonProcessPageWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
pageid: number; // The page id.
|
|
data: ProcessPageData[]; // The data to be saved.
|
|
password?: string; // Optional password (the lesson may be protected).
|
|
review?: boolean; // If we want to review just after finishing (1 hour margin).
|
|
};
|
|
|
|
type ProcessPageData = {
|
|
name: string; // Data name.
|
|
value: string; // Data value.
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_process_page WS.
|
|
*/
|
|
export type AddonModLessonProcessPageWSResponse = {
|
|
newpageid: number; // New page id (if a jump was made).
|
|
inmediatejump: boolean; // Whether the page processing redirect directly to anoter page.
|
|
nodefaultresponse: boolean; // Whether there is not a default response.
|
|
feedback: string; // The response feedback.
|
|
attemptsremaining: number | null; // Number of attempts remaining.
|
|
correctanswer: boolean; // Whether the answer is correct.
|
|
noanswer: boolean; // Whether there aren't answers.
|
|
isessayquestion: boolean; // Whether is a essay question.
|
|
maxattemptsreached: boolean; // Whether we reachered the max number of attempts.
|
|
response: string; // The response.
|
|
studentanswer: string; // The student answer.
|
|
userresponse: string; // The user response.
|
|
reviewmode: boolean; // Whether the user is reviewing.
|
|
ongoingscore: string; // The ongoing message.
|
|
progress: number | null; // Progress percentage in the lesson.
|
|
displaymenu: boolean; // Whether we should display the menu or not in this page.
|
|
messages: AddonModLessonMessageWSData[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Result of process page.
|
|
*/
|
|
export type AddonModLessonProcessPageResponse = AddonModLessonProcessPageWSResponse & {
|
|
sent?: boolean; // Whether the data was sent to server.
|
|
};
|
|
|
|
/**
|
|
* Params of mod_lesson_launch_attempt WS.
|
|
*/
|
|
export type AddonModLessonLaunchAttemptWSParams = {
|
|
lessonid: number; // Lesson instance id.
|
|
password?: string; // Optional password (the lesson may be protected).
|
|
pageid?: number; // Page id to continue from (only when continuing an attempt).
|
|
review?: boolean; // If we want to review just after finishing.
|
|
};
|
|
|
|
/**
|
|
* Data returned by mod_lesson_launch_attempt WS.
|
|
*/
|
|
export type AddonModLessonLaunchAttemptWSResponse = {
|
|
messages: AddonModLessonMessageWSData[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Attempt data, either online or offline attempt.
|
|
*/
|
|
export type AddonModLessonAnyAttemptData = AddonModLessonQuestionAttemptWSData | AddonModLessonPageAttemptRecord;
|
|
|
|
/**
|
|
* Either content page data or page attempt offline record.
|
|
*/
|
|
export type AddonModLessonContentPageOrRecord = AddonModLessonWSContentPageViewed | AddonModLessonPageAttemptRecord;
|
|
|
|
/**
|
|
* Data passed to DATA_SENT_EVENT event.
|
|
*/
|
|
export type AddonModLessonDataSentData = CoreEventSiteData & {
|
|
lessonId: number;
|
|
type: string;
|
|
courseId?: number;
|
|
outOfTime?: boolean;
|
|
review?: boolean;
|
|
pageId?: number;
|
|
};
|