diff --git a/package-lock.json b/package-lock.json index 04fcb069ad..6f62973c84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "ndb-core", "version": "0.0.0", "hasInstallScript": true, "license": "GPL-3.0", @@ -33,20 +34,16 @@ "deep-object-diff": "^1.1.0", "faker": "^5.5.3", "flag-icon-css": "^3.5.0", - "idb": "^6.1.5", "json-query": "^2.2.2", "lodash": "^4.17.21", "md5": "^2.3.0", "moment": "^2.29.1", - "ngx-filter-pipe": "^2.1.2", "ngx-markdown": "^11.1.3", "ngx-papaparse": "^5.0.0", "pouchdb-adapter-memory": "^7.2.2", "pouchdb-browser": "^7.2.2", "reflect-metadata": "^0.1.13", - "reveal": "0.0.4", "rxjs": "^6.6.7", - "stream": "0.0.2", "tslib": "^2.3.1", "uuid": "^8.3.2", "webdav": "^4.7.0", @@ -13410,11 +13407,6 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, - "node_modules/emitter-component": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.1.tgz", - "integrity": "sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY=" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -16657,11 +16649,6 @@ "postcss": "^8.1.0" } }, - "node_modules/idb": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/idb/-/idb-6.1.5.tgz", - "integrity": "sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw==" - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -20574,22 +20561,6 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, - "node_modules/ngx-filter-pipe": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ngx-filter-pipe/-/ngx-filter-pipe-2.1.2.tgz", - "integrity": "sha512-YEXvjEw+Mpg5jL+yqSnFWKiY0P9XtRAJ2Dk3n9sC4stnsuhPzPRwIkF58aBvqYfoi3vrb7KQFImgbmfFAQqnFw==", - "dependencies": { - "tslib": "^1.7.1" - }, - "peerDependencies": { - "@angular/core": ">=5.0.0" - } - }, - "node_modules/ngx-filter-pipe/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, "node_modules/ngx-i18nsupport": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/ngx-i18nsupport/-/ngx-i18nsupport-0.17.1.tgz", @@ -24805,11 +24776,6 @@ "node": ">=0.10.0" } }, - "node_modules/reveal": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/reveal/-/reveal-0.0.4.tgz", - "integrity": "sha1-HBQoGTNZ+wO8ApE5Bi6in49pHYU=" - }, "node_modules/rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", @@ -26818,14 +26784,6 @@ "react-dom": "^16.8.0 || ^17.0.0" } }, - "node_modules/stream": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", - "integrity": "sha1-f1Nj8Ff2WSxVlfALyAon9c7B8O8=", - "dependencies": { - "emitter-component": "^1.1.1" - } - }, "node_modules/stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -42115,11 +42073,6 @@ } } }, - "emitter-component": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.1.tgz", - "integrity": "sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY=" - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -44718,11 +44671,6 @@ "dev": true, "requires": {} }, - "idb": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/idb/-/idb-6.1.5.tgz", - "integrity": "sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw==" - }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -47733,21 +47681,6 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, - "ngx-filter-pipe": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ngx-filter-pipe/-/ngx-filter-pipe-2.1.2.tgz", - "integrity": "sha512-YEXvjEw+Mpg5jL+yqSnFWKiY0P9XtRAJ2Dk3n9sC4stnsuhPzPRwIkF58aBvqYfoi3vrb7KQFImgbmfFAQqnFw==", - "requires": { - "tslib": "^1.7.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, "ngx-i18nsupport": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/ngx-i18nsupport/-/ngx-i18nsupport-0.17.1.tgz", @@ -51015,11 +50948,6 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, - "reveal": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/reveal/-/reveal-0.0.4.tgz", - "integrity": "sha1-HBQoGTNZ+wO8ApE5Bi6in49pHYU=" - }, "rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", @@ -52648,14 +52576,6 @@ "ts-dedent": "^2.1.1" } }, - "stream": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", - "integrity": "sha1-f1Nj8Ff2WSxVlfALyAon9c7B8O8=", - "requires": { - "emitter-component": "^1.1.1" - } - }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", diff --git a/package.json b/package.json index 1758cd39eb..39fbcd1e0d 100644 --- a/package.json +++ b/package.json @@ -47,20 +47,16 @@ "deep-object-diff": "^1.1.0", "faker": "^5.5.3", "flag-icon-css": "^3.5.0", - "idb": "^6.1.5", "json-query": "^2.2.2", "lodash": "^4.17.21", "md5": "^2.3.0", "moment": "^2.29.1", - "ngx-filter-pipe": "^2.1.2", "ngx-markdown": "^11.1.3", "ngx-papaparse": "^5.0.0", "pouchdb-adapter-memory": "^7.2.2", "pouchdb-browser": "^7.2.2", "reflect-metadata": "^0.1.13", - "reveal": "0.0.4", "rxjs": "^6.6.7", - "stream": "0.0.2", "tslib": "^2.3.1", "uuid": "^8.3.2", "webdav": "^4.7.0", diff --git a/src/app/child-dev-project/attendance/attendance-migration/attendance-migration.service.spec.ts b/src/app/child-dev-project/attendance/attendance-migration/attendance-migration.service.spec.ts deleted file mode 100644 index 7d27e58fea..0000000000 --- a/src/app/child-dev-project/attendance/attendance-migration/attendance-migration.service.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { TestBed } from "@angular/core/testing"; -import { AttendanceMigrationService } from "./attendance-migration.service"; -import { AttendanceMonth } from "../model/attendance-month"; -import { AttendanceStatus } from "../model/attendance-status"; -import { Database } from "../../../core/database/database"; -import { EntitySchemaService } from "../../../core/entity/schema/entity-schema.service"; -import { defaultAttendanceStatusTypes } from "../../../core/config/default-config/default-attendance-status-types"; -import { EntityModule } from "../../../core/entity/entity.module"; -import { expectEntitiesToMatch } from "../../../utils/expect-entity-data.spec"; -import { EventNote } from "../model/event-note"; -import { ChildrenService } from "../../children/children.service"; -import { PouchDatabase } from "../../../core/database/pouch-database"; - -describe("AttendanceMigrationService", () => { - let service: AttendanceMigrationService; - let entitySchemaService: EntitySchemaService; - let testDatabase: PouchDatabase; - - beforeEach(async () => { - testDatabase = PouchDatabase.createWithInMemoryDB(); - - TestBed.configureTestingModule({ - imports: [EntityModule], - providers: [ - AttendanceMigrationService, - { - provide: ChildrenService, - useValue: jasmine.createSpyObj(["queryRelationsOf"]), - }, - { provide: Database, useValue: testDatabase }, - ], - }); - service = TestBed.inject(AttendanceMigrationService); - entitySchemaService = TestBed.inject(EntitySchemaService); - }); - - afterEach(async () => { - await testDatabase.destroy(); - }); - - it("should create events for each existing attendance-day in attendance-month", async () => { - const testChild = "1"; - const testInstitution = "school"; - - const old = AttendanceMonth.createAttendanceMonth( - testChild, - testInstitution - ); - old.month = new Date("2020-01-01"); - old.dailyRegister[4].status = AttendanceStatus.EXCUSED; - old.dailyRegister[4].remarks = "some remark"; - old.dailyRegister[5].status = AttendanceStatus.ABSENT; - old.dailyRegister[7].status = AttendanceStatus.PRESENT; - - const expectedNotes: EventNote[] = [ - EventNote.create(old.dailyRegister[4].date, "School"), - EventNote.create(old.dailyRegister[5].date, "School"), - EventNote.create(old.dailyRegister[7].date, "School"), - ]; - for (const event of expectedNotes) { - event.category = service.activities.school.type; - event.relatesTo = service.activities.school._id; - event.children = [testChild]; - } - expectedNotes[0].getAttendance( - testChild - ).status = defaultAttendanceStatusTypes.find((t) => t.shortName === "E"); - expectedNotes[0].getAttendance(testChild).remarks = "some remark"; - expectedNotes[1].getAttendance( - testChild - ).status = defaultAttendanceStatusTypes.find((t) => t.shortName === "A"); - expectedNotes[2].getAttendance( - testChild - ).status = defaultAttendanceStatusTypes.find((t) => t.shortName === "P"); - - await service.createEventsForAttendanceMonth(old); - - await expectEntitiesToMatch(service.existingEvents, expectedNotes, true); - }); - - it("should add to existing event for the same activity and date", async () => { - const testChild1 = "1"; - const testChild2 = "2"; - const testInstitution = "school"; - - const old1 = AttendanceMonth.createAttendanceMonth( - testChild1, - testInstitution - ); - old1.month = new Date("2020-01-01"); - old1.dailyRegister[0].status = AttendanceStatus.ABSENT; - - const old2 = AttendanceMonth.createAttendanceMonth( - testChild2, - testInstitution - ); - old2.month = new Date("2020-01-01"); - old2.dailyRegister[0].status = AttendanceStatus.PRESENT; - - const expectedNotes: EventNote[] = [ - EventNote.create(old1.dailyRegister[0].date, "School"), - ]; - for (const event of expectedNotes) { - event.category = service.activities.school.type; - event.relatesTo = service.activities.school._id; - event.children = [testChild1, testChild2]; - } - expectedNotes[0].getAttendance( - testChild1 - ).status = defaultAttendanceStatusTypes.find((t) => t.shortName === "A"); - expectedNotes[0].getAttendance( - testChild2 - ).status = defaultAttendanceStatusTypes.find((t) => t.shortName === "P"); - - await service.createEventsForAttendanceMonth(old1); - await service.createEventsForAttendanceMonth(old2); - - await expectEntitiesToMatch(service.existingEvents, expectedNotes, true); - }); -}); diff --git a/src/app/child-dev-project/attendance/attendance-migration/attendance-migration.service.ts b/src/app/child-dev-project/attendance/attendance-migration/attendance-migration.service.ts deleted file mode 100644 index 295942938f..0000000000 --- a/src/app/child-dev-project/attendance/attendance-migration/attendance-migration.service.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Injectable } from "@angular/core"; -import { EntityMapperService } from "../../../core/entity/entity-mapper.service"; -import { RecurringActivity } from "../model/recurring-activity"; -import { AttendanceMonth } from "../model/attendance-month"; -import { defaultInteractionTypes } from "../../../core/config/default-config/default-interaction-types"; -import { AttendanceStatus } from "../model/attendance-status"; -import { defaultAttendanceStatusTypes } from "../../../core/config/default-config/default-attendance-status-types"; -import { AttendanceService } from "../attendance.service"; -import moment from "moment"; -import { EventNote } from "../model/event-note"; -import { Note } from "../../notes/model/note"; - -@Injectable({ - providedIn: "root", -}) -export class AttendanceMigrationService { - activities: { [key: string]: RecurringActivity } = { - school: Object.assign(new RecurringActivity("school"), { - entityId: "school", - title: $localize`School`, - type: defaultInteractionTypes.find((t) => t.id === "SCHOOL_CLASS"), - }), - coaching: Object.assign(new RecurringActivity("coaching"), { - entityId: "coaching", - title: $localize`Coaching`, - type: defaultInteractionTypes.find((t) => t.id === "COACHING_CLASS"), - }), - }; - - existingEvents: EventNote[] = []; - - constructor( - private entityMapper: EntityMapperService, - private attendanceService: AttendanceService - ) {} - - async createEventsForAllAttendanceMonths() { - await this.checkOrCreateActivities(); - - this.existingEvents = await this.entityMapper.loadType(EventNote); - - const months = await this.entityMapper.loadType(AttendanceMonth); - console.log( - "starting to migrate attendance-month records:" + months.length - ); - for (let i = 0; i < months.length; i++) { - await this.createEventsForAttendanceMonth(months[i]); - if (i % 100 === 1) { - console.log("finished " + (i + 1)); - } - } - console.log("parsed all AttendanceMonths"); - - for (const e of this.existingEvents) { - await this.entityMapper.save(e); - } - console.log("wrote all events to database"); - - console.log("updating activity participants"); - await this.addStudentsToActivityForAllAttendanceMonths(); - console.log("DONE"); - } - - async addStudentsToActivityForAllAttendanceMonths() { - await this.checkOrCreateActivities(); - - const months = await this.entityMapper.loadType(AttendanceMonth); - for (const month of months) { - if (!this.activities.hasOwnProperty(month.institution)) { - console.warn( - "cannot migrate attendance month because of unknown institution", - month - ); - continue; - } - - this.activities[month.institution].participants.push(month.student); - } - - const unique = (value, index, self) => { - return self.indexOf(value) === index; - }; - for (const activity of Object.values(this.activities)) { - activity.participants = activity.participants.filter(unique); - await this.entityMapper.save(activity); - } - } - - private async checkOrCreateActivities() { - for (const key of Object.keys(this.activities)) { - this.activities[key] = await this.entityMapper - .load(RecurringActivity, this.activities[key].getId()) - .catch(async (err) => { - if (err.status === 404) { - await this.entityMapper.save(this.activities[key]); - return this.entityMapper.load( - RecurringActivity, - this.activities[key].getId() - ); - } else { - throw err; - } - }); - } - } - async createEventsForAttendanceMonth(old: AttendanceMonth) { - if (!this.activities.hasOwnProperty(old.institution)) { - console.warn( - "cannot migrate attendance month because of unknown institution", - old - ); - return; - } - - const existingActivityEvents = this.existingEvents.filter( - (e) => e.relatesTo === this.activities[old.institution]._id - ); - - for (const day of old.dailyRegister) { - if (day.status === AttendanceStatus.UNKNOWN) { - // skip status without actual information - continue; - } - - let newEvent = existingActivityEvents.find((e) => - moment(e.date).isSame(day.date, "day") - ); - if (!newEvent) { - // no Note in the database yet - create a new event - newEvent = await this.attendanceService.createEventForActivity( - this.activities[old.institution], - day.date - ); - newEvent.children = []; - this.existingEvents.push(newEvent); - } - - newEvent.children.push(old.student); - newEvent.getAttendance( - old.student - ).status = defaultAttendanceStatusTypes.find( - (t) => t.shortName === day.status - ); - newEvent.getAttendance(old.student).remarks = day.remarks; - } - } - - async changeNotesToEventNotes() { - const oldEventNotes = (await this.entityMapper.loadType(Note)).filter( - (n) => n.relatesTo - ); - - for (const note of oldEventNotes) { - const newEvent = new EventNote(note.getId()); - newEvent.date = note.date; - newEvent.subject = note.subject; - newEvent.children = note.children; - newEvent.relatesTo = note.relatesTo; - newEvent.category = note.category; - // @ts-ignore - newEvent.childrenAttendance = note.childrenAttendance; - - await this.entityMapper.remove(note); - await this.entityMapper.save(newEvent); - console.log("event-note migrated", newEvent.getId()); - } - } -} diff --git a/src/app/child-dev-project/attendance/demo-data/demo-activity-generator.service.ts b/src/app/child-dev-project/attendance/demo-data/demo-activity-generator.service.ts index 6e0b47bdd7..c8b2711c3c 100644 --- a/src/app/child-dev-project/attendance/demo-data/demo-activity-generator.service.ts +++ b/src/app/child-dev-project/attendance/demo-data/demo-activity-generator.service.ts @@ -9,7 +9,7 @@ import { User } from "../../../core/user/user"; import { defaultInteractionTypes } from "../../../core/config/default-config/default-interaction-types"; /** - * Generate AttendanceMonth entities for the last 15 months + * Generate RecurringActivity entities * Builds upon the generated demo Child entities. */ @Injectable() diff --git a/src/app/child-dev-project/attendance/model/attendance-day.spec.ts b/src/app/child-dev-project/attendance/model/attendance-day.spec.ts deleted file mode 100644 index 3daabfa842..0000000000 --- a/src/app/child-dev-project/attendance/model/attendance-day.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * This file is part of ndb-core. - * - * ndb-core is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ndb-core is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ndb-core. If not, see . - */ - -import { AttendanceDay } from "./attendance-day"; -import { waitForAsync } from "@angular/core/testing"; -import { EntitySchemaService } from "../../../core/entity/schema/entity-schema.service"; -import { AttendanceMonth } from "./attendance-month"; - -describe("AttendanceDay", () => { - let entitySchemaService: EntitySchemaService; - - beforeEach( - waitForAsync(() => { - entitySchemaService = new EntitySchemaService(); - }) - ); - - it("(AttendanceMonth) saves date values as only YYYY-MM-dd", () => { - const month = new Date("2018-01-01"); - const entity = new AttendanceMonth(""); - entity.month = month; - - const data = entitySchemaService.transformEntityToDatabaseFormat(entity); - expect(data.dailyRegister[1].date).toBe("2018-01-02"); // dailyRegister array is zero-based, index 1 is second day - - const loadedEntity = new AttendanceMonth(""); - entitySchemaService.loadDataIntoEntity(loadedEntity, data); - expect(loadedEntity.dailyRegister[1].date).toEqual(new Date("2018-01-02")); - }); -}); diff --git a/src/app/child-dev-project/attendance/model/attendance-day.ts b/src/app/child-dev-project/attendance/model/attendance-day.ts deleted file mode 100644 index ee37929aa2..0000000000 --- a/src/app/child-dev-project/attendance/model/attendance-day.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * This file is part of ndb-core. - * - * ndb-core is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ndb-core is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ndb-core. If not, see . - */ - -import { DatabaseField } from "../../../core/entity/database-field.decorator"; -import { AttendanceStatus } from "./attendance-status"; - -/** - * @deprecated Use Event entities instead of the embedded AttendanceDay and AttendanceMonth - */ -export class AttendanceDay { - @DatabaseField({ dataType: "date-only" }) date: Date; - @DatabaseField() status: AttendanceStatus; - @DatabaseField() remarks: string = ""; - - constructor(date: Date, status: AttendanceStatus = AttendanceStatus.UNKNOWN) { - this.date = date; - this.status = status; - } -} diff --git a/src/app/child-dev-project/attendance/model/attendance-month.spec.ts b/src/app/child-dev-project/attendance/model/attendance-month.spec.ts deleted file mode 100644 index 4393506d2f..0000000000 --- a/src/app/child-dev-project/attendance/model/attendance-month.spec.ts +++ /dev/null @@ -1,235 +0,0 @@ -/* - * This file is part of ndb-core. - * - * ndb-core is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ndb-core is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ndb-core. If not, see . - */ - -import { AttendanceMonth, daysInMonth } from "./attendance-month"; -import { waitForAsync } from "@angular/core/testing"; -import { Entity } from "../../../core/entity/model/entity"; -import { EntitySchemaService } from "../../../core/entity/schema/entity-schema.service"; -import { WarningLevel } from "../../../core/entity/model/warning-level"; - -describe("AttendanceMonth", () => { - const ENTITY_TYPE = "AttendanceMonth"; - let entitySchemaService: EntitySchemaService; - - beforeEach( - waitForAsync(() => { - entitySchemaService = new EntitySchemaService(); - }) - ); - - it("has correct _id and entityId and type", function () { - const id = "test1"; - const entity = new AttendanceMonth(id); - - expect(entity.getId()).toBe(id); - expect(Entity.extractEntityIdFromId(entity._id)).toBe(id); - }); - - it("has correct type/prefix", function () { - const id = "test1"; - const entity = new AttendanceMonth(id); - - expect(entity.getType()).toBe(ENTITY_TYPE); - expect(Entity.extractTypeFromId(entity._id)).toBe(ENTITY_TYPE); - }); - - it("has all and only defined schema fields in rawData", function () { - const id = "test1"; - const expectedData = { - _id: ENTITY_TYPE + ":" + id, - - student: "1", - institution: "school", - month: "2019-01", - remarks: "more notes", - daysWorking: 25, - daysAttended: 20, - daysExcused: 1, - daysLate: 1, - dailyRegister: [], - - searchIndices: [], - }; - - const entity = new AttendanceMonth(id); - entity.student = expectedData.student; - entity.institution = expectedData.institution; - entity.month = new Date(expectedData.month); - entity.remarks = expectedData.remarks; - entity.daysWorking = expectedData.daysWorking; - entity.daysAttended = expectedData.daysAttended; - entity.daysExcused = expectedData.daysExcused; - entity.daysLate = expectedData.daysLate; - - const rawData = entitySchemaService.transformEntityToDatabaseFormat(entity); - - expect(rawData.dailyRegister.length).toBe(31); - expectedData.dailyRegister = rawData.dailyRegister; // simplify the overall comparison by ignoring dailyRegister diff - expect(rawData).toEqual(expectedData); - }); - - it("calculates attendance percentage", () => { - const working = 10; - const attended = 1; - - const entity = new AttendanceMonth(""); - entity.month = new Date("2018-01-01"); - entity.daysWorking = working; - entity.daysAttended = attended; - - expect(entity.getAttendancePercentage()).toBe(attended / working); - }); - - it("gives WarningLevel for low attendance", () => { - const working = 10; - const attended = 1; - - const entity = new AttendanceMonth(""); - entity.month = new Date("2018-01-01"); - entity.daysWorking = working; - entity.daysAttended = attended; - - expect(entity.getWarningLevel()).toBe(WarningLevel.URGENT); - }); - - it("has dailyRegister array after creation", () => { - const entity = new AttendanceMonth(""); - - expect(entity.dailyRegister.length).toBeGreaterThan(-1); - }); - - it("adds/removes dailyRegister entries on month change", () => { - const month = new Date("2018-01-01"); - - const entity = new AttendanceMonth(""); - entity.month = month; - - expect(entity.dailyRegister.length).toBe(daysInMonth(entity.month)); - }); - - it("adds/removes dailyRegister entries on load", () => { - const month = new Date("2018-01-01"); - - const entity = new AttendanceMonth(""); - const data = { month: month, daysWorking: 10, daysAttended: 7 }; - entitySchemaService.loadDataIntoEntity(entity, data); - - expect(entity.dailyRegister.length).toBe(daysInMonth(entity.month)); - }); - - it("updates dailyRegister entries' date on month change (shorter)", () => { - const month = new Date("2018-01-01"); - const month2 = new Date("2018-02-01"); - - const entity = new AttendanceMonth(""); - - entity.month = month; - expect(entity.dailyRegister.length).toBe(daysInMonth(entity.month)); - - entity.month = month2; - expect(entity.dailyRegister.length).toBe(daysInMonth(entity.month)); - for (let i = 1; i <= daysInMonth(entity.month); i++) { - expect(entity.dailyRegister[i - 1].date).toEqual( - new Date(entity.month.getFullYear(), entity.month.getMonth(), i) - ); - } - }); - - it("updates dailyRegister entries' date on month change (longer)", () => { - const month = new Date("2018-02-01"); - const month2 = new Date("2018-01-01"); - - const entity = new AttendanceMonth(""); - - entity.month = month; - expect(entity.dailyRegister.length).toBe(daysInMonth(entity.month)); - - entity.month = month2; - expect(entity.dailyRegister.length).toBe(daysInMonth(entity.month)); - for (let i = 1; i <= daysInMonth(entity.month); i++) { - expect(entity.dailyRegister[i - 1].date).toEqual( - new Date(entity.month.getFullYear(), entity.month.getMonth(), i) - ); - } - }); - - it("saves & loads manually entered attendance values", () => { - const originalData = { - month: new Date("2018-01-01"), - daysWorking: 10, - daysAttended: 7, - daysExcused: 2, - daysLate: 4, - }; - const entity = new AttendanceMonth(""); - - entitySchemaService.loadDataIntoEntity(entity, originalData); - expect(entity.daysWorking).toBe(originalData.daysWorking); - expect(entity.daysAttended).toBe(originalData.daysAttended); - expect(entity.daysExcused).toBe(originalData.daysExcused); - expect(entity.daysLate).toBe(originalData.daysLate); - - const data = entitySchemaService.transformEntityToDatabaseFormat(entity); - expect(data.daysWorking).toBe(originalData.daysWorking); - expect(data.daysAttended).toBe(originalData.daysAttended); - expect(data.daysExcused).toBe(originalData.daysExcused); - expect(data.daysLate).toBe(originalData.daysLate); - }); - - it("returns month as string in rawData", () => { - const month = new Date("2018-01-01"); - const entity = new AttendanceMonth(""); - entity.month = month; - - const rawData = entitySchemaService.transformEntityToDatabaseFormat(entity); - expect(typeof rawData.month).toBe("string"); - expect(rawData.month).toBe("2018-01"); - expect(rawData.p_month).toBeUndefined(); - }); - - it("loads month as date from rawData", () => { - const data = { - month: "2018-1", - }; - const entity = new AttendanceMonth(""); - - entitySchemaService.loadDataIntoEntity(entity, data); - expect(entity.month).toEqual(new Date(data.month)); - }); - - it("creates correct instance using static createAttendanceMonth", () => { - const testInstitution = "coaching"; - const testChildId = "childId1"; - const now = new Date(); - - const actualInstance = AttendanceMonth.createAttendanceMonth( - testChildId, - testInstitution - ); - - expect(actualInstance.student).toBe(testChildId); - expect(actualInstance.institution).toBe(testInstitution); - expect(actualInstance.month.getFullYear()).toBe(now.getFullYear()); - expect(actualInstance.month.getMonth()).toBe(now.getMonth()); - - expect(actualInstance.getId()).toContain(testChildId); - expect(actualInstance.getId()).toContain(testInstitution); - expect(actualInstance.getId()).toContain( - now.getFullYear() + "-" + (now.getMonth() + 1) - ); - }); -}); diff --git a/src/app/child-dev-project/attendance/model/attendance-month.ts b/src/app/child-dev-project/attendance/model/attendance-month.ts deleted file mode 100644 index bf0fbe58ff..0000000000 --- a/src/app/child-dev-project/attendance/model/attendance-month.ts +++ /dev/null @@ -1,239 +0,0 @@ -/* - * This file is part of ndb-core. - * - * ndb-core is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ndb-core is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ndb-core. If not, see . - */ - -import { Entity } from "../../../core/entity/model/entity"; -import { AttendanceDay } from "./attendance-day"; -import { DatabaseEntity } from "../../../core/entity/database-entity.decorator"; -import { DatabaseField } from "../../../core/entity/database-field.decorator"; -import { AttendanceStatus } from "./attendance-status"; -import { WarningLevel } from "../../../core/entity/model/warning-level"; - -/** - * @deprecated Use new system based on EventNote and RecurrentActivity instead - */ -@DatabaseEntity("AttendanceMonth") -export class AttendanceMonth extends Entity { - static readonly THRESHOLD_URGENT = 0.6; - static readonly THRESHOLD_WARNING = 0.8; - - public static createAttendanceMonth(childId: string, institution: string) { - const month = new Date(); - const newAtt = new AttendanceMonth( - childId + - "_" + - month.getFullYear() + - "-" + - (month.getMonth() + 1) + - "_" + - institution - ); - newAtt.month = month; - newAtt.student = childId; - newAtt.institution = institution; - return newAtt; - } - - @DatabaseField() student: string; // id of Child entity - @DatabaseField() remarks: string = ""; - @DatabaseField() institution: string; - - private p_month: Date; - @DatabaseField({ dataType: "month" }) - get month(): Date { - return this.p_month; - } - set month(value: Date) { - if (!(value instanceof Date)) { - console.warn( - "Trying to set invalid date " + - JSON.stringify(value) + - " to Entity " + - this._id - ); - return; - } - - if (value.getDate() !== 2) { - value.setDate(2); - } - this.p_month = new Date(value); - this.updateDailyRegister(); - } - - daysWorking_manuallyEntered: number; - @DatabaseField() - get daysWorking(): number { - if (this.daysWorking_manuallyEntered !== undefined) { - return this.daysWorking_manuallyEntered; - } else { - return this.getDaysWorkingFromDailyAttendance(); - } - } - - set daysWorking(value: number) { - this.daysWorking_manuallyEntered = value; - } - - daysAttended_manuallyEntered: number; - @DatabaseField() - get daysAttended(): number { - if (this.daysAttended_manuallyEntered !== undefined) { - return this.daysAttended_manuallyEntered; - } else { - return this.getDaysAttendedFromDailyAttendance(); - } - } - - set daysAttended(value: number) { - this.daysAttended_manuallyEntered = value; - } - - daysExcused_manuallyEntered: number; - @DatabaseField() - get daysExcused(): number { - if (this.daysExcused_manuallyEntered !== undefined) { - return this.daysExcused_manuallyEntered; - } else { - return this.getDaysExcusedFromDailyAttendance(); - } - } - - set daysExcused(value: number) { - this.daysExcused_manuallyEntered = value; - } - - daysLate_manuallyEntered: number; - @DatabaseField() - get daysLate(): number { - if (this.daysLate_manuallyEntered !== undefined) { - return this.daysLate_manuallyEntered; - } else { - return this.calculateFromDailyRegister(AttendanceStatus.LATE); - } - } - set daysLate(value: number) { - this.daysLate_manuallyEntered = value; - } - - overridden = false; // indicates individual override during bulk adding - - private _dailyRegister = new Array(); - set dailyRegister(value: AttendanceDay[]) { - if (!value) { - return; - } - - for (const attDay of value) { - if (typeof attDay.date.getTime !== "function") { - attDay.date = new Date(attDay.date); - } - } - this._dailyRegister = value; - } - @DatabaseField({ innerDataType: "schema-embed", additional: AttendanceDay }) - get dailyRegister(): AttendanceDay[] { - return this._dailyRegister; - } - - constructor(id: string) { - super(id); - this.month = new Date(); - } - - private updateDailyRegister() { - if (this.month === undefined) { - return; - } - - if (this.dailyRegister === undefined) { - this.dailyRegister = new Array(); - } - - const expectedDays = daysInMonth(this.month); - const currentDays = this.dailyRegister.length; - if (currentDays < expectedDays) { - for (let i = currentDays + 1; i <= expectedDays; i++) { - const date = new Date( - this.month.getFullYear(), - this.month.getMonth(), - i - ); - const day = new AttendanceDay(date); - this.dailyRegister.push(day); - } - } else if (currentDays > expectedDays) { - this.dailyRegister.splice(expectedDays); - } - - this.dailyRegister.forEach((day) => { - day.date.setMonth(this.month.getMonth()); - day.date.setFullYear(this.month.getFullYear()); - }); - } - - private calculateFromDailyRegister(status: AttendanceStatus) { - let count = 0; - this.dailyRegister.forEach((day) => { - if (day.status === status) { - count++; - } - }); - return count; - } - - public getDaysWorkingFromDailyAttendance() { - return ( - this.dailyRegister.length - - this.calculateFromDailyRegister(AttendanceStatus.HOLIDAY) - - this.calculateFromDailyRegister(AttendanceStatus.UNKNOWN) - ); - } - - public getDaysAttendedFromDailyAttendance() { - return ( - this.calculateFromDailyRegister(AttendanceStatus.PRESENT) + - this.calculateFromDailyRegister(AttendanceStatus.LATE) - ); - } - - public getDaysExcusedFromDailyAttendance() { - return this.calculateFromDailyRegister(AttendanceStatus.EXCUSED); - } - - public getDaysLateFromDailyAttendance() { - return this.calculateFromDailyRegister(AttendanceStatus.LATE); - } - - getAttendancePercentage() { - return this.daysAttended / (this.daysWorking - this.daysExcused); - } - - getWarningLevel(): WarningLevel { - const attendance = this.getAttendancePercentage(); - if (attendance < AttendanceMonth.THRESHOLD_URGENT) { - return WarningLevel.URGENT; - } else if (attendance < AttendanceMonth.THRESHOLD_WARNING) { - return WarningLevel.WARNING; - } else { - return WarningLevel.OK; - } - } -} - -export function daysInMonth(date: Date) { - return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); -} diff --git a/src/app/child-dev-project/attendance/model/attendance-status.ts b/src/app/child-dev-project/attendance/model/attendance-status.ts index 4e714c3c95..64958e15e1 100644 --- a/src/app/child-dev-project/attendance/model/attendance-status.ts +++ b/src/app/child-dev-project/attendance/model/attendance-status.ts @@ -1,17 +1,5 @@ import { ConfigurableEnumValue } from "../../../core/configurable-enum/configurable-enum.interface"; -/** - * @deprecated - */ -export enum AttendanceStatus { - UNKNOWN = "?", - HOLIDAY = "H", - ABSENT = "A", - PRESENT = "P", - LATE = "L", - EXCUSED = "E", -} - /** * logical type of an attendance status, i.e. how it will be considered for statistics and analysis. */ diff --git a/src/app/child-dev-project/children/child-photo-service/children-migration.service.spec.ts b/src/app/child-dev-project/children/child-photo-service/children-migration.service.spec.ts deleted file mode 100644 index f6ec043cbc..0000000000 --- a/src/app/child-dev-project/children/child-photo-service/children-migration.service.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { TestBed } from "@angular/core/testing"; - -import { ChildrenMigrationService } from "./children-migration.service"; -import { Database } from "../../../core/database/database"; -import { PouchDatabase } from "../../../core/database/pouch-database"; -import { Child } from "../model/child"; - -describe("ChildrenMigrationService", () => { - let service: ChildrenMigrationService; - let database: PouchDatabase; - - beforeEach(() => { - database = PouchDatabase.createWithInMemoryDB(); - TestBed.configureTestingModule({ - providers: [{ provide: Database, useValue: database }], - }); - service = TestBed.inject(ChildrenMigrationService); - }); - - afterEach(async () => { - await database.destroy(); - }); - - it("should be created", () => { - expect(service).toBeTruthy(); - }); - - it("should migrate children with old format", async () => { - await database.put({ - _id: `${Child.ENTITY_TYPE}:firstChild`, - photoFile: "oldFile1.jpg", - }); - await database.put({ - _id: `${Child.ENTITY_TYPE}:secondChild`, - photoFile: "oldFile2.jpg", - }); - await database.put({ - _id: `${Child.ENTITY_TYPE}:thirdChild`, - photo: "newFormat.jpg", - }); - - await service.migratePhotoFormat(); - - const firstChild = await database.get(`${Child.ENTITY_TYPE}:firstChild`); - expect(firstChild["photo"]).toEqual("oldFile1.jpg"); - const secondChild = await database.get(`${Child.ENTITY_TYPE}:secondChild`); - expect(secondChild["photo"]).toEqual("oldFile2.jpg"); - const thirdChild = await database.get(`${Child.ENTITY_TYPE}:thirdChild`); - expect(thirdChild["photo"]).toEqual("newFormat.jpg"); - }); -}); diff --git a/src/app/child-dev-project/children/child-photo-service/children-migration.service.ts b/src/app/child-dev-project/children/child-photo-service/children-migration.service.ts deleted file mode 100644 index d86fbee559..0000000000 --- a/src/app/child-dev-project/children/child-photo-service/children-migration.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from "@angular/core"; -import { Database } from "../../../core/database/database"; -import { Child } from "../model/child"; -import { LoggingService } from "../../../core/logging/logging.service"; - -@Injectable({ - providedIn: "root", -}) -export class ChildrenMigrationService { - constructor(private database: Database, private logging: LoggingService) {} - - async migratePhotoFormat(): Promise { - const oldFormatChildren = ( - await this.database.getAll(Child.ENTITY_TYPE + ":") - ).filter( - (child) => - child.hasOwnProperty("photoFile") && child["photoFile"].trim() !== "" - ); - oldFormatChildren.forEach((child) => { - const photoFile = child["photoFile"]; - delete child.photoFile; - child["photo"] = photoFile; - }); - - await Promise.all( - oldFormatChildren.map((child) => this.database.put(child)) - ); - this.logging.info(`migrated ${oldFormatChildren.length} objects`); - } -} diff --git a/src/app/child-dev-project/children/children-list/recent-attendance-blocks/recent-attendance-blocks.component.spec.ts b/src/app/child-dev-project/children/children-list/recent-attendance-blocks/recent-attendance-blocks.component.spec.ts index 37a93b198e..7d8417c063 100644 --- a/src/app/child-dev-project/children/children-list/recent-attendance-blocks/recent-attendance-blocks.component.spec.ts +++ b/src/app/child-dev-project/children/children-list/recent-attendance-blocks/recent-attendance-blocks.component.spec.ts @@ -7,7 +7,6 @@ import { } from "@angular/core/testing"; import { RecentAttendanceBlocksComponent } from "./recent-attendance-blocks.component"; -import { FilterPipeModule } from "ngx-filter-pipe"; import { Child } from "../../model/child"; import { AttendanceService } from "../../../attendance/attendance.service"; import { ActivityAttendance } from "../../../attendance/model/activity-attendance"; @@ -29,7 +28,6 @@ describe("RecentAttendanceBlocksComponent", () => { TestBed.configureTestingModule({ declarations: [RecentAttendanceBlocksComponent], - imports: [FilterPipeModule], providers: [ { provide: AttendanceService, useValue: mockAttendanceService }, ], diff --git a/src/app/child-dev-project/children/children.module.ts b/src/app/child-dev-project/children/children.module.ts index 3ce007effb..c8fccb73ad 100644 --- a/src/app/child-dev-project/children/children.module.ts +++ b/src/app/child-dev-project/children/children.module.ts @@ -45,7 +45,6 @@ import { NotesOfChildComponent } from "../notes/notes-of-child/notes-of-child.co import { SchoolsModule } from "../schools/schools.module"; import { EducationalMaterialComponent } from "../educational-material/educational-material-component/educational-material.component"; import { AserComponent } from "../aser/aser-component/aser.component"; -import { FilterPipeModule } from "ngx-filter-pipe"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { NotesDashboardComponent } from "../notes/dashboard-widgets/notes-dashboard/notes-dashboard.component"; import { HealthCheckupComponent } from "../health-checkup/health-checkup-component/health-checkup.component"; @@ -96,7 +95,6 @@ import { ExportModule } from "../../core/export/export.module"; MatChipsModule, BrowserAnimationsModule, ReactiveFormsModule, - FilterPipeModule, SchoolsModule, ReactiveFormsModule, MatDialogModule, diff --git a/src/app/child-dev-project/children/children.service.ts b/src/app/child-dev-project/children/children.service.ts index 2aa96fd264..fd22c27595 100644 --- a/src/app/child-dev-project/children/children.service.ts +++ b/src/app/child-dev-project/children/children.service.ts @@ -2,7 +2,6 @@ import { Injectable, Optional } from "@angular/core"; import { from, Observable, Subject } from "rxjs"; import { Child } from "./model/child"; import { EntityMapperService } from "../../core/entity/entity-mapper.service"; -import { AttendanceMonth } from "../attendance/model/attendance-month"; import { Note } from "../notes/model/note"; import { EducationalMaterial } from "../educational-material/model/educational-material"; import { Aser } from "../aser/model/aser"; @@ -28,9 +27,7 @@ export class ChildrenService { } public createDatabaseIndices() { - this.createAttendanceAnalysisIndex(); this.createNotesIndex(); - this.createAttendancesIndex(); this.createChildSchoolRelationIndex(); } @@ -47,10 +44,6 @@ export class ChildrenService { const childCurrentSchoolInfo = await this.getCurrentSchoolInfoForChild( loadedChild.getId() ); - await this.migrateToNewChildSchoolRelationModel( - loadedChild, - childCurrentSchoolInfo - ); loadedChild.schoolClass = childCurrentSchoolInfo.schoolClass; loadedChild.schoolId = childCurrentSchoolInfo.schoolId; } @@ -61,43 +54,6 @@ export class ChildrenService { return results; } - /** - * DATA MODEL UPGRADE - * Check if the Child Entity still contains direct links to schoolId and schoolClass - * and create a new ChildSchoolRelation if necessary. - * @param loadedChild Child entity to be checked and migrated - * @param childCurrentSchoolInfo Currently available school information according to new data model from ChildSchoolRelation entities - */ - private async migrateToNewChildSchoolRelationModel( - loadedChild: Child, - childCurrentSchoolInfo: { schoolId: string; schoolClass: string } - ) { - if (!loadedChild.schoolClass && !loadedChild.schoolId) { - // no data from old model -> skip migration - return; - } - - if ( - loadedChild.schoolId !== childCurrentSchoolInfo.schoolId || - loadedChild.schoolClass !== childCurrentSchoolInfo.schoolClass - ) { - // generate a ChildSchoolRelation entity from the information of the previous data model - const autoMigratedChildSchoolRelation = new ChildSchoolRelation(); - autoMigratedChildSchoolRelation.childId = loadedChild.getId(); - autoMigratedChildSchoolRelation.schoolId = loadedChild.schoolId; - autoMigratedChildSchoolRelation.schoolClass = loadedChild.schoolClass; - await this.entityMapper.save(autoMigratedChildSchoolRelation); - this.logger?.debug( - "migrated Child entity to new ChildSchoolRelation model " + - loadedChild._id - ); - console.log(autoMigratedChildSchoolRelation); - } - - // save the Child entity to remove the deprecated attributes from the doc in the database - await this.entityMapper.save(loadedChild); - } - /** * returns an observable which retrieves a single child and loads its photo * @param id id of child @@ -117,64 +73,6 @@ export class ChildrenService { return from(promise); } - getAttendances(): Observable { - return from(this.entityMapper.loadType(AttendanceMonth)); - } - - getAttendancesOfChild(childId: string): Observable { - const promise = this.dbIndexing.queryIndexDocs( - AttendanceMonth, - "attendances_index/by_child", - childId - ); - - return from(promise); - } - - getAttendancesOfMonth(month: Date): Observable { - const monthString = - month.getFullYear().toString() + "-" + (month.getMonth() + 1).toString(); - const promise = this.dbIndexing.queryIndexDocs( - AttendanceMonth, - "attendances_index/by_month", - monthString - ); - - return from(promise); - } - - /** - * @deprecated use AttendanceService instead. This can be removed after all AttendanceMigrationService tasks are completed. - * @private - */ - private createAttendancesIndex(): Promise { - const designDoc = { - _id: "_design/attendances_index", - views: { - by_child: { - map: - "(doc) => { " + - 'if (!doc._id.startsWith("' + - AttendanceMonth.ENTITY_TYPE + - '")) return;' + - "emit(doc.student); " + - "}", - }, - by_month: { - map: - "(doc) => { " + - 'if (!doc._id.startsWith("' + - AttendanceMonth.ENTITY_TYPE + - '")) return;' + - "emit(doc.month); " + - "}", - }, - }, - }; - - return this.dbIndexing.createIndex(designDoc); - } - private createChildSchoolRelationIndex(): Promise { const designDoc = { _id: "_design/childSchoolRelations_index", @@ -248,69 +146,6 @@ export class ChildrenService { ); } - async queryAttendanceLast3Months() { - return this.dbIndexing.queryIndexStats("avg_attendance_index/three_months"); - } - - async queryAttendanceLastMonth() { - return this.dbIndexing.queryIndexStats("avg_attendance_index/last_month"); - } - - private createAttendanceAnalysisIndex(): Promise { - const designDoc = { - _id: "_design/avg_attendance_index", - views: { - three_months: { - map: this.getAverageAttendanceMapFunction(), - reduce: "_stats", - }, - last_month: { - map: this.getLastAverageAttendanceMapFunction(), - reduce: "_stats", - }, - }, - }; - - return this.dbIndexing.createIndex(designDoc); - } - - private getAverageAttendanceMapFunction() { - return ( - "(doc) => {" + - 'if (!doc._id.startsWith("AttendanceMonth:") ) { return; }' + - "if (!isWithinLast3Months(new Date(doc.month), new Date())) { return; }" + - "var attendance = (doc.daysAttended / (doc.daysWorking - doc.daysExcused));" + - "if (!Number.isNaN(attendance)) { emit(doc.student, attendance); }" + - "function isWithinLast3Months(date, now) {" + - " let months;" + - " months = (now.getFullYear() - date.getFullYear()) * 12;" + - " months -= date.getMonth();" + - " months += now.getMonth();" + - " if (months < 0) { return false; }" + - " return months <= 3;" + - "}" + - "}" - ); - } - - private getLastAverageAttendanceMapFunction() { - return ( - "(doc) => {" + - 'if (!doc._id.startsWith("AttendanceMonth:")) { return; }' + - "if (!isWithinLastMonth(new Date(doc.month), new Date())) { return; }" + - "var attendance = (doc.daysAttended / (doc.daysWorking - doc.daysExcused));" + - "if (!Number.isNaN(attendance)) { emit(doc.student, attendance); }" + - "function isWithinLastMonth(date, now) {" + - " let months;" + - " months = (now.getFullYear() - date.getFullYear()) * 12;" + - " months -= date.getMonth();" + - " months += now.getMonth();" + - " return months === 1;" + - "}" + - "}" - ); - } - getNotesOfChild(childId: string): Observable { const promise = this.dbIndexing.queryIndexDocs( Note, diff --git a/src/app/child-dev-project/notes/notes-migration/notes-migration.service.spec.ts b/src/app/child-dev-project/notes/notes-migration/notes-migration.service.spec.ts deleted file mode 100644 index f93905f879..0000000000 --- a/src/app/child-dev-project/notes/notes-migration/notes-migration.service.spec.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { TestBed } from "@angular/core/testing"; - -import { NotesMigrationService } from "./notes-migration.service"; -import { EntityMapperService } from "../../../core/entity/entity-mapper.service"; -import { User } from "../../../core/user/user"; -import { Note } from "../model/note"; -import { - mockEntityMapper, - MockEntityMapperService, -} from "../../../core/entity/mock-entity-mapper-service"; -import { AlertService } from "../../../core/alerts/alert.service"; - -function legacyNote(author: string): Note { - const note = new Note(); - note["author"] = author; - return note; -} - -function createUser(name: string): User { - const user = new User(); - user.name = name; - return user; -} - -describe("NotesMigrationService", () => { - let service: NotesMigrationService; - const Peter = createUser("Peter"); - const Ursula = createUser("Ursula"); - const Jens = createUser("Jens"); - const Angela = createUser("Angela"); - const Albrecht = createUser("Albrecht"); - const Johanna = createUser("Johanna"); - const users = [Peter, Ursula, Jens, Angela, Albrecht, Johanna]; - - let entityMapper: MockEntityMapperService; - - beforeEach(() => { - entityMapper = mockEntityMapper([]); - TestBed.configureTestingModule({ - providers: [ - { - provide: EntityMapperService, - useValue: entityMapper, - }, - { provide: AlertService, useValue: jasmine.createSpyObj(["addAlert"]) }, - ], - }); - service = TestBed.inject(NotesMigrationService); - service.allUsers = new Map(users.map((u) => [u.name.toLowerCase(), u])); - }); - - it("should be created", () => { - expect(service).toBeTruthy(); - }); - - it("finds the correct user for different user-names", () => { - const oldAuthors = [ - "Peter Lustig, Ursula", - "Jens & Angela", - "Albrecht", - "Johanna", - "Peter, Jens, Albrecht, Johanna", - ]; - const expectedUsers = [ - [Peter, Ursula], - [Jens, Angela], - [Albrecht], - [Johanna], - [Peter, Jens, Albrecht, Johanna], - ]; - oldAuthors.forEach((authorName, index) => { - const foundUsers = service.findUsers(authorName); - expect(foundUsers.detectedUsers).toEqual(expectedUsers[index]); - expect(foundUsers.additional).toHaveSize(0); - }); - }); - - it("returns an array of users that could not be matched", () => { - const nonMatchedUsers = [ - "Phillip", - "Christian", - "Agnes, Strack & Zimmermann", - "Gregor Giselle", - "Insalata and mista", - ]; - const expected = [ - ["Phillip"], - ["Christian"], - ["Agnes", "Strack", "Zimmermann"], - ["Gregor Giselle"], - ["Insalata", "mista"], - ]; - nonMatchedUsers.forEach((userName, index) => { - const foundUsers = service.findUsers(userName); - expect(foundUsers.additional).toEqual(expected[index]); - expect(foundUsers.detectedUsers).toHaveSize(0); - }); - }); - - it("migrates a note with existing users", () => { - const note = legacyNote("Peter L, Ursula & Jens"); - const expectedUsers = [Peter, Ursula, Jens].map((u) => u.getId()); - service.migrateSingleNote(note); - expect(note["author"]).not.toBeDefined(); - expect(note.authors).toEqual(expectedUsers); - expect(note.text).toHaveSize(0); - }); - - it("appends a note-text with all non-found users", () => { - const note = legacyNote("Peter L, Ursula & Andi"); - note.text = "Lorem ipsum"; - const expectedUsers = [Peter, Ursula].map((u) => u.getId()); - service.migrateSingleNote(note); - expect(note.authors).toEqual(expectedUsers); - const newText = note.text.split("\n"); - expect(newText).toHaveSize(2); - expect(newText[0]).toEqual("Lorem ipsum"); - expect(newText[1]).toEqual("Also authored by Andi"); - }); - - it("migrates all existing notes", async () => { - const notes = [ - legacyNote(""), - legacyNote("Peter L, Ursula"), - legacyNote("Peter L, Ursula & Andi"), - legacyNote("Johannes"), - ]; - entityMapper.addAll(notes); - entityMapper.addAll(users); - await service.migrateToMultiUser(); - const existingUsers = [Peter, Ursula].map((u) => u.getId()); - const migratedNotes = entityMapper.getAll("Note"); - expect(migratedNotes).toHaveSize(4); - migratedNotes.forEach((note) => { - expect(note["author"]).not.toBeDefined(); - }); - expect(migratedNotes[0].authors).toHaveSize(0); - expect(migratedNotes[0].text).toHaveSize(0); - expect(migratedNotes[1].authors).toEqual(existingUsers); - expect(migratedNotes[1].text).toHaveSize(0); - expect(migratedNotes[2].authors).toEqual(existingUsers); - expect(migratedNotes[2].text).toEqual("Also authored by Andi"); - expect(migratedNotes[3].authors).toHaveSize(0); - expect(migratedNotes[3].text).toEqual("Also authored by Johannes"); - }); - - it("does not change an already migrated note", () => { - const note = new Note(); - note.authors = [Peter, Ursula, Jens].map((u) => u.getId()); - note.text = "Lorem ipsum"; - note.date = new Date(); - const copy = note.copy(); - service.migrateSingleNote(note); - expect(note).toEqual(copy); - }); -}); diff --git a/src/app/child-dev-project/notes/notes-migration/notes-migration.service.ts b/src/app/child-dev-project/notes/notes-migration/notes-migration.service.ts deleted file mode 100644 index 1dbe6998e1..0000000000 --- a/src/app/child-dev-project/notes/notes-migration/notes-migration.service.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Injectable } from "@angular/core"; -import { EntityMapperService } from "../../../core/entity/entity-mapper.service"; -import { Note } from "../model/note"; -import { User } from "../../../core/user/user"; -import { AlertService } from "../../../core/alerts/alert.service"; -import { Alert } from "../../../core/alerts/alert"; -import { AlertDisplay } from "../../../core/alerts/alert-display"; -import { RecurringActivity } from "../../attendance/model/recurring-activity"; - -@Injectable({ - providedIn: "root", -}) -export class NotesMigrationService { - allUsers: Map; - constructor( - private entityMapperService: EntityMapperService, - private alertService: AlertService - ) {} - - /** - * migrates all notes in the database to the new format. - *
This format will include users as an array of string-id's - * instead of a single field describing the author(s) of the note - */ - async migrateToMultiUser() { - console.log("Starting to migrate all Notes' authors"); - this.allUsers = new Map( - (await this.entityMapperService.loadType(User)).map((u) => [ - u.name.toLowerCase(), - u, - ]) - ); - const allNotes: Note[] = await this.entityMapperService.loadType(Note); - let amountOfMigratedNotes = 0; - for (const note of allNotes) { - amountOfMigratedNotes += this.migrateSingleNote(note); - await this.entityMapperService.save(note); - } - this.alertService.addAlert( - new Alert( - `Migrated ${amountOfMigratedNotes} note(s) `, - Alert.INFO, - AlertDisplay.TEMPORARY - ) - ); - console.log( - `Completed to migrate all Notes' authors (${amountOfMigratedNotes} note(s) total)` - ); - - this.migrateToMultiAssignedActivities(); - } - - async migrateToMultiAssignedActivities() { - const allActivities: RecurringActivity[] = await this.entityMapperService.loadType( - RecurringActivity - ); - for (const act of allActivities) { - act.assignedTo = []; - await this.entityMapperService.save(act); - } - } - - /** - * migrate a single note to the new format. - * The 'author'-field will be deleted after the migration is done - * @param note The note to migrate - */ - public migrateSingleNote(note: Note): number { - const userStr = note["author"]; - if (userStr === undefined || userStr === null) { - // no migration necessary - return 0; - } - const newUsers = this.findUsers(userStr); - note.authors = newUsers.detectedUsers.map((u) => u.getId()); - delete note["author"]; - if (newUsers.additional.length > 0) { - console.log("could not match all users", note); - this.updateNoteText(note, newUsers.additional); - } - return 1; - } - - private updateNoteText(note: Note, additionalUsers: string[]) { - const additionalText = "Also authored by " + additionalUsers.join(", "); - if (note.text.length === 0) { - note.text = additionalText; - } else { - note.text += "\n" + additionalText; - } - } - - /** - * finds a user based on the following assumptions: - *
  • All leading and trailing whitespaces are ignored - *
  • A single user will be matched by his case-insensitive name - *
  • When the search string contains a ',' or '&'-character, multiple - * users will be matched - *
  • If the string to match contains a whitespace, this name will be matched - * as well as all 'parts' of that name, meaning every sub-string, split by whitespaces - * @param str the string to search - */ - public findUsers( - str: string - ): { detectedUsers: User[]; additional: string[] } { - const detectedUsers: User[] = []; - const additional: string[] = []; - // split on '&', 'and' and ',' - // remove any non alphabet-characters and non-whitespace-characters - const searchStrings = str - .trim() - .replace(/&/g, ",") - .replace(/and/g, ",") - .split(",") - .map((s) => s.replace(/[^a-zA-Z\s]/, "").trim()); - for (const searchString of searchStrings) { - const user = this.findSingleUser(searchString); - if (user) { - detectedUsers.push(user); - } else if (searchString.trim().length > 0) { - additional.push(searchString.trim()); - } - } - return { detectedUsers: detectedUsers, additional: additional }; - } - - /** - * Find a single user based on a search string that should represent a single user - * @param str The string to look for - */ - private findSingleUser(str: string): User | undefined { - const lowerCaseSearch = str.toLowerCase(); - if (lowerCaseSearch.match(/\s/)) { - return ( - // first look for a user that matches exactly (including whitespaces) - this.allUsers.get(lowerCaseSearch) || - // If none is found, look for a user where any name (in most cases name and surname) matches - lowerCaseSearch - .split(/\s/) - .map((s) => this.allUsers.get(s)) - .filter((u) => !!u) - .pop() - ); - } else { - return this.allUsers.get(lowerCaseSearch); - } - } -} diff --git a/src/app/child-dev-project/notes/notes.module.ts b/src/app/child-dev-project/notes/notes.module.ts index 66777393cd..03325b83b0 100644 --- a/src/app/child-dev-project/notes/notes.module.ts +++ b/src/app/child-dev-project/notes/notes.module.ts @@ -23,7 +23,6 @@ import { MatProgressBarModule } from "@angular/material/progress-bar"; import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { MatTooltipModule } from "@angular/material/tooltip"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { FilterPipeModule } from "ngx-filter-pipe"; import { SchoolsModule } from "../schools/schools.module"; import { MatListModule } from "@angular/material/list"; import { ChildrenModule } from "../children/children.module"; @@ -79,7 +78,6 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; MatSlideToggleModule, BrowserAnimationsModule, ReactiveFormsModule, - FilterPipeModule, SchoolsModule, ReactiveFormsModule, MatDialogModule, diff --git a/src/app/core/admin/admin/admin.component.html b/src/app/core/admin/admin/admin.component.html index 3338054d70..0daf0dee35 100644 --- a/src/app/core/admin/admin/admin.component.html +++ b/src/app/core/admin/admin/admin.component.html @@ -19,33 +19,6 @@

    Utility Functions

    > Auto-Update children's photo filenames - - - - -


    diff --git a/src/app/core/admin/admin/admin.component.spec.ts b/src/app/core/admin/admin/admin.component.spec.ts index 212aaf60ae..2e4e57023e 100644 --- a/src/app/core/admin/admin/admin.component.spec.ts +++ b/src/app/core/admin/admin/admin.component.spec.ts @@ -20,11 +20,6 @@ import { of } from "rxjs"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { MatDialogRef } from "@angular/material/dialog"; import { SessionType } from "../../session/session-type"; -import { NotesMigrationService } from "../../../child-dev-project/notes/notes-migration/notes-migration.service"; -import { AttendanceMigrationService } from "../../../child-dev-project/attendance/attendance-migration/attendance-migration.service"; -import { ChildrenMigrationService } from "../../../child-dev-project/children/child-photo-service/children-migration.service"; -import { PermissionsMigrationService } from "../../permissions/permissions-migration.service"; -import { ConfigMigrationService } from "../../config/config-migration.service"; describe("AdminComponent", () => { let component: AdminComponent; @@ -103,26 +98,6 @@ describe("AdminComponent", () => { provide: ConfirmationDialogService, useValue: confirmationDialogMock, }, - { - provide: AttendanceMigrationService, - useValue: {}, - }, - { - provide: NotesMigrationService, - useValue: {}, - }, - { - provide: ChildrenMigrationService, - useValue: {}, - }, - { - provide: PermissionsMigrationService, - useValue: {}, - }, - { - provide: ConfigMigrationService, - useValue: {}, - }, ], }).compileComponents(); }) diff --git a/src/app/core/admin/admin/admin.component.ts b/src/app/core/admin/admin/admin.component.ts index 72efa2788d..40931a662d 100644 --- a/src/app/core/admin/admin/admin.component.ts +++ b/src/app/core/admin/admin/admin.component.ts @@ -9,11 +9,6 @@ import PouchDB from "pouchdb-browser"; import { ChildPhotoUpdateService } from "../services/child-photo-update.service"; import { ConfigService } from "../../config/config.service"; import { EntityMapperService } from "../../entity/entity-mapper.service"; -import { AttendanceMigrationService } from "../../../child-dev-project/attendance/attendance-migration/attendance-migration.service"; -import { NotesMigrationService } from "../../../child-dev-project/notes/notes-migration/notes-migration.service"; -import { ChildrenMigrationService } from "../../../child-dev-project/children/child-photo-service/children-migration.service"; -import { ConfigMigrationService } from "../../config/config-migration.service"; -import { PermissionsMigrationService } from "../../permissions/permissions-migration.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { readFile } from "../../../utils/utils"; @@ -43,12 +38,7 @@ export class AdminComponent implements OnInit { private snackBar: MatSnackBar, private childPhotoUpdateService: ChildPhotoUpdateService, private configService: ConfigService, - private entityMapper: EntityMapperService, - public attendanceMigration: AttendanceMigrationService, - public notesMigration: NotesMigrationService, - public childrenMigrationService: ChildrenMigrationService, - public configMigrationService: ConfigMigrationService, - public permissionsMigrationService: PermissionsMigrationService + private entityMapper: EntityMapperService ) {} ngOnInit() { @@ -63,10 +53,6 @@ export class AdminComponent implements OnInit { this.childPhotoUpdateService.updateChildrenPhotoFilenames(); } - async migrateConfigChanges() { - await this.configMigrationService.migrateConfig(); - } - /** * Send a reference of the PouchDB to the browser's developer console for real-time debugging. */ diff --git a/src/app/core/admin/services/backup.service.spec.ts b/src/app/core/admin/services/backup.service.spec.ts index 4dccfa5d8c..fc437c7da7 100644 --- a/src/app/core/admin/services/backup.service.spec.ts +++ b/src/app/core/admin/services/backup.service.spec.ts @@ -71,10 +71,11 @@ describe("BackupService", () => { const res = await db.getAll(); expect(res).toHaveSize(2); - expect(res.map(ignoreRevProperty)).toEqual( - originalData.map(ignoreRevProperty), - "restored records not identical to original records (_rev ignored)" - ); + expect(res.map(ignoreRevProperty)) + .withContext( + "restored records not identical to original records (_rev ignored)" + ) + .toEqual(originalData.map(ignoreRevProperty)); }); it("getCsvExport should contain a line for every record", async () => { @@ -131,7 +132,9 @@ describe("BackupService", () => { const res = await db.getAll(); expect(res).toHaveSize(1); - expect(res[0].other).toBeUndefined("empty property was added anyway"); + expect(res[0].other) + .withContext("empty property was added anyway") + .toBeUndefined(); expect(res.map(ignoreRevProperty)).toEqual([{ _id: "Test:1", test: 1 }]); }); diff --git a/src/app/core/config/config-migration.service.spec.ts b/src/app/core/config/config-migration.service.spec.ts deleted file mode 100644 index 15417a7e6a..0000000000 --- a/src/app/core/config/config-migration.service.spec.ts +++ /dev/null @@ -1,780 +0,0 @@ -import { TestBed } from "@angular/core/testing"; - -import { ConfigMigrationService } from "./config-migration.service"; -import { ConfigService } from "./config.service"; -import { EntityMapperService } from "../entity/entity-mapper.service"; -import { Config } from "./config"; -import { - EntityConfig, - EntityConfigService, -} from "../entity/entity-config.service"; -import { - CONFIGURABLE_ENUM_CONFIG_PREFIX, - ConfigurableEnumValue, -} from "../configurable-enum/configurable-enum.interface"; -import { genders } from "../../child-dev-project/children/model/genders"; -import { EntitySchemaField } from "../entity/schema/entity-schema-field"; -import { Child } from "../../child-dev-project/children/model/child"; -import { HistoricalEntityData } from "../../features/historical-data/historical-entity-data"; -import { DynamicEntityService } from "../entity/dynamic-entity.service"; -import { EntitySchemaService } from "../entity/schema/entity-schema.service"; - -describe("ConfigMigrationService", () => { - let service: ConfigMigrationService; - let mockEntityMapper: jasmine.SpyObj; - let configService: ConfigService; - let config: Config; - - beforeEach(async () => { - config = new Config(); - config.data = { - "view:child": { - component: "ChildrenList", - config: { - title: "Children List", - columns: [ - { - component: "ChildBlock", - title: "Name", - id: "name", - }, - { - component: "DisplayText", - title: "Age", - id: "age", - }, - { - component: "DisplayDate", - title: "DoB", - id: "dateOfBirth", - }, - { - component: "SchoolBlockWrapper", - title: "School", - id: "schoolId", - }, - { - component: "DisplayText", - title: "Gender", - id: "gender", - }, - { - component: "RecentAttendanceBlocks", - title: "Attendance (School)", - id: "schoolAttendance", - config: { - filterByActivityType: "SCHOOL_CLASS", - }, - noSorting: true, - }, - { - component: "DisplayConfigurableEnum", - title: "Center", - id: "center", - }, - { - component: "ChildBlockList", - title: "Children", - id: "children", - noSorting: true, - }, - ], - columnGroup: { - default: "School Info", - mobile: "Mobile", - groups: [ - { - name: "Basic Info", - columns: ["age", "name"], - }, - { - name: "School Info", - columns: [ - "name", - "schoolClass", - "schoolId", - "schoolAttendance", - ], - }, - ], - }, - filters: [ - { - id: "isActive", - type: "boolean", - default: "true", - true: "Active Children", - false: "Inactive", - all: "All", - }, - { - id: "isActiveAll", - type: "boolean", - default: "", - true: "Active Children", - false: "Inactive", - all: "All", - }, - { - id: "center", - label: "Center", - type: "configurable-enum", - enumId: "center", - display: "dropdown", - }, - { - id: "school", - type: "prebuilt", - label: "School", - display: "dropdown", - }, - { - id: "assignedTo", - }, - ], - }, - }, - "view:child/:id": { - component: "EntityDetails", - config: { - icon: "child", - entity: "Child", - panels: [ - { - title: "Basic Information", - components: [ - { - title: "", - component: "Form", - config: { - cols: [ - [ - { - input: "photo", - id: "photoFile", - placeholder: "Photo Filename", - }, - ], - [ - { - input: "text", - id: "name", - placeholder: "Name", - required: true, - }, - { - input: "configurable-enum-select", - id: "center", - placeholder: "Center", - enumId: "center", - }, - ], - [ - { - input: "age", - tooltip: - "This field is read-only. Edit Date of Birth to change age. Select Jan 1st if you only know the year of birth.", - id: "dateOfBirth", - placeholder: "Date of Birth", - }, - { - input: "select", - id: "gender", - placeholder: "Gender", - options: ["M", "F"], - }, - { - input: "select", - id: "status", - placeholder: "Status", - options: ["Active", "Inactive", "Still Considering"], - }, - ], - [ - { - input: "datepicker", - id: "admissionDate", - placeholder: "Admission Date", - }, - ], - [ - { - input: "textarea", - id: "address", - placeholder: "Address", - }, - { - input: "text", - id: "assignedTo", - placeholder: "Additional Information", - }, - { - input: "entity-select", - id: "children", - entityType: "Child", - placeholder: "Add children...", - label: "Assigned children", - }, - ], - ], - }, - }, - ], - }, - { - title: "Education", - components: [ - { - title: "School History", - component: "PreviousSchools", - config: { - single: true, - columns: [ - { id: "schoolId", label: "School", input: "school" }, - { id: "schoolClass", label: "Class", input: "text" }, - { id: "start", label: "From", input: "date" }, - { id: "end", label: "To", input: "date" }, - { - id: "result", - label: "Result", - input: "percentageResult", - }, - ], - }, - }, - { - title: "Participation History", - component: "PreviousTeams", - }, - ], - }, - { - title: "Observations", - components: [ - { - title: "", - component: "HistoricalDataComponent", - config: [ - { - name: "date", - label: "Date", - inputType: "date", - }, - { - name: "isMotivatedDuringClass", - label: "Motivated", - inputType: "configurable_enum", - enumId: "rating-answer", - tooltip: "The child is motivated during the class.", - }, - { - name: "observer", - label: "Observer", - inputType: "text", - tooltip: "Name of the observer", - }, - ], - }, - { - title: "", - component: "ChildrenOverview", - config: { - displayedColumns: ["name", "schoolClass", "age"], - }, - }, - - { - title: "", - component: "Aser", - config: { - displayedColumns: ["date", "math", "hindi"], - }, - }, - ], - }, - { - title: "Participants", - components: [ - { - component: "ActivityParticipantsSection", - }, - ], - }, - ], - }, - }, - "entity:Child": { - permissions: {}, - attributes: [ - { - name: "children", - schema: { dataType: "array", innerDataType: "string" }, - }, - { - name: "assignedTo", - schema: { dataType: "string" }, - }, - { - name: "address", - schema: { dataType: "string" }, - }, - ], - }, - "entity:HistoricalEntityData": { - permissions: {}, - attributes: [ - { - name: "observer", - schema: { - dataType: "string", - }, - }, - { - name: "isMotivatedDuringClass", - schema: { - dataType: "configurable-enum", - innerDataType: "rating-answer", - }, - }, - ], - }, - }; - mockEntityMapper = jasmine.createSpyObj(["load", "save"]); - mockEntityMapper.load.and.resolveTo(config); - mockEntityMapper.save.and.resolveTo(); - TestBed.configureTestingModule({ - providers: [ - EntityConfigService, - ConfigService, - EntitySchemaService, - DynamicEntityService, - { provide: EntityMapperService, useValue: mockEntityMapper }, - ], - }); - service = TestBed.inject(ConfigMigrationService); - configService = TestBed.inject(ConfigService); - - await configService.loadConfig(mockEntityMapper); - const entityConfigService = TestBed.inject(EntityConfigService); - entityConfigService.addConfigAttributes(Child); - entityConfigService.addConfigAttributes(HistoricalEntityData); - - await service.migrateConfig(); - }); - - it("should be created", () => { - expect(service).toBeTruthy(); - }); - - it("should save the updated config with force update", () => { - expect(mockEntityMapper.save).toHaveBeenCalledWith(config, true); - }); - - it("should migrate the list configs", async () => { - const childrenListConfig = configService.getConfig("view:child"); - expect(childrenListConfig).toEqual(expectedChildrenListConfig); - const childConfig = configService.getConfig("entity:Child"); - const centerSchema = childConfig.attributes.find( - (attr) => attr.name === "center" - ).schema; - expect(centerSchema).toEqual(expectedCenterSchema); - }); - - it("should migrate the details configs", async () => { - const childDetailsConfig = configService.getConfig("view:child/:id"); - expect(childDetailsConfig).toEqual(expectedChildDetailsConfig); - - const childConfig = configService.getConfig("entity:Child"); - const genderSchema = childConfig.attributes.find( - (attr) => attr.name === "gender" - ).schema; - expect(genderSchema).toEqual(expectedGenderSchema); - - const statusSchema = childConfig.attributes.find( - (attr) => attr.name === "status" - ).schema; - expect(statusSchema).toEqual(expectedStatusSchema); - const statusEnum = configService.getConfig( - CONFIGURABLE_ENUM_CONFIG_PREFIX + "status" - ); - expect(statusEnum).toEqual(expectedStatusConfigurableEnum); - - const assignedToSchema = childConfig.attributes.find( - (attr) => attr.name === "assignedTo" - ).schema; - expect(assignedToSchema).toEqual(expectedAssignedToSchema); - }); - - it("should add configurable enum configs", () => { - const genderConfig = configService.getConfig( - CONFIGURABLE_ENUM_CONFIG_PREFIX + "genders" - ); - expect(genderConfig).toEqual(genders); - }); -}); -const expectedChildrenListConfig = { - component: "ChildrenList", - _id: "view:child", - config: { - title: "Children List", - columns: [ - { - view: "ChildBlock", - id: "name", - }, - { - view: "DisplayText", - label: "Age", - id: "age", - }, - { - view: "DisplayDate", - id: "dateOfBirth", - }, - { - view: "DisplayEntity", - label: "School", - id: "schoolId", - additional: "School", - noSorting: true, - }, - { - id: "gender", - }, - { - view: "RecentAttendanceBlocks", - label: "Attendance (School)", - id: "schoolAttendance", - additional: { - filterByActivityType: "SCHOOL_CLASS", - }, - noSorting: true, - }, - { - view: "DisplayConfigurableEnum", - id: "center", - }, - { - view: "DisplayEntityArray", - id: "children", - additional: "Child", - noSorting: true, - }, - ], - columnGroups: { - default: "School Info", - mobile: "Mobile", - groups: [ - { - name: "Basic Info", - columns: ["age", "name"], - }, - { - name: "School Info", - columns: ["name", "schoolClass", "schoolId", "schoolAttendance"], - }, - ], - }, - filters: [ - { - id: "isActive", - type: "boolean", - default: "true", - true: "Active Children", - false: "Inactive", - all: "All", - }, - { - id: "isActiveAll", - type: "boolean", - true: "Active Children", - false: "Inactive", - all: "All", - }, - { - id: "center", - label: "Center", - display: "dropdown", - }, - { - id: "schoolId", - type: "School", - label: "School", - display: "dropdown", - }, - { - id: "assignedTo", - type: "User", - label: "Assigned user(s)", - display: "dropdown", - }, - ], - }, -}; - -const expectedCenterSchema: EntitySchemaField = { - dataType: "configurable-enum", - innerDataType: "center", - labelShort: "Center", - label: "Center", -}; - -const expectedGenderSchema: EntitySchemaField = { - dataType: "configurable-enum", - innerDataType: "genders", - label: "Gender", - labelShort: "Gender", -}; - -const expectedStatusSchema: EntitySchemaField = { - dataType: "configurable-enum", - innerDataType: "status", - label: "Status", -}; - -const expectedAssignedToSchema: EntitySchemaField = { - dataType: "array", - innerDataType: "string", - editComponent: "EditEntityArray", - viewComponent: "DisplayEntityArray", - additional: "User", - label: "Assigned user(s)", -}; - -const expectedStatusConfigurableEnum: ConfigurableEnumValue[] = [ - { - id: "", - label: "", - }, - { - id: "Active", - label: "Active", - }, - { - id: "Inactive", - label: "Inactive", - }, - { - id: "Still Considering", - label: "Still Considering", - }, -]; - -const expectedChildDetailsConfig = { - component: "EntityDetails", - _id: "view:child/:id", - config: { - icon: "child", - entity: "Child", - panels: [ - { - title: "Basic Information", - components: [ - { - title: "", - component: "Form", - config: { - cols: [ - [ - { - edit: "EditPhoto", - id: "photo", - }, - ], - [ - { - edit: "EditText", - id: "name", - required: true, - }, - { - edit: "EditConfigurableEnum", - id: "center", - additional: "center", - }, - ], - [ - { - edit: "EditAge", - tooltip: - "This field is read-only. Edit Date of Birth to change age. Select Jan 1st if you only know the year of birth.", - id: "dateOfBirth", - }, - { - id: "gender", - }, - { - id: "status", - }, - ], - [ - { - edit: "EditDate", - id: "admissionDate", - }, - ], - [ - { - edit: "EditLongText", - id: "address", - }, - { - id: "assignedTo", - }, - { - edit: "EditEntityArray", - id: "children", - additional: "Child", - }, - ], - ], - }, - }, - ], - }, - { - title: "Education", - components: [ - { - title: "School History", - component: "PreviousSchools", - config: { - single: true, - columns: [ - { - id: "schoolId", - view: "DisplayEntity", - edit: "EditSingleEntity", - additional: "School", - }, - { - id: "schoolClass", - edit: "EditText", - }, - { - id: "start", - view: "DisplayDate", - edit: "EditDate", - }, - { - id: "end", - view: "DisplayDate", - edit: "EditDate", - }, - { - id: "result", - view: "DisplayPercentage", - edit: "EditNumber", - }, - ], - }, - }, - { - title: "Participation History", - component: "PreviousSchools", - config: { - single: false, - columns: [ - { - id: "schoolId", - label: "Team", - view: "DisplayEntity", - edit: "EditSingleEntity", - additional: "Team", - }, - { - id: "start", - label: "From", - view: "DisplayDate", - edit: "EditDate", - }, - { - id: "end", - label: "To", - view: "DisplayDate", - edit: "EditDate", - }, - ], - }, - }, - ], - }, - { - title: "Observations", - components: [ - { - title: "", - component: "HistoricalDataComponent", - config: [ - { - id: "date", - view: "DisplayDate", - edit: "EditDate", - }, - { - id: "isMotivatedDuringClass", - view: "DisplayConfigurableEnum", - edit: "EditConfigurableEnum", - additional: "rating-answer", - tooltip: "The child is motivated during the class.", - }, - { - id: "observer", - edit: "EditText", - tooltip: "Name of the observer", - }, - ], - }, - { - title: "", - component: "ChildrenOverview", - config: { - columns: [ - "name", - { id: "schoolClass", label: "Class", view: "DisplayText" }, - { id: "age", label: "Age", view: "DisplayText" }, - ], - }, - }, - { - title: "", - component: "Aser", - config: { - columns: [ - { id: "date", visibleFrom: "xs" }, - { id: "math", visibleFrom: "xs" }, - { id: "hindi", visibleFrom: "md" }, - ], - }, - }, - ], - }, - { - title: "Participants", - components: [ - { - component: "Form", - config: { - cols: [ - [ - { - id: "linkedGroups", - label: "Groups", - edit: "EditEntityArray", - additional: "School", - }, - { - id: "participants", - label: "Participants", - edit: "EditEntityArray", - additional: "Child", - }, - ], - ], - }, - }, - ], - }, - ], - }, -}; diff --git a/src/app/core/config/config-migration.service.ts b/src/app/core/config/config-migration.service.ts deleted file mode 100644 index 1aa22f496a..0000000000 --- a/src/app/core/config/config-migration.service.ts +++ /dev/null @@ -1,530 +0,0 @@ -import { Injectable } from "@angular/core"; -import { ConfigService } from "./config.service"; -import { EntityMapperService } from "../entity/entity-mapper.service"; -import { Config } from "./config"; -import { ViewConfig } from "../view/dynamic-routing/view-config.interface"; -import { - ConfigurableEnumFilterConfig, - EntityListConfig, - FilterConfig, -} from "../entity-components/entity-list/EntityListConfig"; -import { FormFieldConfig } from "../entity-components/entity-form/entity-form/FormConfig"; -import { Entity, EntityConstructor } from "../entity/model/entity"; -import { - EntityConfig, - EntityConfigService, -} from "../entity/entity-config.service"; -import { - EntityDetailsConfig, - PanelComponent, -} from "../entity-components/entity-details/EntityDetailsConfig"; -import { ChildSchoolRelation } from "../../child-dev-project/children/model/childSchoolRelation"; -import { HistoricalEntityData } from "../../features/historical-data/historical-entity-data"; -import { RecurringActivity } from "../../child-dev-project/attendance/model/recurring-activity"; -import { HealthCheck } from "../../child-dev-project/health-checkup/model/health-check"; -import { readingLevels } from "../../child-dev-project/aser/model/readingLevels"; -import { mathLevels } from "../../child-dev-project/aser/model/mathLevels"; -import { genders } from "../../child-dev-project/children/model/genders"; -import { materials } from "../../child-dev-project/educational-material/model/materials"; -import { - CONFIGURABLE_ENUM_CONFIG_PREFIX, - ConfigurableEnumValue, -} from "../configurable-enum/configurable-enum.interface"; -import { warningLevels } from "../../child-dev-project/warning-levels"; -import { User } from "../user/user"; -import { DynamicEntityService } from "../entity/dynamic-entity.service"; - -@Injectable({ - providedIn: "root", -}) -export class ConfigMigrationService { - private config: Config; - constructor( - private configService: ConfigService, - private entityMapper: EntityMapperService, - private dynamicEntityService: DynamicEntityService - ) {} - - async migrateConfig(): Promise { - this.config = await this.configService.loadConfig(this.entityMapper); - this.addNewConfigurableEnums(); - this.migrateViewConfigs(); - console.log("config", this.config); - return this.configService.saveConfig(this.entityMapper, this.config.data); - } - - private addNewConfigurableEnums() { - this.config.data[ - CONFIGURABLE_ENUM_CONFIG_PREFIX + "reading-levels" - ] = readingLevels; - this.config.data[ - CONFIGURABLE_ENUM_CONFIG_PREFIX + "math-levels" - ] = mathLevels; - this.config.data[CONFIGURABLE_ENUM_CONFIG_PREFIX + "genders"] = genders; - this.config.data[CONFIGURABLE_ENUM_CONFIG_PREFIX + "materials"] = materials; - this.config.data[ - CONFIGURABLE_ENUM_CONFIG_PREFIX + "warning-levels" - ] = warningLevels; - } - - private migrateViewConfigs() { - const entityListComponents = [ - "ChildrenList", - "SchoolsList", - "ActivityList", - "NotesManager", - ]; - const viewConfigs = this.configService.getAllConfigs("view:"); - viewConfigs.forEach((viewConfig) => { - const entity = this.getEntity(viewConfig._id); - if (entityListComponents.includes(viewConfig.component)) { - this.migrateEntityListConfig(viewConfig.config, entity); - } - if (viewConfig.component === "EntityDetails") { - this.migrateEntityDetailsConfig(viewConfig.config, entity); - } - }); - } - - private getEntity(viewId: string): EntityConstructor { - const entityType = viewId - .split(":")[1] - .replace("/", "") - .split("-") - .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) - .join(""); - return this.dynamicEntityService.getEntityConstructor(entityType); - } - - private migrateEntityListConfig( - config: EntityListConfig, - entity: EntityConstructor - ) { - if (config.hasOwnProperty("columnGroup")) { - config.columnGroups = config["columnGroup"]; - delete config["columnGroup"]; - } - this.migrateColumnConfigs(config.columns as FormFieldConfig[], entity); - if (config.hasOwnProperty("filters")) { - this.migrateFilters(config.filters); - } - } - - private migrateColumnConfigs( - columns: FormFieldConfig[], - entity: EntityConstructor - ) { - columns.forEach((column: FormFieldConfig) => { - try { - column.view = column["component"]; - delete column["component"]; - column.label = column["title"]; - delete column["title"]; - if (column.hasOwnProperty("config")) { - column.additional = column["config"]; - delete column["config"]; - } - if (column.view === "SchoolBlockWrapper") { - column.view = "DisplayEntity"; - column.additional = "School"; - column.noSorting = true; - } - if (column.view === "DisplayUsers") { - column.view = "DisplayEntityArray"; - column.additional = "User"; - column.noSorting = true; - } - if (column.view === "ChildBlockList") { - column.view = "DisplayEntityArray"; - column.additional = "Child"; - column.noSorting = true; - } - this.addLabelToEntity(column.label, column.id, entity, "short", column); - } catch (e) { - console.warn(`Failed to migrate column ${column.id}: ${e}`); - } - }); - } - - private addLabelToEntity( - label: string, - attribute: string, - entity: EntityConstructor, - type: "short" | "long", - formField?: FormFieldConfig - ) { - try { - const schema = entity.schema.get(attribute); - if (type === "short") { - schema.labelShort = label; - } else { - schema.label = label; - } - const schemaKey = - EntityConfigService.PREFIX_ENTITY_CONFIG + entity.ENTITY_TYPE; - let configSchema = this.configService.getConfig(schemaKey); - if (!configSchema) { - this.config.data[schemaKey] = {}; - configSchema = this.configService.getConfig(schemaKey); - } - if (!configSchema.attributes) { - configSchema.attributes = []; - } - let existing = configSchema.attributes.find( - (attr) => attr.name === attribute - ); - if (!existing) { - existing = { name: attribute, schema: {} }; - configSchema.attributes.push(existing); - } - existing.schema = schema; - if (formField) { - delete formField.label; - if (formField.view === "DisplayText") { - delete formField.view; - } - } - } catch (e) { - console.warn( - `Failed to set label ${label} to attribute ${attribute} of entity ${entity.ENTITY_TYPE}: ${e}` - ); - } - } - - private migrateFilters(filters: FilterConfig[]) { - filters.forEach((filter) => { - try { - if (filter.type === "configurable-enum") { - const enumFilter = filter as ConfigurableEnumFilterConfig; - delete enumFilter.enumId; - delete enumFilter.type; - } else if (filter.id === "school") { - filter.type = "School"; - filter.id = "schoolId"; - } else if (filter.id === "assignedTo") { - filter.type = "User"; - filter.label = "Assigned user(s)"; - filter.display = "dropdown"; - } - if (filter.default === "") { - delete filter.default; - } - } catch (e) { - console.warn(`Failed to migrate filter ${filter.id}: ${e}`); - } - }); - } - - private migrateEntityDetailsConfig( - config: EntityDetailsConfig, - entity: EntityConstructor - ) { - config.panels.forEach((panel) => { - panel.components.forEach((panelComp) => { - switch (panelComp.component) { - case "Form": { - this.migrateFormComponent(panelComp.config["cols"], entity); - break; - } - case "PreviousSchools": { - if (panelComp.hasOwnProperty("config")) { - this.migratePreviousSchoolsComponent(panelComp.config["columns"]); - } - break; - } - case "PreviousTeams": { - this.migratePreviousTeams(panelComp); - break; - } - case "HistoricalDataComponent": { - this.migrateHistoricalDataComponent(panelComp.config as any); - break; - } - case "ActivityParticipantsSection": { - this.migrateActivityParticipantsSection(panelComp); - break; - } - case "Aser": - case "EducationalMaterial": - case "HealthCheckup": - case "NotesOfChild": - case "ChildrenOverview": { - if (panelComp.hasOwnProperty("config")) { - this.migrateTable(panelComp.config); - } - break; - } - } - }); - }); - } - - private migrateFormComponent( - columns: FormFieldConfig[][], - entity: EntityConstructor - ) { - const editMap = new Map([ - ["text", "EditText"], - ["checkbox", "EditBoolean"], - ["textarea", "EditLongText"], - ["photo", "EditPhoto"], - ["configurable-enum-select", "EditConfigurableEnum"], - ["age", "EditAge"], - ["datepicker", "EditDate"], - ["entity-select", "EditEntityArray"], - ]); - columns.forEach((row) => - row.forEach((formField) => { - try { - formField.label = formField.label || formField["placeholder"]; - delete formField["placeholder"]; - formField.additional = - formField["options"] || - formField["enumId"] || - formField["entityType"]; - if (formField.additional === undefined) { - delete formField.additional; - } - delete formField["options"]; - delete formField["enumId"]; - delete formField["entityType"]; - if (formField.id === "photoFile") { - formField.id = "photo"; - } - if (formField["input"] === "select") { - this.migrateSelectFormField(formField, entity); - } else if (formField.id === "assignedTo") { - const schema = entity.schema.get("assignedTo"); - formField.label = "Assigned user(s)"; - schema.dataType = "array"; - schema.innerDataType = "string"; - schema.viewComponent = "DisplayEntityArray"; - schema.editComponent = "EditEntityArray"; - schema.additional = User.ENTITY_TYPE; - } else { - formField.edit = editMap.get(formField["input"]); - } - delete formField["input"]; - this.addLabelToEntity( - formField.label, - formField.id, - entity, - "long", - formField - ); - } catch (e) { - console.warn(`Failed to migrate form field ${formField.id}: ${e}`); - } - }) - ); - } - - private migrateSelectFormField( - formField: FormFieldConfig, - entity: EntityConstructor - ) { - const selectableMap = new Map([ - ["warningLevel", "warning-levels"], - ["materialType", "materials"], - ["gender", "genders"], - ["hindi", "reading-levels"], - ["bengali", "reading-levels"], - ["english", "reading-levels"], - ["math", "math-levels"], - ]); - if (!selectableMap.has(formField.id)) { - const newEnum: ConfigurableEnumValue[] = [{ id: "", label: "" }].concat( - ...formField["additional"].map((option: string) => { - return { - label: option, - id: option, - }; - }) - ); - this.config.data[ - CONFIGURABLE_ENUM_CONFIG_PREFIX + formField.id - ] = newEnum; - console.warn( - `Automatically created enum "${formField.id}" with values:`, - newEnum - ); - selectableMap.set(formField.id, formField.id); - } - const propertySchema = entity.schema.get(formField.id); - propertySchema.dataType = "configurable-enum"; - propertySchema.innerDataType = selectableMap.get(formField.id); - delete formField["additional"]; - } - - private migratePreviousSchoolsComponent(columns: FormFieldConfig[]) { - if (columns) { - columns.forEach((formField) => { - try { - this.migrateEntitySubrecordInput(formField, "input"); - this.addLabelToEntity( - formField.label, - formField.id, - ChildSchoolRelation, - "short", - formField - ); - } catch (e) { - console.warn( - `Filed to migrate previousSchoolsConfig for ${formField.id}: ${e}` - ); - } - }); - } - } - - private migratePreviousTeams(config: PanelComponent) { - config.component = "PreviousSchools"; - config.config = { - single: false, - columns: [ - { - id: "schoolId", - label: "Team", - view: "DisplayEntity", - edit: "EditSingleEntity", - additional: "Team", - }, - { - id: "start", - label: "From", - view: "DisplayDate", - edit: "EditDate", - }, - { - id: "end", - label: "To", - view: "DisplayDate", - edit: "EditDate", - }, - ], - } as any; - this.addLabelToEntity("Team", "schoolId", ChildSchoolRelation, "short"); - this.addLabelToEntity("From", "start", ChildSchoolRelation, "short"); - this.addLabelToEntity("To", "end", ChildSchoolRelation, "short"); - } - - private migrateEntitySubrecordInput( - formField: FormFieldConfig, - inputKey: string - ) { - switch (formField[inputKey]) { - case "school": { - formField.view = "DisplayEntity"; - formField.edit = "EditSingleEntity"; - formField.additional = "School"; - break; - } - case "text": { - formField.view = "DisplayText"; - formField.edit = "EditText"; - break; - } - case "date": { - formField.view = "DisplayDate"; - formField.edit = "EditDate"; - break; - } - case "percentageResult": { - formField.view = "DisplayPercentage"; - formField.edit = "EditNumber"; - break; - } - case "configurable_enum": { - formField.view = "DisplayConfigurableEnum"; - formField.edit = "EditConfigurableEnum"; - break; - } - default: { - console.warn( - `No migration defined for inputType ${formField[inputKey]} at property ${formField.id}` - ); - } - } - delete formField[inputKey]; - } - - private migrateHistoricalDataComponent(columns: FormFieldConfig[]) { - columns.forEach((formField) => { - try { - formField.id = formField["name"]; - delete formField["name"]; - if (formField.hasOwnProperty("enumId")) { - formField.additional = formField["enumId"]; - delete formField["enumId"]; - } - this.migrateEntitySubrecordInput(formField, "inputType"); - this.addLabelToEntity( - formField.label, - formField.id, - HistoricalEntityData, - "short", - formField - ); - } catch (e) { - console.warn( - `Failed to migrate HistoricalDataComponent ${formField.id}: ${e}` - ); - } - }); - } - - private migrateActivityParticipantsSection(config: PanelComponent) { - config.component = "Form"; - config.config = { - cols: [ - [ - { - id: "linkedGroups", - label: "Groups", - edit: "EditEntityArray", - additional: "School", - }, - { - id: "participants", - label: "Participants", - edit: "EditEntityArray", - additional: "Child", - }, - ], - ], - } as any; - this.addLabelToEntity("Groups", "linkedGroups", RecurringActivity, "short"); - this.addLabelToEntity( - "Participants", - "participants", - RecurringActivity, - "short" - ); - } - - private migrateTable(config: any) { - const columnConfigs = [ - { id: "date", visibleFrom: "xs" }, - { id: "materialType", visibleFrom: "xs" }, - { id: "materialAmount", visibleFrom: "md" }, - { id: "description", visibleFrom: "md" }, - { id: "math", visibleFrom: "xs" }, - { id: "english", visibleFrom: "xs" }, - { id: "hindi", visibleFrom: "md" }, - { id: "bengali", visibleFrom: "md" }, - { id: "remarks", visibleFrom: "md" }, - { id: "schoolClass", label: "Class", view: "DisplayText" }, - { id: "age", label: "Age", view: "DisplayText" }, - { - id: "bmi", - label: "BMI", - view: "ReadonlyFunction", - additional: (entity: HealthCheck) => "BMI: " + entity.bmi.toFixed(2), - }, - ]; - config.columns = config.displayedColumns.map((col: string) => { - return columnConfigs.find((cc) => cc.id === col) || col; - }); - delete config.displayedColumns; - } -} diff --git a/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.stories.ts b/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.stories.ts index ec6513717d..53e80fb105 100644 --- a/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.stories.ts +++ b/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.stories.ts @@ -18,10 +18,10 @@ import { FormFieldConfig } from "../../entity-form/entity-form/FormConfig"; import { ChildrenModule } from "../../../../child-dev-project/children/children.module"; import { ChildrenService } from "../../../../child-dev-project/children/children.service"; import { of } from "rxjs"; -import * as faker from "faker"; import { EntityPermissionsService } from "../../../permissions/entity-permissions.service"; import { AttendanceLogicalStatus } from "../../../../child-dev-project/attendance/model/attendance-status"; import { MockSessionModule } from "../../../session/mock-session.module"; +import { faker } from "../../../demo-data/faker"; const configService = new ConfigService(); const schemaService = new EntitySchemaService(); diff --git a/src/app/core/entity/entity-mapper.service.spec.ts b/src/app/core/entity/entity-mapper.service.spec.ts index e33f9573fb..5aacc83364 100644 --- a/src/app/core/entity/entity-mapper.service.spec.ts +++ b/src/app/core/entity/entity-mapper.service.spec.ts @@ -86,7 +86,7 @@ describe("EntityMapperService", () => { it("rejects promise when loading nonexistent entity", async () => { return entityMapper.load(Entity, "nonexistent_id").catch((err) => { - expect(err).toBeDefined('"not found" error not defined'); + expect(err).withContext('"not found" error not defined').toBeDefined(); }); }); diff --git a/src/app/core/latest-changes/latest-changes.service.spec.ts b/src/app/core/latest-changes/latest-changes.service.spec.ts index 47e6e404ec..1a07759860 100644 --- a/src/app/core/latest-changes/latest-changes.service.spec.ts +++ b/src/app/core/latest-changes/latest-changes.service.spec.ts @@ -123,8 +123,10 @@ describe("LatestChangesService", () => { const alertSpy = spyOn(alertService, "addAlert"); service.getChangelogsBetweenVersions("1.0").subscribe( () => {}, - (err) => { - expect(alertSpy.calls.count()).toBe(1, "no Alert message created"); + () => { + expect(alertSpy.calls.count()) + .withContext('"not found" error not defined') + .toBe(1); done(); } ); diff --git a/src/app/core/navigation/navigation/navigation.component.spec.ts b/src/app/core/navigation/navigation/navigation.component.spec.ts index 8deb0b2009..15be74a203 100644 --- a/src/app/core/navigation/navigation/navigation.component.spec.ts +++ b/src/app/core/navigation/navigation/navigation.component.spec.ts @@ -157,14 +157,18 @@ describe("NavigationComponent", () => { routerEvents.next(new NavigationEnd(42, "/child/1", "/child/1")); tick(); - expect(component.activeLink).toBe("/child", "url should match parent menu"); + expect(component.activeLink) + .withContext("url should match parent menu") + .toBe("/child"); routerEvents.next(new NavigationEnd(42, "/", "/")); tick(); - expect(component.activeLink).toBe("/", "root url should match"); + expect(component.activeLink).withContext("root url should match").toBe("/"); routerEvents.next(new NavigationEnd(42, "/other", "/other")); tick(); - expect(component.activeLink).toBe("", "unknown url should not match"); + expect(component.activeLink) + .withContext("unknown url should not match") + .toBe(""); })); }); diff --git a/src/app/core/permissions/permissions-migration.service.spec.ts b/src/app/core/permissions/permissions-migration.service.spec.ts deleted file mode 100644 index dc1ab0f940..0000000000 --- a/src/app/core/permissions/permissions-migration.service.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { fakeAsync, TestBed, tick } from "@angular/core/testing"; - -import { PermissionsMigrationService } from "./permissions-migration.service"; -import { EntityMapperService } from "../entity/entity-mapper.service"; -import { Config } from "../config/config"; -import { ConfigService } from "../config/config.service"; - -describe("PermissionsMigrationService", () => { - let service: PermissionsMigrationService; - let mockEntityMapper: jasmine.SpyObj; - - beforeEach(() => { - mockEntityMapper = jasmine.createSpyObj(["load", "save"]); - TestBed.configureTestingModule({ - providers: [{ provide: EntityMapperService, useValue: mockEntityMapper }], - }); - service = TestBed.inject(PermissionsMigrationService); - }); - - it("should be created", () => { - expect(service).toBeTruthy(); - }); - - it("should update view configurations", fakeAsync(() => { - const oldConfig = { - "view:user": { - component: "UserAccount", - }, - "view:users": { - component: "UserList", - requiresAdmin: true, - }, - "view:admin/conflicts": { - component: "ConflictResolution", - requiresAdmin: true, - lazyLoaded: true, - config: { someConfig: true }, - }, - "entity:Note": { - permissions: {}, - }, - }; - mockEntityMapper.load.and.resolveTo(new Config(oldConfig)); - const saveConfigSpy = spyOn(TestBed.inject(ConfigService), "saveConfig"); - - service.migrateRoutePermissions(); - tick(); - - expect(saveConfigSpy).toHaveBeenCalledWith(mockEntityMapper, { - "view:user": { - component: "UserAccount", - }, - "view:users": { - component: "UserList", - permittedUserRoles: ["admin_app"], - }, - "view:admin/conflicts": { - component: "ConflictResolution", - permittedUserRoles: ["admin_app"], - lazyLoaded: true, - config: { someConfig: true }, - }, - "entity:Note": { - permissions: {}, - }, - }); - })); -}); diff --git a/src/app/core/permissions/permissions-migration.service.ts b/src/app/core/permissions/permissions-migration.service.ts deleted file mode 100644 index 1e3d4182d6..0000000000 --- a/src/app/core/permissions/permissions-migration.service.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Injectable } from "@angular/core"; -import { ConfigService } from "../config/config.service"; -import { EntityMapperService } from "../entity/entity-mapper.service"; -import { - PREFIX_VIEW_CONFIG, - ViewConfig, -} from "../view/dynamic-routing/view-config.interface"; - -@Injectable({ - providedIn: "root", -}) -export class PermissionsMigrationService { - private readonly ADMIN_ROLE = "admin_app"; - - constructor( - private configService: ConfigService, - private entityMapper: EntityMapperService - ) {} - - public async migrateRoutePermissions() { - const currentConfig = await this.configService.loadConfig( - this.entityMapper - ); - Object.keys(currentConfig.data) - .filter((key) => key.startsWith(PREFIX_VIEW_CONFIG)) - .forEach((key) => this.migrateViewConfig(currentConfig.data[key])); - await this.configService.saveConfig(this.entityMapper, currentConfig.data); - } - - private migrateViewConfig(viewConfig: ViewConfig) { - if ( - viewConfig.hasOwnProperty("requiresAdmin") && - viewConfig["requiresAdmin"] === true - ) { - viewConfig.permittedUserRoles = [this.ADMIN_ROLE]; - delete viewConfig["requiresAdmin"]; - } - } -} diff --git a/src/app/utils/expect-entity-data.spec.ts b/src/app/utils/expect-entity-data.spec.ts index 1b4eca688a..d2bd0abb33 100644 --- a/src/app/utils/expect-entity-data.spec.ts +++ b/src/app/utils/expect-entity-data.spec.ts @@ -50,10 +50,9 @@ export function expectEntitiesToMatch( for (let i = 0; i < cleanExpected.length; i++) { const data = cleanExpected[i]; - expect(cleanActual).toContain( - data, - "expected object not found: index " + i - ); + expect(cleanActual) + .withContext("expected object not found: index " + i) + .toContain(data); } } diff --git a/src/app/utils/performance-tests.spec.ts b/src/app/utils/performance-tests.spec.ts index f4b2e47bdf..7508f38dbf 100644 --- a/src/app/utils/performance-tests.spec.ts +++ b/src/app/utils/performance-tests.spec.ts @@ -86,13 +86,12 @@ async function getExecutionDiff( const improvedTimer = new Timer(); const improvedResult = await improvedFunction(); const improvedDuration = improvedTimer.getDuration(); - expect(improvedResult).toEqual( - currentResult, - "current " + - JSON.stringify(currentResult) + - " improved " + - JSON.stringify(improvedResult) - ); + expect(improvedResult) + .withContext( + `current ${JSON.stringify(currentResult)} + improved ${JSON.stringify(improvedResult)}` + ) + .toEqual(currentResult); return currentDuration - improvedDuration; }