Skip to content

Commit

Permalink
[Frontend] Major refactoring of Test-Controller to avoid duplicate st…
Browse files Browse the repository at this point in the history
…ructures I
  • Loading branch information
paflov committed Jan 23, 2024
1 parent b8fb2d8 commit 22d94c7
Show file tree
Hide file tree
Showing 20 changed files with 786 additions and 700 deletions.
4 changes: 2 additions & 2 deletions docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ services:
- ../backend/config/no-cors.htaccess/:/var/www/.htaccess
- ../backend/config/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
- ../backend/config/local.php.ini:/usr/local/etc/php/conf.d/local.ini
environment:
TLS_ENABLED: ${TLS_ENABLED:-off}
# environment:
# TLS_ENABLED: ${TLS_ENABLED:-off}
ports:
- 9005:9003
depends_on:
Expand Down
158 changes: 28 additions & 130 deletions frontend/src/app/group-monitor/booklet/booklet.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ import { Observable, of } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { BackendService } from '../backend.service';
import {
Booklet, BookletError, BookletMetadata, isTestlet, isUnit, Restrictions, Testlet, Unit
isUnit, Booklet, Testlet, BookletError, Unit
} from '../group-monitor.interfaces';
import { BookletConfig } from '../../shared/classes/booklet-config.class';
import { BookletParserService } from '../../shared/services/bookletParser.service';
import {
BookletDef, ContextInBooklet, TestletDef, UnitDef
} from '../../shared/shared.module';

@Injectable()
export class BookletService {
export class BookletService extends BookletParserService<Unit, Testlet, Booklet> {
booklets: { [k: string]: Observable<Booklet | BookletError> } = {};

constructor(
private bs: BackendService
) { }
) {
super();
}

getBooklet(bookletName = ''): Observable<Booklet | BookletError> {
if (typeof this.booklets[bookletName] !== 'undefined') {
Expand All @@ -25,149 +30,42 @@ export class BookletService {
this.booklets[bookletName] = this.bs.getBooklet(bookletName)
.pipe(
// eslint-disable-next-line max-len
map((response: string | BookletError) => (typeof response === 'string' ? BookletService.parseBookletXml(response) : response)),
map((response: string | BookletError) => (typeof response === 'string' ? this.parseXml(response) : response)),
shareReplay(1)
);
}
return this.booklets[bookletName];
}

private static parseBookletXml(xmlString: string): Booklet | BookletError {
parseXml(xmlString: string): Booklet | BookletError {
try {
const domParser = new DOMParser();
const xmlStringWithOutBom = xmlString.replace(/^\uFEFF/gm, '');
const bookletElement = domParser.parseFromString(xmlStringWithOutBom, 'text/xml').documentElement;

if (bookletElement.nodeName !== 'Booklet') {
// console.warn('XML-root is not `Booklet`');
return { error: 'xml', species: null };
}

const units = BookletService.xmlGetChildIfExists(bookletElement, 'Units');
if (units == null) {
return { error: 'xml', species: null };
}

const metadata = BookletService.parseMetadata(bookletElement);
if (metadata == null) {
return { error: 'xml', species: null };
}

const parsedBooklet: Booklet = {
units: BookletService.parseTestlet(units),
metadata: metadata,
config: BookletService.parseBookletConfig(bookletElement),
species: ''
};
BookletService.addBookletStructureInformation(parsedBooklet);
return parsedBooklet;
return this.parseBookletXml(xmlString);
} catch (error) {
// console.warn('Error reading booklet XML:', error);
return { error: 'xml', species: null };
}
}

private static addBookletStructureInformation(booklet: Booklet): void {
booklet.species = BookletService.getBookletSpecies(booklet);
booklet.units.children
.filter(isTestlet)
.forEach((block: Testlet, index, blocks) => {
block.blockId = `block ${index + 1}`;
if (index < blocks.length - 1) {
block.nextBlockId = `block ${index + 2}`;
}
});
toBooklet(bookletDef: BookletDef<Testlet>): Booklet {
return Object.assign(bookletDef, {
species: this.getBookletSpecies(bookletDef)
});
}

private static getBookletSpecies(booklet: Booklet): string {
return `species: ${booklet.units.children.filter(testletOrUnit => !isUnit(testletOrUnit)).length}`;
toTestlet(testletDef: TestletDef<Testlet, Unit>, elem: Element, context: ContextInBooklet<Testlet>): Testlet {
return Object.assign(testletDef, {
descendantCount: this.xmlCountChildrenOfTagNames(elem, ['Unit']),
blockId: `block ${context.localIndexOfTestlets + 1}`,
nextBlockId: `block ${context.localIndexOfTestlets + 2}`
});
}

private static parseBookletConfig(bookletElement: Element): BookletConfig {
const bookletConfigElements = BookletService.xmlGetChildIfExists(bookletElement, 'BookletConfig', true);
const bookletConfig = new BookletConfig();
if (bookletConfigElements) {
bookletConfig.setFromXml(bookletConfigElements);
}
return bookletConfig;
// eslint-disable-next-line class-methods-use-this
toUnit(unitDef: UnitDef): Unit {
return unitDef;
}

private static parseMetadata(bookletElement: Element): BookletMetadata | null {
const metadataElement = BookletService.xmlGetChildIfExists(bookletElement, 'Metadata');
if (!metadataElement) {
return null;
}
return {
id: BookletService.xmlGetChildTextIfExists(metadataElement, 'Id'),
label: BookletService.xmlGetChildTextIfExists(metadataElement, 'Label'),
description: BookletService.xmlGetChildTextIfExists(metadataElement, 'Description', true)
};
}

private static parseTestlet(testletElement: Element): Testlet {
return {
id: testletElement.getAttribute('id') || '',
label: testletElement.getAttribute('label') || '',
restrictions: BookletService.parseRestrictions(testletElement),
children: BookletService.xmlGetDirectChildrenByTagName(testletElement, ['Unit', 'Testlet'])
.map(BookletService.parseUnitOrTestlet),
descendantCount: BookletService.xmlCountChildrenOfTagNames(testletElement, ['Unit'])
};
}

private static parseUnitOrTestlet(unitOrTestletElement: Element): (Unit | Testlet) {
if (unitOrTestletElement.tagName === 'Unit') {
return {
id: unitOrTestletElement.getAttribute('alias') || unitOrTestletElement.getAttribute('id') || '',
label: unitOrTestletElement.getAttribute('label') || '',
labelShort: unitOrTestletElement.getAttribute('labelshort') || ''
};
}
return BookletService.parseTestlet(unitOrTestletElement);
}

private static parseRestrictions(testletElement: Element): Restrictions {
const restrictions: Restrictions = {};
const restrictionsElement = BookletService.xmlGetChildIfExists(testletElement, 'Restrictions', true);
if (!restrictionsElement) {
return restrictions;
}
const codeToEnterElement = restrictionsElement.querySelector('CodeToEnter');
if (codeToEnterElement) {
restrictions.codeToEnter = {
code: codeToEnterElement.getAttribute('code') || '',
message: codeToEnterElement.textContent || ''
};
}
const timeMaxElement = restrictionsElement.querySelector('TimeMax');
if (timeMaxElement) {
restrictions.timeMax = {
minutes: parseFloat(timeMaxElement.getAttribute('minutes') || '')
};
}
return restrictions;
}

private static xmlGetChildIfExists(element: Element, childName: string, isOptional = false): Element | null {
const elements = BookletService.xmlGetDirectChildrenByTagName(element, [childName]);
if (!elements.length && !isOptional) {
throw new Error(`Missing field: '${childName}'`);
}
return elements.length ? elements[0] : null;
}

private static xmlGetChildTextIfExists(element: Element, childName: string, isOptional = false): string {
const childElement = BookletService.xmlGetChildIfExists(element, childName, isOptional);
return (childElement && childElement.textContent) ? childElement.textContent : '';
}

private static xmlGetDirectChildrenByTagName(element: Element, tagNames: string[]): Element[] {
return [].slice.call(element.childNodes)
.filter((elem: Element) => (elem.nodeType === 1))
.filter((elem: Element) => (tagNames.indexOf(elem.tagName) > -1));
}

private static xmlCountChildrenOfTagNames(element: Element, tagNames: string[]): number {
return element.querySelectorAll(tagNames.join(', ')).length;
// eslint-disable-next-line class-methods-use-this
getBookletSpecies(booklet: BookletDef<Testlet>): string {
return `species: ${booklet.units.children.filter(testletOrUnit => !isUnit(testletOrUnit)).length}`;
}
}
9 changes: 5 additions & 4 deletions frontend/src/app/group-monitor/booklet/booklet.util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
Booklet, isTestlet, isUnit, Testlet, Unit
Booklet, isTestlet, isUnit, Testlet
} from '../group-monitor.interfaces';
import { UnitDef } from '../../shared/interfaces/booklet.interfaces';

export class BookletUtil {
static getFirstUnit(testletOrUnit: Testlet | Unit): Unit | null {
static getFirstUnit(testletOrUnit: Testlet | UnitDef): UnitDef | null {
while (!isUnit(testletOrUnit)) {
if (!testletOrUnit.children.length) {
return null;
Expand All @@ -14,9 +15,9 @@ export class BookletUtil {
return testletOrUnit;
}

static getFirstUnitOfBlock(blockId: string, booklet: Booklet): Unit | null {
static getFirstUnitOfBlock(blockId: string, booklet: Booklet): UnitDef | null {
for (let i = 0; i < booklet.units.children.length; i++) {
const child = booklet.units.children[i];
const child = booklet.units.children[i] as Testlet;
if (!isUnit(child) && (child.blockId === blockId)) {
return BookletUtil.getFirstUnit(child);
}
Expand Down
95 changes: 31 additions & 64 deletions frontend/src/app/group-monitor/group-monitor.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TestSessionChange } from 'testcenter-common/interfaces/test-session-change.interface';
// eslint-disable-next-line import/extensions
import { BookletConfig } from '../shared/shared.module';
import { BookletDef, TestletDef, UnitDef } from '../shared/shared.module';

export interface GroupMonitorConfig {
checkForIdleInterval: number;
Expand All @@ -22,59 +21,6 @@ export const TestSessionsSuperStates = ['monitor_group', 'demo', 'pending', 'loc
'connection_websocket', 'connection_polling', 'ok'] as const;
export type TestSessionSuperState = typeof TestSessionsSuperStates[number];

export interface Booklet {
metadata: BookletMetadata;
config: BookletConfig;
restrictions?: Restrictions;
units: Testlet;
species: string;
}

export interface BookletError {
error: 'xml' | 'missing-id' | 'missing-file' | 'general';
species: null;
}

export function isBooklet(bookletOrError: Booklet | BookletError): bookletOrError is Booklet {
return bookletOrError && !('error' in bookletOrError);
}

export interface BookletMetadata {
id: string;
label: string;
description: string;
owner?: string;
lastchange?: string;
status?: string;
project?: string;
}

export interface Testlet {
id: string;
label: string;
restrictions?: Restrictions;
children: (Unit | Testlet)[];
descendantCount: number;
blockId?: string;
nextBlockId?: string;
}

export interface Unit {
id: string;
label: string;
labelShort: string;
}

export interface Restrictions {
codeToEnter?: {
code: string;
message: string;
};
timeMax?: {
minutes: number
};
}

export type TestViewDisplayOptionKey = keyof TestViewDisplayOptions;

export interface TestSessionFilter {
Expand All @@ -99,16 +45,8 @@ export interface CheckingOptions {
autoCheckAll: boolean;
}

export function isUnit(testletOrUnit: Testlet | Unit): testletOrUnit is Unit {
return !('children' in testletOrUnit);
}

export function isTestlet(testletOrUnit: Testlet | Unit): testletOrUnit is Testlet {
return !isUnit(testletOrUnit);
}

export interface UnitContext {
unit?: Unit;
unit?: UnitDef;
parent?: Testlet;
ancestor?: Testlet;
indexGlobal: number;
Expand Down Expand Up @@ -150,3 +88,32 @@ export interface GotoCommandData {
firstUnitId: string
}
}

export type Unit = UnitDef;

export interface Booklet extends BookletDef<Testlet> {
species: string;
}

export interface Testlet extends TestletDef<Testlet, Unit> {
descendantCount: number;
blockId?: string;
nextBlockId?: string;
}

export function isUnit(testletOrUnit: Testlet | UnitDef): testletOrUnit is UnitDef {
return !('children' in testletOrUnit);
}

export function isTestlet(testletOrUnit: Testlet | UnitDef): testletOrUnit is Testlet {
return !isUnit(testletOrUnit);
}

export function isBooklet(bookletOrError: Booklet | BookletError): bookletOrError is Booklet {
return bookletOrError && !('error' in bookletOrError);
}

export interface BookletError {
error: 'xml' | 'missing-id' | 'missing-file' | 'general';
species: null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { TestBed, waitForAsync } from '@angular/core/testing';
import { Observable, of } from 'rxjs';
import { Pipe } from '@angular/core';
import {
Booklet, BookletError, CommandResponse, GroupMonitorConfig,
Selected, Testlet, TestSessionData, TestSessionFilter, TestSessionSetStats
BookletDef, BookletError, CommandResponse, GroupMonitorConfig,
Selected, TestletDef, TestSessionData, TestSessionFilter, TestSessionSetStats
} from '../group-monitor.interfaces';
import { BookletService } from '../booklet/booklet.service';
import { BackendService } from '../backend.service';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import {
} from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import {
Testlet, Unit, TestViewDisplayOptions,
Testlet as Testlet, TestViewDisplayOptions,
isUnit, Selected, TestSession, TestSessionSuperState
} from '../group-monitor.interfaces';
import { TestSessionUtil } from './test-session.util';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { superStates } from './super-states';
import { UnitDef } from '../../shared/interfaces/booklet.interfaces';

interface IconData {
icon: string,
Expand Down Expand Up @@ -41,10 +42,10 @@ export class TestSessionComponent {
hasState = TestSessionUtil.hasState;

// eslint-disable-next-line class-methods-use-this
getTestletType = (testletOrUnit: Unit | Testlet): 'testlet' | 'unit' => (isUnit(testletOrUnit) ? 'unit' : 'testlet');
getTestletType = (testletOrUnit: UnitDef | Testlet): 'testlet' | 'unit' => (isUnit(testletOrUnit) ? 'unit' : 'testlet');

// eslint-disable-next-line class-methods-use-this
trackUnits = (index: number, testlet: Testlet | Unit): string => testlet.id || index.toString();
trackUnits = (index: number, testlet: Testlet | UnitDef): string => testlet.id || index.toString();

mark(testletOrNull: Testlet | null = null): void {
if ((testletOrNull != null) && !testletOrNull.blockId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable object-curly-newline */
import { TestSessionUtil } from './test-session.util';
import { unitTestExampleBooklets } from '../unit-test-example-data.spec';
import { Testlet, UnitContext } from '../group-monitor.interfaces';
import { TestletDef, UnitContext } from '../group-monitor.interfaces';

describe('TestSessionUtil', () => {
describe('getCurrent()', () => {
Expand Down
Loading

0 comments on commit 22d94c7

Please sign in to comment.