diff --git a/src/app/group-monitor/backend.service.ts b/src/app/group-monitor/backend.service.ts index b1e81f1b14092dd9c1ccfc6f13dc8f5d9ac2eb7b..cd648c9f816adbb13c03c63433d358de45e0d81b 100644 --- a/src/app/group-monitor/backend.service.ts +++ b/src/app/group-monitor/backend.service.ts @@ -1,8 +1,11 @@ -import {Injectable} from '@angular/core'; -import {BehaviorSubject, Observable} from 'rxjs'; +import {Inject, Injectable} from '@angular/core'; +import {BehaviorSubject, Observable, of} from 'rxjs'; import {webSocket, WebSocketSubject} from 'rxjs/webSocket'; import {WebSocketMessage} from 'rxjs/internal/observable/dom/WebSocketSubject'; -import {filter, map, share} from 'rxjs/operators'; +import {catchError, filter, map, share} from 'rxjs/operators'; +import {TestData} from '../test-controller/test-controller.interfaces'; +import {ApiError} from '../app.interfaces'; +import {HttpClient} from '@angular/common/http'; interface WsMessage { @@ -22,7 +25,10 @@ export class BackendService { public serviceConnected$ = new BehaviorSubject<boolean>(undefined); - constructor() { + constructor( + @Inject('SERVER_URL') private serverUrl: string, + private http: HttpClient + ) { this.serviceConnected$ .pipe(filter((value: boolean) => (value !== undefined))) @@ -121,4 +127,35 @@ export class BackendService { .pipe(map((event: WsMessage): T => event.data)) .pipe(share()); } + + + // === non websocket stuff -> TODO move to separate service + + getTestData(testId: string): Observable<TestData | boolean> { + + console.log("load booklet for " + testId); + + return this.http + .get<TestData>(this.serverUrl + 'test/' + testId) + .pipe( + catchError((err: ApiError) => { + console.warn(`getTestData Api-Error: ${err.code} ${err.info} `); + return of(false) + }) + ); + + //const loadingTestData = new BehaviorSubject<TestData | boolean>(true); + // const TODO_unsubscribeMe = this.http + // .get<TestData>(this.serverUrl + 'test/' + testId) + // .pipe( + // catchError((err: ApiError) => { + // console.warn(`getTestData Api-Error: ${err.code} ${err.info} `); + // return of(false) + // }) + // ) + // .subscribe(loadingTestData); + // return loadingTestData; + } + + } diff --git a/src/app/group-monitor/booklet.service.ts b/src/app/group-monitor/booklet.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..25486a102a061f7d2ba72219787b9eb33f940730 --- /dev/null +++ b/src/app/group-monitor/booklet.service.ts @@ -0,0 +1,234 @@ +import {Injectable} from '@angular/core'; +import {Testlet} from '../test-controller/test-controller.classes'; +import {BookletConfig} from '../config/booklet-config'; +import {MainDataService} from '../maindata.service'; +import {BackendService} from './backend.service'; +import {BehaviorSubject, of} from 'rxjs'; +import {isDefined} from '@angular/compiler/src/util'; +import {TestData} from '../test-controller/test-controller.interfaces'; +import {map} from 'rxjs/operators'; + +// TODO find a solution for shared classes + +export interface Booklet { + lastUnitSequenceId: number; + lastTestletIndex: number; + allUnitIds; + testlet: Testlet + config: BookletConfig +} + + + +@Injectable() +export class BookletService { + + + public booklets: BehaviorSubject<Booklet>[] = []; + + + constructor( + private bs: BackendService + ) { } + + + public getBooklet(testId: string): BehaviorSubject<Booklet|boolean> { + + if (isDefined(this.booklets[testId])) { + + console.log('FORWARDING testlet data for ' + testId + ''); + return this.booklets[testId]; + } + + if (parseInt(testId) < 1) { + + this.booklets[testId] = of(null); + + } else { + + console.log('LOADING testlet data for ' + testId + ' not available. loading'); + + const loadingTestData = new BehaviorSubject<Booklet|boolean>(true); + const TODO_unsubscribeMe = this.bs.getTestData(testId) + .pipe(map((testData: TestData): string => testData.xml)) + .pipe(map(BookletService.getBookletFromXml)) + .subscribe(loadingTestData); + + this.booklets[testId] = loadingTestData; + } + + return this.booklets[testId]; + } + + + // TODO those functions are more or less copies from test.controller.component. avoid duplicate doce + private static getBookletFromXml(xmlString: string): Booklet|boolean { + + let rootTestlet: Testlet = null; + let booklet: Booklet = 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 = BookletService.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'); + + const bookletConfig = new BookletConfig(); + + bookletConfig.setFromKeyValuePairs(MainDataService.getTestConfig()); + if (bookletConfigElements.length > 0) { + bookletConfig.setFromXml(bookletConfigElements[0]); + } + + // this.tcs.testMode = new TestMode(loginMode); + + // recursive call through all testlets + + booklet = { + lastUnitSequenceId: 1, + lastTestletIndex: 1, + allUnitIds: [], + testlet: rootTestlet, + config: bookletConfig + }; + + BookletService.addTestletContentFromBookletXml(booklet, unitsElements[0]); + } + } + } + } catch (error) { + console.log('error reading booklet XML:'); + console.log(error); + + booklet = null; + } + + if (booklet == null) { + return false; + } + + return booklet; + } + + + private static getChildElements(element) { + return Array.prototype.slice.call(element.childNodes) + .filter(function (e) { return e.nodeType === 1; }); + } + + + private static addTestletContentFromBookletXml(booklet: Booklet, node: Element) { + + const targetTestlet = booklet.testlet; + + const childElements = BookletService.getChildElements(node); + if (childElements.length > 0) { + let codeToEnter = ''; + let codePrompt = ''; + let maxTime = -1; + + let restrictionElement: Element = null; + for (let childIndex = 0; childIndex < childElements.length; childIndex++) { + if (childElements[childIndex].nodeName === 'Restrictions') { + restrictionElement = childElements[childIndex]; + break; + } + } + if (restrictionElement !== null) { + const restrictionElements = BookletService.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; + } + targetTestlet.maxTimeLeft = maxTime; + // 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 (booklet.allUnitIds.indexOf(myUnitAliasClear) > -1) { + myUnitAliasClear = myUnitAlias + '-' + unitIdSuffix.toString(); + unitIdSuffix += 1; + } + booklet.allUnitIds.push(myUnitAliasClear); + + targetTestlet.addUnit(booklet.lastUnitSequenceId, myUnitId, + childElements[childIndex].getAttribute('label'), myUnitAliasClear, + childElements[childIndex].getAttribute('labelshort')); + booklet.lastUnitSequenceId += 1; + + } else if (childElements[childIndex].nodeName === 'Testlet') { + let testletId: string = childElements[childIndex].getAttribute('id'); + if (!testletId) { + testletId = 'Testlet' + booklet.lastTestletIndex.toString(); + booklet.lastTestletIndex += 1; + } + let testletLabel: string = childElements[childIndex].getAttribute('label'); + testletLabel = testletLabel ? testletLabel.trim() : ''; + + booklet.testlet.addTestlet(testletId, testletLabel); + this.addTestletContentFromBookletXml(booklet, childElements[childIndex]); + } + } + } + } + + +} + + + + diff --git a/src/app/group-monitor/group-monitor.component.html b/src/app/group-monitor/group-monitor.component.html index 3210c931635d1f5da645e16e7e7d6c6426100817..55d40ebd7f4ed328a48fc32e7172aa6239f95ddd 100644 --- a/src/app/group-monitor/group-monitor.component.html +++ b/src/app/group-monitor/group-monitor.component.html @@ -62,6 +62,13 @@ </td> </ng-container> + <ng-container matColumnDef="booklet"> + <th mat-header-cell *matHeaderCellDef>Debug</th> + <td mat-cell *matCellDef="let element"> + <pre>getBookletInfo(element).getValue() | json}}</pre> + </td> + </ng-container> + <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> diff --git a/src/app/group-monitor/group-monitor.component.ts b/src/app/group-monitor/group-monitor.component.ts index 83a13a53a00fb42a169bab9c52906689fdccd12b..08282ed23aeb57222c55056a99a6a424268d2669 100644 --- a/src/app/group-monitor/group-monitor.component.ts +++ b/src/app/group-monitor/group-monitor.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from '@angular/core'; import {BackendService} from './backend.service'; -import {Observable} from 'rxjs'; +import {Observable, of} from 'rxjs'; import {StatusUpdate} from './group-monitor.interfaces'; +import {Booklet, BookletService} from './booklet.service'; @Component({ selector: 'app-group-monitor', @@ -10,13 +11,14 @@ import {StatusUpdate} from './group-monitor.interfaces'; }) export class GroupMonitorComponent implements OnInit { - constructor(private bs: BackendService) { + constructor( + private bs: BackendService, + private bookletsService: BookletService, + ) {} - } - - displayedColumns: string[] = ['status', 'name', 'personStatus', 'test', 'testStatus', 'unit', 'unitStatus']; + displayedColumns: string[] = ['status', 'name', 'personStatus', 'test', 'testStatus', 'unit', 'unitStatus', 'booklet']; - dataSource$: Observable<any>; + dataSource$: Observable<StatusUpdate[]>; clientCount$: Observable<number>; serviceConnected$: Observable<boolean>; @@ -34,6 +36,18 @@ export class GroupMonitorComponent implements OnInit { this.dataSource$ = this.bs.observe<StatusUpdate[]>('status'); + this.dataSource$.subscribe((status: StatusUpdate[]) => { + status.forEach((statusUpate: StatusUpdate) => this.getBookletInfo(statusUpate)); + }); } + getBookletInfo(status: StatusUpdate): Observable<Booklet|boolean> { + + if ((typeof status.testState["status"] !== "undefined") && (status.testState["status"] === "locked")) { + console.log('no need to load locked booklet', status.testId); + return of(null); + } + + return this.bookletsService.getBooklet(status.testId.toString()); + } } diff --git a/src/app/group-monitor/group-monitor.interfaces.ts b/src/app/group-monitor/group-monitor.interfaces.ts index fa15d26abfa32e0e88ded4de2d018be0c28c8e16..eef09db408e92998aac26e8399286d2cd8e440fb 100644 --- a/src/app/group-monitor/group-monitor.interfaces.ts +++ b/src/app/group-monitor/group-monitor.interfaces.ts @@ -10,8 +10,6 @@ export interface StatusUpdate { testState: { [testStateKey: string]: string }; - testStateKey?: string; - testStateValue?: string; unitName?: string; unitLabel?: string; unitState: { diff --git a/src/app/group-monitor/group-monitor.module.ts b/src/app/group-monitor/group-monitor.module.ts index 4ecec36fbaa2d9f17f9ee24959105c296cba9e04..a3d06ff78e8cee2433f9ff487b56bc54ea8d31d1 100644 --- a/src/app/group-monitor/group-monitor.module.ts +++ b/src/app/group-monitor/group-monitor.module.ts @@ -10,6 +10,7 @@ import { MatChipsModule } from "@angular/material/chips"; import { CdkTableModule } from '@angular/cdk/table'; import {BackendService} from './backend.service'; +import {BookletService} from './booklet.service'; @@ -27,7 +28,8 @@ import {BackendService} from './backend.service'; MatChipsModule ], providers: [ - BackendService + BackendService, + BookletService ], }) export class GroupMonitorModule {