forked from EVOgeek/Vmeda.Online
		
	
						commit
						badb71cb8e
					
				| @ -684,6 +684,7 @@ | ||||
|   "addon.mod_h5pactivity.no_compatible_track": "h5pactivity", | ||||
|   "addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp", | ||||
|   "addon.mod_h5pactivity.outcome": "h5pactivity", | ||||
|   "addon.mod_h5pactivity.previewmode": "h5pactivity", | ||||
|   "addon.mod_h5pactivity.result_fill-in": "h5pactivity", | ||||
|   "addon.mod_h5pactivity.result_other": "h5pactivity", | ||||
|   "addon.mod_h5pactivity.review_my_attempts": "h5pactivity", | ||||
| @ -1405,6 +1406,7 @@ | ||||
|   "core.confirmdeletefile": "repository", | ||||
|   "core.confirmgotabroot": "local_moodlemobileapp", | ||||
|   "core.confirmgotabrootdefault": "local_moodlemobileapp", | ||||
|   "core.confirmleaveunknownchanges": "local_moodlemobileapp", | ||||
|   "core.confirmloss": "local_moodlemobileapp", | ||||
|   "core.confirmopeninbrowser": "local_moodlemobileapp", | ||||
|   "core.considereddigitalminor": "moodle", | ||||
| @ -1858,6 +1860,7 @@ | ||||
|   "core.mod_folder": "folder/pluginname", | ||||
|   "core.mod_forum": "forum/pluginname", | ||||
|   "core.mod_glossary": "glossary/pluginname", | ||||
|   "core.mod_h5pactivity": "h5pactivity/pluginname", | ||||
|   "core.mod_ims": "imscp/pluginname", | ||||
|   "core.mod_imscp": "imscp/pluginname", | ||||
|   "core.mod_label": "label/pluginname", | ||||
|  | ||||
| @ -5,7 +5,8 @@ | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline"  [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| @ -16,11 +17,21 @@ | ||||
| 
 | ||||
|     <core-course-module-description [description]="description" [component]="component" [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-course-module-description> | ||||
| 
 | ||||
|     <!-- Offline data stored. --> | ||||
|     <ion-card class="core-warning-card" icon-start *ngIf="hasOffline"> | ||||
|         <ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: moduleName} }} | ||||
|     </ion-card> | ||||
| 
 | ||||
|     <!-- Offline disabled. --> | ||||
|     <ion-card class="core-warning-card" icon-start *ngIf="!siteCanDownload && playing"> | ||||
|         <ion-icon name="warning"></ion-icon> {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }} | ||||
|     </ion-card> | ||||
| 
 | ||||
|     <!-- Preview mode. --> | ||||
|     <ion-card class="core-warning-card" icon-start *ngIf="accessInfo && !trackComponent"> | ||||
|         <ion-icon name="warning"></ion-icon> {{ 'addon.mod_h5pactivity.previewmode' | translate }} | ||||
|     </ion-card> | ||||
| 
 | ||||
|     <ion-list *ngIf="deployedFile && !playing"> | ||||
|         <ion-item text-wrap *ngIf="stateMessage"> | ||||
|             <p >{{ stateMessage | translate }}</p> | ||||
| @ -39,5 +50,5 @@ | ||||
|         </ion-item> | ||||
|     </ion-list> | ||||
| 
 | ||||
|     <core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"></core-h5p-iframe> | ||||
|     <core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl" [trackComponent]="trackComponent" [contextId]="h5pActivity.context"></core-h5p-iframe> | ||||
| </core-loading> | ||||
|  | ||||
| @ -24,12 +24,15 @@ import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main | ||||
| import { CoreH5P } from '@core/h5p/providers/h5p'; | ||||
| import { CoreH5PDisplayOptions } from '@core/h5p/classes/core'; | ||||
| import { CoreH5PHelper } from '@core/h5p/classes/helper'; | ||||
| import { CoreXAPI } from '@core/xapi/providers/xapi'; | ||||
| import { CoreXAPIOffline } from '@core/xapi/providers/offline'; | ||||
| import { CoreConstants } from '@core/constants'; | ||||
| import { CoreSite } from '@classes/site'; | ||||
| 
 | ||||
| import { | ||||
|     AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAccessInfo | ||||
| } from '../../providers/h5pactivity'; | ||||
| import { AddonModH5PActivitySyncProvider, AddonModH5PActivitySync } from '../../providers/sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays an H5P activity entry page. | ||||
| @ -57,10 +60,15 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | ||||
|     fileUrl: string; // The fileUrl to use to play the package.
 | ||||
|     state: string; // State of the file.
 | ||||
|     siteCanDownload: boolean; | ||||
|     trackComponent: string; // Component for tracking.
 | ||||
|     hasOffline: boolean; | ||||
|     isOpeningPage: boolean; | ||||
| 
 | ||||
|     protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity'; | ||||
|     protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED; | ||||
|     protected site: CoreSite; | ||||
|     protected observer; | ||||
|     protected messageListenerFunction: (event: MessageEvent) => Promise<void>; | ||||
| 
 | ||||
|     constructor(injector: Injector, | ||||
|             @Optional() protected content: Content) { | ||||
| @ -68,6 +76,10 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | ||||
| 
 | ||||
|         this.site = this.sitesProvider.getCurrentSite(); | ||||
|         this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite(); | ||||
| 
 | ||||
|         // Listen for messages from the iframe.
 | ||||
|         this.messageListenerFunction = this.onIframeMessage.bind(this); | ||||
|         window.addEventListener('message', this.messageListenerFunction); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -96,23 +108,30 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|         try { | ||||
|             this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id); | ||||
|             this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id, false, this.siteId); | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.h5pActivity); | ||||
|             this.description = this.h5pActivity.intro; | ||||
|             this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions); | ||||
| 
 | ||||
|             if (this.h5pActivity.package && this.h5pActivity.package[0]) { | ||||
|                 // The online player should use the original file, not the trusted one.
 | ||||
|                 this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl( | ||||
|                             this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions); | ||||
|             if (sync) { | ||||
|                 await this.syncActivity(showErrors); | ||||
|             } | ||||
| 
 | ||||
|             await Promise.all([ | ||||
|                 this.checkHasOffline(), | ||||
|                 this.fetchAccessInfo(), | ||||
|                 this.fetchDeployedFileData(), | ||||
|             ]); | ||||
| 
 | ||||
|             this.trackComponent = this.accessInfo.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : ''; | ||||
| 
 | ||||
|             if (this.h5pActivity.package && this.h5pActivity.package[0]) { | ||||
|                 // The online player should use the original file, not the trusted one.
 | ||||
|                 this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl( | ||||
|                             this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions, this.trackComponent); | ||||
|             } | ||||
| 
 | ||||
|             if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) { | ||||
|                 // Cannot download the file or already downloaded, play the package directly.
 | ||||
|                 this.play(); | ||||
| @ -127,13 +146,22 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the access info and store it in the right variables. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async checkHasOffline(): Promise<void> { | ||||
|         this.hasOffline = await CoreXAPIOffline.instance.contextHasStatements(this.h5pActivity.context, this.siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the access info and store it in the right variables. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchAccessInfo(): Promise<void> { | ||||
|         this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id); | ||||
|         this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id, false, this.siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -322,8 +350,114 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | ||||
|     /** | ||||
|      * Go to view user events. | ||||
|      */ | ||||
|     viewMyAttempts(): void { | ||||
|         this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {courseId: this.courseId, h5pActivityId: this.h5pActivity.id}); | ||||
|     async viewMyAttempts(): Promise<void> { | ||||
|         this.isOpeningPage = true; | ||||
| 
 | ||||
|         try { | ||||
|             await this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', { | ||||
|                 courseId: this.courseId, | ||||
|                 h5pActivityId: this.h5pActivity.id, | ||||
|             }); | ||||
|         } finally { | ||||
|             this.isOpeningPage = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Treat an iframe message event. | ||||
|      * | ||||
|      * @param event Event. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async onIframeMessage(event: MessageEvent): Promise<void> { | ||||
|         if (!event.data || !CoreXAPI.instance.canPostStatementsInSite(this.site) || !this.isCurrentXAPIPost(event.data)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             const options = { | ||||
|                 offline: this.hasOffline, | ||||
|                 courseId: this.courseId, | ||||
|                 extra: this.h5pActivity.name, | ||||
|                 siteId: this.site.getId(), | ||||
|             }; | ||||
| 
 | ||||
|             const sent = await CoreXAPI.instance.postStatements(this.h5pActivity.context, event.data.component, | ||||
|                     JSON.stringify(event.data.statements), options); | ||||
| 
 | ||||
|             this.hasOffline = !sent; | ||||
| 
 | ||||
|             if (sent) { | ||||
|                 try { | ||||
|                     // Invalidate attempts.
 | ||||
|                     await AddonModH5PActivity.instance.invalidateUserAttempts(this.h5pActivity.id, undefined, this.siteId); | ||||
|                 } catch (error) { | ||||
|                     // Ignore errors.
 | ||||
|                 } | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'Error sending tracking data.'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if an event is an XAPI post statement of the current activity. | ||||
|      * | ||||
|      * @param data Event data. | ||||
|      * @return Whether it's an XAPI post statement of the current activity. | ||||
|      */ | ||||
|     protected isCurrentXAPIPost(data: any): boolean { | ||||
|         if (data.context != 'moodleapp' || data.action != 'xapi_post_statement' || !data.statements) { | ||||
|             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; | ||||
|         } | ||||
| 
 | ||||
|         if (!this.site.containsUrl(trackingUrl)) { | ||||
|             // The event belongs to another site, weird scenario. Maybe some JS running in background.
 | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const match = trackingUrl.match(/xapi\/activity\/(\d+)/); | ||||
| 
 | ||||
|         return match && match[1] == this.h5pActivity.context; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Performs the sync of the activity. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected sync(): Promise<any> { | ||||
|         return AddonModH5PActivitySync.instance.syncActivity(this.h5pActivity.context, this.site.getId()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * An autosync event has been received. | ||||
|      * | ||||
|      * @param syncEventData Data receiven on sync observer. | ||||
|      */ | ||||
|     protected autoSyncEventReceived(syncEventData: any): void { | ||||
|         this.checkHasOffline(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Go to blog posts. | ||||
|      * | ||||
|      * @param event Event. | ||||
|      */ | ||||
|     async gotoBlog(event: any): Promise<void> { | ||||
|         this.isOpeningPage = true; | ||||
| 
 | ||||
|         try { | ||||
|             await super.gotoBlog(event); | ||||
|         } finally { | ||||
|             this.isOpeningPage = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -331,5 +465,6 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.observer && this.observer.off(); | ||||
|         window.removeEventListener('message', this.messageListenerFunction); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -14,6 +14,7 @@ | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreCronDelegate } from '@providers/cron'; | ||||
| import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; | ||||
| import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; | ||||
| import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; | ||||
| @ -21,13 +22,16 @@ import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module- | ||||
| import { AddonModH5PActivityComponentsModule } from './components/components.module'; | ||||
| import { AddonModH5PActivityModuleHandler } from './providers/module-handler'; | ||||
| import { AddonModH5PActivityProvider } from './providers/h5pactivity'; | ||||
| import { AddonModH5PActivitySyncProvider } from './providers/sync'; | ||||
| import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler'; | ||||
| import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler'; | ||||
| import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler'; | ||||
| import { AddonModH5PActivitySyncCronHandler } from './providers/sync-cron-handler'; | ||||
| 
 | ||||
| // List of providers (without handlers).
 | ||||
| export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [ | ||||
|     AddonModH5PActivityProvider, | ||||
|     AddonModH5PActivitySyncProvider, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
| @ -38,10 +42,12 @@ export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [ | ||||
|     ], | ||||
|     providers: [ | ||||
|         AddonModH5PActivityProvider, | ||||
|         AddonModH5PActivitySyncProvider, | ||||
|         AddonModH5PActivityModuleHandler, | ||||
|         AddonModH5PActivityPrefetchHandler, | ||||
|         AddonModH5PActivityIndexLinkHandler, | ||||
|         AddonModH5PActivityReportLinkHandler, | ||||
|         AddonModH5PActivitySyncCronHandler, | ||||
|     ] | ||||
| }) | ||||
| export class AddonModH5PActivityModule { | ||||
| @ -51,11 +57,14 @@ export class AddonModH5PActivityModule { | ||||
|             prefetchHandler: AddonModH5PActivityPrefetchHandler, | ||||
|             linksDelegate: CoreContentLinksDelegate, | ||||
|             indexHandler: AddonModH5PActivityIndexLinkHandler, | ||||
|             reportLinkHandler: AddonModH5PActivityReportLinkHandler) { | ||||
|             reportLinkHandler: AddonModH5PActivityReportLinkHandler, | ||||
|             cronDelegate: CoreCronDelegate, | ||||
|             syncHandler: AddonModH5PActivitySyncCronHandler) { | ||||
| 
 | ||||
|         moduleDelegate.registerHandler(moduleHandler); | ||||
|         prefetchDelegate.registerHandler(prefetchHandler); | ||||
|         linksDelegate.registerHandler(indexHandler); | ||||
|         linksDelegate.registerHandler(reportLinkHandler); | ||||
|         cronDelegate.register(syncHandler); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -24,6 +24,7 @@ | ||||
|     "no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.", | ||||
|     "offlinedisabledwarning": "You will need to be online to view the H5P package.", | ||||
|     "outcome": "Outcome", | ||||
|     "previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.", | ||||
|     "result_fill-in": "Fill-in text", | ||||
|     "result_other": "Unkown interaction type", | ||||
|     "review_my_attempts": "View my attempts", | ||||
|  | ||||
| @ -14,9 +14,12 @@ | ||||
| 
 | ||||
| import { Component, ViewChild } from '@angular/core'; | ||||
| import { IonicPage, NavParams } from 'ionic-angular'; | ||||
| import { CoreDomUtils } from '@providers/utils/dom'; | ||||
| import { AddonModH5PActivityIndexComponent } from '../../components/index/index'; | ||||
| import { AddonModH5PActivityData } from '../../providers/h5pactivity'; | ||||
| 
 | ||||
| import { Translate } from '@singletons/core.singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays an H5P activity. | ||||
|  */ | ||||
| @ -46,4 +49,17 @@ export class AddonModH5PActivityIndexPage { | ||||
|     updateData(h5p: AddonModH5PActivityData): void { | ||||
|         this.title = h5p.name || this.title; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if we can leave the page or not. | ||||
|      * | ||||
|      * @return Resolved if we can leave it, rejected if not. | ||||
|      */ | ||||
|     ionViewCanLeave(): Promise<void> { | ||||
|         if (!this.h5pComponent.playing || this.h5pComponent.isOpeningPage) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         return CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmleaveunknownchanges')); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -31,6 +31,7 @@ import { makeSingleton, Translate } from '@singletons/core.singletons'; | ||||
| @Injectable() | ||||
| export class AddonModH5PActivityProvider { | ||||
|     static COMPONENT = 'mmaModH5PActivity'; | ||||
|     static TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking.
 | ||||
| 
 | ||||
|     protected ROOT_CACHE_KEY = 'mmaModH5PActivity:'; | ||||
| 
 | ||||
| @ -384,6 +385,20 @@ export class AddonModH5PActivityProvider { | ||||
|         return this.getH5PActivityByField(courseId, 'coursemodule', cmId, forceCache, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get an H5P activity by context ID. | ||||
|      * | ||||
|      * @param courseId Course ID. | ||||
|      * @param contextId Context ID. | ||||
|      * @param forceCache Whether it should always return cached data. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the activity data. | ||||
|      */ | ||||
|     getH5PActivityByContextId(courseId: number, contextId: number, forceCache?: boolean, siteId?: string) | ||||
|             : Promise<AddonModH5PActivityData> { | ||||
|         return this.getH5PActivityByField(courseId, 'context', contextId, forceCache, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get an H5P activity by instance ID. | ||||
|      * | ||||
| @ -595,6 +610,7 @@ export type AddonModH5PActivityData = { | ||||
|     grademethod: number; // Which H5P attempt is used for grading.
 | ||||
|     contenthash?: string; // Sha1 hash of file content.
 | ||||
|     coursemodule: number; // Coursemodule.
 | ||||
|     context: number; // Context ID.
 | ||||
|     introfiles: CoreWSExternalFile[]; | ||||
|     package: CoreWSExternalFile[]; | ||||
|     deployedfile?: { | ||||
|  | ||||
							
								
								
									
										46
									
								
								src/addon/mod/h5pactivity/providers/sync-cron-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/addon/mod/h5pactivity/providers/sync-cron-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| // (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 { CoreCronHandler } from '@providers/cron'; | ||||
| import { AddonModH5PActivitySync } from './sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Synchronization cron handler. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModH5PActivitySyncCronHandler implements CoreCronHandler { | ||||
|     name = 'AddonModH5PActivitySyncCronHandler'; | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the process. | ||||
|      * Receives the ID of the site affected, undefined for all sites. | ||||
|      * | ||||
|      * @param siteId ID of the site affected, undefined for all sites. | ||||
|      * @param force Wether the execution is forced (manual sync). | ||||
|      * @return Promise resolved when done, rejected if failure. | ||||
|      */ | ||||
|     execute(siteId?: string, force?: boolean): Promise<any> { | ||||
|         return AddonModH5PActivitySync.instance.syncAllActivities(siteId, force); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the time between consecutive executions. | ||||
|      * | ||||
|      * @return Time between consecutive executions (in ms). | ||||
|      */ | ||||
|     getInterval(): number { | ||||
|         return AddonModH5PActivitySync.instance.syncInterval; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										223
									
								
								src/addon/mod/h5pactivity/providers/sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/addon/mod/h5pactivity/providers/sync.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,223 @@ | ||||
| // (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 { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreEvents } from '@providers/events'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreSyncProvider } from '@providers/sync'; | ||||
| import { CoreUtils } from '@providers/utils/utils'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { CoreCourse } from '@core/course/providers/course'; | ||||
| import { CoreCourseLogHelper } from '@core/course/providers/log-helper'; | ||||
| import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; | ||||
| import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; | ||||
| import { CoreXAPI } from '@core/xapi/providers/xapi'; | ||||
| import { CoreXAPIOffline } from '@core/xapi/providers/offline'; | ||||
| import { AddonModH5PActivity, AddonModH5PActivityProvider } from './h5pactivity'; | ||||
| import { AddonModH5PActivityPrefetchHandler } from './prefetch-handler'; | ||||
| 
 | ||||
| import { makeSingleton } from '@singletons/core.singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to sync H5P activities. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseProvider { | ||||
| 
 | ||||
|     static AUTO_SYNCED = 'addon_mod_h5pactivity_autom_synced'; | ||||
|     protected componentTranslate: string; | ||||
| 
 | ||||
|     constructor(sitesProvider: CoreSitesProvider, | ||||
|             loggerProvider: CoreLoggerProvider, | ||||
|             appProvider: CoreAppProvider, | ||||
|             translate: TranslateService, | ||||
|             textUtils: CoreTextUtilsProvider, | ||||
|             syncProvider: CoreSyncProvider, | ||||
|             timeUtils: CoreTimeUtilsProvider, | ||||
|             prefetchHandler: AddonModH5PActivityPrefetchHandler, | ||||
|             prefetchDelegate: CoreCourseModulePrefetchDelegate) { | ||||
| 
 | ||||
|         super('AddonModH5PActivitySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, | ||||
|                 timeUtils, prefetchDelegate, prefetchHandler); | ||||
| 
 | ||||
|         this.componentTranslate = CoreCourse.instance.translateModuleName('h5pactivity'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Try to synchronize all the H5P activities in a certain site or in all sites. | ||||
|      * | ||||
|      * @param siteId Site ID to sync. If not defined, sync all sites. | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     syncAllActivities(siteId?: string, force?: boolean): Promise<void> { | ||||
|         return this.syncOnSites('H5P activities', this.syncAllActivitiesFunc.bind(this), [force], siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync all H5P activities on a site. | ||||
|      * | ||||
|      * @param siteId Site ID to sync. If not defined, sync all sites. | ||||
|      * @param force Wether to force sync not depending on last execution. | ||||
|      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||
|      */ | ||||
|     protected async syncAllActivitiesFunc(siteId?: string, force?: boolean): Promise<void> { | ||||
|         const entries = await CoreXAPIOffline.instance.getAllStatements(siteId); | ||||
| 
 | ||||
|         // Sync all responses.
 | ||||
|         const promises = entries.map((response) => { | ||||
|             const promise = force ? this.syncActivity(response.contextid, siteId) : | ||||
|                     this.syncActivityIfNeeded(response.contextid, siteId); | ||||
| 
 | ||||
|             return promise.then((result) => { | ||||
|                 if (result && result.updated) { | ||||
|                     // Sync successful, send event.
 | ||||
|                     CoreEvents.instance.trigger(AddonModH5PActivitySyncProvider.AUTO_SYNCED, { | ||||
|                         contextId: response.contextid, | ||||
|                         warnings: result.warnings, | ||||
|                     }, siteId); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync an H5P activity only if a certain time has passed since the last time. | ||||
|      * | ||||
|      * @param contextId Context ID of the activity. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when the activity is synced or it doesn't need to be synced. | ||||
|      */ | ||||
|     async syncActivityIfNeeded(contextId: number, siteId?: string): Promise<any> { | ||||
|         const needed = await this.isSyncNeeded(contextId, siteId); | ||||
| 
 | ||||
|         if (needed) { | ||||
|             return this.syncActivity(contextId, siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize an H5P activity. If it's already being synced it will reuse the same promise. | ||||
|      * | ||||
|      * @param contextId Context ID of the activity. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved if sync is successful, rejected otherwise. | ||||
|      */ | ||||
|     syncActivity(contextId: number, siteId?: string): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (!this.appProvider.isOnline()) { | ||||
|             // Cannot sync in offline.
 | ||||
|             throw this.translate.instant('core.networkerrormsg'); | ||||
|         } | ||||
| 
 | ||||
|         if (this.isSyncing(contextId, siteId)) { | ||||
|             // There's already a sync ongoing for this discussion, return the promise.
 | ||||
|             return this.getOngoingSync(contextId, siteId); | ||||
|         } | ||||
| 
 | ||||
|         return this.addOngoingSync(contextId, this.syncActivityData(contextId, siteId), siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Synchronize an H5P activity. | ||||
|      * | ||||
|      * @param contextId Context ID of the activity. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved if sync is successful, rejected otherwise. | ||||
|      */ | ||||
|     protected async syncActivityData(contextId: number, siteId: string): Promise<{warnings: string[], updated: boolean}> { | ||||
| 
 | ||||
|         this.logger.debug(`Try to sync H5P activity with context ID '${contextId}'`); | ||||
| 
 | ||||
|         const result = { | ||||
|             warnings: [], | ||||
|             updated: false, | ||||
|         }; | ||||
| 
 | ||||
|         // Get all the statements stored for the activity.
 | ||||
|         const entries = await CoreXAPIOffline.instance.getContextStatements(contextId, siteId); | ||||
| 
 | ||||
|         if (!entries || !entries.length) { | ||||
|             // Nothing to sync.
 | ||||
|             await this.setSyncTime(contextId, siteId); | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         // Get the activity instance.
 | ||||
|         const courseId = entries[0].courseid; | ||||
| 
 | ||||
|         const h5pActivity = await AddonModH5PActivity.instance.getH5PActivityByContextId(courseId, contextId, false, siteId); | ||||
| 
 | ||||
|         // Sync offline logs.
 | ||||
|         try { | ||||
|             await CoreCourseLogHelper.instance.syncIfNeeded(AddonModH5PActivityProvider.COMPONENT, h5pActivity.id, siteId); | ||||
|         } catch (error) { | ||||
|             // Ignore errors.
 | ||||
|         } | ||||
| 
 | ||||
|         // Send the statements in order.
 | ||||
|         for (let i = 0; i < entries.length; i++) { | ||||
|             const entry = entries[i]; | ||||
| 
 | ||||
|             try { | ||||
|                 await CoreXAPI.instance.postStatementsOnline(entry.component, entry.statements, siteId); | ||||
| 
 | ||||
|                 result.updated = true; | ||||
| 
 | ||||
|                 await CoreXAPIOffline.instance.deleteStatements(entry.id, siteId); | ||||
|             } catch (error) { | ||||
|                 if (CoreUtils.instance.isWebServiceError(error)) { | ||||
|                     // The WebService has thrown an error, this means that statements cannot be submitted. Delete them.
 | ||||
|                     result.updated = true; | ||||
| 
 | ||||
|                     await CoreXAPIOffline.instance.deleteStatements(entry.id, siteId); | ||||
| 
 | ||||
|                     // Responses deleted, add a warning.
 | ||||
|                     result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { | ||||
|                         component: this.componentTranslate, | ||||
|                         name: entry.extra, | ||||
|                         error: this.textUtils.getErrorMessageFromError(error), | ||||
|                     })); | ||||
|                 } else { | ||||
|                     // Stop synchronizing.
 | ||||
|                     throw error; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (result.updated) { | ||||
|             try { | ||||
|                 // Data has been sent to server, invalidate attempts.
 | ||||
|                 await AddonModH5PActivity.instance.invalidateUserAttempts(h5pActivity.id, undefined, siteId); | ||||
|             } catch (error) { | ||||
|                 // Ignore errors.
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Sync finished, set sync time.
 | ||||
|         await this.setSyncTime(contextId, siteId); | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class AddonModH5PActivitySync extends makeSingleton(AddonModH5PActivitySyncProvider) {} | ||||
| @ -88,6 +88,7 @@ import { CoreFilterModule } from '@core/filter/filter.module'; | ||||
| import { CoreH5PModule } from '@core/h5p/h5p.module'; | ||||
| import { CoreSearchModule } from '@core/search/search.module'; | ||||
| import { CoreEditorModule } from '@core/editor/editor.module'; | ||||
| import { CoreXAPIModule } from '@core/xapi/xapi.module'; | ||||
| 
 | ||||
| // Addon modules.
 | ||||
| import { AddonBadgesModule } from '@addon/badges/badges.module'; | ||||
| @ -241,6 +242,7 @@ export const WP_PROVIDER: any = null; | ||||
|         CoreH5PModule, | ||||
|         CoreSearchModule, | ||||
|         CoreEditorModule, | ||||
|         CoreXAPIModule, | ||||
|         AddonBadgesModule, | ||||
|         AddonBlogModule, | ||||
|         AddonCalendarModule, | ||||
|  | ||||
| @ -684,6 +684,7 @@ | ||||
|     "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.", | ||||
|     "addon.mod_h5pactivity.offlinedisabledwarning": "You will need to be online to view the H5P package.", | ||||
|     "addon.mod_h5pactivity.outcome": "Outcome", | ||||
|     "addon.mod_h5pactivity.previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.", | ||||
|     "addon.mod_h5pactivity.result_fill-in": "Fill-in text", | ||||
|     "addon.mod_h5pactivity.result_other": "Unkown interaction type", | ||||
|     "addon.mod_h5pactivity.review_my_attempts": "View my attempts", | ||||
| @ -1406,6 +1407,7 @@ | ||||
|     "core.confirmdeletefile": "Are you sure you want to delete this file?", | ||||
|     "core.confirmgotabroot": "Are you sure you want to go back to {{name}}?", | ||||
|     "core.confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?", | ||||
|     "core.confirmleaveunknownchanges": "Are you sure you want to leave this page? If you have some unsaved changes they will be lost.", | ||||
|     "core.confirmloss": "Are you sure? All changes will be lost.", | ||||
|     "core.confirmopeninbrowser": "Do you want to open it in a web browser?", | ||||
|     "core.considereddigitalminor": "You are too young to create an account on this site.", | ||||
| @ -1859,6 +1861,7 @@ | ||||
|     "core.mod_folder": "Folder", | ||||
|     "core.mod_forum": "Forum", | ||||
|     "core.mod_glossary": "Glossary", | ||||
|     "core.mod_h5pactivity": "H5P", | ||||
|     "core.mod_ims": "IMS content package", | ||||
|     "core.mod_imscp": "IMS content package", | ||||
|     "core.mod_label": "Label", | ||||
|  | ||||
| @ -1820,7 +1820,7 @@ export class CoreSite { | ||||
| 
 | ||||
|             this.lastAutoLogin = this.timeUtils.timestamp(); | ||||
| 
 | ||||
|             return data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + url; | ||||
|             return data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + encodeURIComponent(url); | ||||
|         }).catch(() => { | ||||
| 
 | ||||
|             // Couldn't get autologin key, return the same URL.
 | ||||
|  | ||||
| @ -41,6 +41,7 @@ import { CORE_PUSHNOTIFICATIONS_PROVIDERS } from '@core/pushnotifications/pushno | ||||
| import { IONIC_NATIVE_PROVIDERS } from '@core/emulator/emulator.module'; | ||||
| import { CORE_EDITOR_PROVIDERS } from '@core/editor/editor.module'; | ||||
| import { CORE_SEARCH_PROVIDERS } from '@core/search/search.module'; | ||||
| import { CORE_XAPI_PROVIDERS } from '@core/xapi/xapi.module'; | ||||
| 
 | ||||
| // Import only this provider to prevent circular dependencies.
 | ||||
| import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins'; | ||||
| @ -243,7 +244,7 @@ export class CoreCompileProvider { | ||||
|                 .concat(ADDON_MOD_WORKSHOP_PROVIDERS).concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS) | ||||
|                 .concat(CORE_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS).concat(CORE_BLOCK_PROVIDERS) | ||||
|                 .concat(CORE_FILTER_PROVIDERS).concat(CORE_H5P_PROVIDERS).concat(CORE_EDITOR_PROVIDERS) | ||||
|                 .concat(CORE_SEARCH_PROVIDERS).concat(ADDON_MOD_H5P_ACTIVITY_PROVIDERS); | ||||
|                 .concat(CORE_SEARCH_PROVIDERS).concat(ADDON_MOD_H5P_ACTIVITY_PROVIDERS).concat(CORE_XAPI_PROVIDERS); | ||||
| 
 | ||||
|         // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
 | ||||
|         for (const i in providers) { | ||||
|  | ||||
| @ -280,8 +280,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|      * | ||||
|      * @param event Event. | ||||
|      */ | ||||
|     gotoBlog(event: any): void { | ||||
|         this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id }); | ||||
|     gotoBlog(event: any): Promise<any> { | ||||
|         return this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -244,6 +244,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { | ||||
| 
 | ||||
|     protected ROOT_CACHE_KEY = 'mmCourse:'; | ||||
|     protected statusCache = new CoreCache(); | ||||
|     protected featurePrefix = 'CoreCourseModuleDelegate_'; | ||||
|     protected handlerNameProperty = 'modName'; | ||||
| 
 | ||||
|     // Promises for check updates, to prevent performing the same request twice at the same time.
 | ||||
|  | ||||
| @ -1,35 +0,0 @@ | ||||
| // (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.
 | ||||
| 
 | ||||
| /** | ||||
|  * Handle display options included in the URL and put them in the H5PIntegration object if it exists. | ||||
|  */ | ||||
| 
 | ||||
| if (window.H5PIntegration && window.H5PIntegration.contents && location.search) { | ||||
|     var contentData = window.H5PIntegration.contents[Object.keys(window.H5PIntegration.contents)[0]]; | ||||
| 
 | ||||
|     if (contentData) { | ||||
|         contentData.displayOptions = contentData.displayOptions || {}; | ||||
| 
 | ||||
|         var search = location.search.replace(/^\?/, ''), | ||||
|             split = search.split('&'); | ||||
| 
 | ||||
|         split.forEach(function(param) { | ||||
|             var nameAndValue = param.split('='); | ||||
|             if (nameAndValue.length == 2) { | ||||
|                 contentData.displayOptions[nameAndValue[0]] = nameAndValue[1] === '1' || nameAndValue[1] === 'true'; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @ -71,6 +71,21 @@ H5PEmbedCommunicator = (function() { | ||||
|             // Parent origin can be anything.
 | ||||
|             window.parent.postMessage(data, '*'); | ||||
|         }; | ||||
| 
 | ||||
|         /** | ||||
|          * Send a xAPI statement to LMS. | ||||
|          * | ||||
|          * @param {string} component | ||||
|          * @param {Object} statements | ||||
|          */ | ||||
|         self.post = function(component, statements) { | ||||
|             window.parent.postMessage({ | ||||
|                 context: 'moodleapp', | ||||
|                 action: 'xapi_post_statement', | ||||
|                 component: component, | ||||
|                 statements: statements, | ||||
|             }, '*'); | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     return (window.postMessage && window.addEventListener ? new Communicator() : undefined); | ||||
| @ -150,6 +165,38 @@ document.onreadystatechange = function() { | ||||
|         }, 0); | ||||
|     }); | ||||
| 
 | ||||
|     // Get emitted xAPI data.
 | ||||
|     H5P.externalDispatcher.on('xAPI', function(event) { | ||||
|         var moodlecomponent = H5P.getMoodleComponent(); | ||||
|         if (moodlecomponent == undefined) { | ||||
|             return; | ||||
|         } | ||||
|         // Skip malformed events.
 | ||||
|         var hasStatement = event && event.data && event.data.statement; | ||||
|         if (!hasStatement) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         var statement = event.data.statement; | ||||
|         var validVerb = statement.verb && statement.verb.id; | ||||
|         if (!validVerb) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         var isCompleted = statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered' | ||||
|                     || statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed'; | ||||
| 
 | ||||
|         var isChild = statement.context && statement.context.contextActivities && | ||||
|         statement.context.contextActivities.parent && | ||||
|         statement.context.contextActivities.parent[0] && | ||||
|         statement.context.contextActivities.parent[0].id; | ||||
| 
 | ||||
|         if (isCompleted && !isChild) { | ||||
|             var statements = H5P.getXAPIStatements(this.contentId, statement); | ||||
|             H5PEmbedCommunicator.post(moodlecomponent, statements); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // Trigger initial resize for instance.
 | ||||
|     H5P.trigger(instance, 'resize'); | ||||
| }; | ||||
|  | ||||
							
								
								
									
										55
									
								
								src/core/h5p/assets/moodle/js/h5p_overrides.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/core/h5p/assets/moodle/js/h5p_overrides.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| // This file is part of Moodle - http://moodle.org/
 | ||||
| //
 | ||||
| // Moodle is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| //
 | ||||
| // Moodle is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| //
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| H5P.findInstanceFromId = function (contentId) { | ||||
|     if (!contentId) { | ||||
|         return H5P.instances[0]; | ||||
|     } | ||||
|     if (H5P.instances !== undefined) { | ||||
|         for (var i = 0; i < H5P.instances.length; i++) { | ||||
|             if (H5P.instances[i].contentId === contentId) { | ||||
|                 return H5P.instances[i]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     return undefined; | ||||
| }; | ||||
| H5P.getXAPIStatements = function (contentId, statement) { | ||||
|     var statements = []; | ||||
|     var instance = H5P.findInstanceFromId(contentId); | ||||
|     if (!instance){ | ||||
|         return statements; | ||||
|     } | ||||
|     if (instance.getXAPIData == undefined) { | ||||
|         var xAPIData = { | ||||
|             statement: statement | ||||
|         }; | ||||
|     } else { | ||||
|         var xAPIData = instance.getXAPIData(); | ||||
|     } | ||||
|     if (xAPIData.statement != undefined) { | ||||
|         statements.push(xAPIData.statement); | ||||
|     } | ||||
|     if (xAPIData.children != undefined) { | ||||
|         statements = statements.concat(xAPIData.children.map(a => a.statement)); | ||||
|     } | ||||
|     return statements; | ||||
| }; | ||||
| H5P.getMoodleComponent = function () { | ||||
|     if (H5PIntegration.moodleComponent) { | ||||
|         return H5PIntegration.moodleComponent; | ||||
|     } | ||||
|     return undefined; | ||||
| }; | ||||
							
								
								
									
										46
									
								
								src/core/h5p/assets/moodle/js/params.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/core/h5p/assets/moodle/js/params.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| // (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.
 | ||||
| 
 | ||||
| /** | ||||
|  * Handle params included in the URL and put them in the H5PIntegration object if it exists. | ||||
|  */ | ||||
| 
 | ||||
| if (window.H5PIntegration && window.H5PIntegration.contents && location.search) { | ||||
|     var contentData = window.H5PIntegration.contents[Object.keys(window.H5PIntegration.contents)[0]]; | ||||
| 
 | ||||
|     var search = location.search.replace(/^\?/, ''); | ||||
|     var split = search.split('&'); | ||||
| 
 | ||||
|     split.forEach(function(param) { | ||||
|         var nameAndValue = param.split('='); | ||||
| 
 | ||||
|         if (nameAndValue[0] == 'displayOptions' && contentData) { | ||||
|             try { | ||||
|                 contentData.displayOptions = contentData.displayOptions || {}; | ||||
| 
 | ||||
|                 var displayOptions = JSON.parse(decodeURIComponent(nameAndValue[1])); | ||||
| 
 | ||||
|                 if (displayOptions && typeof displayOptions == 'object') { | ||||
|                     Object.assign(contentData.displayOptions, displayOptions); | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 console.error('Error parsing display options', decodeURIComponent(nameAndValue[1])); | ||||
|             } | ||||
|         } else if (nameAndValue[0] == 'component') { | ||||
|             window.H5PIntegration.moodleComponent = nameAndValue[1]; | ||||
|         } else if (nameAndValue[0] == 'trackingUrl' && contentData) { | ||||
|             contentData.url = nameAndValue[1]; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| @ -615,6 +615,8 @@ export class CoreH5PCore { | ||||
|             urls.push(libUrl + script); | ||||
|         }); | ||||
| 
 | ||||
|         urls.push(CoreTextUtils.instance.concatenatePaths(libUrl, 'moodle/js/h5p_overrides.js')); | ||||
| 
 | ||||
|         return urls; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -17,6 +17,7 @@ import { CoreSites } from '@providers/sites'; | ||||
| import { CoreMimetypeUtils } from '@providers/utils/mimetype'; | ||||
| import { CoreTextUtils } from '@providers/utils/text'; | ||||
| import { CoreUtils } from '@providers/utils/utils'; | ||||
| import { CoreUser } from '@core/user/providers/user'; | ||||
| import { CoreH5P } from '../providers/h5p'; | ||||
| import { CoreH5PCore, CoreH5PDisplayOptions } from './core'; | ||||
| import { FileEntry } from '@ionic-native/file'; | ||||
| @ -90,6 +91,13 @@ export class CoreH5PHelper { | ||||
|     static async getCoreSettings(siteId?: string): Promise<any> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         let user; | ||||
| 
 | ||||
|         try { | ||||
|             user = await CoreUser.instance.getProfile(site.getUserId(), undefined, true); | ||||
|         } catch (error) { | ||||
|             // Ignore errors.
 | ||||
|         } | ||||
| 
 | ||||
|         const basePath = CoreFile.instance.getBasePathInstant(); | ||||
|         const ajaxPaths = { | ||||
| @ -110,7 +118,7 @@ export class CoreH5PHelper { | ||||
|             l10n: { | ||||
|                 H5P: CoreH5P.instance.h5pCore.getLocalization(), | ||||
|             }, | ||||
|             user: [], | ||||
|             user: {name: site.getInfo().fullname, mail: user && user.email}, | ||||
|             hubIsEnabled: false, | ||||
|             reportingIsEnabled: false, | ||||
|             crossorigin: null, | ||||
|  | ||||
| @ -17,6 +17,7 @@ import { CoreSites } from '@providers/sites'; | ||||
| import { CoreTextUtils } from '@providers/utils/text'; | ||||
| import { CoreUrlUtils } from '@providers/utils/url'; | ||||
| import { CoreUtils } from '@providers/utils/utils'; | ||||
| import { CoreXAPI } from '@core/xapi/providers/xapi'; | ||||
| import { CoreH5P } from '../providers/h5p'; | ||||
| import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core'; | ||||
| import { CoreH5PHelper } from './helper'; | ||||
| @ -81,7 +82,7 @@ export class CoreH5PPlayer { | ||||
|             resizeCode: this.getResizeCode(), | ||||
|             title: content.slug, | ||||
|             displayOptions: {}, | ||||
|             url: this.getEmbedUrl(site.getURL(), h5pUrl), | ||||
|             url: '', // It will be filled using dynamic params if needed.
 | ||||
|             contentUrl: contentUrl, | ||||
|             metadata: content.metadata, | ||||
|             contentUserData: [ | ||||
| @ -109,9 +110,9 @@ export class CoreH5PPlayer { | ||||
|         html += '<script type="text/javascript">var H5PIntegration = ' + | ||||
|                 JSON.stringify(result.settings).replace(/\//g, '\\/') + '</script>'; | ||||
| 
 | ||||
|         // Add our own script to handle the display options.
 | ||||
|         // Add our own script to handle the params.
 | ||||
|         html += '<script type="text/javascript" src="' + CoreTextUtils.instance.concatenatePaths( | ||||
|                 this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/displayoptions.js') + '"></script>'; | ||||
|                 this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/params.js') + '"></script>'; | ||||
| 
 | ||||
|         html += '</head><body>'; | ||||
| 
 | ||||
| @ -241,20 +242,34 @@ export class CoreH5PPlayer { | ||||
|      * | ||||
|      * @param fileUrl URL of the H5P package. | ||||
|      * @param displayOptions Display options. | ||||
|      * @param component Component to send xAPI events to. | ||||
|      * @param contextId Context ID where the H5P is. Required for tracking. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the file URL if exists, rejected otherwise. | ||||
|      */ | ||||
|     async getContentIndexFileUrl(fileUrl: string, displayOptions?: CoreH5PDisplayOptions, siteId?: string): Promise<string> { | ||||
|     async getContentIndexFileUrl(fileUrl: string, displayOptions?: CoreH5PDisplayOptions, component?: string, contextId?: number, | ||||
|             siteId?: string): Promise<string> { | ||||
| 
 | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const path = await this.h5pCore.h5pFS.getContentIndexFileUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         // Add display options to the URL.
 | ||||
|         // Add display options and component to the URL.
 | ||||
|         const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         displayOptions = this.h5pCore.fixDisplayOptions(displayOptions, data.id); | ||||
| 
 | ||||
|         return CoreUrlUtils.instance.addParamsToUrl(path, displayOptions, undefined, true); | ||||
|         const params = { | ||||
|             displayOptions: JSON.stringify(displayOptions), | ||||
|             component: component || '', | ||||
|             trackingUrl: undefined, | ||||
|         }; | ||||
| 
 | ||||
|         if (contextId) { | ||||
|             params.trackingUrl = await CoreXAPI.instance.getUrl(contextId, 'activity', siteId); | ||||
|         } | ||||
| 
 | ||||
|         return CoreUrlUtils.instance.addParamsToUrl(path, params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -38,6 +38,8 @@ export class CoreH5PIframeComponent implements OnChanges { | ||||
|     @Input() fileUrl?: string; // The URL of the H5P file. If not supplied, onlinePlayerUrl is required.
 | ||||
|     @Input() displayOptions?: CoreH5PDisplayOptions; // Display options.
 | ||||
|     @Input() onlinePlayerUrl?: string; // The URL of the online player to display the H5P package.
 | ||||
|     @Input() trackComponent?: string; // Component to send xAPI events to.
 | ||||
|     @Input() contextId?: number; // Context ID. Required for tracking.
 | ||||
|     @Output() onIframeUrlSet = new EventEmitter<{src: string, online: boolean}>(); | ||||
|     @Output() onIframeLoaded = new EventEmitter<void>(); | ||||
| 
 | ||||
| @ -93,7 +95,7 @@ export class CoreH5PIframeComponent implements OnChanges { | ||||
|                 this.iframeSrc = localUrl; | ||||
|             } else { | ||||
|                 this.onlinePlayerUrl = this.onlinePlayerUrl || CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl( | ||||
|                         this.site.getURL(), this.fileUrl, this.displayOptions); | ||||
|                         this.site.getURL(), this.fileUrl, this.displayOptions, this.trackComponent); | ||||
| 
 | ||||
|                 // Never allow downloading in the app. This will only work if the user is allowed to change the params.
 | ||||
|                 const src = this.onlinePlayerUrl.replace(CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1', | ||||
| @ -121,7 +123,8 @@ export class CoreH5PIframeComponent implements OnChanges { | ||||
|      */ | ||||
|     protected async getLocalUrl(): Promise<string> { | ||||
|         try { | ||||
|             const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions, this.siteId); | ||||
|             const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions, | ||||
|                     this.trackComponent, this.contextId, this.siteId); | ||||
| 
 | ||||
|             return url; | ||||
|         } catch (error) { | ||||
| @ -135,7 +138,7 @@ export class CoreH5PIframeComponent implements OnChanges { | ||||
| 
 | ||||
|                 // File treated. Try to get the index file URL again.
 | ||||
|                 const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions, | ||||
|                         this.siteId); | ||||
|                         this.trackComponent, this.contextId, this.siteId); | ||||
| 
 | ||||
|                 return url; | ||||
|             } catch (error) { | ||||
|  | ||||
| @ -391,7 +391,7 @@ export class CoreH5PProvider { | ||||
|      * @param siteId Site ID (empty for current site). | ||||
|      * @return Promise resolved when the data is invalidated. | ||||
|      */ | ||||
|     async invalidateAvailableInContexts(url: string, siteId?: string): Promise<void> { | ||||
|     async invalidateGetTrustedH5PFile(url: string, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         await site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url)); | ||||
|  | ||||
							
								
								
									
										201
									
								
								src/core/xapi/providers/offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/core/xapi/providers/offline.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,201 @@ | ||||
| // (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, CoreSiteSchema } from '@providers/sites'; | ||||
| 
 | ||||
| import { makeSingleton } from '@singletons/core.singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle offline xAPI. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreXAPIOfflineProvider { | ||||
| 
 | ||||
|     // Variables for database.
 | ||||
|     static STATEMENTS_TABLE = 'core_xapi_statements'; | ||||
| 
 | ||||
|     protected siteSchema: CoreSiteSchema = { | ||||
|         name: 'CoreXAPIOfflineProvider', | ||||
|         version: 1, | ||||
|         tables: [ | ||||
|             { | ||||
|                 name: CoreXAPIOfflineProvider.STATEMENTS_TABLE, | ||||
|                 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' | ||||
|                     }, | ||||
|                 ], | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
| 
 | ||||
|     constructor() { | ||||
|         CoreSites.instance.registerSiteSchema(this.siteSchema); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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<boolean> { | ||||
|         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<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         await site.getDb().deleteRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {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<void> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         await site.getDb().deleteRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {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<CoreXAPIOfflineStatementsDBData[]> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         return site.getDb().getRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, 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<CoreXAPIOfflineStatementsDBData[]> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         return site.getDb().getRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {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<CoreXAPIOfflineStatementsDBData> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         return site.getDb().getRecord(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {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<void> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(options.siteId); | ||||
| 
 | ||||
|         const entry = { | ||||
|             contextid: contextId, | ||||
|             component: component, | ||||
|             statements: statements, | ||||
|             timecreated: Date.now(), | ||||
|             courseid: options.courseId, | ||||
|             extra: options.extra, | ||||
|         }; | ||||
| 
 | ||||
|         await site.getDb().insertRecord(CoreXAPIOfflineProvider.STATEMENTS_TABLE, entry); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class CoreXAPIOffline extends makeSingleton(CoreXAPIOfflineProvider) {} | ||||
| 
 | ||||
| /** | ||||
|  * DB data stored for statements. | ||||
|  */ | ||||
| export type CoreXAPIOfflineStatementsDBData = { | ||||
|     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.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * 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.
 | ||||
| }; | ||||
							
								
								
									
										143
									
								
								src/core/xapi/providers/xapi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/core/xapi/providers/xapi.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,143 @@ | ||||
| // (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 '@providers/app'; | ||||
| import { CoreSites } from '@providers/sites'; | ||||
| import { CoreTextUtils } from '@providers/utils/text'; | ||||
| import { CoreUtils } from '@providers/utils/utils'; | ||||
| import { CoreSite } from '@classes/site'; | ||||
| import { CoreXAPIOffline, CoreXAPIOfflineSaveStatementsOptions } from './offline'; | ||||
| 
 | ||||
| import { makeSingleton } from '@singletons/core.singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to provide XAPI functionalities. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreXAPIProvider { | ||||
| 
 | ||||
|     protected ROOT_CACHE_KEY = 'CoreXAPI:'; | ||||
| 
 | ||||
|     /** | ||||
|      * 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<boolean> { | ||||
|         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.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<string> { | ||||
|         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<boolean> { | ||||
| 
 | ||||
|         options = options || {}; | ||||
|         options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a message to be synchronized later.
 | ||||
|         const storeOffline = async (): Promise<boolean> => { | ||||
|             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<number[]> { | ||||
| 
 | ||||
|         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.
 | ||||
| }; | ||||
							
								
								
									
										34
									
								
								src/core/xapi/xapi.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/core/xapi/xapi.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| // (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 { CoreXAPIProvider } from './providers/xapi'; | ||||
| import { CoreXAPIOfflineProvider } from './providers/offline'; | ||||
| 
 | ||||
| // List of providers (without handlers).
 | ||||
| export const CORE_XAPI_PROVIDERS: any[] = [ | ||||
|     CoreXAPIProvider, | ||||
|     CoreXAPIOfflineProvider, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [], | ||||
|     imports: [], | ||||
|     providers: [ | ||||
|         CoreXAPIProvider, | ||||
|         CoreXAPIOfflineProvider, | ||||
|     ], | ||||
|     exports: [] | ||||
| }) | ||||
| export class CoreXAPIModule { } | ||||
| @ -43,6 +43,7 @@ | ||||
|     "confirmdeletefile": "Are you sure you want to delete this file?", | ||||
|     "confirmgotabroot": "Are you sure you want to go back to {{name}}?", | ||||
|     "confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?", | ||||
|     "confirmleaveunknownchanges": "Are you sure you want to leave this page? If you have some unsaved changes they will be lost.", | ||||
|     "confirmloss": "Are you sure? All changes will be lost.", | ||||
|     "confirmopeninbrowser": "Do you want to open it in a web browser?", | ||||
|     "considereddigitalminor": "You are too young to create an account on this site.", | ||||
| @ -153,6 +154,7 @@ | ||||
|     "mod_folder": "Folder", | ||||
|     "mod_forum": "Forum", | ||||
|     "mod_glossary": "Glossary", | ||||
|     "mod_h5pactivity": "H5P", | ||||
|     "mod_ims": "IMS content package", | ||||
|     "mod_imscp": "IMS content package", | ||||
|     "mod_label": "Label", | ||||
|  | ||||
| @ -66,6 +66,7 @@ export class CoreTextUtilsProvider { | ||||
|         {old: /_mmaModFolder/g, new: '_AddonModFolder'}, | ||||
|         {old: /_mmaModForum/g, new: '_AddonModForum'}, | ||||
|         {old: /_mmaModGlossary/g, new: '_AddonModGlossary'}, | ||||
|         {old: /_mmaModH5pactivity/g, new: '_AddonModH5PActivity'}, | ||||
|         {old: /_mmaModImscp/g, new: '_AddonModImscp'}, | ||||
|         {old: /_mmaModLabel/g, new: '_AddonModLabel'}, | ||||
|         {old: /_mmaModLesson/g, new: '_AddonModLesson'}, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user