566 lines
19 KiB
TypeScript
566 lines
19 KiB
TypeScript
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { Injectable } from '@angular/core';
|
|
import { 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;
|
|
};
|