forked from EVOgeek/Vmeda.Online
		
	
						commit
						badb71cb8e
					
				| @ -684,6 +684,7 @@ | |||||||
|   "addon.mod_h5pactivity.no_compatible_track": "h5pactivity", |   "addon.mod_h5pactivity.no_compatible_track": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp", |   "addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp", | ||||||
|   "addon.mod_h5pactivity.outcome": "h5pactivity", |   "addon.mod_h5pactivity.outcome": "h5pactivity", | ||||||
|  |   "addon.mod_h5pactivity.previewmode": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.result_fill-in": "h5pactivity", |   "addon.mod_h5pactivity.result_fill-in": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.result_other": "h5pactivity", |   "addon.mod_h5pactivity.result_other": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.review_my_attempts": "h5pactivity", |   "addon.mod_h5pactivity.review_my_attempts": "h5pactivity", | ||||||
| @ -1405,6 +1406,7 @@ | |||||||
|   "core.confirmdeletefile": "repository", |   "core.confirmdeletefile": "repository", | ||||||
|   "core.confirmgotabroot": "local_moodlemobileapp", |   "core.confirmgotabroot": "local_moodlemobileapp", | ||||||
|   "core.confirmgotabrootdefault": "local_moodlemobileapp", |   "core.confirmgotabrootdefault": "local_moodlemobileapp", | ||||||
|  |   "core.confirmleaveunknownchanges": "local_moodlemobileapp", | ||||||
|   "core.confirmloss": "local_moodlemobileapp", |   "core.confirmloss": "local_moodlemobileapp", | ||||||
|   "core.confirmopeninbrowser": "local_moodlemobileapp", |   "core.confirmopeninbrowser": "local_moodlemobileapp", | ||||||
|   "core.considereddigitalminor": "moodle", |   "core.considereddigitalminor": "moodle", | ||||||
| @ -1858,6 +1860,7 @@ | |||||||
|   "core.mod_folder": "folder/pluginname", |   "core.mod_folder": "folder/pluginname", | ||||||
|   "core.mod_forum": "forum/pluginname", |   "core.mod_forum": "forum/pluginname", | ||||||
|   "core.mod_glossary": "glossary/pluginname", |   "core.mod_glossary": "glossary/pluginname", | ||||||
|  |   "core.mod_h5pactivity": "h5pactivity/pluginname", | ||||||
|   "core.mod_ims": "imscp/pluginname", |   "core.mod_ims": "imscp/pluginname", | ||||||
|   "core.mod_imscp": "imscp/pluginname", |   "core.mod_imscp": "imscp/pluginname", | ||||||
|   "core.mod_label": "label/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="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="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="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="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-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> |     </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> |     <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. --> |     <!-- Offline disabled. --> | ||||||
|     <ion-card class="core-warning-card" icon-start *ngIf="!siteCanDownload && playing"> |     <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-icon name="warning"></ion-icon> {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }} | ||||||
|     </ion-card> |     </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-list *ngIf="deployedFile && !playing"> | ||||||
|         <ion-item text-wrap *ngIf="stateMessage"> |         <ion-item text-wrap *ngIf="stateMessage"> | ||||||
|             <p >{{ stateMessage | translate }}</p> |             <p >{{ stateMessage | translate }}</p> | ||||||
| @ -39,5 +50,5 @@ | |||||||
|         </ion-item> |         </ion-item> | ||||||
|     </ion-list> |     </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> | </core-loading> | ||||||
|  | |||||||
| @ -24,12 +24,15 @@ import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main | |||||||
| import { CoreH5P } from '@core/h5p/providers/h5p'; | import { CoreH5P } from '@core/h5p/providers/h5p'; | ||||||
| import { CoreH5PDisplayOptions } from '@core/h5p/classes/core'; | import { CoreH5PDisplayOptions } from '@core/h5p/classes/core'; | ||||||
| import { CoreH5PHelper } from '@core/h5p/classes/helper'; | 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 { CoreConstants } from '@core/constants'; | ||||||
| import { CoreSite } from '@classes/site'; | import { CoreSite } from '@classes/site'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|     AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAccessInfo |     AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAccessInfo | ||||||
| } from '../../providers/h5pactivity'; | } from '../../providers/h5pactivity'; | ||||||
|  | import { AddonModH5PActivitySyncProvider, AddonModH5PActivitySync } from '../../providers/sync'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Component that displays an H5P activity entry page. |  * 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.
 |     fileUrl: string; // The fileUrl to use to play the package.
 | ||||||
|     state: string; // State of the file.
 |     state: string; // State of the file.
 | ||||||
|     siteCanDownload: boolean; |     siteCanDownload: boolean; | ||||||
|  |     trackComponent: string; // Component for tracking.
 | ||||||
|  |     hasOffline: boolean; | ||||||
|  |     isOpeningPage: boolean; | ||||||
| 
 | 
 | ||||||
|     protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity'; |     protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity'; | ||||||
|  |     protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED; | ||||||
|     protected site: CoreSite; |     protected site: CoreSite; | ||||||
|     protected observer; |     protected observer; | ||||||
|  |     protected messageListenerFunction: (event: MessageEvent) => Promise<void>; | ||||||
| 
 | 
 | ||||||
|     constructor(injector: Injector, |     constructor(injector: Injector, | ||||||
|             @Optional() protected content: Content) { |             @Optional() protected content: Content) { | ||||||
| @ -68,6 +76,10 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | |||||||
| 
 | 
 | ||||||
|         this.site = this.sitesProvider.getCurrentSite(); |         this.site = this.sitesProvider.getCurrentSite(); | ||||||
|         this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite(); |         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> { |     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||||
|         try { |         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.dataRetrieved.emit(this.h5pActivity); | ||||||
|             this.description = this.h5pActivity.intro; |             this.description = this.h5pActivity.intro; | ||||||
|             this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions); |             this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions); | ||||||
| 
 | 
 | ||||||
|             if (this.h5pActivity.package && this.h5pActivity.package[0]) { |             if (sync) { | ||||||
|                 // The online player should use the original file, not the trusted one.
 |                 await this.syncActivity(showErrors); | ||||||
|                 this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl( |  | ||||||
|                             this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             await Promise.all([ |             await Promise.all([ | ||||||
|  |                 this.checkHasOffline(), | ||||||
|                 this.fetchAccessInfo(), |                 this.fetchAccessInfo(), | ||||||
|                 this.fetchDeployedFileData(), |                 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) { |             if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) { | ||||||
|                 // Cannot download the file or already downloaded, play the package directly.
 |                 // Cannot download the file or already downloaded, play the package directly.
 | ||||||
|                 this.play(); |                 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. |      * Fetch the access info and store it in the right variables. | ||||||
|      * |      * | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected async fetchAccessInfo(): Promise<void> { |     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. |      * Go to view user events. | ||||||
|      */ |      */ | ||||||
|     viewMyAttempts(): void { |     async viewMyAttempts(): Promise<void> { | ||||||
|         this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {courseId: this.courseId, h5pActivityId: this.h5pActivity.id}); |         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 { |     ngOnDestroy(): void { | ||||||
|         this.observer && this.observer.off(); |         this.observer && this.observer.off(); | ||||||
|  |         window.removeEventListener('message', this.messageListenerFunction); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ | |||||||
| 
 | 
 | ||||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||||
| 
 | 
 | ||||||
|  | import { CoreCronDelegate } from '@providers/cron'; | ||||||
| import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; | import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; | ||||||
| import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; | import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; | ||||||
| import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-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 { AddonModH5PActivityComponentsModule } from './components/components.module'; | ||||||
| import { AddonModH5PActivityModuleHandler } from './providers/module-handler'; | import { AddonModH5PActivityModuleHandler } from './providers/module-handler'; | ||||||
| import { AddonModH5PActivityProvider } from './providers/h5pactivity'; | import { AddonModH5PActivityProvider } from './providers/h5pactivity'; | ||||||
|  | import { AddonModH5PActivitySyncProvider } from './providers/sync'; | ||||||
| import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler'; | import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler'; | ||||||
| import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler'; | import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler'; | ||||||
| import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler'; | import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler'; | ||||||
|  | import { AddonModH5PActivitySyncCronHandler } from './providers/sync-cron-handler'; | ||||||
| 
 | 
 | ||||||
| // List of providers (without handlers).
 | // List of providers (without handlers).
 | ||||||
| export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [ | export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [ | ||||||
|     AddonModH5PActivityProvider, |     AddonModH5PActivityProvider, | ||||||
|  |     AddonModH5PActivitySyncProvider, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
| @ -38,10 +42,12 @@ export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [ | |||||||
|     ], |     ], | ||||||
|     providers: [ |     providers: [ | ||||||
|         AddonModH5PActivityProvider, |         AddonModH5PActivityProvider, | ||||||
|  |         AddonModH5PActivitySyncProvider, | ||||||
|         AddonModH5PActivityModuleHandler, |         AddonModH5PActivityModuleHandler, | ||||||
|         AddonModH5PActivityPrefetchHandler, |         AddonModH5PActivityPrefetchHandler, | ||||||
|         AddonModH5PActivityIndexLinkHandler, |         AddonModH5PActivityIndexLinkHandler, | ||||||
|         AddonModH5PActivityReportLinkHandler, |         AddonModH5PActivityReportLinkHandler, | ||||||
|  |         AddonModH5PActivitySyncCronHandler, | ||||||
|     ] |     ] | ||||||
| }) | }) | ||||||
| export class AddonModH5PActivityModule { | export class AddonModH5PActivityModule { | ||||||
| @ -51,11 +57,14 @@ export class AddonModH5PActivityModule { | |||||||
|             prefetchHandler: AddonModH5PActivityPrefetchHandler, |             prefetchHandler: AddonModH5PActivityPrefetchHandler, | ||||||
|             linksDelegate: CoreContentLinksDelegate, |             linksDelegate: CoreContentLinksDelegate, | ||||||
|             indexHandler: AddonModH5PActivityIndexLinkHandler, |             indexHandler: AddonModH5PActivityIndexLinkHandler, | ||||||
|             reportLinkHandler: AddonModH5PActivityReportLinkHandler) { |             reportLinkHandler: AddonModH5PActivityReportLinkHandler, | ||||||
|  |             cronDelegate: CoreCronDelegate, | ||||||
|  |             syncHandler: AddonModH5PActivitySyncCronHandler) { | ||||||
| 
 | 
 | ||||||
|         moduleDelegate.registerHandler(moduleHandler); |         moduleDelegate.registerHandler(moduleHandler); | ||||||
|         prefetchDelegate.registerHandler(prefetchHandler); |         prefetchDelegate.registerHandler(prefetchHandler); | ||||||
|         linksDelegate.registerHandler(indexHandler); |         linksDelegate.registerHandler(indexHandler); | ||||||
|         linksDelegate.registerHandler(reportLinkHandler); |         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.", |     "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.", |     "offlinedisabledwarning": "You will need to be online to view the H5P package.", | ||||||
|     "outcome": "Outcome", |     "outcome": "Outcome", | ||||||
|  |     "previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.", | ||||||
|     "result_fill-in": "Fill-in text", |     "result_fill-in": "Fill-in text", | ||||||
|     "result_other": "Unkown interaction type", |     "result_other": "Unkown interaction type", | ||||||
|     "review_my_attempts": "View my attempts", |     "review_my_attempts": "View my attempts", | ||||||
|  | |||||||
| @ -14,9 +14,12 @@ | |||||||
| 
 | 
 | ||||||
| import { Component, ViewChild } from '@angular/core'; | import { Component, ViewChild } from '@angular/core'; | ||||||
| import { IonicPage, NavParams } from 'ionic-angular'; | import { IonicPage, NavParams } from 'ionic-angular'; | ||||||
|  | import { CoreDomUtils } from '@providers/utils/dom'; | ||||||
| import { AddonModH5PActivityIndexComponent } from '../../components/index/index'; | import { AddonModH5PActivityIndexComponent } from '../../components/index/index'; | ||||||
| import { AddonModH5PActivityData } from '../../providers/h5pactivity'; | import { AddonModH5PActivityData } from '../../providers/h5pactivity'; | ||||||
| 
 | 
 | ||||||
|  | import { Translate } from '@singletons/core.singletons'; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Page that displays an H5P activity. |  * Page that displays an H5P activity. | ||||||
|  */ |  */ | ||||||
| @ -46,4 +49,17 @@ export class AddonModH5PActivityIndexPage { | |||||||
|     updateData(h5p: AddonModH5PActivityData): void { |     updateData(h5p: AddonModH5PActivityData): void { | ||||||
|         this.title = h5p.name || this.title; |         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() | @Injectable() | ||||||
| export class AddonModH5PActivityProvider { | export class AddonModH5PActivityProvider { | ||||||
|     static COMPONENT = 'mmaModH5PActivity'; |     static COMPONENT = 'mmaModH5PActivity'; | ||||||
|  |     static TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking.
 | ||||||
| 
 | 
 | ||||||
|     protected ROOT_CACHE_KEY = 'mmaModH5PActivity:'; |     protected ROOT_CACHE_KEY = 'mmaModH5PActivity:'; | ||||||
| 
 | 
 | ||||||
| @ -384,6 +385,20 @@ export class AddonModH5PActivityProvider { | |||||||
|         return this.getH5PActivityByField(courseId, 'coursemodule', cmId, forceCache, siteId); |         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. |      * Get an H5P activity by instance ID. | ||||||
|      * |      * | ||||||
| @ -595,6 +610,7 @@ export type AddonModH5PActivityData = { | |||||||
|     grademethod: number; // Which H5P attempt is used for grading.
 |     grademethod: number; // Which H5P attempt is used for grading.
 | ||||||
|     contenthash?: string; // Sha1 hash of file content.
 |     contenthash?: string; // Sha1 hash of file content.
 | ||||||
|     coursemodule: number; // Coursemodule.
 |     coursemodule: number; // Coursemodule.
 | ||||||
|  |     context: number; // Context ID.
 | ||||||
|     introfiles: CoreWSExternalFile[]; |     introfiles: CoreWSExternalFile[]; | ||||||
|     package: CoreWSExternalFile[]; |     package: CoreWSExternalFile[]; | ||||||
|     deployedfile?: { |     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 { CoreH5PModule } from '@core/h5p/h5p.module'; | ||||||
| import { CoreSearchModule } from '@core/search/search.module'; | import { CoreSearchModule } from '@core/search/search.module'; | ||||||
| import { CoreEditorModule } from '@core/editor/editor.module'; | import { CoreEditorModule } from '@core/editor/editor.module'; | ||||||
|  | import { CoreXAPIModule } from '@core/xapi/xapi.module'; | ||||||
| 
 | 
 | ||||||
| // Addon modules.
 | // Addon modules.
 | ||||||
| import { AddonBadgesModule } from '@addon/badges/badges.module'; | import { AddonBadgesModule } from '@addon/badges/badges.module'; | ||||||
| @ -241,6 +242,7 @@ export const WP_PROVIDER: any = null; | |||||||
|         CoreH5PModule, |         CoreH5PModule, | ||||||
|         CoreSearchModule, |         CoreSearchModule, | ||||||
|         CoreEditorModule, |         CoreEditorModule, | ||||||
|  |         CoreXAPIModule, | ||||||
|         AddonBadgesModule, |         AddonBadgesModule, | ||||||
|         AddonBlogModule, |         AddonBlogModule, | ||||||
|         AddonCalendarModule, |         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.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.offlinedisabledwarning": "You will need to be online to view the H5P package.", | ||||||
|     "addon.mod_h5pactivity.outcome": "Outcome", |     "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_fill-in": "Fill-in text", | ||||||
|     "addon.mod_h5pactivity.result_other": "Unkown interaction type", |     "addon.mod_h5pactivity.result_other": "Unkown interaction type", | ||||||
|     "addon.mod_h5pactivity.review_my_attempts": "View my attempts", |     "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.confirmdeletefile": "Are you sure you want to delete this file?", | ||||||
|     "core.confirmgotabroot": "Are you sure you want to go back to {{name}}?", |     "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.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.confirmloss": "Are you sure? All changes will be lost.", | ||||||
|     "core.confirmopeninbrowser": "Do you want to open it in a web browser?", |     "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.", |     "core.considereddigitalminor": "You are too young to create an account on this site.", | ||||||
| @ -1859,6 +1861,7 @@ | |||||||
|     "core.mod_folder": "Folder", |     "core.mod_folder": "Folder", | ||||||
|     "core.mod_forum": "Forum", |     "core.mod_forum": "Forum", | ||||||
|     "core.mod_glossary": "Glossary", |     "core.mod_glossary": "Glossary", | ||||||
|  |     "core.mod_h5pactivity": "H5P", | ||||||
|     "core.mod_ims": "IMS content package", |     "core.mod_ims": "IMS content package", | ||||||
|     "core.mod_imscp": "IMS content package", |     "core.mod_imscp": "IMS content package", | ||||||
|     "core.mod_label": "Label", |     "core.mod_label": "Label", | ||||||
|  | |||||||
| @ -1820,7 +1820,7 @@ export class CoreSite { | |||||||
| 
 | 
 | ||||||
|             this.lastAutoLogin = this.timeUtils.timestamp(); |             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(() => { |         }).catch(() => { | ||||||
| 
 | 
 | ||||||
|             // Couldn't get autologin key, return the same URL.
 |             // 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 { IONIC_NATIVE_PROVIDERS } from '@core/emulator/emulator.module'; | ||||||
| import { CORE_EDITOR_PROVIDERS } from '@core/editor/editor.module'; | import { CORE_EDITOR_PROVIDERS } from '@core/editor/editor.module'; | ||||||
| import { CORE_SEARCH_PROVIDERS } from '@core/search/search.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 only this provider to prevent circular dependencies.
 | ||||||
| import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins'; | 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(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_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS).concat(CORE_BLOCK_PROVIDERS) | ||||||
|                 .concat(CORE_FILTER_PROVIDERS).concat(CORE_H5P_PROVIDERS).concat(CORE_EDITOR_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.
 |         // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
 | ||||||
|         for (const i in providers) { |         for (const i in providers) { | ||||||
|  | |||||||
| @ -280,8 +280,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | |||||||
|      * |      * | ||||||
|      * @param event Event. |      * @param event Event. | ||||||
|      */ |      */ | ||||||
|     gotoBlog(event: any): void { |     gotoBlog(event: any): Promise<any> { | ||||||
|         this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id }); |         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 ROOT_CACHE_KEY = 'mmCourse:'; | ||||||
|     protected statusCache = new CoreCache(); |     protected statusCache = new CoreCache(); | ||||||
|  |     protected featurePrefix = 'CoreCourseModuleDelegate_'; | ||||||
|     protected handlerNameProperty = 'modName'; |     protected handlerNameProperty = 'modName'; | ||||||
| 
 | 
 | ||||||
|     // Promises for check updates, to prevent performing the same request twice at the same time.
 |     // 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.
 |             // Parent origin can be anything.
 | ||||||
|             window.parent.postMessage(data, '*'); |             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); |     return (window.postMessage && window.addEventListener ? new Communicator() : undefined); | ||||||
| @ -150,6 +165,38 @@ document.onreadystatechange = function() { | |||||||
|         }, 0); |         }, 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.
 |     // Trigger initial resize for instance.
 | ||||||
|     H5P.trigger(instance, 'resize'); |     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(libUrl + script); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |         urls.push(CoreTextUtils.instance.concatenatePaths(libUrl, 'moodle/js/h5p_overrides.js')); | ||||||
|  | 
 | ||||||
|         return urls; |         return urls; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ import { CoreSites } from '@providers/sites'; | |||||||
| import { CoreMimetypeUtils } from '@providers/utils/mimetype'; | import { CoreMimetypeUtils } from '@providers/utils/mimetype'; | ||||||
| import { CoreTextUtils } from '@providers/utils/text'; | import { CoreTextUtils } from '@providers/utils/text'; | ||||||
| import { CoreUtils } from '@providers/utils/utils'; | import { CoreUtils } from '@providers/utils/utils'; | ||||||
|  | import { CoreUser } from '@core/user/providers/user'; | ||||||
| import { CoreH5P } from '../providers/h5p'; | import { CoreH5P } from '../providers/h5p'; | ||||||
| import { CoreH5PCore, CoreH5PDisplayOptions } from './core'; | import { CoreH5PCore, CoreH5PDisplayOptions } from './core'; | ||||||
| import { FileEntry } from '@ionic-native/file'; | import { FileEntry } from '@ionic-native/file'; | ||||||
| @ -90,6 +91,13 @@ export class CoreH5PHelper { | |||||||
|     static async getCoreSettings(siteId?: string): Promise<any> { |     static async getCoreSettings(siteId?: string): Promise<any> { | ||||||
| 
 | 
 | ||||||
|         const site = await CoreSites.instance.getSite(siteId); |         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 basePath = CoreFile.instance.getBasePathInstant(); | ||||||
|         const ajaxPaths = { |         const ajaxPaths = { | ||||||
| @ -110,7 +118,7 @@ export class CoreH5PHelper { | |||||||
|             l10n: { |             l10n: { | ||||||
|                 H5P: CoreH5P.instance.h5pCore.getLocalization(), |                 H5P: CoreH5P.instance.h5pCore.getLocalization(), | ||||||
|             }, |             }, | ||||||
|             user: [], |             user: {name: site.getInfo().fullname, mail: user && user.email}, | ||||||
|             hubIsEnabled: false, |             hubIsEnabled: false, | ||||||
|             reportingIsEnabled: false, |             reportingIsEnabled: false, | ||||||
|             crossorigin: null, |             crossorigin: null, | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ import { CoreSites } from '@providers/sites'; | |||||||
| import { CoreTextUtils } from '@providers/utils/text'; | import { CoreTextUtils } from '@providers/utils/text'; | ||||||
| import { CoreUrlUtils } from '@providers/utils/url'; | import { CoreUrlUtils } from '@providers/utils/url'; | ||||||
| import { CoreUtils } from '@providers/utils/utils'; | import { CoreUtils } from '@providers/utils/utils'; | ||||||
|  | import { CoreXAPI } from '@core/xapi/providers/xapi'; | ||||||
| import { CoreH5P } from '../providers/h5p'; | import { CoreH5P } from '../providers/h5p'; | ||||||
| import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core'; | import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core'; | ||||||
| import { CoreH5PHelper } from './helper'; | import { CoreH5PHelper } from './helper'; | ||||||
| @ -81,7 +82,7 @@ export class CoreH5PPlayer { | |||||||
|             resizeCode: this.getResizeCode(), |             resizeCode: this.getResizeCode(), | ||||||
|             title: content.slug, |             title: content.slug, | ||||||
|             displayOptions: {}, |             displayOptions: {}, | ||||||
|             url: this.getEmbedUrl(site.getURL(), h5pUrl), |             url: '', // It will be filled using dynamic params if needed.
 | ||||||
|             contentUrl: contentUrl, |             contentUrl: contentUrl, | ||||||
|             metadata: content.metadata, |             metadata: content.metadata, | ||||||
|             contentUserData: [ |             contentUserData: [ | ||||||
| @ -109,9 +110,9 @@ export class CoreH5PPlayer { | |||||||
|         html += '<script type="text/javascript">var H5PIntegration = ' + |         html += '<script type="text/javascript">var H5PIntegration = ' + | ||||||
|                 JSON.stringify(result.settings).replace(/\//g, '\\/') + '</script>'; |                 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( |         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>'; |         html += '</head><body>'; | ||||||
| 
 | 
 | ||||||
| @ -241,20 +242,34 @@ export class CoreH5PPlayer { | |||||||
|      * |      * | ||||||
|      * @param fileUrl URL of the H5P package. |      * @param fileUrl URL of the H5P package. | ||||||
|      * @param displayOptions Display options. |      * @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. |      * @param siteId The site ID. If not defined, current site. | ||||||
|      * @return Promise resolved with the file URL if exists, rejected otherwise. |      * @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(); |         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         const path = await this.h5pCore.h5pFS.getContentIndexFileUrl(fileUrl, siteId); |         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); |         const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId); | ||||||
| 
 | 
 | ||||||
|         displayOptions = this.h5pCore.fixDisplayOptions(displayOptions, data.id); |         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() fileUrl?: string; // The URL of the H5P file. If not supplied, onlinePlayerUrl is required.
 | ||||||
|     @Input() displayOptions?: CoreH5PDisplayOptions; // Display options.
 |     @Input() displayOptions?: CoreH5PDisplayOptions; // Display options.
 | ||||||
|     @Input() onlinePlayerUrl?: string; // The URL of the online player to display the H5P package.
 |     @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() onIframeUrlSet = new EventEmitter<{src: string, online: boolean}>(); | ||||||
|     @Output() onIframeLoaded = new EventEmitter<void>(); |     @Output() onIframeLoaded = new EventEmitter<void>(); | ||||||
| 
 | 
 | ||||||
| @ -93,7 +95,7 @@ export class CoreH5PIframeComponent implements OnChanges { | |||||||
|                 this.iframeSrc = localUrl; |                 this.iframeSrc = localUrl; | ||||||
|             } else { |             } else { | ||||||
|                 this.onlinePlayerUrl = this.onlinePlayerUrl || CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl( |                 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.
 |                 // 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', |                 const src = this.onlinePlayerUrl.replace(CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1', | ||||||
| @ -121,7 +123,8 @@ export class CoreH5PIframeComponent implements OnChanges { | |||||||
|      */ |      */ | ||||||
|     protected async getLocalUrl(): Promise<string> { |     protected async getLocalUrl(): Promise<string> { | ||||||
|         try { |         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; |             return url; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
| @ -135,7 +138,7 @@ export class CoreH5PIframeComponent implements OnChanges { | |||||||
| 
 | 
 | ||||||
|                 // File treated. Try to get the index file URL again.
 |                 // File treated. Try to get the index file URL again.
 | ||||||
|                 const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions, |                 const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions, | ||||||
|                         this.siteId); |                         this.trackComponent, this.contextId, this.siteId); | ||||||
| 
 | 
 | ||||||
|                 return url; |                 return url; | ||||||
|             } catch (error) { |             } catch (error) { | ||||||
|  | |||||||
| @ -391,7 +391,7 @@ export class CoreH5PProvider { | |||||||
|      * @param siteId Site ID (empty for current site). |      * @param siteId Site ID (empty for current site). | ||||||
|      * @return Promise resolved when the data is invalidated. |      * @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); |         const site = await CoreSites.instance.getSite(siteId); | ||||||
| 
 | 
 | ||||||
|         await site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url)); |         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?", |     "confirmdeletefile": "Are you sure you want to delete this file?", | ||||||
|     "confirmgotabroot": "Are you sure you want to go back to {{name}}?", |     "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?", |     "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.", |     "confirmloss": "Are you sure? All changes will be lost.", | ||||||
|     "confirmopeninbrowser": "Do you want to open it in a web browser?", |     "confirmopeninbrowser": "Do you want to open it in a web browser?", | ||||||
|     "considereddigitalminor": "You are too young to create an account on this site.", |     "considereddigitalminor": "You are too young to create an account on this site.", | ||||||
| @ -153,6 +154,7 @@ | |||||||
|     "mod_folder": "Folder", |     "mod_folder": "Folder", | ||||||
|     "mod_forum": "Forum", |     "mod_forum": "Forum", | ||||||
|     "mod_glossary": "Glossary", |     "mod_glossary": "Glossary", | ||||||
|  |     "mod_h5pactivity": "H5P", | ||||||
|     "mod_ims": "IMS content package", |     "mod_ims": "IMS content package", | ||||||
|     "mod_imscp": "IMS content package", |     "mod_imscp": "IMS content package", | ||||||
|     "mod_label": "Label", |     "mod_label": "Label", | ||||||
|  | |||||||
| @ -66,6 +66,7 @@ export class CoreTextUtilsProvider { | |||||||
|         {old: /_mmaModFolder/g, new: '_AddonModFolder'}, |         {old: /_mmaModFolder/g, new: '_AddonModFolder'}, | ||||||
|         {old: /_mmaModForum/g, new: '_AddonModForum'}, |         {old: /_mmaModForum/g, new: '_AddonModForum'}, | ||||||
|         {old: /_mmaModGlossary/g, new: '_AddonModGlossary'}, |         {old: /_mmaModGlossary/g, new: '_AddonModGlossary'}, | ||||||
|  |         {old: /_mmaModH5pactivity/g, new: '_AddonModH5PActivity'}, | ||||||
|         {old: /_mmaModImscp/g, new: '_AddonModImscp'}, |         {old: /_mmaModImscp/g, new: '_AddonModImscp'}, | ||||||
|         {old: /_mmaModLabel/g, new: '_AddonModLabel'}, |         {old: /_mmaModLabel/g, new: '_AddonModLabel'}, | ||||||
|         {old: /_mmaModLesson/g, new: '_AddonModLesson'}, |         {old: /_mmaModLesson/g, new: '_AddonModLesson'}, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user