3463 lines
129 KiB
TypeScript
3463 lines
129 KiB
TypeScript
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { Injectable } from '@angular/core';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
import { CoreEventsProvider } from '@providers/events';
|
|
import { CoreLoggerProvider } from '@providers/logger';
|
|
import { CoreSitesProvider, CoreSiteSchema, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } 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 { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
|
|
import { CoreSite } from '@classes/site';
|
|
import { AddonModLessonOfflineProvider } from './lesson-offline';
|
|
import { CoreCourseCommonModWSOptions } from '@core/course/providers/course';
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
nquestions: number;
|
|
|
|
/**
|
|
* Number of question attempts.
|
|
*/
|
|
attempts: number;
|
|
|
|
/**
|
|
* Max points possible.
|
|
*/
|
|
total: number;
|
|
|
|
/**
|
|
* Points earned by the student.
|
|
*/
|
|
earned: number;
|
|
|
|
/**
|
|
* Calculated percentage grade.
|
|
*/
|
|
grade: number;
|
|
|
|
/**
|
|
* Numer of manually graded questions.
|
|
*/
|
|
nmanual: number;
|
|
|
|
/**
|
|
* Point value for manually graded questions.
|
|
*/
|
|
manualpoints: number;
|
|
}
|
|
|
|
/**
|
|
* 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';
|
|
static DATA_SENT_EVENT = 'addon_mod_lesson_data_sent';
|
|
|
|
// 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 = '@^#|';
|
|
|
|
static LESSON_OTHER_ANSWERS = '@#wronganswer#@';
|
|
|
|
// Variables for database.
|
|
static PASSWORD_TABLE = 'addon_mod_lesson_password';
|
|
protected siteSchema: CoreSiteSchema = {
|
|
name: 'AddonModLessonProvider',
|
|
version: 1,
|
|
tables: [
|
|
{
|
|
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, private logHelper: CoreCourseLogHelperProvider,
|
|
private eventsProvider: CoreEventsProvider) {
|
|
this.logger = logger.getInstance('AddonModLessonProvider');
|
|
|
|
this.sitesProvider.registerSiteSchema(this.siteSchema);
|
|
}
|
|
|
|
/**
|
|
* Add an answer and its response to a feedback string (HTML).
|
|
*
|
|
* @param feedback The current feedback.
|
|
* @param answer Student answer.
|
|
* @param answerFormat Answer format.
|
|
* @param response Response.
|
|
* @param className Class to add to the response.
|
|
* @return New feedback.
|
|
*/
|
|
protected addAnswerAndResponseToFeedback(feedback: string, answer: string, answerFormat: number, response: string,
|
|
className: string): string {
|
|
|
|
// Add a table row containing the answer.
|
|
feedback += '<tr><td class="cell c0 lastcol">' + (answerFormat ? answer : 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 messages List of messages where to add the message.
|
|
* @param stringName The ID of the message to be translated. E.g. 'addon.mod_lesson.numberofpagesviewednotice'.
|
|
* @param stringParams The params of the message (if any).
|
|
*/
|
|
protected addMessage(messages: 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 result Result where to add the value.
|
|
* @param name Name of the property.
|
|
* @param value Value to add.
|
|
* @param addMessage Whether to add a message related to the value.
|
|
*/
|
|
protected addResultValueEolPage(result: 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 page Answer page.
|
|
* @return 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 page Answer page.
|
|
* @return 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 lesson Lesson.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the data.
|
|
*/
|
|
protected calculateOfflineData(lesson: any, options: AddonModLessonCalculateOfflineDataOptions = {})
|
|
: Promise<{reviewmode: boolean, progress: number, ongoingscore: string}> {
|
|
|
|
const accessInfo = options.accessInfo || {};
|
|
const reviewMode = options.review || accessInfo.reviewmode,
|
|
promises = [];
|
|
let ongoingMessage = '',
|
|
progress: number;
|
|
|
|
if (!accessInfo.canmanage) {
|
|
if (lesson.ongoing && !reviewMode) {
|
|
promises.push(this.getOngoingScoreMessage(lesson, accessInfo, options).then((message) => {
|
|
ongoingMessage = message;
|
|
}));
|
|
}
|
|
if (lesson.progressbar) {
|
|
const modOptions = {
|
|
cmId: lesson.coursemodule,
|
|
...options, // Include all options.
|
|
};
|
|
|
|
promises.push(this.calculateProgress(lesson.id, accessInfo, modOptions).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 lessonId Lesson ID.
|
|
* @param accessInfo Result of get access info.
|
|
* @param password Lesson password (if any).
|
|
* @param review If the user wants to review just after finishing (1 hour margin).
|
|
* @param pageIndex Object containing all the pages indexed by ID. If not defined, it will be calculated.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with a number: the progress (scale 0-100).
|
|
*/
|
|
calculateProgress(lessonId: number, accessInfo: any, options: AddonModLessonCalculateProgressOptions = {}): Promise<number> {
|
|
|
|
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
|
|
|
// Check if the user is reviewing the attempt.
|
|
if (options.review) {
|
|
return Promise.resolve(100);
|
|
}
|
|
|
|
const retake = accessInfo.attemptscount;
|
|
const commonOptions = {
|
|
cmId: options.cmId,
|
|
siteId: options.siteId,
|
|
};
|
|
let viewedPagesIds;
|
|
let promise;
|
|
|
|
if (options.pageIndex) {
|
|
promise = Promise.resolve();
|
|
} else {
|
|
// Retrieve the index.
|
|
promise = this.getPages(lessonId, {
|
|
cmId: options.cmId,
|
|
password: options.password,
|
|
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
|
siteId: options.siteId,
|
|
}).then((pages) => {
|
|
options.pageIndex = this.createPagesIndex(pages);
|
|
});
|
|
}
|
|
|
|
return promise.then(() => {
|
|
// Get the list of question pages attempted.
|
|
return this.getPagesIdsWithQuestionAttempts(lessonId, retake, commonOptions);
|
|
}).then((ids) => {
|
|
viewedPagesIds = ids;
|
|
|
|
// Get the list of viewed content pages.
|
|
return this.getContentPagesViewedIds(lessonId, retake, commonOptions);
|
|
}).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(options.pageIndex, options.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 lesson Lesson.
|
|
* @param pageData Result of getPageData for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param jumps Result of get pages possible jumps.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @return 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 pageData Result of getPageData for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param 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;
|
|
}
|
|
|
|
// The name was changed to "answer_editor" in 3.7. Before it was just "answer". Support both cases.
|
|
if (typeof data['answer_editor[text]'] != 'undefined') {
|
|
studentAnswer = data['answer_editor[text]'];
|
|
} else if (typeof data.answer_editor == 'object') {
|
|
studentAnswer = data.answer_editor.text;
|
|
} else 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 pageData Result of getPageData for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param 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 lesson Lesson.
|
|
* @param pageData Result of getPageData for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkAnswerMultichoice(lesson: 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 lesson Lesson.
|
|
* @param pageData Result of getPageData for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkAnswerNumerical(lesson: 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;
|
|
}
|
|
}
|
|
|
|
this.checkOtherAnswers(lesson, pageData, result);
|
|
}
|
|
|
|
/**
|
|
* Check a short answer.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageData Result of getPageData for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkAnswerShort(lesson: 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.
|
|
}
|
|
}
|
|
|
|
this.checkOtherAnswers(lesson, pageData, result);
|
|
|
|
result.userresponse = studentAnswer;
|
|
result.studentanswer = this.textUtils.s(studentAnswer); // Clean student answer as it goes to output.
|
|
}
|
|
|
|
/**
|
|
* Check a truefalse answer.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageData Result of getPageData for the page to process.
|
|
* @param data Data containing the user answer.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkAnswerTruefalse(lesson: 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check the "other answers" value.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageData Result of getPageData for the page to process.
|
|
* @param result Object where to store the result.
|
|
*/
|
|
protected checkOtherAnswers(lesson: any, pageData: any, result: AddonModLessonCheckAnswerResult): void {
|
|
// We could check here to see if we have a wrong answer jump to use.
|
|
if (result.answerid == 0) {
|
|
// Use the all other answers jump details if it is set up.
|
|
const lastAnswer = pageData.answers[pageData.answers.length - 1] || {};
|
|
|
|
// Double check that this is the OTHER_ANSWERS answer.
|
|
if (typeof lastAnswer.answer == 'string' &&
|
|
lastAnswer.answer.indexOf(AddonModLessonProvider.LESSON_OTHER_ANSWERS) != -1) {
|
|
result.newpageid = lastAnswer.jumpto;
|
|
result.response = lastAnswer.response;
|
|
|
|
if (lesson.custom) {
|
|
result.correctanswer = lastAnswer.score > 0;
|
|
}
|
|
result.answerid = lastAnswer.id;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a list of pages indexed by page ID based on a list of pages.
|
|
*
|
|
* @param pageList Result of get pages.
|
|
* @return 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 lesson Lesson.
|
|
* @param courseId Course ID the lesson belongs to.
|
|
* @param options Other options.
|
|
* @return Promise resolved in success, rejected otherwise.
|
|
*/
|
|
finishRetake(lesson: any, courseId: number, options: AddonModLessonFinishRetakeOptions = {}): Promise<any> {
|
|
|
|
if (options.offline) {
|
|
const retake = options.accessInfo.attemptscount;
|
|
const newOptions = {
|
|
cmId: lesson.coursemodule,
|
|
password: options.password,
|
|
review: options.review,
|
|
siteId: options.siteId,
|
|
};
|
|
|
|
return this.lessonOfflineProvider.finishRetake(lesson.id, courseId, retake, true, options.outOfTime, options.siteId)
|
|
.then(() => {
|
|
// Get the lesson grade.
|
|
return this.lessonGrade(lesson, retake, newOptions).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 && !options.accessInfo.canmanage) {
|
|
/* Before we calculate the custom score make sure they answered the minimum number of questions.
|
|
We only need to do this for custom scoring as we can not get the miniumum score the user should achieve.
|
|
If we are not using custom scoring (so all questions are valued as 1) then we simply check if they
|
|
answered more than the minimum questions, if not, we mark it out of the number specified in the minimum
|
|
questions setting - which is done in lesson_grade(). */
|
|
|
|
// Get the number of answers given.
|
|
if (gradeInfo.nquestions < lesson.minquestions) {
|
|
gradeLesson = false;
|
|
messageParams = {
|
|
nquestions: gradeInfo.nquestions,
|
|
minquestions: lesson.minquestions
|
|
};
|
|
this.addMessage(result.messages, 'addon.mod_lesson.numberofpagesviewednotice', {$a: messageParams});
|
|
}
|
|
}
|
|
|
|
if (!options.accessInfo.canmanage) {
|
|
if (gradeLesson) {
|
|
promises.push(this.calculateProgress(lesson.id, options.accessInfo, newOptions).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 (options.outOfTime) {
|
|
this.addResultValueEolPage(result, 'eolstudentoutoftimenoanswers', true, true);
|
|
}
|
|
} else {
|
|
this.addResultValueEolPage(result, 'welldone', true, true);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Display for teacher.
|
|
if (lesson.grade != CoreGradesProvider.TYPE_NONE) {
|
|
this.addResultValueEolPage(result, 'displayofgrade', true, true);
|
|
}
|
|
}
|
|
|
|
if (lesson.modattempts && options.accessInfo.canmanage) {
|
|
this.addResultValueEolPage(result, 'modattemptsnoteacher', true, true);
|
|
}
|
|
|
|
if (gradeLesson) {
|
|
this.addResultValueEolPage(result, 'gradelesson', 1);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
return this.finishRetakeOnline(lesson.id, options).then((response) => {
|
|
this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, {
|
|
lessonId: lesson.id,
|
|
type: 'finish',
|
|
courseId: courseId,
|
|
outOfTime: options.outOfTime,
|
|
review: options.review,
|
|
}, this.sitesProvider.getCurrentSiteId());
|
|
|
|
return response;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Finishes a retake. It will fail if offline or cannot connect.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved in success, rejected otherwise.
|
|
*/
|
|
finishRetakeOnline(lessonId: number, options: AddonModLessonFinishRetakeOnlineOptions = {}): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: lessonId,
|
|
outoftime: options.outOfTime ? 1 : 0,
|
|
review: options.review ? 1 : 0,
|
|
};
|
|
|
|
if (typeof options.password == 'string') {
|
|
params.password = options.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 lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the access information.
|
|
*/
|
|
getAccessInformation(lessonId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const params = {
|
|
lessonid: lessonId,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getAccessInformationCacheKey(lessonId),
|
|
updateFrequency: CoreSite.FREQUENCY_OFTEN,
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
return site.read('mod_lesson_get_lesson_access_information', params, preSets);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for access information WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getAccessInformationCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'accessInfo:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get content pages viewed in online and offline.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with an object with the online and offline viewed pages.
|
|
*/
|
|
getContentPagesViewed(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {})
|
|
: Promise<{online: any[], offline: any[]}> {
|
|
const promises = [],
|
|
type = AddonModLessonProvider.TYPE_STRUCTURE,
|
|
result = {
|
|
online: [],
|
|
offline: []
|
|
};
|
|
|
|
// Get the online pages.
|
|
promises.push(this.getContentPagesViewedOnline(lessonId, retake, options).then((pages) => {
|
|
result.online = pages;
|
|
}));
|
|
|
|
// Get the offline pages.
|
|
promises.push(this.lessonOfflineProvider.getRetakeAttemptsForType(lessonId, retake, type, options.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 lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @return Cache key.
|
|
*/
|
|
protected getContentPagesViewedCacheKey(lessonId: number, retake: number): string {
|
|
return this.getContentPagesViewedCommonCacheKey(lessonId) + ':' + retake;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get content pages viewed WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getContentPagesViewedCommonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'contentPagesViewed:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get IDS of content pages viewed in online and offline.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with list of IDs.
|
|
*/
|
|
getContentPagesViewedIds(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise<number[]> {
|
|
return this.getContentPagesViewed(lessonId, retake, options).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 lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the viewed pages.
|
|
*/
|
|
getContentPagesViewedOnline(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise<any[]> {
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const params = {
|
|
lessonid: lessonId,
|
|
lessonattempt: retake,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getContentPagesViewedCacheKey(lessonId, retake),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
return site.read('mod_lesson_get_content_pages_viewed', params, preSets).then((result) => {
|
|
return result.pages;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the last content page viewed.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the last content page viewed.
|
|
*/
|
|
getLastContentPageViewed(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
|
return this.getContentPagesViewed(lessonId, retake, options).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 lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the last page seen.
|
|
*/
|
|
getLastPageSeen(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise<number> {
|
|
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
|
|
|
let lastPageSeen: number;
|
|
|
|
// Get the last question answered.
|
|
return this.lessonOfflineProvider.getLastQuestionPageAttempt(lessonId, retake, options.siteId).then((answer) => {
|
|
if (answer) {
|
|
lastPageSeen = answer.newpageid;
|
|
}
|
|
|
|
// Now get the last content page viewed.
|
|
return this.getLastContentPageViewed(lessonId, retake, options).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 courseId Course ID.
|
|
* @param cmid Course module ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved when the lesson is retrieved.
|
|
*/
|
|
getLesson(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<any> {
|
|
return this.getLessonByField(courseId, 'coursemodule', cmId, options);
|
|
}
|
|
|
|
/**
|
|
* Get a Lesson with key=value. If more than one is found, only the first will be returned.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param key Name of the property to check.
|
|
* @param value Value to search.
|
|
* @param options Other options.
|
|
* @return Promise resolved when the lesson is retrieved.
|
|
*/
|
|
protected getLessonByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const params = {
|
|
courseids: [courseId],
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getLessonDataCacheKey(courseId),
|
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
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 courseId Course ID.
|
|
* @param id Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved when the lesson is retrieved.
|
|
*/
|
|
getLessonById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<any> {
|
|
return this.getLessonByField(courseId, 'id', id, options);
|
|
}
|
|
|
|
/**
|
|
* Get cache key for Lesson data WS calls.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getLessonDataCacheKey(courseId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'lesson:' + courseId;
|
|
}
|
|
|
|
/**
|
|
* Get a lesson protected with password.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the lesson.
|
|
*/
|
|
getLessonWithPassword(lessonId: number, options: AddonModLessonGetWithPasswordOptions = {}): Promise<any> {
|
|
const validatePassword = typeof options.validatePassword == 'undefined' ? true : options.validatePassword;
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: lessonId,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getLessonWithPasswordCacheKey(lessonId),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
if (typeof options.password == 'string') {
|
|
params.password = options.password;
|
|
}
|
|
|
|
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 lessonId Lesson ID.
|
|
* @return 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 pageId Current page ID.
|
|
* @param jumpTo The jumpto.
|
|
* @param jumps Result of get pages possible jumps.
|
|
* @return 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 lesson Lesson.
|
|
* @param accessInfo Result of get access info.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the ongoing score message.
|
|
*/
|
|
getOngoingScoreMessage(lesson: any, accessInfo: any, options: AddonModLessonGradeOptions = {}): Promise<string> {
|
|
|
|
if (accessInfo.canmanage) {
|
|
return Promise.resolve(this.translate.instant('addon.mod_lesson.teacherongoingwarning'));
|
|
} else {
|
|
let retake = accessInfo.attemptscount;
|
|
if (options.review) {
|
|
retake--;
|
|
}
|
|
|
|
return this.lessonGrade(lesson, retake, options).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 lesson Lesson.
|
|
* @param pageId Page ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the list of possible answers.
|
|
*/
|
|
protected getPageAnswers(lesson: any, pageId: number, options: AddonModLessonPwdReviewOptions = {}): Promise<any[]> {
|
|
return this.getPageData(lesson, pageId, {
|
|
includeContents: true,
|
|
...options, // Include all options.
|
|
readingStrategy: options.readingStrategy || CoreSitesReadingStrategy.PreferCache,
|
|
}).then((data) => {
|
|
return data.answers;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all the possible answers from a list of pages, indexed by answerId.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param pageIds List of page IDs.
|
|
* @param options Other options.
|
|
* @return Promise resolved with an object containing the answers.
|
|
*/
|
|
protected getPagesAnswers(lesson: any, pageIds: number[], options: AddonModLessonPwdReviewOptions = {}): Promise<any> {
|
|
|
|
const answers = {},
|
|
promises = [];
|
|
|
|
pageIds.forEach((pageId) => {
|
|
promises.push(this.getPageAnswers(lesson, pageId, options).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 lesson Lesson.
|
|
* @param pageId Page ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the page data.
|
|
*/
|
|
getPageData(lesson: any, pageId: number, options: AddonModLessonGetPageDataOptions = {}): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: lesson.id,
|
|
pageid: Number(pageId),
|
|
review: options.review ? 1 : 0,
|
|
returncontents: options.includeContents ? 1 : 0,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getPageDataCacheKey(lesson.id, pageId),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
if (typeof options.password == 'string') {
|
|
params.password = options.password;
|
|
}
|
|
|
|
if (options.review) {
|
|
// Force online mode in review.
|
|
preSets.getFromCache = false;
|
|
preSets.saveToCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_page_data', params, preSets).then((data) => {
|
|
if (preSets.omitExpires && options.accessInfo && data.page) {
|
|
// Offline mode and valid page. Calculate the data that might be affected.
|
|
return this.calculateOfflineData(lesson, options).then((calcData) => {
|
|
Object.assign(data, calcData);
|
|
|
|
return this.getPageViewMessages(lesson, options.accessInfo, data.page, options.jumps, {
|
|
password: options.password,
|
|
siteId: options.siteId,
|
|
});
|
|
}).then((messages) => {
|
|
data.messages = messages;
|
|
|
|
return data;
|
|
});
|
|
}
|
|
|
|
return data;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get page data WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param pageId Page ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getPageDataCacheKey(lessonId: number, pageId: number): string {
|
|
return this.getPageDataCommonCacheKey(lessonId) + ':' + pageId;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get page data WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getPageDataCommonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'pageData:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get lesson pages.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the pages.
|
|
*/
|
|
getPages(lessonId: number, options: AddonModLessonPwdReviewOptions = {}): Promise<any[]> {
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: lessonId,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getPagesCacheKey(lessonId),
|
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
if (typeof options.password == 'string') {
|
|
params.password = options.password;
|
|
}
|
|
|
|
return site.read('mod_lesson_get_pages', params, preSets).then((response) => {
|
|
return response.pages;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get pages WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getPagesCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'pages:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get possible jumps for a lesson.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the jumps.
|
|
*/
|
|
getPagesPossibleJumps(lessonId: number, options: CoreCourseCommonModWSOptions = {}): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const params = {
|
|
lessonid: lessonId,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getPagesPossibleJumpsCacheKey(lessonId),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
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 lessonId Lesson ID.
|
|
* @return 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 lesson Lesson.
|
|
* @param accessInfo Result of get access info.
|
|
* @param result Result of process page.
|
|
* @param review If the user wants to review just after finishing (1 hour margin).
|
|
* @param jumps Result of get pages possible jumps.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the IDs.
|
|
*/
|
|
getPagesIdsWithQuestionAttempts(lessonId: number, retake: number, options: AddonModLessonGetPagesIdsWithAttemptsOptions = {})
|
|
: Promise<number[]> {
|
|
|
|
return this.getQuestionsAttempts(lessonId, retake, options).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 lesson Lesson.
|
|
* @param accessInfo Result of get access info. Required if offline is true.
|
|
* @param page Page loaded.
|
|
* @param jumps Result of get pages possible jumps.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the list of messages.
|
|
*/
|
|
getPageViewMessages(lesson: any, accessInfo: any, page: any, jumps: any, options: AddonModLessonGetPageViewMessagesOptions = {})
|
|
: 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, options).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 (!options.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 lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the questions attempts.
|
|
*/
|
|
getQuestionsAttempts(lessonId: number, retake: number, options: AddonModLessonGetQuestionsAttemptsOptions = {})
|
|
: Promise<{online: any[], offline: any[]}> {
|
|
|
|
const promises = [],
|
|
result = {
|
|
online: [],
|
|
offline: []
|
|
};
|
|
|
|
promises.push(this.getQuestionsAttemptsOnline(lessonId, retake, options).then((attempts) => {
|
|
result.online = attempts;
|
|
}));
|
|
|
|
promises.push(this.lessonOfflineProvider.getQuestionsAttempts(lessonId, retake, options.correct, options.pageId,
|
|
options.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 lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param userId User ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getQuestionsAttemptsCacheKey(lessonId: number, retake: number, userId: number): string {
|
|
return this.getQuestionsAttemptsCommonCacheKey(lessonId) + ':' + userId + ':' + retake;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get questions attempts WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getQuestionsAttemptsCommonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'questionsAttempts:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get questions attempts from the site.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the questions attempts.
|
|
*/
|
|
getQuestionsAttemptsOnline(lessonId: number, retake: number, options: AddonModLessonGetQuestionsAttemptsOptions = {})
|
|
: Promise<any[]> {
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const userId = options.userId || site.getUserId();
|
|
|
|
// Don't pass "pageId" and "correct" params, they will be filtered locally.
|
|
const params = {
|
|
lessonid: lessonId,
|
|
attempt: retake,
|
|
userid: userId,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getQuestionsAttemptsCacheKey(lessonId, retake, userId),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
return site.read('mod_lesson_get_questions_attempts', params, preSets).then((response) => {
|
|
if (options.pageId || options.correct) {
|
|
// Filter the attempts.
|
|
return response.attempts.filter((attempt) => {
|
|
if (options.correct && !attempt.correct) {
|
|
return false;
|
|
}
|
|
|
|
if (options.pageId && attempt.pageid != options.pageId) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return response.attempts;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the overview of retakes in a lesson (named "attempts overview" in Moodle).
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the retakes overview.
|
|
*/
|
|
getRetakesOverview(lessonId: number, options: AddonModLessonGroupOptions = {}): Promise<any> {
|
|
|
|
const groupId = options.groupId || 0;
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const params = {
|
|
lessonid: lessonId,
|
|
groupid: groupId,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getRetakesOverviewCacheKey(lessonId, groupId),
|
|
updateFrequency: CoreSite.FREQUENCY_OFTEN,
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
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 lessonId Lesson ID.
|
|
* @param groupId Group ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getRetakesOverviewCacheKey(lessonId: number, groupId: number): string {
|
|
return this.getRetakesOverviewCommonCacheKey(lessonId) + ':' + groupId;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get retakes overview WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getRetakesOverviewCommonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'retakesOverview:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get a password stored in DB.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with password on success, rejected otherwise.
|
|
*/
|
|
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 pages Index of lesson pages, indexed by page ID. See createPagesIndex.
|
|
* @param pageId Page ID to get subpages of.
|
|
* @param end An array of LESSON_PAGE_* types that signify an end of the subtype.
|
|
* @return List of subpages.
|
|
*/
|
|
getSubpagesOf(pages: 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 lessonId Lesson ID.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the pages.
|
|
*/
|
|
getTimers(lessonId: number, options: AddonModLessonUserOptions = {}): Promise<any[]> {
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const userId = options.userId || site.getUserId();
|
|
|
|
const params = {
|
|
lessonid: lessonId,
|
|
userid: userId,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getTimersCacheKey(lessonId, userId),
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
return site.read('mod_lesson_get_user_timers', params, preSets).then((response) => {
|
|
return response.timers;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get timers WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param userId User ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getTimersCacheKey(lessonId: number, userId: number): string {
|
|
return this.getTimersCommonCacheKey(lessonId) + ':' + userId;
|
|
}
|
|
|
|
/**
|
|
* Get common cache key for get timers WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getTimersCommonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'timers:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get the list of used answers (with valid answer) in a multichoice question page.
|
|
*
|
|
* @param pageData Result of getPageData for the page to process.
|
|
* @return 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 data Data containing the user answer.
|
|
* @return 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 data Data containing the user answer.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param retake Retake number
|
|
* @param options Other options.
|
|
* @return Promise resolved with the retake data.
|
|
*/
|
|
getUserRetake(lessonId: number, retake: number, options: AddonModLessonUserOptions = {}): Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const userId = options.userId || site.getUserId();
|
|
|
|
const params = {
|
|
lessonid: lessonId,
|
|
userid: userId,
|
|
lessonattempt: retake,
|
|
};
|
|
const preSets = {
|
|
cacheKey: this.getUserRetakeCacheKey(lessonId, userId, retake),
|
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
|
component: AddonModLessonProvider.COMPONENT,
|
|
componentId: options.cmId,
|
|
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
return site.read('mod_lesson_get_user_attempt', params, preSets);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get user retake WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param userId User ID.
|
|
* @param retake Retake number
|
|
* @return Cache key.
|
|
*/
|
|
protected getUserRetakeCacheKey(lessonId: number, userId: number, retake: number): string {
|
|
return this.getUserRetakeUserCacheKey(lessonId, userId) + ':' + retake;
|
|
}
|
|
|
|
/**
|
|
* Get user cache key for get user retake WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param userId User ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getUserRetakeUserCacheKey(lessonId: number, userId: number): string {
|
|
return this.getUserRetakeLessonCacheKey(lessonId) + ':' + userId;
|
|
}
|
|
|
|
/**
|
|
* Get lesson cache key for get user retake WS calls.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getUserRetakeLessonCacheKey(lessonId: number): string {
|
|
return this.ROOT_CACHE_KEY + 'userRetake:' + lessonId;
|
|
}
|
|
|
|
/**
|
|
* Get the prevent access reason to display for a certain lesson.
|
|
*
|
|
* @param info Lesson access info.
|
|
* @param ignorePassword Whether password protected reason should be ignored (user already entered the password).
|
|
* @param isReview Whether user is reviewing a retake.
|
|
* @return Prevent access reason.
|
|
*/
|
|
getPreventAccessReason(info: any, ignorePassword?: boolean, isReview?: boolean): any {
|
|
let result;
|
|
|
|
if (info && info.preventaccessreasons) {
|
|
for (let i = 0; i < info.preventaccessreasons.length; i++) {
|
|
const entry = info.preventaccessreasons[i];
|
|
|
|
if (entry.reason == 'lessonopen' || entry.reason == 'lessonclosed') {
|
|
// Time restrictions are the most prioritary, return it.
|
|
return entry;
|
|
} else if (entry.reason == 'passwordprotectedlesson') {
|
|
if (!ignorePassword) {
|
|
// Treat password before all other reasons.
|
|
result = entry;
|
|
}
|
|
} else if (entry.reason == 'noretake' && isReview) {
|
|
// Ignore noretake error when reviewing.
|
|
} else if (!result) {
|
|
// Rest of cases, just return any of them.
|
|
result = entry;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Check if a jump is correct.
|
|
* Based in Moodle's jumpto_is_correct.
|
|
*
|
|
* @param pageId ID of the page from which you are jumping from.
|
|
* @param jumpTo The jumpto number.
|
|
* @param pageIndex Object containing all the pages indexed by ID. See createPagesIndex.
|
|
* @return Whether jump is correct.
|
|
*/
|
|
jumptoIsCorrect(pageId: number, jumpTo: number, pageIndex: 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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 courseId Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param pageId Page ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param siteId Site ID. If not defined, current site..
|
|
* @param userId User ID. If not defined, site's user.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param groupId Group ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @param userId User ID. If not defined, site's current user.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
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 lessonId Lesson ID.
|
|
* @param retake Retake number.
|
|
* @param userId User ID. Undefined for current user.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param userId User ID. Undefined for current user.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
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 lesson Lesson.
|
|
* @param pageId The page ID.
|
|
* @param answer The answer to check.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @return Whether the answer is correct.
|
|
*/
|
|
protected isAnswerCorrect(lesson: 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 lesson Lesson.
|
|
* @return Whether offline is enabled.
|
|
*/
|
|
isLessonOffline(lesson: any): boolean {
|
|
return !!lesson.allowofflineattempts;
|
|
}
|
|
|
|
/**
|
|
* Check if a lesson is password protected based in the access info.
|
|
*
|
|
* @param info Lesson access info.
|
|
* @return Whether the lesson is password protected.
|
|
*/
|
|
isPasswordProtected(info: 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 siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
|
|
*/
|
|
isPluginEnabled(siteId?: string): Promise<boolean> {
|
|
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 type Type of the page.
|
|
* @return True if question page, false if content page.
|
|
*/
|
|
isQuestionPage(type: number): boolean {
|
|
return type == AddonModLessonProvider.TYPE_QUESTION;
|
|
}
|
|
|
|
/**
|
|
* Start or continue a retake.
|
|
*
|
|
* @param id Lesson ID.
|
|
* @param password Lesson password (if any).
|
|
* @param pageId Page id to continue from (only when continuing a retake).
|
|
* @param review If the user wants to review just after finishing (1 hour margin).
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the WS call is successful.
|
|
*/
|
|
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).then((response) => {
|
|
this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, {
|
|
lessonId: id,
|
|
type: 'launch'
|
|
}, this.sitesProvider.getCurrentSiteId());
|
|
|
|
return response;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if the user left during a timed session.
|
|
*
|
|
* @param info Lesson access info.
|
|
* @return True if left during timed, false otherwise.
|
|
*/
|
|
leftDuringTimed(info: 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 jumps Result of get pages possible jumps.
|
|
* @return 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 lesson Lesson.
|
|
* @param retake Retake number.
|
|
* @param options Other options.
|
|
* @return Promise resolved with the grade data.
|
|
*/
|
|
lessonGrade(lesson: any, retake: number, options: AddonModLessonGradeOptions = {}): 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, {
|
|
cmId: lesson.coursemodule,
|
|
siteId: options.siteId,
|
|
userId: options.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 (!options.pageIndex) {
|
|
promise = this.getPages(lesson.id, {
|
|
password: options.password,
|
|
cmId: lesson.coursemodule,
|
|
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
|
siteId: options.siteId,
|
|
}).then((pages) => {
|
|
options.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, options);
|
|
}).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 (options.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 (options.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 id Module ID.
|
|
* @param password Lesson password (if any).
|
|
* @param name Name of the assign.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when the WS call is successful.
|
|
*/
|
|
logViewLesson(id: number, password?: string, name?: 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 this.logHelper.logSingle('mod_lesson_view_lesson', params, AddonModLessonProvider.COMPONENT, id, name,
|
|
'lesson', {}, siteId);
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
* Process a lesson page, saving its data.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param courseId Course ID the lesson belongs to.
|
|
* @param pageData Result of getPageData for the page to process.
|
|
* @param data Data to save.
|
|
* @param options Other options.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
processPage(lesson: any, courseId: number, pageData: any, data: any, options: AddonModLessonProcessPageOptions = {})
|
|
: Promise<any> {
|
|
|
|
options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId();
|
|
|
|
const page = pageData.page,
|
|
pageId = page.id;
|
|
let result,
|
|
pageIndex;
|
|
|
|
if (options.offline) {
|
|
// Get the list of pages of the lesson.
|
|
return this.getPages(lesson.id, {
|
|
cmId: lesson.coursemodule,
|
|
password: options.password,
|
|
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
|
siteId: options.siteId,
|
|
}).then((pages) => {
|
|
pageIndex = this.createPagesIndex(pages);
|
|
|
|
if (pageData.answers.length) {
|
|
return this.recordAttempt(lesson, courseId, pageData, data, options.review, options.accessInfo, options.jumps,
|
|
pageIndex, options.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, options.jumps);
|
|
|
|
// Calculate some needed offline data.
|
|
return this.calculateOfflineData(lesson, {
|
|
accessInfo: options.accessInfo,
|
|
password: options.password,
|
|
review: options.review,
|
|
pageIndex,
|
|
siteId: options.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, options.accessInfo, result, options.review, options.jumps);
|
|
result.sent = false;
|
|
Object.assign(result, calculatedData);
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
return this.processPageOnline(lesson.id, pageId, data, options).then((response) => {
|
|
this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, {
|
|
lessonId: lesson.id,
|
|
type: 'process',
|
|
courseId: courseId,
|
|
pageId: pageId,
|
|
review: options.review,
|
|
}, this.sitesProvider.getCurrentSiteId());
|
|
|
|
response.sent = true;
|
|
|
|
return response;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Process a lesson page, saving its data. It will fail if offline or cannot connect.
|
|
*
|
|
* @param lessonId Lesson ID.
|
|
* @param pageId Page ID.
|
|
* @param data Data to save.
|
|
* @param options Other options.
|
|
* @return Promise resolved in success, rejected otherwise.
|
|
*/
|
|
processPageOnline(lessonId: number, pageId: number, data: any, options: AddonModLessonProcessPageOnlineOptions = {})
|
|
: Promise<any> {
|
|
|
|
return this.sitesProvider.getSite(options.siteId).then((site) => {
|
|
const params: any = {
|
|
lessonid: lessonId,
|
|
pageid: pageId,
|
|
data: this.utils.objectToArrayOfObjects(data, 'name', 'value', true),
|
|
review: options.review ? 1 : 0,
|
|
};
|
|
|
|
if (typeof options.password == 'string') {
|
|
params.password = options.password;
|
|
}
|
|
|
|
return site.write('mod_lesson_process_page', params);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Records an attempt on a certain page.
|
|
* Based on Moodle's record_attempt.
|
|
*
|
|
* @param lesson Lesson.
|
|
* @param courseId Course ID the lesson belongs to.
|
|
* @param pageData Result of getPageData for the page to process.
|
|
* @param data Data to save.
|
|
* @param review If the user wants to review just after finishing (1 hour margin).
|
|
* @param accessInfo Result of get access info.
|
|
* @param jumps Result of get pages possible jumps.
|
|
* @param pageIndex Object containing all the pages indexed by ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with the result.
|
|
*/
|
|
protected 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, {
|
|
cmId: lesson.coursemodule,
|
|
pageId: 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, {
|
|
cmId: lesson.coursemodule,
|
|
pageId: 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 lessonId Lesson ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 lessonId Lesson ID.
|
|
* @param password Password to store.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return 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 pages Index of lesson pages, indexed by page ID. See createPagesIndex.
|
|
* @param page Page to check.
|
|
* @param validPages Valid pages, indexed by page ID.
|
|
* @param viewedPagesIds List of viewed pages IDs.
|
|
* @return Next page ID.
|
|
*/
|
|
validPageAndView(pages: 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Common options including a group ID.
|
|
*/
|
|
export type AddonModLessonGroupOptions = CoreCourseCommonModWSOptions & {
|
|
groupId?: number; // The group to get. If not defined, all participants.
|
|
};
|
|
|
|
/**
|
|
* Common options including a group ID.
|
|
*/
|
|
export type AddonModLessonUserOptions = CoreCourseCommonModWSOptions & {
|
|
userId?: number; // User ID. If not defined, site's current user.
|
|
};
|
|
|
|
/**
|
|
* Common options including a password.
|
|
*/
|
|
export type AddonModLessonPasswordOptions = CoreCourseCommonModWSOptions & {
|
|
password?: string; // Lesson password (if any).
|
|
};
|
|
|
|
/**
|
|
* Common options including password and review.
|
|
*/
|
|
export type AddonModLessonPwdReviewOptions = AddonModLessonPasswordOptions & {
|
|
review?: boolean; // If the user wants to review just after finishing (1 hour margin).
|
|
};
|
|
|
|
/**
|
|
* Options to pass to get lesson with password.
|
|
*/
|
|
export type AddonModLessonGetWithPasswordOptions = AddonModLessonPasswordOptions & {
|
|
validatePassword?: boolean; // Defauls to true. If true, the function will fail if the password is wrong.
|
|
// If false, it will return a lesson with the basic data if password is wrong.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to calculateProgress.
|
|
*/
|
|
export type AddonModLessonCalculateProgressBasicOptions = {
|
|
password?: string; // Lesson password (if any).
|
|
review?: boolean; // If the user wants to review just after finishing (1 hour margin).
|
|
pageIndex?: any; // Object containing all the pages indexed by ID. If not provided, it will be calculated.
|
|
siteId?: string; // Site ID. If not defined, current site.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to calculateProgress.
|
|
*/
|
|
export type AddonModLessonCalculateProgressOptions = AddonModLessonCalculateProgressBasicOptions & {
|
|
cmId?: number; // Module ID.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to lessonGrade.
|
|
*/
|
|
export type AddonModLessonGradeOptions = AddonModLessonCalculateProgressBasicOptions & {
|
|
userId?: number; // User ID. If not defined, site's user.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to calculateOfflineData.
|
|
*/
|
|
export type AddonModLessonCalculateOfflineDataOptions = AddonModLessonCalculateProgressBasicOptions & {
|
|
accessInfo?: any; // Result of get access info.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to get page data.
|
|
*/
|
|
export type AddonModLessonGetPageDataOptions = AddonModLessonPwdReviewOptions & {
|
|
includeContents?: boolean; // Include the page rendered contents.
|
|
accessInfo?: any; // Result of get access info. Required if offline is true.
|
|
jumps?: any; // Result of get pages possible jumps. Required if offline is true.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to get page data.
|
|
*/
|
|
export type AddonModLessonGetPageViewMessagesOptions = {
|
|
password?: string; // Lesson password (if any).
|
|
review?: boolean; // If the user wants to review just after finishing (1 hour margin).
|
|
siteId?: string; // Site ID. If not defined, current site.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to get questions attempts.
|
|
*/
|
|
export type AddonModLessonGetQuestionsAttemptsOptions = CoreCourseCommonModWSOptions & {
|
|
correct?: boolean; // True to only fetch correct attempts, false to get them all.
|
|
pageId?: number; // If defined, only get attempts on this page.
|
|
userId?: number; // User ID. If not defined, site's user.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to getPagesIdsWithQuestionAttempts.
|
|
*/
|
|
export type AddonModLessonGetPagesIdsWithAttemptsOptions = CoreCourseCommonModWSOptions & {
|
|
correct?: boolean; // True to only fetch correct attempts, false to get them all.
|
|
userId?: number; // User ID. If not defined, site's user.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to processPageOnline.
|
|
*/
|
|
export type AddonModLessonProcessPageOnlineOptions = {
|
|
password?: string; // Lesson password (if any).
|
|
review?: boolean; // If the user wants to review just after finishing (1 hour margin).
|
|
siteId?: string; // Site ID. If not defined, current site.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to processPage.
|
|
*/
|
|
export type AddonModLessonProcessPageOptions = AddonModLessonProcessPageOnlineOptions & {
|
|
offline?: boolean; // Whether it's offline mode.
|
|
accessInfo?: any; // Result of get access info. Required if offline is true.
|
|
jumps?: any; // Result of get pages possible jumps. Required if offline is true.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to finishRetakeOnline.
|
|
*/
|
|
export type AddonModLessonFinishRetakeOnlineOptions = {
|
|
password?: string; // Lesson password (if any).
|
|
outOfTime?: boolean; // Whether the user ran out of time.
|
|
review?: boolean; // If the user wants to review just after finishing (1 hour margin).
|
|
siteId?: string; // Site ID. If not defined, current site.
|
|
};
|
|
|
|
/**
|
|
* Options to pass to finishRetake.
|
|
*/
|
|
export type AddonModLessonFinishRetakeOptions = AddonModLessonFinishRetakeOnlineOptions & {
|
|
offline?: boolean; // Whether it's offline mode.
|
|
accessInfo?: any; // Result of get access info. Required if offline is true.
|
|
};
|