From c83ff34ae0a3b31796c499e794982f2ee621e1df Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 16 Dec 2020 13:08:20 +0100 Subject: [PATCH] MOBILE-3666 xapi: Implement XAPI services --- src/core/features/features.module.ts | 2 + .../features/xapi/services/database/xapi.ts | 74 +++++++++ src/core/features/xapi/services/offline.ts | 144 +++++++++++++++++ src/core/features/xapi/services/xapi.ts | 146 ++++++++++++++++++ src/core/features/xapi/xapi.module.ts | 32 ++++ 5 files changed, 398 insertions(+) create mode 100644 src/core/features/xapi/services/database/xapi.ts create mode 100644 src/core/features/xapi/services/offline.ts create mode 100644 src/core/features/xapi/services/xapi.ts create mode 100644 src/core/features/xapi/xapi.module.ts diff --git a/src/core/features/features.module.ts b/src/core/features/features.module.ts index ff237f5d9..b3d5c06e5 100644 --- a/src/core/features/features.module.ts +++ b/src/core/features/features.module.ts @@ -25,6 +25,7 @@ import { CoreSiteHomeModule } from './sitehome/sitehome.module'; import { CoreTagModule } from './tag/tag.module'; import { CoreUserModule } from './user/user.module'; import { CorePushNotificationsModule } from './pushnotifications/pushnotifications.module'; +import { CoreXAPIModule } from './xapi/xapi.module'; @NgModule({ imports: [ @@ -39,6 +40,7 @@ import { CorePushNotificationsModule } from './pushnotifications/pushnotificatio CoreTagModule, CoreUserModule, CorePushNotificationsModule, + CoreXAPIModule, ], }) export class CoreFeaturesModule {} diff --git a/src/core/features/xapi/services/database/xapi.ts b/src/core/features/xapi/services/database/xapi.ts new file mode 100644 index 000000000..6164804fd --- /dev/null +++ b/src/core/features/xapi/services/database/xapi.ts @@ -0,0 +1,74 @@ +// (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 CoreXAPIOfflineProvider service. + */ +export const STATEMENTS_TABLE_NAME = 'core_xapi_statements'; +export const SITE_SCHEMA: CoreSiteSchema = { + name: 'CoreXAPIOfflineProvider', + version: 1, + tables: [ + { + name: STATEMENTS_TABLE_NAME, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'contextid', + type: 'INTEGER', + }, + { + name: 'component', + type: 'TEXT', + }, + { + name: 'statements', + type: 'TEXT', + }, + { + name: 'timecreated', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'extra', + type: 'TEXT', + }, + ], + }, + ], +}; + +/** + * Structure of statement data stored in DB. + */ +export type CoreXAPIStatementDBRecord = { + id: number; // ID. + contextid: number; // Context ID of the statements. + component: string; // Component to send the statements to. + statements: string; // Statements (JSON-encoded). + timecreated: number; // When were the statements created. + courseid?: number; // Course ID if the context is inside a course. + extra?: string; // Extra data. +}; diff --git a/src/core/features/xapi/services/offline.ts b/src/core/features/xapi/services/offline.ts new file mode 100644 index 000000000..32563902c --- /dev/null +++ b/src/core/features/xapi/services/offline.ts @@ -0,0 +1,144 @@ +// (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 { makeSingleton } from '@singletons'; +import { CoreXAPIStatementDBRecord, STATEMENTS_TABLE_NAME } from './database/xapi'; + +/** + * Service to handle offline xAPI. + */ +@Injectable({ providedIn: 'root' }) +export class CoreXAPIOfflineProvider { + + /** + * Check if there are offline statements to send for a context. + * + * @param contextId Context ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: true if has offline statements, false otherwise. + */ + async contextHasStatements(contextId: number, siteId?: string): Promise { + const statementsList = await this.getContextStatements(contextId, siteId); + + return statementsList && statementsList.length > 0; + } + + /** + * Delete certain statements. + * + * @param id ID of the statements. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if stored, rejected if failure. + */ + async deleteStatements(id: number, siteId?: string): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + + await db.deleteRecords(STATEMENTS_TABLE_NAME, { id }); + } + + /** + * Delete all statements of a certain context. + * + * @param contextId Context ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if stored, rejected if failure. + */ + async deleteStatementsForContext(contextId: number, siteId?: string): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + + await db.deleteRecords(STATEMENTS_TABLE_NAME, { contextid: contextId }); + } + + /** + * Get all offline statements. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with all the data. + */ + async getAllStatements(siteId?: string): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + + return db.getRecords(STATEMENTS_TABLE_NAME, undefined, 'timecreated ASC'); + } + + /** + * Get statements for a context. + * + * @param contextId Context ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the data. + */ + async getContextStatements(contextId: number, siteId?: string): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + + return db.getRecords(STATEMENTS_TABLE_NAME, { contextid: contextId }, 'timecreated ASC'); + } + + /** + * Get certain statements. + * + * @param id ID of the statements. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the data. + */ + async getStatements(id: number, siteId?: string): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + + return db.getRecord(STATEMENTS_TABLE_NAME, { id }); + } + + /** + * Save statements. + * + * @param contextId Context ID. + * @param component Component to send the statements to. + * @param statements Statements (JSON-encoded). + * @param options Options. + * @return Promise resolved when statements are successfully saved. + */ + async saveStatements( + contextId: number, + component: string, + statements: string, + options?: CoreXAPIOfflineSaveStatementsOptions, + ): Promise { + const db = await CoreSites.instance.getSiteDb(options?.siteId); + + const entry: Omit = { + contextid: contextId, + component: component, + statements: statements, + timecreated: Date.now(), + courseid: options?.courseId, + extra: options?.extra, + }; + + await db.insertRecord(STATEMENTS_TABLE_NAME, entry); + } + +} + +export class CoreXAPIOffline extends makeSingleton(CoreXAPIOfflineProvider) {} + +/** + * Options to pass to saveStatements function. + */ +export type CoreXAPIOfflineSaveStatementsOptions = { + courseId?: number; // Course ID if the context is inside a course. + extra?: string; // Extra data to store. + siteId?: string; // Site ID. If not defined, current site. +}; diff --git a/src/core/features/xapi/services/xapi.ts b/src/core/features/xapi/services/xapi.ts new file mode 100644 index 000000000..7ea06df17 --- /dev/null +++ b/src/core/features/xapi/services/xapi.ts @@ -0,0 +1,146 @@ +// (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 { CoreApp } from '@services/app'; +import { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreSite } from '@classes/site'; +import { CoreXAPIOffline, CoreXAPIOfflineSaveStatementsOptions } from './offline'; +import { makeSingleton } from '@singletons'; + +/** + * Service to provide XAPI functionalities. + */ +@Injectable({ providedIn: 'root' }) +export class CoreXAPIProvider { + + /** + * Returns whether or not WS to post XAPI statement is available. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if ws is available, false otherwise. + * @since 3.9 + */ + async canPostStatements(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.canPostStatementsInSite(site); + } + + /** + * Returns whether or not WS to post XAPI statement is available in a certain site. + * + * @param site Site. If not defined, current site. + * @return Promise resolved with true if ws is available, false otherwise. + * @since 3.9 + */ + canPostStatementsInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!(site && site.wsAvailable('core_xapi_statement_post')); + } + + /** + * Get URL for XAPI events. + * + * @param contextId Context ID. + * @param type Type (e.g. 'activity'). + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async getUrl(contextId: number, type: string, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return CoreTextUtils.instance.concatenatePaths(site.getURL(), `xapi/${type}/${contextId}`); + } + + /** + * Post statements. + * + * @param contextId Context ID. + * @param component Component. + * @param json JSON string to send. + * @param options Options. + * @return Promise resolved with boolean: true if response was sent to server, false if stored in device. + */ + async postStatements( + contextId: number, + component: string, + json: string, + options?: CoreXAPIPostStatementsOptions, + ): Promise { + + options = options || {}; + options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId(); + + // Convenience function to store a message to be synchronized later. + const storeOffline = async (): Promise => { + await CoreXAPIOffline.instance.saveStatements(contextId, component, json, options); + + return false; + }; + + if (!CoreApp.instance.isOnline() || options.offline) { + // App is offline, store the action. + return storeOffline(); + } + + try { + await this.postStatementsOnline(component, json, options.siteId); + + return true; + } catch (error) { + if (CoreUtils.instance.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; + } else { + // Couldn't connect to server, store it offline. + return storeOffline(); + } + } + } + + /** + * Post statements. It will fail if offline or cannot connect. + * + * @param component Component. + * @param json JSON string to send. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async postStatementsOnline(component: string, json: string, siteId?: string): Promise { + + const site = await CoreSites.instance.getSite(siteId); + + const data = { + component: component, + requestjson: json, + }; + + return site.write('core_xapi_statement_post', data); + } + +} + +export class CoreXAPI extends makeSingleton(CoreXAPIProvider) {} + +/** + * Options to pass to postStatements function. + */ +export type CoreXAPIPostStatementsOptions = CoreXAPIOfflineSaveStatementsOptions & { + offline?: boolean; // Whether to force storing it in offline. +}; diff --git a/src/core/features/xapi/xapi.module.ts b/src/core/features/xapi/xapi.module.ts new file mode 100644 index 000000000..8671585e0 --- /dev/null +++ b/src/core/features/xapi/xapi.module.ts @@ -0,0 +1,32 @@ +// (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 { STATEMENTS_TABLE_NAME } from './services/database/xapi'; + +@NgModule({ + imports: [], + providers: [ + { + provide: CORE_SITE_SCHEMAS, + useValue: [ + STATEMENTS_TABLE_NAME, + ], + multi: true, + }, + ], +}) +export class CoreXAPIModule {}