Skip to content

Commit 90dfe5a

Browse files
committed
MOBILE-3412 h5pactivity: Support tracking in online
1 parent 3297ec0 commit 90dfe5a

File tree

20 files changed

+401
-53
lines changed

20 files changed

+401
-53
lines changed

scripts/langindex.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,7 @@
684684
"addon.mod_h5pactivity.no_compatible_track": "h5pactivity",
685685
"addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp",
686686
"addon.mod_h5pactivity.outcome": "h5pactivity",
687+
"addon.mod_h5pactivity.previewmode": "h5pactivity",
687688
"addon.mod_h5pactivity.result_fill-in": "h5pactivity",
688689
"addon.mod_h5pactivity.result_other": "h5pactivity",
689690
"addon.mod_h5pactivity.review_my_attempts": "h5pactivity",

src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
<ion-icon name="warning"></ion-icon> {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
2222
</ion-card>
2323

24+
<!-- Preview mode. -->
25+
<ion-card class="core-warning-card" icon-start *ngIf="accessInfo && !trackComponent">
26+
<ion-icon name="warning"></ion-icon> {{ 'addon.mod_h5pactivity.previewmode' | translate }}
27+
</ion-card>
28+
2429
<ion-list *ngIf="deployedFile && !playing">
2530
<ion-item text-wrap *ngIf="stateMessage">
2631
<p >{{ stateMessage | translate }}</p>
@@ -39,5 +44,5 @@ <h2 *ngIf="progressMessage">{{ progressMessage | translate }}</h2>
3944
</ion-item>
4045
</ion-list>
4146

42-
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"></core-h5p-iframe>
47+
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl" [trackComponent]="trackComponent" [contextId]="h5pActivity.context"></core-h5p-iframe>
4348
</core-loading>

src/addon/mod/h5pactivity/components/index/index.ts

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main
2424
import { CoreH5P } from '@core/h5p/providers/h5p';
2525
import { CoreH5PDisplayOptions } from '@core/h5p/classes/core';
2626
import { CoreH5PHelper } from '@core/h5p/classes/helper';
27+
import { CoreXAPI } from '@core/xapi/providers/xapi';
2728
import { CoreConstants } from '@core/constants';
2829
import { CoreSite } from '@classes/site';
2930

@@ -57,17 +58,23 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
5758
fileUrl: string; // The fileUrl to use to play the package.
5859
state: string; // State of the file.
5960
siteCanDownload: boolean;
61+
trackComponent: string; // Component for tracking.
6062

6163
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
6264
protected site: CoreSite;
6365
protected observer;
66+
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
6467

6568
constructor(injector: Injector,
6669
@Optional() protected content: Content) {
6770
super(injector, content);
6871

6972
this.site = this.sitesProvider.getCurrentSite();
7073
this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite();
74+
75+
// Listen for messages from the iframe.
76+
this.messageListenerFunction = this.onIframeMessage.bind(this);
77+
window.addEventListener('message', this.messageListenerFunction);
7178
}
7279

7380
/**
@@ -102,17 +109,19 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
102109
this.description = this.h5pActivity.intro;
103110
this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions);
104111

105-
if (this.h5pActivity.package && this.h5pActivity.package[0]) {
106-
// The online player should use the original file, not the trusted one.
107-
this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
108-
this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions);
109-
}
110-
111112
await Promise.all([
112113
this.fetchAccessInfo(),
113114
this.fetchDeployedFileData(),
114115
]);
115116

117+
this.trackComponent = this.accessInfo.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : '';
118+
119+
if (this.h5pActivity.package && this.h5pActivity.package[0]) {
120+
// The online player should use the original file, not the trusted one.
121+
this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
122+
this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions, this.trackComponent);
123+
}
124+
116125
if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) {
117126
// Cannot download the file or already downloaded, play the package directly.
118127
this.play();
@@ -326,10 +335,56 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
326335
this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {courseId: this.courseId, h5pActivityId: this.h5pActivity.id});
327336
}
328337

338+
/**
339+
* Treat an iframe message event.
340+
*
341+
* @param event Event.
342+
* @return Promise resolved when done.
343+
*/
344+
protected async onIframeMessage(event: MessageEvent): Promise<void> {
345+
if (!event.data || !CoreXAPI.instance.canPostStatementInSite(this.site) || !this.isCurrentXAPIPost(event.data)) {
346+
return;
347+
}
348+
349+
try {
350+
await CoreXAPI.instance.postStatement(event.data.component, JSON.stringify(event.data.statements));
351+
} catch (error) {
352+
CoreDomUtils.instance.showErrorModalDefault(error, 'Error sending tracking data.');
353+
}
354+
}
355+
356+
/**
357+
* Check if an event is an XAPI post statement of the current activity.
358+
*
359+
* @param data Event data.
360+
* @return Whether it's an XAPI post statement of the current activity.
361+
*/
362+
protected isCurrentXAPIPost(data: any): boolean {
363+
if (data.context != 'moodleapp' || data.action != 'xapi_post_statement' || !data.statements) {
364+
return false;
365+
}
366+
367+
// Check the event belongs to this activity.
368+
const trackingUrl = data.statements[0] && data.statements[0].object && data.statements[0].object.id;
369+
if (!trackingUrl) {
370+
return false;
371+
}
372+
373+
if (!this.site.containsUrl(trackingUrl)) {
374+
// The event belongs to another site, weird scenario. Maybe some JS running in background.
375+
return false;
376+
}
377+
378+
const match = trackingUrl.match(/xapi\/activity\/(\d+)/);
379+
380+
return match && match[1] == this.h5pActivity.context;
381+
}
382+
329383
/**
330384
* Component destroyed.
331385
*/
332386
ngOnDestroy(): void {
333387
this.observer && this.observer.off();
388+
window.removeEventListener('message', this.messageListenerFunction);
334389
}
335390
}

src/addon/mod/h5pactivity/lang/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.",
2525
"offlinedisabledwarning": "You will need to be online to view the H5P package.",
2626
"outcome": "Outcome",
27+
"previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.",
2728
"result_fill-in": "Fill-in text",
2829
"result_other": "Unkown interaction type",
2930
"review_my_attempts": "View my attempts",

src/addon/mod/h5pactivity/pages/index/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
import { Component, ViewChild } from '@angular/core';
1616
import { IonicPage, NavParams } from 'ionic-angular';
17+
import { CoreDomUtils } from '@providers/utils/dom';
1718
import { AddonModH5PActivityIndexComponent } from '../../components/index/index';
1819
import { AddonModH5PActivityData } from '../../providers/h5pactivity';
1920

21+
import { Translate } from '@singletons/core.singletons';
22+
2023
/**
2124
* Page that displays an H5P activity.
2225
*/
@@ -46,4 +49,17 @@ export class AddonModH5PActivityIndexPage {
4649
updateData(h5p: AddonModH5PActivityData): void {
4750
this.title = h5p.name || this.title;
4851
}
52+
53+
/**
54+
* Check if we can leave the page or not.
55+
*
56+
* @return Resolved if we can leave it, rejected if not.
57+
*/
58+
ionViewCanLeave(): Promise<void> {
59+
if (!this.h5pComponent.playing) {
60+
return;
61+
}
62+
63+
return CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmleaveunknownchanges'));
64+
}
4965
}

src/addon/mod/h5pactivity/providers/h5pactivity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { makeSingleton, Translate } from '@singletons/core.singletons';
3131
@Injectable()
3232
export class AddonModH5PActivityProvider {
3333
static COMPONENT = 'mmaModH5PActivity';
34+
static TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking.
3435

3536
protected ROOT_CACHE_KEY = 'mmaModH5PActivity:';
3637

@@ -595,6 +596,7 @@ export type AddonModH5PActivityData = {
595596
grademethod: number; // Which H5P attempt is used for grading.
596597
contenthash?: string; // Sha1 hash of file content.
597598
coursemodule: number; // Coursemodule.
599+
context: number; // Context ID.
598600
introfiles: CoreWSExternalFile[];
599601
package: CoreWSExternalFile[];
600602
deployedfile?: {

src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import { CoreFilterModule } from '@core/filter/filter.module';
8888
import { CoreH5PModule } from '@core/h5p/h5p.module';
8989
import { CoreSearchModule } from '@core/search/search.module';
9090
import { CoreEditorModule } from '@core/editor/editor.module';
91+
import { CoreXAPIModule } from '@core/xapi/xapi.module';
9192

9293
// Addon modules.
9394
import { AddonBadgesModule } from '@addon/badges/badges.module';
@@ -241,6 +242,7 @@ export const WP_PROVIDER: any = null;
241242
CoreH5PModule,
242243
CoreSearchModule,
243244
CoreEditorModule,
245+
CoreXAPIModule,
244246
AddonBadgesModule,
245247
AddonBlogModule,
246248
AddonCalendarModule,

src/assets/lang/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,7 @@
684684
"addon.mod_h5pactivity.no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.",
685685
"addon.mod_h5pactivity.offlinedisabledwarning": "You will need to be online to view the H5P package.",
686686
"addon.mod_h5pactivity.outcome": "Outcome",
687+
"addon.mod_h5pactivity.previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.",
687688
"addon.mod_h5pactivity.result_fill-in": "Fill-in text",
688689
"addon.mod_h5pactivity.result_other": "Unkown interaction type",
689690
"addon.mod_h5pactivity.review_my_attempts": "View my attempts",
@@ -1407,6 +1408,7 @@
14071408
"core.confirmdeletefile": "Are you sure you want to delete this file?",
14081409
"core.confirmgotabroot": "Are you sure you want to go back to {{name}}?",
14091410
"core.confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?",
1411+
"core.confirmleaveunknownchanges": "Are you sure you want to leave this page? If you have some unsaved changes they will be lost.",
14101412
"core.confirmloss": "Are you sure? All changes will be lost.",
14111413
"core.confirmopeninbrowser": "Do you want to open it in a web browser?",
14121414
"core.considereddigitalminor": "You are too young to create an account on this site.",

src/core/h5p/assets/moodle/js/displayoptions.js

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/core/h5p/assets/moodle/js/embed.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ H5PEmbedCommunicator = (function() {
7171
// Parent origin can be anything.
7272
window.parent.postMessage(data, '*');
7373
};
74+
75+
/**
76+
* Send a xAPI statement to LMS.
77+
*
78+
* @param {string} component
79+
* @param {Object} statements
80+
*/
81+
self.post = function(component, statements) {
82+
window.parent.postMessage({
83+
context: 'moodleapp',
84+
action: 'xapi_post_statement',
85+
component: component,
86+
statements: statements,
87+
}, '*');
88+
};
7489
}
7590

7691
return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
@@ -150,6 +165,38 @@ document.onreadystatechange = function() {
150165
}, 0);
151166
});
152167

168+
// Get emitted xAPI data.
169+
H5P.externalDispatcher.on('xAPI', function(event) {
170+
var moodlecomponent = H5P.getMoodleComponent();
171+
if (moodlecomponent == undefined) {
172+
return;
173+
}
174+
// Skip malformed events.
175+
var hasStatement = event && event.data && event.data.statement;
176+
if (!hasStatement) {
177+
return;
178+
}
179+
180+
var statement = event.data.statement;
181+
var validVerb = statement.verb && statement.verb.id;
182+
if (!validVerb) {
183+
return;
184+
}
185+
186+
var isCompleted = statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered'
187+
|| statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed';
188+
189+
var isChild = statement.context && statement.context.contextActivities &&
190+
statement.context.contextActivities.parent &&
191+
statement.context.contextActivities.parent[0] &&
192+
statement.context.contextActivities.parent[0].id;
193+
194+
if (isCompleted && !isChild) {
195+
var statements = H5P.getXAPIStatements(this.contentId, statement);
196+
H5PEmbedCommunicator.post(moodlecomponent, statements);
197+
}
198+
});
199+
153200
// Trigger initial resize for instance.
154201
H5P.trigger(instance, 'resize');
155202
};

0 commit comments

Comments
 (0)