2019-02-08 12:28:05 +01:00

607 lines
23 KiB
TypeScript

// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { AddonModLessonProvider } from './lesson';
/**
* Service to handle offline lesson.
*/
@Injectable()
export class AddonModLessonOfflineProvider {
protected logger;
// Variables for database. We use lowercase in the names to match the WS responses.
static RETAKES_TABLE = 'addon_mod_lesson_retakes';
static PAGE_ATTEMPTS_TABLE = 'addon_mod_lesson_page_attempts';
protected siteSchema: CoreSiteSchema = {
name: 'AddonModLessonOfflineProvider',
version: 1,
tables: [
{
name: AddonModLessonOfflineProvider.RETAKES_TABLE,
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: AddonModLessonOfflineProvider.PAGE_ATTEMPTS_TABLE,
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']
}
]
};
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider,
private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider) {
this.logger = logger.getInstance('AddonModLessonOfflineProvider');
this.sitesProvider.registerSiteSchema(this.siteSchema);
}
/**
* Delete an offline attempt.
*
* @param {number} lessonId Lesson ID.
* @param {number} retake Lesson retake number.
* @param {number} pageId Page ID.
* @param {number} timemodified The timemodified of the attempt.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
deleteAttempt(lessonId: number, retake: number, pageId: number, timemodified: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().deleteRecords(AddonModLessonOfflineProvider.PAGE_ATTEMPTS_TABLE, {
lessonid: lessonId,
retake: retake,
pageid: pageId,
timemodified: timemodified
});
});
}
/**
* Delete offline lesson retake.
*
* @param {number} lessonId Lesson ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
deleteRetake(lessonId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().deleteRecords(AddonModLessonOfflineProvider.RETAKES_TABLE, {lessonid: lessonId});
});
}
/**
* Delete offline attempts for a retake and page.
*
* @param {number} lessonId Lesson ID.
* @param {number} retake Lesson retake number.
* @param {number} pageId Page ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
deleteRetakeAttemptsForPage(lessonId: number, retake: number, pageId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().deleteRecords(AddonModLessonOfflineProvider.PAGE_ATTEMPTS_TABLE, {lessonid: lessonId,
retake: retake, pageid: pageId});
});
}
/**
* Mark a retake as finished.
*
* @param {number} lessonId Lesson ID.
* @param {number} courseId Course ID the lesson belongs to.
* @param {number} retake Retake number.
* @param {boolean} finished Whether retake is finished.
* @param {boolean} outOfTime If the user ran out of time.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
*/
finishRetake(lessonId: number, courseId: number, retake: number, finished?: boolean, outOfTime?: boolean, siteId?: string)
: Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
// Get current stored retake (if any). If not found, it will create a new one.
return this.getRetakeWithFallback(lessonId, courseId, retake, site.id).then((entry) => {
entry.finished = finished ? 1 : 0;
entry.outoftime = outOfTime ? 1 : 0;
entry.timemodified = this.timeUtils.timestamp();
return site.getDb().insertRecord(AddonModLessonOfflineProvider.RETAKES_TABLE, entry);
});
});
}
/**
* Get all the offline page attempts in a certain site.
*
* @param {string} [siteId] Site ID. If not set, use current site.
* @return {Promise<any>} Promise resolved when the offline attempts are retrieved.
*/
getAllAttempts(siteId?: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.getAllRecords(AddonModLessonOfflineProvider.PAGE_ATTEMPTS_TABLE);
}).then((attempts) => {
return this.parsePageAttempts(attempts);
});
}
/**
* Get all the lessons that have offline data in a certain site.
*
* @param {string} [siteId] Site ID. If not set, use current site.
* @return {Promise<any>} Promise resolved with an object containing the lessons.
*/
getAllLessonsWithData(siteId?: string): Promise<any> {
const promises = [],
lessons = {};
// Get the lessons from page attempts.
promises.push(this.getAllAttempts(siteId).then((entries) => {
this.getLessonsFromEntries(lessons, entries);
}).catch(() => {
// Ignore errors.
}));
// Get the lessons from retakes.
promises.push(this.getAllRetakes(siteId).then((entries) => {
this.getLessonsFromEntries(lessons, entries);
}).catch(() => {
// Ignore errors.
}));
return Promise.all(promises).then(() => {
return this.utils.objectToArray(lessons);
});
}
/**
* Get all the offline retakes in a certain site.
*
* @param {string} [siteId] Site ID. If not set, use current site.
* @return {Promise<any>} Promise resolved when the offline retakes are retrieved.
*/
getAllRetakes(siteId?: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.getAllRecords(AddonModLessonOfflineProvider.RETAKES_TABLE);
});
}
/**
* Retrieve the last offline attempt stored in a retake.
*
* @param {number} lessonId Lesson ID.
* @param {number} retake Retake number.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the attempt (undefined if no attempts).
*/
getLastQuestionPageAttempt(lessonId: number, retake: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
return this.getRetakeWithFallback(lessonId, 0, retake, siteId).then((retakeData) => {
if (!retakeData.lastquestionpage) {
// No question page attempted.
return;
}
return this.getRetakeAttemptsForPage(lessonId, retake, retakeData.lastquestionpage, siteId).then((attempts) => {
// Return the attempt with highest timemodified.
return attempts.reduce((a, b) => {
return a.timemodified > b.timemodified ? a : b;
});
});
}).catch(() => {
// Error, return undefined.
});
}
/**
* Retrieve all offline attempts for a lesson.
*
* @param {number} lessonId Lesson ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with the attempts.
*/
getLessonAttempts(lessonId: number, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonModLessonOfflineProvider.PAGE_ATTEMPTS_TABLE, {lessonid: lessonId});
}).then((attempts) => {
return this.parsePageAttempts(attempts);
});
}
/**
* Given a list of DB entries (either retakes or page attempts), get the list of lessons.
*
* @param {any} lessons Object where to store the lessons.
* @param {any[]} entries List of DB entries.
*/
protected getLessonsFromEntries(lessons: any, entries: any[]): 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 {number} lessonId Lesson ID.
* @param {number} retake Retake number.
* @param {boolean} [correct] True to only fetch correct attempts, false to get them all.
* @param {number} [pageId] If defined, only get attempts on this page.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with the attempts.
*/
getQuestionsAttempts(lessonId: number, retake: number, correct?: boolean, pageId?: number, siteId?: string): Promise<any[]> {
let promise;
if (pageId) {
// Page ID is set, only get the attempts for that page.
promise = this.getRetakeAttemptsForPage(lessonId, retake, pageId, siteId);
} else {
// Page ID not specified, get all the attempts.
promise = this.getRetakeAttemptsForType(lessonId, retake, AddonModLessonProvider.TYPE_QUESTION, siteId);
}
return promise.then((attempts) => {
if (correct) {
return attempts.filter((attempt) => {
return !!attempt.correct;
});
}
return attempts;
});
}
/**
* Retrieve a retake from site DB.
*
* @param {number} lessonId Lesson ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the retake.
*/
getRetake(lessonId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecord(AddonModLessonOfflineProvider.RETAKES_TABLE, {lessonid: lessonId});
});
}
/**
* Retrieve all offline attempts for a retake.
*
* @param {number} lessonId Lesson ID.
* @param {number} retake Retake number.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with the retake attempts.
*/
getRetakeAttempts(lessonId: number, retake: number, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonModLessonOfflineProvider.PAGE_ATTEMPTS_TABLE, {lessonid: lessonId, retake: retake});
}).then((attempts) => {
return this.parsePageAttempts(attempts);
});
}
/**
* Retrieve offline attempts for a retake and page.
*
* @param {number} lessonId Lesson ID.
* @param {number} retake Lesson retake number.
* @param {number} pageId Page ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the retake attempts.
*/
getRetakeAttemptsForPage(lessonId: number, retake: number, pageId: number, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonModLessonOfflineProvider.PAGE_ATTEMPTS_TABLE, {lessonid: lessonId, retake: retake,
pageid: pageId});
}).then((attempts) => {
return this.parsePageAttempts(attempts);
});
}
/**
* Retrieve offline attempts for certain pages for a retake.
*
* @param {number} lessonId Lesson ID.
* @param {number} retake Retake number.
* @param {number} type Type of the pages to get: TYPE_QUESTION or TYPE_STRUCTURE.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the retake attempts.
*/
getRetakeAttemptsForType(lessonId: number, retake: number, type: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonModLessonOfflineProvider.PAGE_ATTEMPTS_TABLE, {lessonid: lessonId, retake: retake,
type: type});
}).then((attempts) => {
return this.parsePageAttempts(attempts);
});
}
/**
* Get stored retake. If not found or doesn't match the retake number, return a new one.
*
* @param {number} lessonId Lesson ID.
* @param {number} courseId Course ID the lesson belongs to.
* @param {number} retake Retake number.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the retake.
*/
protected getRetakeWithFallback(lessonId: number, courseId: number, retake: number, siteId?: string): Promise<any> {
// Get current stored retake.
return this.getRetake(lessonId, siteId).then((retakeData) => {
if (retakeData.retake != retake) {
// The stored retake doesn't match the retake number, create a new one.
return Promise.reject(null);
}
return retakeData;
}).catch(() => {
// No retake, create a new one.
return {
lessonid: lessonId,
retake: retake,
courseid: courseId,
finished: 0
};
});
}
/**
* Check if there is a finished retake for a certain lesson.
*
* @param {number} lessonId Lesson ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean.
*/
hasFinishedRetake(lessonId: number, siteId?: string): Promise<boolean> {
return this.getRetake(lessonId, siteId).then((retake) => {
return !!retake.finished;
}).catch(() => {
return false;
});
}
/**
* Check if a lesson has offline data.
*
* @param {number} lessonId Lesson ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean.
*/
hasOfflineData(lessonId: number, siteId?: string): Promise<boolean> {
const promises = [];
let hasData = false;
promises.push(this.getRetake(lessonId, siteId).then(() => {
hasData = true;
}).catch(() => {
// Ignore errors.
}));
promises.push(this.getLessonAttempts(lessonId, siteId).then((attempts) => {
hasData = hasData || !!attempts.length;
}).catch(() => {
// Ignore errors.
}));
return Promise.all(promises).then(() => {
return hasData;
});
}
/**
* Check if there are offline attempts for a retake.
*
* @param {number} lessonId Lesson ID.
* @param {number} retake Retake number.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with a boolean.
*/
hasRetakeAttempts(lessonId: number, retake: number, siteId?: string): Promise<boolean> {
return this.getRetakeAttempts(lessonId, retake, siteId).then((list) => {
return !!list.length;
}).catch(() => {
return false;
});
}
/**
* Parse some properties of a page attempt.
*
* @param {any} attempt The attempt to treat.
* @return {any} The treated attempt.
*/
protected parsePageAttempt(attempt: any): any {
attempt.data = this.textUtils.parseJSON(attempt.data);
attempt.useranswer = this.textUtils.parseJSON(attempt.useranswer);
return attempt;
}
/**
* Parse some properties of some page attempts.
*
* @param {any[]} attempts The attempts to treat.
* @return {any[]} The treated attempts.
*/
protected parsePageAttempts(attempts: any[]): any[] {
attempts.forEach((attempt) => {
this.parsePageAttempt(attempt);
});
return attempts;
}
/**
* Process a lesson page, saving its data.
*
* @param {number} lessonId Lesson ID.
* @param {number} courseId Course ID the lesson belongs to.
* @param {number} retake Retake number.
* @param {any} page Page.
* @param {any} data Data to save.
* @param {number} newPageId New page ID (calculated).
* @param {number} [answerId] The answer ID that the user answered.
* @param {boolean} [correct] If answer is correct. Only for question pages.
* @param {any} [userAnswer] The user's answer (userresponse from checkAnswer).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
*/
processPage(lessonId: number, courseId: number, retake: number, page: any, data: any, newPageId: number, answerId?: number,
correct?: boolean, userAnswer?: any, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const entry = {
lessonid: lessonId,
retake: retake,
pageid: page.id,
timemodified: this.timeUtils.timestamp(),
courseid: courseId,
data: data ? JSON.stringify(data) : null,
type: page.type,
newpageid: newPageId,
correct: correct ? 1 : 0,
answerid: Number(answerId),
useranswer: userAnswer ? JSON.stringify(userAnswer) : null,
};
return site.getDb().insertRecord(AddonModLessonOfflineProvider.PAGE_ATTEMPTS_TABLE, entry);
}).then(() => {
if (page.type == AddonModLessonProvider.TYPE_QUESTION) {
// It's a question page, set it as last question page attempted.
return this.setLastQuestionPageAttempted(lessonId, courseId, retake, page.id, siteId);
}
});
}
/**
* Set the last question page attempted in a retake.
*
* @param {number} lessonId Lesson ID.
* @param {number} courseId Course ID the lesson belongs to.
* @param {number} retake Retake number.
* @param {number} lastPage ID of the last question page attempted.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
*/
setLastQuestionPageAttempted(lessonId: number, courseId: number, retake: number, lastPage: number, siteId?: string)
: Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
// Get current stored retake (if any). If not found, it will create a new one.
return this.getRetakeWithFallback(lessonId, courseId, retake, site.id).then((entry) => {
entry.lastquestionpage = lastPage;
entry.timemodified = this.timeUtils.timestamp();
return site.getDb().insertRecord(AddonModLessonOfflineProvider.RETAKES_TABLE, entry);
});
});
}
}