diff --git a/.gitignore b/.gitignore index 0261a90deccd65899998610b512a0de5128f65b0..51094d15974ca28dbffd1186764f209aa81fae13 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,4 @@ testem.log .DS_Store Thumbs.db - -src/environments/environment.ts .gitignore - diff --git a/Makefile b/Makefile index c045e1b51127d1080cfcc1c292e286516deaae87..e31459f85d939f26aac7d34b3e4a15dde51184a7 100644 --- a/Makefile +++ b/Makefile @@ -13,24 +13,12 @@ down: build: docker-compose -f docker/docker-compose.yml build -# TODO does not wait for server to start and fails -# test: run-detached test-unit test-e2e stop - test-unit: docker-compose -f docker/docker-compose.yml exec testcenter-frontend-dev ng test --watch=false test-e2e: docker-compose -f docker/docker-compose.yml exec testcenter-frontend-dev ng e2e --webdriver-update=false --port 4202 -init-dev-config: - cp src/environments/environment.dev.ts src/environments/environment.ts - -build-image: - docker build --target prod -t iqbberlin/testcenter-frontend -f docker/Dockerfile . - -push-image: - docker push iqbberlin/testcenter-frontend:latest - tag-major: scripts/new_version.py major diff --git a/docker/Dockerfile b/docker/Dockerfile index db5a1091040fde46111ca2186b11989c3e32eb6b..4ae2ab7e5342e60323f1ef76ade506cc40ea0656 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,9 +19,7 @@ RUN npx webdriver-manager update --versions.chrome=`google-chrome --product-vers COPY . /app -RUN ng test --watch=false - -RUN ng build --prod="true" --output-path=dist +RUN ng build --prod EXPOSE 4200 diff --git a/package.json b/package.json index a97675b662c0c2f8c853e6bb5ccb541fba41378a..1e7be35bedff522b85eadffb65a0db6a9170c003 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "itc-ng", - "version": "6.4.0", + "version": "6.5.0", "license": "MIT", "repository": { "type": "git", @@ -10,7 +10,7 @@ "ng": "ng", "start": "ng serve", "build": "ng build", - "test": "ng test", + "test": "ng test --watch=false", "lint": "ng lint", "e2e": "ng e2e" }, diff --git a/src/app/app-root/status-card/status-card.component.html b/src/app/app-root/status-card/status-card.component.html index 354b4fb0c91ecd021e1314b872b8857a4c580e25..14dd57b66bbef2a7b3685fe0b2095e46cdb8b6ae 100644 --- a/src/app/app-root/status-card/status-card.component.html +++ b/src/app/app-root/status-card/status-card.component.html @@ -12,11 +12,16 @@ <p *ngIf="!loginName"><b>Status: Derzeit nicht angemeldet.</b></p> <p style="margin-bottom: 0;"><b>Angaben zu dieser Web-Anwendung:</b></p> <ul style="margin: 0;"> - <li>Interner Programmname: {{ appName }}</li> - <li>Programmversion: {{ appVersion }}</li> <li *ngIf="!isProductionMode">Build-Modus: Dev</li> - <li>Erforderliche Version der Server-Programmierung: {{ apiVersionExpected }}</li> - <li>Unterstützte Version der Verona Interfaces Player Definition: {{ veronaApiVersionSupported }}</li> - <li>Copyright: {{ appPublisher }}</li> + <li>Version {{appVersion}}</li> + <li>API: Version {{mds.apiVersion}}, {{apiVersionExpected}} erforderlich</li> + <li>Broadcasting-Service: {{mds.broadcastingServiceInfo.status}} + <span *ngIf="mds.broadcastingServiceInfo.version"> + - Version {{mds.broadcastingServiceInfo.version}}, + {{mds.broadcastingServiceInfo.versionExpected}} erforderlich + </span> + </li> + <li>Verona Player Interface: Version {{veronaApiVersionSupported}}</li> + <li>Copyright: {{appPublisher}}</li> </ul> </div> diff --git a/src/app/app-root/status-card/status-card.component.ts b/src/app/app-root/status-card/status-card.component.ts index 4c807d1cd527ff07a78dac811e2e5321c13d27db..be3b49809a57293962a6fa60db575426543f51c8 100644 --- a/src/app/app-root/status-card/status-card.component.ts +++ b/src/app/app-root/status-card/status-card.component.ts @@ -9,6 +9,7 @@ import {AuthAccessKeyType, AuthFlagType} from '../../app.interfaces'; export class StatusCardComponent implements OnInit { loginName = ''; loginAuthority: string[] = []; + apiVersion: string; constructor( @Inject('APP_NAME') public appName: string, @@ -16,7 +17,8 @@ export class StatusCardComponent implements OnInit { @Inject('APP_VERSION') public appVersion: string, @Inject('API_VERSION_EXPECTED') public apiVersionExpected: string, @Inject('VERONA_API_VERSION_SUPPORTED') public veronaApiVersionSupported: string, - @Inject('IS_PRODUCTION_MODE') public isProductionMode + @Inject('IS_PRODUCTION_MODE') public isProductionMode, + public mds: MainDataService ) { } ngOnInit(): void { diff --git a/src/app/app.component.ts b/src/app/app.component.ts index ab917c380c7c94996a0c7d77c59b79d9d37926e4..ff6eb29521e5ac049c691839667f023308135b12 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -6,6 +6,7 @@ import { CustomtextService } from 'iqb-components'; import { MainDataService } from './maindata.service'; import { BackendService } from './backend.service'; import { AppError } from './app.interfaces'; +import {BroadCastingServiceInfo, SysConfig} from './config/app.config'; @Component({ selector: 'tc-root', @@ -30,43 +31,32 @@ export class AppComponent implements OnInit, OnDestroy { if (expectedVersion) { const searchPattern = /\d+/g; const expectedVersionNumbers = expectedVersion.match(searchPattern); - if (!expectedVersionNumbers) { - return false; - } - if (!reportedVersion) { - return false; - } - const reportedVersionNumbers = reportedVersion.match(searchPattern); - if (!reportedVersionNumbers) { - return false; - } - if (reportedVersionNumbers[0] !== expectedVersionNumbers[0]) { - return false; - } - if (expectedVersionNumbers.length > 1) { - if ((reportedVersionNumbers.length < 2) || +reportedVersionNumbers[1] < +expectedVersionNumbers[1]) { - return false; - } - if ((expectedVersionNumbers.length > 2) && reportedVersionNumbers[1] === expectedVersionNumbers[1]) { - if ((reportedVersionNumbers.length < 3) || +reportedVersionNumbers[2] < +expectedVersionNumbers[2]) { + if (expectedVersionNumbers) { + if (reportedVersion) { + const reportedVersionNumbers = reportedVersion.match(searchPattern); + if (reportedVersionNumbers) { + if (reportedVersionNumbers[0] !== expectedVersionNumbers[0]) { + return false; + } else if (expectedVersionNumbers.length > 1) { + if ((reportedVersionNumbers.length < 2) || +reportedVersionNumbers[1] < +expectedVersionNumbers[1]) { + return false; + } else if ((expectedVersionNumbers.length > 2) && reportedVersionNumbers[1] === expectedVersionNumbers[1]) { + if ((reportedVersionNumbers.length < 3) || +reportedVersionNumbers[2] < +expectedVersionNumbers[2]) { + return false; + } + } + } + } else { return false; } + } else { + return false; } } } return true; } - private static localTime(date: Date): string { - const year = date.getFullYear(); - const month = (`0${date.getMonth() + 1}`).slice(-2); - const day = (`0${date.getDate()}`).slice(-2); - const hours = (`0${date.getHours()}`).slice(-2); - const minutes = (`0${date.getMinutes()}`).slice(-2); - const seconds = (`0${date.getSeconds()}`).slice(-2); - return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`; - } - closeErrorBox(): void { this.showError = false; } @@ -84,7 +74,7 @@ export class AppComponent implements OnInit, OnDestroy { window.addEventListener('message', (event: MessageEvent) => { const msgData = event.data; - const msgType = msgData.type; + const msgType = msgData['type']; if ((msgType !== undefined) && (msgType !== null)) { if (msgType.substr(0, 2) === 'vo') { this.mds.postMessage$.next(event); @@ -95,38 +85,36 @@ export class AppComponent implements OnInit, OnDestroy { this.setupFocusListeners(); this.bs.getSysConfig().subscribe(sysConfig => { - if (!sysConfig) { - this.mds.isApiValid = false; // push on this.mds.appError$ ? - return; - } - this.cts.addCustomTexts(sysConfig.customTexts); - const authData = MainDataService.getAuthData(); - if (authData) { - this.cts.addCustomTexts(authData.customTexts); - } - this.mds.isApiValid = AppComponent.isValidVersion(this.expectedApiVersion, sysConfig.version); - if (!this.mds.isApiValid) { - this.mds.appError$.next({ - label: 'Server-Problem: API-Version ungültig', - description: `erwartet: ${this.expectedApiVersion}, gefunden: ${sysConfig.version}`, - category: 'FATAL' - }); - } - const clientTime = new Date(); - const serverTime = new Date(sysConfig.serverTimestamp); - if (Math.abs(sysConfig.serverTimestamp - clientTime.getTime()) > 90000) { - this.mds.appError$.next({ - label: 'Server- und Client-Uhr stimmen nicht überein.', - description: `Server-Zeit: ${AppComponent.localTime(serverTime)}, - Client-Zeit: ${AppComponent.localTime(clientTime)}`, - category: 'FATAL' - }); + if (sysConfig) { + this.cts.addCustomTexts(sysConfig.customTexts); + const authData = MainDataService.getAuthData(); + if (authData) { + this.cts.addCustomTexts(authData.customTexts); + } + console.log('info', sysConfig); + if (sysConfig.broadcastingService && sysConfig.broadcastingService.status) { + this.mds.broadcastingServiceInfo = sysConfig.broadcastingService; + } + this.mds.isApiValid = AppComponent.isValidVersion(this.expectedApiVersion, sysConfig.version); + this.mds.apiVersion = sysConfig.version; + if (!this.mds.isApiValid) { + this.mds.appError$.next({ + label: 'Server-Problem: API-Version ungültig', + description: `erwartet: ${this.expectedApiVersion}, gefunden: ${sysConfig.version}`, + category: 'FATAL' + }); + } + if (sysConfig.mainLogo) { + console.warn('SysConfig.mainLogo not implemented yet'); + } + this.mds.setTestConfig(sysConfig.testConfig); + } else { + this.mds.isApiValid = false; } - this.mds.setTestConfig(sysConfig.testConfig); }); - this.bs.getSysCheckInfo().subscribe(sysChecks => { - this.mds.sysCheckAvailable = !!sysChecks; + this.bs.getSysCheckInfo().subscribe((myConfigs) => { + this.mds.sysCheckAvailable = !!myConfigs; }); }); } @@ -137,15 +125,12 @@ export class AppComponent implements OnInit, OnDestroy { if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support hidden = 'hidden'; visibilityChange = 'visibilitychange'; - // eslint-disable-next-line @typescript-eslint/dot-notation } else if (typeof document['msHidden'] !== 'undefined') { hidden = 'msHidden'; visibilityChange = 'msvisibilitychange'; - // eslint-disable-next-line @typescript-eslint/dot-notation } else if (typeof document['mozHidden'] !== 'undefined') { hidden = 'mozHidden'; visibilityChange = 'mozHidden'; - // eslint-disable-next-line @typescript-eslint/dot-notation } else if (typeof document['webkitHidden'] !== 'undefined') { hidden = 'webkitHidden'; visibilityChange = 'webkitvisibilitychange'; diff --git a/src/app/config/app.config.ts b/src/app/config/app.config.ts index da2072c5f4dc992ca7c5ff693722b54bad0910a6..38cc5036800330e6bc531cd933101cd57ed61527 100644 --- a/src/app/config/app.config.ts +++ b/src/app/config/app.config.ts @@ -7,7 +7,13 @@ export interface SysConfig { version: string; mainLogo: string; testConfig: KeyValuePairs; - serverTimestamp: number; + broadcastingService: BroadCastingServiceInfo; +} + +export interface BroadCastingServiceInfo { + status: string; + version?: string; + versionExpected?: string; } export class AppConfig { @@ -16,11 +22,11 @@ export class AppConfig { ) { } - setDefaultCustomTexts(): void { + setDefaultCustomTexts() { const ctDefaults = {}; - Object.keys(customTextsDefault).forEach(key => { - ctDefaults[key] = customTextsDefault[key].defaultvalue; - }); + for (const k of Object.keys(customTextsDefault)) { + ctDefaults[k] = customTextsDefault[k].defaultvalue + } this.cts.addCustomTexts(ctDefaults); } } diff --git a/src/app/maindata.service.ts b/src/app/maindata.service.ts index 2bfa502c856be218e2afd060a96117ff05111a06..94b385608c49efad0ccfdffad390d5ecda7e9c3b 100644 --- a/src/app/maindata.service.ts +++ b/src/app/maindata.service.ts @@ -6,7 +6,7 @@ import { AuthData, KeyValuePairs } from './app.interfaces'; import { BackendService } from './backend.service'; -import { AppConfig } from './config/app.config'; +import {AppConfig, BroadCastingServiceInfo, SysConfig} from './config/app.config'; const localStorageAuthDataKey = 'iqb-tc-a'; const localStorageTestConfigKey = 'iqb-tc-c'; @@ -21,6 +21,8 @@ export class MainDataService { public isSpinnerOn$ = new BehaviorSubject<boolean>(false); public progressVisualEnabled = true; public isApiValid = true; + public apiVersion: string; + public broadcastingServiceInfo: BroadCastingServiceInfo = { status: 'none' }; public appConfig: AppConfig = null; public sysCheckAvailable = false; diff --git a/src/app/test-controller/backend.service.ts b/src/app/test-controller/backend.service.ts index bdad2d215698ecec9780b5aced086212047c3dc3..43221e1a5f664a0251ce14697c097b2ebdbf62e9 100644 --- a/src/app/test-controller/backend.service.ts +++ b/src/app/test-controller/backend.service.ts @@ -1,21 +1,20 @@ import { Injectable, Inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import {Observable, of, Subscription} from 'rxjs'; -import {catchError, map, switchMap} from 'rxjs/operators'; +import { Observable, of, Subscription } from 'rxjs'; +import { catchError, map, switchMap } from 'rxjs/operators'; import { UnitData, TaggedString, TestData, - StateReportEntry + TestStateKey, + StateReportEntry, AppFocusState } from './test-controller.interfaces'; -import {ApiError} from '../app.interfaces'; - +import { ApiError } from '../app.interfaces'; @Injectable({ providedIn: 'root' }) export class BackendService { - constructor( @Inject('SERVER_URL') private serverUrl: string, private http: HttpClient @@ -24,7 +23,7 @@ export class BackendService { saveUnitReview(testId: string, unitName: string, priority: number, categories: string, entry: string) : Observable<boolean> { return this.http - .put(this.serverUrl + `test/${testId}/unit/${unitName}/review`, {priority, categories, entry}) + .put(`${this.serverUrl}test/${testId}/unit/${unitName}/review`, { priority, categories, entry }) .pipe( map(() => true), catchError((err: ApiError) => { @@ -36,7 +35,7 @@ export class BackendService { saveTestReview(testId: string, priority: number, categories: string, entry: string): Observable<boolean> { return this.http - .put(this.serverUrl + `test/${testId}/review`, {priority, categories, entry}) + .put(`${this.serverUrl}test/${testId}/review`, { priority, categories, entry }) .pipe( map(() => true), catchError((err: ApiError) => { @@ -48,7 +47,7 @@ export class BackendService { getTestData(testId: string): Observable<TestData | boolean> { return this.http - .get<TestData>(this.serverUrl + 'test/' + testId) + .get<TestData>(`${this.serverUrl}test/${testId}`) .pipe( catchError((err: ApiError) => { console.warn(`getTestData Api-Error: ${err.code} ${err.info} `); @@ -57,9 +56,9 @@ export class BackendService { ); } - getUnitData(testId: string, unitid: string): Observable<UnitData | boolean> { + getUnitData(testId: string, unitid: string, unitalias: string): Observable<UnitData | boolean> { return this.http - .get<UnitData>(this.serverUrl + 'test/' + testId + '/unit/' + unitid) + .get<UnitData>(`${this.serverUrl}test/${testId}/unit/${unitid}/alias/${unitalias}`) .pipe( catchError((err: ApiError) => { console.warn(`getUnitData Api-Error: ${err.code} ${err.info} `); @@ -71,13 +70,14 @@ export class BackendService { getResource(testId: string, internalKey: string, resId: string, versionning = false): Observable<TaggedString | number> { return this.http .get( - this.serverUrl + `test/${testId}/resource/${resId}`, + `${this.serverUrl}test/${testId}/resource/${resId}`, { params: new HttpParams().set('v', versionning ? '1' : 'f'), responseType: 'text' - }) + } + ) .pipe( - map(def => <TaggedString>{tag: internalKey, value: def}), + map(def => <TaggedString>{ tag: internalKey, value: def }), catchError((err: ApiError) => { console.warn(`getResource Api-Error: ${err.code} ${err.info} `); return of(err.code); @@ -87,37 +87,35 @@ export class BackendService { updateTestState(testId: string, newState: StateReportEntry[]): Subscription { return this.http - .patch(this.serverUrl + `test/${testId}/state`, newState) - .subscribe({error: (err: ApiError) => console.error(`updateTestState Api-Error: ${err.code} ${err.info}`)}); + .patch(`${this.serverUrl}test/${testId}/state`, newState) + .subscribe({ error: (err: ApiError) => console.error(`updateTestState Api-Error: ${err.code} ${err.info}`) }); } addTestLog(testId: string, logEntries: StateReportEntry[]): Subscription { return this.http - .put(this.serverUrl + `test/${testId}/log`, logEntries) - .subscribe({error: (err: ApiError) => console.error(`addTestLog Api-Error: ${err.code} ${err.info}`)}); + .put(`${this.serverUrl}test/${testId}/log`, logEntries) + .subscribe({ error: (err: ApiError) => console.error(`addTestLog Api-Error: ${err.code} ${err.info}`) }); } updateUnitState(testId: string, unitName: string, newState: StateReportEntry[]): Subscription { return this.http - .patch(this.serverUrl + `test/${testId}/unit/${unitName}/state`, newState) - .subscribe({error: (err: ApiError) => console.error(`setUnitState Api-Error: ${err.code} ${err.info}`)}); + .patch(`${this.serverUrl}test/${testId}/unit/${unitName}/state`, newState) + .subscribe({ error: (err: ApiError) => console.error(`setUnitState Api-Error: ${err.code} ${err.info}`) }); } addUnitLog(testId: string, unitName: string, logEntries: StateReportEntry[]): Subscription { return this.http - .put(this.serverUrl + `test/${testId}/unit/${unitName}/log`, logEntries) - .subscribe({error: (err: ApiError) => console.error(`addUnitLog Api-Error: ${err.code} ${err.info}`)}); + .put(`${this.serverUrl}test/${testId}/unit/${unitName}/log`, logEntries) + .subscribe({ error: (err: ApiError) => console.error(`addUnitLog Api-Error: ${err.code} ${err.info}`) }); } - notifyDyingTest(testId: string): void { - if (navigator.sendBeacon) { - navigator.sendBeacon(this.serverUrl + `test/${testId}/connection-lost`); - } else { - fetch(this.serverUrl + `test/${testId}/connection-lost`, { - keepalive: true, - method: 'POST' - }); - } + notifyDyingTest(testId: string) { + // TODO add auth or change end point + if (navigator.sendBeacon) { + navigator.sendBeacon(`${this.serverUrl}test/${testId}/state`, JSON.stringify(<StateReportEntry>{ + key: TestStateKey.FOCUS, timeStamp: Date.now(), content: AppFocusState.DEAD + })); + } } updateUnitStateData(testId: string, timeStamp: number, unitName: string, dataPartsAllString: string, unitStateDataType: string) @@ -127,19 +125,17 @@ export class BackendService { const restorePoint = dataPartsAllString; const responseType = unitStateDataType; return this.http - .put(this.serverUrl + `test/${testId}/unit/${unitName}/response`, {timeStamp, response, responseType}) + .put(`${this.serverUrl}test/${testId}/unit/${unitName}/response`, { timeStamp, response, responseType }) .pipe( - switchMap(() => { - return this.http - .patch(this.serverUrl + `test/${testId}/unit/${unitName}/restorepoint`, {timeStamp, restorePoint}) - .pipe( - map(() => true), - catchError((err: ApiError) => { - console.warn(`newUnitStateData/restorepoint Api-Error: ${err.code} ${err.info} `); - return of(false); - }) - ); - }), + switchMap(() => this.http + .patch(`${this.serverUrl}test/${testId}/unit/${unitName}/restorepoint`, { timeStamp, restorePoint }) + .pipe( + map(() => true), + catchError((err: ApiError) => { + console.warn(`newUnitStateData/restorepoint Api-Error: ${err.code} ${err.info} `); + return of(false); + }) + )), catchError((err: ApiError) => { console.warn(`newUnitStateData/response Api-Error: ${err.code} ${err.info} `); return of(false); @@ -149,7 +145,7 @@ export class BackendService { lockTest(testId: string, timeStamp: number, content: string): Observable<boolean> { return this.http - .patch<boolean>(this.serverUrl + `test/${testId}/lock`, {timeStamp, content}) + .patch<boolean>(`${this.serverUrl}test/${testId}/lock`, { timeStamp, content }) .pipe( map(() => true), catchError((err: ApiError) => { diff --git a/src/app/test-controller/test-controller.component.ts b/src/app/test-controller/test-controller.component.ts index cf2fa7cb9debd9d7dd1e973e483c299cd7ef7c95..a8e86095c64766507673f389183d6498b4d2b87d 100644 --- a/src/app/test-controller/test-controller.component.ts +++ b/src/app/test-controller/test-controller.component.ts @@ -1,11 +1,17 @@ -import {ReviewDialogComponent} from './review-dialog/review-dialog.component'; -import {ActivatedRoute, Router} from '@angular/router'; -import {MainDataService} from '../maindata.service'; -import {BackendService} from './backend.service'; - -import {TestControllerService} from './test-controller.service'; -import {Component, HostListener, Inject, OnDestroy, OnInit} from '@angular/core'; -import {EnvironmentData, MaxTimerData, Testlet, UnitDef} from './test-controller.classes'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { + Component, HostListener, Inject, OnDestroy, OnInit +} from '@angular/core'; +import { + from, Observable, of, Subscription, throwError +} from 'rxjs'; +import { + concatMap, debounceTime, distinctUntilChanged, map, switchMap +} from 'rxjs/operators'; +import { CustomtextService } from 'iqb-components'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; import { AppFocusState, Command, @@ -21,15 +27,17 @@ import { UnitNavigationTarget, UnitStateKey, WindowFocusState } from './test-controller.interfaces'; -import {from, Observable, of, Subscription, throwError} from 'rxjs'; -import {concatMap, debounceTime, distinctUntilChanged, 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'; -import {CommandService} from './command.service'; - +import { + EnvironmentData, MaxTimerData, Testlet, UnitDef +} from './test-controller.classes'; +import { BackendService } from './backend.service'; +import { MainDataService } from '../maindata.service'; +import { TestControllerService } from './test-controller.service'; +import { ReviewDialogComponent } from './review-dialog/review-dialog.component'; +// eslint-disable-next-line import/extensions +import { BookletConfig } from '../config/booklet-config'; +import { TestMode } from '../config/test-mode'; +import { CommandService } from './command.service'; @Component({ templateUrl: './test-controller.component.html', @@ -57,8 +65,7 @@ export class TestControllerComponent implements OnInit, OnDestroy { unitNavigationTarget = UnitNavigationTarget; debugPane = false; - - constructor ( + constructor( @Inject('APP_VERSION') public appVersion: string, @Inject('IS_PRODUCTION_MODE') public isProductionMode, private mds: MainDataService, @@ -75,7 +82,7 @@ export class TestControllerComponent implements OnInit, OnDestroy { private static getChildElements(element) { return Array.prototype.slice.call(element.childNodes) - .filter(function (e) { return e.nodeType === 1; }); + .filter(e => e.nodeType === 1); } // private: recursive reading testlets/units from xml @@ -106,7 +113,7 @@ export class TestControllerComponent implements OnInit, OnDestroy { const restrictionParameter = restrictionElements[childIndex].getAttribute('minutes'); if ((typeof restrictionParameter !== 'undefined') && (restrictionParameter !== null)) { maxTime = Number(restrictionParameter); - if (isNaN(maxTime)) { + if (Number.isNaN(maxTime)) { maxTime = -1; } } @@ -135,20 +142,19 @@ export class TestControllerComponent implements OnInit, OnDestroy { let myUnitAliasClear = myUnitAlias; let unitIdSuffix = 1; while (this.allUnitIds.indexOf(myUnitAliasClear) > -1) { - myUnitAliasClear = myUnitAlias + '-' + unitIdSuffix.toString(); + myUnitAliasClear = `${myUnitAlias}-${unitIdSuffix.toString()}`; unitIdSuffix += 1; } this.allUnitIds.push(myUnitAliasClear); targetTestlet.addUnit(this.lastUnitSequenceId, myUnitId, - childElements[childIndex].getAttribute('label'), myUnitAliasClear, - childElements[childIndex].getAttribute('labelshort')); + 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'); if (!testletId) { - testletId = 'Testlet' + this.lastTestletIndex.toString(); + testletId = `Testlet${this.lastTestletIndex.toString()}`; this.lastTestletIndex += 1; } let testletLabel: string = childElements[childIndex].getAttribute('label'); @@ -219,85 +225,81 @@ export class TestControllerComponent implements OnInit, OnDestroy { } // private: read unitdata - private loadUnitOk (myUnit: UnitDef, sequenceId: number): Observable<number> { + private loadUnitOk(myUnit: UnitDef, sequenceId: number): Observable<number> { myUnit.setCanEnter('n', 'Fehler beim Laden'); - return this.bs.getUnitData(this.tcs.testId, myUnit.id) + return this.bs.getUnitData(this.tcs.testId, myUnit.id, myUnit.alias) .pipe( switchMap(myData => { if (myData === false) { return throwError(`error requesting unit ${this.tcs.testId}/${myUnit.id}`); - } else { - const myUnitData = myData as UnitData; - if (myUnitData.restorepoint) { - this.tcs.addUnitStateData(sequenceId, JSON.parse(myUnitData.restorepoint)); - } - let playerId = null; - let definitionRef = ''; - if (myUnitData.laststate && myUnitData.laststate[UnitStateKey.PRESENTATION_PROGRESS]) { - this.tcs.setOldUnitPresentationComplete(sequenceId, myUnitData.laststate[UnitStateKey.PRESENTATION_PROGRESS]); - } + } + const myUnitData = myData as UnitData; + if (myUnitData.restorepoint) { + this.tcs.addUnitStateData(sequenceId, JSON.parse(myUnitData.restorepoint)); + } + let playerId = null; + let definitionRef = ''; + if (myUnitData.laststate && myUnitData.laststate[UnitStateKey.PRESENTATION_PROGRESS]) { + this.tcs.setOldUnitPresentationComplete(sequenceId, myUnitData.laststate[UnitStateKey.PRESENTATION_PROGRESS]); + } - try { - const oParser = new DOMParser(); - const oDOM = oParser.parseFromString(myUnitData.xml, 'text/xml'); + 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 (oDOM.documentElement.nodeName === 'Unit') { + const defElements = oDOM.documentElement.getElementsByTagName('Definition'); - if (defElements.length > 0) { - const defElement = defElements[0]; - this.tcs.addUnitDefinition(sequenceId, defElement.textContent); - playerId = defElement.getAttribute('player'); - } else { - const defRefElements = oDOM.documentElement.getElementsByTagName('DefinitionRef'); + if (defElements.length > 0) { + const defElement = defElements[0]; + this.tcs.addUnitDefinition(sequenceId, defElement.textContent); + playerId = defElement.getAttribute('player'); + } else { + const defRefElements = oDOM.documentElement.getElementsByTagName('DefinitionRef'); - if (defRefElements.length > 0) { - const defRefElement = defRefElements[0]; - definitionRef = defRefElement.textContent; - // this.tcs.addUnitDefinition(sequenceId, ''); - playerId = defRefElement.getAttribute('player'); - } + if (defRefElements.length > 0) { + const defRefElement = defRefElements[0]; + definitionRef = defRefElement.textContent; + // this.tcs.addUnitDefinition(sequenceId, ''); + playerId = defRefElement.getAttribute('player'); } } - } catch (error) { - return throwError(`error parsing unit def ${this.tcs.testId}/${myUnit.id} (${error.toString()})`); } + } catch (error) { + return throwError(`error parsing unit def ${this.tcs.testId}/${myUnit.id} (${error.toString()})`); + } - if (playerId) { - myUnit.playerId = playerId; - if (definitionRef.length > 0) { - this.unitLoadQueue.push(<TaggedString>{ - tag: sequenceId.toString(), - value: definitionRef - }); - } - myUnit.setCanEnter('y', ''); + if (playerId) { + myUnit.playerId = playerId; + 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((data: number|TaggedString) => { - if (typeof data === 'number') { - return throwError(`error getting player "${playerId}"`); - } else { - const player = data 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)`); - } - } - })); - } - } else { - return throwError(`player def missing for unit ${this.tcs.testId}/${myUnit.id}`); + if (this.tcs.hasPlayer(playerId)) { + return of(sequenceId); } + // 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((data: number|TaggedString) => { + if (typeof data === 'number') { + return throwError(`error getting player "${playerId}"`); + } + const player = data as TaggedString; + if (player.value.length > 0) { + this.tcs.addPlayer(playerId, player.value); + return of(sequenceId); + } + return throwError(`error getting player "${playerId}" (size = 0)`); + }) + ); } + return throwError(`player def missing for unit ${this.tcs.testId}/${myUnit.id}`); }) ); } @@ -318,7 +320,7 @@ export class TestControllerComponent implements OnInit, OnDestroy { } this.errorReportingSubscription = this.mds.appError$.subscribe(e => { if (this.isProductionMode && this.tcs.testMode.saveResponses) { - console.error(e.label + ' / ' + e.description); + console.error(`${e.label} / ${e.description}`); } this.tcs.testStatus$.next(TestControllerState.ERROR); }); @@ -344,28 +346,28 @@ export class TestControllerComponent implements OnInit, OnDestroy { this.tcs.windowFocusState$.next(hasFocus ? WindowFocusState.HOST : WindowFocusState.UNKNOWN); }); this.commandSubscription = this.cmd.command$.pipe( - distinctUntilChanged((command1: Command, command2: Command): boolean => (command1.id === command2.id)) - ) + distinctUntilChanged((command1: Command, command2: Command): boolean => (command1.id === command2.id)) + ) .subscribe((command: Command) => { this.handleCommand(command.keyword, command.arguments); - }); + }); this.routingSubscription = this.route.params.subscribe(params => { if (this.tcs.testStatus$.getValue() !== TestControllerState.ERROR) { - this.tcs.testId = params['t']; - localStorage.setItem(TestControllerComponent.localStorageTestKey, params['t']); + this.tcs.testId = params.t; + localStorage.setItem(TestControllerComponent.localStorageTestKey, params.t); this.unsubscribeTestSubscriptions(); this.maxTimerSubscription = this.tcs.maxTimeTimer$.subscribe(maxTimerData => { switch (maxTimerData.type) { case MaxTimerDataType.STARTED: - this.snackBar.open(this.cts.getCustomText('booklet_msgTimerStarted') - + maxTimerData.timeLeftMinString, '', {duration: 3000}); + this.snackBar.open(this.cts.getCustomText('booklet_msgTimerStarted') + + maxTimerData.timeLeftMinString, '', { duration: 3000 }); this.timerValue = maxTimerData; break; case MaxTimerDataType.ENDED: - this.snackBar.open(this.cts.getCustomText('booklet_msgTimeOver'), '', {duration: 3000}); + this.snackBar.open(this.cts.getCustomText('booklet_msgTimeOver'), '', { duration: 3000 }); this.tcs.rootTestlet.setTimeLeft(maxTimerData.testletId, 0); this.tcs.LastMaxTimerState[maxTimerData.testletId] = 0; if (this.tcs.testMode.saveResponses) { @@ -380,7 +382,7 @@ export class TestControllerComponent implements OnInit, OnDestroy { } break; case MaxTimerDataType.CANCELLED: - this.snackBar.open(this.cts.getCustomText('booklet_msgTimerCancelled'), '', {duration: 3000}); + this.snackBar.open(this.cts.getCustomText('booklet_msgTimerCancelled'), '', { duration: 3000 }); this.tcs.rootTestlet.setTimeLeft(maxTimerData.testletId, 0); this.tcs.LastMaxTimerState[maxTimerData.testletId] = 0; if (this.tcs.testMode.saveResponses) { @@ -405,9 +407,9 @@ export class TestControllerComponent implements OnInit, OnDestroy { } } if ((maxTimerData.timeLeftSeconds / 60) === 5) { - this.snackBar.open(this.cts.getCustomText('booklet_msgSoonTimeOver5Minutes'), '', {duration: 3000}); + 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.snackBar.open(this.cts.getCustomText('booklet_msgSoonTimeOver1Minute'), '', { duration: 3000 }); } break; } @@ -454,11 +456,11 @@ export class TestControllerComponent implements OnInit, OnDestroy { this.tcs.rootTestlet = this.getBookletFromXml(testData.xml); document.documentElement.style.setProperty('--tc-unit-title-height', - this.tcs.bookletConfig.unit_title === 'ON' ? this.mds.defaultTcUnitTitleHeight : '0'); + 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); + 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'); + this.tcs.bookletConfig.page_navibuttons === 'SEPARATE_BOTTOM' ? this.mds.defaultTcUnitPageNavHeight : '0'); if (this.tcs.rootTestlet === null) { this.mds.appError$.next({ @@ -485,91 +487,88 @@ export class TestControllerComponent implements OnInit, OnDestroy { 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(); - let navTarget = 1; - if (navTargetUnitId) { - const tmpNavTarget = this.tcs.rootTestlet.getSequenceIdByUnitAlias(navTargetUnitId); - if (tmpNavTarget > 0) { - navTarget = tmpNavTarget; - } + this.incrementProgressValueBy1(); + }, + errorMessage => { + this.mds.appError$.next({ + label: 'Problem beim Laden der Testinformation', + description: errorMessage, + category: 'PROBLEM' + }); + }, + () => { + this.tcs.rootTestlet.lockUnitsIfTimeLeftNull(); + let navTarget = 1; + if (navTargetUnitId) { + const tmpNavTarget = this.tcs.rootTestlet.getSequenceIdByUnitAlias(navTargetUnitId); + if (tmpNavTarget > 0) { + navTarget = tmpNavTarget; } - 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') { - 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' - }); - this.tcs.testStatus$.next(TestControllerState.ERROR); - }, - () => { // complete - if (this.tcs.testMode.saveResponses) { - envData.loadTime = Date.now() - loadStartTimeStamp; - this.bs.addTestLog(this.tcs.testId, [<StateReportEntry>{ - key: TestLogEntryKey.LOADCOMPLETE, timeStamp: Date.now(), content: JSON.stringify(envData) - }]); - } - this.tcs.loadProgressValue = 100; - - this.tcs.loadComplete = true; - if (this.tcs.bookletConfig.loading_mode === 'EAGER') { - this.resumeTargetUnitId = navTarget; - this.tcs.setUnitNavigationRequest(navTarget.toString()); - this.tcs.testStatus$.next(newTestStatus); - if (this.tcs.testMode.saveResponses) { - this.addAppFocusSubscription(); + } + 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') { + this.incrementProgressValueBy1(); + } + // avoid to load unit def if not necessary + if (unitSequ < this.tcs.minUnitSequenceId) { + return of(<TaggedString>{ tag: unitSequ.toString(), value: '' }); + } + 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}`); } - } + 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' + }); + this.tcs.testStatus$.next(TestControllerState.ERROR); + }, + () => { // complete + if (this.tcs.testMode.saveResponses) { + envData.loadTime = Date.now() - loadStartTimeStamp; + this.bs.addTestLog(this.tcs.testId, [<StateReportEntry>{ + key: TestLogEntryKey.LOADCOMPLETE, timeStamp: Date.now(), content: JSON.stringify(envData) + }]); } - ); + this.tcs.loadProgressValue = 100; - if (this.tcs.bookletConfig.loading_mode === 'LAZY') { - this.resumeTargetUnitId = navTarget; - this.tcs.setUnitNavigationRequest(navTarget.toString()); - this.tcs.testStatus$.next(newTestStatus); - if (this.tcs.testMode.saveResponses) { - this.addAppFocusSubscription(); + this.tcs.loadComplete = true; + if (this.tcs.bookletConfig.loading_mode === 'EAGER') { + this.resumeTargetUnitId = navTarget; + this.tcs.setUnitNavigationRequest(navTarget.toString()); + this.tcs.testStatus$.next(newTestStatus); + if (this.tcs.testMode.saveResponses) { + this.addAppFocusSubscription(); + } } } - - } // complete + ); + + if (this.tcs.bookletConfig.loading_mode === 'LAZY') { + this.resumeTargetUnitId = navTarget; + this.tcs.setUnitNavigationRequest(navTarget.toString()); + this.tcs.testStatus$.next(newTestStatus); + if (this.tcs.testMode.saveResponses) { + this.addAppFocusSubscription(); + } + } + } // complete ); } } @@ -602,9 +601,9 @@ export class TestControllerComponent implements OnInit, OnDestroy { debounceTime(500) ).subscribe((newState: WindowFocusState) => { if (newState === WindowFocusState.UNKNOWN) { - this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{ - key: TestStateKey.FOCUS, timeStamp: Date.now(), content: AppFocusState.HAS_NOT - }]); + this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{ + key: TestStateKey.FOCUS, timeStamp: Date.now(), content: AppFocusState.HAS_NOT + }]); } else { this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{ key: TestStateKey.FOCUS, timeStamp: Date.now(), content: AppFocusState.HAS @@ -615,7 +614,7 @@ export class TestControllerComponent implements OnInit, OnDestroy { showReviewDialog() { if (this.tcs.rootTestlet === null) { - this.snackBar.open('Kein Testheft verfügbar.', '', {duration: 3000}); + this.snackBar.open('Kein Testheft verfügbar.', '', { duration: 3000 }); } else { const authData = MainDataService.getAuthData(); const dialogRef = this.reviewDialog.open(ReviewDialogComponent, { @@ -631,32 +630,32 @@ export class TestControllerComponent implements OnInit, OnDestroy { dialogRef.afterClosed().subscribe(result => { if (typeof result !== 'undefined') { if (result !== false) { - const targetSelection = result['target']; + const targetSelection = result.target; if (targetSelection === 'u') { this.bs.saveUnitReview( this.tcs.testId, this.tcs.currentUnitDbKey, - result['priority'], + 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}); - } else { - 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 }); + } else { + this.snackBar.open('Kommentar gespeichert', '', { duration: 1000 }); + } + }); } else { this.bs.saveTestReview( this.tcs.testId, - result['priority'], + result.priority, dialogRef.componentInstance.getCategories(), - result['sender'] ? result['sender'] + ': ' + result['entry'] : result['entry'] + result.sender ? `${result.sender}: ${result.entry}` : result.entry ).subscribe(ok => { if (!ok) { - this.snackBar.open('Konnte Kommentar nicht speichern', '', {duration: 3000}); + this.snackBar.open('Konnte Kommentar nicht speichern', '', { duration: 3000 }); } else { - this.snackBar.open('Kommentar gespeichert', '', {duration: 1000}); + this.snackBar.open('Kommentar gespeichert', '', { duration: 1000 }); } }); } diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 4422e58c408dfdacfc9d5d1450ea711277ed6ee6..6c9895f3a9949699e444e610bd3ecc553515cabc 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,9 +1,7 @@ -// ng build --prod - export const environment = { production: true, testcenterUrl: '/api/', appPublisher: 'IQB - Institut zur Qualitätsentwicklung im Bildungswesen', - apiVersionExpected: '9.0.0-nextmajor', + apiVersionExpected: '9.1.0', veronaApiVersionSupported: '2.1.0' }; diff --git a/src/environments/environment.dev.ts b/src/environments/environment.ts similarity index 80% rename from src/environments/environment.dev.ts rename to src/environments/environment.ts index c172344b23705ebeb801acb0794125b7e5833793..1468f991e774b2db88b4a45be6ea505c129dd2bc 100644 --- a/src/environments/environment.dev.ts +++ b/src/environments/environment.ts @@ -4,8 +4,8 @@ export const environment = { production: false, - testcenterUrl: '/api/', + testcenterUrl: 'http://localhost/2020/testcenter-iqb-php/', appPublisher: 'IQB - Institut zur Qualitätsentwicklung im Bildungswesen', - apiVersionExpected: '9.0.0-nextmajor', + apiVersionExpected: '9.1.0', veronaApiVersionSupported: '2.1.0' };