Commit 57a15c03 authored by Konstantin Schulz's avatar Konstantin Schulz

added a new curated exercise by Potsdam University

parent 7de52892
Pipeline #14594 passed with stages
in 2 minutes and 52 seconds
......@@ -358,7 +358,7 @@ class McTestCase(unittest.TestCase):
DatabaseService.commit()
response = Mocks.app_dict[self.class_name].client.post(
TestingConfig.SERVER_URI_H5P, headers=Mocks.headers_form_data, data=hf.to_dict())
self.assertEqual(len(response.get_data()), 1940145)
self.assertEqual(len(response.get_data()), 1963607)
with patch.object(mcserver.app.api.h5pAPI, "get_text_field_content", return_value=""):
response = Mocks.app_dict[self.class_name].client.post(
TestingConfig.SERVER_URI_H5P, headers=Mocks.headers_form_data, data=hf.to_dict())
......
......@@ -2,7 +2,7 @@
1. Clone the repo: `git clone https://scm.cms.hu-berlin.de/callidus/machina-callida.git`
2. Move to the newly created folder: `cd machina-callida/mc_frontend`
3. Run `npm install`
4. Run `npm install -g @angular/cli` (you may need `sudo`).
4. Run `npm install -g @angular/cli` (you may need `sudo`). Optional: Install the Ionic CLI for additional command-line utilities: `npm i -g @ionic/cli`.
5. Check that the Angular command line interface is installed by running `ng --version`. It should print the version of the Angular CLI.
6. Run `npm start`.
If you already ran `npm install` and the CLI still complains about missing dependencies, install them one by one using `npm install DEPENDENCY_NAME`.
......
......@@ -30,6 +30,10 @@ export const routes: Routes = [
path: 'embed',
loadChildren: () => import('./embed/embed.module').then( m => m.EmbedPageModule)
},
{
path: 'sequences',
loadChildren: () => import('./sequences/sequences.module').then( m => m.SequencesPageModule)
},
......
......@@ -25,6 +25,7 @@ import {LoadChildrenCallback, Route} from '@angular/router';
import configMC from '../configMC';
import {SemanticsPageModule} from './semantics/semantics.module';
import {EmbedPageModule} from './embed/embed.module';
import {SequencesPageModule} from './sequences/sequences.module';
describe('AppComponent', () => {
let statusBarSpy, splashScreenSpy, platformReadySpy, fixture: ComponentFixture<AppComponent>,
......@@ -105,8 +106,9 @@ describe('AppComponent', () => {
});
it('should test routing', (done) => {
const urls: string[] = [configMC.pageUrlEmbed.slice(1), configMC.pageUrlSemantics.slice(1)];
const modules: any[] = [EmbedPageModule, SemanticsPageModule];
const urls: string[] = [configMC.pageUrlEmbed.slice(1), configMC.pageUrlSemantics.slice(1),
configMC.pageUrlSequences.slice(1)];
const modules: any[] = [EmbedPageModule, SemanticsPageModule, SequencesPageModule];
let doneCount = 0;
new Promise(resolve => {
urls.forEach((url, index) => {
......
......@@ -74,7 +74,7 @@ describe('CorpusService', () => {
corpusService.checkAnnisResponse().then(() => {
}, () => {
expect(corpusService.annisResponse).toBeFalsy();
helperService.applicationState.next(new ApplicationState());
helperService.applicationState.next(new ApplicationState({mostRecentSetup: new TextData()}));
corpusService.checkAnnisResponse().then(() => {
}, () => {
expect(corpusService.annisResponse).toBeFalsy();
......
......@@ -122,15 +122,11 @@ export class CorpusService {
return resolve();
}
this.helperService.applicationState.pipe(take(1)).subscribe((state: ApplicationState) => {
if (state.mostRecentSetup) {
this.annisResponse = state.mostRecentSetup.annisResponse;
this.currentAuthor = state.mostRecentSetup.currentAuthor;
this.currentUrn = state.mostRecentSetup.currentUrn;
this.currentCorpusCache = state.mostRecentSetup.currentCorpus;
return resolve();
} else {
return reject();
}
return this.annisResponse ? resolve() : reject();
}, () => {
return reject();
});
......
......@@ -31,7 +31,7 @@ describe('EmbedPage', () => {
beforeEach(() => {
fixture = TestBed.createComponent(EmbedPage);
embedPage = fixture.componentInstance;
loadExerciseSpy = spyOn(embedPage, 'loadExercise').and.returnValue(Promise.resolve());
loadExerciseSpy = spyOn(embedPage.exerciseService, 'loadExerciseFromQueryParams').and.returnValue(Promise.resolve());
fixture.detectChanges();
});
......@@ -46,10 +46,12 @@ describe('EmbedPage', () => {
it('should load an exercise', (done) => {
loadExerciseSpy.and.callThrough();
const loadH5Pspy: Spy = spyOn(embedPage.exerciseService, 'loadH5P').and.returnValue(Promise.resolve());
embedPage.loadExercise().then(() => {
const loadH5Pspy: Spy = spyOn(embedPage.exerciseService, 'loadExercise').and.returnValue(Promise.resolve());
embedPage.exerciseService.loadExerciseFromQueryParams(embedPage.activatedRoute).then(() => {
expect(loadH5Pspy).toHaveBeenCalledTimes(1);
loadH5Pspy.and.callFake(() => Promise.reject());
embedPage.loadExercise().then(() => {}, () => {
embedPage.exerciseService.loadExerciseFromQueryParams(embedPage.activatedRoute).then(() => {
}, () => {
expect(loadH5Pspy).toHaveBeenCalledTimes(2);
done();
});
......
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {ExerciseService} from '../exercise.service';
import {ExerciseParams} from '../models/exerciseParams';
@Component({
selector: 'app-embed',
......@@ -14,21 +13,9 @@ export class EmbedPage implements OnInit {
public exerciseService: ExerciseService) {
}
loadExercise(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.activatedRoute.queryParams.subscribe((params: ExerciseParams) => {
this.exerciseService.loadH5P(params.eid).then(() => {
return resolve();
}, () => {
return reject();
});
});
});
}
ngOnInit(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.loadExercise().then(() => {
this.exerciseService.loadExerciseFromQueryParams(this.activatedRoute).then(() => {
return resolve();
}, () => {
return reject();
......
......@@ -8,11 +8,13 @@ import {TranslateTestingModule} from './translate-testing/translate-testing.modu
import {ExercisePart} from './models/exercisePart';
import MockMC from './models/mockMC';
import {ApplicationState} from './models/applicationState';
import {AnnisResponse, ExerciseTypePath} from '../../openapi';
import {ExerciseType, MoodleExerciseType} from './models/enum';
import {AnnisResponse, ExerciseTypePath, Solution} from '../../openapi';
import {ExerciseType, LanguageShortcut, MoodleExerciseType} from './models/enum';
import {ExerciseParams} from './models/exerciseParams';
import configMC from '../configMC';
import Spy = jasmine.Spy;
import {ActivatedRoute} from '@angular/router';
import {of} from 'rxjs';
declare var H5PStandalone: any;
......@@ -76,24 +78,29 @@ describe('ExerciseService', () => {
const postSpy: Spy = spyOn(exerciseService.helperService, 'makePostRequest').and.callFake(() => Promise.resolve(new Blob()));
const downloadSpy: Spy = spyOn(exerciseService, 'downloadBlobAsFile');
exerciseService.corpusService.annisResponse = {exercise_id: ''};
exerciseService.corpusService.currentSolutions = exerciseService.corpusService.annisResponse.solutions = [{
exerciseService.corpusService.exercise.type = ExerciseType.markWords;
exerciseService.currentExerciseLanguage = LanguageShortcut.English;
exerciseService.downloadH5Pexercise().then(() => {
expect(downloadSpy).toHaveBeenCalledTimes(1);
exerciseService.corpusService.currentSolutions =
exerciseService.corpusService.annisResponse.solutions = [{
target: {
token_id: 1,
sentence_id: 1
}
}];
exerciseService.corpusService.exercise.type = ExerciseType.markWords;
exerciseService.downloadH5Pexercise().then(() => {
expect(downloadSpy).toHaveBeenCalledTimes(1);
expect(downloadSpy).toHaveBeenCalledTimes(2);
postSpy.and.callFake(() => Promise.reject());
exerciseService.corpusService.exercise.type = ExerciseType.cloze;
exerciseService.downloadH5Pexercise().then(() => {
}, () => {
expect(downloadSpy).toHaveBeenCalledTimes(1);
expect(downloadSpy).toHaveBeenCalledTimes(2);
done();
});
});
});
});
it('should get H5P elements', () => {
const getSpy: Spy = spyOn(exerciseService, 'getH5PIframe').and.returnValue(null);
......@@ -107,22 +114,32 @@ describe('ExerciseService', () => {
expect(nodeList.length).toBe(1);
});
it('should get solution indices', () => {
const solution: Solution = {target: {sentence_id: 1, token_id: 1}};
exerciseService.corpusService.annisResponse = {solutions: [solution]};
const result: string = exerciseService.getSolutionIndices([solution]);
expect(result).toContain('0');
});
it('should initialize H5P', (done) => {
let h5pCalled = false;
spyOn(exerciseService, 'createH5Pstandalone').and.callFake(() => new Promise(resolve => {
h5pCalled = true;
return resolve();
}));
exerciseService.initH5P('', false).then(() => {
exerciseService.initH5P('', '', false).then(() => {
expect(h5pCalled).toBe(true);
done();
});
});
it('should load an exercise', (done) => {
const initSpy: Spy = spyOn(exerciseService, 'initH5P').and.returnValue(Promise.resolve());
exerciseService.loadExercise({}).then(() => {
}, () => {
expect(initSpy).toHaveBeenCalledTimes(0);
const ar: AnnisResponse = {exercise_type: MoodleExerciseType.cloze.toString()};
const getSpy: Spy = spyOn(exerciseService.helperService, 'makeGetRequest').and.returnValue(Promise.resolve(ar));
const initSpy: Spy = spyOn(exerciseService, 'initH5P').and.returnValue(Promise.resolve());
exerciseService.helperService.applicationState.next(
exerciseService.helperService.deepCopy(MockMC.applicationState) as ApplicationState);
let ep: ExerciseParams = {eid: 'eid'};
......@@ -132,7 +149,7 @@ describe('ExerciseService', () => {
exerciseService.loadExercise(ep).then(() => {
}, () => {
expect(initSpy).toHaveBeenCalledTimes(1);
ep = {file: '', type: ''};
ep = {file: '', type: '', language: LanguageShortcut.English, showActions: true};
exerciseService.loadExercise(ep).then(() => {
ep = {file: '', type: ExerciseTypePath.VocList};
exerciseService.loadExercise(ep).then(() => {
......@@ -143,15 +160,33 @@ describe('ExerciseService', () => {
});
});
});
});
it('should load an exercise from query params', (done) => {
const loadSpy: Spy = spyOn(exerciseService, 'loadExercise').and.returnValue(Promise.resolve());
const activatedRoute: ActivatedRoute = {queryParams: of({})} as ActivatedRoute;
exerciseService.loadExerciseFromQueryParams(activatedRoute).then(() => {
loadSpy.and.callFake(() => Promise.reject());
exerciseService.loadExerciseFromQueryParams(activatedRoute).then(() => {}, () => {
expect(loadSpy).toHaveBeenCalledTimes(2);
done();
});
});
});
it('should load H5P', (done) => {
const initSpy: Spy = spyOn(exerciseService, 'initH5P').and.returnValue(Promise.resolve());
exerciseService.corpusService.exercise.type = ExerciseType.markWords;
exerciseService.loadH5P('').then(() => {
expect(initSpy).toHaveBeenCalledTimes(1);
exerciseService.excludeOOV = true;
exerciseService.corpusService.currentSolutions = [];
exerciseService.loadH5P('').then(() => {
expect(initSpy).toHaveBeenCalledTimes(2);
done();
});
});
});
it('should set the H5P download event handler', () => {
const downloadSpy: Spy = spyOn(exerciseService, 'downloadH5Pexercise').and.returnValue(Promise.resolve());
......@@ -214,6 +249,12 @@ describe('ExerciseService', () => {
.withArgs(exerciseService.embedTextAreaString).and.returnValue(textarea);
getSpy.withArgs(exerciseService.embedSizeInputString, true).and.returnValue(inputs);
exerciseService.corpusService.annisResponse = {exercise_type: MoodleExerciseType.cloze.toString()};
exerciseService.currentExerciseParams = {eid: ''};
exerciseService.updateEmbedUrl();
expect(textarea.innerHTML).toBeTruthy();
textarea.innerHTML = '';
expect(textarea.innerHTML).toBeFalsy();
exerciseService.currentExerciseParams = {eid: 'eid'};
exerciseService.updateEmbedUrl();
expect(textarea.innerHTML).toBeTruthy();
});
......
......@@ -3,9 +3,9 @@ import {Injectable} from '@angular/core';
import configMC from '../configMC';
import {HelperService} from './helper.service';
import {ExercisePart} from './models/exercisePart';
import {EventMC, ExerciseType, MoodleExerciseType} from './models/enum';
import {EventMC, ExerciseType, LanguageShortcut, MoodleExerciseType} from './models/enum';
import {HttpClient, HttpParams} from '@angular/common/http';
import {AnnisResponse, ExerciseTypePath, H5PForm} from '../../openapi';
import {AnnisResponse, ExerciseTypePath, H5PForm, Solution} from '../../openapi';
import {take} from 'rxjs/operators';
import {ApplicationState} from './models/applicationState';
import {ToastController} from '@ionic/angular';
......@@ -14,6 +14,7 @@ import {TranslateService} from '@ngx-translate/core';
import {Storage} from '@ionic/storage';
import {ExerciseParams} from './models/exerciseParams';
import {DisplayOptions, Options} from './models/h5pStandalone';
import {ActivatedRoute} from '@angular/router';
declare var H5PStandalone: any;
......@@ -38,9 +39,12 @@ export class ExerciseService {
'';
}
public currentExerciseLanguage: string | LanguageShortcut;
public currentExerciseName: string;
public currentExerciseParams: ExerciseParams;
public currentExercisePartIndex: number;
public currentExerciseParts: ExercisePart[] = [];
public currentExerciseTypePath: ExerciseTypePath;
public displayOptions: DisplayOptions = { // Customise the look of the H5P
frame: true,
copyright: true,
......@@ -56,7 +60,9 @@ export class ExerciseService {
public excludeOOV = false;
public h5pContainerString = '.h5p-container';
public h5pIframeString = '.h5p-iframe';
public h5pPathLocal = 'assets/h5p';
public kwicGraphs: string;
public nonH5Pstring = 'nonH5P';
public options: Options = {
frameCss: 'assets/h5p-standalone-master/dist/styles/h5p.css',
frameJs: 'assets/h5p-standalone-master/dist/frame.bundle.js',
......@@ -108,15 +114,12 @@ export class ExerciseService {
downloadH5Pexercise(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const url = `${configMC.backendBaseUrl}${configMC.backendApiH5pPath}`;
const indices: number[] = this.corpusService.currentSolutions.map(
x => this.corpusService.annisResponse.solutions.indexOf(x));
const exerciseTypePath: ExerciseTypePath =
this.corpusService.exercise.type === ExerciseType.markWords ?
ExerciseTypePath.MarkWords : ExerciseTypePath.DragText;
const indices: number[] = this.corpusService.currentSolutions ? this.corpusService.currentSolutions.map(
x => this.corpusService.annisResponse.solutions.indexOf(x)) : [];
const h5pForm: H5PForm = {
eid: this.corpusService.annisResponse.exercise_id,
exercise_type_path: exerciseTypePath,
lang: this.translateService.currentLang,
exercise_type_path: this.currentExerciseTypePath,
lang: this.currentExerciseLanguage.toString(),
solution_indices: indices
};
const formData: FormData = new FormData();
......@@ -125,7 +128,7 @@ export class ExerciseService {
const errorMsg: string = HelperService.generalErrorAlertMessage;
this.helperService.makePostRequest(this.http, this.toastCtrl, url, formData, errorMsg, options)
.then((result: Blob) => {
const fileName = exerciseTypePath + '.h5p';
const fileName = this.currentExerciseTypePath + '.h5p';
this.downloadBlobAsFile(result, fileName);
return resolve();
}, () => {
......@@ -147,10 +150,18 @@ export class ExerciseService {
return document.querySelector(this.h5pIframeString);
}
initH5P(exerciseTypePath: string, showActions: boolean = true): Promise<void> {
getSolutionIndices(solutions: Solution[]): string {
const indices: string[] =
solutions.map(x => this.corpusService.annisResponse.solutions.indexOf(x).toString());
return '&solution_indices=' + indices.join(',');
}
initH5P(exerciseTypePath: ExerciseTypePath | string, url: string, showActions: boolean = true): Promise<void> {
return new Promise((resolve) => {
this.setH5Purl(url);
this.currentExerciseTypePath = exerciseTypePath as ExerciseTypePath;
const el: HTMLDivElement = document.querySelector(this.h5pContainerString);
const h5pLocation = 'assets/h5p/' + exerciseTypePath;
const h5pLocation = this.h5pPathLocal + '/' + exerciseTypePath;
const displayOptions: DisplayOptions = this.helperService.deepCopy(this.displayOptions);
if (!showActions) {
displayOptions.embed = false;
......@@ -169,6 +180,12 @@ export class ExerciseService {
loadExercise(params: ExerciseParams): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!Object.keys(params).length) {
return reject();
}
this.currentExerciseLanguage = params.language ?
params.language.toString() : this.translateService.currentLang;
this.currentExerciseParams = params;
if (params.eid) {
const url: string = configMC.backendBaseUrl + configMC.backendApiExercisePath;
const httpParams: HttpParams = new HttpParams().set('eid', params.eid);
......@@ -186,29 +203,38 @@ export class ExerciseService {
return reject();
});
} else {
const exerciseType: string = params.type;
const exerciseTypePath: string = exerciseType === ExerciseTypePath.VocList ?
ExerciseTypePath.FillBlanks : exerciseType;
const file: string = params.file;
const lang: string = this.translateService.currentLang;
this.storage.set(configMC.localStorageKeyH5P,
this.helperService.baseUrl + '/assets/h5p/' + exerciseType + '/content/' + file + '_' + lang + '.json')
.then();
this.initH5P(exerciseTypePath).then(() => {
let etp: ExerciseTypePath = params.type as ExerciseTypePath;
etp = etp === ExerciseTypePath.VocList ? ExerciseTypePath.FillBlanks : etp;
const url = `${this.helperService.baseUrl}/${this.h5pPathLocal}/${etp}/content/${params.author}/` +
`${params.file}_${this.currentExerciseLanguage}.json`;
this.initH5P(etp, url, params.showActions ? params.showActions : true)
.then(() => {
return resolve();
});
}
});
}
loadExerciseFromQueryParams(activatedRoute: ActivatedRoute) {
return new Promise<void>((resolve, reject) => {
activatedRoute.queryParams.subscribe((params: ExerciseParams) => {
this.loadExercise(params).then(() => {
return resolve();
}, () => {
return reject();
});
});
});
}
loadH5P(eid: string): Promise<void> {
const solutionIndicesString: string = this.excludeOOV ? this.getSolutionIndices(this.corpusService.currentSolutions) : '';
// this will be called via GET request from the h5p standalone javascript library
const url: string = `${configMC.backendBaseUrl}${configMC.backendApiH5pPath}` +
`?eid=${eid}&lang=${this.translateService.currentLang}`;
this.setH5Purl(url);
const exerciseTypePath: string = this.corpusService.exercise.type === ExerciseType.markWords ?
`?eid=${eid}&lang=${this.translateService.currentLang}${solutionIndicesString}`;
const etp: ExerciseTypePath = this.corpusService.exercise.type === ExerciseType.markWords ?
ExerciseTypePath.MarkWords : ExerciseTypePath.DragText;
return this.initH5P(exerciseTypePath);
return this.initH5P(etp, url);
}
setH5PdownloadEventHandler(): void {
......@@ -252,12 +278,22 @@ export class ExerciseService {
updateEmbedUrl(): void {
const embedTextarea: HTMLTextAreaElement = this.getH5Pelements(this.embedTextAreaString);
const baseUrl: string = configMC.frontendBaseUrl + configMC.pageUrlEmbed;
const eid: string = this.corpusService.annisResponse.exercise_id;
const params: string[] = [];
if (this.currentExerciseParams.eid) {
params.push('eid=' + this.currentExerciseParams.eid);
} else {
params.push('author=' + this.currentExerciseParams.author);
params.push('file=' + this.currentExerciseParams.file);
params.push('language=' + this.currentExerciseParams.language);
params.push('type=' + this.currentExerciseParams.type);
}
const inputs: NodeListOf<HTMLInputElement> = this.getH5Pelements(this.embedSizeInputString, true);
const width: string = inputs[0].value;
const height: string = inputs[1].value;
embedTextarea.innerHTML =
`<iframe src="${baseUrl}?eid=${eid}" width="${width}px" height="${height}px" allowfullscreen="allowfullscreen"></iframe>
<script src="https://h5p.org/sites/all/modules/h5p/library/js/h5p-resizer.js" charset="UTF-8"></script>`;
`<iframe src="${baseUrl}?${params.join('&')}" width="${width}px" ` +
`height="${height}px" allowfullscreen="allowfullscreen"></iframe>` +
`<script src="https://h5p.org/sites/all/modules/h5p/library/js/h5p-resizer.js" ` +
`charset="UTF-8"></script>`;
}
}
......@@ -18,5 +18,12 @@
</ion-header>
<ion-content>
<ion-grid *ngIf="!helperService.openRequests.length && !exerciseLoadedSuccess">
<ion-row>
<ion-col>
<h2>{{ 'NO_EXERCISES_FOUND' | translate }}</h2>
</ion-col>
</ion-row>
</ion-grid>
<div class="h5p-container"></div>
</ion-content>
......@@ -48,17 +48,20 @@ describe('ExercisePage', () => {
expect(loadExerciseSpy).toHaveBeenCalledTimes(2);
checkSpy.and.callFake(() => Promise.reject());
exercisePage.ngOnInit().then(() => {
expect(loadExerciseSpy).toHaveBeenCalledTimes(2);
expect(loadExerciseSpy).toHaveBeenCalledTimes(3);
checkSpy.and.callFake(() => Promise.reject());
exercisePage.ngOnInit().then(() => {
expect(loadExerciseSpy).toHaveBeenCalledTimes(4);
done();
});
});
});
});
it('should load an exercise', (done) => {
loadExerciseSpy.and.returnValue(Promise.resolve());
exercisePage.initExercise().then(() => {
exercisePage.exerciseService.loadExerciseFromQueryParams(exercisePage.activatedRoute).then(() => {
loadExerciseSpy.and.callFake(() => Promise.reject());
exercisePage.initExercise().then(() => {
exercisePage.exerciseService.loadExerciseFromQueryParams(exercisePage.activatedRoute).then(() => {
}, () => {
expect(loadExerciseSpy).toHaveBeenCalledTimes(3);
done();
......
......@@ -2,7 +2,6 @@
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {ExerciseService} from 'src/app/exercise.service';
import {ExerciseParams} from '../models/exerciseParams';
import {CorpusService} from '../corpus.service';
import {HelperService} from '../helper.service';
import {NavController} from '@ionic/angular';
......@@ -13,6 +12,7 @@ import {NavController} from '@ionic/angular';
styleUrls: ['./exercise.page.scss'],
})
export class ExercisePage implements OnInit {
exerciseLoadedSuccess = false;
constructor(public activatedRoute: ActivatedRoute,
public exerciseService: ExerciseService,
......@@ -21,28 +21,15 @@ export class ExercisePage implements OnInit {
public navCtrl: NavController) {
}
initExercise(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.activatedRoute.queryParams.subscribe((params: ExerciseParams) => {
this.exerciseService.loadExercise(params).then(() => {
return resolve();
}, () => {
return reject();
});
});
});
}
ngOnInit(): Promise<void> {
return new Promise<void>((resolve) => {
this.corpusService.checkAnnisResponse().then(() => {
this.initExercise().then(() => {
this.corpusService.checkAnnisResponse().finally(() => {
this.exerciseService.loadExerciseFromQueryParams(this.activatedRoute).then(() => {
this.exerciseLoadedSuccess = true;
return resolve();
}, () => {
return resolve();
});
}, () => {
return resolve();
});
});