MOBILE-3645 h5pactivity: Implement services and index page
parent
5edc8de380
commit
612bc6bffa
|
@ -0,0 +1,37 @@
|
||||||
|
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||||
|
import { AddonModH5PActivityIndexComponent } from './index';
|
||||||
|
import { CoreH5PComponentsModule } from '@features/h5p/components/components.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModH5PActivityIndexComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreCourseComponentsModule,
|
||||||
|
CoreH5PComponentsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModH5PActivityIndexComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModH5PActivityComponentsModule {}
|
|
@ -0,0 +1,91 @@
|
||||||
|
<!-- Buttons to add to the header. -->
|
||||||
|
<core-navbar-buttons slot="end">
|
||||||
|
<core-context-menu>
|
||||||
|
<core-context-menu-item *ngIf="h5pActivity && h5pActivity.enabletracking && accessInfo && !accessInfo.canreviewattempts"
|
||||||
|
[priority]="1000" [content]="'addon.mod_h5pactivity.review_my_attempts' | translate" (action)="viewMyAttempts()"
|
||||||
|
iconAction="stats-chart">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
|
||||||
|
[href]="externalUrl" iconAction="fas-external-link-alt">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
|
||||||
|
(action)="expandDescription()" iconAction="fas-arrow-right">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
|
||||||
|
iconAction="far-newspaper" (action)="gotoBlog()">
|
||||||
|
</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" (action)="doRefresh(null, $event, true)"
|
||||||
|
[content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
|
||||||
|
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}"
|
||||||
|
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
|
||||||
|
</core-context-menu-item>
|
||||||
|
</core-context-menu>
|
||||||
|
</core-navbar-buttons>
|
||||||
|
|
||||||
|
<!-- Content. -->
|
||||||
|
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
|
||||||
|
|
||||||
|
<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" *ngIf="hasOffline">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||||
|
<ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Offline disabled. -->
|
||||||
|
<ion-card class="core-warning-card" *ngIf="!siteCanDownload && playing">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
{{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Preview mode. -->
|
||||||
|
<ion-card class="core-warning-card" *ngIf="accessInfo && !trackComponent">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
{{ 'addon.mod_h5pactivity.previewmode' | translate }}
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ion-list *ngIf="deployedFile && !playing">
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="stateMessage">
|
||||||
|
<ion-label>{{ stateMessage | translate }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Button to download the package. -->
|
||||||
|
<ion-button *ngIf="!downloading && needsDownload" class="ion-text-wrap ion-margin" expand="block"
|
||||||
|
(click)="downloadAndPlay($event)">
|
||||||
|
{{ 'addon.mod_h5pactivity.downloadh5pfile' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<!-- Download progress. -->
|
||||||
|
<ion-item class="ion-text-center" *ngIf="downloading">
|
||||||
|
<ion-label>
|
||||||
|
<ion-spinner></ion-spinner>
|
||||||
|
<h2 *ngIf="progressMessage">{{ progressMessage | translate }}</h2>
|
||||||
|
<core-progress-bar *ngIf="showPercentage" [progress]="percentage"></core-progress-bar>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"
|
||||||
|
[trackComponent]="trackComponent" [contextId]="h5pActivity?.context">
|
||||||
|
</core-h5p-iframe>
|
||||||
|
</core-loading>
|
|
@ -0,0 +1,495 @@
|
||||||
|
// (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 { Component, Optional, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
import { IonContent } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { CoreConstants } from '@/core/constants';
|
||||||
|
import { CoreSite } from '@classes/site';
|
||||||
|
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||||
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
import { CoreH5PDisplayOptions } from '@features/h5p/classes/core';
|
||||||
|
import { CoreH5PHelper } from '@features/h5p/classes/helper';
|
||||||
|
import { CoreH5P } from '@features/h5p/services/h5p';
|
||||||
|
import { CoreXAPIOffline } from '@features/xapi/services/offline';
|
||||||
|
import { CoreXAPI } from '@features/xapi/services/xapi';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreWSExternalFile } from '@services/ws';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import {
|
||||||
|
AddonModH5PActivity,
|
||||||
|
AddonModH5PActivityAccessInfo,
|
||||||
|
AddonModH5PActivityData,
|
||||||
|
AddonModH5PActivityProvider,
|
||||||
|
} from '../../services/h5pactivity';
|
||||||
|
import {
|
||||||
|
AddonModH5PActivitySync,
|
||||||
|
AddonModH5PActivitySyncProvider,
|
||||||
|
AddonModH5PActivitySyncResult,
|
||||||
|
} from '../../services/h5pactivity-sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays an H5P activity entry page.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-h5pactivity-index',
|
||||||
|
templateUrl: 'addon-mod-h5pactivity-index.html',
|
||||||
|
})
|
||||||
|
export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
component = AddonModH5PActivityProvider.COMPONENT;
|
||||||
|
moduleName = 'h5pactivity';
|
||||||
|
|
||||||
|
h5pActivity?: AddonModH5PActivityData; // The H5P activity object.
|
||||||
|
accessInfo?: AddonModH5PActivityAccessInfo; // Info about the user capabilities.
|
||||||
|
deployedFile?: CoreWSExternalFile; // The H5P deployed file.
|
||||||
|
|
||||||
|
stateMessage?: string; // Message about the file state.
|
||||||
|
downloading = false; // Whether the H5P file is being downloaded.
|
||||||
|
needsDownload = false; // Whether the file needs to be downloaded.
|
||||||
|
percentage?: string; // Download/unzip percentage.
|
||||||
|
showPercentage = false; // Whether to show the percentage.
|
||||||
|
progressMessage?: string; // Message about download/unzip.
|
||||||
|
playing = false; // Whether the package is being played.
|
||||||
|
displayOptions?: CoreH5PDisplayOptions; // Display options for the package.
|
||||||
|
onlinePlayerUrl?: string; // URL to play the package in online.
|
||||||
|
fileUrl?: string; // The fileUrl to use to play the package.
|
||||||
|
state?: string; // State of the file.
|
||||||
|
siteCanDownload = false;
|
||||||
|
trackComponent?: string; // Component for tracking.
|
||||||
|
hasOffline = false;
|
||||||
|
isOpeningPage = false;
|
||||||
|
|
||||||
|
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
|
||||||
|
protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED;
|
||||||
|
protected site: CoreSite;
|
||||||
|
protected observer?: CoreEventObserver;
|
||||||
|
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected content?: IonContent,
|
||||||
|
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||||
|
) {
|
||||||
|
super('AddonModH5PActivityIndexComponent', content, courseContentsPage);
|
||||||
|
|
||||||
|
this.site = CoreSites.getCurrentSite()!;
|
||||||
|
this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.isOfflineDisabledInSite();
|
||||||
|
|
||||||
|
// Listen for messages from the iframe.
|
||||||
|
this.messageListenerFunction = this.onIframeMessage.bind(this);
|
||||||
|
window.addEventListener('message', this.messageListenerFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
this.loadContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.module.id, {
|
||||||
|
siteId: this.siteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dataRetrieved.emit(this.h5pActivity);
|
||||||
|
this.description = this.h5pActivity.intro;
|
||||||
|
this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions);
|
||||||
|
|
||||||
|
if (sync) {
|
||||||
|
await this.syncActivity(showErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.checkHasOffline(),
|
||||||
|
this.fetchAccessInfo(),
|
||||||
|
this.fetchDeployedFileData(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.trackComponent = this.accessInfo?.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : '';
|
||||||
|
|
||||||
|
if (this.h5pActivity.package && this.h5pActivity.package[0]) {
|
||||||
|
// The online player should use the original file, not the trusted one.
|
||||||
|
this.onlinePlayerUrl = CoreH5P.h5pPlayer.calculateOnlinePlayerUrl(
|
||||||
|
this.site.getURL(),
|
||||||
|
this.h5pActivity.package[0].fileurl,
|
||||||
|
this.displayOptions,
|
||||||
|
this.trackComponent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) {
|
||||||
|
// Cannot download the file or already downloaded, play the package directly.
|
||||||
|
this.play();
|
||||||
|
|
||||||
|
} else if ((this.state == CoreConstants.NOT_DOWNLOADED || this.state == CoreConstants.OUTDATED) && CoreApp.isOnline() &&
|
||||||
|
this.deployedFile?.filesize && CoreFilepool.shouldDownload(this.deployedFile.filesize)) {
|
||||||
|
// Package is small, download it automatically. Don't block this function for this.
|
||||||
|
this.downloadAutomatically();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.fillContextMenu(refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.contextHasStatements(this.h5pActivity!.context, this.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the access info and store it in the right variables.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchAccessInfo(): Promise<void> {
|
||||||
|
this.accessInfo = await AddonModH5PActivity.getAccessInformation(this.h5pActivity!.id, {
|
||||||
|
cmId: this.module.id,
|
||||||
|
siteId: this.siteId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the deployed file data if needed and store it in the right variables.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchDeployedFileData(): Promise<void> {
|
||||||
|
if (!this.siteCanDownload) {
|
||||||
|
// Cannot download the file, no need to fetch the file data.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deployedFile = await AddonModH5PActivity.getDeployedFile(this.h5pActivity!, {
|
||||||
|
displayOptions: this.displayOptions,
|
||||||
|
siteId: this.siteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fileUrl = this.deployedFile.fileurl;
|
||||||
|
|
||||||
|
// Listen for changes in the state.
|
||||||
|
const eventName = await CoreFilepool.getFileEventNameByUrl(this.site.getId(), this.deployedFile.fileurl);
|
||||||
|
|
||||||
|
if (!this.observer) {
|
||||||
|
this.observer = CoreEvents.on(eventName, () => {
|
||||||
|
this.calculateFileState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.calculateFileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the state of the deployed file.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async calculateFileState(): Promise<void> {
|
||||||
|
this.state = await CoreFilepool.getFileStateByUrl(
|
||||||
|
this.site.getId(),
|
||||||
|
this.deployedFile!.fileurl,
|
||||||
|
this.deployedFile!.timemodified,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.showFileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected invalidateContent(): Promise<void> {
|
||||||
|
return AddonModH5PActivity.invalidateActivityData(this.courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays some data based on the state of the main file.
|
||||||
|
*/
|
||||||
|
protected async showFileState(): Promise<void> {
|
||||||
|
if (this.state == CoreConstants.OUTDATED) {
|
||||||
|
this.stateMessage = 'addon.mod_h5pactivity.filestateoutdated';
|
||||||
|
this.needsDownload = true;
|
||||||
|
} else if (this.state == CoreConstants.NOT_DOWNLOADED) {
|
||||||
|
this.stateMessage = 'addon.mod_h5pactivity.filestatenotdownloaded';
|
||||||
|
this.needsDownload = true;
|
||||||
|
} else if (this.state == CoreConstants.DOWNLOADING) {
|
||||||
|
this.stateMessage = '';
|
||||||
|
|
||||||
|
if (!this.downloading) {
|
||||||
|
// It's being downloaded right now but the view isn't tracking it. "Restore" the download.
|
||||||
|
await this.downloadDeployedFile();
|
||||||
|
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.stateMessage = '';
|
||||||
|
this.needsDownload = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the file and play it.
|
||||||
|
*
|
||||||
|
* @param event Click event.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async downloadAndPlay(event?: MouseEvent): Promise<void> {
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
|
|
||||||
|
if (!CoreApp.isOnline()) {
|
||||||
|
CoreDomUtils.showErrorModal('core.networkerrormsg', true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Confirm the download if needed.
|
||||||
|
await CoreDomUtils.confirmDownloadSize({ size: this.deployedFile!.filesize!, total: true });
|
||||||
|
|
||||||
|
await this.downloadDeployedFile();
|
||||||
|
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (CoreDomUtils.isCanceledError(error) || this.isDestroyed) {
|
||||||
|
// User cancelled or view destroyed, stop.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the file automatically.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async downloadAutomatically(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.downloadDeployedFile();
|
||||||
|
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download athe H5P deployed file or restores an ongoing download.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async downloadDeployedFile(): Promise<void> {
|
||||||
|
this.downloading = true;
|
||||||
|
this.progressMessage = 'core.downloading';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreFilepool.downloadUrl(
|
||||||
|
this.site.getId(),
|
||||||
|
this.deployedFile!.fileurl,
|
||||||
|
false,
|
||||||
|
this.component,
|
||||||
|
this.componentId,
|
||||||
|
this.deployedFile!.timemodified,
|
||||||
|
(data: DownloadProgressData) => {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.percentage = undefined;
|
||||||
|
this.showPercentage = false;
|
||||||
|
|
||||||
|
if (data.message) {
|
||||||
|
// Show a message.
|
||||||
|
this.progressMessage = data.message;
|
||||||
|
} else if (data.loaded !== undefined) {
|
||||||
|
// Downloading or unzipping.
|
||||||
|
const totalSize = this.progressMessage == 'core.downloading' ? this.deployedFile!.filesize : data.total;
|
||||||
|
|
||||||
|
if (totalSize !== undefined) {
|
||||||
|
const percentageNumber = (Number(data.loaded / totalSize) * 100);
|
||||||
|
this.percentage = percentageNumber.toFixed(1);
|
||||||
|
this.showPercentage = percentageNumber >= 0 && percentageNumber <= 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
this.progressMessage = undefined;
|
||||||
|
this.percentage = undefined;
|
||||||
|
this.showPercentage = false;
|
||||||
|
this.downloading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play the package.
|
||||||
|
*/
|
||||||
|
play(): void {
|
||||||
|
this.playing = true;
|
||||||
|
|
||||||
|
// Mark the activity as viewed.
|
||||||
|
AddonModH5PActivity.logView(this.h5pActivity!.id, this.h5pActivity!.name, this.siteId);
|
||||||
|
|
||||||
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to view user events.
|
||||||
|
*/
|
||||||
|
async viewMyAttempts(): Promise<void> {
|
||||||
|
this.isOpeningPage = true;
|
||||||
|
const userId = CoreSites.getCurrentSiteUserId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreNavigator.navigate(`userattempts/${userId}`);
|
||||||
|
} 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.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.postStatements(
|
||||||
|
this.h5pActivity!.context,
|
||||||
|
event.data.component,
|
||||||
|
JSON.stringify(event.data.statements),
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.hasOffline = !sent;
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
try {
|
||||||
|
// Invalidate attempts.
|
||||||
|
await AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity!.id, undefined, this.siteId);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.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.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
protected isCurrentXAPIPost(data: any): boolean {
|
||||||
|
if (data.environment != 'moodleapp' || data.context != 'h5p' || 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected sync(): Promise<AddonModH5PActivitySyncResult> {
|
||||||
|
return AddonModH5PActivitySync.syncActivity(this.h5pActivity!.context, this.site.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected autoSyncEventReceived(): void {
|
||||||
|
this.checkHasOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async gotoBlog(): Promise<void> {
|
||||||
|
this.isOpeningPage = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await super.gotoBlog();
|
||||||
|
} finally {
|
||||||
|
this.isOpeningPage = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
|
||||||
|
this.observer?.off();
|
||||||
|
window.removeEventListener('message', this.messageListenerFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadProgressData = {
|
||||||
|
message?: string;
|
||||||
|
loaded?: number;
|
||||||
|
total?: number;
|
||||||
|
};
|
|
@ -0,0 +1,41 @@
|
||||||
|
// (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 { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CanLeaveGuard } from '@guards/can-leave';
|
||||||
|
import { AddonModH5PActivityComponentsModule } from './components/components.module';
|
||||||
|
import { AddonModH5PActivityIndexPage } from './pages/index/index';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: ':courseId/:cmId',
|
||||||
|
component: AddonModH5PActivityIndexPage,
|
||||||
|
canDeactivate: [CanLeaveGuard],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CoreSharedModule,
|
||||||
|
AddonModH5PActivityComponentsModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AddonModH5PActivityIndexPage,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModH5PActivityLazyModule {}
|
|
@ -0,0 +1,64 @@
|
||||||
|
// (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core';
|
||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
|
||||||
|
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||||
|
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||||
|
import { CoreCronDelegate } from '@services/cron';
|
||||||
|
import { AddonModH5PActivityComponentsModule } from './components/components.module';
|
||||||
|
import { AddonModH5PActivityProvider } from './services/h5pactivity';
|
||||||
|
import { AddonModH5PActivitySyncProvider } from './services/h5pactivity-sync';
|
||||||
|
import { AddonModH5PActivityIndexLinkHandler } from './services/handlers/index-link';
|
||||||
|
import { AddonModH5PActivityModuleHandler, AddonModH5PActivityModuleHandlerService } from './services/handlers/module';
|
||||||
|
import { AddonModH5PActivityPrefetchHandler } from './services/handlers/prefetch';
|
||||||
|
import { AddonModH5PActivityReportLinkHandler } from './services/handlers/report-link';
|
||||||
|
import { AddonModH5PActivitySyncCronHandler } from './services/handlers/sync-cron';
|
||||||
|
|
||||||
|
// List of providers (without handlers).
|
||||||
|
export const ADDON_MOD_H5P_ACTIVITY_SERVICES: Type<unknown>[] = [
|
||||||
|
AddonModH5PActivityProvider,
|
||||||
|
AddonModH5PActivitySyncProvider,
|
||||||
|
];
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: AddonModH5PActivityModuleHandlerService.PAGE_NAME,
|
||||||
|
loadChildren: () => import('./h5pactivity-lazy.module').then(m => m.AddonModH5PActivityLazyModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||||
|
AddonModH5PActivityComponentsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [],
|
||||||
|
useFactory: () => () => {
|
||||||
|
CoreCourseModuleDelegate.registerHandler(AddonModH5PActivityModuleHandler.instance);
|
||||||
|
CoreContentLinksDelegate.registerHandler(AddonModH5PActivityIndexLinkHandler.instance);
|
||||||
|
CoreContentLinksDelegate.registerHandler(AddonModH5PActivityReportLinkHandler.instance);
|
||||||
|
CoreCourseModulePrefetchDelegate.registerHandler(AddonModH5PActivityPrefetchHandler.instance);
|
||||||
|
CoreCronDelegate.register(AddonModH5PActivitySyncCronHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModH5PActivityModule {}
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"all_attempts": "All user attempts",
|
||||||
|
"answer_checked": "Answer checked",
|
||||||
|
"answer_correct": "Your answer is correct",
|
||||||
|
"answer_fail": "Incorrect answer",
|
||||||
|
"answer_incorrect": "Your answer is incorrect",
|
||||||
|
"answer_pass": "Correct answer",
|
||||||
|
"attempt": "Attempt",
|
||||||
|
"attempt_completion_no": "This attempt is not marked as completed",
|
||||||
|
"attempt_completion_yes": "This attempt is completed",
|
||||||
|
"attempt_success_fail": "Fail",
|
||||||
|
"attempt_success_pass": "Pass",
|
||||||
|
"attempt_success_unknown": "Not reported",
|
||||||
|
"attempts_none": "This user has no attempts to display.",
|
||||||
|
"completion": "Completion",
|
||||||
|
"downloadh5pfile": "Download H5P file",
|
||||||
|
"duration": "Duration",
|
||||||
|
"errorgetactivity": "Error getting H5P activity data.",
|
||||||
|
"filestatenotdownloaded": "The H5P package is not downloaded. You need to download it to be able to use it.",
|
||||||
|
"filestateoutdated": "The H5P package has been modified since the last download. You need to download it again to be able to use it.",
|
||||||
|
"maxscore": "Max score",
|
||||||
|
"modulenameplural": "H5P",
|
||||||
|
"myattempts": "My attempts",
|
||||||
|
"no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking\n provided is not compatible with the current activity version.",
|
||||||
|
"offlinedisabledwarning": "You need to be online to view the H5P package.",
|
||||||
|
"outcome": "Outcome",
|
||||||
|
"previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.",
|
||||||
|
"result_fill-in": "Fill-in text",
|
||||||
|
"result_other": "Unknown interaction type",
|
||||||
|
"review_my_attempts": "View my attempts",
|
||||||
|
"score": "Score",
|
||||||
|
"score_out_of": "{{$a.rawscore}} out of {{$a.maxscore}}",
|
||||||
|
"startdate": "Start date",
|
||||||
|
"totalscore": "Total score",
|
||||||
|
"viewattempt": "View attempt {{$a}}"
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>
|
||||||
|
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-title>
|
||||||
|
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<!-- The buttons defined by the component will be added in here. -->
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<addon-mod-h5pactivity-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)">
|
||||||
|
</addon-mod-h5pactivity-index>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,52 @@
|
||||||
|
// (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 { Component, ViewChild } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
|
||||||
|
import { CanLeave } from '@guards/can-leave';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { AddonModH5PActivityIndexComponent } from '../../components/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays an H5P activity.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-h5pactivity-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class AddonModH5PActivityIndexPage extends CoreCourseModuleMainActivityPage<AddonModH5PActivityIndexComponent>
|
||||||
|
implements CanLeave {
|
||||||
|
|
||||||
|
@ViewChild(AddonModH5PActivityIndexComponent) activityComponent?: AddonModH5PActivityIndexComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async canLeave(): Promise<boolean> {
|
||||||
|
if (!this.activityComponent || !this.activityComponent.playing || this.activityComponent.isOpeningPage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreDomUtils.showConfirm(Translate.instant('core.confirmleaveunknownchanges'));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
// (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 { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||||
|
import { CoreXAPIOffline } from '@features/xapi/services/offline';
|
||||||
|
import { CoreXAPI } from '@features/xapi/services/xapi';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
|
import { AddonModH5PActivity, AddonModH5PActivityProvider } from './h5pactivity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to sync H5P activities.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModH5PActivitySyncResult> {
|
||||||
|
|
||||||
|
static readonly AUTO_SYNCED = 'addon_mod_h5pactivity_autom_synced';
|
||||||
|
|
||||||
|
protected componentTranslate?: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('AddonModH5PActivitySyncProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get component name translated.
|
||||||
|
*
|
||||||
|
* @return Component name translated.
|
||||||
|
*/
|
||||||
|
protected getComponentTranslate(): string {
|
||||||
|
if (!this.componentTranslate) {
|
||||||
|
this.componentTranslate = CoreCourse.translateModuleName('h5pactivity');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.componentTranslate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 force Wether to force sync not depending on last execution.
|
||||||
|
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
protected async syncAllActivitiesFunc(force: boolean, siteId?: string): Promise<void> {
|
||||||
|
const entries = await CoreXAPIOffline.getAllStatements(siteId);
|
||||||
|
|
||||||
|
// Sync all responses.
|
||||||
|
const promises = entries.map(async (response) => {
|
||||||
|
const result = await (force ? this.syncActivity(response.contextid, siteId) :
|
||||||
|
this.syncActivityIfNeeded(response.contextid, siteId));
|
||||||
|
|
||||||
|
if (result?.updated) {
|
||||||
|
// Sync successful, send event.
|
||||||
|
CoreEvents.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<AddonModH5PActivitySyncResult | undefined> {
|
||||||
|
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<AddonModH5PActivitySyncResult> {
|
||||||
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (!CoreApp.isOnline()) {
|
||||||
|
// Cannot sync in offline.
|
||||||
|
throw new CoreNetworkError();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<AddonModH5PActivitySyncResult> {
|
||||||
|
|
||||||
|
this.logger.debug(`Try to sync H5P activity with context ID '${contextId}'`);
|
||||||
|
|
||||||
|
const result: AddonModH5PActivitySyncResult = {
|
||||||
|
warnings: [],
|
||||||
|
updated: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all the statements stored for the activity.
|
||||||
|
const entries = await CoreXAPIOffline.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.getH5PActivityByContextId(courseId, contextId, { siteId });
|
||||||
|
|
||||||
|
// Sync offline logs.
|
||||||
|
await CoreUtils.ignoreErrors(
|
||||||
|
CoreCourseLogHelper.syncActivity(AddonModH5PActivityProvider.COMPONENT, h5pActivity.id, siteId),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send the statements in order.
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
const entry = entries[i];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreXAPI.postStatementsOnline(entry.component, entry.statements, siteId);
|
||||||
|
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
await CoreXAPIOffline.deleteStatements(entry.id, siteId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!CoreUtils.isWebServiceError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The WebService has thrown an error, this means that statements cannot be submitted. Delete them.
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
await CoreXAPIOffline.deleteStatements(entry.id, siteId);
|
||||||
|
|
||||||
|
// Responses deleted, add a warning.
|
||||||
|
result.warnings.push(Translate.instant('core.warningofflinedatadeleted', {
|
||||||
|
component: this.componentTranslate,
|
||||||
|
name: entry.extra,
|
||||||
|
error: CoreTextUtils.getErrorMessageFromError(error),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.updated) {
|
||||||
|
// Data has been sent to server, invalidate attempts.
|
||||||
|
await CoreUtils.ignoreErrors(AddonModH5PActivity.invalidateUserAttempts(h5pActivity.id, undefined, siteId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync finished, set sync time.
|
||||||
|
await this.setSyncTime(contextId, siteId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModH5PActivitySync = makeSingleton(AddonModH5PActivitySyncProvider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync result.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivitySyncResult = {
|
||||||
|
updated: boolean;
|
||||||
|
warnings: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data passed to AUTO_SYNC event.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityAutoSyncData = {
|
||||||
|
contextId: number;
|
||||||
|
warnings: string[];
|
||||||
|
};
|
|
@ -0,0 +1,858 @@
|
||||||
|
// (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, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
|
||||||
|
import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||||
|
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||||
|
import { CoreH5P } from '@features/h5p/services/h5p';
|
||||||
|
import { CoreH5PDisplayOptions } from '@features/h5p/classes/core';
|
||||||
|
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
||||||
|
import { makeSingleton, Translate } from '@singletons/index';
|
||||||
|
import { CoreWSError } from '@classes/errors/wserror';
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { AddonModH5PActivityAutoSyncData, AddonModH5PActivitySyncProvider } from './h5pactivity-sync';
|
||||||
|
|
||||||
|
const ROOT_CACHE_KEY = 'mmaModH5PActivity:';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that provides some features for H5P activity.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModH5PActivityProvider {
|
||||||
|
|
||||||
|
static readonly COMPONENT = 'mmaModH5PActivity';
|
||||||
|
static readonly TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an attempt's data.
|
||||||
|
*
|
||||||
|
* @param attempt Attempt to format.
|
||||||
|
* @return Formatted attempt.
|
||||||
|
*/
|
||||||
|
protected formatAttempt(attempt: AddonModH5PActivityWSAttempt): AddonModH5PActivityAttempt {
|
||||||
|
const formattedAttempt: AddonModH5PActivityAttempt = attempt;
|
||||||
|
|
||||||
|
formattedAttempt.timecreated = attempt.timecreated * 1000; // Convert to milliseconds.
|
||||||
|
formattedAttempt.timemodified = attempt.timemodified * 1000; // Convert to milliseconds.
|
||||||
|
formattedAttempt.success = formattedAttempt.success ?? null;
|
||||||
|
|
||||||
|
if (!attempt.duration) {
|
||||||
|
formattedAttempt.durationReadable = '-';
|
||||||
|
formattedAttempt.durationCompact = '-';
|
||||||
|
} else {
|
||||||
|
formattedAttempt.durationReadable = CoreTimeUtils.formatTime(attempt.duration);
|
||||||
|
formattedAttempt.durationCompact = CoreTimeUtils.formatDurationShort(attempt.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedAttempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format attempt data and results.
|
||||||
|
*
|
||||||
|
* @param attempt Attempt and results to format.
|
||||||
|
*/
|
||||||
|
protected formatAttemptResults(attempt: AddonModH5PActivityWSAttemptResults): AddonModH5PActivityAttemptResults {
|
||||||
|
const formattedAttempt: AddonModH5PActivityAttemptResults = this.formatAttempt(attempt);
|
||||||
|
|
||||||
|
formattedAttempt.results = formattedAttempt.results?.map((result) => this.formatResult(result));
|
||||||
|
|
||||||
|
return formattedAttempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the attempts of a user.
|
||||||
|
*
|
||||||
|
* @param data Data to format.
|
||||||
|
* @return Formatted data.
|
||||||
|
*/
|
||||||
|
protected formatUserAttempts(data: AddonModH5PActivityWSUserAttempts): AddonModH5PActivityUserAttempts {
|
||||||
|
const formatted: AddonModH5PActivityUserAttempts = data;
|
||||||
|
|
||||||
|
formatted.attempts = formatted.attempts.map((attempt) => this.formatAttempt(attempt));
|
||||||
|
|
||||||
|
if (formatted.scored) {
|
||||||
|
formatted.scored.attempts = formatted.scored.attempts.map((attempt) => this.formatAttempt(attempt));
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an attempt's result.
|
||||||
|
*
|
||||||
|
* @param result Result to format.
|
||||||
|
*/
|
||||||
|
protected formatResult(result: AddonModH5PActivityWSResult): AddonModH5PActivityWSResult {
|
||||||
|
result.timecreated = result.timecreated * 1000; // Convert to milliseconds.
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for access information WS calls.
|
||||||
|
*
|
||||||
|
* @param id H5P activity ID.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getAccessInformationCacheKey(id: number): string {
|
||||||
|
return ROOT_CACHE_KEY + 'accessInfo:' + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get access information for a given H5P activity.
|
||||||
|
*
|
||||||
|
* @param id H5P activity ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with the data.
|
||||||
|
*/
|
||||||
|
async getAccessInformation(id: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModH5PActivityAccessInfo> {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const params: AddonModH5pactivityGetH5pactivityAccessInformationWSParams = {
|
||||||
|
h5pactivityid: id,
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getAccessInformationCacheKey(id),
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_OFTEN,
|
||||||
|
component: AddonModH5PActivityProvider.COMPONENT,
|
||||||
|
componentId: options.cmId,
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.read('mod_h5pactivity_get_h5pactivity_access_information', params, preSets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get attempt results for all user attempts.
|
||||||
|
*
|
||||||
|
* @param id Activity ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with the results of the attempt.
|
||||||
|
*/
|
||||||
|
async getAllAttemptsResults(
|
||||||
|
id: number,
|
||||||
|
options?: AddonModH5PActivityGetAttemptResultsOptions,
|
||||||
|
): Promise<AddonModH5PActivityAttemptsResults> {
|
||||||
|
|
||||||
|
const userAttempts = await this.getUserAttempts(id, options);
|
||||||
|
|
||||||
|
const attemptIds = userAttempts.attempts.map((attempt) => attempt.id);
|
||||||
|
|
||||||
|
if (attemptIds.length) {
|
||||||
|
// Get all the attempts with a single call.
|
||||||
|
return this.getAttemptsResults(id, attemptIds, options);
|
||||||
|
} else {
|
||||||
|
// No attempts.
|
||||||
|
return {
|
||||||
|
activityid: id,
|
||||||
|
attempts: [],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for results WS calls.
|
||||||
|
*
|
||||||
|
* @param id Instance ID.
|
||||||
|
* @param attemptsIds Attempts IDs.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getAttemptResultsCacheKey(id: number, attemptsIds: number[]): string {
|
||||||
|
return this.getAttemptResultsCommonCacheKey(id) + ':' + JSON.stringify(attemptsIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common cache key for results WS calls.
|
||||||
|
*
|
||||||
|
* @param id Instance ID.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getAttemptResultsCommonCacheKey(id: number): string {
|
||||||
|
return ROOT_CACHE_KEY + 'results:' + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get attempt results.
|
||||||
|
*
|
||||||
|
* @param id Activity ID.
|
||||||
|
* @param attemptId Attempt ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with the results of the attempt.
|
||||||
|
*/
|
||||||
|
async getAttemptResults(
|
||||||
|
id: number,
|
||||||
|
attemptId: number,
|
||||||
|
options?: AddonModH5PActivityGetAttemptResultsOptions,
|
||||||
|
): Promise<AddonModH5PActivityAttemptResults> {
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const params: AddonModH5pactivityGetResultsWSParams = {
|
||||||
|
h5pactivityid: id,
|
||||||
|
attemptids: [attemptId],
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getAttemptResultsCacheKey(id, params.attemptids!),
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
|
component: AddonModH5PActivityProvider.COMPONENT,
|
||||||
|
componentId: options.cmId,
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await site.read<AddonModH5pactivityGetResultsWSResponse>(
|
||||||
|
'mod_h5pactivity_get_results',
|
||||||
|
params,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.warnings?.[0]) {
|
||||||
|
throw new CoreWSError(response.warnings[0]); // Cannot view attempt.
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formatAttemptResults(response.attempts[0]);
|
||||||
|
} catch (error) {
|
||||||
|
if (CoreUtils.isWebServiceError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the full list of results is cached. If so, get the results from there.
|
||||||
|
const cacheOptions: AddonModH5PActivityGetAttemptResultsOptions = {
|
||||||
|
...options, // Include all the original options.
|
||||||
|
readingStrategy: CoreSitesReadingStrategy.OnlyCache,
|
||||||
|
};
|
||||||
|
|
||||||
|
const attemptsResults = await AddonModH5PActivity.getAllAttemptsResults(id, cacheOptions);
|
||||||
|
|
||||||
|
const attempt = attemptsResults.attempts.find((attempt) => attempt.id == attemptId);
|
||||||
|
|
||||||
|
if (!attempt) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attempt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get attempts results.
|
||||||
|
*
|
||||||
|
* @param id Activity ID.
|
||||||
|
* @param attemptsIds Attempts IDs.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with all the attempts.
|
||||||
|
*/
|
||||||
|
async getAttemptsResults(
|
||||||
|
id: number,
|
||||||
|
attemptsIds: number[],
|
||||||
|
options?: AddonModH5PActivityGetAttemptResultsOptions,
|
||||||
|
): Promise<AddonModH5PActivityAttemptsResults> {
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const params: AddonModH5pactivityGetResultsWSParams = {
|
||||||
|
h5pactivityid: id,
|
||||||
|
attemptids: attemptsIds,
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getAttemptResultsCommonCacheKey(id),
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
|
component: AddonModH5PActivityProvider.COMPONENT,
|
||||||
|
componentId: options.cmId,
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await site.read<AddonModH5pactivityGetResultsWSResponse>(
|
||||||
|
'mod_h5pactivity_get_results',
|
||||||
|
params,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
|
response.attempts = response.attempts.map((attempt) => this.formatAttemptResults(attempt));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get deployed file from an H5P activity instance.
|
||||||
|
*
|
||||||
|
* @param h5pActivity Activity instance.
|
||||||
|
* @param options Options
|
||||||
|
* @return Promise resolved with the file.
|
||||||
|
*/
|
||||||
|
async getDeployedFile(
|
||||||
|
h5pActivity: AddonModH5PActivityData,
|
||||||
|
options?: AddonModH5PActivityGetDeployedFileOptions,
|
||||||
|
): Promise<CoreWSExternalFile> {
|
||||||
|
|
||||||
|
if (h5pActivity.deployedfile) {
|
||||||
|
// File already deployed and still valid, use this one.
|
||||||
|
return h5pActivity.deployedfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!h5pActivity.package || !h5pActivity.package[0]) {
|
||||||
|
// Shouldn't happen.
|
||||||
|
throw new CoreError('No H5P package found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
// Deploy the file in the server.
|
||||||
|
return CoreH5P.getTrustedH5PFile(
|
||||||
|
h5pActivity.package[0].fileurl,
|
||||||
|
options.displayOptions,
|
||||||
|
options.ignoreCache,
|
||||||
|
options.siteId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for H5P activity data WS calls.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getH5PActivityDataCacheKey(courseId: number): string {
|
||||||
|
return ROOT_CACHE_KEY + 'h5pactivity:' + courseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an H5P activity with key=value. If more than one is found, only the first will be returned.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param key Name of the property to check.
|
||||||
|
* @param value Value to search.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with the activity data.
|
||||||
|
*/
|
||||||
|
protected async getH5PActivityByField(
|
||||||
|
courseId: number,
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Promise<AddonModH5PActivityData> {
|
||||||
|
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const params: AddonModH5pactivityGetByCoursesWSParams = {
|
||||||
|
courseids: [courseId],
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getH5PActivityDataCacheKey(courseId),
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
|
component: AddonModH5PActivityProvider.COMPONENT,
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await site.read<AddonModH5pactivityGetByCoursesWSResponse>(
|
||||||
|
'mod_h5pactivity_get_h5pactivities_by_courses',
|
||||||
|
params,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentActivity = response.h5pactivities.find((h5pActivity) => h5pActivity[key] == value);
|
||||||
|
|
||||||
|
if (currentActivity) {
|
||||||
|
return currentActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CoreError(Translate.instant('addon.mod_h5pactivity.errorgetactivity'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an H5P activity by module ID.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param cmId Course module ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with the activity data.
|
||||||
|
*/
|
||||||
|
getH5PActivity(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModH5PActivityData> {
|
||||||
|
return this.getH5PActivityByField(courseId, 'coursemodule', cmId, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an H5P activity by context ID.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param contextId Context ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with the activity data.
|
||||||
|
*/
|
||||||
|
getH5PActivityByContextId(
|
||||||
|
courseId: number,
|
||||||
|
contextId: number,
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Promise<AddonModH5PActivityData> {
|
||||||
|
return this.getH5PActivityByField(courseId, 'context', contextId, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an H5P activity by instance ID.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param id Instance ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with the activity data.
|
||||||
|
*/
|
||||||
|
getH5PActivityById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModH5PActivityData> {
|
||||||
|
return this.getH5PActivityByField(courseId, 'id', id, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for attemps WS calls.
|
||||||
|
*
|
||||||
|
* @param id Instance ID.
|
||||||
|
* @param userIds User IDs.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getUserAttemptsCacheKey(id: number, userIds: number[]): string {
|
||||||
|
return this.getUserAttemptsCommonCacheKey(id) + ':' + JSON.stringify(userIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common cache key for attempts WS calls.
|
||||||
|
*
|
||||||
|
* @param id Instance ID.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getUserAttemptsCommonCacheKey(id: number): string {
|
||||||
|
return ROOT_CACHE_KEY + 'attempts:' + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get attempts of a certain user.
|
||||||
|
*
|
||||||
|
* @param id Activity ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with the attempts of the user.
|
||||||
|
*/
|
||||||
|
async getUserAttempts(
|
||||||
|
id: number,
|
||||||
|
options: AddonModH5PActivityGetAttemptsOptions = {},
|
||||||
|
): Promise<AddonModH5PActivityUserAttempts> {
|
||||||
|
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const params: AddonModH5pactivityGetAttemptsWSParams = {
|
||||||
|
h5pactivityid: id,
|
||||||
|
userids: [options.userId || site.getUserId()],
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getUserAttemptsCacheKey(id, params.userids!),
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
|
component: AddonModH5PActivityProvider.COMPONENT,
|
||||||
|
componentId: options.cmId,
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await site.read<AddonModH5pactivityGetAttemptsWSResponse>('mod_h5pactivity_get_attempts', params, preSets);
|
||||||
|
|
||||||
|
if (response.warnings?.[0]) {
|
||||||
|
throw new CoreWSError(response.warnings[0]); // Cannot view user attempts.
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formatUserAttempts(response.usersattempts[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates access information.
|
||||||
|
*
|
||||||
|
* @param id H5P activity ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateAccessInformation(id: number, siteId?: string): Promise<void> {
|
||||||
|
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates H5P activity data.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateActivityData(courseId: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getH5PActivityDataCacheKey(courseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates all attempts results for H5P activity.
|
||||||
|
*
|
||||||
|
* @param id Activity ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateAllResults(id: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getAttemptResultsCommonCacheKey(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates results of a certain attempt for H5P activity.
|
||||||
|
*
|
||||||
|
* @param id Activity ID.
|
||||||
|
* @param attemptId Attempt ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateAttemptResults(id: number, attemptId: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getAttemptResultsCacheKey(id, [attemptId]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates all users attempts for H5P activity.
|
||||||
|
*
|
||||||
|
* @param id Activity ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateAllUserAttempts(id: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getUserAttemptsCommonCacheKey(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates attempts of a certain user for H5P activity.
|
||||||
|
*
|
||||||
|
* @param id Activity ID.
|
||||||
|
* @param userId User ID. If not defined, current user in the site.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateUserAttempts(id: number, userId?: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
userId = userId || site.getUserId();
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getUserAttemptsCacheKey(id, [userId]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete launcher.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when the launcher file is deleted.
|
||||||
|
*/
|
||||||
|
async isPluginEnabled(siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
return site.wsAvailable('mod_h5pactivity_get_h5pactivities_by_courses');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report an H5P activity as being viewed.
|
||||||
|
*
|
||||||
|
* @param id H5P activity ID.
|
||||||
|
* @param name Name of the activity.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the WS call is successful.
|
||||||
|
*/
|
||||||
|
logView(id: number, name?: string, siteId?: string): Promise<void> {
|
||||||
|
const params: AddonModH5pactivityViewH5pactivityWSParams = {
|
||||||
|
h5pactivityid: id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return CoreCourseLogHelper.logSingle(
|
||||||
|
'mod_h5pactivity_view_h5pactivity',
|
||||||
|
params,
|
||||||
|
AddonModH5PActivityProvider.COMPONENT,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
'h5pactivity',
|
||||||
|
{},
|
||||||
|
siteId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModH5PActivity = makeSingleton(AddonModH5PActivityProvider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic data for an H5P activity, exported by Moodle class h5pactivity_summary_exporter.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityData = {
|
||||||
|
id: number; // The primary key of the record.
|
||||||
|
course: number; // Course id this h5p activity is part of.
|
||||||
|
name: string; // The name of the activity module instance.
|
||||||
|
timecreated?: number; // Timestamp of when the instance was added to the course.
|
||||||
|
timemodified?: number; // Timestamp of when the instance was last modified.
|
||||||
|
intro: string; // H5P activity description.
|
||||||
|
introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||||
|
grade?: number; // The maximum grade for submission.
|
||||||
|
displayoptions: number; // H5P Button display options.
|
||||||
|
enabletracking: number; // Enable xAPI tracking.
|
||||||
|
grademethod: number; // Which H5P attempt is used for grading.
|
||||||
|
contenthash?: string; // Sha1 hash of file content.
|
||||||
|
coursemodule: number; // Coursemodule.
|
||||||
|
context: number; // Context ID.
|
||||||
|
introfiles: CoreWSExternalFile[];
|
||||||
|
package: CoreWSExternalFile[];
|
||||||
|
deployedfile?: {
|
||||||
|
filename?: string; // File name.
|
||||||
|
filepath?: string; // File path.
|
||||||
|
filesize?: number; // File size.
|
||||||
|
fileurl: string; // Downloadable file url.
|
||||||
|
timemodified?: number; // Time modified.
|
||||||
|
mimetype?: string; // File mime type.
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_h5pactivity_get_h5pactivities_by_courses WS.
|
||||||
|
*/
|
||||||
|
export type AddonModH5pactivityGetByCoursesWSParams = {
|
||||||
|
courseids?: number[]; // Array of course ids.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by mod_h5pactivity_get_h5pactivities_by_courses WS.
|
||||||
|
*/
|
||||||
|
export type AddonModH5pactivityGetByCoursesWSResponse = {
|
||||||
|
h5pactivities: AddonModH5PActivityData[];
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_h5pactivity_get_h5pactivity_access_information WS.
|
||||||
|
*/
|
||||||
|
export type AddonModH5pactivityGetH5pactivityAccessInformationWSParams = {
|
||||||
|
h5pactivityid: number; // H5p activity instance id.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by mod_h5pactivity_get_h5pactivity_access_information WS.
|
||||||
|
*/
|
||||||
|
export type AddonModH5pactivityGetH5pactivityAccessInformationWSResponse = {
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
canview?: boolean; // Whether the user has the capability mod/h5pactivity:view allowed.
|
||||||
|
canaddinstance?: boolean; // Whether the user has the capability mod/h5pactivity:addinstance allowed.
|
||||||
|
cansubmit?: boolean; // Whether the user has the capability mod/h5pactivity:submit allowed.
|
||||||
|
canreviewattempts?: boolean; // Whether the user has the capability mod/h5pactivity:reviewattempts allowed.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of WS mod_h5pactivity_get_h5pactivity_access_information.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityAccessInfo = AddonModH5pactivityGetH5pactivityAccessInformationWSResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_h5pactivity_get_attempts WS.
|
||||||
|
*/
|
||||||
|
export type AddonModH5pactivityGetAttemptsWSParams = {
|
||||||
|
h5pactivityid: number; // H5p activity instance id.
|
||||||
|
userids?: number[]; // User ids.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by mod_h5pactivity_get_attempts WS.
|
||||||
|
*/
|
||||||
|
export type AddonModH5pactivityGetAttemptsWSResponse = {
|
||||||
|
activityid: number; // Activity course module ID.
|
||||||
|
usersattempts: AddonModH5PActivityWSUserAttempts[]; // The complete users attempts list.
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_h5pactivity_get_results WS.
|
||||||
|
*/
|
||||||
|
export type AddonModH5pactivityGetResultsWSParams = {
|
||||||
|
h5pactivityid: number; // H5p activity instance id.
|
||||||
|
attemptids?: number[]; // Attempt ids.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by mod_h5pactivity_get_results WS.
|
||||||
|
*/
|
||||||
|
export type AddonModH5pactivityGetResultsWSResponse = {
|
||||||
|
activityid: number; // Activity course module ID.
|
||||||
|
attempts: AddonModH5PActivityWSAttemptResults[]; // The complete attempts list.
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts results with some calculated data.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityAttemptsResults = Omit<AddonModH5pactivityGetResultsWSResponse, 'attempts'> & {
|
||||||
|
attempts: AddonModH5PActivityAttemptResults[]; // The complete attempts list.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts data for a user as returned by the WS mod_h5pactivity_get_attempts.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityWSUserAttempts = {
|
||||||
|
userid: number; // The user id.
|
||||||
|
attempts: AddonModH5PActivityWSAttempt[]; // The complete attempts list.
|
||||||
|
scored?: { // Attempts used to grade the activity.
|
||||||
|
title: string; // Scored attempts title.
|
||||||
|
grademethod: string; // Scored attempts title.
|
||||||
|
attempts: AddonModH5PActivityWSAttempt[]; // List of the grading attempts.
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt data as returned by the WS mod_h5pactivity_get_attempts.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityWSAttempt = {
|
||||||
|
id: number; // ID of the context.
|
||||||
|
h5pactivityid: number; // ID of the H5P activity.
|
||||||
|
userid: number; // ID of the user.
|
||||||
|
timecreated: number; // Attempt creation.
|
||||||
|
timemodified: number; // Attempt modified.
|
||||||
|
attempt: number; // Attempt number.
|
||||||
|
rawscore: number; // Attempt score value.
|
||||||
|
maxscore: number; // Attempt max score.
|
||||||
|
duration: number; // Attempt duration in seconds.
|
||||||
|
completion?: number; // Attempt completion.
|
||||||
|
success?: number | null; // Attempt success.
|
||||||
|
scaled: number; // Attempt scaled.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt and results data as returned by the WS mod_h5pactivity_get_results.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityWSAttemptResults = AddonModH5PActivityWSAttempt & {
|
||||||
|
results?: AddonModH5PActivityWSResult[]; // The results of the attempt.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt result data as returned by the WS mod_h5pactivity_get_results.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityWSResult = {
|
||||||
|
id: number; // ID of the context.
|
||||||
|
attemptid: number; // ID of the H5P attempt.
|
||||||
|
subcontent: string; // Subcontent identifier.
|
||||||
|
timecreated: number; // Result creation.
|
||||||
|
interactiontype: string; // Interaction type.
|
||||||
|
description: string; // Result description.
|
||||||
|
content?: string; // Result extra content.
|
||||||
|
rawscore: number; // Result score value.
|
||||||
|
maxscore: number; // Result max score.
|
||||||
|
duration?: number; // Result duration in seconds.
|
||||||
|
completion?: number; // Result completion.
|
||||||
|
success?: number | null; // Result success.
|
||||||
|
optionslabel?: string; // Label used for result options.
|
||||||
|
correctlabel?: string; // Label used for correct answers.
|
||||||
|
answerlabel?: string; // Label used for user answers.
|
||||||
|
track?: boolean; // If the result has valid track information.
|
||||||
|
options?: { // The statement options.
|
||||||
|
description: string; // Option description.
|
||||||
|
id: number; // Option identifier.
|
||||||
|
correctanswer: AddonModH5PActivityWSResultAnswer; // The option correct answer.
|
||||||
|
useranswer: AddonModH5PActivityWSResultAnswer; // The option user answer.
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result answer as returned by the WS mod_h5pactivity_get_results.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityWSResultAnswer = {
|
||||||
|
answer?: string; // Option text value.
|
||||||
|
correct?: boolean; // If has to be displayed as correct.
|
||||||
|
incorrect?: boolean; // If has to be displayed as incorrect.
|
||||||
|
text?: boolean; // If has to be displayed as simple text.
|
||||||
|
checked?: boolean; // If has to be displayed as a checked option.
|
||||||
|
unchecked?: boolean; // If has to be displayed as a unchecked option.
|
||||||
|
pass?: boolean; // If has to be displayed as passed.
|
||||||
|
fail?: boolean; // If has to be displayed as failed.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User attempts data with some calculated data.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityUserAttempts = Omit<AddonModH5PActivityWSUserAttempts, 'attempts|scored'> & {
|
||||||
|
attempts: AddonModH5PActivityAttempt[]; // The complete attempts list.
|
||||||
|
scored?: { // Attempts used to grade the activity.
|
||||||
|
title: string; // Scored attempts title.
|
||||||
|
grademethod: string; // Scored attempts title.
|
||||||
|
attempts: AddonModH5PActivityAttempt[]; // List of the grading attempts.
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt with some calculated data.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityAttempt = AddonModH5PActivityWSAttempt & {
|
||||||
|
durationReadable?: string; // Duration in a human readable format.
|
||||||
|
durationCompact?: string; // Duration in a "short" human readable format.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt and results data with some calculated data.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityAttemptResults = AddonModH5PActivityAttempt & {
|
||||||
|
results?: AddonModH5PActivityWSResult[]; // The results of the attempt.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to pass to getDeployedFile function.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityGetDeployedFileOptions = {
|
||||||
|
displayOptions?: CoreH5PDisplayOptions; // Display options
|
||||||
|
ignoreCache?: boolean; // Whether to ignore cache. Will fail if offline or server down.
|
||||||
|
siteId?: string; // Site ID. If not defined, current site.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to pass to getAttemptResults function.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityGetAttemptResultsOptions = CoreCourseCommonModWSOptions & {
|
||||||
|
userId?: number; // User ID. If not defined, user of the site.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to pass to getAttempts function.
|
||||||
|
*/
|
||||||
|
export type AddonModH5PActivityGetAttemptsOptions = AddonModH5PActivityGetAttemptResultsOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_h5pactivity_view_h5pactivity WS.
|
||||||
|
*/
|
||||||
|
export type AddonModH5pactivityViewH5pactivityWSParams = {
|
||||||
|
h5pactivityid: number; // H5P activity instance id.
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Augment CoreEventsData interface with events specific to this service.
|
||||||
|
*
|
||||||
|
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||||
|
*/
|
||||||
|
export interface CoreEventsData {
|
||||||
|
[AddonModH5PActivitySyncProvider.AUTO_SYNCED]: AddonModH5PActivityAutoSyncData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (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 { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to H5P activity index.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModH5PActivityIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
|
||||||
|
|
||||||
|
name = 'AddonModH5PActivityIndexLinkHandler';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('AddonModH5PActivity', 'h5pactivity');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModH5PActivityIndexLinkHandler = makeSingleton(AddonModH5PActivityIndexLinkHandlerService);
|
|
@ -0,0 +1,85 @@
|
||||||
|
// (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 { CoreConstants } from '@/core/constants';
|
||||||
|
import { Injectable, Type } from '@angular/core';
|
||||||
|
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
|
||||||
|
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||||
|
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||||
|
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModH5PActivityIndexComponent } from '../../components/index';
|
||||||
|
import { AddonModH5PActivity } from '../h5pactivity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support H5P activities.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModH5PActivityModuleHandlerService implements CoreCourseModuleHandler {
|
||||||
|
|
||||||
|
static readonly PAGE_NAME = 'mod_h5pactivity';
|
||||||
|
|
||||||
|
name = 'AddonModH5PActivity';
|
||||||
|
modName = 'h5pactivity';
|
||||||
|
|
||||||
|
supportedFeatures = {
|
||||||
|
[CoreConstants.FEATURE_GROUPS]: true,
|
||||||
|
[CoreConstants.FEATURE_GROUPINGS]: true,
|
||||||
|
[CoreConstants.FEATURE_MOD_INTRO]: true,
|
||||||
|
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
|
||||||
|
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
|
||||||
|
[CoreConstants.FEATURE_MODEDIT_DEFAULT_COMPLETION]: true,
|
||||||
|
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: true,
|
||||||
|
[CoreConstants.FEATURE_GRADE_OUTCOMES]: true,
|
||||||
|
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
isEnabled(): Promise<boolean> {
|
||||||
|
return AddonModH5PActivity.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
|
||||||
|
title: module.name,
|
||||||
|
class: 'addon-mod_h5pactivity-handler',
|
||||||
|
showDownloadButton: true,
|
||||||
|
action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) {
|
||||||
|
options = options || {};
|
||||||
|
options.params = options.params || {};
|
||||||
|
Object.assign(options.params, { module });
|
||||||
|
const routeParams = '/' + courseId + '/' + module.id;
|
||||||
|
|
||||||
|
CoreNavigator.navigateToSitePath(AddonModH5PActivityModuleHandlerService.PAGE_NAME + routeParams, options);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getMainComponent(): Promise<Type<unknown>> {
|
||||||
|
return AddonModH5PActivityIndexComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModH5PActivityModuleHandler = makeSingleton(AddonModH5PActivityModuleHandlerService);
|
|
@ -0,0 +1,171 @@
|
||||||
|
// (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 { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
|
||||||
|
import { CoreCourseAnyModuleData } from '@features/course/services/course';
|
||||||
|
import { CoreH5PHelper } from '@features/h5p/classes/helper';
|
||||||
|
import { CoreH5P } from '@features/h5p/services/h5p';
|
||||||
|
import { CoreUser } from '@features/user/services/user';
|
||||||
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||||
|
import { CoreWSExternalFile } from '@services/ws';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModH5PActivity, AddonModH5PActivityData, AddonModH5PActivityProvider } from '../h5pactivity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to prefetch h5p activity.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
|
||||||
|
|
||||||
|
name = 'AddonModH5PActivity';
|
||||||
|
modName = 'h5pactivity';
|
||||||
|
component = AddonModH5PActivityProvider.COMPONENT;
|
||||||
|
updatesNames = /^configuration$|^.*files$|^tracks$|^usertracks$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
|
||||||
|
|
||||||
|
const h5pActivity = await AddonModH5PActivity.getH5PActivity(courseId, module.id);
|
||||||
|
|
||||||
|
const displayOptions = CoreH5PHelper.decodeDisplayOptions(h5pActivity.displayoptions);
|
||||||
|
|
||||||
|
const deployedFile = await AddonModH5PActivity.getDeployedFile(h5pActivity, {
|
||||||
|
displayOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [deployedFile].concat(this.getIntroFilesFromInstance(module, h5pActivity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async invalidateModule(): Promise<void> {
|
||||||
|
// No need to invalidate anything.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async isDownloadable(): Promise<boolean> {
|
||||||
|
return !!CoreSites.getCurrentSite()?.canDownloadFiles() && !CoreH5P.isOfflineDisabledInSite();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
isEnabled(): Promise<boolean> {
|
||||||
|
return AddonModH5PActivity.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
|
||||||
|
return this.prefetchPackage(module, courseId, this.prefetchActivity.bind(this, module, courseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch an H5P activity.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async prefetchActivity(
|
||||||
|
module: CoreCourseAnyModuleData,
|
||||||
|
courseId: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
const h5pActivity = await AddonModH5PActivity.getH5PActivity(courseId, module.id, {
|
||||||
|
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||||
|
siteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const introFiles = this.getIntroFilesFromInstance(module, h5pActivity);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.prefetchWSData(h5pActivity, siteId),
|
||||||
|
CoreFilepool.addFilesToQueue(siteId, introFiles, AddonModH5PActivityProvider.COMPONENT, module.id),
|
||||||
|
this.prefetchMainFile(module, h5pActivity, siteId),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch the deployed file of the activity.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param h5pActivity Activity instance.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async prefetchMainFile(
|
||||||
|
module: CoreCourseAnyModuleData,
|
||||||
|
h5pActivity: AddonModH5PActivityData,
|
||||||
|
siteId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
const displayOptions = CoreH5PHelper.decodeDisplayOptions(h5pActivity.displayoptions);
|
||||||
|
|
||||||
|
const deployedFile = await AddonModH5PActivity.getDeployedFile(h5pActivity, {
|
||||||
|
displayOptions: displayOptions,
|
||||||
|
ignoreCache: true,
|
||||||
|
siteId: siteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await CoreFilepool.addFilesToQueue(siteId, [deployedFile], AddonModH5PActivityProvider.COMPONENT, module.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch all the WebService data.
|
||||||
|
*
|
||||||
|
* @param h5pActivity Activity instance.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async prefetchWSData(h5pActivity: AddonModH5PActivityData, siteId: string): Promise<void> {
|
||||||
|
|
||||||
|
const accessInfo = await AddonModH5PActivity.getAccessInformation(h5pActivity.id, {
|
||||||
|
cmId: h5pActivity.coursemodule,
|
||||||
|
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
||||||
|
siteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!accessInfo.canreviewattempts) {
|
||||||
|
// Not a teacher, prefetch user attempts and the current user profile.
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
cmId: h5pActivity.coursemodule,
|
||||||
|
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||||
|
siteId: siteId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
AddonModH5PActivity.getAllAttemptsResults(h5pActivity.id, options),
|
||||||
|
CoreUser.prefetchProfiles([site.getUserId()], h5pActivity.course, siteId),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModH5PActivityPrefetchHandler = makeSingleton(AddonModH5PActivityPrefetchHandlerService);
|
|
@ -0,0 +1,136 @@
|
||||||
|
// (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 { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||||
|
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModH5PActivity } from '../h5pactivity';
|
||||||
|
import { AddonModH5PActivityModuleHandlerService } from './module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to H5P activity report.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModH5PActivityReportLinkHandlerService extends CoreContentLinksHandlerBase {
|
||||||
|
|
||||||
|
name = 'AddonModH5PActivityReportLinkHandler';
|
||||||
|
featureName = 'CoreCourseModuleDelegate_AddonModH5PActivity';
|
||||||
|
pattern = /\/mod\/h5pactivity\/report\.php.*([&?]a=\d+)/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getActions(
|
||||||
|
siteIds: string[],
|
||||||
|
url: string,
|
||||||
|
params: Record<string, string>,
|
||||||
|
courseId?: number,
|
||||||
|
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||||
|
courseId = courseId || Number(params.courseid) || Number(params.cid);
|
||||||
|
|
||||||
|
return [{
|
||||||
|
action: async (siteId) => {
|
||||||
|
try {
|
||||||
|
const instanceId = Number(params.a);
|
||||||
|
|
||||||
|
if (!courseId) {
|
||||||
|
courseId = await this.getCourseId(instanceId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const module = await CoreCourse.getModuleBasicInfoByInstance(instanceId, 'h5pactivity', siteId);
|
||||||
|
|
||||||
|
if (typeof params.attemptid != 'undefined') {
|
||||||
|
this.openAttemptResults(module.id, Number(params.attemptid), courseId, siteId);
|
||||||
|
} else {
|
||||||
|
const userId = params.userid ? Number(params.userid) : undefined;
|
||||||
|
|
||||||
|
this.openUserAttempts(module.id, courseId, siteId, userId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'Error processing link.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get course Id for an activity.
|
||||||
|
*
|
||||||
|
* @param id Activity ID.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @return Promise resolved with course ID.
|
||||||
|
*/
|
||||||
|
protected async getCourseId(id: number, siteId: string): Promise<number> {
|
||||||
|
const modal = await CoreDomUtils.showModalLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const module = await CoreCourse.getModuleBasicInfoByInstance(id, 'h5pactivity', siteId);
|
||||||
|
|
||||||
|
return module.course;
|
||||||
|
} finally {
|
||||||
|
modal.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
isEnabled(): Promise<boolean> {
|
||||||
|
return AddonModH5PActivity.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open attempt results.
|
||||||
|
*
|
||||||
|
* @param cmId Module ID.
|
||||||
|
* @param attemptId Attempt ID.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected openAttemptResults(cmId: number, attemptId: number, courseId: number, siteId: string): void {
|
||||||
|
const path = AddonModH5PActivityModuleHandlerService.PAGE_NAME + `/${courseId}/${cmId}/attemptresults/${attemptId}`;
|
||||||
|
|
||||||
|
CoreNavigator.navigateToSitePath(path, {
|
||||||
|
siteId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open user attempts.
|
||||||
|
*
|
||||||
|
* @param cmId Module ID.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @param userId User ID. If not defined, current user in site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected openUserAttempts(cmId: number, courseId: number, siteId: string, userId?: number): void {
|
||||||
|
userId = userId || CoreSites.getCurrentSiteUserId();
|
||||||
|
const path = AddonModH5PActivityModuleHandlerService.PAGE_NAME + `/${courseId}/${cmId}/userattempts/${userId}`;
|
||||||
|
|
||||||
|
CoreNavigator.navigateToSitePath(path, {
|
||||||
|
siteId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModH5PActivityReportLinkHandler = makeSingleton(AddonModH5PActivityReportLinkHandlerService);
|
|
@ -0,0 +1,52 @@
|
||||||
|
// (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 '@services/cron';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModH5PActivitySync } from '../h5pactivity-sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronization cron handler.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModH5PActivitySyncCronHandlerService 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<void> {
|
||||||
|
return AddonModH5PActivitySync.syncAllActivities(siteId, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time between consecutive executions.
|
||||||
|
*
|
||||||
|
* @return Time between consecutive executions (in ms).
|
||||||
|
*/
|
||||||
|
getInterval(): number {
|
||||||
|
return AddonModH5PActivitySync.syncInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModH5PActivitySyncCronHandler = makeSingleton(AddonModH5PActivitySyncCronHandlerService);
|
|
@ -26,6 +26,7 @@ import { AddonModQuizModule } from './quiz/quiz.module';
|
||||||
import { AddonModResourceModule } from './resource/resource.module';
|
import { AddonModResourceModule } from './resource/resource.module';
|
||||||
import { AddonModUrlModule } from './url/url.module';
|
import { AddonModUrlModule } from './url/url.module';
|
||||||
import { AddonModLtiModule } from './lti/lti.module';
|
import { AddonModLtiModule } from './lti/lti.module';
|
||||||
|
import { AddonModH5PActivityModule } from './h5pactivity/h5pactivity.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [],
|
declarations: [],
|
||||||
|
@ -42,6 +43,7 @@ import { AddonModLtiModule } from './lti/lti.module';
|
||||||
AddonModFolderModule,
|
AddonModFolderModule,
|
||||||
AddonModImscpModule,
|
AddonModImscpModule,
|
||||||
AddonModLtiModule,
|
AddonModLtiModule,
|
||||||
|
AddonModH5PActivityModule,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [],
|
exports: [],
|
||||||
|
|
|
@ -129,7 +129,7 @@ import { ADDON_MOD_BOOK_SERVICES } from '@addons/mod/book/book.module';
|
||||||
import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module';
|
import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module';
|
||||||
import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module';
|
import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module';
|
||||||
// @todo import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module';
|
// @todo import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module';
|
||||||
// @todo import { ADDON_MOD_H5P_ACTIVITY_SERVICES } from '@addons/mod/h5pactivity/h5pactivity.module';
|
import { ADDON_MOD_H5P_ACTIVITY_SERVICES } from '@addons/mod/h5pactivity/h5pactivity.module';
|
||||||
import { ADDON_MOD_IMSCP_SERVICES } from '@addons/mod/imscp/imscp.module';
|
import { ADDON_MOD_IMSCP_SERVICES } from '@addons/mod/imscp/imscp.module';
|
||||||
import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module';
|
import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module';
|
||||||
import { ADDON_MOD_LTI_SERVICES } from '@addons/mod/lti/lti.module';
|
import { ADDON_MOD_LTI_SERVICES } from '@addons/mod/lti/lti.module';
|
||||||
|
@ -294,7 +294,7 @@ export class CoreCompileProvider {
|
||||||
...ADDON_MOD_FOLDER_SERVICES,
|
...ADDON_MOD_FOLDER_SERVICES,
|
||||||
...ADDON_MOD_FORUM_SERVICES,
|
...ADDON_MOD_FORUM_SERVICES,
|
||||||
// @todo ...ADDON_MOD_GLOSSARY_SERVICES,
|
// @todo ...ADDON_MOD_GLOSSARY_SERVICES,
|
||||||
// @todo ...ADDON_MOD_H5P_ACTIVITY_SERVICES,
|
...ADDON_MOD_H5P_ACTIVITY_SERVICES,
|
||||||
...ADDON_MOD_IMSCP_SERVICES,
|
...ADDON_MOD_IMSCP_SERVICES,
|
||||||
...ADDON_MOD_LESSON_SERVICES,
|
...ADDON_MOD_LESSON_SERVICES,
|
||||||
...ADDON_MOD_LTI_SERVICES,
|
...ADDON_MOD_LTI_SERVICES,
|
||||||
|
|
|
@ -226,7 +226,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
||||||
async gotoBlog(): Promise<void> {
|
async gotoBlog(): Promise<void> {
|
||||||
const params: Params = { cmId: this.module.id };
|
const params: Params = { cmId: this.module.id };
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params });
|
await CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -68,7 +68,6 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy {
|
||||||
this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.isOfflineDisabledInSite();
|
this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.isOfflineDisabledInSite();
|
||||||
|
|
||||||
// Send resize events when the page holding this component is re-entered.
|
// Send resize events when the page holding this component is re-entered.
|
||||||
// @todo: Check that this works as expected.
|
|
||||||
this.currentPageRoute = router.url;
|
this.currentPageRoute = router.url;
|
||||||
this.subscription = router.events
|
this.subscription = router.events
|
||||||
.pipe(filter(event => event instanceof NavigationEnd))
|
.pipe(filter(event => event instanceof NavigationEnd))
|
||||||
|
|
|
@ -1215,7 +1215,12 @@ export class CoreFileProvider {
|
||||||
* @return Path.
|
* @return Path.
|
||||||
*/
|
*/
|
||||||
getWWWPath(): string {
|
getWWWPath(): string {
|
||||||
const position = window.location.href.indexOf('index.html');
|
// Use current URL, removing the path.
|
||||||
|
if (!window.location.pathname || window.location.pathname == '/') {
|
||||||
|
return window.location.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = window.location.href.indexOf(window.location.pathname);
|
||||||
|
|
||||||
if (position != -1) {
|
if (position != -1) {
|
||||||
return window.location.href.substr(0, position);
|
return window.location.href.substr(0, position);
|
||||||
|
|
Loading…
Reference in New Issue