3288 lines
129 KiB
TypeScript
3288 lines
129 KiB
TypeScript
// (C) Copyright 2015 Martin Dougiamas
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { Injectable } from '@angular/core';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
import { CoreLoggerProvider } from '@providers/logger';
|
|
import { CoreSitesProvider } from '@providers/sites';
|
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
|
import { CoreGradesProvider } from '@core/grades/providers/grades';
|
|
import { CoreSiteWSPreSets } from '@classes/site';
|
|
import { AddonModLessonOfflineProvider } from './lesson-offline';
|
|
|
|
/**
|
|
* Result of check answer.
|
|
*/
|
|
export interface AddonModLessonCheckAnswerResult {
|
|
answerid?: number;
|
|
noanswer?: boolean;
|
|
correctanswer?: boolean;
|
|
isessayquestion?: boolean;
|
|
response?: string;
|
|
newpageid?: number;
|
|
studentanswer?: any;
|
|
userresponse?: any;
|
|
feedback?: string;
|
|
nodefaultresponse?: boolean;
|
|
inmediatejump?: boolean;
|
|
studentanswerformat?: number;
|
|
useranswer?: any;
|
|
}
|
|
|
|
/**
|
|
* Result of record attempt.
|
|
*/
|
|
export interface AddonModLessonRecordAttemptResult extends AddonModLessonCheckAnswerResult {
|
|
attemptsremaining?: number;
|
|
maxattemptsreached?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Result of lesson grade.
|
|
*/
|
|
export interface AddonModLessonGrade {
|
|
/**
|
|
* Number of questions answered.
|
|
* @type {number}
|
|
*/
|
|
nquestions: number;
|
|
|
|
/**
|
|
* Number of question attempts.
|
|
* @type {number}
|
|
*/
|
|
attempts: number;
|
|
|
|
/**
|
|
* Max points possible.
|
|
* @type {number}
|
|
*/
|
|
total: number;
|
|
|
|
/**
|
|
* Points earned by the student.
|
|
* @type {number}
|
|
*/
|
|
earned: number;
|
|
|
|
/**
|
|
* Calculated percentage grade.
|
|
* @type {number}
|
|
*/
|
|
grade: number;
|
|
|
|
/**
|
|
* Numer of manually graded questions.
|
|
* @type {number}
|
|
*/
|
|
nmanual: number;
|
|
|
|
/**
|
|
* Point value for manually graded questions.
|
|
* @type {number}
|
|
*/
|
|
manualpoints: number;
|
|
}
|
|
|
|
/**
|
|
* 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()
|
|
export class AddonModLessonProvider {
|
|
static COMPONENT = 'mmaModLesson';
|
|
|
|
// This page.
|
|
static LESSON_THISPAGE = 0;
|
|
// Next page -> any page not seen before.
|
|
static LESSON_UNSEENPAGE = 1;
|
|
// Next page -> any page not answered correctly.
|
|
static LESSON_UNANSWEREDPAGE = 2;
|
|
// Jump to Next Page.
|
|
static LESSON_NEXTPAGE = -1;
|
|
// End of Lesson.
|
|
static LESSON_EOL = -9;
|
|
// Jump to an unseen page within a branch and end of branch or end of lesson.
|
|
static LESSON_UNSEENBRANCHPAGE = -50;
|
|
// Jump to a random page within a branch and end of branch or end of lesson.
|
|
static LESSON_RANDOMPAGE = -60;
|
|
// Jump to a random Branch.
|
|
static LESSON_RANDOMBRANCH = -70;
|
|
// Cluster Jump.
|
|
static LESSON_CLUSTERJUMP = -80;
|
|
|
|
// Type of page: question or structure (content).
|
|
static TYPE_QUESTION = 0;
|
|
static TYPE_STRUCTURE = 1;
|
|
|
|
// Type of question pages.
|
|
static LESSON_PAGE_SHORTANSWER = 1;
|
|
static LESSON_PAGE_TRUEFALSE = 2;
|
|
static LESSON_PAGE_MULTICHOICE = 3;
|
|
static LESSON_PAGE_MATCHING = 5;
|
|
static LESSON_PAGE_NUMERICAL = 8;
|
|
static LESSON_PAGE_ESSAY = 10;
|
|
static LESSON_PAGE_BRANCHTABLE = 20; // Content page.
|
|
static LESSON_PAGE_ENDOFBRANCH = 21;
|
|
static LESSON_PAGE_CLUSTER = 30;
|
|
static LESSON_PAGE_ENDOFCLUSTER = 31;
|
|
|
|
/**
|
|
* Constant used as a delimiter when parsing multianswer questions
|
|
*/
|
|
static MULTIANSWER_DELIMITER = '@^#|';
|
|
|
|
// Variables for database.
|
|
static PASSWORD_TABLE = 'addon_mod_lesson_password';
|
|
protected tablesSchema = {
|
|
name: AddonModLessonProvider.PASSWORD_TABLE,
|
|
columns: [
|
|
{
|
|
name: 'lessonid',
|
|
type: 'INTEGER',
|
|
primaryKey: true
|
|
},
|
|
{
|
|
name: 'password',
|
|
type: 'TEXT'
|
|
},
|
|
{
|
|
name: 'timemodified',
|
|
type: 'INTEGER'
|
|
}
|
|
]
|
|
};
|
|
|
|
protected ROOT_CACHE_KEY = 'mmaModLesson:';
|
|
protected logger;
|
|
|
|
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
|
|
private translate: TranslateService, private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider,
|
|
private lessonOfflineProvider: AddonModLessonOfflineProvider) {
|
|
this.logger = logger.getInstance('AddonModLessonProvider');
|
|
|
|
this.sitesProvider.createTableFromSchema(this.tablesSchema);
|
|
}
|
|
|
|
/**
|
|
* Add an answer and its response to a feedback string (HTML).
|
|
*
|
|
* @param {string} feedback The current feedback.
|
|
* @param {string} answer Student answer.
|
|
* @param {number} answerFormat Answer format.
|
|
* @param {string} response Response.
|
|
* @param {string} className Class to add to the response.
|
|
* @return {string} 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 : this.textUtils.cleanTags(answer)) +
|
|
'</td></tr>';
|
|
|
|
// If the response exists, add a table row containing the response. If not, add en empty row.
|
|
if (response && response.trim()) {
|
|
feedback += '<tr><td class="cell c0 lastcol ' + className + '"><em>' +
|
|
this.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 {any[]} messages List of messages where to add the message.
|
|
* @param {string} stringName The ID of the message to be translated. E.g. 'addon.mod_lesson.numberofpagesviewednotice'.
|
|
* @param {any} [stringParams] The params of the message (if any).
|
|
*/
|
|
protected addMessage(messages: any[], stringName: string, stringParams?: any): void {
|
|
messages.push({
|
|
message: this.translate.instant(stringName, stringParams)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a property to the result of the "process EOL page" simulation in offline.
|
|
*
|
|
* @param {any} result Result where to add the value.
|
|
* @param {string} name Name of the property.
|
|
* @param {any} value Value to add.
|
|
* @param {boolean} addMessage Whether to add a message related to the value.
|
|
*/
|
|
protected addResultValueEolPage(result: any, name: string, value: any, addMessage?: boolean): void {
|
|
let message = '';
|
|
|
|
if (addMessage) {
|
|
const params = typeof value != 'boolean' ? {$a: value} : undefined;
|
|
message = this.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 {any} page Answer page.
|
|
* @return {boolean} Whether it's a content page.
|
|
*/
|
|
answerPageIsContent(page: any): boolean {
|
|
// The page doesn't have any reliable field to use for checking this. Check qtype first (translated string).
|
|
if (page.qtype == this.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 = this.domUtils.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 {any} page Answer page.
|
|
* @return {boolean} Whether it's a question page.
|
|
*/
|
|
answerPageIsQuestion(page: any): 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 {any} lesson Lesson.
|
|
* @param {any} accessInfo Result of get access info.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {any} [pageIndex] Object containing all the pages indexed by ID. If not defined, it will be calculated.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<{reviewMode: boolean, progress: number, ongoingScore: string}>} Promise resolved with the data.
|
|
*/
|
|
protected calculateOfflineData(lesson: any, accessInfo?: any, password?: string, review?: boolean, pageIndex?: any,
|
|
siteId?: string): Promise<{reviewMode: boolean, progress: number, ongoingScore: string}> {
|
|
|
|
accessInfo = accessInfo || {};
|
|
|
|
const reviewMode = review || accessInfo.reviewmode,
|
|
promises = [];
|
|
let ongoingMessage = '',
|
|
progress: number;
|
|
|
|
if (!accessInfo.canmanage) {
|
|
if (lesson.ongoing && !reviewMode) {
|
|
promises.push(this.getOngoingScoreMessage(lesson, accessInfo, password, review, pageIndex, siteId)
|
|
.then((message) => {
|
|
ongoingMessage = message;
|
|
}));
|
|
}
|
|
if (lesson.progressbar) {
|
|
promises.push(this.calculateProgress(lesson.id, accessInfo, password, review, pageIndex, siteId).then((p) => {
|
|
progress = p;
|
|
}));
|
|
}
|
|
}
|
|
|
|
return Promise.all(promises).then(() => {
|
|
return {
|
|
reviewMode: reviewMode,
|
|
progress: progress,
|
|
ongoingScore: ongoingMessage
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculate the progress of the current user in the lesson.
|
|
* Based on Moodle's calculate_progress.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {any} accessInfo Result of get access info.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {any} [pageIndex] Object containing all the pages indexed by ID. If not defined, it will be calculated.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with a number: the progress (scale 0-100).
|
|
*/
|
|
calculateProgress(lessonId: number, accessInfo: any, password?: string, review?: boolean, pageIndex?: any, siteId?: string)
|
|
: Promise<number> {
|
|
|
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
|
|
|
// Check if the user is reviewing the attempt.
|
|
if (review) {
|
|
return Promise.resolve(100);
|
|
}
|
|
|
|
const retake = accessInfo.attemptscount;
|
|
let viewedPagesIds,
|
|
promise;
|
|
|
|
if (pageIndex) {
|
|
promise = Promise.resolve();
|
|
} else {
|
|
// Retrieve the index.
|
|
promise = this.getPages(lessonId, password, true, false, siteId).then((pages) => {
|
|
pageIndex = this.createPagesIndex(pages);
|
|
});
|
|
}
|
|
|
|
return promise.then(() => {
|
|
// Get the list of question pages attempted.
|
|
return this.getPagesIdsWithQuestionAttempts(lessonId, retake, false, siteId);
|
|
}).then((ids) => {
|
|
viewedPagesIds = ids;
|
|
|
|
// Get the list of viewed content pages.
|
|
return this.getContentPagesViewedIds(lessonId, retake, siteId);
|
|
}).then((viewedContentPagesIds) => {
|
|
const validPages = {};
|
|
let pageId = accessInfo.firstpageid;
|
|
|
|
viewedPagesIds = this.utils.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(pageIndex, pageIndex[pageId], validPages, viewedPagesIds);
|
|
}
|
|
|
|
// Progress calculation as a percent.
|
|
return this.textUtils.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 {any} lesson Lesson.
|
|
* @param {any} pageData Result of getPageData for the page to process.
|
|
* @param {any} data Data containing the user answer.
|
|
* @param {any} jumps Result of get pages possible jumps.
|
|
* @param {any} pageIndex Object containing all the pages indexed by ID.
|
|
* @return {AddonModLessonCheckAnswerResult} Result.
|
|
*/
|
|
protected checkAnswer(lesson: any, pageData: any, data: any, jumps: any, pageIndex: any): AddonModLessonCheckAnswerResult {
|
|
// Default result.
|
|
const result: AddonModLessonCheckAnswerResult = {
|
|
answerid: 0,
|
|
noanswer: false,
|
|
correctanswer: false,
|
|
isessayquestion: false,
|
|
response: '',
|
|
newpageid: 0,
|
|
studentanswer: '',
|
|
userresponse: null,
|
|
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, 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 {any} pageData Result of getPageData for the page to process.
|
|
* @param {any} data Data containing the user answer.
|
|
* @param {AddonModLessonCheckAnswerResult} result Object where to store the result.
|
|
*/
|
|
protected checkAnswerEssay(pageData: any, data: any, result: AddonModLessonCheckAnswerResult): void {
|
|
let studentAnswer;
|
|
|
|
result.isessayquestion = true;
|
|
|
|
if (!data) {
|
|
result.inmediatejump = true;
|
|
result.newpageid = pageData.page.id;
|
|
|
|
return;
|
|
}
|
|
|
|
if (typeof data['answer[text]'] != 'undefined') {
|
|
studentAnswer = data['answer[text]'];
|
|
} else if (typeof data.answer == 'object') {
|
|
studentAnswer = 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;
|
|
});
|
|
|
|
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 {any} pageData Result of getPageData for the page to process.
|
|
* @param {any} data Data containing the user answer.
|
|
* @param {AddonModLessonCheckAnswerResult} result Object where to store the result.
|
|
*/
|
|
protected checkAnswerMatching(pageData: any, data: any, result: AddonModLessonCheckAnswerResult): void {
|
|
if (!data) {
|
|
result.inmediatejump = true;
|
|
result.newpageid = pageData.page.id;
|
|
|
|
return;
|
|
}
|
|
|
|
const response = this.getUserResponseMatching(data),
|
|
getAnswers = this.utils.clone(pageData.answers),
|
|
correct = getAnswers.shift(),
|
|
wrong = getAnswers.shift(),
|
|
answers = {};
|
|
|
|
getAnswers.forEach((answer) => {
|
|
if (answer.answer !== '' || answer.response !== '') {
|
|
answers[answer.id] = answer;
|
|
}
|
|
});
|
|
|
|
// Get the user's exact responses for record keeping.
|
|
const userResponse = [];
|
|
let hits = 0;
|
|
|
|
result.studentanswer = '';
|
|
result.studentanswerformat = 1;
|
|
|
|
for (const id in response) {
|
|
let value = response[id];
|
|
|
|
if (!value) {
|
|
result.noanswer = true;
|
|
|
|
return;
|
|
}
|
|
|
|
value = this.textUtils.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;
|
|
} else {
|
|
result.correctanswer = false;
|
|
result.response = wrong.answer;
|
|
result.answerid = wrong.id;
|
|
result.newpageid = wrong.jumpto;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check a multichoice answer.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {any} pageData Result of getPageData for the page to process.
|
|
* @param {any} data Data containing the user answer.
|
|
* @param {any} pageIndex Object containing all the pages indexed by ID.
|
|
* @param {AddonModLessonCheckAnswerResult} result Object where to store the result.
|
|
*/
|
|
protected checkAnswerMultichoice(lesson: any, pageData: any, data: any, pageIndex: any,
|
|
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 = [],
|
|
responses = [];
|
|
let nHits = 0,
|
|
nCorrect = 0,
|
|
correctAnswerId = 0,
|
|
wrongAnswerId = 0,
|
|
correctPageId,
|
|
wrongPageId;
|
|
|
|
// 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;
|
|
result.answerid = correctAnswerId;
|
|
} else {
|
|
result.correctanswer = false;
|
|
result.response = responses.join(AddonModLessonProvider.MULTIANSWER_DELIMITER);
|
|
result.newpageid = wrongPageId;
|
|
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 = 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;
|
|
result.response = answer.response;
|
|
result.userresponse = result.studentanswer = answer.answer;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check a numerical answer.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {any} pageData Result of getPageData for the page to process.
|
|
* @param {any} data Data containing the user answer.
|
|
* @param {any} pageIndex Object containing all the pages indexed by ID.
|
|
* @param {any} result Object where to store the result.
|
|
*/
|
|
protected checkAnswerNumerical(lesson: any, pageData: any, data: any, pageIndex: any, result: AddonModLessonCheckAnswerResult)
|
|
: void {
|
|
|
|
const parsedAnswer = parseFloat(data.answer);
|
|
|
|
// Set defaults.
|
|
result.response = '';
|
|
result.newpageid = 0;
|
|
|
|
if (!data.answer || isNaN(parsedAnswer)) {
|
|
result.noanswer = true;
|
|
|
|
return;
|
|
} else {
|
|
result.useranswer = parsedAnswer;
|
|
}
|
|
|
|
result.studentanswer = result.userresponse = result.useranswer;
|
|
|
|
// Find the answer.
|
|
for (const i in pageData.answers) {
|
|
const answer = pageData.answers[i];
|
|
let max, min;
|
|
|
|
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 (result.useranswer >= min && result.useranswer <= max) {
|
|
result.newpageid = answer.jumpto;
|
|
result.response = answer.response;
|
|
result.correctanswer = this.isAnswerCorrect(lesson, pageData.page.id, answer, pageIndex);
|
|
result.answerid = answer.id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check a short answer.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {any} pageData Result of getPageData for the page to process.
|
|
* @param {any} data Data containing the user answer.
|
|
* @param {any} pageIndex Object containing all the pages indexed by ID.
|
|
* @param {any} result Object where to store the result.
|
|
*/
|
|
protected checkAnswerShort(lesson: any, pageData: any, data: any, pageIndex: any, result: AddonModLessonCheckAnswerResult)
|
|
: void {
|
|
|
|
let studentAnswer = data.answer && data.answer.trim ? 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],
|
|
useRegExp = pageData.page.qoption;
|
|
let expectedAnswer = answer.answer,
|
|
isMatch = false,
|
|
ignoreCase;
|
|
|
|
if (useRegExp) {
|
|
ignoreCase = '';
|
|
if (expectedAnswer.substr(-2) == '/i') {
|
|
expectedAnswer = expectedAnswer.substr(0, expectedAnswer.length - 2);
|
|
ignoreCase = 'i';
|
|
}
|
|
} else {
|
|
expectedAnswer = expectedAnswer.replace('*', '#####');
|
|
expectedAnswer = this.textUtils.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,
|
|
original = [],
|
|
marked = [];
|
|
|
|
for (let j = 0; j < nb; j++) {
|
|
original.push(matches[0][j]);
|
|
marked.push('<span class="incorrect matches">' + matches[0][j] + '</span>');
|
|
}
|
|
|
|
studentAnswer = studentAnswer.replace(original, marked);
|
|
}
|
|
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;
|
|
result.response = answer.response;
|
|
result.answerid = answer.id;
|
|
break; // Quit answer analysis immediately after a match has been found.
|
|
}
|
|
}
|
|
|
|
result.userresponse = studentAnswer;
|
|
result.studentanswer = this.textUtils.s(studentAnswer); // Clean student answer as it goes to output.
|
|
}
|
|
|
|
/**
|
|
* Check a truefalse answer.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {any} pageData Result of getPageData for the page to process.
|
|
* @param {any} data Data containing the user answer.
|
|
* @param {any} pageIndex Object containing all the pages indexed by ID.
|
|
* @param {any} result Object where to store the result.
|
|
*/
|
|
protected checkAnswerTruefalse(lesson: any, pageData: any, data: any, pageIndex: any, result: AddonModLessonCheckAnswerResult)
|
|
: void {
|
|
|
|
if (!data.answerid) {
|
|
result.noanswer = true;
|
|
|
|
return;
|
|
}
|
|
|
|
result.answerid = 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;
|
|
result.response = answer.response;
|
|
result.studentanswer = result.userresponse = answer.answer;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a list of pages indexed by page ID based on a list of pages.
|
|
*
|
|
* @param {Object[]} pageList Result of get pages.
|
|
* @return {any} Pages index.
|
|
*/
|
|
protected createPagesIndex(pageList: any[]): any {
|
|
// Index the pages by page ID.
|
|
const pages = {};
|
|
|
|
pageList.forEach((pageData) => {
|
|
pages[pageData.page.id] = pageData.page;
|
|
});
|
|
|
|
return pages;
|
|
}
|
|
|
|
/**
|
|
* Finishes a retake.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {number} courseId Course ID the lesson belongs to.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [outOfTime] If the user ran out of time.
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {boolean} [offline] Whether it's offline mode.
|
|
* @param {any} [accessInfo] Result of get access info. Required if offline is true.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
|
|
*/
|
|
finishRetake(lesson: any, courseId: number, password?: string, outOfTime?: boolean, review?: boolean, offline?: boolean,
|
|
accessInfo?: any, siteId?: string): Promise<any> {
|
|
|
|
if (offline) {
|
|
const retake = accessInfo.attemptscount;
|
|
|
|
return this.lessonOfflineProvider.finishRetake(lesson.id, courseId, retake, true, outOfTime, siteId).then(() => {
|
|
// Get the lesson grade.
|
|
return this.lessonGrade(lesson, retake, password, review, undefined, siteId).catch(() => {
|
|
// Ignore errors.
|
|
return {};
|
|
});
|
|
}).then((gradeInfo: AddonModLessonGrade) => {
|
|
// Retake marked, now return the response. We won't return all the possible data.
|
|
// This code is based in Moodle's process_eol_page.
|
|
const result = {
|
|
data: {},
|
|
messages: [],
|
|
warnings: []
|
|
},
|
|
promises = [];
|
|
let gradeLesson = true,
|
|
messageParams,
|
|
entryData;
|
|
|
|
this.addResultValueEolPage(result, 'offline', true); // Mark the result as offline.
|
|
this.addResultValueEolPage(result, 'gradeinfo', gradeInfo);
|
|
|
|
if (lesson.custom && !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.nquestions < lesson.minquestions) {
|
|
gradeLesson = false;
|
|
messageParams = {
|
|
nquestions: gradeInfo.nquestions,
|
|
minquestions: lesson.minquestions
|
|
};
|
|
this.addMessage(result.messages, 'addon.mod_lesson.numberofpagesviewednotice', {$a: messageParams});
|
|
}
|
|
}
|
|
|
|
if (!accessInfo.canmanage) {
|
|
if (gradeLesson) {
|
|
promises.push(this.calculateProgress(lesson.id, accessInfo, password, review, undefined, siteId)
|
|
.then((progress) => {
|
|
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);
|
|
}
|
|
|
|
entryData = {
|
|
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 != CoreGradesProvider.TYPE_NONE) {
|
|
entryData = {
|
|
grade: this.textUtils.roundToDecimals(gradeInfo.grade * lesson.grade / 100, 1),
|
|
total: lesson.grade
|
|
};
|
|
this.addResultValueEolPage(result, 'yourcurrentgradeisoutof', entryData, true);
|
|
}
|
|
|
|
} else {
|
|
// User hasn't answered any question, only content pages.
|
|
if (lesson.timelimit) {
|
|
if (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 && accessInfo.canmanage) {
|
|
this.addResultValueEolPage(result, 'modattemptsnoteacher', true, true);
|
|
}
|
|
|
|
if (gradeLesson) {
|
|
this.addResultValueEolPage(result, 'gradelesson', 1);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
return this.finishRetakeOnline(lesson.id, password, outOfTime, review, siteId);
|
|
}
|
|
|
|
/**
|
|
* Finishes a retake. It will fail if offline or cannot connect.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [outOfTime] If the user ran out of time.
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
|
|
*/
|
|
finishRetakeOnline(lessonId: number, password?: string, outOfTime?: boolean, review?: boolean, siteId?: string): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: lessonId,
|
|
outoftime: outOfTime ? 1 : 0,
|
|
review: review ? 1 : 0
|
|
};
|
|
|
|
if (typeof password == 'string') {
|
|
params.password = password;
|
|
}
|
|
|
|
return site.write('mod_lesson_finish_attempt', params).then((response) => {
|
|
// Convert the data array into an object and decode the values.
|
|
const map = {};
|
|
|
|
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 = this.textUtils.parseJSON(entry.value);
|
|
}
|
|
|
|
map[entry.name] = entry;
|
|
});
|
|
response.data = map;
|
|
|
|
return response;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the access information of a certain lesson.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data. Has priority over ignoreCache.
|
|
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with the access information.
|
|
*/
|
|
getAccessInformation(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params = {
|
|
lessonid: lessonId
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getAccessInformationCacheKey(lessonId)
|
|
};
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_lesson_access_information', params, preSets);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for access information WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getAccessInformationCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'accessInfo:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get content pages viewed in online and offline.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<{online: any[], offline: any[]}>} Promise resolved with an object with the online and offline viewed pages.
|
|
*/
|
|
getContentPagesViewed(lessonId: number, retake: number, siteId?: string): Promise<{online: any[], offline: any[]}> {
|
|
const promises = [],
|
|
type = AddonModLessonProvider.TYPE_STRUCTURE,
|
|
result = {
|
|
online: [],
|
|
offline: []
|
|
};
|
|
|
|
// Get the online pages.
|
|
promises.push(this.getContentPagesViewedOnline(lessonId, retake, false, false, siteId).then((pages) => {
|
|
result.online = pages;
|
|
}));
|
|
|
|
// Get the offline pages.
|
|
promises.push(this.lessonOfflineProvider.getRetakeAttemptsForType(lessonId, retake, type, siteId).catch(() => {
|
|
return [];
|
|
}).then((pages) => {
|
|
result.offline = pages;
|
|
}));
|
|
|
|
return Promise.all(promises).then(() => {
|
|
return result;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get content pages viewed WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @return {string} 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 {number} lessonId Lesson ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getContentPagesViewedCommonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'contentPagesViewed:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get IDS of content pages viewed in online and offline.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<number[]>} Promise resolved with list of IDs.
|
|
*/
|
|
getContentPagesViewedIds(lessonId: number, retake: number, siteId?: string): Promise<number[]> {
|
|
return this.getContentPagesViewed(lessonId, retake, siteId).then((result) => {
|
|
const ids = {},
|
|
pages = result.online.concat(result.offline);
|
|
|
|
pages.forEach((page) => {
|
|
if (!ids[page.pageid]) {
|
|
ids[page.pageid] = true;
|
|
}
|
|
});
|
|
|
|
return Object.keys(ids).map((id) => {
|
|
return Number(id);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the list of content pages viewed in the site for a certain retake.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data. Has priority over ignoreCache.
|
|
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any[]>} Promise resolved with the viewed pages.
|
|
*/
|
|
getContentPagesViewedOnline(lessonId: number, retake: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string)
|
|
: Promise<any[]> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params = {
|
|
lessonid: lessonId,
|
|
lessonattempt: retake
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getContentPagesViewedCacheKey(lessonId, retake)
|
|
};
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_content_pages_viewed', params, preSets).then((result) => {
|
|
return result.pages;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the last content page viewed.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with the last content page viewed.
|
|
*/
|
|
getLastContentPageViewed(lessonId: number, retake: number, siteId?: string): Promise<any> {
|
|
return this.getContentPagesViewed(lessonId, retake, siteId).then((data) => {
|
|
let lastPage,
|
|
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 {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<number>} Promise resolved with the last page seen.
|
|
*/
|
|
getLastPageSeen(lessonId: number, retake: number, siteId?: string): Promise<number> {
|
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
|
|
|
let lastPageSeen: number;
|
|
|
|
// Get the last question answered.
|
|
return this.lessonOfflineProvider.getLastQuestionPageAttempt(lessonId, retake, siteId).then((answer) => {
|
|
if (answer) {
|
|
lastPageSeen = answer.newpageid;
|
|
}
|
|
|
|
// Now get the last content page viewed.
|
|
return this.getLastContentPageViewed(lessonId, retake, siteId).then((page) => {
|
|
if (page) {
|
|
if (answer) {
|
|
if (page.timemodified > answer.timemodified) {
|
|
// This content page was viewed more recently than the question page.
|
|
lastPageSeen = page.newpageid || page.pageid;
|
|
}
|
|
} else {
|
|
// Has not answered any questions but has viewed a content page.
|
|
lastPageSeen = page.newpageid || page.pageid;
|
|
}
|
|
}
|
|
|
|
return lastPageSeen;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get a Lesson by module ID.
|
|
*
|
|
* @param {number} courseId Course ID.
|
|
* @param {number} cmid Course module ID.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the lesson is retrieved.
|
|
*/
|
|
getLesson(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise<any> {
|
|
return this.getLessonByField(courseId, 'coursemodule', cmId, forceCache, siteId);
|
|
}
|
|
|
|
/**
|
|
* Get a Lesson with key=value. If more than one is found, only the first will be returned.
|
|
*
|
|
* @param {number} courseId Course ID.
|
|
* @param {string} key Name of the property to check.
|
|
* @param {any} value Value to search.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the lesson is retrieved.
|
|
*/
|
|
protected getLessonByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params = {
|
|
courseids: [courseId]
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getLessonDataCacheKey(courseId)
|
|
};
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_lessons_by_courses', params, preSets).then((response) => {
|
|
if (response && response.lessons) {
|
|
const currentLesson = response.lessons.find((lesson) => {
|
|
return lesson[key] == value;
|
|
});
|
|
|
|
if (currentLesson) {
|
|
return currentLesson;
|
|
}
|
|
}
|
|
|
|
return Promise.reject(null);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get a Lesson by lesson ID.
|
|
*
|
|
* @param {number} courseId Course ID.
|
|
* @param {number} id Lesson ID.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the lesson is retrieved.
|
|
*/
|
|
getLessonById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise<any> {
|
|
return this.getLessonByField(courseId, 'id', id, forceCache, siteId);
|
|
}
|
|
|
|
/**
|
|
* Get cache key for Lesson data WS calls.
|
|
*
|
|
* @param {number} courseId Course ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getLessonDataCacheKey(courseId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'lesson:' + courseId;
|
|
}
|
|
|
|
/**
|
|
* Get a lesson protected with password.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [password] Password.
|
|
* @param {boolean} [validatePassword=true] If true, the function will fail if the password is wrong.
|
|
* If false, it will return a lesson with the basic data if password is wrong.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data. Has priority over ignoreCache.
|
|
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with the lesson.
|
|
*/
|
|
getLessonWithPassword(lessonId: number, password?: string, validatePassword: boolean = true, forceCache?: boolean,
|
|
ignoreCache?: boolean, siteId?: string): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: lessonId
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getLessonWithPasswordCacheKey(lessonId)
|
|
};
|
|
|
|
if (typeof password == 'string') {
|
|
params.password = password;
|
|
}
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_lesson', params, preSets).then((response) => {
|
|
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.
|
|
return this.invalidateLessonWithPassword(lessonId, site.id).catch(() => {
|
|
// Shouldn't happen.
|
|
}).then(() => {
|
|
return Promise.reject(this.translate.instant('addon.mod_lesson.loginfail'));
|
|
});
|
|
}
|
|
}
|
|
|
|
return response.lesson;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get lesson with password WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getLessonWithPasswordCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'lessonWithPswrd:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Given a page ID, a jumpto and all the possible jumps, calcualate the new page ID.
|
|
*
|
|
* @param {number} pageId Current page ID.
|
|
* @param {number} jumpTo The jumpto.
|
|
* @param {any} jumps Result of get pages possible jumps.
|
|
* @return {number} New page ID.
|
|
*/
|
|
protected getNewPageId(pageId: number, jumpTo: number, jumps: any): 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 {any} lesson Lesson.
|
|
* @param {any} accessInfo Result of get access info.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {any} [pageIndex] Object containing all the pages indexed by ID. If not provided, it will be calculated.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<string>} Promise resolved with the ongoing score message.
|
|
*/
|
|
getOngoingScoreMessage(lesson: any, accessInfo: any, password?: string, review?: boolean, pageIndex?: any, siteId?: string)
|
|
: Promise<string> {
|
|
|
|
if (accessInfo.canmanage) {
|
|
return Promise.resolve(this.translate.instant('addon.mod_lesson.teacherongoingwarning'));
|
|
} else {
|
|
let retake = accessInfo.attemptscount;
|
|
if (review) {
|
|
retake--;
|
|
}
|
|
|
|
return this.lessonGrade(lesson, retake, password, review, pageIndex, siteId).then((gradeInfo) => {
|
|
const data: any = {};
|
|
|
|
if (lesson.custom) {
|
|
data.score = gradeInfo.earned;
|
|
data.currenthigh = gradeInfo.total;
|
|
|
|
return this.translate.instant('addon.mod_lesson.ongoingcustom', {$a: data});
|
|
} else {
|
|
data.correct = gradeInfo.earned;
|
|
data.viewed = gradeInfo.attempts;
|
|
|
|
return this.translate.instant('addon.mod_lesson.ongoingnormal', {$a: data});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the possible answers from a page.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {number} pageId Page ID.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with the list of possible answers.
|
|
*/
|
|
protected getPageAnswers(lesson: any, pageId: number, password?: string, review?: boolean, siteId?: string): Promise<any[]> {
|
|
return this.getPageData(lesson, pageId, password, review, true, true, false, undefined, undefined, siteId).then((data) => {
|
|
return data.answers;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all the possible answers from a list of pages, indexed by answerId.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {number[]} pageIds List of page IDs.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with an object containing the answers.
|
|
*/
|
|
protected getPagesAnswers(lesson: any, pageIds: number[], password?: string, review?: boolean, siteId?: string)
|
|
: Promise<any> {
|
|
|
|
const answers = {},
|
|
promises = [];
|
|
|
|
pageIds.forEach((pageId) => {
|
|
promises.push(this.getPageAnswers(lesson, pageId, password, review, siteId).then((pageAnswers) => {
|
|
pageAnswers.forEach((answer) => {
|
|
// Include the pageid in each answer and add them to the final list.
|
|
answer.pageid = pageId;
|
|
answers[answer.id] = answer;
|
|
});
|
|
}));
|
|
});
|
|
|
|
return Promise.all(promises).then(() => {
|
|
return answers;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get page data.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {number} pageId Page ID.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {boolean} [includeContents] Include the page rendered contents.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data. Has priority over ignoreCache.
|
|
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
|
|
* @param {any} [accessInfo] Result of get access info. Required if offline is true.
|
|
* @param {any} [jumps] Result of get pages possible jumps. Required if offline is true.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with the page data.
|
|
*/
|
|
getPageData(lesson: any, pageId: number, password?: string, review?: boolean, includeContents?: boolean, forceCache?: boolean,
|
|
ignoreCache?: boolean, accessInfo?: any, jumps?: any, siteId?: string): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: lesson.id,
|
|
pageid: Number(pageId),
|
|
review: review ? 1 : 0,
|
|
returncontents: includeContents ? 1 : 0
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getPageDataCacheKey(lesson.id, pageId)
|
|
};
|
|
|
|
if (typeof password == 'string') {
|
|
params.password = password;
|
|
}
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
if (review) {
|
|
// Force online mode in review.
|
|
preSets.getFromCache = false;
|
|
preSets.saveToCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_page_data', params, preSets).then((data) => {
|
|
if (forceCache && accessInfo && data.page) {
|
|
// Offline mode and valid page. Calculate the data that might be affected.
|
|
return this.calculateOfflineData(lesson, accessInfo, password, review, undefined, siteId).then((calcData) => {
|
|
Object.assign(data, calcData);
|
|
|
|
return this.getPageViewMessages(lesson, accessInfo, data.page, review, jumps, password, siteId);
|
|
}).then((messages) => {
|
|
data.messages = messages;
|
|
|
|
return data;
|
|
});
|
|
}
|
|
|
|
return data;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get page data WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} pageId Page ID.
|
|
* @return {string} 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 {number} lessonId Lesson ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getPageDataCommonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'pageData:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get lesson pages.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [forceCache] Whether it should always return cached data. Has priority over ignoreCache.
|
|
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any[]>} Promise resolved with the pages.
|
|
*/
|
|
getPages(lessonId: number, password?: string, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise<any[]> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: lessonId,
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getPagesCacheKey(lessonId)
|
|
};
|
|
|
|
if (typeof password == 'string') {
|
|
params.password = password;
|
|
}
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_pages', params, preSets).then((response) => {
|
|
return response.pages;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get pages WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getPagesCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'pages:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get possible jumps for a lesson.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data. Has priority over ignoreCache.
|
|
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with the jumps.
|
|
*/
|
|
getPagesPossibleJumps(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params = {
|
|
lessonid: lessonId,
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getPagesPossibleJumpsCacheKey(lessonId)
|
|
};
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_pages_possible_jumps', params, preSets).then((response) => {
|
|
// Index the jumps by page and jumpto.
|
|
if (response.jumps) {
|
|
const jumps = {};
|
|
|
|
response.jumps.forEach((jump) => {
|
|
if (typeof jumps[jump.pageid] == 'undefined') {
|
|
jumps[jump.pageid] = {};
|
|
}
|
|
jumps[jump.pageid][jump.jumpto] = jump;
|
|
});
|
|
|
|
return jumps;
|
|
}
|
|
|
|
return Promise.reject(null);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get pages possible jumps WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getPagesPossibleJumpsCacheKey(lessonId: number): string {
|
|
return this.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 {any} lesson Lesson.
|
|
* @param {any} accessInfo Result of get access info.
|
|
* @param {any} result Result of process page.
|
|
* @param {boolean} review If the user wants to review just after finishing (1 hour margin).
|
|
* @param {any} jumps Result of get pages possible jumps.
|
|
* @return {any[]} Array with the messages.
|
|
*/
|
|
getPageProcessMessages(lesson: any, accessInfo: any, result: any, review: boolean, jumps: any): any[] {
|
|
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: this.translate.instant('addon.mod_lesson.clusterjump'),
|
|
unseen: this.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 > 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 {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {boolean} [correct] True to only fetch correct attempts, false to get them all.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @param {number} [userId] User ID. If not defined, site's user.
|
|
* @return {Promise<number[]>} Promise resolved with the IDs.
|
|
*/
|
|
getPagesIdsWithQuestionAttempts(lessonId: number, retake: number, correct?: boolean, siteId?: string, userId?: number)
|
|
: Promise<number[]> {
|
|
|
|
return this.getQuestionsAttempts(lessonId, retake, correct, undefined, siteId, userId).then((result) => {
|
|
const ids = {},
|
|
attempts = result.online.concat(result.offline);
|
|
|
|
attempts.forEach((attempt) => {
|
|
if (!ids[attempt.pageid]) {
|
|
ids[attempt.pageid] = true;
|
|
}
|
|
});
|
|
|
|
return Object.keys(ids).map((id) => {
|
|
return 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 {any} lesson Lesson.
|
|
* @param {any} accessInfo Result of get access info. Required if offline is true.
|
|
* @param {any} page Page loaded.
|
|
* @param {boolean} review If the user wants to review just after finishing (1 hour margin).
|
|
* @param {any} jumps Result of get pages possible jumps.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any[]>} Promise resolved with the list of messages.
|
|
*/
|
|
getPageViewMessages(lesson: any, accessInfo: any, page: any, review: boolean, jumps: any, password?: string, siteId?: string)
|
|
: Promise<any[]> {
|
|
|
|
const messages = [];
|
|
let promise = Promise.resolve();
|
|
|
|
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;
|
|
|
|
promise = this.lessonGrade(lesson, retake, password, review, undefined, siteId).then((gradeInfo) => {
|
|
if (gradeInfo.attempts) {
|
|
if (gradeInfo.nquestions < lesson.minquestions) {
|
|
this.addMessage(messages, 'addon.mod_lesson.numberofpagesviewednotice', {$a: {
|
|
nquestions: gradeInfo.nquestions,
|
|
minquestions: lesson.minquestions
|
|
}});
|
|
}
|
|
|
|
if (!review && !lesson.retake) {
|
|
this.addMessage(messages, 'addon.mod_lesson.numberofcorrectanswers', {$a: gradeInfo.earned});
|
|
|
|
if (lesson.grade != CoreGradesProvider.TYPE_NONE) {
|
|
this.addMessage(messages, 'addon.mod_lesson.yourcurrentgradeisoutof', {$a: {
|
|
grade: this.textUtils.roundToDecimals(gradeInfo.grade * lesson.grade / 100, 1),
|
|
total: lesson.grade
|
|
}});
|
|
}
|
|
}
|
|
}
|
|
}).catch(() => {
|
|
// Ignore errors.
|
|
});
|
|
}
|
|
} 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: this.translate.instant('addon.mod_lesson.clusterjump'),
|
|
unseen: this.translate.instant('addon.mod_lesson.unseenpageinbranch')
|
|
}});
|
|
}
|
|
}
|
|
|
|
return promise.then(() => {
|
|
return messages;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get questions attempts, including offline attempts.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {boolean} [correct] True to only fetch correct attempts, false to get them all.
|
|
* @param {number} [pageId] If defined, only get attempts on this page.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @param {number} [userId] User ID. If not defined, site's user.
|
|
* @return {Promise<{online: any[], offline: any[]}>} Promise resolved with the questions attempts.
|
|
*/
|
|
getQuestionsAttempts(lessonId: number, retake: number, correct?: boolean, pageId?: number, siteId?: string, userId?: number)
|
|
: Promise<{online: any[], offline: any[]}> {
|
|
|
|
const promises = [],
|
|
result = {
|
|
online: [],
|
|
offline: []
|
|
};
|
|
|
|
promises.push(this.getQuestionsAttemptsOnline(lessonId, retake, correct, pageId, false, false, siteId, userId)
|
|
.then((attempts) => {
|
|
result.online = attempts;
|
|
}));
|
|
|
|
promises.push(this.lessonOfflineProvider.getQuestionsAttempts(lessonId, retake, correct, pageId, siteId).catch(() => {
|
|
// Error, assume no attempts.
|
|
return [];
|
|
}).then((attempts) => {
|
|
result.offline = attempts;
|
|
}));
|
|
|
|
return Promise.all(promises).then(() => {
|
|
return result;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get questions attempts WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {number} userId User ID.
|
|
* @return {string} 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 {number} lessonId Lesson ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getQuestionsAttemptsCommonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'questionsAttempts:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get questions attempts from the site.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {boolean} [correct] True to only fetch correct attempts, false to get them all.
|
|
* @param {number} [pageId] If defined, only get attempts on this page.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data. Has priority over ignoreCache.
|
|
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @param {number} [userId] User ID. If not defined, site's user.
|
|
* @return {Promise<any[]>} Promise resolved with the questions attempts.
|
|
*/
|
|
getQuestionsAttemptsOnline(lessonId: number, retake: number, correct?: boolean, pageId?: number, forceCache?: boolean,
|
|
ignoreCache?: boolean, siteId?: string, userId?: number): Promise<any[]> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
userId = userId || site.getUserId();
|
|
|
|
// Don't pass "pageId" and "correct" params, they will be filtered locally.
|
|
const params = {
|
|
lessonid: lessonId,
|
|
attempt: retake,
|
|
userid: userId
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getQuestionsAttemptsCacheKey(lessonId, retake, userId)
|
|
};
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_questions_attempts', params, preSets).then((response) => {
|
|
if (pageId || correct) {
|
|
// Filter the attempts.
|
|
return response.attempts.filter((attempt) => {
|
|
if (correct && !attempt.correct) {
|
|
return false;
|
|
}
|
|
|
|
if (pageId && attempt.pageid != pageId) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return response.attempts;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the overview of retakes in a lesson (named "attempts overview" in Moodle).
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} [groupId] The group to get. If not defined, all participants.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data. Has priority over ignoreCache.
|
|
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with the retakes overview.
|
|
*/
|
|
getRetakesOverview(lessonId: number, groupId?: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string)
|
|
: Promise<any> {
|
|
|
|
groupId = groupId || 0;
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params = {
|
|
lessonid: lessonId,
|
|
groupid: groupId
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getRetakesOverviewCacheKey(lessonId, groupId)
|
|
};
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_attempts_overview', params, preSets).then((response) => {
|
|
return response.data;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get retakes overview WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} groupId Group ID.
|
|
* @return {string} 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 {number} lessonId Lesson ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getRetakesOverviewCommonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'retakesOverview:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get a password stored in DB.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<string>} Promise resolved with password on success, rejected otherwise.
|
|
*/
|
|
getStoredPassword(lessonId: number, siteId?: string): Promise<string> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.getDb().getRecord(AddonModLessonProvider.PASSWORD_TABLE, {lessonid: lessonId}).then((entry) => {
|
|
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 {any} pages Index of lesson pages, indexed by page ID. See createPagesIndex.
|
|
* @param {number} pageId Page ID to get subpages of.
|
|
* @param {number[]} end An array of LESSON_PAGE_* types that signify an end of the subtype.
|
|
* @return {Object[]} List of subpages.
|
|
*/
|
|
getSubpagesOf(pages: any, pageId: number, ends: number[]): any[] {
|
|
const subPages = [];
|
|
|
|
pageId = pages[pageId].nextpageid; // Move to the first page after the given page.
|
|
ends = ends || [];
|
|
|
|
while (true) {
|
|
if (!pageId || ends.indexOf(pages[pageId].qtype) != -1) {
|
|
// No more pages or it reached a page of the searched types. Stop.
|
|
break;
|
|
}
|
|
|
|
subPages.push(pages[pageId]);
|
|
pageId = pages[pageId].nextpageid;
|
|
}
|
|
|
|
return subPages;
|
|
}
|
|
|
|
/**
|
|
* Get lesson timers.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data. Has priority over ignoreCache.
|
|
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @param {number} [userId] User ID. If not defined, site's current user.
|
|
* @return {Promise<any[]>} Promise resolved with the pages.
|
|
*/
|
|
getTimers(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string, userId?: number): Promise<any[]> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
userId = userId || site.getUserId();
|
|
|
|
const params = {
|
|
lessonid: lessonId,
|
|
userid: userId
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getTimersCacheKey(lessonId, userId)
|
|
};
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_user_timers', params, preSets).then((response) => {
|
|
return response.timers;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get timers WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} userId User ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getTimersCacheKey(lessonId: number, userId: number): string {
|
|
return this.getTimersCommonCacheKey(lessonId) + ':' + userId;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get timers WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getTimersCommonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'timers:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get the list of used answers (with valid answer) in a multichoice question page.
|
|
*
|
|
* @param {any} pageData Result of getPageData for the page to process.
|
|
* @return {any[]} List of used answers.
|
|
*/
|
|
protected getUsedAnswersMultichoice(pageData: any): any[] {
|
|
const answers = this.utils.clone(pageData.answers);
|
|
|
|
return answers.filter((entry) => {
|
|
return entry.answer !== '';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the user's response in a matching question page.
|
|
*
|
|
* @param {any} data Data containing the user answer.
|
|
* @return {any} User response.
|
|
*/
|
|
protected getUserResponseMatching(data: any): any {
|
|
if (data.response) {
|
|
// The data is already stored as expected. Return it.
|
|
return data.response;
|
|
}
|
|
|
|
// Data is stored in properties like 'response[379]'. Recreate the response object.
|
|
const response = {};
|
|
|
|
for (const key in data) {
|
|
const match = key.match(/^response\[(\d+)\]/);
|
|
|
|
if (match && match.length > 1) {
|
|
response[match[1]] = data[key];
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Get the user's response in a multichoice page if multiple answers are allowed.
|
|
*
|
|
* @param {any} data Data containing the user answer.
|
|
* @return {any[]} User response.
|
|
*/
|
|
protected getUserResponseMultichoice(data: any): any[] {
|
|
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) => {
|
|
return parseInt(value, 10);
|
|
});
|
|
}
|
|
|
|
return data.answer;
|
|
}
|
|
|
|
// Data is stored in properties like 'answer[379]'. Recreate the answer array.
|
|
const answer = [];
|
|
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 {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number
|
|
* @param {number} [userId] User ID. Undefined for current user.
|
|
* @param {boolean} [forceCache] Whether it should always return cached data. Has priority over ignoreCache.
|
|
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with the retake data.
|
|
*/
|
|
getUserRetake(lessonId: number, retake: number, userId?: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string)
|
|
: Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
userId = userId || site.getUserId();
|
|
|
|
const params = {
|
|
lessonid: lessonId,
|
|
userid: userId,
|
|
lessonattempt: retake
|
|
},
|
|
preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getUserRetakeCacheKey(lessonId, userId, retake)
|
|
};
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_user_attempt', params, preSets);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get user retake WS calls.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} userId User ID.
|
|
* @param {number} retake Retake number
|
|
* @return {string} 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 {number} lessonId Lesson ID.
|
|
* @param {number} userId User ID.
|
|
* @return {string} 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 {number} lessonId Lesson ID.
|
|
* @return {string} Cache key.
|
|
*/
|
|
protected getUserRetakeLessonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'userRetake:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Check if a jump is correct.
|
|
* Based in Moodle's jumpto_is_correct.
|
|
*
|
|
* @param {number} pageId ID of the page from which you are jumping from.
|
|
* @param {number} jumpTo The jumpto number.
|
|
* @param {any} pageIndex Object containing all the pages indexed by ID. See createPagesIndex.
|
|
* @return {boolean} Whether jump is correct.
|
|
*/
|
|
jumptoIsCorrect(pageId: number, jumpTo: number, pageIndex: any): 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 {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateAccessInformation(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(lessonId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates content pages viewed for all retakes.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateContentPagesViewed(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKeyStartingWith(this.getContentPagesViewedCommonCacheKey(lessonId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates content pages viewed for a certain retake.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateContentPagesViewedForRetake(lessonId: number, retake: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKey(this.getContentPagesViewedCacheKey(lessonId, retake));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates Lesson data.
|
|
*
|
|
* @param {number} courseId Course ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateLessonData(courseId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKey(this.getLessonDataCacheKey(courseId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates lesson with password.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateLessonWithPassword(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKey(this.getLessonWithPasswordCacheKey(lessonId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates page data for all pages.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidatePageData(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKeyStartingWith(this.getPageDataCommonCacheKey(lessonId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates page data for a certain page.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} pageId Page ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidatePageDataForPage(lessonId: number, pageId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKey(this.getPageDataCacheKey(lessonId, pageId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates pages.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidatePages(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKey(this.getPagesCacheKey(lessonId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates pages possible jumps.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidatePagesPossibleJumps(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKey(this.getPagesPossibleJumpsCacheKey(lessonId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates questions attempts for all retakes.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateQuestionsAttempts(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKeyStartingWith(this.getQuestionsAttemptsCommonCacheKey(lessonId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates question attempts for a certain retake and user.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {string} [siteId] Site ID. If not defined, current site..
|
|
* @param {number} [userId] User ID. If not defined, site's user.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateQuestionsAttemptsForRetake(lessonId: number, retake: number, siteId?: string, userId?: number): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
userId = userId || site.getUserId();
|
|
|
|
return site.invalidateWsCacheForKey(this.getQuestionsAttemptsCacheKey(lessonId, retake, userId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates retakes overview for all groups in a lesson.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateRetakesOverview(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKeyStartingWith(this.getRetakesOverviewCommonCacheKey(lessonId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates retakes overview for a certain group in a lesson.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} groupId Group ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateRetakesOverviewForGroup(lessonId: number, groupId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKey(this.getRetakesOverviewCacheKey(lessonId, groupId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates timers for all users in a lesson.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateTimers(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKeyStartingWith(this.getTimersCommonCacheKey(lessonId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates timers for a certain user.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @param {number} [userId] User ID. If not defined, site's current user.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateTimersForUser(lessonId: number, siteId?: string, userId?: number): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
userId = userId || site.getUserId();
|
|
|
|
return site.invalidateWsCacheForKey(this.getTimersCacheKey(lessonId, userId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates a certain retake for a certain user.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} retake Retake number.
|
|
* @param {number} [userId] User ID. Undefined for current user.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateUserRetake(lessonId: number, retake: number, userId?: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
userId = userId || site.getUserId();
|
|
|
|
return site.invalidateWsCacheForKey(this.getUserRetakeCacheKey(lessonId, userId, retake));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates all retakes for all users in a lesson.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateUserRetakesForLesson(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.invalidateWsCacheForKeyStartingWith(this.getUserRetakeLessonCacheKey(lessonId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates all retakes for a certain user in a lesson.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} [userId] User ID. Undefined for current user.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateUserRetakesForUser(lessonId: number, userId?: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
userId = userId || site.getUserId();
|
|
|
|
return site.invalidateWsCacheForKeyStartingWith(this.getUserRetakeUserCacheKey(lessonId, userId));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if a page answer is correct.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {number} pageId The page ID.
|
|
* @param {any} answer The answer to check.
|
|
* @param {any} pageIndex Object containing all the pages indexed by ID.
|
|
* @return {boolean} Whether the answer is correct.
|
|
*/
|
|
protected isAnswerCorrect(lesson: any, pageId: number, answer: any, pageIndex: any): boolean {
|
|
if (lesson.custom) {
|
|
// Custom scores. If score on answer is positive, it is correct.
|
|
return answer.score > 0;
|
|
} else {
|
|
return this.jumptoIsCorrect(pageId, answer.jumpto, pageIndex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a lesson is enabled to be used in offline.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @return {boolean} Whether offline is enabled.
|
|
*/
|
|
isLessonOffline(lesson: any): boolean {
|
|
return !!lesson.allowofflineattempts;
|
|
}
|
|
|
|
/**
|
|
* Check if a lesson is password protected based in the access info.
|
|
*
|
|
* @param {any} info Lesson access info.
|
|
* @return {boolean} Whether the lesson is password protected.
|
|
*/
|
|
isPasswordProtected(info: any): boolean {
|
|
if (info && info.preventaccessreasons) {
|
|
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 {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
|
|
*/
|
|
isPluginEnabled(siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
// 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 {number} type Type of the page.
|
|
* @return {boolean} True if question page, false if content page.
|
|
*/
|
|
isQuestionPage(type: number): boolean {
|
|
return type == AddonModLessonProvider.TYPE_QUESTION;
|
|
}
|
|
|
|
/**
|
|
* Start or continue a retake.
|
|
*
|
|
* @param {string} id Lesson ID.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {number} [pageId] Page id to continue from (only when continuing a retake).
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the WS call is successful.
|
|
*/
|
|
launchRetake(id: number, password?: string, pageId?: number, review?: boolean, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: id,
|
|
review: review ? 1 : 0
|
|
};
|
|
|
|
if (typeof password == 'string') {
|
|
params.password = password;
|
|
}
|
|
if (typeof pageId == 'number') {
|
|
params.pageid = pageId;
|
|
}
|
|
|
|
return site.write('mod_lesson_launch_attempt', params);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if the user left during a timed session.
|
|
*
|
|
* @param {any} info Lesson access info.
|
|
* @return {boolean} True if left during timed, false otherwise.
|
|
*/
|
|
leftDuringTimed(info: any): boolean {
|
|
return info && 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 {any} jumps Result of get pages possible jumps.
|
|
* @return {boolean} Whether the lesson uses one of those jumps.
|
|
*/
|
|
lessonDisplayTeacherWarning(jumps: any): 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 {any} lesson Lesson.
|
|
* @param {number} retake Retake number.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {any} [pageIndex] Object containing all the pages indexed by ID. If not provided, it will be calculated.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @param {number} [userId] User ID. If not defined, site's user.
|
|
* @return {Promise<AddonModLessonCheckAnswerResult>} Promise resolved with the grade data.
|
|
*/
|
|
lessonGrade(lesson: any, retake: number, password?: string, review?: boolean, pageIndex?: any, siteId?: string,
|
|
userId?: number): Promise<AddonModLessonGrade> {
|
|
|
|
// Initialize all variables.
|
|
let nViewed = 0,
|
|
nManual = 0,
|
|
manualPoints = 0,
|
|
theGrade = 0,
|
|
nQuestions = 0,
|
|
total = 0,
|
|
earned = 0;
|
|
|
|
// Get the questions attempts for the user.
|
|
return this.getQuestionsAttempts(lesson.id, retake, false, undefined, siteId, userId).then((attemptsData) => {
|
|
const attempts = attemptsData.online.concat(attemptsData.offline);
|
|
|
|
if (!attempts.length) {
|
|
// No attempts.
|
|
return;
|
|
}
|
|
|
|
const attemptSet = {};
|
|
let promise;
|
|
|
|
// Create the pageIndex if it isn't provided.
|
|
if (!pageIndex) {
|
|
promise = this.getPages(lesson.id, password, true, false, siteId).then((pages) => {
|
|
pageIndex = this.createPagesIndex(pages);
|
|
});
|
|
} else {
|
|
promise = Promise.resolve();
|
|
}
|
|
|
|
return promise.then(() => {
|
|
const pageIds = [];
|
|
|
|
// 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);
|
|
});
|
|
|
|
// 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) => {
|
|
return (a.timeseen || a.timemodified) - (b.timeseen || b.timemodified);
|
|
});
|
|
|
|
attemptSet[pageId] = attempts.slice(0, lesson.maxattempts);
|
|
}
|
|
|
|
// Get all the answers from the pages the user answered.
|
|
return this.getPagesAnswers(lesson, pageIds, password, review, siteId);
|
|
}).then((answers) => {
|
|
// Number of pages answered.
|
|
nQuestions = Object.keys(attemptSet).length;
|
|
|
|
for (const pageId in attemptSet) {
|
|
const attempts = attemptSet[pageId],
|
|
lastAttempt = attempts[attempts.length - 1];
|
|
|
|
if (lesson.custom) {
|
|
// If essay question, handle it, otherwise add to score.
|
|
if (pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) {
|
|
if (lastAttempt.useranswer && typeof lastAttempt.useranswer.score != 'undefined') {
|
|
earned += lastAttempt.useranswer.score;
|
|
}
|
|
nManual++;
|
|
manualPoints += answers[lastAttempt.answerid].score;
|
|
} else if (lastAttempt.answerid) {
|
|
earned += answers[lastAttempt.answerid].score;
|
|
}
|
|
} else {
|
|
attempts.forEach((attempt) => {
|
|
earned += attempt.correct ? 1 : 0;
|
|
});
|
|
|
|
// If essay question, increase numbers.
|
|
if (pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) {
|
|
nManual++;
|
|
manualPoints++;
|
|
}
|
|
}
|
|
|
|
// Number of times answered.
|
|
nViewed += attempts.length;
|
|
}
|
|
|
|
if (lesson.custom) {
|
|
const bestScores = {};
|
|
|
|
// 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;
|
|
} else if (bestScores[answer.pageid] < answer.score) {
|
|
bestScores[answer.pageid] = answer.score;
|
|
}
|
|
}
|
|
|
|
// Sum all the scores.
|
|
for (const pageId in bestScores) {
|
|
total += bestScores[pageId];
|
|
}
|
|
} else {
|
|
// Check to make sure the student has answered the minimum questions.
|
|
if (lesson.minquestions && nQuestions < lesson.minquestions) {
|
|
// Nope, increase number viewed by the amount of unanswered questions.
|
|
total = nViewed + (lesson.minquestions - nQuestions);
|
|
} else {
|
|
total = nViewed;
|
|
}
|
|
}
|
|
});
|
|
}).then(() => {
|
|
if (total) { // Not zero.
|
|
theGrade = this.textUtils.roundToDecimals(earned * 100 / total, 5);
|
|
}
|
|
|
|
return {
|
|
nquestions: nQuestions,
|
|
attempts: nViewed,
|
|
total: total,
|
|
earned: earned,
|
|
grade: theGrade,
|
|
nmanual: nManual,
|
|
manualpoints: manualPoints
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Report a lesson as being viewed.
|
|
*
|
|
* @param {string} id Module ID.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when the WS call is successful.
|
|
*/
|
|
logViewLesson(id: number, password?: string, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: id
|
|
};
|
|
|
|
if (typeof password == 'string') {
|
|
params.password = password;
|
|
}
|
|
|
|
return site.write('mod_lesson_view_lesson', params).then((result) => {
|
|
if (!result.status) {
|
|
return Promise.reject(null);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Process a lesson page, saving its data.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {number} courseId Course ID the lesson belongs to.
|
|
* @param {any} pageData Result of getPageData for the page to process.
|
|
* @param {any} data Data to save.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {boolean} [offline] Whether it's offline mode.
|
|
* @param {any} [accessInfo] Result of get access info. Required if offline is true.
|
|
* @param {any} [jumps] Result of get pages possible jumps. Required if offline is true.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when done.
|
|
*/
|
|
processPage(lesson: any, courseId: number, pageData: any, data: any, password?: string, review?: boolean, offline?: boolean,
|
|
accessInfo?: boolean, jumps?: any, siteId?: string): Promise<any> {
|
|
|
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
|
|
|
const page = pageData.page,
|
|
pageId = page.id;
|
|
let result,
|
|
pageIndex;
|
|
|
|
if (offline) {
|
|
// Get the list of pages of the lesson.
|
|
return this.getPages(lesson.id, password, true, false, siteId).then((pages) => {
|
|
pageIndex = this.createPagesIndex(pages);
|
|
|
|
if (pageData.answers.length) {
|
|
return this.recordAttempt(lesson, courseId, pageData, data, review, accessInfo, jumps, pageIndex, siteId);
|
|
} else {
|
|
// The page has no answers so we will just progress to the next page (as set by newpageid).
|
|
return {
|
|
nodefaultresponse: true,
|
|
newpageid: data.newpageid
|
|
};
|
|
}
|
|
}).then((res) => {
|
|
result = res;
|
|
result.newpageid = this.getNewPageId(pageData.page.id, result.newpageid, jumps);
|
|
|
|
// Calculate some needed offline data.
|
|
return this.calculateOfflineData(lesson, accessInfo, password, review, pageIndex, siteId);
|
|
}).then((calculatedData) => {
|
|
// Add some default data to match the WS response.
|
|
result.warnings = [];
|
|
result.displaymenu = pageData.displaymenu; // Keep the same value since we can't calculate it in offline.
|
|
result.messages = this.getPageProcessMessages(lesson, accessInfo, result, review, jumps);
|
|
Object.assign(result, calculatedData);
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
return this.processPageOnline(lesson.id, pageId, data, password, review, siteId);
|
|
}
|
|
|
|
/**
|
|
* Process a lesson page, saving its data. It will fail if offline or cannot connect.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {number} pageId Page ID.
|
|
* @param {any} data Data to save.
|
|
* @param {string} [password] Lesson password (if any).
|
|
* @param {boolean} [review] If the user wants to review just after finishing (1 hour margin).
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
|
|
*/
|
|
processPageOnline(lessonId: number, pageId: number, data: any, password?: string, review?: boolean, siteId?: string)
|
|
: Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: lessonId,
|
|
pageid: pageId,
|
|
data: this.utils.objectToArrayOfObjects(data, 'name', 'value', true),
|
|
review: review ? 1 : 0
|
|
};
|
|
|
|
if (typeof password == 'string') {
|
|
params.password = password;
|
|
}
|
|
|
|
return site.write('mod_lesson_process_page', params);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Records an attempt on a certain page.
|
|
* Based on Moodle's record_attempt.
|
|
*
|
|
* @param {any} lesson Lesson.
|
|
* @param {number} courseId Course ID the lesson belongs to.
|
|
* @param {any} pageData Result of getPageData for the page to process.
|
|
* @param {any} data Data to save.
|
|
* @param {boolean} review If the user wants to review just after finishing (1 hour margin).
|
|
* @param {any} accessInfo Result of get access info.
|
|
* @param {any} jumps Result of get pages possible jumps.
|
|
* @param {any} pageIndex Object containing all the pages indexed by ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<AddonModLessonRecordAttemptResult>} Promise resolved with the result.
|
|
*/
|
|
protected recordAttempt(lesson: any, courseId: number, pageData: any, data: any, review: boolean, accessInfo: any, jumps: any,
|
|
pageIndex: any, siteId?: string): Promise<AddonModLessonRecordAttemptResult> {
|
|
|
|
// Check the user answer. Each page type has its own implementation.
|
|
const result: AddonModLessonRecordAttemptResult = this.checkAnswer(lesson, pageData, data, jumps, pageIndex),
|
|
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.
|
|
return this.lessonOfflineProvider.processPage(lesson.id, courseId, retake, pageData.page, data,
|
|
result.newpageid, result.answerid, false, result.userresponse, siteId).then(() => {
|
|
return result;
|
|
});
|
|
}
|
|
|
|
return Promise.resolve(result);
|
|
}
|
|
|
|
let promise = Promise.resolve(),
|
|
stop = false,
|
|
nAttempts;
|
|
|
|
result.attemptsremaining = 0;
|
|
result.maxattemptsreached = false;
|
|
|
|
if (result.noanswer) {
|
|
result.newpageid = pageData.page.id; // Display same page again.
|
|
result.feedback = this.translate.instant('addon.mod_lesson.noanswer');
|
|
} else {
|
|
if (!accessInfo.canmanage) {
|
|
// Get the number of attempts that have been made on this question for this student and retake.
|
|
promise = this.getQuestionsAttempts(lesson.id, retake, false, pageData.page.id, siteId).then((attempts) => {
|
|
nAttempts = attempts.online.length + attempts.offline.length;
|
|
|
|
// Check if they have reached (or exceeded) the maximum number of attempts allowed.
|
|
if (nAttempts >= lesson.maxattempts) {
|
|
result.maxattemptsreached = true;
|
|
result.feedback = this.translate.instant('addon.mod_lesson.maximumnumberofattemptsreached');
|
|
result.newpageid = AddonModLessonProvider.LESSON_NEXTPAGE;
|
|
stop = true; // Set stop to true to prevent further calculations.
|
|
|
|
return;
|
|
}
|
|
|
|
let subPromise;
|
|
|
|
// Only insert a record if we are not reviewing the lesson.
|
|
if (!review) {
|
|
if (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);
|
|
subPromise = this.lessonOfflineProvider.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 (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 > 1) { // Don't bother with message if only one attempt
|
|
result.attemptsremaining = lesson.maxattempts - nAttempts;
|
|
}
|
|
}
|
|
|
|
return subPromise;
|
|
});
|
|
}
|
|
|
|
promise = promise.then(() => {
|
|
if (stop) {
|
|
return;
|
|
}
|
|
|
|
// 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 = this.translate.instant('addon.mod_lesson.defaultessayresponse');
|
|
} else if (result.correctanswer) {
|
|
result.response = this.translate.instant('addon.mod_lesson.thatsthecorrectanswer');
|
|
} else {
|
|
result.response = this.translate.instant('addon.mod_lesson.thatsthewronganswer');
|
|
}
|
|
}
|
|
|
|
if (result.response) {
|
|
let subPromise;
|
|
|
|
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') {
|
|
subPromise = this.getQuestionsAttempts(lesson.id, retake, false, pageData.page.id, siteId)
|
|
.then((result) => {
|
|
nAttempts = result.online.length + result.offline.length;
|
|
});
|
|
} else {
|
|
subPromise = Promise.resolve();
|
|
}
|
|
|
|
subPromise.then(() => {
|
|
const messageId = nAttempts == 1 ? 'firstwrong' : 'secondpluswrong';
|
|
|
|
result.feedback = '<div class="box feedback">' +
|
|
this.translate.instant('addon.mod_lesson.' + messageId) + '</div>';
|
|
});
|
|
} else {
|
|
result.feedback = '';
|
|
subPromise = Promise.resolve();
|
|
}
|
|
|
|
let className = 'response';
|
|
if (result.correctanswer) {
|
|
className += ' correct';
|
|
} else if (!result.isessayquestion) {
|
|
className += ' incorrect';
|
|
}
|
|
|
|
return subPromise.then(() => {
|
|
result.feedback += '<div class="box generalbox boxaligncenter p-y-1">' + pageData.page.contents + '</div>';
|
|
result.feedback += '<div class="correctanswer generalbox"><em>' +
|
|
this.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) : [],
|
|
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, responseArray[i], className);
|
|
}
|
|
} else {
|
|
// Only 1 answer, add it to the table.
|
|
result.feedback = this.addAnswerAndResponseToFeedback(result.feedback, result.studentanswer,
|
|
result.studentanswerformat, result.response, className);
|
|
}
|
|
|
|
result.feedback += '</tbody></table></div></div>';
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return promise.then(() => {
|
|
return result;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove a password stored in DB.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when removed.
|
|
*/
|
|
removeStoredPassword(lessonId: number, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
return site.getDb().deleteRecords(AddonModLessonProvider.PASSWORD_TABLE, {lessonid: lessonId});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Store a password in DB.
|
|
*
|
|
* @param {number} lessonId Lesson ID.
|
|
* @param {string} password Password to store.
|
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
|
* @return {Promise<any>} Promise resolved when stored.
|
|
*/
|
|
storePassword(lessonId: number, password: string, siteId?: string): Promise<any> {
|
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
|
const entry = {
|
|
lessonid: lessonId,
|
|
password: password,
|
|
timemodified: Date.now()
|
|
};
|
|
|
|
return site.getDb().insertRecord(AddonModLessonProvider.PASSWORD_TABLE, 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 {any} pages Index of lesson pages, indexed by page ID. See createPagesIndex.
|
|
* @param {any} page Page to check.
|
|
* @param {any} validPages Valid pages, indexed by page ID.
|
|
* @param {number[]} viewedPagesIds List of viewed pages IDs.
|
|
* @return {number} Next page ID.
|
|
*/
|
|
validPageAndView(pages: any, page: any, validPages: any, 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) {
|
|
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;
|
|
}
|
|
}
|