Newer
Older
import {ReviewDialogComponent} from './review-dialog/review-dialog.component';
import {MainDataService} from '../maindata.service';
import {BackendService} from './backend.service';
import {TestControllerService} from './test-controller.service';
import {Component, Inject, OnDestroy, OnInit} from '@angular/core';
import {EnvironmentData, MaxTimerData, Testlet, UnitDef} from './test-controller.classes';
TestData,
TestStatus,
UnitData,
UnitNavigationTarget
import {from, Observable, of, Subscription, throwError} from 'rxjs';
import {concatMap, map, switchMap} from 'rxjs/operators';
import {CustomtextService} from 'iqb-components';
import {MatDialog} from "@angular/material/dialog";
import {MatSnackBar} from '@angular/material/snack-bar';
import {BookletConfig} from "../config/booklet-config";
import { TestMode } from '../config/test-mode';
templateUrl: './test-controller.component.html',
styleUrls: ['./test-controller.component.css']
export class TestControllerComponent implements OnInit, OnDestroy {
static localStorageTestKey = 'iqb-tc-t';
private errorReportingSubscription: Subscription = null;
private testStatusSubscription: Subscription = null;
private unitLoadXmlSubscription: Subscription = null;
private unitLoadBlobSubscription: Subscription = null;
private timerValue: MaxTimerData = null;
private timerRunning = false;
unitNavigationTarget = UnitNavigationTarget;
isTopMargin = true;
isBottomMargin = true;
@Inject('APP_VERSION') public appVersion: string,
@Inject('IS_PRODUCTION_MODE') public isProductionMode,
public tcs: TestControllerService,
private bs: BackendService,
private reviewDialog: MatDialog,

Martin Mechtel
committed
private router: Router,

Martin Mechtel
committed
private cts: CustomtextService

Martin Mechtel
committed
private static getChildElements(element) {
return Array.prototype.slice.call(element.childNodes)
.filter(function (e) { return e.nodeType === 1; });
}
// ''''''''''''''''''''''''''''''''''''''''''''''''''''
// private: recursive reading testlets/units from xml
// ''''''''''''''''''''''''''''''''''''''''''''''''''''
private addTestletContentFromBookletXml(targetTestlet: Testlet, node: Element) {

Martin Mechtel
committed
const childElements = TestControllerComponent.getChildElements(node);
if (childElements.length > 0) {
let codeToEnter = '';
let codePrompt = '';
let restrictionElement: Element = null;
for (let childIndex = 0; childIndex < childElements.length; childIndex++) {
if (childElements[childIndex].nodeName === 'Restrictions') {
restrictionElement = childElements[childIndex];
break;
}
}
if (restrictionElement !== null) {

Martin Mechtel
committed
const restrictionElements = TestControllerComponent.getChildElements(restrictionElement);
for (let childIndex = 0; childIndex < restrictionElements.length; childIndex++) {
if (restrictionElements[childIndex].nodeName === 'CodeToEnter') {
const restrictionParameter = restrictionElements[childIndex].getAttribute('parameter');
if ((typeof restrictionParameter !== 'undefined') && (restrictionParameter !== null)) {
codeToEnter = restrictionParameter.toUpperCase();
codePrompt = restrictionElements[childIndex].textContent;
}
} else if (restrictionElements[childIndex].nodeName === 'TimeMax') {
const restrictionParameter = restrictionElements[childIndex].getAttribute('parameter');
if ((typeof restrictionParameter !== 'undefined') && (restrictionParameter !== null)) {
maxTime = Number(restrictionParameter);
if (isNaN(maxTime)) {
maxTime = -1;
}
}
}
}
}
if (codeToEnter.length > 0) {
targetTestlet.codeToEnter = codeToEnter;
targetTestlet.codePrompt = codePrompt;
}
if (this.tcs.LastMaxTimerState) {
if (this.tcs.LastMaxTimerState.hasOwnProperty(targetTestlet.id)) {
targetTestlet.maxTimeLeft = this.tcs.LastMaxTimerState[targetTestlet.id];
}
}
for (let childIndex = 0; childIndex < childElements.length; childIndex++) {
if (childElements[childIndex].nodeName === 'Unit') {
const myUnitId = childElements[childIndex].getAttribute('id');
let myUnitAlias = childElements[childIndex].getAttribute('alias');
if (!myUnitAlias) {
myUnitAlias = myUnitId;
}
let myUnitAliasClear = myUnitAlias;
let unitIdSuffix = 1;
while (this.allUnitIds.indexOf(myUnitAliasClear) > -1) {
myUnitAliasClear = myUnitAlias + '-' + unitIdSuffix.toString();
unitIdSuffix += 1;
}
this.allUnitIds.push(myUnitAliasClear);
targetTestlet.addUnit(this.lastUnitSequenceId, myUnitId,
childElements[childIndex].getAttribute('label'), myUnitAliasClear,
childElements[childIndex].getAttribute('labelshort'));
this.lastUnitSequenceId += 1;
} else if (childElements[childIndex].nodeName === 'Testlet') {
let testletId: string = childElements[childIndex].getAttribute('id');
testletId = 'Testlet' + this.lastTestletIndex.toString();
this.lastTestletIndex += 1;
}
let testletLabel: string = childElements[childIndex].getAttribute('label');
testletLabel = testletLabel ? testletLabel.trim() : '';
this.addTestletContentFromBookletXml(targetTestlet.addTestlet(testletId, testletLabel), childElements[childIndex]);
// ''''''''''''''''''''''''''''''''''''''''''''''''''''
// private: reading booklet from xml
// ''''''''''''''''''''''''''''''''''''''''''''''''''''
private getBookletFromXml(xmlString: string, loginMode: string): Testlet {
let rootTestlet: Testlet = null;
try {
const oParser = new DOMParser();
const oDOM = oParser.parseFromString(xmlString, 'text/xml');
if (oDOM.documentElement.nodeName === 'Booklet') {
// ________________________
const metadataElements = oDOM.documentElement.getElementsByTagName('Metadata');
if (metadataElements.length > 0) {
const metadataElement = metadataElements[0];
const IdElement = metadataElement.getElementsByTagName('Id')[0];
const LabelElement = metadataElement.getElementsByTagName('Label')[0];
rootTestlet = new Testlet(0, IdElement.textContent, LabelElement.textContent);
const unitsElements = oDOM.documentElement.getElementsByTagName('Units');
if (unitsElements.length > 0) {
const customTextsElements = oDOM.documentElement.getElementsByTagName('CustomTexts');
if (customTextsElements.length > 0) {
const customTexts = TestControllerComponent.getChildElements(customTextsElements[0]);
const customTextsForBooklet = {};
for (let childIndex = 0; childIndex < customTexts.length; childIndex++) {
if (customTexts[childIndex].nodeName === 'Text') {
const customTextKey = customTexts[childIndex].getAttribute('key');
if ((typeof customTextKey !== 'undefined') && (customTextKey !== null)) {
customTextsForBooklet[customTextKey] = customTexts[childIndex].textContent;
this.cts.addCustomTexts(customTextsForBooklet);
const bookletConfigElements = oDOM.documentElement.getElementsByTagName('BookletConfig');
this.tcs.bookletConfig = new BookletConfig();
this.tcs.bookletConfig.setFromKeyValuePairs(MainDataService.getTestConfig());
this.tcs.bookletConfig.setFromXml(bookletConfigElements[0]);
this.tcs.testMode = new TestMode(loginMode);
// recursive call through all testlets
this.lastUnitSequenceId = 1;
this.lastTestletIndex = 1;
this.addTestletContentFromBookletXml(rootTestlet, unitsElements[0]);
console.log('error reading booklet XML:');
console.log(error);
rootTestlet = null;
}
return rootTestlet;
}
private incrementProgressValueBy1() {
this.loadedUnitCount += 1;
this.loadProgressValue = this.loadedUnitCount * 100 / this.lastUnitSequenceId;
// ''''''''''''''''''''''''''''''''''''''''''''''''''''
// private: read unitdata
// ''''''''''''''''''''''''''''''''''''''''''''''''''''
private loadUnitOk (myUnit: UnitDef, sequenceId: number): Observable<number> {
myUnit.setCanEnter('n', 'Fehler beim Laden');
if (myData === false) {
return throwError(`error requesting unit ${this.tcs.testId}/${myUnit.id}`);
} else {
const myUnitData = myData as UnitData;
if (myUnitData.restorepoint) {
this.tcs.newUnitRestorePoint(myUnit.id, sequenceId, JSON.parse(myUnitData.restorepoint), false);
}
let definitionRef = '';
try {
const oParser = new DOMParser();
const oDOM = oParser.parseFromString(myUnitData.xml, 'text/xml');
if (oDOM.documentElement.nodeName === 'Unit') {
const defElements = oDOM.documentElement.getElementsByTagName('Definition');
if (defElements.length > 0) {
const defElement = defElements[0];
this.tcs.addUnitDefinition(sequenceId, defElement.textContent);
} else {
const defRefElements = oDOM.documentElement.getElementsByTagName('DefinitionRef');
if (defRefElements.length > 0) {
const defRefElement = defRefElements[0];
definitionRef = defRefElement.textContent;
// this.tcs.addUnitDefinition(sequenceId, '');
return throwError(`error parsing unit def ${this.tcs.testId}/${myUnit.id} (${error.toString()})`);
if (definitionRef.length > 0) {
this.unitLoadQueue.push(<TaggedString>{
tag: sequenceId.toString(),
value: definitionRef
});
}
myUnit.setCanEnter('y', '');
if (this.tcs.hasPlayer(playerId)) {
return of(sequenceId)
} else {
// to avoid multiple calls before returning:
this.tcs.addPlayer(playerId, '');
return this.bs.getResource(this.tcs.testId, '', this.tcs.normaliseId(playerId, 'html'), true)
.pipe(
switchMap(myData => {
if (typeof myData === 'number') {
return throwError(`error getting player "${playerId}"`);
} else {
const player = myData as TaggedString;
if (player.value.length > 0) {
this.tcs.addPlayer(playerId, player.value);
return of(sequenceId);
} else {
return throwError(`error getting player "${playerId}" (size = 0)`);
}
}
}));
}
return throwError(`player def missing for unit ${this.tcs.testId}/${myUnit.id}`);
// #####################################################################################
// #####################################################################################
this.mds.progressVisualEnabled = false;
if (this.isProductionMode && this.tcs.testMode.saveResponses) {
this.mds.errorReportingSilent = true;
}
this.errorReportingSubscription = this.mds.appError$.subscribe(e => {
if (this.isProductionMode && this.tcs.testMode.saveResponses) {
console.error(e.label + " / " + e.description);
}
this.tcs.testStatus$.next(TestStatus.ERROR);
});
this.testStatusSubscription = this.tcs.testStatus$.subscribe(ts => {
switch (ts) {
case TestStatus.ERROR:
this.tcs.setUnitNavigationRequest(UnitNavigationTarget.ERROR);
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
break;
case TestStatus.PAUSED:
this.tcs.setUnitNavigationRequest(UnitNavigationTarget.PAUSE);
break;
}
});
this.routingSubscription = this.route.params.subscribe(params => {
console.log(this.tcs.testStatus$.getValue());
if (this.tcs.testStatus$.getValue() !== TestStatus.ERROR) {
this.tcs.testId = params['t'];
localStorage.setItem(TestControllerComponent.localStorageTestKey, params['t']);
this.unsubscribeTestSubscriptions();
this.maxTimerSubscription = this.tcs.maxTimeTimer$.subscribe(maxTimerData => {
if (maxTimerData.type === MaxTimerDataType.STARTED) {
this.snackBar.open(this.cts.getCustomText('booklet_msgTimerStarted') + maxTimerData.timeLeftMinString, '', {duration: 3000});
this.timerValue = maxTimerData;
} else if (maxTimerData.type === MaxTimerDataType.ENDED) {
this.snackBar.open(this.cts.getCustomText('booklet_msgTimeOver'), '', {duration: 3000});
this.tcs.rootTestlet.setTimeLeftNull(maxTimerData.testletId);
this.tcs.LastMaxTimerState[maxTimerData.testletId] = 0;
this.tcs.setBookletState(LastStateKey.MAXTIMELEFT, JSON.stringify(this.tcs.LastMaxTimerState));
this.timerRunning = false;
this.timerValue = null;
if (this.tcs.testMode.forceTimeRestrictions) {
this.tcs.setUnitNavigationRequest(UnitNavigationTarget.NEXT);
} else if (maxTimerData.type === MaxTimerDataType.CANCELLED) {
this.snackBar.open(this.cts.getCustomText('booklet_msgTimerCancelled'), '', {duration: 3000});
this.tcs.rootTestlet.setTimeLeftNull(maxTimerData.testletId);
this.tcs.LastMaxTimerState[maxTimerData.testletId] = 0;
this.tcs.setBookletState(LastStateKey.MAXTIMELEFT, JSON.stringify(this.tcs.LastMaxTimerState));
this.timerValue = null;
} else {
this.timerValue = maxTimerData;
if ((maxTimerData.timeLeftSeconds % 15) === 0) {
this.tcs.LastMaxTimerState[maxTimerData.testletId] = Math.round(maxTimerData.timeLeftSeconds / 60);
this.tcs.setBookletState(LastStateKey.MAXTIMELEFT, JSON.stringify(this.tcs.LastMaxTimerState));
}
if ((maxTimerData.timeLeftSeconds / 60) === 5) {
this.snackBar.open(this.cts.getCustomText('booklet_msgSoonTimeOver5Minutes'), '', {duration: 3000});
} else if ((maxTimerData.timeLeftSeconds / 60) === 1) {
this.snackBar.open(this.cts.getCustomText('booklet_msgSoonTimeOver1Minute'), '', {duration: 3000});
});
this.tcs.resetDataStore();
const envData = new EnvironmentData(this.appVersion);
this.tcs.addBookletLog(LogEntryKey.BOOKLETLOADSTART, JSON.stringify(envData));
this.tcs.testStatus$.next(TestStatus.WAITING_LOAD_COMPLETE);
this.loadProgressValue = 0;
this.tcs.loadComplete = false;
this.bs.getTestData(this.tcs.testId).subscribe(testDataUntyped => {
if (testDataUntyped === false) {
label: "Konnte Testinformation nicht laden",
description: "TestController.Component: getTestData()",
const testData = testDataUntyped as TestData;
let navTarget = 1;
if (testData.laststate !== null) {
if (testData.laststate.hasOwnProperty(LastStateKey.LASTUNIT)) {
const navTargetTemp = Number(testData.laststate[LastStateKey.LASTUNIT]);
if (!isNaN(navTargetTemp)) {
navTarget = navTargetTemp;
}
}
if (testData.laststate.hasOwnProperty(LastStateKey.MAXTIMELEFT) && (this.tcs.testMode.saveResponses)) {
this.tcs.LastMaxTimerState = JSON.parse(testData.laststate[LastStateKey.MAXTIMELEFT]);
}
this.tcs.rootTestlet = this.getBookletFromXml(testData.xml, testData.mode);
document.documentElement.style.setProperty('--tc-unit-title-height', this.tcs.bookletConfig.unit_title === 'ON' ? this.mds.defaultTcUnitTitleHeight : '0');
document.documentElement.style.setProperty('--tc-header-height', this.tcs.bookletConfig.unit_screenheader === 'OFF' ? '0' : this.mds.defaultTcHeaderHeight);
document.documentElement.style.setProperty('--tc-unit-page-nav-height', this.tcs.bookletConfig.page_navibuttons === 'SEPARATE_BOTTOM' ? this.mds.defaultTcUnitPageNavHeight : '0');
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
if (this.tcs.rootTestlet === null) {
this.mds.appError$.next({
label: "Problem beim Laden der Testinformation",
description: "TestController.Component: getBookletFromXml(testData.xml)",
category: "PROBLEM"
});
} else {
this.tcs.maxUnitSequenceId = this.lastUnitSequenceId - 1;
this.loadedUnitCount = 0;
const sequArray = [];
for (let i = 1; i < this.tcs.maxUnitSequenceId + 1; i++) {
sequArray.push(i);
}
this.unitLoadXmlSubscription = from(sequArray).pipe(
concatMap(uSequ => {
const ud = this.tcs.rootTestlet.getUnitAt(uSequ);
return this.loadUnitOk(ud.unitDef, uSequ);
})
).subscribe(() => {
this.incrementProgressValueBy1();
},
errorMessage => {
this.mds.appError$.next({
label: "Problem beim Laden der Testinformation",
description: errorMessage,
category: "PROBLEM"
});
},
() => {
this.tcs.rootTestlet.lockUnitsIfTimeLeftNull();
this.tcs.updateMinMaxUnitSequenceId(navTarget);
this.loadedUnitCount = 0;
this.unitLoadBlobSubscription = from(this.unitLoadQueue).pipe(
concatMap(queueEntry => {
const unitSequ = Number(queueEntry.tag);
if (this.tcs.bookletConfig.loading_mode === "EAGER") {
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
this.incrementProgressValueBy1();
}
// avoid to load unit def if not necessary
if (unitSequ < this.tcs.minUnitSequenceId) {
return of(<TaggedString>{tag: unitSequ.toString(), value: ''});
} else {
return this.bs.getResource(this.tcs.testId, queueEntry.tag, queueEntry.value).pipe(
map(response => {
if (typeof response === 'number') {
return throwError(`error loading voud ${this.tcs.testId} / ${queueEntry.tag} / ${queueEntry.value}: status ${response}`)
} else {
return response
}
})
);
}
})
).subscribe(
(def: TaggedString) => {
this.tcs.addUnitDefinition(Number(def.tag), def.value);
},
errorMessage => {
this.mds.appError$.next({
label: "Problem beim Laden der Testinformation",
description: errorMessage,
category: "PROBLEM"
});
},
() => { // complete
this.tcs.addBookletLog(LogEntryKey.BOOKLETLOADCOMPLETE);
this.loadProgressValue = 100;
this.tcs.loadComplete = true;
if (this.tcs.bookletConfig.loading_mode === "EAGER") {
this.tcs.setUnitNavigationRequest(navTarget.toString());
this.tcs.testStatus$.next(TestStatus.RUNNING);
if (this.tcs.bookletConfig.loading_mode === "LAZY") {
this.tcs.setUnitNavigationRequest(navTarget.toString());
this.tcs.testStatus$.next(TestStatus.RUNNING);
} // complete
);
}
}); // getTestData
}
// #####################################################################################
showReviewDialog() {
if (this.tcs.rootTestlet === null) {
this.snackBar.open('Kein Testheft verfügbar.', '', {duration: 3000});
} else {
const authData = MainDataService.getAuthData();
const dialogRef = this.reviewDialog.open(ReviewDialogComponent, {
width: '700px',
unitTitle: this.tcs.currentUnitTitle,
unitDbKey: this.tcs.currentUnitDbKey
}
});
dialogRef.afterClosed().subscribe(result => {
if (typeof result !== 'undefined') {
if (result !== false) {
const targetSelection = result['target'];
this.tcs.testId,
this.tcs.currentUnitDbKey,
result['priority'],
dialogRef.componentInstance.getCategories(),
result['sender'] ? result['sender'] + ': ' + result['entry'] : result['entry']
).subscribe(ok => {
if (!ok) {
this.snackBar.open('Konnte Kommentar nicht speichern', '', {duration: 3000});
this.snackBar.open('Kommentar gespeichert', '', {duration: 1000});
result['sender'] ? result['sender'] + ': ' + result['entry'] : result['entry']
).subscribe(ok => {
if (!ok) {
this.snackBar.open('Konnte Kommentar nicht speichern', '', {duration: 3000});
this.snackBar.open('Kommentar gespeichert', '', {duration: 1000});
if (this.maxTimerSubscription !== null) {
this.maxTimerSubscription.unsubscribe();
this.maxTimerSubscription = null
if (this.unitLoadXmlSubscription !== null) {
this.unitLoadXmlSubscription.unsubscribe();
this.unitLoadXmlSubscription = null
if (this.unitLoadBlobSubscription !== null) {
this.unitLoadBlobSubscription.unsubscribe();
this.unitLoadBlobSubscription = null
topMargin() {
this.isTopMargin = !this.isTopMargin;
}
bottomMargin() {
this.isBottomMargin = !this.isBottomMargin;
}
// % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % % %
ngOnDestroy() {
if (this.routingSubscription !== null) {
this.routingSubscription.unsubscribe();
}
if (this.errorReportingSubscription !== null) {
this.errorReportingSubscription.unsubscribe();
}
if (this.testStatusSubscription !== null) {
this.testStatusSubscription.unsubscribe();
}
this.mds.progressVisualEnabled = true;
this.mds.errorReportingSilent = false;