MOBILE-3648 lesson: Implement base services
This commit is contained in:
		
							parent
							
								
									3796654443
								
							
						
					
					
						commit
						2ff5883026
					
				@ -23,6 +23,7 @@ import { AddonCalendarModule } from './calendar/calendar.module';
 | 
			
		||||
import { AddonNotificationsModule } from './notifications/notifications.module';
 | 
			
		||||
import { AddonMessageOutputModule } from './messageoutput/messageoutput.module';
 | 
			
		||||
import { AddonMessagesModule } from './messages/messages.module';
 | 
			
		||||
import { AddonModModule } from './mod/mod.module';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
@ -35,6 +36,7 @@ import { AddonMessagesModule } from './messages/messages.module';
 | 
			
		||||
        AddonUserProfileFieldModule,
 | 
			
		||||
        AddonNotificationsModule,
 | 
			
		||||
        AddonMessageOutputModule,
 | 
			
		||||
        AddonModModule,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonsModule {}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										86
									
								
								src/addons/mod/lesson/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/addons/mod/lesson/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -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}}"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								src/addons/mod/lesson/lesson.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/addons/mod/lesson/lesson.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 {}
 | 
			
		||||
							
								
								
									
										185
									
								
								src/addons/mod/lesson/services/database/lesson.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								src/addons/mod/lesson/services/database/lesson.ts
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										723
									
								
								src/addons/mod/lesson/services/lesson-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										723
									
								
								src/addons/mod/lesson/services/lesson-helper.ts
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
							
								
								
									
										565
									
								
								src/addons/mod/lesson/services/lesson-offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										565
									
								
								src/addons/mod/lesson/services/lesson-offline.ts
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										4231
									
								
								src/addons/mod/lesson/services/lesson.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4231
									
								
								src/addons/mod/lesson/services/lesson.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										27
									
								
								src/addons/mod/mod.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/addons/mod/mod.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user