Commit aa35072d authored by Konstantin Schulz's avatar Konstantin Schulz
Browse files

updated H5P standalone to the latest version (2.1.3)

parent 64467d43
Pipeline #12042 passed with stages
in 3 minutes and 39 seconds
......@@ -81,10 +81,10 @@ class TextService:
return text_with_gaps
@staticmethod
def get_solutions_by_index(exercise: Exercise, solution_indices: List[int] = None) -> List[Solution]:
def get_solutions_by_index(exercise: Exercise, solution_indices: List[int]) -> List[Solution]:
""" If available, makes use of the solution indices to return only the wanted solutions. """
available_solutions: List[Solution] = [Solution.from_dict(x) for x in json.loads(exercise.solutions)]
if solution_indices is None:
if not solution_indices:
return available_solutions
return [available_solutions[i] for i in solution_indices] if len(solution_indices) > 0 else []
......
......@@ -973,7 +973,7 @@ class CommonTestCase(unittest.TestCase):
def test_get_solutions_by_index(self):
""" If available, makes use of the solution indices to return only the wanted solutions. """
solutions: List[Solution] = TextService.get_solutions_by_index(Mocks.exercise)
solutions: List[Solution] = TextService.get_solutions_by_index(Mocks.exercise, [])
self.assertEqual(len(solutions), 1)
def test_get_treebank_annotations(self):
......
......@@ -19,5 +19,5 @@ Use the `--host 0.0.0.0 --disable-host-check` flag for `ng serve` if you want to
## Other
For all other kinds of configuration, use `src/configMC.ts`.
# Testing
To test the application and check the code coverage, run `npm run test`.
To test the application and check the code coverage, run `npm run test-ci`.
To write new tests or debug existing ones, use `npm run test-debug`.
......@@ -3801,6 +3801,11 @@
"@types/jasmine": "*"
}
},
"@types/json-schema": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ=="
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
......@@ -8748,6 +8753,78 @@
"resolve-cwd": "^2.0.0"
}
},
"imports-loader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/imports-loader/-/imports-loader-1.0.0.tgz",
"integrity": "sha512-gCS5kUte6B8oOduQsKP292ugVRK33Wjcapn7ZctetcYnRbZhRlF5i2cUhx7Anqqg4lM7G9DGedAbnchPhPlpVg==",
"requires": {
"loader-utils": "^2.0.0",
"schema-utils": "^2.7.0",
"source-map": "^0.6.1",
"strip-comments": "^2.0.1"
},
"dependencies": {
"ajv": {
"version": "6.12.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
"integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"schema-utils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
"requires": {
"@types/json-schema": "^7.0.4",
"ajv": "^6.12.2",
"ajv-keywords": "^3.4.1"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
......@@ -13409,6 +13486,11 @@
"ansi-regex": "^2.0.0"
}
},
"strip-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz",
"integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw=="
},
"strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
......@@ -13837,6 +13919,11 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"toposort-class": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
"integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg="
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
......
......@@ -42,7 +42,9 @@
"cordova-plugin-statusbar": "^2.4.3",
"cordova-plugin-whitelist": "^1.3.4",
"core-js": "^3.6.4",
"imports-loader": "^1.0.0",
"rxjs": "^6.5.4",
"toposort-class": "^1.0.1",
"tslib": "^1.11.1",
"webpack": "^4.42.0",
"zone.js": "^0.10.2"
......
......@@ -49,9 +49,13 @@ describe('ExerciseListPage', () => {
fixture.detectChanges();
});
it('should create', () => {
it('should create', (done) => {
expect(exerciseListPage).toBeTruthy();
expect(exerciseListPage.getExerciseList).toHaveBeenCalled();
getExerciseListSpy.and.callFake(() => Promise.reject());
exerciseListPage.ngOnInit().then(() => {}, () => {
done();
});
});
it('should filter exercises', () => {
......@@ -72,20 +76,21 @@ describe('ExerciseListPage', () => {
spyOn(exerciseListPage.storage, 'get').withArgs(configMC.localStorageKeyUpdateInfo).and.returnValue(
Promise.resolve(JSON.stringify(new UpdateInfo({exerciseList: 0}))));
exerciseListPage.getExerciseList().then(() => {
}, () => {
}, async () => {
expect(requestSpy).toHaveBeenCalledTimes(1);
exerciseListPage.vocService.currentReferenceVocabulary = VocabularyMC.Agldt;
exerciseListPage.helperService.applicationState.next(new ApplicationState({exerciseList: []}));
requestSpy.and.returnValue(Promise.resolve([]));
exerciseListPage.getExerciseList().then(() => {
expect(exerciseListPage.availableExercises.length).toBe(0);
exerciseListPage.helperService.applicationState.next(exerciseListPage.helperService.deepCopy(MockMC.applicationState));
requestSpy.and.returnValue(Promise.resolve([new ExerciseMC()]));
exerciseListPage.getExerciseList().then(() => {
expect(exerciseListPage.availableExercises.length).toBe(1);
done();
});
});
await exerciseListPage.getExerciseList();
expect(exerciseListPage.availableExercises.length).toBe(0);
exerciseListPage.helperService.applicationState.next(exerciseListPage.helperService.deepCopy(MockMC.applicationState));
requestSpy.and.returnValue(Promise.resolve([new ExerciseMC()]));
await exerciseListPage.getExerciseList();
expect(exerciseListPage.availableExercises.length).toBe(1);
exerciseListPage.availableExercises = [];
await exerciseListPage.getExerciseList(true);
expect(exerciseListPage.availableExercises.length).toBe(1);
done();
});
});
......
......@@ -120,9 +120,15 @@ export class ExerciseListPage implements OnInit {
return exercise.matching_degree ? Math.round(exercise.matching_degree).toString() : '';
}
ngOnInit(): void {
this.vocService.currentReferenceVocabulary = null;
this.getExerciseList().then();
ngOnInit(): Promise<void> {
return new Promise<void>(((resolve, reject) => {
this.vocService.currentReferenceVocabulary = null;
this.getExerciseList().then(() => {
return resolve();
}, () => {
return reject();
});
}));
}
public processExercises(): void {
......
......@@ -33,21 +33,10 @@ describe('ExerciseService', () => {
it('should initialize H5P', (done) => {
let h5pCalled = false;
const h5p = {
jQuery: () => {
h5pCalled = true;
return {
empty: () => {
return {
h5p: (selector: string) => {
}
};
}
};
}, off: () => {
}
};
spyOn(exerciseService.helperService, 'getH5P').and.returnValue(h5p);
spyOn(exerciseService, 'createH5Pstandalone').and.callFake(() => new Promise(resolve => {
h5pCalled = true;
return resolve();
}));
exerciseService.initH5P('').then(() => {
expect(h5pCalled).toBe(true);
done();
......
......@@ -3,6 +3,10 @@ import {Injectable} from '@angular/core';
import configMC from '../configMC';
import {HelperService} from './helper.service';
import {ExercisePart} from './models/exercisePart';
import {Options} from './models/h5p-standalone.class';
import {EventMC} from './models/enum';
declare var H5PStandalone: any;
@Injectable({
providedIn: 'root'
......@@ -17,11 +21,12 @@ export class ExerciseService {
set currentExerciseIndex(value: number) {
this._currentExerciseIndex = value;
this.currentExercisePartIndex = [...Array(this.currentExerciseParts.length).keys()].find(
i => this.currentExerciseParts[i].startIndex <= this.currentExerciseIndex && (!this.currentExerciseParts[i + 1]
|| this.currentExerciseParts[i + 1].startIndex > this.currentExerciseIndex));
i => this.currentExerciseParts[i].startIndex <= this.currentExerciseIndex &&
(!this.currentExerciseParts[i + 1] || this.currentExerciseParts[i + 1].startIndex > this.currentExerciseIndex));
const cepi: number = this.currentExercisePartIndex;
this.currentExerciseName = this.currentExercisePartIndex ?
this.currentExerciseParts[cepi].exercises[this.currentExerciseIndex - this.currentExerciseParts[cepi].startIndex] : '';
this.currentExerciseParts[cepi].exercises[this.currentExerciseIndex - this.currentExerciseParts[cepi].startIndex] :
'';
}
public currentExerciseName: string;
......@@ -30,7 +35,7 @@ export class ExerciseService {
public excludeOOV = false;
public fillBlanksString = 'fill_blanks';
public h5pContainerString = '.h5p-container';
public h5pIframeString = '#h5p-iframe-1';
public h5pIframeString = '.h5p-iframe';
public kwicGraphs: string;
public vocListString = 'voc_list';
......@@ -48,18 +53,26 @@ export class ExerciseService {
s4() + '-' + s4() + s4() + s4();
}
createH5Pstandalone(el: HTMLElement, h5pLocation: string): Promise<void> {
return new H5PStandalone.H5P(el, h5pLocation);
}
initH5P(exerciseTypePath: string): Promise<void> {
return new Promise(resolve => {
// dirty hack to get H5P going without explicit button click on the new page
setTimeout(() => {
// noinspection TypeScriptValidateJSTypes
this.helperService.getH5P().jQuery(this.h5pContainerString).empty().h5p({
frameJs: 'assets/dist/js/h5p-standalone-frame.min.js',
frameCss: 'assets/dist/styles/h5p.css',
h5pContent: 'assets/h5p/' + exerciseTypePath
});
return resolve();
}, 50);
return new Promise((resolve, reject) => {
const el: HTMLDivElement = document.querySelector(this.h5pContainerString);
const h5pLocation = 'assets/h5p/' + exerciseTypePath;
const options: Options = {
frameCss: 'assets/h5p-standalone-master/dist/styles/h5p.css',
frameJs: 'assets/h5p-standalone-master/dist/frame.bundle.js',
preventH5PInit: false
};
this.createH5Pstandalone(el, h5pLocation).then(() => {
// dirty hack to wait for all the H5P elements being added to the DOM
setTimeout(() => {
this.helperService.events.trigger(EventMC.h5pCreated, {data: {library: exerciseTypePath}});
return resolve();
}, 150);
});
});
}
......
<ion-header>
<ion-toolbar>
<div class="toolbar-left">
<ion-title>{{ 'EXERCISE' | translate }}</ion-title>
</div>
<div class="toolbar-right">
<ion-spinner *ngIf="helperService.openRequests.length"></ion-spinner>
<button (click)="helperService.goToHomePage(navCtrl)">
<ion-icon name="home"></ion-icon>
</button>
</div>
<ion-buttons slot="start">
<div class="home-logo">
<a (click)="helperService.goToHomePage(navCtrl)">
<img src="assets/imgs/logo.png" width="32px" height="32px" alt="CALLIDUS Logo">
</a>
</div>
</ion-buttons>
<ion-spinner *ngIf="helperService.openRequests.length"></ion-spinner>
<ion-title>{{ 'EXERCISE' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-menu-button autoHide="false">
<ion-icon name="menu"></ion-icon>
</ion-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
......
......@@ -11,6 +11,7 @@ import {Language} from 'src/app/models/language';
import {ReplaySubject} from 'rxjs';
import {TextData} from './models/textData';
import configMC from '../configMC';
import EventRegistry from './models/eventRegistry';
declare var H5P: any;
// dirty hack to prevent H5P access errors after resize events
......@@ -85,6 +86,7 @@ export class HelperService {
vocative: DependencyValue.vocative,
xcomp: DependencyValue.clausalComplement,
};
public events: EventRegistry = new EventRegistry();
public isIE11: boolean = !!(window as any).MSInputMethodContext && !!(document as any).documentMode;
public isDevMode = ['localhost'].indexOf(window.location.hostname) > -1; // set this to "false" for simulated production mode
public isVocabularyCheck = false;
......
......@@ -90,6 +90,11 @@ export enum DependencyTranslation {
vocative = 'DEPENDENCY_VOCATIVE' as any,
}
export enum EventMC {
h5pCreated = 'h5pCreated' as any,
xAPI = 'xAPI' as any,
}
export enum ExerciseType {
cloze = 'cloze' as any,
kwic = 'kwic' as any,
......
import {EventMC} from './enum';
export default class EventRegistry {
listeners: { [eventName: string]: any[] } = {};
public off(eventName: EventMC) {
this.listeners[eventName] = [];
}
public on(eventName: EventMC, callback: any) {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
this.listeners[eventName].push(callback);
}
public trigger(eventName: EventMC, event: any) {
if (this.listeners[eventName]) {
this.listeners[eventName].forEach(value => value(event));
}
}
}
import Toposort from 'toposort-class';
declare var H5P: any;
declare var H5PIntegration: any;
export interface Options {
id?: string;
frameCss: string;
frameJs: string;
preventH5PInit: boolean;
}
interface Library {
machineName: string;
minorVersion: string;
majorVersion: string;
dependencies: LibraryRef[];
preloadedCss: string[];
preloadedJs: string[];
}
interface LibraryRef {
machineName: string;
minorVersion: string;
majorVersion: string;
}
interface H5Pjson {
mainLibrary: string;
preloadedDependencies: LibraryRef[];
}
export default class H5PStandaloneClass {
id: string;
librariesPath: string;
path: string;
h5p: H5Pjson;
mainLibrary: Library;
pathIncludesVersion: boolean;
mainLibraryPath: string;
constructor(el: HTMLElement, pathToContent = 'workspace',
options: Options = {
frameCss: null,
frameJs: null,
id: null,
preventH5PInit: null
},
displayOptions = {}, librariesPath: string = null) {
this.id = options.id || Math.random().toString(36).substr(2, 9);
this.path = pathToContent;
if (!librariesPath) {
this.librariesPath = this.path;
} else {
this.librariesPath = librariesPath;
}
console.log(this.librariesPath);
this.initElement(el);
this.initH5P(options.frameCss, options.frameJs, displayOptions, options.preventH5PInit);
return this;
}
initElement(el: HTMLElement) {
if (!(el instanceof HTMLElement)) {
throw new Error('createH5P must be passed an element');
}
el.innerHTML = `<div class="h5p-iframe-wrapper" style="background-color:#DDD;">
<iframe id="h5p-iframe-${this.id}" class="h5p-iframe" data-content-id="${this.id}" style="width: 100%;
height: 100%; border: none; display: block;" src="about:blank" frameBorder="0"></iframe>
</div>`;
}
async initH5P(frameCss = 'assets/h5p-standalone-master/dist/styles/h5p.css',
frameJs = 'assets/h5p-standalone-master/dist/frame.bundle.js',
displayOptions, preventH5PInit: boolean) {
this.h5p = (await this.getJSON(`${this.path}/h5p.json`)) as H5Pjson;
const content = await this.getJSON(window.localStorage.getItem('mc/h5p'));
H5PIntegration.pathIncludesVersion = this.pathIncludesVersion = await this.checkIfPathIncludesVersion();
this.mainLibrary = await this.findMainLibrary();
const dependencies = await this.findAllDependencies();
const {styles, scripts} = this.sortDependencies(dependencies);
H5PIntegration.url = this.path;
H5PIntegration.contents = H5PIntegration.contents ? H5PIntegration.contents : {};
H5PIntegration.core = {
styles: [frameCss],
scripts: [frameJs]
};
H5PIntegration.contents[`cid-${this.id}`] = {
library: `${this.mainLibrary.machineName} ${this.mainLibrary.majorVersion}.${this.mainLibrary.minorVersion}`,
jsonContent: JSON.stringify(content),
styles,
scripts,
displayOptions
};
// if (!preventH5PInit) {
H5P.init();
// }
}
async getJSON(url: string): Promise<any> {
const res = await fetch(url);
return res.json();
}
/**
* Check if the library folder include the version or not
* This was changed at some point in H5P and we need to be backwards compatible
*
* @return {boolean}
*/
async checkIfPathIncludesVersion(): Promise<boolean> {
const dependency = this.h5p.preloadedDependencies[0];
const machinePath = dependency.machineName + '-' + dependency.majorVersion + '.' + dependency.minorVersion;
let pathIncludesVersion: boolean;
try {
await this.getJSON(`${this.librariesPath}/${machinePath}/library.json`);
pathIncludesVersion = true;
} catch (e) {
pathIncludesVersion = false;
}
return pathIncludesVersion;
}
/**
* return the path to a library
* @param {object} library
* @return {string}
*/
libraryPath(library: LibraryRef): string {
return library.machineName + (this.pathIncludesVersion ? '-' + library.majorVersion + '.' + library.minorVersion : '');
}
/**
* FInd the main library for this H5P
* @return {Promise}
*/
async findMainLibrary(): Promise<Library> {
const mainLibraryInfo = this.h5p.preloadedDependencies.find(dep => dep.machineName === this.h5p.mainLibrary);
this.mainLibraryPath = this.h5p.mainLibrary + (this.pathIncludesVersion ? '-' + mainLibraryInfo.majorVersion +
'.' + mainLibraryInfo.minorVersion : '');
return this.getJSON(`${this.librariesPath}/${this.mainLibraryPath}/library.json`);
}
/**
* find all the libraries used in this H5P
* @return {Promise}
*/
async findAllDependencies() {
const directDependencyNames = this.h5p.preloadedDependencies.map(dependency => this.libraryPath(dependency));
return this.loadDependencies(directDependencyNames, []);
}
/**
* searches through all supplied libraries for dependencies, this is recursive and repeats until all deep dependencies have been found
* @param {string[]} toFind list of libraries to find the dependencies of
* @param {string[]} alreadyFound the dependencies that have already been found
*/
async loadDependencies(toFind: string[], alreadyFound: string[]) {
// console.log(`loading dependency level: ${dependencyDepth}`);
// dependencyDepth++;
const dependencies: any[] = alreadyFound;
const findNext = [];
const newDependencies = await Promise.all(toFind.map((libraryName) => this.findLibraryDependencies(libraryName)));
// loop over newly found libraries
newDependencies.forEach((library: any) => {
// push into found list