MOBILE-3648 lesson: Implement base services
parent
3796654443
commit
2ff5883026
|
@ -23,6 +23,7 @@ import { AddonCalendarModule } from './calendar/calendar.module';
|
||||||
import { AddonNotificationsModule } from './notifications/notifications.module';
|
import { AddonNotificationsModule } from './notifications/notifications.module';
|
||||||
import { AddonMessageOutputModule } from './messageoutput/messageoutput.module';
|
import { AddonMessageOutputModule } from './messageoutput/messageoutput.module';
|
||||||
import { AddonMessagesModule } from './messages/messages.module';
|
import { AddonMessagesModule } from './messages/messages.module';
|
||||||
|
import { AddonModModule } from './mod/mod.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -35,6 +36,7 @@ import { AddonMessagesModule } from './messages/messages.module';
|
||||||
AddonUserProfileFieldModule,
|
AddonUserProfileFieldModule,
|
||||||
AddonNotificationsModule,
|
AddonNotificationsModule,
|
||||||
AddonMessageOutputModule,
|
AddonMessageOutputModule,
|
||||||
|
AddonModModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AddonsModule {}
|
export class AddonsModule {}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
{
|
||||||
|
"answer": "Answer",
|
||||||
|
"attempt": "Attempt: {{$a}}",
|
||||||
|
"attemptheader": "Attempt",
|
||||||
|
"attemptsremaining": "You have {{$a}} attempt(s) remaining",
|
||||||
|
"averagescore": "Average score",
|
||||||
|
"averagetime": "Average time",
|
||||||
|
"branchtable": "Content",
|
||||||
|
"cannotfindattempt": "Error: could not find attempt",
|
||||||
|
"cannotfinduser": "Error: could not find users",
|
||||||
|
"clusterjump": "Unseen question within a cluster",
|
||||||
|
"completed": "Completed",
|
||||||
|
"congratulations": "Congratulations - end of lesson reached",
|
||||||
|
"continue": "Continue",
|
||||||
|
"continuetonextpage": "Continue to next page.",
|
||||||
|
"defaultessayresponse": "Your essay will be graded by your teacher.",
|
||||||
|
"detailedstats": "Detailed statistics",
|
||||||
|
"didnotanswerquestion": "Did not answer this question.",
|
||||||
|
"displayofgrade": "Display of grade (for students only)",
|
||||||
|
"displayscorewithessays": "<p>You earned {{$a.score}} out of {{$a.tempmaxgrade}} for the automatically graded questions.</p>\n<p>Your {{$a.essayquestions}} essay question(s) will be graded and added into your final score at a later date.</p>\n<p>Your current grade without the essay question(s) is {{$a.score}} out of {{$a.grade}}.</p>",
|
||||||
|
"displayscorewithoutessays": "Your score is {{$a.score}} (out of {{$a.grade}}).",
|
||||||
|
"emptypassword": "Password cannot be empty",
|
||||||
|
"enterpassword": "Please enter the password:",
|
||||||
|
"eolstudentoutoftimenoanswers": "You did not answer any questions. You have received a 0 for this lesson.",
|
||||||
|
"errorprefetchrandombranch": "This lesson contains a jump to a random content page. It can't be attempted in the app until it has been started in a web browser.",
|
||||||
|
"errorreviewretakenotlast": "This attempt can no longer be reviewed because another attempt has been finished.",
|
||||||
|
"finish": "Finish",
|
||||||
|
"finishretakeoffline": "This attempt was finished offline.",
|
||||||
|
"firstwrong": "You have answered incorrectly. Would you like to attempt the question again? (If you now answer the question correctly, it will not count towards your final score.)",
|
||||||
|
"gotoendoflesson": "Go to the end of the lesson",
|
||||||
|
"grade": "Grade",
|
||||||
|
"highscore": "High score",
|
||||||
|
"hightime": "High time",
|
||||||
|
"leftduringtimed": "You have left during a timed lesson.<br />Please click on Continue to restart the lesson.",
|
||||||
|
"leftduringtimednoretake": "You have left during a timed lesson and you are<br />not allowed to retake or continue the lesson.",
|
||||||
|
"lessonmenu": "Lesson menu",
|
||||||
|
"lessonstats": "Lesson statistics",
|
||||||
|
"linkedmedia": "Linked media",
|
||||||
|
"loginfail": "Login failed, please try again...",
|
||||||
|
"lowscore": "Low score",
|
||||||
|
"lowtime": "Low time",
|
||||||
|
"maximumnumberofattemptsreached": "Maximum number of attempts reached - Moving to next page",
|
||||||
|
"modattemptsnoteacher": "Student review only works for students.",
|
||||||
|
"modulenameplural": "Lessons",
|
||||||
|
"noanswer": "One or more questions have no answer given. Please go back and submit an answer.",
|
||||||
|
"nolessonattempts": "No attempts have been made on this lesson.",
|
||||||
|
"nolessonattemptsgroup": "No attempts have been made by {{$a}} group members on this lesson.",
|
||||||
|
"notcompleted": "Not completed",
|
||||||
|
"numberofcorrectanswers": "Number of correct answers: {{$a}}",
|
||||||
|
"numberofpagesviewed": "Number of questions answered: {{$a}}",
|
||||||
|
"numberofpagesviewednotice": "Number of questions answered: {{$a.nquestions}} (You should answer at least {{$a.minquestions}})",
|
||||||
|
"ongoingcustom": "You have earned {{$a.score}} point(s) out of {{$a.currenthigh}} point(s) thus far.",
|
||||||
|
"ongoingnormal": "You have answered {{$a.correct}} correctly out of {{$a.viewed}} attempts.",
|
||||||
|
"or": "OR",
|
||||||
|
"overview": "Overview",
|
||||||
|
"preview": "Preview",
|
||||||
|
"progressbarteacherwarning2": "You will not see the progress bar because you can edit this lesson",
|
||||||
|
"progresscompleted": "You have completed {{$a}}% of the lesson",
|
||||||
|
"question": "Question",
|
||||||
|
"rawgrade": "Raw grade",
|
||||||
|
"reports": "Reports",
|
||||||
|
"response": "Response",
|
||||||
|
"retakefinishedinsync": "An offline attempt was synchronised. Do you want to review it?",
|
||||||
|
"retakelabelfull": "{{retake}}: {{grade}} {{timestart}} ({{duration}})",
|
||||||
|
"retakelabelshort": "{{retake}}: {{grade}} {{timestart}}",
|
||||||
|
"review": "Review",
|
||||||
|
"reviewlesson": "Review lesson",
|
||||||
|
"reviewquestionback": "Yes, I'd like to try again",
|
||||||
|
"reviewquestioncontinue": "No, I just want to go on to the next question",
|
||||||
|
"secondpluswrong": "Not quite. Would you like to try again?",
|
||||||
|
"submit": "Submit",
|
||||||
|
"teacherjumpwarning": "A {{$a.cluster}} jump or an {{$a.unseen}} jump is being used in this lesson. The next page jump will be used instead. Log in as a student to test these jumps.",
|
||||||
|
"teacherongoingwarning": "The ongoing score is only displayed for the student. Log in as a student to test the ongoing score.",
|
||||||
|
"teachertimerwarning": "Timer only works for students. Test the timer by logging in as a student.",
|
||||||
|
"thatsthecorrectanswer": "That's the correct answer",
|
||||||
|
"thatsthewronganswer": "That's the wrong answer",
|
||||||
|
"timeremaining": "Time remaining",
|
||||||
|
"timetaken": "Time taken",
|
||||||
|
"unseenpageinbranch": "Unseen question within a content page",
|
||||||
|
"warningretakefinished": "The attempt was finished on the site.",
|
||||||
|
"welldone": "Well done!",
|
||||||
|
"youhaveseen": "You have seen more than one page of this lesson already.<br />Do you want to start at the last page you saw?",
|
||||||
|
"youranswer": "Your answer",
|
||||||
|
"yourcurrentgradeisoutof": "Your current grade is {{$a.grade}} out of {{$a.total}}",
|
||||||
|
"youshouldview": "You should answer at least: {{$a}}"
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
// (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 { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||||
|
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/lesson';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CORE_SITE_SCHEMAS,
|
||||||
|
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA],
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModLessonModule {}
|
|
@ -0,0 +1,185 @@
|
||||||
|
// (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 { CoreSiteSchema } from '@services/sites';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database variables for AddonModLessonOfflineProvider.
|
||||||
|
*/
|
||||||
|
export const PASSWORD_TABLE_NAME = 'addon_mod_lesson_password';
|
||||||
|
export const SITE_SCHEMA: CoreSiteSchema = {
|
||||||
|
name: 'AddonModLessonProvider',
|
||||||
|
version: 1,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: PASSWORD_TABLE_NAME,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'lessonid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'password',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timemodified',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database variables for AddonModLessonOfflineProvider.
|
||||||
|
*/
|
||||||
|
export const RETAKES_TABLE_NAME = 'addon_mod_lesson_retakes';
|
||||||
|
export const PAGE_ATTEMPTS_TABLE_NAME = 'addon_mod_lesson_page_attempts';
|
||||||
|
export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
|
||||||
|
name: 'AddonModLessonOfflineProvider',
|
||||||
|
version: 1,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: RETAKES_TABLE_NAME,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'lessonid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
primaryKey: true, // Only 1 offline retake per lesson.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'retake', // Retake number.
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'courseid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finished',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'outoftime',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timemodified',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lastquestionpage',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: PAGE_ATTEMPTS_TABLE_NAME,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'lessonid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'retake', // Retake number.
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pageid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timemodified',
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'courseid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'data',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'newpageid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'correct',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'answerid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useranswer',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// A user can attempt several times per page and retake.
|
||||||
|
primaryKeys: ['lessonid', 'retake', 'pageid', 'timemodified'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lesson retake data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonPasswordDBRecord = {
|
||||||
|
lessonid: number;
|
||||||
|
password: string;
|
||||||
|
timemodified: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lesson retake data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonRetakeDBRecord = {
|
||||||
|
lessonid: number;
|
||||||
|
retake: number;
|
||||||
|
courseid: number;
|
||||||
|
finished: number;
|
||||||
|
outoftime?: number | null;
|
||||||
|
timemodified?: number | null;
|
||||||
|
lastquestionpage?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lesson page attempts data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonPageAttemptDBRecord = {
|
||||||
|
lessonid: number;
|
||||||
|
retake: number;
|
||||||
|
pageid: number;
|
||||||
|
timemodified: number;
|
||||||
|
courseid: number;
|
||||||
|
data: string | null;
|
||||||
|
type: number;
|
||||||
|
newpageid: number;
|
||||||
|
correct: number;
|
||||||
|
answerid: number | null;
|
||||||
|
useranswer: string | null;
|
||||||
|
};
|
|
@ -0,0 +1,723 @@
|
||||||
|
// (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 { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||||
|
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
import {
|
||||||
|
AddonModLesson,
|
||||||
|
AddonModLessonAttemptsOverviewsAttemptWSData,
|
||||||
|
AddonModLessonGetPageDataWSResponse,
|
||||||
|
AddonModLessonProvider,
|
||||||
|
} from './lesson';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper service that provides some features for quiz.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModLessonHelperProvider {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected formBuilder: FormBuilder,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the HTML of next activity link, format it to extract the href and the text.
|
||||||
|
*
|
||||||
|
* @param activityLink HTML of the activity link.
|
||||||
|
* @return Formatted data.
|
||||||
|
*/
|
||||||
|
formatActivityLink(activityLink: string): {formatted: boolean; label: string; href: string} {
|
||||||
|
const element = CoreDomUtils.instance.convertToElement(activityLink);
|
||||||
|
const anchor = element.querySelector('a');
|
||||||
|
|
||||||
|
if (!anchor) {
|
||||||
|
// Anchor not found, return the original HTML.
|
||||||
|
return {
|
||||||
|
formatted: false,
|
||||||
|
label: activityLink,
|
||||||
|
href: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatted: true,
|
||||||
|
label: anchor.innerHTML,
|
||||||
|
href: anchor.href,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the HTML of an answer from a content page, extract the data to render the answer.
|
||||||
|
*
|
||||||
|
* @param html Answer's HTML.
|
||||||
|
* @return Data to render the answer.
|
||||||
|
*/
|
||||||
|
getContentPageAnswerDataFromHtml(html: string): {buttonText: string; content: string} {
|
||||||
|
const data = {
|
||||||
|
buttonText: '',
|
||||||
|
content: '',
|
||||||
|
};
|
||||||
|
const element = CoreDomUtils.instance.convertToElement(html);
|
||||||
|
|
||||||
|
// Search the input button.
|
||||||
|
const button = <HTMLInputElement> element.querySelector('input[type="button"]');
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
// Extract the button content and remove it from the HTML.
|
||||||
|
data.buttonText = button.value;
|
||||||
|
button.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
data.content = element.innerHTML.trim();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the buttons to change pages.
|
||||||
|
*
|
||||||
|
* @param html Page's HTML.
|
||||||
|
* @return List of buttons.
|
||||||
|
*/
|
||||||
|
getPageButtonsFromHtml(html: string): AddonModLessonPageButton[] {
|
||||||
|
const buttons: AddonModLessonPageButton[] = [];
|
||||||
|
const element = CoreDomUtils.instance.convertToElement(html);
|
||||||
|
|
||||||
|
// Get the container of the buttons if it exists.
|
||||||
|
let buttonsContainer = element.querySelector('.branchbuttoncontainer');
|
||||||
|
|
||||||
|
if (!buttonsContainer) {
|
||||||
|
// Button container not found, might be a legacy lesson (from 1.9).
|
||||||
|
if (!element.querySelector('form input[type="submit"]')) {
|
||||||
|
// No buttons found.
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
buttonsContainer = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forms = Array.from(buttonsContainer.querySelectorAll('form'));
|
||||||
|
forms.forEach((form) => {
|
||||||
|
const buttonSelector = 'input[type="submit"], button[type="submit"]';
|
||||||
|
const buttonEl = <HTMLInputElement | HTMLButtonElement> form.querySelector(buttonSelector);
|
||||||
|
const inputs = Array.from(form.querySelectorAll('input'));
|
||||||
|
|
||||||
|
if (!buttonEl || !inputs || !inputs.length) {
|
||||||
|
// Button not found or no inputs, ignore it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button: AddonModLessonPageButton = {
|
||||||
|
id: buttonEl.id,
|
||||||
|
title: buttonEl.title || buttonEl.value,
|
||||||
|
content: buttonEl.tagName == 'INPUT' ? buttonEl.value : buttonEl.innerHTML.trim(),
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
inputs.forEach((input) => {
|
||||||
|
if (input.type != 'submit') {
|
||||||
|
button.data[input.name] = input.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buttons.push(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a page data, get the page contents.
|
||||||
|
*
|
||||||
|
* @param data Page data.
|
||||||
|
* @return Page contents.
|
||||||
|
*/
|
||||||
|
getPageContentsFromPageData(data: AddonModLessonGetPageDataWSResponse): string {
|
||||||
|
// Search the page contents inside the whole page HTML. Use data.pagecontent because it's filtered.
|
||||||
|
const element = CoreDomUtils.instance.convertToElement(data.pagecontent || '');
|
||||||
|
const contents = element.querySelector('.contents');
|
||||||
|
|
||||||
|
if (contents) {
|
||||||
|
return contents.innerHTML.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot find contents element.
|
||||||
|
if (AddonModLesson.instance.isQuestionPage(data.page?.type || -1) ||
|
||||||
|
data.page?.qtype == AddonModLessonProvider.LESSON_PAGE_BRANCHTABLE) {
|
||||||
|
// Return page.contents to prevent having duplicated elements (some elements like videos might not work).
|
||||||
|
return data.page?.contents || '';
|
||||||
|
} else {
|
||||||
|
// It's an end of cluster, end of branch, etc. Return the whole pagecontent to match what's displayed in web.
|
||||||
|
return data.pagecontent || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a question and all the data required to render it from the page data.
|
||||||
|
*
|
||||||
|
* @param questionForm The form group where to add the controls.
|
||||||
|
* @param pageData Page data.
|
||||||
|
* @return Question data.
|
||||||
|
*/
|
||||||
|
getQuestionFromPageData(questionForm: FormGroup, pageData: AddonModLessonGetPageDataWSResponse): AddonModLessonQuestion {
|
||||||
|
const element = CoreDomUtils.instance.convertToElement(pageData.pagecontent || '');
|
||||||
|
|
||||||
|
// Get the container of the question answers if it exists.
|
||||||
|
const fieldContainer = <HTMLElement> element.querySelector('.fcontainer');
|
||||||
|
|
||||||
|
// Get hidden inputs and add their data to the form group.
|
||||||
|
const hiddenInputs = <HTMLInputElement[]> Array.from(element.querySelectorAll('input[type="hidden"]'));
|
||||||
|
hiddenInputs.forEach((input) => {
|
||||||
|
questionForm.addControl(input.name, this.formBuilder.control(input.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the submit button and extract its value.
|
||||||
|
const submitButton = <HTMLInputElement> element.querySelector('input[type="submit"]');
|
||||||
|
const question: AddonModLessonQuestion = {
|
||||||
|
template: '',
|
||||||
|
submitLabel: submitButton ? submitButton.value : Translate.instance.instant('addon.mod_lesson.submit'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!fieldContainer) {
|
||||||
|
// Element not found, return.
|
||||||
|
return question;
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = 'text';
|
||||||
|
|
||||||
|
switch (pageData.page?.qtype) {
|
||||||
|
case AddonModLessonProvider.LESSON_PAGE_TRUEFALSE:
|
||||||
|
case AddonModLessonProvider.LESSON_PAGE_MULTICHOICE:
|
||||||
|
return this.getMultiChoiceQuestionData(questionForm, question, fieldContainer);
|
||||||
|
|
||||||
|
case AddonModLessonProvider.LESSON_PAGE_NUMERICAL:
|
||||||
|
type = 'number';
|
||||||
|
case AddonModLessonProvider.LESSON_PAGE_SHORTANSWER:
|
||||||
|
return this.getInputQuestionData(questionForm, question, fieldContainer, type);
|
||||||
|
|
||||||
|
case AddonModLessonProvider.LESSON_PAGE_ESSAY: {
|
||||||
|
return this.getEssayQuestionData(questionForm, question, fieldContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
case AddonModLessonProvider.LESSON_PAGE_MATCHING: {
|
||||||
|
return this.getMatchingQuestionData(questionForm, question, fieldContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return question;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a multichoice question data.
|
||||||
|
*
|
||||||
|
* @param questionForm The form group where to add the controls.
|
||||||
|
* @param question Basic question data.
|
||||||
|
* @param fieldContainer HTMLElement containing the data.
|
||||||
|
* @return Question data.
|
||||||
|
*/
|
||||||
|
protected getMultiChoiceQuestionData(
|
||||||
|
questionForm: FormGroup,
|
||||||
|
question: AddonModLessonQuestion,
|
||||||
|
fieldContainer: HTMLElement,
|
||||||
|
): AddonModLessonMultichoiceQuestion {
|
||||||
|
const multiChoiceQuestion = <AddonModLessonMultichoiceQuestion> {
|
||||||
|
...question,
|
||||||
|
template: 'multichoice',
|
||||||
|
options: [],
|
||||||
|
multi: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all the inputs. Search radio first.
|
||||||
|
let inputs = <HTMLInputElement[]> Array.from(fieldContainer.querySelectorAll('input[type="radio"]'));
|
||||||
|
if (!inputs || !inputs.length) {
|
||||||
|
// Radio buttons not found, it might be a multi answer. Search for checkbox.
|
||||||
|
multiChoiceQuestion.multi = true;
|
||||||
|
inputs = <HTMLInputElement[]> Array.from(fieldContainer.querySelectorAll('input[type="checkbox"]'));
|
||||||
|
|
||||||
|
if (!inputs || !inputs.length) {
|
||||||
|
// No checkbox found either. Stop.
|
||||||
|
return multiChoiceQuestion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let controlAdded = false;
|
||||||
|
inputs.forEach((input) => {
|
||||||
|
const parent = input.parentElement;
|
||||||
|
const option: AddonModLessonMultichoiceOption = {
|
||||||
|
id: input.id,
|
||||||
|
name: input.name,
|
||||||
|
value: input.value,
|
||||||
|
checked: !!input.checked,
|
||||||
|
disabled: !!input.disabled,
|
||||||
|
text: parent?.innerHTML.trim() || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (option.checked || multiChoiceQuestion.multi) {
|
||||||
|
// Add the control.
|
||||||
|
const value = multiChoiceQuestion.multi ?
|
||||||
|
{ value: option.checked, disabled: option.disabled } : option.value;
|
||||||
|
questionForm.addControl(option.name, this.formBuilder.control(value));
|
||||||
|
controlAdded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the input and use the rest of the parent contents as the label.
|
||||||
|
input.remove();
|
||||||
|
multiChoiceQuestion.options!.push(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!multiChoiceQuestion.multi) {
|
||||||
|
multiChoiceQuestion.controlName = inputs[0].name;
|
||||||
|
|
||||||
|
if (!controlAdded) {
|
||||||
|
// No checked option for single choice, add the control with an empty value.
|
||||||
|
questionForm.addControl(multiChoiceQuestion.controlName, this.formBuilder.control(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiChoiceQuestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an input question data.
|
||||||
|
*
|
||||||
|
* @param questionForm The form group where to add the controls.
|
||||||
|
* @param question Basic question data.
|
||||||
|
* @param fieldContainer HTMLElement containing the data.
|
||||||
|
* @param type Type of the input.
|
||||||
|
* @return Question data.
|
||||||
|
*/
|
||||||
|
protected getInputQuestionData(
|
||||||
|
questionForm: FormGroup,
|
||||||
|
question: AddonModLessonQuestion,
|
||||||
|
fieldContainer: HTMLElement,
|
||||||
|
type: string,
|
||||||
|
): AddonModLessonInputQuestion {
|
||||||
|
|
||||||
|
const inputQuestion = <AddonModLessonInputQuestion> question;
|
||||||
|
inputQuestion.template = 'shortanswer';
|
||||||
|
|
||||||
|
// Get the input.
|
||||||
|
const input = <HTMLInputElement> fieldContainer.querySelector('input[type="text"], input[type="number"]');
|
||||||
|
if (!input) {
|
||||||
|
return inputQuestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputQuestion.input = {
|
||||||
|
id: input.id,
|
||||||
|
name: input.name,
|
||||||
|
maxlength: input.maxLength,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Init the control.
|
||||||
|
questionForm.addControl(input.name, this.formBuilder.control({ value: input.value, disabled: input.readOnly }));
|
||||||
|
|
||||||
|
return inputQuestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an essay question data.
|
||||||
|
*
|
||||||
|
* @param questionForm The form group where to add the controls.
|
||||||
|
* @param question Basic question data.
|
||||||
|
* @param fieldContainer HTMLElement containing the data.
|
||||||
|
* @return Question data.
|
||||||
|
*/
|
||||||
|
protected getEssayQuestionData(
|
||||||
|
questionForm: FormGroup,
|
||||||
|
question: AddonModLessonQuestion,
|
||||||
|
fieldContainer: HTMLElement,
|
||||||
|
): AddonModLessonEssayQuestion {
|
||||||
|
const essayQuestion = <AddonModLessonEssayQuestion> question;
|
||||||
|
essayQuestion.template = 'essay';
|
||||||
|
|
||||||
|
// Get the textarea.
|
||||||
|
const textarea = fieldContainer.querySelector('textarea');
|
||||||
|
|
||||||
|
if (!textarea) {
|
||||||
|
// Textarea not found, probably review mode.
|
||||||
|
const answerEl = fieldContainer.querySelector('.reviewessay');
|
||||||
|
if (!answerEl) {
|
||||||
|
// Answer not found, stop.
|
||||||
|
return essayQuestion;
|
||||||
|
}
|
||||||
|
essayQuestion.useranswer = answerEl.innerHTML;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
essayQuestion.textarea = {
|
||||||
|
id: textarea.id,
|
||||||
|
name: textarea.name || 'answer[text]',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Init the control.
|
||||||
|
essayQuestion.control = this.formBuilder.control('');
|
||||||
|
questionForm.addControl(essayQuestion.textarea.name, essayQuestion.control);
|
||||||
|
}
|
||||||
|
|
||||||
|
return essayQuestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a matching question data.
|
||||||
|
*
|
||||||
|
* @param questionForm The form group where to add the controls.
|
||||||
|
* @param question Basic question data.
|
||||||
|
* @param fieldContainer HTMLElement containing the data.
|
||||||
|
* @return Question data.
|
||||||
|
*/
|
||||||
|
protected getMatchingQuestionData(
|
||||||
|
questionForm: FormGroup,
|
||||||
|
question: AddonModLessonQuestion,
|
||||||
|
fieldContainer: HTMLElement,
|
||||||
|
): AddonModLessonMatchingQuestion {
|
||||||
|
|
||||||
|
const matchingQuestion = <AddonModLessonMatchingQuestion> {
|
||||||
|
...question,
|
||||||
|
template: 'matching',
|
||||||
|
rows: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const rows = Array.from(fieldContainer.querySelectorAll('.answeroption'));
|
||||||
|
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const label = row.querySelector('label');
|
||||||
|
const select = row.querySelector('select');
|
||||||
|
const options = Array.from(row.querySelectorAll('option'));
|
||||||
|
|
||||||
|
if (!label || !select || !options || !options.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the row's text (label).
|
||||||
|
const rowData: AddonModLessonMatchingRow = {
|
||||||
|
text: label.innerHTML.trim(),
|
||||||
|
id: select.id,
|
||||||
|
name: select.name,
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Treat each option.
|
||||||
|
let controlAdded = false;
|
||||||
|
options.forEach((option) => {
|
||||||
|
if (typeof option.value == 'undefined') {
|
||||||
|
// Option not valid, ignore it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionData: AddonModLessonMatchingRowOption = {
|
||||||
|
value: option.value,
|
||||||
|
label: option.innerHTML.trim(),
|
||||||
|
selected: option.selected,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (optionData.selected) {
|
||||||
|
controlAdded = true;
|
||||||
|
questionForm.addControl(
|
||||||
|
rowData.name,
|
||||||
|
this.formBuilder.control({ value: optionData.value, disabled: !!select.disabled }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData.options.push(optionData);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!controlAdded) {
|
||||||
|
// No selected option, add the control with an empty value.
|
||||||
|
questionForm.addControl(rowData.name, this.formBuilder.control({ value: '', disabled: !!select.disabled }));
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingQuestion.rows.push(rowData);
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchingQuestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the HTML of an answer from a question page, extract the data to render the answer.
|
||||||
|
*
|
||||||
|
* @param html Answer's HTML.
|
||||||
|
* @return Object with the data to render the answer. If the answer doesn't require any parsing, return a string with the HTML.
|
||||||
|
*/
|
||||||
|
getQuestionPageAnswerDataFromHtml(html: string): AddonModLessonAnswerData {
|
||||||
|
const element = CoreDomUtils.instance.convertToElement(html);
|
||||||
|
|
||||||
|
// Check if it has a checkbox.
|
||||||
|
let input = <HTMLInputElement> element.querySelector('input[type="checkbox"][name*="answer"]');
|
||||||
|
if (input) {
|
||||||
|
// Truefalse or multichoice.
|
||||||
|
const data: AddonModLessonCheckboxAnswerData = {
|
||||||
|
isCheckbox: true,
|
||||||
|
checked: !!input.checked,
|
||||||
|
name: input.name,
|
||||||
|
highlight: !!element.querySelector('.highlight'),
|
||||||
|
content: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
input.remove();
|
||||||
|
data.content = element.innerHTML.trim();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it has an input text or number.
|
||||||
|
input = <HTMLInputElement> element.querySelector('input[type="number"],input[type="text"]');
|
||||||
|
if (input) {
|
||||||
|
// Short answer or numeric.
|
||||||
|
return {
|
||||||
|
isText: true,
|
||||||
|
value: input.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it has a select.
|
||||||
|
const select = element.querySelector('select');
|
||||||
|
if (select?.options) {
|
||||||
|
// Matching.
|
||||||
|
const selectedOption = select.options[select.selectedIndex];
|
||||||
|
const data: AddonModLessonSelectAnswerData = {
|
||||||
|
isSelect: true,
|
||||||
|
id: select.id,
|
||||||
|
value: selectedOption ? selectedOption.value : '',
|
||||||
|
content: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
select.remove();
|
||||||
|
data.content = element.innerHTML.trim();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The answer doesn't need any parsing, return the HTML as it is.
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a label to identify a retake (lesson attempt).
|
||||||
|
*
|
||||||
|
* @param retake Retake object.
|
||||||
|
* @param includeDuration Whether to include the duration of the retake.
|
||||||
|
* @return Retake label.
|
||||||
|
*/
|
||||||
|
getRetakeLabel(retake: AddonModLessonAttemptsOverviewsAttemptWSData, includeDuration?: boolean): string {
|
||||||
|
const data = {
|
||||||
|
retake: retake.try + 1,
|
||||||
|
grade: '',
|
||||||
|
timestart: '',
|
||||||
|
duration: '',
|
||||||
|
};
|
||||||
|
const hasGrade = retake.grade != null;
|
||||||
|
|
||||||
|
if (hasGrade || retake.end) {
|
||||||
|
// Retake finished with or without grade (if the lesson only has content pages, it has no grade).
|
||||||
|
if (hasGrade) {
|
||||||
|
data.grade = Translate.instance.instant('core.percentagenumber', { $a: retake.grade });
|
||||||
|
}
|
||||||
|
data.timestart = CoreTimeUtils.instance.userDate(retake.timestart * 1000);
|
||||||
|
if (includeDuration) {
|
||||||
|
data.duration = CoreTimeUtils.instance.formatTime(retake.timeend - retake.timestart);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The user has not completed the retake.
|
||||||
|
data.grade = Translate.instance.instant('addon.mod_lesson.notcompleted');
|
||||||
|
if (retake.timestart) {
|
||||||
|
data.timestart = CoreTimeUtils.instance.userDate(retake.timestart * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Translate.instance.instant('addon.mod_lesson.retakelabel' + (includeDuration ? 'full' : 'short'), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the question data to be sent to server.
|
||||||
|
*
|
||||||
|
* @param question Question to prepare.
|
||||||
|
* @param data Data to prepare.
|
||||||
|
* @return Data to send.
|
||||||
|
*/
|
||||||
|
prepareQuestionData(question: AddonModLessonQuestion, data: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
if (question.template == 'essay') {
|
||||||
|
const textarea = (<AddonModLessonEssayQuestion> question).textarea;
|
||||||
|
|
||||||
|
// Add some HTML to the answer if needed.
|
||||||
|
if (textarea) {
|
||||||
|
data[textarea.name] = CoreTextUtils.instance.formatHtmlLines(<string> data[textarea.name]);
|
||||||
|
}
|
||||||
|
} else if (question.template == 'multichoice' && (<AddonModLessonMultichoiceQuestion> question).multi) {
|
||||||
|
// Only send the options with value set to true.
|
||||||
|
for (const name in data) {
|
||||||
|
if (name.match(/answer\[\d+\]/) && data[name] == false) {
|
||||||
|
delete data[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the feedback of a process page in HTML, remove the question text.
|
||||||
|
*
|
||||||
|
* @param html Feedback's HTML.
|
||||||
|
* @return Feedback without the question text.
|
||||||
|
*/
|
||||||
|
removeQuestionFromFeedback(html: string): string {
|
||||||
|
const element = CoreDomUtils.instance.convertToElement(html);
|
||||||
|
|
||||||
|
// Remove the question text.
|
||||||
|
CoreDomUtils.instance.removeElement(element, '.generalbox:not(.feedback):not(.correctanswer)');
|
||||||
|
|
||||||
|
return element.innerHTML.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddonModLessonHelper extends makeSingleton(AddonModLessonHelperProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page button data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonPageButton = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
data: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic question data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonQuestion = {
|
||||||
|
template: string; // Name of the template to use.
|
||||||
|
submitLabel: string; // Text to display in submit.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multichoice question data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonMultichoiceQuestion = AddonModLessonQuestion & {
|
||||||
|
multi: boolean; // Whether it allows multiple answers.
|
||||||
|
options: AddonModLessonMultichoiceOption[]; // Options for multichoice question.
|
||||||
|
controlName?: string; // Name of the form control, for single choice.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short answer or numeric question data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonInputQuestion = AddonModLessonQuestion & {
|
||||||
|
input?: AddonModLessonQuestionInput; // Text input for text/number questions.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Essay question data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonEssayQuestion = AddonModLessonQuestion & {
|
||||||
|
useranswer?: string; // User answer, for reviewing.
|
||||||
|
textarea?: AddonModLessonTextareaData; // Data for the textarea.
|
||||||
|
control?: FormControl; // Form control.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matching question data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonMatchingQuestion = AddonModLessonQuestion & {
|
||||||
|
rows: AddonModLessonMatchingRow[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data for each option in a multichoice question.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonMultichoiceOption = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
checked: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input data for text/number questions.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonQuestionInput = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
maxlength: number;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Textarea data for essay questions.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonTextareaData = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data for each row in a matching question.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonMatchingRow = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
text: string;
|
||||||
|
options: AddonModLessonMatchingRowOption[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data for each option in a row in a matching question.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonMatchingRowOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
selected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkbox answer.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonCheckboxAnswerData = {
|
||||||
|
isCheckbox: true;
|
||||||
|
checked: boolean;
|
||||||
|
name: string;
|
||||||
|
highlight: boolean;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text answer.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonTextAnswerData = {
|
||||||
|
isText: true;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select answer.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonSelectAnswerData = {
|
||||||
|
isSelect: true;
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any possible answer data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonAnswerData =
|
||||||
|
AddonModLessonCheckboxAnswerData | AddonModLessonTextAnswerData | AddonModLessonSelectAnswerData | string;
|
|
@ -0,0 +1,565 @@
|
||||||
|
// (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 { CoreSites } from '@services/sites';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import {
|
||||||
|
AddonModLessonPageAttemptDBRecord,
|
||||||
|
AddonModLessonRetakeDBRecord,
|
||||||
|
PAGE_ATTEMPTS_TABLE_NAME,
|
||||||
|
RETAKES_TABLE_NAME,
|
||||||
|
} from './database/lesson';
|
||||||
|
|
||||||
|
import { AddonModLessonPageWSData, AddonModLessonProvider } from './lesson';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle offline lesson.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModLessonOfflineProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an offline attempt.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param retake Lesson retake number.
|
||||||
|
* @param pageId Page ID.
|
||||||
|
* @param timemodified The timemodified of the attempt.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async deleteAttempt(lessonId: number, retake: number, pageId: number, timemodified: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(PAGE_ATTEMPTS_TABLE_NAME, <Partial<AddonModLessonPageAttemptDBRecord>> {
|
||||||
|
lessonid: lessonId,
|
||||||
|
retake: retake,
|
||||||
|
pageid: pageId,
|
||||||
|
timemodified: timemodified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete offline lesson retake.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async deleteRetake(lessonId: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(RETAKES_TABLE_NAME, <Partial<AddonModLessonRetakeDBRecord>> { lessonid: lessonId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete offline attempts for a retake and page.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param retake Lesson retake number.
|
||||||
|
* @param pageId Page ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async deleteRetakeAttemptsForPage(lessonId: number, retake: number, pageId: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(PAGE_ATTEMPTS_TABLE_NAME, <Partial<AddonModLessonPageAttemptDBRecord>> {
|
||||||
|
lessonid: lessonId,
|
||||||
|
retake: retake,
|
||||||
|
pageid: pageId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a retake as finished.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param courseId Course ID the lesson belongs to.
|
||||||
|
* @param retake Retake number.
|
||||||
|
* @param finished Whether retake is finished.
|
||||||
|
* @param outOfTime If the user ran out of time.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved in success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
async finishRetake(
|
||||||
|
lessonId: number,
|
||||||
|
courseId: number,
|
||||||
|
retake: number,
|
||||||
|
finished?: boolean,
|
||||||
|
outOfTime?: boolean,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
// Get current stored retake (if any). If not found, it will create a new one.
|
||||||
|
const entry = await this.getRetakeWithFallback(lessonId, courseId, retake, site.id);
|
||||||
|
|
||||||
|
entry.finished = finished ? 1 : 0;
|
||||||
|
entry.outoftime = outOfTime ? 1 : 0;
|
||||||
|
entry.timemodified = CoreTimeUtils.instance.timestamp();
|
||||||
|
|
||||||
|
await site.getDb().insertRecord(RETAKES_TABLE_NAME, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the offline page attempts in a certain site.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not set, use current site.
|
||||||
|
* @return Promise resolved when the offline attempts are retrieved.
|
||||||
|
*/
|
||||||
|
async getAllAttempts(siteId?: string): Promise<AddonModLessonPageAttemptRecord[]> {
|
||||||
|
const db = await CoreSites.instance.getSiteDb(siteId);
|
||||||
|
|
||||||
|
const attempts = await db.getAllRecords<AddonModLessonPageAttemptDBRecord>(PAGE_ATTEMPTS_TABLE_NAME);
|
||||||
|
|
||||||
|
return this.parsePageAttempts(attempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the lessons that have offline data in a certain site.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not set, use current site.
|
||||||
|
* @return Promise resolved with an object containing the lessons.
|
||||||
|
*/
|
||||||
|
async getAllLessonsWithData(siteId?: string): Promise<AddonModLessonLessonStoredData[]> {
|
||||||
|
const lessons: Record<number, AddonModLessonLessonStoredData> = {};
|
||||||
|
|
||||||
|
const [pageAttempts, retakes] = await Promise.all([
|
||||||
|
CoreUtils.instance.ignoreErrors(this.getAllAttempts(siteId)),
|
||||||
|
CoreUtils.instance.ignoreErrors(this.getAllRetakes(siteId)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.getLessonsFromEntries(lessons, pageAttempts || []);
|
||||||
|
this.getLessonsFromEntries(lessons, retakes || []);
|
||||||
|
|
||||||
|
return CoreUtils.instance.objectToArray(lessons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the offline retakes in a certain site.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not set, use current site.
|
||||||
|
* @return Promise resolved when the offline retakes are retrieved.
|
||||||
|
*/
|
||||||
|
async getAllRetakes(siteId?: string): Promise<AddonModLessonRetakeDBRecord[]> {
|
||||||
|
const db = await CoreSites.instance.getSiteDb(siteId);
|
||||||
|
|
||||||
|
return db.getAllRecords(RETAKES_TABLE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the last offline attempt stored in a retake.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param retake Retake number.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the attempt (undefined if no attempts).
|
||||||
|
*/
|
||||||
|
async getLastQuestionPageAttempt(
|
||||||
|
lessonId: number,
|
||||||
|
retake: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<AddonModLessonPageAttemptRecord | undefined> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const retakeData = await this.getRetakeWithFallback(lessonId, 0, retake, siteId);
|
||||||
|
if (!retakeData.lastquestionpage) {
|
||||||
|
// No question page attempted.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attempts = await this.getRetakeAttemptsForPage(lessonId, retake, retakeData.lastquestionpage, siteId);
|
||||||
|
|
||||||
|
// Return the attempt with highest timemodified.
|
||||||
|
return attempts.reduce((a, b) => a.timemodified > b.timemodified ? a : b);
|
||||||
|
} catch {
|
||||||
|
// Error, return undefined.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all offline attempts for a lesson.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the attempts.
|
||||||
|
*/
|
||||||
|
async getLessonAttempts(lessonId: number, siteId?: string): Promise<AddonModLessonPageAttemptRecord[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const attempts = await site.getDb().getRecords<AddonModLessonPageAttemptDBRecord>(
|
||||||
|
PAGE_ATTEMPTS_TABLE_NAME,
|
||||||
|
{ lessonid: lessonId },
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.parsePageAttempts(attempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of DB entries (either retakes or page attempts), get the list of lessons.
|
||||||
|
*
|
||||||
|
* @param lessons Object where to store the lessons.
|
||||||
|
* @param entries List of DB entries.
|
||||||
|
*/
|
||||||
|
protected getLessonsFromEntries(
|
||||||
|
lessons: Record<number, AddonModLessonLessonStoredData>,
|
||||||
|
entries: (AddonModLessonPageAttemptRecord | AddonModLessonRetakeDBRecord)[],
|
||||||
|
): void {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!lessons[entry.lessonid]) {
|
||||||
|
lessons[entry.lessonid] = {
|
||||||
|
id: entry.lessonid,
|
||||||
|
courseId: entry.courseid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get attempts for question pages and retake in a lesson.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param retake Retake number.
|
||||||
|
* @param correct True to only fetch correct attempts, false to get them all.
|
||||||
|
* @param pageId If defined, only get attempts on this page.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the attempts.
|
||||||
|
*/
|
||||||
|
async getQuestionsAttempts(
|
||||||
|
lessonId: number,
|
||||||
|
retake: number,
|
||||||
|
correct?: boolean,
|
||||||
|
pageId?: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<AddonModLessonPageAttemptRecord[]> {
|
||||||
|
const attempts = pageId ?
|
||||||
|
await this.getRetakeAttemptsForPage(lessonId, retake, pageId, siteId) :
|
||||||
|
await this.getRetakeAttemptsForType(lessonId, retake, AddonModLessonProvider.TYPE_QUESTION, siteId);
|
||||||
|
|
||||||
|
if (correct) {
|
||||||
|
return attempts.filter((attempt) => !!attempt.correct);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attempts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a retake from site DB.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the retake.
|
||||||
|
*/
|
||||||
|
async getRetake(lessonId: number, siteId?: string): Promise<AddonModLessonRetakeDBRecord> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
return site.getDb().getRecord(RETAKES_TABLE_NAME, { lessonid: lessonId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all offline attempts for a retake.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param retake Retake number.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the retake attempts.
|
||||||
|
*/
|
||||||
|
async getRetakeAttempts(lessonId: number, retake: number, siteId?: string): Promise<AddonModLessonPageAttemptRecord[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const attempts = await site.getDb().getRecords<AddonModLessonPageAttemptDBRecord>(
|
||||||
|
PAGE_ATTEMPTS_TABLE_NAME,
|
||||||
|
<Partial<AddonModLessonPageAttemptDBRecord>> {
|
||||||
|
lessonid: lessonId,
|
||||||
|
retake,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.parsePageAttempts(attempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve offline attempts for a retake and page.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param retake Lesson retake number.
|
||||||
|
* @param pageId Page ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the retake attempts.
|
||||||
|
*/
|
||||||
|
async getRetakeAttemptsForPage(
|
||||||
|
lessonId: number,
|
||||||
|
retake: number,
|
||||||
|
pageId: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<AddonModLessonPageAttemptRecord[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const attempts = await site.getDb().getRecords<AddonModLessonPageAttemptDBRecord>(
|
||||||
|
PAGE_ATTEMPTS_TABLE_NAME,
|
||||||
|
<Partial<AddonModLessonPageAttemptDBRecord>> {
|
||||||
|
lessonid: lessonId,
|
||||||
|
retake,
|
||||||
|
pageid: pageId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.parsePageAttempts(attempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve offline attempts for certain pages for a retake.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param retake Retake number.
|
||||||
|
* @param type Type of the pages to get: TYPE_QUESTION or TYPE_STRUCTURE.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the retake attempts.
|
||||||
|
*/
|
||||||
|
async getRetakeAttemptsForType(
|
||||||
|
lessonId: number,
|
||||||
|
retake: number,
|
||||||
|
type: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<AddonModLessonPageAttemptRecord[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const attempts = await site.getDb().getRecords<AddonModLessonPageAttemptDBRecord>(
|
||||||
|
PAGE_ATTEMPTS_TABLE_NAME,
|
||||||
|
<Partial<AddonModLessonPageAttemptDBRecord>> {
|
||||||
|
lessonid: lessonId,
|
||||||
|
retake,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.parsePageAttempts(attempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stored retake. If not found or doesn't match the retake number, return a new one.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param courseId Course ID the lesson belongs to.
|
||||||
|
* @param retake Retake number.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the retake.
|
||||||
|
*/
|
||||||
|
protected async getRetakeWithFallback(
|
||||||
|
lessonId: number,
|
||||||
|
courseId: number,
|
||||||
|
retake: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<AddonModLessonRetakeDBRecord> {
|
||||||
|
try {
|
||||||
|
// Get current stored retake.
|
||||||
|
const retakeData = await this.getRetake(lessonId, siteId);
|
||||||
|
|
||||||
|
if (retakeData.retake == retake) {
|
||||||
|
return retakeData;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No retake, create a new one.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new retake.
|
||||||
|
return {
|
||||||
|
lessonid: lessonId,
|
||||||
|
retake,
|
||||||
|
courseid: courseId,
|
||||||
|
finished: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there is a finished retake for a certain lesson.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean.
|
||||||
|
*/
|
||||||
|
async hasFinishedRetake(lessonId: number, siteId?: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const retake = await this.getRetake(lessonId, siteId);
|
||||||
|
|
||||||
|
return !!retake.finished;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a lesson has offline data.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean.
|
||||||
|
*/
|
||||||
|
async hasOfflineData(lessonId: number, siteId?: string): Promise<boolean> {
|
||||||
|
const [retake, attempts] = await Promise.all([
|
||||||
|
CoreUtils.instance.ignoreErrors(this.getRetake(lessonId, siteId)),
|
||||||
|
CoreUtils.instance.ignoreErrors(this.getLessonAttempts(lessonId, siteId)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return !!retake || !!attempts?.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are offline attempts for a retake.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param retake Retake number.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with a boolean.
|
||||||
|
*/
|
||||||
|
async hasRetakeAttempts(lessonId: number, retake: number, siteId?: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const list = await this.getRetakeAttempts(lessonId, retake, siteId);
|
||||||
|
|
||||||
|
return !!list.length;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse some properties of a page attempt.
|
||||||
|
*
|
||||||
|
* @param attempt The attempt to treat.
|
||||||
|
* @return The treated attempt.
|
||||||
|
*/
|
||||||
|
protected parsePageAttempt(attempt: AddonModLessonPageAttemptDBRecord): AddonModLessonPageAttemptRecord {
|
||||||
|
return {
|
||||||
|
...attempt,
|
||||||
|
data: attempt.data ? CoreTextUtils.instance.parseJSON(attempt.data) : null,
|
||||||
|
useranswer: attempt.useranswer ? CoreTextUtils.instance.parseJSON(attempt.useranswer) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse some properties of some page attempts.
|
||||||
|
*
|
||||||
|
* @param attempts The attempts to treat.
|
||||||
|
* @return The treated attempts.
|
||||||
|
*/
|
||||||
|
protected parsePageAttempts(attempts: AddonModLessonPageAttemptDBRecord[]): AddonModLessonPageAttemptRecord[] {
|
||||||
|
return attempts.map((attempt) => this.parsePageAttempt(attempt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a lesson page, saving its data.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param courseId Course ID the lesson belongs to.
|
||||||
|
* @param retake Retake number.
|
||||||
|
* @param page Page.
|
||||||
|
* @param data Data to save.
|
||||||
|
* @param newPageId New page ID (calculated).
|
||||||
|
* @param answerId The answer ID that the user answered.
|
||||||
|
* @param correct If answer is correct. Only for question pages.
|
||||||
|
* @param userAnswer The user's answer (userresponse from checkAnswer).
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved in success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
async processPage(
|
||||||
|
lessonId: number,
|
||||||
|
courseId: number,
|
||||||
|
retake: number,
|
||||||
|
page: AddonModLessonPageWSData,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
newPageId: number,
|
||||||
|
answerId?: number,
|
||||||
|
correct?: boolean,
|
||||||
|
userAnswer?: unknown,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const entry: AddonModLessonPageAttemptDBRecord = {
|
||||||
|
lessonid: lessonId,
|
||||||
|
retake: retake,
|
||||||
|
pageid: page.id,
|
||||||
|
timemodified: CoreTimeUtils.instance.timestamp(),
|
||||||
|
courseid: courseId,
|
||||||
|
data: data ? JSON.stringify(data) : null,
|
||||||
|
type: page.type,
|
||||||
|
newpageid: newPageId,
|
||||||
|
correct: correct ? 1 : 0,
|
||||||
|
answerid: answerId || null,
|
||||||
|
useranswer: userAnswer ? JSON.stringify(userAnswer) : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await site.getDb().insertRecord(PAGE_ATTEMPTS_TABLE_NAME, entry);
|
||||||
|
|
||||||
|
if (page.type == AddonModLessonProvider.TYPE_QUESTION) {
|
||||||
|
// It's a question page, set it as last question page attempted.
|
||||||
|
await this.setLastQuestionPageAttempted(lessonId, courseId, retake, page.id, siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the last question page attempted in a retake.
|
||||||
|
*
|
||||||
|
* @param lessonId Lesson ID.
|
||||||
|
* @param courseId Course ID the lesson belongs to.
|
||||||
|
* @param retake Retake number.
|
||||||
|
* @param lastPage ID of the last question page attempted.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved in success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
async setLastQuestionPageAttempted(
|
||||||
|
lessonId: number,
|
||||||
|
courseId: number,
|
||||||
|
retake: number,
|
||||||
|
lastPage: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
// Get current stored retake (if any). If not found, it will create a new one.
|
||||||
|
const entry = await this.getRetakeWithFallback(lessonId, courseId, retake, site.id);
|
||||||
|
|
||||||
|
entry.lastquestionpage = lastPage;
|
||||||
|
entry.timemodified = CoreTimeUtils.instance.timestamp();
|
||||||
|
|
||||||
|
await site.getDb().insertRecord(RETAKES_TABLE_NAME, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddonModLessonOffline extends makeSingleton(AddonModLessonOfflineProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt DB record with parsed data.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonPageAttemptRecord = Omit<AddonModLessonPageAttemptDBRecord, 'data'|'useranswer'> & {
|
||||||
|
data: Record<string, unknown> | null;
|
||||||
|
useranswer: unknown | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lesson data stored in DB.
|
||||||
|
*/
|
||||||
|
export type AddonModLessonLessonStoredData = {
|
||||||
|
id: number;
|
||||||
|
courseId: number;
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,27 @@
|
||||||
|
// (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 { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { AddonModLessonModule } from './lesson/lesson.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
AddonModLessonModule,
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
exports: [],
|
||||||
|
})
|
||||||
|
export class AddonModModule { }
|
Loading…
Reference in New Issue