MOBILE-4304 scorm: Update database usage
parent
38d0ad1aad
commit
b6f32dfddd
|
@ -13,7 +13,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { SQLiteDB } from '@classes/sqlitedb';
|
|
||||||
import { CoreUser } from '@features/user/services/user';
|
import { CoreUser } from '@features/user/services/user';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreSync } from '@services/sync';
|
import { CoreSync } from '@services/sync';
|
||||||
|
@ -38,6 +37,10 @@ import {
|
||||||
AddonModScormUserDataMap,
|
AddonModScormUserDataMap,
|
||||||
AddonModScormWSSco,
|
AddonModScormWSSco,
|
||||||
} from './scorm';
|
} from './scorm';
|
||||||
|
import { lazyMap, LazyMap } from '@/core/utils/lazy-map';
|
||||||
|
import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance';
|
||||||
|
import { CoreDatabaseTable } from '@classes/database/database-table';
|
||||||
|
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to handle offline SCORM.
|
* Service to handle offline SCORM.
|
||||||
|
@ -47,8 +50,36 @@ export class AddonModScormOfflineProvider {
|
||||||
|
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
|
|
||||||
|
protected tracksTables: LazyMap<
|
||||||
|
AsyncInstance<CoreDatabaseTable<AddonModScormTrackDBRecord, 'scormid' | 'userid' | 'attempt' | 'scoid' | 'element'>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
protected attemptsTables: LazyMap<
|
||||||
|
AsyncInstance<CoreDatabaseTable<AddonModScormAttemptDBRecord, 'scormid' | 'userid' | 'attempt'>>
|
||||||
|
>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.logger = CoreLogger.getInstance('AddonModScormOfflineProvider');
|
this.logger = CoreLogger.getInstance('AddonModScormOfflineProvider');
|
||||||
|
this.tracksTables = lazyMap(
|
||||||
|
siteId => asyncInstance(
|
||||||
|
() => CoreSites.getSiteTable(TRACKS_TABLE_NAME, {
|
||||||
|
siteId,
|
||||||
|
primaryKeyColumns: ['scormid', 'userid', 'attempt', 'scoid', 'element'],
|
||||||
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
|
||||||
|
onDestroy: () => delete this.tracksTables[siteId],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.attemptsTables = lazyMap(
|
||||||
|
siteId => asyncInstance(
|
||||||
|
() => CoreSites.getSiteTable(ATTEMPTS_TABLE_NAME, {
|
||||||
|
siteId,
|
||||||
|
primaryKeyColumns: ['scormid', 'userid', 'attempt'],
|
||||||
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
|
||||||
|
onDestroy: () => delete this.tracksTables[siteId],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,40 +107,43 @@ export class AddonModScormOfflineProvider {
|
||||||
|
|
||||||
this.logger.debug(`Change attempt number from ${attempt} to ${newAttempt} in SCORM ${scormId}`);
|
this.logger.debug(`Change attempt number from ${attempt} to ${newAttempt} in SCORM ${scormId}`);
|
||||||
|
|
||||||
// Update the attempt number.
|
|
||||||
const db = site.getDb();
|
|
||||||
const currentAttemptConditions: AddonModScormOfflineDBCommonData = {
|
|
||||||
scormid: scormId,
|
|
||||||
userid: userId,
|
|
||||||
attempt,
|
|
||||||
};
|
|
||||||
const newAttemptConditions: AddonModScormOfflineDBCommonData = {
|
|
||||||
scormid: scormId,
|
|
||||||
userid: userId,
|
|
||||||
attempt: newAttempt,
|
|
||||||
};
|
|
||||||
const newAttemptData: Partial<AddonModScormAttemptDBRecord> = {
|
|
||||||
attempt: newAttempt,
|
|
||||||
timemodified: CoreTimeUtils.timestamp(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Block the SCORM so it can't be synced.
|
// Block the SCORM so it can't be synced.
|
||||||
CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scormId, 'changeAttemptNumber', site.id);
|
CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scormId, 'changeAttemptNumber', site.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.updateRecords(ATTEMPTS_TABLE_NAME, newAttemptData, currentAttemptConditions);
|
const currentAttemptConditions = {
|
||||||
|
sql: 'scormid = ? AND userid = ? AND attempt = ?',
|
||||||
|
sqlParams: [scormId, userId, attempt],
|
||||||
|
js: (record: AddonModScormOfflineDBCommonData) =>
|
||||||
|
record.scormid === scormId &&
|
||||||
|
record.userid === userId &&
|
||||||
|
record.attempt === attempt,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.attemptsTables[site.id].updateWhere(
|
||||||
|
{ attempt: newAttempt, timemodified: CoreTimeUtils.timestamp() },
|
||||||
|
currentAttemptConditions,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Now update the attempt number of all the tracks and mark them as not synced.
|
// Now update the attempt number of all the tracks and mark them as not synced.
|
||||||
const newTrackData: Partial<AddonModScormTrackDBRecord> = {
|
await this.tracksTables[site.id].updateWhere(
|
||||||
attempt: newAttempt,
|
{ attempt: newAttempt, synced: 0 },
|
||||||
synced: 0,
|
currentAttemptConditions,
|
||||||
};
|
);
|
||||||
|
|
||||||
await db.updateRecords(TRACKS_TABLE_NAME, newTrackData, currentAttemptConditions);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Failed to update the tracks, restore the old attempt number.
|
// Failed to update the tracks, restore the old attempt number.
|
||||||
await db.updateRecords(ATTEMPTS_TABLE_NAME, { attempt }, newAttemptConditions);
|
await this.attemptsTables[site.id].updateWhere(
|
||||||
|
{ attempt },
|
||||||
|
{
|
||||||
|
sql: 'scormid = ? AND userid = ? AND attempt = ?',
|
||||||
|
sqlParams: [scormId, userId, newAttempt],
|
||||||
|
js: (attempt) =>
|
||||||
|
attempt.scormid === scormId &&
|
||||||
|
attempt.userid === userId &&
|
||||||
|
attempt.attempt === newAttempt,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
@ -148,7 +182,6 @@ export class AddonModScormOfflineProvider {
|
||||||
CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scorm.id, 'createNewAttempt', site.id);
|
CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scorm.id, 'createNewAttempt', site.id);
|
||||||
|
|
||||||
// Create attempt in DB.
|
// Create attempt in DB.
|
||||||
const db = site.getDb();
|
|
||||||
const entry: AddonModScormAttemptDBRecord = {
|
const entry: AddonModScormAttemptDBRecord = {
|
||||||
scormid: scorm.id,
|
scormid: scorm.id,
|
||||||
userid: userId,
|
userid: userId,
|
||||||
|
@ -166,7 +199,7 @@ export class AddonModScormOfflineProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.insertRecord(ATTEMPTS_TABLE_NAME, entry);
|
await this.attemptsTables[site.id].insert(entry);
|
||||||
|
|
||||||
// Store all the data in userData.
|
// Store all the data in userData.
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
|
@ -204,16 +237,15 @@ export class AddonModScormOfflineProvider {
|
||||||
|
|
||||||
this.logger.debug(`Delete offline attempt ${attempt} in SCORM ${scormId}`);
|
this.logger.debug(`Delete offline attempt ${attempt} in SCORM ${scormId}`);
|
||||||
|
|
||||||
const db = site.getDb();
|
const conditions = {
|
||||||
const conditions: AddonModScormOfflineDBCommonData = {
|
|
||||||
scormid: scormId,
|
scormid: scormId,
|
||||||
userid: userId,
|
userid: userId,
|
||||||
attempt,
|
attempt,
|
||||||
};
|
};
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.deleteRecords(ATTEMPTS_TABLE_NAME, conditions),
|
this.attemptsTables[site.id].delete(conditions),
|
||||||
db.deleteRecords(TRACKS_TABLE_NAME, conditions),
|
this.tracksTables[site.id].delete(conditions),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,9 +312,9 @@ export class AddonModScormOfflineProvider {
|
||||||
* @returns Promise resolved when the offline attempts are retrieved.
|
* @returns Promise resolved when the offline attempts are retrieved.
|
||||||
*/
|
*/
|
||||||
async getAllAttempts(siteId?: string): Promise<AddonModScormOfflineAttempt[]> {
|
async getAllAttempts(siteId?: string): Promise<AddonModScormOfflineAttempt[]> {
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
siteId ??= CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
const attempts = await db.getAllRecords<AddonModScormAttemptDBRecord>(ATTEMPTS_TABLE_NAME);
|
const attempts = await this.attemptsTables[siteId].getMany();
|
||||||
|
|
||||||
return attempts.map((attempt) => this.parseAttempt(attempt));
|
return attempts.map((attempt) => this.parseAttempt(attempt));
|
||||||
}
|
}
|
||||||
|
@ -300,7 +332,7 @@ export class AddonModScormOfflineProvider {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
userId = userId || site.getUserId();
|
userId = userId || site.getUserId();
|
||||||
|
|
||||||
const attemptRecord = await site.getDb().getRecord<AddonModScormAttemptDBRecord>(ATTEMPTS_TABLE_NAME, {
|
const attemptRecord = await this.attemptsTables[site.id].getOneByPrimaryKey({
|
||||||
scormid: scormId,
|
scormid: scormId,
|
||||||
userid: userId,
|
userid: userId,
|
||||||
attempt,
|
attempt,
|
||||||
|
@ -340,7 +372,7 @@ export class AddonModScormOfflineProvider {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
userId = userId || site.getUserId();
|
userId = userId || site.getUserId();
|
||||||
|
|
||||||
const attempts = await site.getDb().getRecords<AddonModScormAttemptDBRecord>(ATTEMPTS_TABLE_NAME, {
|
const attempts = await this.attemptsTables[site.id].getMany({
|
||||||
scormid: scormId,
|
scormid: scormId,
|
||||||
userid: userId,
|
userid: userId,
|
||||||
});
|
});
|
||||||
|
@ -428,7 +460,7 @@ export class AddonModScormOfflineProvider {
|
||||||
conditions.synced = 1;
|
conditions.synced = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tracks = await site.getDb().getRecords<AddonModScormTrackDBRecord>(TRACKS_TABLE_NAME, conditions);
|
const tracks = await this.tracksTables[site.id].getMany(conditions);
|
||||||
|
|
||||||
return this.parseTracks(tracks);
|
return this.parseTracks(tracks);
|
||||||
}
|
}
|
||||||
|
@ -598,7 +630,6 @@ export class AddonModScormOfflineProvider {
|
||||||
userId = userId || site.getUserId();
|
userId = userId || site.getUserId();
|
||||||
|
|
||||||
const scoUserData = scoData?.userdata || {};
|
const scoUserData = scoData?.userdata || {};
|
||||||
const db = site.getDb();
|
|
||||||
let lessonStatusInserted = false;
|
let lessonStatusInserted = false;
|
||||||
|
|
||||||
if (forceCompleted) {
|
if (forceCompleted) {
|
||||||
|
@ -611,7 +642,16 @@ export class AddonModScormOfflineProvider {
|
||||||
if (scoUserData['cmi.core.lesson_status'] == 'incomplete') {
|
if (scoUserData['cmi.core.lesson_status'] == 'incomplete') {
|
||||||
lessonStatusInserted = true;
|
lessonStatusInserted = true;
|
||||||
|
|
||||||
await this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'completed');
|
await this.tracksTables[site.id].insert({
|
||||||
|
userid: userId,
|
||||||
|
scormid: scormId,
|
||||||
|
scoid: scoId,
|
||||||
|
attempt,
|
||||||
|
element: 'cmi.core.lesson_status',
|
||||||
|
value: JSON.stringify('completed'),
|
||||||
|
timemodified: CoreTimeUtils.timestamp(),
|
||||||
|
synced: 0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -622,78 +662,32 @@ export class AddonModScormOfflineProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.insertTrackToDB(db, userId, scormId, scoId, attempt, element, value);
|
await this.tracksTables[site.id].insert({
|
||||||
} catch (error) {
|
|
||||||
if (lessonStatusInserted) {
|
|
||||||
// Rollback previous insert.
|
|
||||||
await this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'incomplete');
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a track in the DB.
|
|
||||||
*
|
|
||||||
* @param db Site's DB.
|
|
||||||
* @param userId User ID.
|
|
||||||
* @param scormId SCORM ID.
|
|
||||||
* @param scoId SCO ID.
|
|
||||||
* @param attempt Attempt number.
|
|
||||||
* @param element Name of the element to insert.
|
|
||||||
* @param value Value of the element to insert.
|
|
||||||
* @param synchronous True if insert should NOT return a promise. Please use it only if synchronous is a must.
|
|
||||||
* @returns Returns a promise if synchronous=false, otherwise returns a boolean.
|
|
||||||
*/
|
|
||||||
protected insertTrackToDB(
|
|
||||||
db: SQLiteDB,
|
|
||||||
userId: number,
|
|
||||||
scormId: number,
|
|
||||||
scoId: number,
|
|
||||||
attempt: number,
|
|
||||||
element: string,
|
|
||||||
value: AddonModScormDataValue | undefined,
|
|
||||||
synchronous: true,
|
|
||||||
): boolean;
|
|
||||||
protected insertTrackToDB(
|
|
||||||
db: SQLiteDB,
|
|
||||||
userId: number,
|
|
||||||
scormId: number,
|
|
||||||
scoId: number,
|
|
||||||
attempt: number,
|
|
||||||
element: string,
|
|
||||||
value?: AddonModScormDataValue,
|
|
||||||
synchronous?: false,
|
|
||||||
): Promise<number>;
|
|
||||||
protected insertTrackToDB(
|
|
||||||
db: SQLiteDB,
|
|
||||||
userId: number,
|
|
||||||
scormId: number,
|
|
||||||
scoId: number,
|
|
||||||
attempt: number,
|
|
||||||
element: string,
|
|
||||||
value?: AddonModScormDataValue,
|
|
||||||
synchronous?: boolean,
|
|
||||||
): boolean | Promise<number> {
|
|
||||||
const entry: AddonModScormTrackDBRecord = {
|
|
||||||
userid: userId,
|
userid: userId,
|
||||||
scormid: scormId,
|
scormid: scormId,
|
||||||
scoid: scoId,
|
scoid: scoId,
|
||||||
attempt,
|
attempt,
|
||||||
element: element,
|
element,
|
||||||
value: value === undefined ? null : JSON.stringify(value),
|
value: value === undefined ? null : JSON.stringify(value),
|
||||||
timemodified: CoreTimeUtils.timestamp(),
|
timemodified: CoreTimeUtils.timestamp(),
|
||||||
synced: 0,
|
synced: 0,
|
||||||
};
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (lessonStatusInserted) {
|
||||||
|
// Rollback previous insert.
|
||||||
|
await this.tracksTables[site.id].insert({
|
||||||
|
userid: userId,
|
||||||
|
scormid: scormId,
|
||||||
|
scoid: scoId,
|
||||||
|
attempt,
|
||||||
|
element: 'cmi.core.lesson_status',
|
||||||
|
value: JSON.stringify('incomplete'),
|
||||||
|
timemodified: CoreTimeUtils.timestamp(),
|
||||||
|
synced: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (synchronous) {
|
throw error;
|
||||||
// The insert operation is always asynchronous, always return true.
|
|
||||||
db.insertRecord(TRACKS_TABLE_NAME, entry);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return db.insertRecord(TRACKS_TABLE_NAME, entry);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -730,8 +724,7 @@ export class AddonModScormOfflineProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
const scoUserData = scoData?.userdata || {};
|
const scoUserData = scoData?.userdata || {};
|
||||||
const db = CoreSites.getRequiredCurrentSite().getDb();
|
const siteId = CoreSites.getRequiredCurrentSite().id;
|
||||||
let lessonStatusInserted = false;
|
|
||||||
|
|
||||||
if (forceCompleted) {
|
if (forceCompleted) {
|
||||||
if (element == 'cmi.core.lesson_status' && value == 'incomplete') {
|
if (element == 'cmi.core.lesson_status' && value == 'incomplete') {
|
||||||
|
@ -741,11 +734,16 @@ export class AddonModScormOfflineProvider {
|
||||||
}
|
}
|
||||||
if (element == 'cmi.core.score.raw') {
|
if (element == 'cmi.core.score.raw') {
|
||||||
if (scoUserData['cmi.core.lesson_status'] == 'incomplete') {
|
if (scoUserData['cmi.core.lesson_status'] == 'incomplete') {
|
||||||
lessonStatusInserted = true;
|
this.tracksTables[siteId].syncInsert({
|
||||||
|
userid: userId,
|
||||||
if (!this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'completed', true)) {
|
scormid: scormId,
|
||||||
return false;
|
scoid: scoId,
|
||||||
}
|
attempt,
|
||||||
|
element: 'cmi.core.lesson_status',
|
||||||
|
value: JSON.stringify('completed'),
|
||||||
|
timemodified: CoreTimeUtils.timestamp(),
|
||||||
|
synced: 0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -755,15 +753,16 @@ export class AddonModScormOfflineProvider {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.insertTrackToDB(db, userId, scormId, scoId, attempt, element, value, true)) {
|
this.tracksTables[siteId].syncInsert({
|
||||||
// Insert failed.
|
userid: userId,
|
||||||
if (lessonStatusInserted) {
|
scormid: scormId,
|
||||||
// Rollback previous insert.
|
scoid: scoId,
|
||||||
this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'incomplete', true);
|
attempt,
|
||||||
}
|
element: element,
|
||||||
|
value: value === undefined ? null : JSON.stringify(value),
|
||||||
return false;
|
timemodified: CoreTimeUtils.timestamp(),
|
||||||
}
|
synced: 0,
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -784,7 +783,7 @@ export class AddonModScormOfflineProvider {
|
||||||
|
|
||||||
this.logger.debug(`Mark SCO ${scoId} as synced for attempt ${attempt} in SCORM ${scormId}`);
|
this.logger.debug(`Mark SCO ${scoId} as synced for attempt ${attempt} in SCORM ${scormId}`);
|
||||||
|
|
||||||
await site.getDb().updateRecords(TRACKS_TABLE_NAME, { synced: 1 }, <Partial<AddonModScormTrackDBRecord>> {
|
await this.tracksTables[site.id].update({ synced: 1 }, {
|
||||||
scormid: scormId,
|
scormid: scormId,
|
||||||
userid: userId,
|
userid: userId,
|
||||||
attempt,
|
attempt,
|
||||||
|
@ -971,10 +970,13 @@ export class AddonModScormOfflineProvider {
|
||||||
snapshot: JSON.stringify(this.removeDefaultData(userData)),
|
snapshot: JSON.stringify(this.removeDefaultData(userData)),
|
||||||
};
|
};
|
||||||
|
|
||||||
await site.getDb().updateRecords(ATTEMPTS_TABLE_NAME, newData, <Partial<AddonModScormAttemptDBRecord>> {
|
await this.attemptsTables[site.id].updateWhere(newData, {
|
||||||
scormid: scormId,
|
sql: 'scormid = ? AND userid = ? AND attempt = ?',
|
||||||
userid: userId,
|
sqlParams: [scormId, userId, attempt],
|
||||||
attempt,
|
js: (record: AddonModScormOfflineDBCommonData) =>
|
||||||
|
record.scormid === scormId &&
|
||||||
|
record.userid === userId &&
|
||||||
|
record.attempt === attempt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -258,6 +258,18 @@ export class CoreDatabaseTable<
|
||||||
await this.database.insertRecord(this.tableName, record);
|
await this.database.insertRecord(this.tableName, record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new record synchronously.
|
||||||
|
*
|
||||||
|
* @param record Database record.
|
||||||
|
*/
|
||||||
|
syncInsert(record: DBRecord): void {
|
||||||
|
// The current database architecture does not support synchronous operations,
|
||||||
|
// so calling this method will mean that errors will be silenced. Because of that,
|
||||||
|
// this should only be called if using the asynchronous alternatives is not possible.
|
||||||
|
this.insert(record);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update records matching the given conditions.
|
* Update records matching the given conditions.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue