MOBILE-4269 h5pactivity: Support save state in online
parent
0deeffac2d
commit
dfe185f28c
|
@ -64,7 +64,8 @@
|
|||
</ion-list>
|
||||
|
||||
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"
|
||||
[trackComponent]="trackComponent" [contextId]="h5pActivity?.context" [enableInAppFullscreen]="true">
|
||||
[trackComponent]="trackComponent" [contextId]="h5pActivity?.context" [enableInAppFullscreen]="true" [saveFreq]="saveFreq"
|
||||
[state]="contentState">
|
||||
</core-h5p-iframe>
|
||||
</core-loading>
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ import { CoreXAPI } from '@features/xapi/services/xapi';
|
|||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreWSFile } from '@services/ws';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
|
@ -36,7 +36,9 @@ import {
|
|||
AddonModH5PActivityAccessInfo,
|
||||
AddonModH5PActivityData,
|
||||
AddonModH5PActivityProvider,
|
||||
AddonModH5PActivityXAPIData,
|
||||
AddonModH5PActivityXAPIPostStateData,
|
||||
AddonModH5PActivityXAPIStateData,
|
||||
AddonModH5PActivityXAPIStatementsData,
|
||||
} from '../../services/h5pactivity';
|
||||
import {
|
||||
AddonModH5PActivitySync,
|
||||
|
@ -45,6 +47,7 @@ import {
|
|||
} from '../../services/h5pactivity-sync';
|
||||
import { CoreFileHelper } from '@services/file-helper';
|
||||
import { AddonModH5PActivityModuleHandlerService } from '../../services/handlers/module';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
|
||||
/**
|
||||
* Component that displays an H5P activity entry page.
|
||||
|
@ -80,6 +83,9 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
hasOffline = false;
|
||||
isOpeningPage = false;
|
||||
canViewAllAttempts = false;
|
||||
saveStateEnabled = false;
|
||||
saveFreq?: number;
|
||||
contentState?: string;
|
||||
|
||||
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
|
||||
protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED;
|
||||
|
@ -133,6 +139,8 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
this.fetchDeployedFileData(),
|
||||
]);
|
||||
|
||||
await this.loadContentState(); // Loading the state requires the access info.
|
||||
|
||||
this.trackComponent = this.accessInfo?.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : '';
|
||||
this.canViewAllAttempts = !!this.h5pActivity.enabletracking && !!this.accessInfo?.canreviewattempts &&
|
||||
AddonModH5PActivity.canGetUsersAttemptsInSite();
|
||||
|
@ -217,6 +225,40 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
await this.calculateFileState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the content's state (if enabled and there's any).
|
||||
*/
|
||||
protected async loadContentState(): Promise<void> {
|
||||
if (!this.h5pActivity?.enabletracking || !this.h5pActivity.enablesavestate || !this.accessInfo?.cansubmit) {
|
||||
this.saveStateEnabled = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.saveStateEnabled = true;
|
||||
this.saveFreq = this.h5pActivity.savestatefreq;
|
||||
|
||||
const contentState = await CoreXAPI.getStateOnline(
|
||||
AddonModH5PActivityProvider.TRACK_COMPONENT,
|
||||
this.h5pActivity.context,
|
||||
'state',
|
||||
{
|
||||
appComponent: AddonModH5PActivityProvider.COMPONENT,
|
||||
appComponentId: this.h5pActivity.coursemodule,
|
||||
readingStrategy: CoreSitesReadingStrategy.PREFER_NETWORK,
|
||||
},
|
||||
);
|
||||
|
||||
if (contentState === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentStateObj = CoreTextUtils.parseJSON<{h5p: string}>(contentState, { h5p: '{}' });
|
||||
|
||||
// The H5P state doesn't always use JSON, so an h5p property was added to jsonize it.
|
||||
this.contentState = contentStateObj.h5p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the state of the deployed file.
|
||||
*
|
||||
|
@ -434,7 +476,78 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
*/
|
||||
protected async onIframeMessage(event: MessageEvent): Promise<void> {
|
||||
const data = event.data;
|
||||
if (!data || !this.h5pActivity || !CoreXAPI.canPostStatementsInSite(this.site) || !this.isCurrentXAPIPost(data)) {
|
||||
if (!data || !this.h5pActivity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CoreXAPI.canPostStatementsInSite(this.site) && this.isCurrentXAPIPostStatement(data)) {
|
||||
this.postStatements(data);
|
||||
} else if (this.saveStateEnabled && this.isCurrentXAPIState(data, 'xapi_post_state') && this.isXAPIPostState(data)) {
|
||||
this.postState(data);
|
||||
} else if (this.saveStateEnabled && this.isCurrentXAPIState(data, 'xapi_delete_state')) {
|
||||
this.deleteState(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event is an H5P event meant for this app.
|
||||
*
|
||||
* @param data Event data.
|
||||
* @returns Whether it's an H5P event meant for this app.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected isH5PEventForApp(data: any): boolean {
|
||||
return data.environment === 'moodleapp' && data.context === 'h5p';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an activity ID (an IRI) belongs to the current activity.
|
||||
*
|
||||
* @param activityId Activity ID (IRI).
|
||||
* @returns Whether it belongs to the current activity.
|
||||
*/
|
||||
protected activityIdIsCurrentActivity(activityId?: string): boolean {
|
||||
if (!activityId || !this.h5pActivity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.site.containsUrl(activityId)) {
|
||||
// The event belongs to another site, weird scenario. Maybe some JS running in background.
|
||||
return false;
|
||||
}
|
||||
|
||||
const match = activityId.match(/xapi\/activity\/(\d+)/);
|
||||
|
||||
return !!match && Number(match[1]) === this.h5pActivity.context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event is an XAPI post statement of the current activity.
|
||||
*
|
||||
* @param data Event data.
|
||||
* @returns Whether it's an XAPI post statement of the current activity.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected isCurrentXAPIPostStatement(data: any): data is AddonModH5PActivityXAPIStatementsData {
|
||||
if (!this.h5pActivity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.isH5PEventForApp(data) || data.action !== 'xapi_post_statement' || !data.statements) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the event belongs to this activity.
|
||||
return this.activityIdIsCurrentActivity(data.statements[0] && data.statements[0].object && data.statements[0].object.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post statements.
|
||||
*
|
||||
* @param data Event data.
|
||||
*/
|
||||
protected async postStatements(data: AddonModH5PActivityXAPIStatementsData): Promise<void> {
|
||||
if (!this.h5pActivity) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -476,35 +589,77 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if an event is an XAPI post statement of the current activity.
|
||||
* Check if an event is an XAPI state event of the current activity.
|
||||
*
|
||||
* @param data Event data.
|
||||
* @returns Whether it's an XAPI post statement of the current activity.
|
||||
* @param action Action to check.
|
||||
* @returns Whether it's an XAPI state event of the current activity.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected isCurrentXAPIPost(data: any): data is AddonModH5PActivityXAPIData {
|
||||
protected isCurrentXAPIState(data: any, action: string): data is AddonModH5PActivityXAPIStateData {
|
||||
if (!this.h5pActivity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.environment != 'moodleapp' || data.context != 'h5p' || data.action != 'xapi_post_statement' || !data.statements) {
|
||||
if (!this.isH5PEventForApp(data) || data.action !== action) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the event belongs to this activity.
|
||||
const trackingUrl = data.statements[0] && data.statements[0].object && data.statements[0].object.id;
|
||||
if (!trackingUrl) {
|
||||
return false;
|
||||
return this.activityIdIsCurrentActivity(data.activityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an xAPI state event data is a post state event.
|
||||
*
|
||||
* @param data Event data.
|
||||
* @returns Whether it's an XAPI post state.
|
||||
*/
|
||||
protected isXAPIPostState(data: AddonModH5PActivityXAPIStateData): data is AddonModH5PActivityXAPIPostStateData {
|
||||
return 'stateData' in data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post state.
|
||||
*
|
||||
* @param data Event data.
|
||||
*/
|
||||
protected async postState(data: AddonModH5PActivityXAPIPostStateData): Promise<void> {
|
||||
try {
|
||||
await CoreXAPI.postStateOnline(
|
||||
data.component,
|
||||
data.activityId,
|
||||
JSON.stringify(data.agent),
|
||||
data.stateId,
|
||||
data.stateData,
|
||||
{
|
||||
siteId: this.site.getId(),
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error sending tracking data.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.site.containsUrl(trackingUrl)) {
|
||||
// The event belongs to another site, weird scenario. Maybe some JS running in background.
|
||||
return false;
|
||||
/**
|
||||
* Delete state.
|
||||
*
|
||||
* @param data Event data.
|
||||
*/
|
||||
protected async deleteState(data: AddonModH5PActivityXAPIStateData): Promise<void> {
|
||||
try {
|
||||
await CoreXAPI.deleteStateOnline(
|
||||
data.component,
|
||||
data.activityId,
|
||||
JSON.stringify(data.agent),
|
||||
data.stateId,
|
||||
{
|
||||
siteId: this.site.getId(),
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error sending tracking data.');
|
||||
}
|
||||
|
||||
const match = trackingUrl.match(/xapi\/activity\/(\d+)/);
|
||||
|
||||
return match && match[1] == this.h5pActivity.context;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -535,7 +690,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
|||
super.ngOnDestroy();
|
||||
|
||||
this.observer?.off();
|
||||
window.removeEventListener('message', this.messageListenerFunction);
|
||||
|
||||
// Wait a bit to make sure all messages have been received.
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('message', this.messageListenerFunction);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ export class AddonModH5PActivityIndexPage extends CoreCourseModuleMainActivityPa
|
|||
* @inheritdoc
|
||||
*/
|
||||
async canLeave(): Promise<boolean> {
|
||||
if (!this.activityComponent || !this.activityComponent.playing || this.activityComponent.isOpeningPage) {
|
||||
if (!this.activityComponent?.playing || this.activityComponent.isOpeningPage || this.activityComponent.saveStateEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -528,7 +528,10 @@ export class AddonModH5PActivityProvider {
|
|||
const currentActivity = response.h5pactivities.find((h5pActivity) => h5pActivity[key] == value);
|
||||
|
||||
if (currentActivity) {
|
||||
return currentActivity;
|
||||
return {
|
||||
...currentActivity,
|
||||
...response.h5pglobalsettings,
|
||||
};
|
||||
}
|
||||
|
||||
throw new CoreError(Translate.instant('core.course.modulenotfound'));
|
||||
|
@ -822,7 +825,7 @@ export const AddonModH5PActivity = makeSingleton(AddonModH5PActivityProvider);
|
|||
/**
|
||||
* Basic data for an H5P activity, exported by Moodle class h5pactivity_summary_exporter.
|
||||
*/
|
||||
export type AddonModH5PActivityData = {
|
||||
export type AddonModH5PActivityWSData = {
|
||||
id: number; // The primary key of the record.
|
||||
course: number; // Course id this h5p activity is part of.
|
||||
name: string; // The name of the activity module instance.
|
||||
|
@ -849,6 +852,19 @@ export type AddonModH5PActivityData = {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Basic data for an H5P activity, with some calculated data.
|
||||
*/
|
||||
export type AddonModH5PActivityData = AddonModH5PActivityWSData & Partial<AddonModH5pactivityGlobalSettings>;
|
||||
|
||||
/**
|
||||
* Global settings for H5P activities.
|
||||
*/
|
||||
export type AddonModH5pactivityGlobalSettings = {
|
||||
enablesavestate: boolean; // Whether saving state is enabled.
|
||||
savestatefreq?: number; // How often (in seconds) the state is saved.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of mod_h5pactivity_get_h5pactivities_by_courses WS.
|
||||
*/
|
||||
|
@ -860,7 +876,8 @@ export type AddonModH5pactivityGetByCoursesWSParams = {
|
|||
* Data returned by mod_h5pactivity_get_h5pactivities_by_courses WS.
|
||||
*/
|
||||
export type AddonModH5pactivityGetByCoursesWSResponse = {
|
||||
h5pactivities: AddonModH5PActivityData[];
|
||||
h5pactivities: AddonModH5PActivityWSData[];
|
||||
h5pglobalsettings?: AddonModH5pactivityGlobalSettings;
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
|
@ -1136,14 +1153,36 @@ declare module '@singletons/events' {
|
|||
/**
|
||||
* Data to be sent using xAPI.
|
||||
*/
|
||||
export type AddonModH5PActivityXAPIData = {
|
||||
export type AddonModH5PActivityXAPIBasicData = {
|
||||
action: string;
|
||||
component: string;
|
||||
context: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Statements data to be sent using xAPI.
|
||||
*/
|
||||
export type AddonModH5PActivityXAPIStatementsData = AddonModH5PActivityXAPIBasicData & {
|
||||
statements: AddonModH5PActivityStatement[];
|
||||
};
|
||||
|
||||
/**
|
||||
* States data to be sent using xAPI.
|
||||
*/
|
||||
export type AddonModH5PActivityXAPIStateData = AddonModH5PActivityXAPIBasicData & {
|
||||
activityId: string;
|
||||
agent: Record<string, unknown>;
|
||||
stateId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Post state data to be sent using xAPI.
|
||||
*/
|
||||
export type AddonModH5PActivityXAPIPostStateData = AddonModH5PActivityXAPIStateData & {
|
||||
stateData: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* xAPI statement.
|
||||
*/
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
CoreWSExternalWarning,
|
||||
CoreWSUploadFileResult,
|
||||
CoreWSPreSetsSplitRequest,
|
||||
CoreWSTypeExpected,
|
||||
} from '@services/ws';
|
||||
import { CoreDomUtils, ToastDuration } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
|
@ -2571,7 +2572,7 @@ export type CoreSiteWSPreSets = {
|
|||
/**
|
||||
* Defaults to 'object'. Use it when you expect a type that's not an object|array.
|
||||
*/
|
||||
typeExpected?: string;
|
||||
typeExpected?: CoreWSTypeExpected;
|
||||
|
||||
/**
|
||||
* Wehther a pending request in the queue matching the same function and arguments can be reused instead of adding
|
||||
|
|
|
@ -2389,6 +2389,11 @@ H5P.createTitle = function (rawTitle, maxLength) {
|
|||
done('Not signed in.');
|
||||
return;
|
||||
}
|
||||
// Moodle patch to let override this method.
|
||||
if (H5P.contentUserDataAjax !== undefined) {
|
||||
return H5P.contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async);
|
||||
}
|
||||
// End of Moodle patch.
|
||||
|
||||
var options = {
|
||||
url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0),
|
||||
|
|
|
@ -77,6 +77,7 @@ H5PEmbedCommunicator = (function() {
|
|||
*
|
||||
* @param {string} component
|
||||
* @param {Object} statements
|
||||
* @returns {void}
|
||||
*/
|
||||
self.post = function(component, statements) {
|
||||
window.parent.postMessage({
|
||||
|
@ -87,6 +88,50 @@ H5PEmbedCommunicator = (function() {
|
|||
statements: statements,
|
||||
}, '*');
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a xAPI state to LMS.
|
||||
*
|
||||
* @param {string} component
|
||||
* @param {string} activityId
|
||||
* @param {Object} agent
|
||||
* @param {string} stateId
|
||||
* @param {string} stateData
|
||||
* @returns {void}
|
||||
*/
|
||||
self.postState = function(component, activityId, agent, stateId, stateData) {
|
||||
window.parent.postMessage({
|
||||
environment: 'moodleapp',
|
||||
context: 'h5p',
|
||||
action: 'xapi_post_state',
|
||||
component: component,
|
||||
activityId: activityId,
|
||||
agent: agent,
|
||||
stateId: stateId,
|
||||
stateData: stateData,
|
||||
}, '*');
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a xAPI state from LMS.
|
||||
*
|
||||
* @param {string} component
|
||||
* @param {string} activityId
|
||||
* @param {Object} agent
|
||||
* @param {string} stateId
|
||||
* @returns {void}
|
||||
*/
|
||||
self.deleteState = function(component, activityId, agent, stateId) {
|
||||
window.parent.postMessage({
|
||||
environment: 'moodleapp',
|
||||
context: 'h5p',
|
||||
action: 'xapi_delete_state',
|
||||
component: component,
|
||||
activityId: activityId,
|
||||
agent: agent,
|
||||
stateId: stateId,
|
||||
}, '*');
|
||||
};
|
||||
}
|
||||
|
||||
return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
|
||||
|
@ -115,6 +160,9 @@ document.onreadystatechange = async() => {
|
|||
return;
|
||||
}
|
||||
|
||||
/** @var {boolean} statementPosted Whether the statement has been sent or not, to avoid sending xAPI State after it. */
|
||||
var statementPosted = false;
|
||||
|
||||
// Check for H5P iFrame.
|
||||
var iFrame = document.querySelector('.h5p-iframe');
|
||||
if (!iFrame || !iFrame.contentWindow) {
|
||||
|
@ -184,6 +232,7 @@ document.onreadystatechange = async() => {
|
|||
|
||||
// Get emitted xAPI data.
|
||||
H5P.externalDispatcher.on('xAPI', function(event) {
|
||||
statementPosted = false;
|
||||
var moodlecomponent = H5P.getMoodleComponent();
|
||||
if (moodlecomponent == undefined) {
|
||||
return;
|
||||
|
@ -211,6 +260,33 @@ document.onreadystatechange = async() => {
|
|||
if (isCompleted && !isChild) {
|
||||
var statements = H5P.getXAPIStatements(this.contentId, statement);
|
||||
H5PEmbedCommunicator.post(moodlecomponent, statements);
|
||||
// Mark the statement has been sent, to avoid sending xAPI State after it.
|
||||
statementPosted = true;
|
||||
}
|
||||
});
|
||||
|
||||
H5P.externalDispatcher.on('xAPIState', function(event) {
|
||||
var moodlecomponent = H5P.getMoodleComponent();
|
||||
var contentId = event.data.activityId;
|
||||
var stateId = event.data.stateId;
|
||||
var state = event.data.state;
|
||||
if (state === undefined) {
|
||||
// When state is undefined, a call to the WS for getting the state could be done. However, for now, this is not
|
||||
// required because the content state is initialised with PHP.
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === null) {
|
||||
// When this method is called from the H5P API with null state, the state must be deleted using the rest of attributes.
|
||||
H5PEmbedCommunicator.deleteState(moodlecomponent, contentId, H5P.getxAPIActor(), stateId);
|
||||
} else if (!statementPosted) {
|
||||
// Only update the state if a statement hasn't been posted recently.
|
||||
// When state is defined, it needs to be updated. As not all the H5P content types are returning a JSON, we need
|
||||
// to simulate it because xAPI State defines statedata as a JSON.
|
||||
var statedata = {
|
||||
h5p: state
|
||||
};
|
||||
H5PEmbedCommunicator.postState(moodlecomponent, contentId, H5P.getxAPIActor(), stateId, JSON.stringify(statedata));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -94,3 +94,72 @@ H5P.getMoodleComponent = function () {
|
|||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the actor.
|
||||
*
|
||||
* @returns {Object} The Actor object.
|
||||
*/
|
||||
H5P.getxAPIActor = function() {
|
||||
var actor = null;
|
||||
if (H5PIntegration.user !== undefined) {
|
||||
actor = {
|
||||
'name': H5PIntegration.user.name,
|
||||
'objectType': 'Agent'
|
||||
};
|
||||
if (H5PIntegration.user.id !== undefined) {
|
||||
actor.account = {
|
||||
'name': H5PIntegration.user.id,
|
||||
'homePage': H5PIntegration.siteUrl
|
||||
};
|
||||
} else if (H5PIntegration.user.mail !== undefined) {
|
||||
actor.mbox = 'mailto:' + H5PIntegration.user.mail;
|
||||
}
|
||||
} else {
|
||||
var uuid;
|
||||
try {
|
||||
if (localStorage.H5PUserUUID) {
|
||||
uuid = localStorage.H5PUserUUID;
|
||||
} else {
|
||||
uuid = H5P.createUUID();
|
||||
localStorage.H5PUserUUID = uuid;
|
||||
}
|
||||
} catch (err) {
|
||||
// LocalStorage and Cookies are probably disabled. Do not track the user.
|
||||
uuid = 'not-trackable-' + H5P.createUUID();
|
||||
}
|
||||
actor = {
|
||||
'account': {
|
||||
'name': uuid,
|
||||
'homePage': H5PIntegration.siteUrl
|
||||
},
|
||||
'objectType': 'Agent'
|
||||
};
|
||||
}
|
||||
return actor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates requests for inserting, updating and deleting content user data.
|
||||
* It overrides the contentUserDataAjax private method in h5p.js.
|
||||
*
|
||||
* @param {number} contentId What content to store the data for.
|
||||
* @param {string} dataType Identifies the set of data for this content.
|
||||
* @param {string} subContentId Identifies sub content
|
||||
* @param {function} [done] Callback when ajax is done.
|
||||
* @param {object} [data] To be stored for future use.
|
||||
* @param {boolean} [preload=false] Data is loaded when content is loaded.
|
||||
* @param {boolean} [invalidate=false] Data is invalidated when content changes.
|
||||
* @param {boolean} [async=true]
|
||||
*/
|
||||
H5P.contentUserDataAjax = function(contentId, dataType, subContentId, done, data, preload, invalidate, async) {
|
||||
var instance = H5P.findInstanceFromId(contentId);
|
||||
if (instance !== undefined) {
|
||||
var xAPIState = {
|
||||
activityId: H5P.XAPIEvent.prototype.getContentXAPIId(instance),
|
||||
stateId: dataType,
|
||||
state: data
|
||||
};
|
||||
H5P.externalDispatcher.trigger('xAPIState', xAPIState);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -44,6 +44,10 @@ if (window.H5PIntegration && window.H5PIntegration.contents && location.search)
|
|||
}
|
||||
} else if (nameAndValue[0] == 'trackingUrl' && contentData) {
|
||||
contentData.url = nameAndValue[1];
|
||||
} else if (nameAndValue[0] == 'saveFreq' && nameAndValue[1] !== undefined) {
|
||||
window.H5PIntegration.saveFreq = nameAndValue[1];
|
||||
} else if (nameAndValue[0] == 'state' && nameAndValue[1] !== undefined && contentData) {
|
||||
contentData.contentUserData = [{ state: decodeURIComponent(nameAndValue[1]) }];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,3 +6,4 @@ Changes:
|
|||
1. The h5p.js file has been modified to make fullscreen work in the Moodle app. In line 34, the code inside the condition document.documentElement.webkitRequestFullScreen has changed, the original code has been commented.
|
||||
2. The h5p.js file has been modified to simulate a fake full screen in iOS. The H5P file now sends post messages to the app, and also listens to messages sent by the app to enter/exit full screen.
|
||||
3. The embed.js file has been modified to remove optional chaining because it isn't supported in some old devices.
|
||||
4. The h5p.js has been modified to include a call to contentUserDataAjax (this change was done in LMS too).
|
||||
|
|
|
@ -122,9 +122,10 @@ export class CoreH5PHelper {
|
|||
throw new CoreError('Site info could not be fetched.');
|
||||
}
|
||||
|
||||
// H5P doesn't currently support xAPI State. It implements a mechanism in contentUserDataAjax() in h5p.js to update user
|
||||
// data. However, in our case, we're overriding this method to call the xAPI State web services.
|
||||
const basePath = CoreFile.getBasePathInstant();
|
||||
const ajaxPaths = {
|
||||
xAPIResult: '',
|
||||
contentUserData: '',
|
||||
};
|
||||
|
||||
|
@ -144,7 +145,7 @@ export class CoreH5PHelper {
|
|||
),
|
||||
postUserStatistics: false,
|
||||
ajax: ajaxPaths,
|
||||
saveFreq: false,
|
||||
saveFreq: false, // saveFreq will be overridden in params.js.
|
||||
siteUrl: site.getURL(),
|
||||
l10n: {
|
||||
H5P: CoreH5P.h5pCore.getLocalization(), // eslint-disable-line @typescript-eslint/naming-convention
|
||||
|
@ -240,7 +241,7 @@ export type CoreH5PCoreSettings = {
|
|||
urlLibraries: string;
|
||||
postUserStatistics: boolean;
|
||||
ajax: {
|
||||
xAPIResult: string;
|
||||
xAPIResult?: string;
|
||||
contentUserData: string;
|
||||
};
|
||||
saveFreq: boolean;
|
||||
|
|
|
@ -16,12 +16,12 @@ import { CoreFile } from '@services/file';
|
|||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreXAPI } from '@features/xapi/services/xapi';
|
||||
import { CoreH5P } from '../services/h5p';
|
||||
import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core';
|
||||
import { CoreH5PCoreSettings, CoreH5PHelper } from './helper';
|
||||
import { CoreH5PStorage } from './storage';
|
||||
import { CorePath } from '@singletons/path';
|
||||
import { CoreXAPIIRI } from '@features/xapi/classes/iri';
|
||||
|
||||
/**
|
||||
* Equivalent to Moodle's H5P player class.
|
||||
|
@ -98,7 +98,7 @@ export class CoreH5PPlayer {
|
|||
metadata: content.metadata,
|
||||
contentUserData: [
|
||||
{
|
||||
state: '{}',
|
||||
state: '{}', // state will be overridden in params.js to use the latest state when the package is played.
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -272,6 +272,7 @@ export class CoreH5PPlayer {
|
|||
component?: string,
|
||||
contextId?: number,
|
||||
siteId?: string,
|
||||
otherOptions: CoreH5PGetContentUrlOptions = {},
|
||||
): Promise<string> {
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
|
@ -282,13 +283,19 @@ export class CoreH5PPlayer {
|
|||
|
||||
displayOptions = this.h5pCore.fixDisplayOptions(displayOptions || {}, data.id);
|
||||
|
||||
const params: Record<string, string> = {
|
||||
const params: Record<string, string | number> = {
|
||||
displayOptions: JSON.stringify(displayOptions),
|
||||
component: component || '',
|
||||
};
|
||||
|
||||
if (contextId) {
|
||||
params.trackingUrl = await CoreXAPI.getUrl(contextId, 'activity', siteId);
|
||||
params.trackingUrl = await CoreXAPIIRI.generate(contextId, 'activity', siteId);
|
||||
}
|
||||
if (otherOptions.saveFreq !== undefined) {
|
||||
params.saveFreq = otherOptions.saveFreq;
|
||||
}
|
||||
if (otherOptions.state !== undefined) {
|
||||
params.state = otherOptions.state;
|
||||
}
|
||||
|
||||
return CoreUrlUtils.addParamsToUrl(path, params);
|
||||
|
@ -420,3 +427,8 @@ type AssetsSettings = CoreH5PCoreSettings & {
|
|||
[libString: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type CoreH5PGetContentUrlOptions = {
|
||||
saveFreq?: number; // State save frequency (if enabled).
|
||||
state?: string; // Current state.
|
||||
};
|
||||
|
|
|
@ -45,6 +45,8 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy {
|
|||
@Input() trackComponent?: string; // Component to send xAPI events to.
|
||||
@Input() contextId?: number; // Context ID. Required for tracking.
|
||||
@Input() enableInAppFullscreen?: boolean; // Whether to enable our custom in-app fullscreen feature.
|
||||
@Input() saveFreq?: number; // Save frequency (in seconds) if enabled.
|
||||
@Input() state?: string; // Initial content state.
|
||||
@Output() onIframeUrlSet = new EventEmitter<{src: string; online: boolean}>();
|
||||
@Output() onIframeLoaded = new EventEmitter<void>();
|
||||
|
||||
|
@ -150,6 +152,11 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy {
|
|||
* @returns Promise resolved with the local URL.
|
||||
*/
|
||||
protected async getLocalUrl(): Promise<string | undefined> {
|
||||
const otherOptions = {
|
||||
saveFreq: this.saveFreq,
|
||||
state: this.state,
|
||||
};
|
||||
|
||||
try {
|
||||
const url = await CoreH5P.h5pPlayer.getContentIndexFileUrl(
|
||||
this.fileUrl!,
|
||||
|
@ -157,6 +164,7 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy {
|
|||
this.trackComponent,
|
||||
this.contextId,
|
||||
this.siteId,
|
||||
otherOptions,
|
||||
);
|
||||
|
||||
return url;
|
||||
|
@ -176,6 +184,7 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy {
|
|||
this.trackComponent,
|
||||
this.contextId,
|
||||
this.siteId,
|
||||
otherOptions,
|
||||
);
|
||||
|
||||
return url;
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
// (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 { CoreSites } from '@services/sites';
|
||||
import { CorePath } from '@singletons/path';
|
||||
|
||||
/**
|
||||
* xAPI IRI values generator.
|
||||
*/
|
||||
export class CoreXAPIIRI {
|
||||
|
||||
/**
|
||||
* Generate a valid IRI element from a value and an optional type.
|
||||
*
|
||||
* @param value Value.
|
||||
* @param type Type (e.g. 'activity'). Defaults to 'element'.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
static async generate(value: string|number, type = 'element', siteId?: string): Promise<string> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
return CorePath.concatenatePaths(site.getURL(), `xapi/${type}/${value}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to extract the original value from an IRI.
|
||||
*
|
||||
* @param iri IRI.
|
||||
* @param type Type (e.g. 'activity'). Defaults to 'element'.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
static async extract(iri: string, type = 'element', siteId?: string): Promise<string> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const baseUrl = CorePath.concatenatePaths(site.getURL(), `xapi/${type}/`);
|
||||
|
||||
return iri.replace(baseUrl, '');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// (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 { CoreSite } from '@classes/site';
|
||||
|
||||
/**
|
||||
* Statement agent (user) object for xAPI structure checking and usage.
|
||||
*/
|
||||
export class CoreXAPIItemAgent {
|
||||
|
||||
protected constructor(protected data: Record<string, unknown>, protected user: CoreXAPIItemAgentUser) { }
|
||||
|
||||
/**
|
||||
* Get agent's user.
|
||||
*
|
||||
* @returns User.
|
||||
*/
|
||||
getUser(): CoreXAPIItemAgentUser {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent's data.
|
||||
*
|
||||
* @returns Data.
|
||||
*/
|
||||
getData(): Record<string, unknown> {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an item agent based on a certain's site user.
|
||||
*
|
||||
* @param site Site to use.
|
||||
* @returns Item agent instance.
|
||||
*/
|
||||
static createFromSite(site: CoreSite): CoreXAPIItemAgent {
|
||||
const username = site.getInfo()?.username ?? '';
|
||||
const data = {
|
||||
name: username,
|
||||
objectType: 'Agent',
|
||||
account: {
|
||||
name: site.getUserId(),
|
||||
homePage: site.getURL(),
|
||||
},
|
||||
};
|
||||
|
||||
return new CoreXAPIItemAgent(data, {
|
||||
id: site.getUserId(),
|
||||
username,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type CoreXAPIItemAgentUser = {
|
||||
id: number;
|
||||
username: string;
|
||||
};
|
|
@ -15,12 +15,13 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreXAPIOffline, CoreXAPIOfflineSaveStatementsOptions } from './offline';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CorePath } from '@singletons/path';
|
||||
import { CoreXAPIItemAgent } from '../classes/item-agent';
|
||||
import { CoreXAPIIRI } from '../classes/iri';
|
||||
|
||||
/**
|
||||
* Service to provide XAPI functionalities.
|
||||
|
@ -28,6 +29,8 @@ import { CorePath } from '@singletons/path';
|
|||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreXAPIProvider {
|
||||
|
||||
static readonly ROOT_CACHE_KEY = 'CoreXAPI:';
|
||||
|
||||
/**
|
||||
* Returns whether or not WS to post XAPI statement is available.
|
||||
*
|
||||
|
@ -54,6 +57,100 @@ export class CoreXAPIProvider {
|
|||
return !!(site && site.wsAvailable('core_xapi_statement_post'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete state. It will fail if offline or cannot connect.
|
||||
*
|
||||
* @param component Component.
|
||||
* @param activityIRI XAPI activity ID IRI.
|
||||
* @param agent The xAPI agent json.
|
||||
* @param stateId The xAPI state ID.
|
||||
* @param options Options.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async deleteStateOnline(
|
||||
component: string,
|
||||
activityIRI: string,
|
||||
agent: string,
|
||||
stateId: string,
|
||||
options: CoreXAPIStateOptions = {},
|
||||
): Promise<boolean> {
|
||||
const site = await CoreSites.getSite(options.siteId);
|
||||
|
||||
const data: CoreXAPIDeleteStateWSParams = {
|
||||
component,
|
||||
activityId: activityIRI,
|
||||
agent,
|
||||
stateId,
|
||||
registration: options.registration,
|
||||
};
|
||||
|
||||
return site.write('core_xapi_delete_state', data, { typeExpected: 'boolean' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for H5P activity data WS calls.
|
||||
*
|
||||
* @param siteUrl Site URL.
|
||||
* @param component Component.
|
||||
* @param activityId Activity ID.
|
||||
* @param stateId The xAPI state ID.
|
||||
* @param registration Registration ID.
|
||||
* @returns Cache key.
|
||||
*/
|
||||
protected getStateCacheKey(
|
||||
siteUrl: string,
|
||||
component: string,
|
||||
activityId: number,
|
||||
stateId: string,
|
||||
registration?: string,
|
||||
): string {
|
||||
return `${CoreXAPIProvider.ROOT_CACHE_KEY}state:${component}:${activityId}:${stateId}:${registration ?? ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get state from WS.
|
||||
*
|
||||
* @param component Component.
|
||||
* @param activityId Activity ID.
|
||||
* @param stateId The xAPI state ID.
|
||||
* @param options Options.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async getStateOnline(
|
||||
component: string,
|
||||
activityId: number,
|
||||
stateId: string,
|
||||
options: CoreXAPIGetStateOptions = {},
|
||||
): Promise<string | null> {
|
||||
const [site, activityIRI] = await Promise.all([
|
||||
CoreSites.getSite(options.siteId),
|
||||
CoreXAPIIRI.generate(activityId, 'activity'),
|
||||
]);
|
||||
|
||||
const data: CoreXAPIGetStateWSParams = {
|
||||
component,
|
||||
activityId: activityIRI,
|
||||
agent: JSON.stringify(CoreXAPIItemAgent.createFromSite(site).getData()),
|
||||
stateId,
|
||||
registration: options.registration,
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
typeExpected: 'jsonstring',
|
||||
cacheKey: this.getStateCacheKey(
|
||||
site.getURL(),
|
||||
component,
|
||||
activityId,
|
||||
stateId,
|
||||
options.registration,
|
||||
),
|
||||
component: options.appComponent,
|
||||
componentId: options.appComponentId,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
return site.read('core_xapi_get_state', data, preSets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL for XAPI events.
|
||||
*
|
||||
|
@ -61,11 +158,36 @@ export class CoreXAPIProvider {
|
|||
* @param type Type (e.g. 'activity').
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved when done.
|
||||
* @deprecated since 4.2. Use CoreXAPIIRI.generate instead.
|
||||
*/
|
||||
async getUrl(contextId: number, type: string, siteId?: string): Promise<string> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
return CoreXAPIIRI.generate(contextId, type, siteId);
|
||||
}
|
||||
|
||||
return CorePath.concatenatePaths(site.getURL(), `xapi/${type}/${contextId}`);
|
||||
/**
|
||||
* Invalidates a state.
|
||||
*
|
||||
* @param component Component.
|
||||
* @param activityId Activity ID.
|
||||
* @param stateId The xAPI state ID.
|
||||
* @param options Options.
|
||||
* @returns Promise resolved when the data is invalidated.
|
||||
*/
|
||||
async invalidateState(
|
||||
component: string,
|
||||
activityId: number,
|
||||
stateId: string,
|
||||
options: CoreXAPIStateOptions = {},
|
||||
): Promise<void> {
|
||||
const site = await CoreSites.getSite(options.siteId);
|
||||
|
||||
await site.invalidateWsCacheForKey(this.getStateCacheKey(
|
||||
site.getURL(),
|
||||
component,
|
||||
activityId,
|
||||
stateId,
|
||||
options.registration,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -122,11 +244,11 @@ export class CoreXAPIProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async postStatementsOnline(component: string, json: string, siteId?: string): Promise<number[]> {
|
||||
async postStatementsOnline(component: string, json: string, siteId?: string): Promise<boolean[]> {
|
||||
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const data = {
|
||||
const data: CoreXAPIStatementPostWSParams = {
|
||||
component: component,
|
||||
requestjson: json,
|
||||
};
|
||||
|
@ -134,6 +256,39 @@ export class CoreXAPIProvider {
|
|||
return site.write('core_xapi_statement_post', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post state. It will fail if offline or cannot connect.
|
||||
*
|
||||
* @param component Component.
|
||||
* @param activityIRI XAPI activity ID IRI.
|
||||
* @param agent The xAPI agent json.
|
||||
* @param stateId The xAPI state ID.
|
||||
* @param stateData JSON object with the state data.
|
||||
* @param options Options.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async postStateOnline(
|
||||
component: string,
|
||||
activityIRI: string,
|
||||
agent: string,
|
||||
stateId: string,
|
||||
stateData: string,
|
||||
options: CoreXAPIPostStateOptions = {},
|
||||
): Promise<boolean> {
|
||||
const site = await CoreSites.getSite(options.siteId);
|
||||
|
||||
const data: CoreXAPIPostStateWSParams = {
|
||||
component,
|
||||
activityId: activityIRI,
|
||||
agent,
|
||||
stateId,
|
||||
stateData,
|
||||
registration: options.registration,
|
||||
};
|
||||
|
||||
return site.write('core_xapi_post_state', data, { typeExpected: 'boolean' });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreXAPI = makeSingleton(CoreXAPIProvider);
|
||||
|
@ -144,3 +299,68 @@ export const CoreXAPI = makeSingleton(CoreXAPIProvider);
|
|||
export type CoreXAPIPostStatementsOptions = CoreXAPIOfflineSaveStatementsOptions & {
|
||||
offline?: boolean; // Whether to force storing it in offline.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_xapi_statement_post WS.
|
||||
*/
|
||||
export type CoreXAPIStatementPostWSParams = {
|
||||
component: string; // Component name.
|
||||
requestjson: string; // Json object with all the statements to post.
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to pass to state functions.
|
||||
*/
|
||||
export type CoreXAPIStateOptions = {
|
||||
registration?: string; // The xAPI registration UUID.
|
||||
siteId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to pass to getState function.
|
||||
*/
|
||||
export type CoreXAPIGetStateOptions = CoreXAPIStateOptions & CoreSitesCommonWSOptions & {
|
||||
appComponent?: string; // The app component to link the WS call to.
|
||||
appComponentId?: number; // The app component ID to link the WS call to.
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to pass to postState function.
|
||||
*/
|
||||
export type CoreXAPIPostStateOptions = CoreXAPIStateOptions & {
|
||||
offline?: boolean; // Whether to force storing it in offline.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_xapi_post_state WS.
|
||||
*/
|
||||
export type CoreXAPIPostStateWSParams = {
|
||||
component: string; // Component name.
|
||||
activityId: string; // XAPI activity ID IRI.
|
||||
agent: string; // The xAPI agent json.
|
||||
stateId: string; // The xAPI state ID.
|
||||
stateData: string; // JSON object with the state data.
|
||||
registration?: string; // The xAPI registration UUID.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_xapi_delete_state WS.
|
||||
*/
|
||||
export type CoreXAPIDeleteStateWSParams = {
|
||||
component: string; // Component name.
|
||||
activityId: string; // XAPI activity ID IRI.
|
||||
agent: string; // The xAPI agent json.
|
||||
stateId: string; // The xAPI state ID.
|
||||
registration?: string; // The xAPI registration UUID.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_xapi_get_state WS.
|
||||
*/
|
||||
export type CoreXAPIGetStateWSParams = {
|
||||
component: string; // Component name.
|
||||
activityId: string; // XAPI activity ID IRI.
|
||||
agent: string; // The xAPI agent json.
|
||||
stateId: string; // The xAPI state ID.
|
||||
registration?: string; // The xAPI registration UUID.
|
||||
};
|
||||
|
|
|
@ -647,12 +647,13 @@ export class CoreWSProvider {
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return promise.then(async (data: any) => {
|
||||
// Some moodle web services return null.
|
||||
// If the responseExpected value is set to false, we create a blank object if the response is null.
|
||||
if (!data && !preSets.responseExpected) {
|
||||
data = {};
|
||||
// Some moodle web services always return null, and some others can return a primitive type or null.
|
||||
if (data === null && (!preSets.responseExpected || preSets.typeExpected !== 'object')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeExpected = preSets.typeExpected === 'jsonstring' ? 'string' : preSets.typeExpected;
|
||||
|
||||
if (!data) {
|
||||
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
|
||||
errorcode: 'serverconnectionpost',
|
||||
|
@ -660,26 +661,26 @@ export class CoreWSProvider {
|
|||
details: Translate.instant('core.errorinvalidresponse', { method }),
|
||||
}),
|
||||
});
|
||||
} else if (typeof data != preSets.typeExpected) {
|
||||
} else if (typeof data !== typeExpected) {
|
||||
// If responseType is text an string will be returned, parse before returning.
|
||||
if (typeof data == 'string') {
|
||||
if (preSets.typeExpected == 'number') {
|
||||
if (typeExpected === 'number') {
|
||||
data = Number(data);
|
||||
if (isNaN(data)) {
|
||||
this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`);
|
||||
this.logger.warn(`Response expected type "${typeExpected}" cannot be parsed to number`);
|
||||
|
||||
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
|
||||
errorcode: 'invalidresponse',
|
||||
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
|
||||
});
|
||||
}
|
||||
} else if (preSets.typeExpected == 'boolean') {
|
||||
} else if (typeExpected === 'boolean') {
|
||||
if (data === 'true') {
|
||||
data = true;
|
||||
} else if (data === 'false') {
|
||||
data = false;
|
||||
} else {
|
||||
this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`);
|
||||
this.logger.warn(`Response expected type "${typeExpected}" is not true or false`);
|
||||
|
||||
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
|
||||
errorcode: 'invalidresponse',
|
||||
|
@ -687,7 +688,7 @@ export class CoreWSProvider {
|
|||
});
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
|
||||
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${typeExpected}"`);
|
||||
|
||||
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
|
||||
errorcode: 'invalidresponse',
|
||||
|
@ -695,7 +696,7 @@ export class CoreWSProvider {
|
|||
});
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
|
||||
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${typeExpected}"`);
|
||||
|
||||
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
|
||||
errorcode: 'invalidresponse',
|
||||
|
@ -1296,7 +1297,7 @@ export type CoreWSPreSets = {
|
|||
/**
|
||||
* Defaults to 'object'. Use it when you expect a type that's not an object|array.
|
||||
*/
|
||||
typeExpected?: string;
|
||||
typeExpected?: CoreWSTypeExpected;
|
||||
|
||||
/**
|
||||
* Defaults to false. Clean multibyte Unicode chars from data.
|
||||
|
@ -1310,6 +1311,8 @@ export type CoreWSPreSets = {
|
|||
splitRequest?: CoreWSPreSetsSplitRequest;
|
||||
};
|
||||
|
||||
export type CoreWSTypeExpected = 'boolean'|'number'|'string'|'jsonstring'|'object';
|
||||
|
||||
/**
|
||||
* Options to split a request.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue