File

src/app/test-controller/services/test-controller.service.ts

Index

Properties
Methods
Accessors

Constructor

constructor(router: Router, bs: BackendService)
Parameters :
Name Type Optional
router Router No
bs BackendService No

Methods

addClearedCodeTestlet
addClearedCodeTestlet(testletId: string)
Parameters :
Name Type Optional
testletId string No
Returns : void
addPlayer
addPlayer(id: string, player: string)
Parameters :
Name Type Optional
id string No
player string No
Returns : void
addUnitDefinition
addUnitDefinition(sequenceId: number, uDef: string)
Parameters :
Name Type Optional
sequenceId number No
uDef string No
Returns : void
addUnitStateDataParts
addUnitStateDataParts(unitSequenceId: number, dataPartsAllString: string)
Parameters :
Name Type Optional
unitSequenceId number No
dataPartsAllString string No
Returns : void
cancelMaxTimer
cancelMaxTimer()
Returns : void
errorOut
errorOut()
Returns : void
Private finishTest
finishTest(logEntryKey: string, lockTest: boolean)
Parameters :
Name Type Optional Default value
logEntryKey string No
lockTest boolean No false
Returns : void
getPlayer
getPlayer(id: string)
Parameters :
Name Type Optional
id string No
Returns : string
getUnitDefinition
getUnitDefinition(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : string
getUnitLoadProgress$
getUnitLoadProgress$(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
getUnitPresentationProgress
getUnitPresentationProgress(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : string
getUnitResponseProgress
getUnitResponseProgress(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : string
getUnitStateCurrentPage
getUnitStateCurrentPage(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : string
getUnitStateDataParts
getUnitStateDataParts(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : string
hasPlayer
hasPlayer(id: string)
Parameters :
Name Type Optional
id string No
Returns : boolean
hasUnitDefinition
hasUnitDefinition(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : boolean
hasUnitPresentationProgress
hasUnitPresentationProgress(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : boolean
hasUnitResponseProgress
hasUnitResponseProgress(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : boolean
hasUnitStateCurrentPage
hasUnitStateCurrentPage(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : boolean
hasUnitStateDataParts
hasUnitStateDataParts(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : boolean
interruptMaxTimer
interruptMaxTimer()
Returns : void
isUnitContentLoaded
isUnitContentLoaded(sequenceId: number)
Parameters :
Name Type Optional
sequenceId number No
Returns : boolean
newUnitStateCurrentPage
newUnitStateCurrentPage(unitDbKey: string, unitSequenceId: number, pageNr: number, pageId: string, pageCount: number)
Parameters :
Name Type Optional
unitDbKey string No
unitSequenceId number No
pageNr number No
pageId string No
pageCount number No
Returns : void
newUnitStateData
newUnitStateData(unitDbKey: string, sequenceId: number, dataPartsAllString: string, unitStateDataType: string)
Parameters :
Name Type Optional
unitDbKey string No
sequenceId number No
dataPartsAllString string No
unitStateDataType string No
Returns : void
newUnitStateResponseProgress
newUnitStateResponseProgress(unitDbKey: string, unitSeqId: number, responseProgress: string)
Parameters :
Name Type Optional
unitDbKey string No
unitSeqId number No
responseProgress string No
Returns : void
Static normaliseId
normaliseId(id: string, expectedExtension: string)
Parameters :
Name Type Optional Default value
id string No
expectedExtension string No ''
Returns : string
notifyNavigationDenied
notifyNavigationDenied(sourceUnitSequenceId: number, reason: VeronaNavigationDeniedReason[])
Parameters :
Name Type Optional
sourceUnitSequenceId number No
reason VeronaNavigationDeniedReason[] No
Returns : void
pause
pause()
Returns : void
resetDataStore
resetDataStore()
Returns : void
setOldUnitDataCurrentPage
setOldUnitDataCurrentPage(sequenceId: number, pageId: string)
Parameters :
Name Type Optional
sequenceId number No
pageId string No
Returns : void
setOldUnitPresentationProgress
setOldUnitPresentationProgress(sequenceId: number, state: string)
Parameters :
Name Type Optional
sequenceId number No
state string No
Returns : void
setOldUnitResponseProgress
setOldUnitResponseProgress(sequenceId: number, state: string)
Parameters :
Name Type Optional
sequenceId number No
state string No
Returns : void
setUnitLoadProgress$
setUnitLoadProgress$(sequenceId: number, progress: Observable<LoadingProgress>)
Parameters :
Name Type Optional
sequenceId number No
progress Observable<LoadingProgress> No
Returns : void
setUnitNavigationRequest
setUnitNavigationRequest(navString: string, force)
Parameters :
Name Type Optional Default value
navString string No
force No false
Returns : void
startMaxTimer
startMaxTimer(testletId: string, timeLeftMinutes: number)
Parameters :
Name Type Optional
testletId string No
timeLeftMinutes number No
Returns : void
terminateTest
terminateTest(logEntryKey: string, force: boolean, lockTest: boolean)
Parameters :
Name Type Optional Default value
logEntryKey string No
force boolean No
lockTest boolean No false
Returns : void
updateUnitStatePresentationProgress
updateUnitStatePresentationProgress(unitDbKey: string, unitSeqId: number, presentationProgress: string)
Parameters :
Name Type Optional
unitDbKey string No
unitSeqId number No
presentationProgress string No
Returns : void

Properties

Private _currentUnitSequenceId$
Type : BehaviorSubject<number>
Default value : new BehaviorSubject<number>(-Infinity)
Private _navigationDenial
Default value : new Subject<{ sourceUnitSequenceId: number, reason: VeronaNavigationDeniedReason[] }>()
allUnitIds
Type : string[]
Default value : []
bookletConfig
Default value : new BookletConfig()
clearCodeTestlets
Type : string[]
Default value : []
currentMaxTimerTestletId
Type : string
Default value : ''
currentUnitDbKey
Type : string
Default value : ''
currentUnitTitle
Type : string
Default value : ''
Private maxTimeIntervalSubscription
Type : Subscription
Default value : null
maxTimeTimer$
Default value : new Subject<MaxTimerData>()
maxTimeTimers
Type : KeyValuePairNumber
Default value : {}
Private players
Type : literal type
Default value : {}

the structure of this service is a little bit weird. instead of distributing the UnitDefs into the several arrays below we could store a single artraay with UnitDefs (wich would be a flattend version of the rooot testlet). Thus we would could get rid of all thos arrays, get-, set- and has- functions. I leave this out for the next refactoring. TODO simply data structure

resumeTargetUnitSequenceId
Type : number
Default value : 0
rootTestlet
Type : Testlet
Default value : null
testId
Type : string
Default value : ''
testMode
Default value : new TestMode()
testStatus$
Default value : new BehaviorSubject<TestControllerState>(TestControllerState.INIT)
testStatusEnum
Default value : TestControllerState
totalLoadingProgress
Type : number
Default value : 0
Private unitContentLoadProgress$
Type : literal type
Default value : {}
Private unitDefinitions
Type : literal type
Default value : {}
Private unitPresentationProgressStates
Type : literal type
Default value : {}
Private unitResponseProgressStates
Type : literal type
Default value : {}
Private unitStateCurrentPages
Type : literal type
Default value : {}
Private unitStateDataParts
Type : literal type
Default value : {}
Private unitStateDataToSave$
Default value : new Subject<UnitStateData>()
windowFocusState$
Default value : new Subject<WindowFocusState>()

Accessors

navigationDenial
getnavigationDenial()
currentUnitSequenceId
getcurrentUnitSequenceId()
setcurrentUnitSequenceId(v: number)
Parameters :
Name Type Optional
v number No
Returns : void
currentUnitSequenceId$
getcurrentUnitSequenceId$()
import { debounceTime, map, takeUntil } from 'rxjs/operators';
import {
  BehaviorSubject, interval, Observable, Subject, Subscription, timer
} from 'rxjs';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { MaxTimerData, Testlet } from '../classes/test-controller.classes';
import {
  KeyValuePairNumber, LoadingProgress,
  MaxTimerDataType, StateReportEntry,
  TestControllerState, TestStateKey,
  UnitNavigationTarget,
  UnitStateData, UnitStateKey, WindowFocusState
} from '../interfaces/test-controller.interfaces';
import { BackendService } from './backend.service';
import { TestMode } from '../../config/test-mode';
// eslint-disable-next-line import/extensions
import { BookletConfig } from '../../config/booklet-config';
import { VeronaNavigationDeniedReason } from '../interfaces/verona.interfaces';

@Injectable({
  providedIn: 'root'
})
export class TestControllerService {
  testId = '';
  testStatus$ = new BehaviorSubject<TestControllerState>(TestControllerState.INIT);
  testStatusEnum = TestControllerState;

  totalLoadingProgress = 0;

  clearCodeTestlets: string[] = [];

  testMode = new TestMode();
  bookletConfig = new BookletConfig();
  rootTestlet: Testlet = null;

  maxTimeTimer$ = new Subject<MaxTimerData>();
  currentMaxTimerTestletId = '';
  private maxTimeIntervalSubscription: Subscription = null;
  maxTimeTimers: KeyValuePairNumber = {};

  currentUnitDbKey = '';
  currentUnitTitle = '';

  allUnitIds: string[] = [];

  private unitStateDataToSave$ = new Subject<UnitStateData>();
  windowFocusState$ = new Subject<WindowFocusState>();

  resumeTargetUnitSequenceId = 0;

  private _navigationDenial = new Subject<{ sourceUnitSequenceId: number, reason: VeronaNavigationDeniedReason[] }>();
  get navigationDenial(): Observable<{ sourceUnitSequenceId: number, reason: VeronaNavigationDeniedReason[] }> {
    return this._navigationDenial;
  }

  private _currentUnitSequenceId$: BehaviorSubject<number> = new BehaviorSubject<number>(-Infinity);
  get currentUnitSequenceId(): number {
    return this._currentUnitSequenceId$.getValue();
  }

  set currentUnitSequenceId(v: number) {
    this._currentUnitSequenceId$.next(v);
  }

  get currentUnitSequenceId$(): Observable<number> {
    return this._currentUnitSequenceId$.asObservable();
  }

  /**
   * the structure of this service is a little bit weird. instead of distributing the UnitDefs into the several arrays
   * below we could store a single artraay with UnitDefs (wich would be a flattend version of the rooot testlet). Thus
   * we would could get rid of all thos arrays, get-, set- and has- functions. I leave this out for the next
   * refactoring. TODO simply data structure
   */
  private players: { [filename: string]: string } = {};
  private unitDefinitions: { [sequenceId: number]: string } = {};
  private unitStateDataParts: { [sequenceId: number]: string } = {};
  private unitPresentationProgressStates: { [sequenceId: number]: string } = {};
  private unitResponseProgressStates: { [sequenceId: number]: string } = {};
  private unitStateCurrentPages: { [sequenceId: number]: string } = {};
  private unitContentLoadProgress$: { [sequenceId: number]: Observable<LoadingProgress> } = {};

  constructor(
    private router: Router,
    private bs: BackendService
  ) {
    this.unitStateDataToSave$
      .pipe(debounceTime(200))
      .subscribe(unitStateData => {
        this.bs.updateUnitStateData(
          this.testId,
          unitStateData.unitDbKey,
          JSON.stringify(unitStateData.dataPartsAllString),
          unitStateData.unitStateDataType
        ).subscribe(ok => {
          if (!ok) {
            console.warn('storing unitData failed');
          }
        });
      });
  }

  resetDataStore(): void {
    this.players = {};
    this.unitDefinitions = {};
    this.unitStateDataParts = {};
    this.rootTestlet = null;
    this.clearCodeTestlets = [];
    this.currentUnitSequenceId = 0;
    this.currentUnitDbKey = '';
    this.currentUnitTitle = '';
    if (this.maxTimeIntervalSubscription !== null) {
      this.maxTimeIntervalSubscription.unsubscribe();
      this.maxTimeIntervalSubscription = null;
    }
    this.currentMaxTimerTestletId = '';
    this.maxTimeTimers = {};
    this.unitPresentationProgressStates = {};
  }

  // uppercase and add extension if not part
  static normaliseId(id: string, expectedExtension = ''): string {
    let normalisedId = id.trim().toUpperCase();
    const normalisedExtension = expectedExtension.toUpperCase();
    if (normalisedExtension && (normalisedId.split('.').pop() !== normalisedExtension)) {
      normalisedId += `.${normalisedExtension}`;
    }
    return normalisedId;
  }

  addPlayer(id: string, player: string): void {
    this.players[TestControllerService.normaliseId(id, 'html')] = player;
  }

  hasPlayer(id: string): boolean {
    return TestControllerService.normaliseId(id, 'html') in this.players;
  }

  getPlayer(id: string): string {
    return this.players[TestControllerService.normaliseId(id, 'html')];
  }

  addUnitDefinition(sequenceId: number, uDef: string): void {
    this.unitDefinitions[sequenceId] = uDef;
  }

  hasUnitDefinition(sequenceId: number): boolean {
    return sequenceId in this.unitDefinitions;
  }

  getUnitDefinition(sequenceId: number): string {
    return this.unitDefinitions[sequenceId];
  }

  hasUnitStateDataParts(sequenceId: number): boolean {
    return sequenceId in this.unitStateDataParts;
  }

  getUnitStateDataParts(sequenceId: number): string {
    return this.unitStateDataParts[sequenceId];
  }

  addUnitStateDataParts(unitSequenceId: number, dataPartsAllString: string): void {
    this.unitStateDataParts[unitSequenceId] = dataPartsAllString;
  }

  setOldUnitPresentationProgress(sequenceId: number, state: string): void {
    this.unitPresentationProgressStates[sequenceId] = state;
  }

  hasUnitPresentationProgress(sequenceId: number): boolean {
    return sequenceId in this.unitPresentationProgressStates;
  }

  getUnitPresentationProgress(sequenceId: number): string {
    return this.unitPresentationProgressStates[sequenceId];
  }

  hasUnitResponseProgress(sequenceId: number): boolean {
    return sequenceId in this.unitResponseProgressStates;
  }

  setOldUnitResponseProgress(sequenceId: number, state: string): void {
    this.unitResponseProgressStates[sequenceId] = state;
  }

  getUnitResponseProgress(sequenceId: number): string {
    return this.unitResponseProgressStates[sequenceId];
  }

  hasUnitStateCurrentPage(sequenceId: number): boolean {
    return sequenceId in this.unitStateCurrentPages;
  }

  getUnitStateCurrentPage(sequenceId: number): string {
    return this.unitStateCurrentPages[sequenceId];
  }

  setOldUnitDataCurrentPage(sequenceId: number, pageId: string): void {
    this.unitStateCurrentPages[sequenceId] = pageId;
  }

  setUnitLoadProgress$(sequenceId: number, progress: Observable<LoadingProgress>): void {
    this.unitContentLoadProgress$[sequenceId] = progress;
  }

  getUnitLoadProgress$(sequenceId: number): Observable<LoadingProgress> {
    return this.unitContentLoadProgress$[sequenceId];
  }

  newUnitStateData(unitDbKey: string, sequenceId: number, dataPartsAllString: string, unitStateDataType: string): void {
    this.unitStateDataParts[sequenceId] = dataPartsAllString;
    if (this.testMode.saveResponses) {
      this.unitStateDataToSave$.next({ unitDbKey, dataPartsAllString, unitStateDataType });
    }
  }

  addClearedCodeTestlet(testletId: string): void {
    if (this.clearCodeTestlets.indexOf(testletId) < 0) {
      this.clearCodeTestlets.push(testletId);
      if (this.testMode.saveResponses) {
        this.bs.updateTestState(
          this.testId,
          [<StateReportEntry>{
            key: TestStateKey.TESTLETS_CLEARED_CODE,
            timeStamp: Date.now(),
            content: JSON.stringify(this.clearCodeTestlets)
          }]
        );
      }
    }
  }

  updateUnitStatePresentationProgress(unitDbKey: string, unitSeqId: number, presentationProgress: string): void {
    let stateChanged = false;
    if (!this.unitPresentationProgressStates[unitSeqId] || this.unitPresentationProgressStates[unitSeqId] === 'none') {
      this.unitPresentationProgressStates[unitSeqId] = presentationProgress;
      stateChanged = true;
    } else if (this.unitPresentationProgressStates[unitSeqId] === 'some' && presentationProgress === 'complete') {
      this.unitPresentationProgressStates[unitSeqId] = presentationProgress;
      stateChanged = true;
    }
    if (stateChanged && this.testMode.saveResponses) {
      this.bs.updateUnitState(this.testId, unitDbKey, [<StateReportEntry>{
        key: UnitStateKey.PRESENTATION_PROGRESS, timeStamp: Date.now(), content: presentationProgress
      }]);
    }
  }

  newUnitStateResponseProgress(unitDbKey: string, unitSeqId: number, responseProgress: string): void {
    if (this.testMode.saveResponses) {
      if (
        !this.unitResponseProgressStates[unitSeqId] || this.unitResponseProgressStates[unitSeqId] !== responseProgress
      ) {
        this.unitResponseProgressStates[unitSeqId] = responseProgress;
        this.bs.updateUnitState(this.testId, unitDbKey, [<StateReportEntry>{
          key: UnitStateKey.RESPONSE_PROGRESS, timeStamp: Date.now(), content: responseProgress
        }]);
      }
    }
  }

  newUnitStateCurrentPage(
    unitDbKey: string, unitSequenceId: number, pageNr: number, pageId: string, pageCount: number
  ): void {
    this.unitStateCurrentPages[unitSequenceId] = pageId;
    if (this.testMode.saveResponses) {
      this.bs.updateUnitState(this.testId, unitDbKey, [
          <StateReportEntry>{ key: UnitStateKey.CURRENT_PAGE_NR, timeStamp: Date.now(), content: pageNr.toString() },
          <StateReportEntry>{ key: UnitStateKey.CURRENT_PAGE_ID, timeStamp: Date.now(), content: pageId },
          <StateReportEntry>{ key: UnitStateKey.PAGE_COUNT, timeStamp: Date.now(), content: pageCount.toString() }
      ]);
    }
  }

  startMaxTimer(testletId: string, timeLeftMinutes: number): void {
    if (this.maxTimeIntervalSubscription !== null) {
      this.maxTimeIntervalSubscription.unsubscribe();
    }
    this.maxTimeTimer$.next(new MaxTimerData(timeLeftMinutes, testletId, MaxTimerDataType.STARTED));
    this.currentMaxTimerTestletId = testletId;
    this.maxTimeIntervalSubscription = interval(1000)
      .pipe(
        takeUntil(
          timer(timeLeftMinutes * 60 * 1000)
        ),
        map(val => (timeLeftMinutes * 60) - val - 1)
      ).subscribe(
        val => {
          this.maxTimeTimer$.next(new MaxTimerData(val / 60, testletId, MaxTimerDataType.STEP));
        },
        e => console.log('maxTime onError: %s', e),
        () => {
          this.maxTimeTimer$.next(new MaxTimerData(0, testletId, MaxTimerDataType.ENDED));
          this.currentMaxTimerTestletId = '';
        }
      );
  }

  cancelMaxTimer(): void {
    if (this.maxTimeIntervalSubscription !== null) {
      this.maxTimeIntervalSubscription.unsubscribe();
      this.maxTimeIntervalSubscription = null;
      this.maxTimeTimer$.next(new MaxTimerData(0, this.currentMaxTimerTestletId, MaxTimerDataType.CANCELLED));
    }
    this.currentMaxTimerTestletId = '';
  }

  interruptMaxTimer(): void {
    if (this.maxTimeIntervalSubscription !== null) {
      this.maxTimeIntervalSubscription.unsubscribe();
      this.maxTimeIntervalSubscription = null;
      this.maxTimeTimer$.next(new MaxTimerData(0, this.currentMaxTimerTestletId, MaxTimerDataType.INTERRUPTED));
    }
    this.currentMaxTimerTestletId = '';
  }

  notifyNavigationDenied(sourceUnitSequenceId: number, reason: VeronaNavigationDeniedReason[]): void {
    this._navigationDenial.next({ sourceUnitSequenceId, reason });
  }

  terminateTest(logEntryKey: string, force: boolean, lockTest: boolean = false): void {
    if (
      (this.testStatus$.getValue() === TestControllerState.TERMINATED) ||
      (this.testStatus$.getValue() === TestControllerState.FINISHED)
    ) {
      // sometimes terminateTest get called two times from player
      return;
    }

    const oldTestStatus = this.testStatus$.getValue();
    this.testStatus$.next(TestControllerState.TERMINATED); // last state that will an can be logged

    this.router.navigate(['/r/test-starter'], { state: { force } })
      .then(navigationSuccessful => {
        if (!(navigationSuccessful || force)) {
          this.testStatus$.next(oldTestStatus); // navigation was denied, test continues
          return;
        }
        this.finishTest(logEntryKey, lockTest);
      });
  }

  private finishTest(logEntryKey: string, lockTest: boolean = false): void {
    if (lockTest) {
      this.bs.lockTest(this.testId, Date.now(), logEntryKey)
        .subscribe(bsOk => {
          this.testStatus$.next(bsOk ? TestControllerState.FINISHED : TestControllerState.ERROR);
        });
    } else {
      this.testStatus$.next(TestControllerState.FINISHED); // will not be logged, test is already locked maybe
    }
  }

  setUnitNavigationRequest(navString: string, force = false): void {
    if (!this.rootTestlet) {
      this.router.navigate([`/t/${this.testId}/status`], { skipLocationChange: true });
    } else {
      switch (navString) {
        case UnitNavigationTarget.ERROR:
        case UnitNavigationTarget.PAUSE:
          this.router.navigate([`/t/${this.testId}/status`], { skipLocationChange: true, state: { force } });
          break;
        case UnitNavigationTarget.NEXT:
          this.router.navigate([`/t/${this.testId}/u/${this.currentUnitSequenceId + 1}`], { state: { force } });
          break;
        case UnitNavigationTarget.PREVIOUS:
          this.router.navigate([`/t/${this.testId}/u/${this.currentUnitSequenceId - 1}`], { state: { force } });
          break;
        case UnitNavigationTarget.FIRST:
          this.router.navigate([`/t/${this.testId}/u/1`],
            { state: { force } });
          break;
        case UnitNavigationTarget.LAST:
          this.router.navigate([`/t/${this.testId}/u/${this.allUnitIds.length}`],
            { state: { force } });
          break;
        case UnitNavigationTarget.END:
          this.terminateTest(force ? 'BOOKLETLOCKEDforced' : 'BOOKLETLOCKEDbyTESTEE', force);
          break;

        default:
          this.router.navigate([`/t/${this.testId}/u/${navString}`], { state: { force } })
            .then(navOk => {
              if (!navOk) {
                console.log(`navigation failed ("${navString}")`);
              }
            });
          break;
      }
    }
  }

  errorOut(): void {
    this.totalLoadingProgress = 0;
    this.testStatus$.next(TestControllerState.ERROR);
    this.setUnitNavigationRequest(UnitNavigationTarget.ERROR);
  }

  pause(): void {
    this.interruptMaxTimer();
    this.testStatus$.next(TestControllerState.PAUSED);
    this.setUnitNavigationRequest(UnitNavigationTarget.PAUSE, true);
  }

  isUnitContentLoaded(sequenceId: number): boolean {
    return !!this.unitDefinitions[sequenceId];
  }
}

results matching ""

    No results matching ""