>
Add New
- {{ listName }}
Download CSV
-
+
diff --git a/src/app/core/entity-components/entity-list/entity-list.component.spec.ts b/src/app/core/entity-components/entity-list/entity-list.component.spec.ts
index e8f039f9ac..87497a7b35 100644
--- a/src/app/core/entity-components/entity-list/entity-list.component.spec.ts
+++ b/src/app/core/entity-components/entity-list/entity-list.component.spec.ts
@@ -9,7 +9,6 @@ import { Entity } from "../../entity/model/entity";
import { EntityMapperService } from "../../entity/entity-mapper.service";
import { User } from "../../user/user";
import { SessionService } from "../../session/session-service/session.service";
-import { ExportDataComponent } from "../../admin/export-data/export-data.component";
import { ChildrenListComponent } from "../../../child-dev-project/children/children-list/children-list.component";
import { Child } from "../../../child-dev-project/children/model/child";
import { ConfigService } from "../../config/config.service";
@@ -21,6 +20,7 @@ import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
import { DatabaseField } from "../../entity/database-field.decorator";
import { ReactiveFormsModule } from "@angular/forms";
import { AttendanceService } from "../../../child-dev-project/attendance/attendance.service";
+import { ExportModule } from "../../export/export.module";
describe("EntityListComponent", () => {
let component: EntityListComponent;
@@ -98,11 +98,12 @@ describe("EntityListComponent", () => {
);
TestBed.configureTestingModule({
- declarations: [EntityListComponent, ExportDataComponent],
+ declarations: [EntityListComponent],
imports: [
CommonModule,
NoopAnimationsModule,
EntityListModule,
+ ExportModule,
Angulartics2Module.forRoot(),
ReactiveFormsModule,
RouterTestingModule.withRoutes([
diff --git a/src/app/core/entity-components/entity-list/entity-list.module.ts b/src/app/core/entity-components/entity-list/entity-list.module.ts
index adcb875918..d357555415 100644
--- a/src/app/core/entity-components/entity-list/entity-list.module.ts
+++ b/src/app/core/entity-components/entity-list/entity-list.module.ts
@@ -15,7 +15,7 @@ import { MatTableModule } from "@angular/material/table";
import { MatSortModule } from "@angular/material/sort";
import { MatPaginatorModule } from "@angular/material/paginator";
import { FormsModule } from "@angular/forms";
-import { AdminModule } from "../../admin/admin.module";
+import { ExportModule } from "../../export/export.module";
import { ViewModule } from "../../view/view.module";
import { ListFilterComponent } from "./list-filter/list-filter.component";
import { PermissionsModule } from "../../permissions/permissions.module";
@@ -30,7 +30,7 @@ import { EntityFormModule } from "../entity-form/entity-form.module";
FormsModule,
MatFormFieldModule,
MatSelectModule,
- AdminModule,
+ ExportModule,
MatIconModule,
Angulartics2Module,
MatButtonModule,
diff --git a/src/app/core/admin/export-data/export-data.component.html b/src/app/core/export/export-data-button/export-data-button.component.html
similarity index 100%
rename from src/app/core/admin/export-data/export-data.component.html
rename to src/app/core/export/export-data-button/export-data-button.component.html
diff --git a/src/app/core/admin/export-data/export-data.component.scss b/src/app/core/export/export-data-button/export-data-button.component.scss
similarity index 100%
rename from src/app/core/admin/export-data/export-data.component.scss
rename to src/app/core/export/export-data-button/export-data-button.component.scss
diff --git a/src/app/core/admin/export-data/export-data.component.spec.ts b/src/app/core/export/export-data-button/export-data-button.component.spec.ts
similarity index 78%
rename from src/app/core/admin/export-data/export-data.component.spec.ts
rename to src/app/core/export/export-data-button/export-data-button.component.spec.ts
index e7cbbb175f..717573cb4a 100644
--- a/src/app/core/admin/export-data/export-data.component.spec.ts
+++ b/src/app/core/export/export-data-button/export-data-button.component.spec.ts
@@ -1,24 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
-import { BackupService } from "../services/backup.service";
-import { ExportDataComponent } from "./export-data.component";
+import { BackupService } from "../../admin/services/backup.service";
+import { ExportDataButtonComponent } from "./export-data-button.component";
describe("ExportDataComponent", () => {
- let component: ExportDataComponent;
- let fixture: ComponentFixture;
+ let component: ExportDataButtonComponent;
+ let fixture: ComponentFixture;
let mockBackupService: jasmine.SpyObj;
beforeEach(
waitForAsync(() => {
mockBackupService = jasmine.createSpyObj(["createJson", "createCsv"]);
TestBed.configureTestingModule({
- declarations: [ExportDataComponent],
+ declarations: [ExportDataButtonComponent],
providers: [{ provide: BackupService, useValue: mockBackupService }],
}).compileComponents();
})
);
beforeEach(() => {
- fixture = TestBed.createComponent(ExportDataComponent);
+ fixture = TestBed.createComponent(ExportDataButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
diff --git a/src/app/core/admin/export-data/export-data.component.ts b/src/app/core/export/export-data-button/export-data-button.component.ts
similarity index 77%
rename from src/app/core/admin/export-data/export-data.component.ts
rename to src/app/core/export/export-data-button/export-data-button.component.ts
index ff126c2105..2b1711a238 100644
--- a/src/app/core/admin/export-data/export-data.component.ts
+++ b/src/app/core/export/export-data-button/export-data-button.component.ts
@@ -1,15 +1,15 @@
import { Component, Input } from "@angular/core";
-import { BackupService } from "../services/backup.service";
+import { ExportService } from "../export-service/export.service";
/**
* Generic export data button that allows the user to download a file of the given data.
*/
@Component({
- selector: "app-export-data",
- templateUrl: "./export-data.component.html",
- styleUrls: ["./export-data.component.scss"],
+ selector: "app-export-data-button",
+ templateUrl: "./export-data-button.component.html",
+ styleUrls: ["./export-data-button.component.scss"],
})
-export class ExportDataComponent {
+export class ExportDataButtonComponent {
/** data to be exported */
@Input() data: any = [];
@@ -21,7 +21,7 @@ export class ExportDataComponent {
@Input() disabled: boolean = false;
- constructor(private backupService: BackupService) {}
+ constructor(private exportService: ExportService) {}
/**
* Trigger the download of the export file.
@@ -46,10 +46,10 @@ export class ExportDataComponent {
let result = "";
switch (this.format.toLowerCase()) {
case "json":
- result = this.backupService.createJson(this.data);
+ result = this.exportService.createJson(this.data);
return new Blob([result], { type: "application/json" });
case "csv":
- result = this.backupService.createCsv(this.data);
+ result = this.exportService.createCsv(this.data);
return new Blob([result], { type: "text/csv" });
default:
console.warn("Not supported format:", this.format);
diff --git a/src/app/core/export/export-service/export.service.spec.ts b/src/app/core/export/export-service/export.service.spec.ts
new file mode 100644
index 0000000000..b746d7fcab
--- /dev/null
+++ b/src/app/core/export/export-service/export.service.spec.ts
@@ -0,0 +1,138 @@
+import { TestBed } from "@angular/core/testing";
+
+import { ExportService } from "./export.service";
+import { ConfigurableEnumValue } from "../../configurable-enum/configurable-enum.interface";
+import { DatabaseField } from "../../entity/database-field.decorator";
+import { DatabaseEntity } from "../../entity/database-entity.decorator";
+import { Entity } from "../../entity/model/entity";
+import { BackupService } from "../../admin/services/backup.service";
+
+describe("ExportService", () => {
+ let service: ExportService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [ExportService, QueryService],
+ });
+
+ service = TestBed.inject(ExportService);
+ });
+
+ it("should be created", () => {
+ expect(service).toBeTruthy();
+ });
+
+ it("should create the correct json object", () => {
+ class TestClass {
+ propertyOne;
+ propertyTwo;
+ }
+ const test = new TestClass();
+ test.propertyOne = "Hello";
+ test.propertyTwo = "World";
+
+ const result = service.createJson([test]);
+
+ const expected = JSON.stringify({
+ propertyOne: "Hello",
+ propertyTwo: "World",
+ });
+ expect(result).toEqual(expected);
+ });
+
+ it("should transform a json array", () => {
+ class TestClass {
+ propertyOne;
+ propertyTwo;
+ }
+ const test = new TestClass();
+ test.propertyOne = "Hello";
+ test.propertyTwo = "World";
+
+ const result = service.createJson([test, test]);
+
+ let expected = JSON.stringify({
+ propertyOne: "Hello",
+ propertyTwo: "World",
+ });
+ expected += BackupService.SEPARATOR_ROW;
+ expected += JSON.stringify({ propertyOne: "Hello", propertyTwo: "World" });
+ expect(result).toEqual(expected);
+ });
+
+ it("should contain a column for every property", async () => {
+ const docs = [
+ { _id: "Test:1", test: 1 },
+ { _id: "Test:2", other: 2 },
+ ];
+
+ const csvExport = await service.createCsv(docs);
+
+ const rows = csvExport.split(ExportService.SEPARATOR_ROW);
+ expect(rows).toHaveSize(2 + 1); // includes 1 header line
+ expect(rows[0].split(ExportService.SEPARATOR_COL)).toHaveSize(3);
+ });
+
+ it("should create a csv string correctly", () => {
+ class TestClass {
+ _id;
+ _rev;
+ propOne;
+ propTwo;
+ }
+ const test = new TestClass();
+ test._id = 1;
+ test._rev = 2;
+ test.propOne = "first";
+ test.propTwo = "second";
+ const expected =
+ '"_id","_rev","propOne","propTwo"\r\n"1","2","first","second"';
+ const result = service.createCsv([test]);
+ expect(result).toEqual(expected);
+ });
+
+ it("should create a csv string correctly with multiple objects", () => {
+ class TestClass {
+ _id;
+ _rev;
+ propOne;
+ propTwo;
+ }
+ const test = new TestClass();
+ test._id = 1;
+ test._rev = 2;
+ test.propOne = "first";
+ test.propTwo = "second";
+ const expected =
+ '"_id","_rev","propOne","propTwo"\r\n"1","2","first","second"\r\n"1","2","first","second"';
+ const result = service.createCsv([test, test]);
+ expect(result).toEqual(expected);
+ });
+
+ it("should transform object properties to their label for export", async () => {
+ const testEnumValue: ConfigurableEnumValue = {
+ id: "ID VALUE",
+ label: "label value",
+ };
+ const testDate = "2020-01-30";
+
+ @DatabaseEntity("TestEntity")
+ class TestEntity extends Entity {
+ @DatabaseField() enumProperty: ConfigurableEnumValue;
+ @DatabaseField() dateProperty: Date;
+ }
+
+ const testEntity = new TestEntity();
+ testEntity.enumProperty = testEnumValue;
+ testEntity.dateProperty = new Date(testDate);
+
+ const csvExport = await service.createCsv([testEntity]);
+
+ const rows = csvExport.split(ExportService.SEPARATOR_ROW);
+ expect(rows).toHaveSize(1 + 1); // includes 1 header line
+ const columnValues = rows[1].split(ExportService.SEPARATOR_COL);
+ expect(columnValues).toHaveSize(2 + 1);
+ expect(columnValues).toContain('"' + testEnumValue.label + '"');
+ expect(columnValues).toContain(new Date(testDate).toISOString());
+ });
+});
diff --git a/src/app/core/export/export-service/export.service.ts b/src/app/core/export/export-service/export.service.ts
new file mode 100644
index 0000000000..c0f1fef331
--- /dev/null
+++ b/src/app/core/export/export-service/export.service.ts
@@ -0,0 +1,63 @@
+import { Injectable } from "@angular/core";
+import { Papa } from "ngx-papaparse";
+import { entityListSortingAccessor } from "../../entity-components/entity-subrecord/entity-subrecord/sorting-accessor";
+
+/**
+ * Prepare data for export in csv format.
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class ExportService {
+ /** CSV row separator */
+ static readonly SEPARATOR_ROW = "\n";
+ /** CSV column/field separator */
+ static readonly SEPARATOR_COL = ",";
+
+ constructor(private papa: Papa) {}
+
+ /**
+ * Creates a JSON string of the given data.
+ *
+ * @param data the data which should be converted to JSON
+ * @returns string containing all the values stringified elements of the input data
+ */
+ createJson(data): string {
+ let res = "";
+ data.forEach((r) => {
+ res += JSON.stringify(r) + ExportService.SEPARATOR_ROW;
+ });
+
+ return res.trim();
+ }
+
+ /**
+ * Creates a CSV string of the input data
+ *
+ * @param data an array of elements
+ * @returns string a valid CSV string of the input data
+ */
+ createCsv(data: any[]): string {
+ const allFields = new Set();
+ const exportableData = [];
+
+ data.forEach((element: any) => {
+ const exportableObj = {};
+ Object.keys(element).forEach((key: string) => {
+ const res = entityListSortingAccessor(element, key);
+
+ if (res?.toString().match(/\[object.*\]/) === null) {
+ allFields.add(key);
+ exportableObj[key] = res;
+ }
+ });
+
+ exportableData.push(exportableObj);
+ });
+
+ return this.papa.unparse(
+ { data: exportableData, fields: [...new Set(allFields)] },
+ { quotes: true, header: true }
+ );
+ }
+}
diff --git a/src/app/core/export/export.module.ts b/src/app/core/export/export.module.ts
new file mode 100644
index 0000000000..5652554d67
--- /dev/null
+++ b/src/app/core/export/export.module.ts
@@ -0,0 +1,11 @@
+import { NgModule } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { ExportDataButtonComponent } from "./export-data-button/export-data-button.component";
+import { MatButtonModule } from "@angular/material/button";
+
+@NgModule({
+ declarations: [ExportDataButtonComponent],
+ imports: [CommonModule, MatButtonModule],
+ exports: [ExportDataButtonComponent],
+})
+export class ExportModule {}
diff --git a/src/app/features/reporting/reporting.module.ts b/src/app/features/reporting/reporting.module.ts
index 794cbff25b..0b46bbdf1b 100644
--- a/src/app/features/reporting/reporting.module.ts
+++ b/src/app/features/reporting/reporting.module.ts
@@ -10,7 +10,7 @@ import { MatIconModule } from "@angular/material/icon";
import { MatDatepickerModule } from "@angular/material/datepicker";
import { MatFormFieldModule } from "@angular/material/form-field";
import { FormsModule } from "@angular/forms";
-import { AdminModule } from "../../core/admin/admin.module";
+import { ExportModule } from "../../core/export/export.module";
import { ReportRowComponent } from "./reporting/report-row/report-row.component";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { MatSelectModule } from "@angular/material/select";
@@ -29,7 +29,7 @@ import { FlexModule } from "@angular/flex-layout";
MatDatepickerModule,
MatFormFieldModule,
FormsModule,
- AdminModule,
+ ExportModule,
MatProgressBarModule,
MatSelectModule,
FlexModule,
diff --git a/src/app/features/reporting/reporting/reporting.component.html b/src/app/features/reporting/reporting/reporting.component.html
index 517d05ac27..4579cfdb70 100644
--- a/src/app/features/reporting/reporting/reporting.component.html
+++ b/src/app/features/reporting/reporting/reporting.component.html
@@ -54,7 +54,7 @@
>
Calculate
-
Download report
-
+
From 2cebde260707789ac08ab692aebc84a253a54693 Mon Sep 17 00:00:00 2001
From: Sebastian Leidig
Date: Wed, 28 Jul 2021 18:15:05 +0200
Subject: [PATCH 07/34] feat: make fields included in list exports configurable
---
src/app/core/config/config-fix.ts | 5 ++
.../entity-list/EntityListConfig.ts | 6 ++
.../entity-list/entity-list.component.html | 1 +
.../export-data-button.component.ts | 12 +++-
.../export-service/export-column-config.ts | 14 +++++
.../export-service/export.service.spec.ts | 58 +++++++++++++------
.../export/export-service/export.service.ts | 25 +++++---
7 files changed, 93 insertions(+), 28 deletions(-)
create mode 100644 src/app/core/export/export-service/export-column-config.ts
diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts
index d2de252a95..dbd0b7bf03 100644
--- a/src/app/core/config/config-fix.ts
+++ b/src/app/core/config/config-fix.ts
@@ -645,6 +645,11 @@ export const defaultJsonConfig = {
"type",
"assignedTo"
],
+ "exportConfig": [
+ { label: "Title", key: "title" },
+ { label: "Type", key: "type" },
+ { label: "Assigned users", key: "assignedTo" }
+ ]
}
},
"view:recurring-activity/:id": {
diff --git a/src/app/core/entity-components/entity-list/EntityListConfig.ts b/src/app/core/entity-components/entity-list/EntityListConfig.ts
index fdf1f5207d..73dcf7bfed 100644
--- a/src/app/core/entity-components/entity-list/EntityListConfig.ts
+++ b/src/app/core/entity-components/entity-list/EntityListConfig.ts
@@ -1,6 +1,7 @@
import { Entity } from "../../entity/model/entity";
import { FilterSelectionOption } from "../../filter/filter-selection/filter-selection";
import { FormFieldConfig } from "../entity-form/entity-form/FormConfig";
+import { ExportColumnConfig } from "../../export/export-service/export-column-config";
export interface EntityListConfig {
title: string;
@@ -17,6 +18,11 @@ export interface EntityListConfig {
* Default is no filters.
*/
filters?: FilterConfig[];
+
+ /**
+ * Optional config defining what fields are included in exports.
+ */
+ exportConfig?: ExportColumnConfig[];
}
export interface ColumnGroupsConfig {
diff --git a/src/app/core/entity-components/entity-list/entity-list.component.html b/src/app/core/entity-components/entity-list/entity-list.component.html
index 99389f2cda..d8a058aa28 100644
--- a/src/app/core/entity-components/entity-list/entity-list.component.html
+++ b/src/app/core/entity-components/entity-list/entity-list.component.html
@@ -59,6 +59,7 @@
{{ listName }}
{
beforeEach(() => {
TestBed.configureTestingModule({
- providers: [ExportService, QueryService],
+ providers: [ExportService],
});
service = TestBed.inject(ExportService);
@@ -91,22 +91,41 @@ describe("ExportService", () => {
expect(result).toEqual(expected);
});
- it("should create a csv string correctly with multiple objects", () => {
- class TestClass {
- _id;
- _rev;
- propOne;
- propTwo;
- }
- const test = new TestClass();
- test._id = 1;
- test._rev = 2;
- test.propOne = "first";
- test.propTwo = "second";
- const expected =
- '"_id","_rev","propOne","propTwo"\r\n"1","2","first","second"\r\n"1","2","first","second"';
- const result = service.createCsv([test, test]);
- expect(result).toEqual(expected);
+ it("should export all properties", async () => {
+ const testObject1 = {
+ name: "foo",
+ age: 12,
+ };
+ const testObject2 = {
+ name: "bar",
+ age: 15,
+ extra: true,
+ };
+
+ const csvResult = await service.createCsv([testObject1, testObject2]);
+
+ expect(csvResult).toBe(
+ '"name","age","extra"\r\n"foo","12",\r\n"bar","15","1"'
+ );
+ });
+
+ it("should export only properties mentioned in config", async () => {
+ const testObject1 = {
+ name: "foo",
+ age: 12,
+ };
+ const testObject2 = {
+ name: "bar",
+ age: 15,
+ extra: true,
+ };
+
+ const csvResult = await service.createCsv(
+ [testObject1, testObject2],
+ [{ label: "test name", key: "name" }]
+ );
+
+ expect(csvResult).toBe('"test name"\r\n"foo"\r\n"bar"');
});
it("should transform object properties to their label for export", async () => {
@@ -120,19 +139,22 @@ describe("ExportService", () => {
class TestEntity extends Entity {
@DatabaseField() enumProperty: ConfigurableEnumValue;
@DatabaseField() dateProperty: Date;
+ @DatabaseField() boolProperty: boolean;
}
const testEntity = new TestEntity();
testEntity.enumProperty = testEnumValue;
testEntity.dateProperty = new Date(testDate);
+ testEntity.boolProperty = true;
const csvExport = await service.createCsv([testEntity]);
const rows = csvExport.split(ExportService.SEPARATOR_ROW);
expect(rows).toHaveSize(1 + 1); // includes 1 header line
const columnValues = rows[1].split(ExportService.SEPARATOR_COL);
- expect(columnValues).toHaveSize(2 + 1);
+ expect(columnValues).toHaveSize(3 + 1);
expect(columnValues).toContain('"' + testEnumValue.label + '"');
expect(columnValues).toContain(new Date(testDate).toISOString());
+ expect(columnValues).toContain('"1"'); // true => "1"
});
});
diff --git a/src/app/core/export/export-service/export.service.ts b/src/app/core/export/export-service/export.service.ts
index c0f1fef331..c069a68bd5 100644
--- a/src/app/core/export/export-service/export.service.ts
+++ b/src/app/core/export/export-service/export.service.ts
@@ -1,6 +1,7 @@
import { Injectable } from "@angular/core";
import { Papa } from "ngx-papaparse";
import { entityListSortingAccessor } from "../../entity-components/entity-subrecord/entity-subrecord/sorting-accessor";
+import { ExportColumnConfig } from "./export-column-config";
/**
* Prepare data for export in csv format.
@@ -35,28 +36,36 @@ export class ExportService {
* Creates a CSV string of the input data
*
* @param data an array of elements
+ * @param config (Optional) config specifying which fields should be exported
* @returns string a valid CSV string of the input data
*/
- createCsv(data: any[]): string {
+ createCsv(data: any[], config?: ExportColumnConfig[]): string {
const allFields = new Set();
const exportableData = [];
data.forEach((element: any) => {
const exportableObj = {};
- Object.keys(element).forEach((key: string) => {
- const res = entityListSortingAccessor(element, key);
- if (res?.toString().match(/\[object.*\]/) === null) {
- allFields.add(key);
- exportableObj[key] = res;
+ const currentRowConfig =
+ config ??
+ Object.keys(element).map((key) => ({ key: key } as ExportColumnConfig));
+ for (const columnConfig of currentRowConfig) {
+ const label = columnConfig.label ?? columnConfig.key;
+ const value = entityListSortingAccessor(element, columnConfig.key);
+ if (value?.toString().match(/\[object.*\]/) !== null) {
+ // skip object values that cannot be converted to a meaningful string
+ continue;
}
- });
+
+ exportableObj[label] = value;
+ allFields.add(label);
+ }
exportableData.push(exportableObj);
});
return this.papa.unparse(
- { data: exportableData, fields: [...new Set(allFields)] },
+ { data: exportableData, fields: [...allFields] },
{ quotes: true, header: true }
);
}
From 89c6b3c4a74d2825359d73c9c03dbd102e3f29ac Mon Sep 17 00:00:00 2001
From: Sebastian Leidig
Date: Tue, 3 Aug 2021 10:46:26 +0200
Subject: [PATCH 08/34] refactor: include optional prefix parameter in
query-service toEntity
to avoid explicit ":addPrefix" calls
---
.../features/reporting/query.service.spec.ts | 24 +++++++++++++++++++
src/app/features/reporting/query.service.ts | 7 +++++-
2 files changed, 30 insertions(+), 1 deletion(-)
diff --git a/src/app/features/reporting/query.service.spec.ts b/src/app/features/reporting/query.service.spec.ts
index 82b393251c..5aa89d4d09 100644
--- a/src/app/features/reporting/query.service.spec.ts
+++ b/src/app/features/reporting/query.service.spec.ts
@@ -442,4 +442,28 @@ describe("QueryService", () => {
expect(eventsWithNotes).toContain(note1);
expect(eventsWithNotes).toContain(note2);
});
+
+ it("should do addPrefix as part of toEntities if optional parameter is given", async () => {
+ const queryWithAddPrefix = `
+ ${School.ENTITY_TYPE}:toArray[*privateSchool=true]
+ :getRelated(${ChildSchoolRelation.ENTITY_TYPE}, schoolId)
+ .childId:addPrefix(${Child.ENTITY_TYPE}):toEntities.name`;
+ const queryWithoutAddPrefix = `
+ ${School.ENTITY_TYPE}:toArray[*privateSchool=true]
+ :getRelated(${ChildSchoolRelation.ENTITY_TYPE}, schoolId)
+ .childId:toEntities(${Child.ENTITY_TYPE}).name`;
+
+ const resultWithAddPrefix = await service.queryData(
+ queryWithAddPrefix,
+ null,
+ null
+ );
+ const resultWithoutAddPrefix = await service.queryData(
+ queryWithoutAddPrefix,
+ null,
+ null
+ );
+
+ expect(resultWithoutAddPrefix).toEqual(resultWithAddPrefix);
+ });
});
diff --git a/src/app/features/reporting/query.service.ts b/src/app/features/reporting/query.service.ts
index 2cd635ddb5..3451e5ab5c 100644
--- a/src/app/features/reporting/query.service.ts
+++ b/src/app/features/reporting/query.service.ts
@@ -154,9 +154,14 @@ export class QueryService {
/**
* Turns a list of ids (with the entity prefix) into a list of entities
* @param ids the array of ids with entity prefix
+ * @param entityPrefix (Optional) entity type prefix that should be added to the given ids where prefix is still missing
* @returns a list of entity objects
*/
- toEntities(ids: string[]): Entity[] {
+ toEntities(ids: string[], entityPrefix?: string): Entity[] {
+ if (entityPrefix) {
+ ids = this.addPrefix(ids, entityPrefix);
+ }
+
return ids.map((id) => {
const prefix = id.split(":")[0];
return this.entities[prefix][id];
From 1546f53fd9cf66b31075d5ed452539007ee1a00e Mon Sep 17 00:00:00 2001
From: Sebastian Leidig
Date: Tue, 3 Aug 2021 20:10:48 +0200
Subject: [PATCH 09/34] fix: create exports in valid json format
closes #469
---
.../core/admin/admin/admin.component.spec.ts | 4 +-
src/app/core/admin/admin/admin.component.ts | 8 ++--
.../admin/services/backup.service.spec.ts | 42 +++++++------------
src/app/core/admin/services/backup.service.ts | 9 +---
.../export-service/export.service.spec.ts | 33 +++------------
.../export/export-service/export.service.ts | 7 +---
6 files changed, 29 insertions(+), 74 deletions(-)
diff --git a/src/app/core/admin/admin/admin.component.spec.ts b/src/app/core/admin/admin/admin.component.spec.ts
index 3e36b05b26..51f19e6583 100644
--- a/src/app/core/admin/admin/admin.component.spec.ts
+++ b/src/app/core/admin/admin/admin.component.spec.ts
@@ -170,8 +170,8 @@ describe("AdminComponent", () => {
}));
it("should open dialog and call backup service when loading backup", fakeAsync(() => {
- const mockFileReader = createFileReaderMock();
- mockBackupService.getJsonExport.and.returnValue(Promise.resolve(""));
+ const mockFileReader = createFileReaderMock("[]");
+ mockBackupService.getJsonExport.and.returnValue(Promise.resolve("[]"));
createDialogMock();
component.loadBackup(null);
diff --git a/src/app/core/admin/admin/admin.component.ts b/src/app/core/admin/admin/admin.component.ts
index 79a8b0b61d..90422c7f54 100644
--- a/src/app/core/admin/admin/admin.component.ts
+++ b/src/app/core/admin/admin/admin.component.ts
@@ -129,11 +129,9 @@ export class AdminComponent implements OnInit {
const dialogRef = this.confirmationDialog.openDialog(
$localize`Overwrite complete database?`,
- $localize`Are you sure you want to restore this backup? This will delete all ${
- restorePoint.split("\n").length
- } existing records in the database, restoring ${
- newData.split("\n").length
- } records from the loaded file.`
+ $localize`Are you sure you want to restore this backup? This will
+ delete all ${JSON.parse(restorePoint).length} existing records,
+ restoring ${JSON.parse(newData).length} records from the loaded file.`
);
dialogRef.afterClosed().subscribe(async (confirmed) => {
diff --git a/src/app/core/admin/services/backup.service.spec.ts b/src/app/core/admin/services/backup.service.spec.ts
index f1d5f20889..e8ec8af91e 100644
--- a/src/app/core/admin/services/backup.service.spec.ts
+++ b/src/app/core/admin/services/backup.service.spec.ts
@@ -3,6 +3,7 @@ import { TestBed } from "@angular/core/testing";
import { BackupService } from "./backup.service";
import { Database } from "../../database/database";
import { PouchDatabase } from "../../database/pouch-database";
+import { ExportService } from "../../export/export-service/export.service";
describe("BackupService", () => {
let db: PouchDatabase;
@@ -46,7 +47,9 @@ describe("BackupService", () => {
const jsonExport = await service.getJsonExport();
- expect(jsonExport.split(BackupService.SEPARATOR_ROW)).toHaveSize(2);
+ expect(jsonExport).toBe(
+ `[{"test":1,"_id":"Test:1","_rev":"${res[0]._rev}"},{"test":2,"_id":"Test:2","_rev":"${res[1]._rev}"}]`
+ );
});
it("getJsonExport | clearDatabase | importJson should restore all records", async () => {
@@ -77,36 +80,23 @@ describe("BackupService", () => {
const csvExport = await service.getCsvExport();
- expect(csvExport.split(BackupService.SEPARATOR_ROW)).toHaveSize(2 + 1); // includes 1 header line
- });
-
- it("getCsvExport should contain a column for every property", async () => {
- await db.put({ _id: "Test:1", test: 1 });
- await db.put({ _id: "Test:2", other: 2 });
- const res = await db.getAll();
- expect(res).toHaveSize(2);
-
- const csvExport = await service.getCsvExport();
-
- const rows = csvExport.split(BackupService.SEPARATOR_ROW);
- expect(rows).toHaveSize(2 + 1); // includes 1 header line
- expect(rows[0].split(BackupService.SEPARATOR_COL)).toHaveSize(3 + 1); // includes _rev
+ expect(csvExport.split(ExportService.SEPARATOR_ROW)).toHaveSize(2 + 1); // includes 1 header line
});
it("importCsv should add records", async () => {
const csv =
"_id" +
- BackupService.SEPARATOR_COL +
+ ExportService.SEPARATOR_COL +
"test" +
- BackupService.SEPARATOR_ROW +
+ ExportService.SEPARATOR_ROW +
'"Test:1"' +
- BackupService.SEPARATOR_COL +
+ ExportService.SEPARATOR_COL +
"1" +
- BackupService.SEPARATOR_ROW +
+ ExportService.SEPARATOR_ROW +
'"Test:2"' +
- BackupService.SEPARATOR_COL +
+ ExportService.SEPARATOR_COL +
"2" +
- BackupService.SEPARATOR_ROW;
+ ExportService.SEPARATOR_ROW;
await service.importCsv(csv, true);
@@ -121,14 +111,14 @@ describe("BackupService", () => {
it("importCsv should not add empty properties to records", async () => {
const csv =
"_id" +
- BackupService.SEPARATOR_COL +
+ ExportService.SEPARATOR_COL +
"other" +
- BackupService.SEPARATOR_COL +
+ ExportService.SEPARATOR_COL +
"test" +
- BackupService.SEPARATOR_ROW +
+ ExportService.SEPARATOR_ROW +
'"Test:1"' +
- BackupService.SEPARATOR_COL +
- BackupService.SEPARATOR_COL +
+ ExportService.SEPARATOR_COL +
+ ExportService.SEPARATOR_COL +
"1";
await service.importCsv(csv);
diff --git a/src/app/core/admin/services/backup.service.ts b/src/app/core/admin/services/backup.service.ts
index 5f3e8ff301..bdcde1569c 100644
--- a/src/app/core/admin/services/backup.service.ts
+++ b/src/app/core/admin/services/backup.service.ts
@@ -11,11 +11,6 @@ import { ExportService } from "../../export/export-service/export.service";
providedIn: "root",
})
export class BackupService {
- /** CSV row separator */
- static readonly SEPARATOR_ROW = "\n";
- /** CSV column/field separator */
- static readonly SEPARATOR_COL = ",";
-
constructor(
private db: Database,
private papa: Papa,
@@ -66,8 +61,8 @@ export class BackupService {
* @param forceUpdate should existing objects be overridden? Default false
*/
async importJson(json, forceUpdate = false): Promise {
- for (const stringRecord of json.split(BackupService.SEPARATOR_ROW)) {
- const record = JSON.parse(stringRecord);
+ const documents = JSON.parse(json);
+ for (const record of documents) {
// Remove _rev so CouchDB treats it as a new rather than a updated document
delete record._rev;
await this.db.put(record, forceUpdate);
diff --git a/src/app/core/export/export-service/export.service.spec.ts b/src/app/core/export/export-service/export.service.spec.ts
index 7326c33adb..e159c5bd56 100644
--- a/src/app/core/export/export-service/export.service.spec.ts
+++ b/src/app/core/export/export-service/export.service.spec.ts
@@ -5,7 +5,6 @@ import { ConfigurableEnumValue } from "../../configurable-enum/configurable-enum
import { DatabaseField } from "../../entity/database-field.decorator";
import { DatabaseEntity } from "../../entity/database-entity.decorator";
import { Entity } from "../../entity/model/entity";
-import { BackupService } from "../../admin/services/backup.service";
describe("ExportService", () => {
let service: ExportService;
@@ -22,25 +21,7 @@ describe("ExportService", () => {
expect(service).toBeTruthy();
});
- it("should create the correct json object", () => {
- class TestClass {
- propertyOne;
- propertyTwo;
- }
- const test = new TestClass();
- test.propertyOne = "Hello";
- test.propertyTwo = "World";
-
- const result = service.createJson([test]);
-
- const expected = JSON.stringify({
- propertyOne: "Hello",
- propertyTwo: "World",
- });
- expect(result).toEqual(expected);
- });
-
- it("should transform a json array", () => {
+ it("should export to json array", () => {
class TestClass {
propertyOne;
propertyTwo;
@@ -51,13 +32,9 @@ describe("ExportService", () => {
const result = service.createJson([test, test]);
- let expected = JSON.stringify({
- propertyOne: "Hello",
- propertyTwo: "World",
- });
- expected += BackupService.SEPARATOR_ROW;
- expected += JSON.stringify({ propertyOne: "Hello", propertyTwo: "World" });
- expect(result).toEqual(expected);
+ expect(result).toEqual(
+ '[{"propertyOne":"Hello","propertyTwo":"World"},{"propertyOne":"Hello","propertyTwo":"World"}]'
+ );
});
it("should contain a column for every property", async () => {
@@ -91,7 +68,7 @@ describe("ExportService", () => {
expect(result).toEqual(expected);
});
- it("should export all properties", async () => {
+ it("should export all properties if no config is provided", async () => {
const testObject1 = {
name: "foo",
age: 12,
diff --git a/src/app/core/export/export-service/export.service.ts b/src/app/core/export/export-service/export.service.ts
index c069a68bd5..71a59d5662 100644
--- a/src/app/core/export/export-service/export.service.ts
+++ b/src/app/core/export/export-service/export.service.ts
@@ -24,12 +24,7 @@ export class ExportService {
* @returns string containing all the values stringified elements of the input data
*/
createJson(data): string {
- let res = "";
- data.forEach((r) => {
- res += JSON.stringify(r) + ExportService.SEPARATOR_ROW;
- });
-
- return res.trim();
+ return JSON.stringify(data);
}
/**
From 8710e65f28cd8cc51a064986405232e4269f2642 Mon Sep 17 00:00:00 2001
From: Sebastian Leidig
Date: Wed, 4 Aug 2021 11:53:53 +0200
Subject: [PATCH 10/34] fix: export boolean as true/false rather than as number
---
.../entity-subrecord/entity-subrecord/sorting-accessor.ts | 6 +++---
src/app/core/export/export-service/export.service.spec.ts | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/app/core/entity-components/entity-subrecord/entity-subrecord/sorting-accessor.ts b/src/app/core/entity-components/entity-subrecord/entity-subrecord/sorting-accessor.ts
index 1ddc624164..9e374eb628 100644
--- a/src/app/core/entity-components/entity-subrecord/entity-subrecord/sorting-accessor.ts
+++ b/src/app/core/entity-components/entity-subrecord/entity-subrecord/sorting-accessor.ts
@@ -12,10 +12,10 @@ export function entityListSortingAccessor(data: Object, sortingHeader: string) {
"label" in data[sortingHeader]
) {
return data[sortingHeader].label;
- } else if (data[sortingHeader] instanceof Date) {
- return data[sortingHeader];
- } else {
+ } else if (typeof data[sortingHeader] === "string") {
return tryNumber(data[sortingHeader]);
+ } else {
+ return data[sortingHeader];
}
}
diff --git a/src/app/core/export/export-service/export.service.spec.ts b/src/app/core/export/export-service/export.service.spec.ts
index e159c5bd56..ed14ff51ce 100644
--- a/src/app/core/export/export-service/export.service.spec.ts
+++ b/src/app/core/export/export-service/export.service.spec.ts
@@ -82,7 +82,7 @@ describe("ExportService", () => {
const csvResult = await service.createCsv([testObject1, testObject2]);
expect(csvResult).toBe(
- '"name","age","extra"\r\n"foo","12",\r\n"bar","15","1"'
+ '"name","age","extra"\r\n"foo","12",\r\n"bar","15","true"'
);
});
@@ -132,6 +132,6 @@ describe("ExportService", () => {
expect(columnValues).toHaveSize(3 + 1);
expect(columnValues).toContain('"' + testEnumValue.label + '"');
expect(columnValues).toContain(new Date(testDate).toISOString());
- expect(columnValues).toContain('"1"'); // true => "1"
+ expect(columnValues).toContain('"true"');
});
});
From f0a0a2f81ad2bb368e9d859352e0275d9649d2b4 Mon Sep 17 00:00:00 2001
From: Simon <33730997+TheSlimvReal@users.noreply.github.com>
Date: Wed, 4 Aug 2021 12:07:51 +0200
Subject: [PATCH 11/34] fix(core): forward requests to base assets folder in
i18n subfolders (#936)
fixes #926
---
build/default.conf | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/build/default.conf b/build/default.conf
index 141a9d51b7..d38723fd83 100644
--- a/build/default.conf
+++ b/build/default.conf
@@ -6,12 +6,20 @@ server {
#access_log /var/log/nginx/host.access.log main;
root /usr/share/nginx/html;
- location ^~ /de {
+
+ # Catch requests to the (locale) assets folder and add fallback to super-folder
+ # E.g. if '/en-US/assets/config.json' doesn't exist, try '/assets/config.json'
+ location ~* ^/.+/assets/(.+)$ {
+ # $1 refers to everything after 'assets/'
+ try_files $uri /assets/$1 =404;
+ }
+
+ location /de {
index index.html index.htm;
try_files $uri $uri/ /de/index.html;
}
- location ^~ /en {
+ location /en {
index index.html index.htm;
try_files $uri $uri/ /en-US/index.html;
}
From 5fd685695425d72e0fb8a8088143efc0f9d1f579 Mon Sep 17 00:00:00 2001
From: kirtijadhav <83791155+kirtijadhav@users.noreply.github.com>
Date: Sat, 7 Aug 2021 06:34:26 +0200
Subject: [PATCH 12/34] fix: Tooltips open when clicking the question mark on
mobile devices
fixes #891
---
src/app/core/config/config-fix.ts | 20 +++++------
.../edit-configurable-enum.component.html | 7 +---
.../entity-form/entity-form.module.ts | 6 ++++
.../entity-form/entity-form.component.html | 33 ++++++++++++-------
.../entity-form/entity-form.component.scss | 4 +++
.../edit-long-text.component.html | 6 ----
.../edit-text/edit-text.component.html | 6 ----
7 files changed, 42 insertions(+), 40 deletions(-)
diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts
index dbd0b7bf03..e2bd05617c 100644
--- a/src/app/core/config/config-fix.ts
+++ b/src/app/core/config/config-fix.ts
@@ -602,16 +602,16 @@ export const defaultJsonConfig = {
component: "HistoricalDataComponent",
config: [
"date",
- "isMotivatedDuringClass" ,
- "isParticipatingInClass",
- "isInteractingWithOthers",
- "doesHomework",
- "isOnTime",
- "asksQuestions",
- "listens",
- "canWorkOnBoard",
- "isConcentrated",
- "doesNotDisturb",
+ {id: "isMotivatedDuringClass", visibleFrom: "lg" },
+ {id: "isParticipatingInClass", visibleFrom: "lg" },
+ {id: "isInteractingWithOthers", visibleFrom: "lg" },
+ {id: "doesHomework", visibleFrom: "lg" },
+ {id: "isOnTime", visibleFrom: "lg" },
+ {id: "asksQuestions", visibleFrom: "lg" },
+ {id: "listens", visibleFrom: "lg" },
+ {id: "canWorkOnBoard", visibleFrom: "lg" },
+ {id: "isConcentrated", visibleFrom: "lg" },
+ {id: "doesNotDisturb", visibleFrom: "lg" },
]
}
]
diff --git a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html
index f4b3c1d632..b208f491af 100644
--- a/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html
+++ b/src/app/core/configurable-enum/edit-configurable-enum/edit-configurable-enum.component.html
@@ -7,10 +7,5 @@
{{ o.label }}
-
+
diff --git a/src/app/core/entity-components/entity-form/entity-form.module.ts b/src/app/core/entity-components/entity-form/entity-form.module.ts
index 6819f3585f..072959356f 100644
--- a/src/app/core/entity-components/entity-form/entity-form.module.ts
+++ b/src/app/core/entity-components/entity-form/entity-form.module.ts
@@ -6,6 +6,9 @@ import { MatButtonModule } from "@angular/material/button";
import { FlexModule } from "@angular/flex-layout";
import { ViewModule } from "../../view/view.module";
import { PermissionsModule } from "../../permissions/permissions.module";
+import { MatIconModule } from "@angular/material/icon";
+import { MatTooltipModule } from "@angular/material/tooltip";
+import { MatFormFieldModule } from "@angular/material/form-field";
@NgModule({
declarations: [EntityFormComponent],
@@ -15,6 +18,9 @@ import { PermissionsModule } from "../../permissions/permissions.module";
FlexModule,
ViewModule,
PermissionsModule,
+ MatIconModule,
+ MatTooltipModule,
+ MatFormFieldModule,
],
providers: [EntityFormService],
exports: [EntityFormComponent],
diff --git a/src/app/core/entity-components/entity-form/entity-form/entity-form.component.html b/src/app/core/entity-components/entity-form/entity-form/entity-form.component.html
index b4ee057a16..98eb8e2c3e 100644
--- a/src/app/core/entity-components/entity-form/entity-form/entity-form.component.html
+++ b/src/app/core/entity-components/entity-form/entity-form/entity-form.component.html
@@ -6,18 +6,27 @@
fxLayout.sm="row wrap"
>
-
-
-
+
+
+
+
+
+
diff --git a/src/app/core/entity-components/entity-form/entity-form/entity-form.component.scss b/src/app/core/entity-components/entity-form/entity-form/entity-form.component.scss
index 23aed4d2ea..f95ec42686 100644
--- a/src/app/core/entity-components/entity-form/entity-form/entity-form.component.scss
+++ b/src/app/core/entity-components/entity-form/entity-form/entity-form.component.scss
@@ -1,3 +1,7 @@
.edit-button {
margin-left: 10px;
}
+.form-field {
+ display: inline-block;
+ margin-left: 20px;
+}
diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-long-text/edit-long-text.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-long-text/edit-long-text.component.html
index a9fa8eea4e..e4daf3c9a4 100644
--- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-long-text/edit-long-text.component.html
+++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-long-text/edit-long-text.component.html
@@ -1,10 +1,4 @@
-
-
This field is required
From b5e4d423967f1358685a228472dfa4eab814926e Mon Sep 17 00:00:00 2001
From: kirtijadhav <83791155+kirtijadhav@users.noreply.github.com>
Date: Sat, 7 Aug 2021 06:52:51 +0200
Subject: [PATCH 13/34] fix: add spacing between author names
fixes #869
---
.../display-entity-array/display-entity-array.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/core/entity-components/entity-utils/view-components/display-entity-array/display-entity-array.component.html b/src/app/core/entity-components/entity-utils/view-components/display-entity-array/display-entity-array.component.html
index 61389d7c5f..8c7876499e 100644
--- a/src/app/core/entity-components/entity-utils/view-components/display-entity-array/display-entity-array.component.html
+++ b/src/app/core/entity-components/entity-utils/view-components/display-entity-array/display-entity-array.component.html
@@ -1,5 +1,5 @@
-
From a7c308fc7584120debcd539734e0c127b9367b6d Mon Sep 17 00:00:00 2001
From: Simon <33730997+TheSlimvReal@users.noreply.github.com>
Date: Mon, 9 Aug 2021 08:24:46 +0200
Subject: [PATCH 14/34] docs: updated number of guides
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index ff34a05fd3..34b54b88a1 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[![Build Status](https://travis-ci.org/Aam-Digital/ndb-core.svg?branch=master)](https://travis-ci.org/Aam-Digital/ndb-core)
[![Code Climate](https://codeclimate.com/github/Aam-Digital/ndb-core/badges/gpa.svg)](https://codeclimate.com/github/Aam-Digital/ndb-core)
[![Test Coverage](https://api.codeclimate.com/v1/badges/4e4a7a6301064019b2c9/test_coverage)](https://codeclimate.com/github/Aam-Digital/ndb-core/test_coverage)
-[![Guides](https://img.shields.io/badge/Tutorial%20%26%20Guides-13-blue)](https://aam-digital.github.io/ndb-core/documentation/additional-documentation/overview.html)
+[![Guides](https://img.shields.io/badge/Tutorial%20%26%20Guides-20-blue)](https://aam-digital.github.io/ndb-core/documentation/additional-documentation/overview.html)
[![Doc CoverageDocs](https://aam-digital.github.io/ndb-core/documentation/images/coverage-badge-documentation.svg)](https://aam-digital.github.io/ndb-core/documentation/modules.html)
From 8074740dfc26f7aa689feeab22fcef0f73b67e8d Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Mon, 9 Aug 2021 08:52:30 +0200
Subject: [PATCH 15/34] fix(changelog): remove irrelevant versions and details
(#939)
excludes prerelease versions
removes lines marked as hidden by starting with .
fixes #925
---
.../latest-changes/latest-changes.module.ts | 4 ++
.../latest-changes.service.spec.ts | 59 +++++++++++++++++++
.../latest-changes/latest-changes.service.ts | 33 ++++++++---
3 files changed, 87 insertions(+), 9 deletions(-)
diff --git a/src/app/core/latest-changes/latest-changes.module.ts b/src/app/core/latest-changes/latest-changes.module.ts
index ff42cbaaff..bc1d5c3bd8 100644
--- a/src/app/core/latest-changes/latest-changes.module.ts
+++ b/src/app/core/latest-changes/latest-changes.module.ts
@@ -38,6 +38,10 @@ import { LatestChangesService } from "./latest-changes.service";
* Displaying app version and changelog information to the user
* through components that can be used in other templates
* as well as automatic popups on updates (see {@link UpdateManagerService}, {@link LatestChangesService}).
+ *
+ * Changelogs are dynamically loaded from GitHub Releases through the GitHub API.
+ * pre-releases are excluded and individual lines in the body can be hidden by starting
+ * text (after markdown characters) with a ".".
*/
@NgModule({
imports: [
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 c730323c29..47e6e404ec 100644
--- a/src/app/core/latest-changes/latest-changes.service.spec.ts
+++ b/src/app/core/latest-changes/latest-changes.service.spec.ts
@@ -47,6 +47,13 @@ describe("LatestChangesService", () => {
body: "A",
published_at: "2018-01-01",
},
+ {
+ name: "prerelease 1",
+ tag_name: "1.0-rc.1",
+ prerelease: true,
+ body: "A",
+ published_at: "2018-01-01",
+ },
];
beforeEach(() => {
@@ -122,4 +129,56 @@ describe("LatestChangesService", () => {
}
);
});
+
+ it("should not include prereleases", (done) => {
+ spyOn(http, "get").and.returnValue(of(testReleases));
+
+ service.getChangelogsBeforeVersion("1.1", 10).subscribe((result) => {
+ expect(result).toEqual([testReleases[2]]);
+ expect(result[0]["prerelease"]).toBeFalsy();
+ expect(result).not.toContain(testReleases[3]);
+ done();
+ });
+ });
+
+ it("should remove lines from release changelog body that are explicitly hidden by starting with a '.'", (done) => {
+ const testRelease = {
+ name: "test with notes",
+ tag_name: "3.0",
+ body: `changelog
+### Bugs
+* relevant fix
+* .hidden fix
+### .Hidden
+`,
+ };
+
+ spyOn(http, "get").and.returnValue(of([testRelease]));
+
+ service.getChangelogsBetweenVersions("3.0", "2.9").subscribe((result) => {
+ expect(result[0].tag_name).toBe(testRelease.tag_name);
+ expect(result[0].body).toBe(`changelog
+# Bugs
+* relevant fix
+`);
+ done();
+ });
+ });
+
+ it("should remove irrelevant details from release changelog body", (done) => {
+ const testRelease = {
+ name: "test with notes",
+ tag_name: "3.0",
+ body:
+ "* fix ([e03dcca](https://github.com/Aam-Digital/ndb-core/commit/e03dcca7d89e584b8f08cc7fe30621c1ad428dba))",
+ };
+
+ spyOn(http, "get").and.returnValue(of([testRelease]));
+
+ service.getChangelogsBetweenVersions("3.0", "2.9").subscribe((result) => {
+ expect(result[0].tag_name).toBe(testRelease.tag_name);
+ expect(result[0].body).toBe("* fix");
+ done();
+ });
+ });
});
diff --git a/src/app/core/latest-changes/latest-changes.service.ts b/src/app/core/latest-changes/latest-changes.service.ts
index 5bd1eb54cf..786c1f005a 100644
--- a/src/app/core/latest-changes/latest-changes.service.ts
+++ b/src/app/core/latest-changes/latest-changes.service.ts
@@ -119,6 +119,7 @@ export class LatestChangesService {
LatestChangesService.GITHUB_API + environment.repositoryId + "/releases"
)
.pipe(
+ map(excludePrereleases),
map(releaseFilter),
map((relevantReleases) =>
relevantReleases.map((r) => this.parseGithubApiRelease(r))
@@ -130,23 +131,37 @@ export class LatestChangesService {
return throwError("Could not load latest changes.");
})
);
+
+ function excludePrereleases(releases: any[]): Changelog[] {
+ return releases.filter(
+ (release) => !release.prerelease && !release.draft
+ );
+ }
}
private parseGithubApiRelease(githubResponse: any): Changelog {
- const releaseNotesWithoutHeading = githubResponse.body.replace(
- /#{1,2}[^###]*/,
- ""
- );
- const releaseNotesWithoutCommitRefs = releaseNotesWithoutHeading.replace(
- / \(\[\w{7}\]\([^\)]*\)\)/g,
- ""
- );
+ const cleanedReleaseNotes = githubResponse.body
+ .replace(
+ // remove heading
+ /#{1,2}[^###]*/,
+ ""
+ )
+ .replace(
+ // remove commit refs
+ / \(\[\w{7}\]\([^\)]*\)\)/g,
+ ""
+ )
+ .replace(
+ // remove lines starting with "." after markdown characters
+ /^(\*|\#)* *\.(.*)(\n|\r\n)/gm,
+ ""
+ );
return {
tag_name: githubResponse.tag_name,
name: githubResponse.name,
published_at: githubResponse.published_at,
- body: releaseNotesWithoutCommitRefs,
+ body: cleanedReleaseNotes,
};
}
}
From 7060c0f70a0db776858067672b7305fa8fddedd1 Mon Sep 17 00:00:00 2001
From: Sebastian Leidig
Date: Mon, 9 Aug 2021 17:44:55 +0200
Subject: [PATCH 16/34] fix(core): .add safeguard if changelogs not loaded
to prevent .length of undefined error
---
src/app/core/latest-changes/changelog/changelog.component.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/app/core/latest-changes/changelog/changelog.component.ts b/src/app/core/latest-changes/changelog/changelog.component.ts
index 2b21db58b3..a6c5082787 100644
--- a/src/app/core/latest-changes/changelog/changelog.component.ts
+++ b/src/app/core/latest-changes/changelog/changelog.component.ts
@@ -75,6 +75,10 @@ export class ChangelogComponent implements OnInit {
* Add one more previous release card to the end of the currently displayed list of changelogs.
*/
loadPreviousRelease() {
+ if (!this.changelogs) {
+ return;
+ }
+
const lastDisplayedVersion = this.changelogs[this.changelogs.length - 1]
.tag_name;
this.latestChangesService
From 99bc452e890c5a1291bd326a2bc0055570d9eb40 Mon Sep 17 00:00:00 2001
From: Sebastian Leidig
Date: Wed, 11 Aug 2021 11:18:47 +0200
Subject: [PATCH 17/34] chore(demo): add additional report including events and
notes as an example
---
src/app/core/config/config-fix.ts | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts
index e2bd05617c..9e552dc78e 100644
--- a/src/app/core/config/config-fix.ts
+++ b/src/app/core/config/config-fix.ts
@@ -10,6 +10,7 @@ import { mathLevels } from "../../child-dev-project/aser/model/mathLevels";
import { readingLevels } from "../../child-dev-project/aser/model/readingLevels";
import { warningLevels } from "../../child-dev-project/warning-levels";
import { ratingAnswers } from "../../features/historical-data/rating-answers";
+import { Note } from "../../child-dev-project/notes/model/note";
// prettier-ignore
export const defaultJsonConfig = {
@@ -783,6 +784,23 @@ export const defaultJsonConfig = {
]
}
],
+ },
+ {
+ "title": $localize`:Name of a report:Overall Activity Report`,
+ "aggregationDefinitions": [
+ {
+ "query": `${EventNote.ENTITY_TYPE}:toArray:addEntities(${Note.ENTITY_TYPE})[*date >= ? & date <= ?]`,
+ "groupBy": ["category"],
+ "label": $localize`:Label for a report query:Events`,
+ "aggregations": [
+ {
+ "query": `:getParticipantsWithAttendance(PRESENT):unique:addPrefix(${Child.ENTITY_TYPE}):toEntities`,
+ "groupBy": ["gender", "religion"],
+ "label": $localize`:Label for a report query:Participants`
+ }
+ ]
+ }
+ ],
}
]
}
From 326c5a773530a44d3e39b0f1928f6cd63b932308 Mon Sep 17 00:00:00 2001
From: Simon <33730997+TheSlimvReal@users.noreply.github.com>
Date: Thu, 12 Aug 2021 11:48:09 +0300
Subject: [PATCH 18/34] refactor: Local session is managed with local storage
---
.../previous-schools.stories.ts | 10 +-
.../entity-select/entity-select.component.ts | 1 +
.../session/login/login.component.spec.ts | 84 ++-
src/app/core/session/login/login.component.ts | 67 +-
.../session-service/local-session.spec.ts | 81 ++-
.../session/session-service/local-session.ts | 169 +++--
.../session-service/local-user.spec.ts | 10 +
.../session/session-service/local-user.ts | 53 ++
.../new-local-session.service.spec.ts | 31 -
.../new-local-session.service.ts | 173 ------
.../session-service/online-session.service.ts | 131 ----
.../session-service/remote-session.spec.ts | 74 ++-
.../session/session-service/remote-session.ts | 126 ++--
.../session-service/session.service.spec.ts | 96 +--
.../session-service/session.service.ts | 36 +-
.../synced-session.service.spec.ts | 583 ++++++++----------
.../session-service/synced-session.service.ts | 214 ++++---
.../session-states/connection-state.enum.ts | 28 -
.../session-states/login-state.enum.ts | 2 +
.../core/session/session.service.provider.ts | 20 +-
.../core/user/demo-user-generator.service.ts | 18 +-
.../user-account/user-account.component.html | 6 +-
.../user-account.component.spec.ts | 40 +-
.../user-account/user-account.component.ts | 18 +-
.../user-account/user-account.service.spec.ts | 55 +-
.../user/user-account/user-account.service.ts | 41 +-
src/app/core/user/user.spec.ts | 30 +-
src/app/core/user/user.ts | 69 +--
...le-service-user-settings.component.spec.ts | 37 +-
...ud-file-service-user-settings.component.ts | 24 +-
.../webdav/cloud-file-service.service.spec.ts | 2 +-
src/locale/messages.de.xlf | 366 ++++++-----
src/locale/messages.xlf | 330 +++++-----
33 files changed, 1379 insertions(+), 1646 deletions(-)
create mode 100644 src/app/core/session/session-service/local-user.spec.ts
create mode 100644 src/app/core/session/session-service/local-user.ts
delete mode 100644 src/app/core/session/session-service/new-local-session.service.spec.ts
delete mode 100644 src/app/core/session/session-service/new-local-session.service.ts
delete mode 100644 src/app/core/session/session-service/online-session.service.ts
delete mode 100644 src/app/core/session/session-states/connection-state.enum.ts
diff --git a/src/app/child-dev-project/previous-schools/previous-schools.stories.ts b/src/app/child-dev-project/previous-schools/previous-schools.stories.ts
index b320163af6..a11716940b 100644
--- a/src/app/child-dev-project/previous-schools/previous-schools.stories.ts
+++ b/src/app/child-dev-project/previous-schools/previous-schools.stories.ts
@@ -16,17 +16,15 @@ import { PouchDatabase } from "../../core/database/pouch-database";
import { EntitySchemaService } from "../../core/entity/schema/entity-schema.service";
import { Database } from "../../core/database/database";
import { ChildrenModule } from "../children/children.module";
-import { NewLocalSessionService } from "../../core/session/session-service/new-local-session.service";
-import { LoggingService } from "../../core/logging/logging.service";
import { SessionService } from "../../core/session/session-service/session.service";
+import { LocalSession } from "../../core/session/session-service/local-session";
const database = PouchDatabase.createWithInMemoryDB();
const schemaService = new EntitySchemaService();
const entityMapper = new EntityMapperService(database, schemaService);
-const sessionService = new NewLocalSessionService(
- new LoggingService(),
- schemaService,
- database
+const sessionService = new LocalSession(
+ database,
+ schemaService
);
const child = new Child("testChild");
diff --git a/src/app/core/entity-components/entity-utils/entity-select/entity-select.component.ts b/src/app/core/entity-components/entity-utils/entity-select/entity-select.component.ts
index a29bb1f4fc..e600a8817f 100644
--- a/src/app/core/entity-components/entity-utils/entity-select/entity-select.component.ts
+++ b/src/app/core/entity-components/entity-utils/entity-select/entity-select.component.ts
@@ -151,6 +151,7 @@ export class EntitySelectComponent implements OnChanges {
constructor(private entityMapperService: EntityMapperService) {
this.filteredEntities = this.formControl.valueChanges.pipe(
+ untilDestroyed(this),
filter((value) => value === null || typeof value === "string"), // sometimes produces entities
map((searchText?: string) => this.filter(searchText))
);
diff --git a/src/app/core/session/login/login.component.spec.ts b/src/app/core/session/login/login.component.spec.ts
index 73831781e3..24407ea696 100644
--- a/src/app/core/session/login/login.component.spec.ts
+++ b/src/app/core/session/login/login.component.spec.ts
@@ -15,18 +15,50 @@
* along with ndb-core. If not, see .
*/
-import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
+import {
+ ComponentFixture,
+ fakeAsync,
+ TestBed,
+ tick,
+ waitForAsync,
+} from "@angular/core/testing";
import { LoginComponent } from "./login.component";
+import { LoggingService } from "../../logging/logging.service";
+import { RouterTestingModule } from "@angular/router/testing";
+import { SessionService } from "../session-service/session.service";
+import { MatCardModule } from "@angular/material/card";
+import { MatFormFieldModule } from "@angular/material/form-field";
+import { MatInputModule } from "@angular/material/input";
+import { MatButtonModule } from "@angular/material/button";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { FormsModule } from "@angular/forms";
+import { LoginState } from "../session-states/login-state.enum";
+import { Router } from "@angular/router";
describe("LoginComponent", () => {
let component: LoginComponent;
let fixture: ComponentFixture;
+ let mockSessionService: jasmine.SpyObj;
beforeEach(
waitForAsync(() => {
+ mockSessionService = jasmine.createSpyObj(["login"]);
TestBed.configureTestingModule({
declarations: [LoginComponent],
+ imports: [
+ RouterTestingModule,
+ MatCardModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatButtonModule,
+ NoopAnimationsModule,
+ FormsModule,
+ ],
+ providers: [
+ LoggingService,
+ { provide: SessionService, useValue: mockSessionService },
+ ],
}).compileComponents();
})
);
@@ -37,9 +69,53 @@ describe("LoginComponent", () => {
fixture.detectChanges();
});
- /* TODO fix test case
- it('should be created', () => {
+ it("should be created", () => {
expect(component).toBeTruthy();
});
- */
+
+ it("should call router on successful login", fakeAsync(() => {
+ mockSessionService.login.and.resolveTo(LoginState.LOGGED_IN);
+ const routerSpy = spyOn(TestBed.inject(Router), "navigate");
+
+ component.login();
+ tick();
+
+ expect(routerSpy).toHaveBeenCalled();
+ }));
+
+ it("should show a message when login is unavailable", fakeAsync(() => {
+ expectErrorMessageOnState(LoginState.UNAVAILABLE);
+ }));
+
+ it("should show a message when login fails", fakeAsync(() => {
+ expectErrorMessageOnState(LoginState.LOGIN_FAILED);
+ }));
+
+ it("should show a message and call logging service on unexpected login state", fakeAsync(() => {
+ const loggerSpy = spyOn(TestBed.inject(LoggingService), "error");
+
+ expectErrorMessageOnState(LoginState.LOGGED_OUT);
+ expect(loggerSpy).toHaveBeenCalled();
+ }));
+
+ it("should show a message and call logging service on error", fakeAsync(() => {
+ mockSessionService.login.and.rejectWith();
+ expect(component.errorMessage).toBeFalsy();
+ const loggerSpy = spyOn(TestBed.inject(LoggingService), "error");
+
+ component.login();
+ tick();
+ expect(loggerSpy).toHaveBeenCalled();
+ expect(component.errorMessage).toBeTruthy();
+ }));
+
+ function expectErrorMessageOnState(loginState: LoginState) {
+ mockSessionService.login.and.resolveTo(loginState);
+ expect(component.errorMessage).toBeFalsy();
+
+ component.login();
+ tick();
+
+ expect(component.errorMessage).toBeTruthy();
+ }
});
diff --git a/src/app/core/session/login/login.component.ts b/src/app/core/session/login/login.component.ts
index b0bf3cac46..c14b8e4871 100644
--- a/src/app/core/session/login/login.component.ts
+++ b/src/app/core/session/login/login.component.ts
@@ -15,12 +15,11 @@
* along with ndb-core. If not, see .
*/
-import { Component, Optional } from "@angular/core";
-import { SyncState } from "../session-states/sync-state.enum";
+import { Component } from "@angular/core";
import { SessionService } from "../session-service/session.service";
import { LoginState } from "../session-states/login-state.enum";
-import { ConnectionState } from "../session-states/connection-state.enum";
import { ActivatedRoute, Router } from "@angular/router";
+import { LoggingService } from "../../logging/logging.service";
/**
* Form to allow users to enter their credentials and log in.
@@ -45,8 +44,9 @@ export class LoginComponent {
constructor(
private _sessionService: SessionService,
- @Optional() private router: Router,
- @Optional() private route: ActivatedRoute
+ private loggingService: LoggingService,
+ private router: Router,
+ private route: ActivatedRoute
) {}
/**
@@ -54,55 +54,48 @@ export class LoginComponent {
*/
login() {
this.loginInProgress = true;
+ this.errorMessage = "";
this._sessionService
.login(this.username, this.password)
.then((loginState) => {
- if (loginState === LoginState.LOGGED_IN) {
- this.onLoginSuccess();
- } else {
- if (
- this._sessionService.getSyncState().getState() ===
- SyncState.ABORTED &&
- this._sessionService.getConnectionState().getState() ===
- ConnectionState.OFFLINE
- ) {
+ switch (loginState) {
+ case LoginState.LOGGED_IN:
+ this.onLoginSuccess();
+ break;
+ case LoginState.UNAVAILABLE:
this.onLoginFailure(
- $localize`Can't login for the first time when offline. Please try again later.`
+ $localize`:LoginError:Please connect to the internet and try again`
);
- } else if (
- this._sessionService.getConnectionState().getState() ===
- ConnectionState.OFFLINE
- ) {
+ break;
+ case LoginState.LOGIN_FAILED:
this.onLoginFailure(
- $localize`Username or password incorrect!
- You might also face this problem because you are currently offline.
- Please connect to the internet to synchronize the latest user data.`
+ $localize`:LoginError:Username and/or password incorrect`
);
- } else {
- this.onLoginFailure($localize`Username or password incorrect!`);
- }
+ break;
+ default:
+ throw new Error(`Unexpected login state: ${loginState}`);
}
})
- .catch((reason) =>
- this.onLoginFailure(
- typeof reason === "string" ? reason : JSON.stringify(reason)
- )
- );
+ .catch((reason) => {
+ this.loggingService.error(`Unexpected login error: ${reason}`);
+ this.onLoginFailure($localize`:LoginError:An unexpected error occurred.
+ Please reload the the page and try again.
+ If you keep seeing this error message, please contact your system administrator.
+ `);
+ });
}
private onLoginSuccess() {
- // New routes are added at runtime,
- if (this.router && this.route) {
- this.router.navigate([], {
- relativeTo: this.route,
- });
- }
+ // New routes are added at runtime
+ this.router.navigate([], {
+ relativeTo: this.route,
+ });
this.reset();
// login component is automatically hidden based on _sessionService.isLoggedIn()
}
- private onLoginFailure(reason: any) {
+ private onLoginFailure(reason: string) {
this.reset();
this.errorMessage = reason;
}
diff --git a/src/app/core/session/session-service/local-session.spec.ts b/src/app/core/session/session-service/local-session.spec.ts
index 782cf51dbc..898660c71d 100644
--- a/src/app/core/session/session-service/local-session.spec.ts
+++ b/src/app/core/session/session-service/local-session.spec.ts
@@ -15,13 +15,20 @@
* along with ndb-core. If not, see .
*/
-import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
import { AppConfig } from "../../app-config/app-config";
import { LocalSession } from "./local-session";
import { SessionType } from "../session-type";
+import { passwordEqualsEncrypted, DatabaseUser, LocalUser } from "./local-user";
+import { LoginState } from "../session-states/login-state.enum";
+import {
+ TEST_PASSWORD,
+ TEST_USER,
+ testSessionServiceImplementation,
+} from "./session.service.spec";
describe("LocalSessionService", () => {
let localSession: LocalSession;
+ let testUser: DatabaseUser;
beforeEach(() => {
AppConfig.settings = {
@@ -32,11 +39,79 @@ describe("LocalSessionService", () => {
remote_url: "https://demo.aam-digital.com/db/",
},
};
+ localSession = new LocalSession();
+ });
- localSession = new LocalSession(new EntitySchemaService());
+ beforeEach(() => {
+ testUser = {
+ name: TEST_USER,
+ roles: ["user_app"],
+ };
+ localSession.saveUser(testUser, TEST_PASSWORD);
});
- it("should be created", async () => {
+ afterEach(() => {
+ localSession.removeUser(TEST_USER);
+ });
+
+ it("should be created", () => {
expect(localSession).toBeDefined();
});
+
+ it("should save user objects to local storage", () => {
+ const storedUser: LocalUser = JSON.parse(
+ window.localStorage.getItem(testUser.name)
+ );
+ expect(storedUser.name).toBe(testUser.name);
+ expect(storedUser.roles).toEqual(testUser.roles);
+ expect(
+ passwordEqualsEncrypted(TEST_PASSWORD, storedUser.encryptedPassword)
+ ).toBeTrue();
+ });
+
+ it("should login a previously saved user with correct password", async () => {
+ expect(localSession.getLoginState().getState()).toBe(LoginState.LOGGED_OUT);
+
+ await localSession.login(TEST_USER, TEST_PASSWORD);
+
+ expect(localSession.getLoginState().getState()).toBe(LoginState.LOGGED_IN);
+ });
+
+ it("should fail login with correct username but wrong password", async () => {
+ await localSession.login(TEST_USER, "wrong password");
+
+ expect(localSession.getLoginState().getState()).toBe(
+ LoginState.LOGIN_FAILED
+ );
+ });
+
+ it("should fail login with wrong username", async () => {
+ await localSession.login("wrongUsername", TEST_PASSWORD);
+
+ expect(localSession.getLoginState().getState()).toBe(
+ LoginState.UNAVAILABLE
+ );
+ });
+
+ it("should assign current user after successful login", async () => {
+ await localSession.login(TEST_USER, TEST_PASSWORD);
+
+ const currentUser = localSession.getCurrentDBUser();
+
+ expect(currentUser.name).toBe(TEST_USER);
+ expect(currentUser.roles).toEqual(testUser.roles);
+ });
+
+ it("should fail login after a user is removed", async () => {
+ localSession.removeUser(TEST_USER);
+
+ await localSession.login(TEST_USER, TEST_PASSWORD);
+
+ expect(localSession.getLoginState().getState()).toBe(
+ LoginState.UNAVAILABLE
+ );
+ expect(localSession.getCurrentUser()).toBeUndefined();
+ });
+
+ testSessionServiceImplementation(() => Promise.resolve(localSession));
});
diff --git a/src/app/core/session/session-service/local-session.ts b/src/app/core/session/session-service/local-session.ts
index b8f37acd1b..de982d8a52 100644
--- a/src/app/core/session/session-service/local-session.ts
+++ b/src/app/core/session/session-service/local-session.ts
@@ -14,136 +14,129 @@
* You should have received a copy of the GNU General Public License
* along with ndb-core. If not, see .
*/
-
-import PouchDB from "pouchdb-browser";
-
import { Injectable } from "@angular/core";
-
-import { AppConfig } from "../../app-config/app-config";
-import { User } from "../../user/user";
-
-import { SyncState } from "../session-states/sync-state.enum";
import { LoginState } from "../session-states/login-state.enum";
-import { StateHandler } from "../session-states/state-handler";
+import {
+ DatabaseUser,
+ encryptPassword,
+ LocalUser,
+ passwordEqualsEncrypted,
+} from "./local-user";
+import { Database } from "../../database/database";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
+import { User } from "../../user/user";
+import { SessionService } from "./session.service";
/**
* Responsibilities:
- * - Hold the local DB
- * - Hold local user
- * - Check credentials against DB
- * - Provide the state of the synchronisation of the local db
- * - we want to block before the first full sync
- * - Provide an interface to access the data
+ * - Manage local authentication
+ * - Save users in local storage
*/
@Injectable()
-export class LocalSession {
- /** local (IndexedDb) database PouchDB */
- public database: any;
- public liveSyncHandle: any;
-
- /** StateHandler for login state changes */
- public loginState: StateHandler;
- /** StateHandler for sync state changes */
- public syncState: StateHandler;
-
- /** The currently authenticated user entity */
- public currentUser: User;
-
+export class LocalSession extends SessionService {
+ private currentDBUser: DatabaseUser;
/**
- * Create a LocalSession and set up the local PouchDB instance based on AppConfig settings.
- * @param _entitySchemaService
+ * @deprecated instead use currentUser
*/
- constructor(private _entitySchemaService: EntitySchemaService) {
- this.database = new PouchDB(AppConfig.settings.database.name);
+ private currentUserEntity: User;
- this.loginState = new StateHandler(LoginState.LOGGED_OUT);
- this.syncState = new StateHandler(SyncState.UNSYNCED);
+ constructor(
+ private database?: Database,
+ private entitySchemaService?: EntitySchemaService
+ ) {
+ super();
}
/**
- * Get a login at the local session by fetching the user from the local database and validating the password.
+ * Get a login at the local session by fetching the user from the local storage and validating the password.
* Returns a Promise resolving with the loginState.
- * Attention: This method waits for the first synchronisation of the database (or a fail of said initial sync).
* @param username Username
* @param password Password
*/
public async login(username: string, password: string): Promise {
- try {
- await this.waitForFirstSync();
- const userEntity = await this.loadUser(username);
- if (userEntity.checkPassword(password)) {
- this.currentUser = userEntity;
- this.currentUser.decryptCloudPassword(password);
- this.loginState.setState(LoginState.LOGGED_IN);
- return LoginState.LOGGED_IN;
+ const user: LocalUser = JSON.parse(window.localStorage.getItem(username));
+ if (user) {
+ if (passwordEqualsEncrypted(password, user.encryptedPassword)) {
+ this.currentDBUser = user;
+ this.currentUserEntity = await this.loadUser(username);
+ this.getLoginState().setState(LoginState.LOGGED_IN);
} else {
- this.loginState.setState(LoginState.LOGIN_FAILED);
- return LoginState.LOGIN_FAILED;
- }
- } catch (error) {
- // possible error: initial sync failed or aborted
- if (
- error &&
- error.toState &&
- [SyncState.ABORTED, SyncState.FAILED].includes(error.toState)
- ) {
- if (this.loginState.getState() === LoginState.LOGIN_FAILED) {
- // The sync failed because the remote rejected
- return LoginState.LOGIN_FAILED;
- }
- // The sync failed for other reasons. The user should try again
- this.loginState.setState(LoginState.LOGGED_OUT);
- return LoginState.LOGGED_OUT;
- }
- // possible error: user object not found locally, which should return loginFailed.
- if (error && error.status && error.status === 404) {
- this.loginState.setState(LoginState.LOGIN_FAILED);
- return LoginState.LOGIN_FAILED;
+ this.getLoginState().setState(LoginState.LOGIN_FAILED);
}
- // all other cases must throw an error
- throw error;
+ } else {
+ this.getLoginState().setState(LoginState.UNAVAILABLE);
}
+ return this.getLoginState().getState();
}
/**
- * Wait for the first sync of the database, returns a Promise.
- * Resolves directly, if the database is not initial, otherwise waits for the first change of the SyncState to completed (or failed)
+ * Saves a user to the local storage
+ * @param user a object holding the username and the roles of the user
+ * @param password of the user
*/
- public async waitForFirstSync() {
- if (await this.isInitial()) {
- return await this.syncState.waitForChangeTo(SyncState.COMPLETED, [
- SyncState.FAILED,
- SyncState.ABORTED,
- ]);
+ public saveUser(user: DatabaseUser, password: string) {
+ const localUser: LocalUser = {
+ name: user.name,
+ roles: user.roles,
+ encryptedPassword: encryptPassword(password),
+ };
+ window.localStorage.setItem(localUser.name, JSON.stringify(localUser));
+ // Update when already logged in
+ if (this.getCurrentDBUser()?.name === localUser.name) {
+ this.currentDBUser = localUser;
}
}
/**
- * Check whether the local database is in an initial state.
- * This check can only be performed async, so this method returns a Promise
+ * Removes the user from the local storage.
+ * Method never fails, even if the user was not stored before
+ * @param username
*/
- public isInitial(): Promise {
- // `doc_count === 0 => initial` is a valid assumptions, as documents for users must always be present, even after db-clean
- return this.database.info().then((result) => result.doc_count === 0);
+ public removeUser(username: string) {
+ window.localStorage.removeItem(username);
+ }
+
+ public getCurrentUser(): User {
+ return this.currentUserEntity;
+ }
+
+ public checkPassword(username: string, password: string): boolean {
+ const user: LocalUser = JSON.parse(window.localStorage.getItem(username));
+ return user && passwordEqualsEncrypted(password, user.encryptedPassword);
+ }
+
+ public getCurrentDBUser(): DatabaseUser {
+ return this.currentDBUser;
}
/**
- * Logout
+ * Resets the login state and current user (leaving it in local storage to allow later local login)
*/
public logout() {
- this.currentUser = undefined;
- this.loginState.setState(LoginState.LOGGED_OUT);
+ this.currentDBUser = undefined;
+ this.currentUserEntity = undefined;
+ this.getLoginState().setState(LoginState.LOGGED_OUT);
}
/**
+ * TODO remove once admin information is migrated to new format (CouchDB)
* Helper to get a User Entity from the Database without needing the EntityMapperService
* @param userId Id of the User to be loaded
*/
public async loadUser(userId: string): Promise {
- const user = new User("");
- const userData = await this.database.get("User:" + userId);
- this._entitySchemaService.loadDataIntoEntity(user, userData);
- return user;
+ if (this.database && this.entitySchemaService) {
+ const user = new User("");
+ const userData = await this.database.get("User:" + userId);
+ this.entitySchemaService.loadDataIntoEntity(user, userData);
+ return user;
+ }
+ }
+
+ getDatabase(): Database {
+ return this.database;
+ }
+
+ sync(): Promise {
+ return Promise.reject(new Error("Cannot sync local session"));
}
}
diff --git a/src/app/core/session/session-service/local-user.spec.ts b/src/app/core/session/session-service/local-user.spec.ts
new file mode 100644
index 0000000000..34863a0f05
--- /dev/null
+++ b/src/app/core/session/session-service/local-user.spec.ts
@@ -0,0 +1,10 @@
+import { passwordEqualsEncrypted, encryptPassword } from "./local-user";
+
+describe("LocalUser", () => {
+ it("should match a password with its hash", () => {
+ const password = "TestPassword123-";
+ const encryptedPassword = encryptPassword(password);
+
+ expect(passwordEqualsEncrypted(password, encryptedPassword)).toBeTrue();
+ });
+});
diff --git a/src/app/core/session/session-service/local-user.ts b/src/app/core/session/session-service/local-user.ts
new file mode 100644
index 0000000000..176bc87816
--- /dev/null
+++ b/src/app/core/session/session-service/local-user.ts
@@ -0,0 +1,53 @@
+import * as CryptoJS from "crypto-js";
+
+/**
+ * User object as received from the remote server database.
+ * See {@link https://docs.couchdb.org/en/stable/intro/security.html?highlight=_users#users-documents}
+ */
+export interface DatabaseUser {
+ name: string;
+ roles: string[];
+}
+
+/**
+ * User object as prepared and used by the local session.
+ */
+export interface LocalUser extends DatabaseUser {
+ encryptedPassword: EncryptedPassword;
+}
+
+export interface EncryptedPassword {
+ hash: string;
+ salt: string;
+ iterations: number;
+ keySize: number;
+}
+
+export function encryptPassword(
+ password: string,
+ iterations = 128,
+ keySize = 256 / 32,
+ salt = CryptoJS.lib.WordArray.random(128 / 8).toString()
+): EncryptedPassword {
+ const hash = CryptoJS.PBKDF2(password, salt, {
+ keySize: keySize,
+ iterations: iterations,
+ }).toString();
+ return {
+ hash: hash,
+ iterations: iterations,
+ keySize: keySize,
+ salt: salt,
+ };
+}
+
+export function passwordEqualsEncrypted(
+ password: string,
+ encryptedPassword: EncryptedPassword
+): boolean {
+ const hash = CryptoJS.PBKDF2(password, encryptedPassword?.salt, {
+ iterations: encryptedPassword?.iterations,
+ keySize: encryptedPassword?.keySize,
+ }).toString();
+ return hash === encryptedPassword.hash;
+}
diff --git a/src/app/core/session/session-service/new-local-session.service.spec.ts b/src/app/core/session/session-service/new-local-session.service.spec.ts
deleted file mode 100644
index 17d853613e..0000000000
--- a/src/app/core/session/session-service/new-local-session.service.spec.ts
+++ /dev/null
@@ -1,31 +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 { EntitySchemaService } from "../../entity/schema/entity-schema.service";
-import { NewLocalSessionService } from "./new-local-session.service";
-import { testSessionServiceImplementation } from "./session.service.spec";
-import { PouchDatabase } from "../../database/pouch-database";
-
-describe("NewLocalSessionService", async () => {
- testSessionServiceImplementation(async () => {
- return new NewLocalSessionService(
- jasmine.createSpyObj(["warn"]),
- new EntitySchemaService(),
- PouchDatabase.createWithInMemoryDB()
- );
- });
-});
diff --git a/src/app/core/session/session-service/new-local-session.service.ts b/src/app/core/session/session-service/new-local-session.service.ts
deleted file mode 100644
index 49992bdda7..0000000000
--- a/src/app/core/session/session-service/new-local-session.service.ts
+++ /dev/null
@@ -1,173 +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 { Injectable } from "@angular/core";
-
-import { SessionService } from "./session.service";
-import { LoginState } from "../session-states/login-state.enum";
-import { Database } from "../../database/database";
-import { PouchDatabase } from "../../database/pouch-database";
-import { ConnectionState } from "../session-states/connection-state.enum";
-import { SyncState } from "../session-states/sync-state.enum";
-import { User } from "../../user/user";
-import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
-import { LoggingService } from "../../logging/logging.service";
-import { StateHandler } from "../session-states/state-handler";
-
-@Injectable()
-export class NewLocalSessionService extends SessionService {
- public liveSyncHandle: any;
-
- /** StateHandler for login state changes */
- public loginState: StateHandler = new StateHandler(
- LoginState.LOGGED_OUT
- );
- /** StateHandler for sync state changes */
- public syncState: StateHandler = new StateHandler(
- SyncState.UNSYNCED
- );
- /** StateHandler for connection state changes (not relevant for LocalSession) */
- public connectionState: StateHandler = new StateHandler(
- ConnectionState.DISCONNECTED
- );
-
- /** The currently authenticated user entity */
- public currentUser: User;
-
- constructor(
- private loggingService: LoggingService,
- private entitySchemaService: EntitySchemaService,
- private database: PouchDatabase
- ) {
- super();
- }
-
- /** see {@link SessionService} */
- public isLoggedIn(): boolean {
- return this.loginState.getState() === LoginState.LOGGED_IN;
- }
-
- /**
- * Get a login at the local session by fetching the user from the local database and validating the password.
- * Returns a Promise resolving with the loginState.
- * Attention: This method waits for the first synchronisation of the database (or a fail of said initial sync).
- * @param username Username
- * @param password Password
- */
- public async login(username: string, password: string): Promise {
- let userEntity: User;
-
- try {
- userEntity = await this.loadUser(username);
- } catch (error) {
- if (error?.status === 404) {
- return this.failLogin();
- } else {
- throw error;
- }
- }
-
- if (!userEntity.checkPassword(password)) {
- return this.failLogin();
- }
-
- return this.succeedLogin(userEntity, password);
- }
-
- /**
- * Update all states when login failed
- * @private
- */
- private failLogin(): LoginState {
- this.loginState.setState(LoginState.LOGIN_FAILED);
- return LoginState.LOGIN_FAILED;
- }
-
- /**
- * Update all states when login succeeded
- * @param loggedInUser
- * @param password
- * @private
- */
- private succeedLogin(loggedInUser: User, password: string): LoginState {
- this.currentUser = loggedInUser;
- this.currentUser.decryptCloudPassword(password);
- this.loginState.setState(LoginState.LOGGED_IN);
- this.connectionState.setState(ConnectionState.OFFLINE);
- return LoginState.LOGGED_IN;
- }
-
- /**
- * Helper to get a User Entity from the Database without needing the EntityMapperService
- * @param userId Id of the User to be loaded
- */
- private async loadUser(userId: string): Promise {
- const user = new User("");
- const userData = await this.database.get("User:" + userId);
- this.entitySchemaService.loadDataIntoEntity(user, userData);
- return user;
- }
-
- /** see {@link SessionService} */
- public getCurrentUser(): User {
- return this.currentUser;
- }
-
- /** see {@link SessionService} */
- public getLoginState() {
- return this.loginState;
- }
- /** see {@link SessionService} */
- public getConnectionState() {
- return this.connectionState;
- }
- /** see {@link SessionService} */
- public getSyncState() {
- return this.syncState;
- }
-
- /** see {@link SessionService} */
- public async sync(remoteDatabase?): Promise {
- this.syncState.setState(SyncState.STARTED);
- try {
- const result = await this.database.sync(remoteDatabase);
- this.syncState.setState(SyncState.COMPLETED);
- return result;
- } catch (error) {
- this.syncState.setState(SyncState.FAILED);
- throw error;
- }
- }
-
- /**
- * Get the local database instance that should be used for regular data access.
- * als see {@link SessionService}
- */
- public getDatabase(): Database {
- return this.database;
- }
-
- /**
- * Logout and stop any existing sync.
- * also see {@link SessionService}
- */
- public logout() {
- this.currentUser = undefined;
- this.loginState.setState(LoginState.LOGGED_OUT);
- this.connectionState.setState(ConnectionState.DISCONNECTED);
- }
-}
diff --git a/src/app/core/session/session-service/online-session.service.ts b/src/app/core/session/session-service/online-session.service.ts
deleted file mode 100644
index 85ed741be6..0000000000
--- a/src/app/core/session/session-service/online-session.service.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import { SessionService } from "./session.service";
-import { User } from "../../user/user";
-import { StateHandler } from "../session-states/state-handler";
-import { ConnectionState } from "../session-states/connection-state.enum";
-import { LoginState } from "../session-states/login-state.enum";
-import { SyncState } from "../session-states/sync-state.enum";
-import { Database } from "../../database/database";
-import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
-import { RemoteSession } from "./remote-session";
-import { PouchDatabase } from "../../database/pouch-database";
-import { LoggingService } from "../../logging/logging.service";
-
-/**
- * SessionService implementation for use of the app with direct requests to the remote server
- * avoiding any sync to local data.
- *
- * This can be a useful mode for users to save time (no waiting till initial sync is complete)
- * or when working on a device that the user is not regularly using (e.g. a public computer)
- * where data should not be saved locally for security reasons also.
- *
- * The OnlineSessionService mode is not usable without an internet connection. Offline functionality is not available.
- *
- * TODO: requires a configuration or UI option to select OnlineSession
- *
- * For an CouchDB/PouchDB sync based session implementation that allows offline use see {@link SyncedSessionService}
- */
-export class OnlineSessionService extends SessionService {
- private currentUser: User;
- private loginState: StateHandler = new StateHandler(
- LoginState.LOGGED_OUT
- );
- private connectionState: StateHandler = new StateHandler(
- ConnectionState.DISCONNECTED
- );
- private syncState: StateHandler = new StateHandler(
- SyncState.UNSYNCED
- );
- private remoteSession: RemoteSession;
- private database: PouchDatabase;
-
- constructor(
- private loggingService: LoggingService,
- private entitySchemaService: EntitySchemaService
- ) {
- super();
- this.remoteSession = new RemoteSession();
- this.database = new PouchDatabase(
- this.remoteSession.database,
- this.loggingService
- );
- }
-
- /** see {@link SessionService} */
- public getCurrentUser(): User {
- return this.currentUser;
- }
-
- /** see {@link SessionService} */
- public isLoggedIn(): boolean {
- return this.loginState.getState() === LoginState.LOGGED_IN;
- }
-
- /** see {@link SessionService} */
- public getConnectionState(): StateHandler {
- return this.connectionState;
- }
-
- /** see {@link SessionService} */
- public getLoginState(): StateHandler {
- return this.loginState;
- }
-
- /** see {@link SessionService} */
- public getSyncState(): StateHandler {
- return this.syncState;
- }
-
- /** see {@link SessionService} */
- public getDatabase(): Database {
- return this.database;
- }
-
- /**
- * Log in the given user authenticating against the remote server's CouchDB.
- *
- * also see {@link SessionService}
- */
- public async login(username, password): Promise {
- const connectionState: ConnectionState = await this.remoteSession.login(
- username,
- password
- );
- if (connectionState === ConnectionState.CONNECTED) {
- this.currentUser = await this.loadUser(username);
-
- this.loginState.setState(LoginState.LOGGED_IN);
- this.connectionState.setState(ConnectionState.CONNECTED);
- this.syncState.setState(SyncState.COMPLETED);
-
- return LoginState.LOGGED_IN;
- }
- return LoginState.LOGIN_FAILED;
- }
-
- /** see {@link SessionService} */
- public logout(): void {
- this.remoteSession.logout();
-
- this.loginState.setState(LoginState.LOGGED_OUT);
- this.connectionState.setState(ConnectionState.DISCONNECTED);
- }
-
- /**
- * Dummy implementation, will directly go to SyncState.COMPLETED
- * OnlineSession does not require any kind of synchronisation.
- */
- public async sync(): Promise {
- this.syncState.setState(SyncState.COMPLETED);
- }
-
- /**
- * Helper to get a User Entity from the Database without needing the EntityMapperService
- * @param userId Id of the User to be loaded
- */
- private async loadUser(userId: string): Promise {
- const user = new User("");
- const userData = await this.getDatabase().get("User:" + userId);
- this.entitySchemaService.loadDataIntoEntity(user, userData);
- return user;
- }
-}
diff --git a/src/app/core/session/session-service/remote-session.spec.ts b/src/app/core/session/session-service/remote-session.spec.ts
index d244076b5d..274375a547 100644
--- a/src/app/core/session/session-service/remote-session.spec.ts
+++ b/src/app/core/session/session-service/remote-session.spec.ts
@@ -1,14 +1,22 @@
import { TestBed } from "@angular/core/testing";
import { RemoteSession } from "./remote-session";
-import { HttpClient } from "@angular/common/http";
-import { ConnectionState } from "../session-states/connection-state.enum";
+import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { of, throwError } from "rxjs";
import { AppConfig } from "../../app-config/app-config";
import { SessionType } from "../session-type";
+import { LoggingService } from "../../logging/logging.service";
+import {
+ TEST_PASSWORD,
+ TEST_USER,
+ testSessionServiceImplementation,
+} from "./session.service.spec";
+import { DatabaseUser } from "./local-user";
+import { LoginState } from "../session-states/login-state.enum";
describe("RemoteSessionService", () => {
let service: RemoteSession;
let mockHttpClient: jasmine.SpyObj;
+ let dbUser: DatabaseUser;
beforeEach(() => {
AppConfig.settings = {
@@ -20,54 +28,72 @@ describe("RemoteSessionService", () => {
},
};
mockHttpClient = jasmine.createSpyObj(["post", "delete"]);
+ mockHttpClient.delete.and.returnValue(of());
+
TestBed.configureTestingModule({
providers: [
RemoteSession,
+ LoggingService,
{ provide: HttpClient, useValue: mockHttpClient },
],
});
+
+ // Remote session allows TEST_USER and TEST_PASSWORD as valid credentials
+ dbUser = { name: TEST_USER, roles: ["user_app"] };
service = TestBed.inject(RemoteSession);
+
+ mockHttpClient.post.and.callFake((url, body) => {
+ if (body.name === TEST_USER && body.password === TEST_PASSWORD) {
+ return of(dbUser as any);
+ } else {
+ return throwError(
+ new HttpErrorResponse({ status: service.UNAUTHORIZED_STATUS_CODE })
+ );
+ }
+ });
});
it("should be connected after successful login", async () => {
- expect(service.connectionState.getState()).toBe(
- ConnectionState.DISCONNECTED
- );
+ expect(service.getLoginState().getState()).toBe(LoginState.LOGGED_OUT);
- mockHttpClient.post.and.returnValue(of());
-
- await service.login("", "");
+ await service.login(TEST_USER, TEST_PASSWORD);
expect(mockHttpClient.post).toHaveBeenCalled();
- expect(service.connectionState.getState()).toBe(ConnectionState.CONNECTED);
+ expect(service.getLoginState().getState()).toBe(LoginState.LOGGED_IN);
});
- it("should be offline if login fails", async () => {
- mockHttpClient.post.and.returnValue(throwError(new Error()));
+ it("should be unavailable if requests fails with error other than 401", async () => {
+ mockHttpClient.post.and.returnValue(
+ throwError(new HttpErrorResponse({ status: 501 }))
+ );
- await service.login("", "");
+ await service.login(TEST_USER, TEST_PASSWORD);
- expect(service.connectionState.getState()).toBe(ConnectionState.OFFLINE);
+ expect(service.getLoginState().getState()).toBe(LoginState.UNAVAILABLE);
});
it("should be rejected if login is unauthorized", async () => {
- const unauthorizedError = new Error();
- unauthorizedError.name = "unauthorized";
- mockHttpClient.post.and.returnValue(throwError(unauthorizedError));
+ await service.login(TEST_USER, "wrongPassword");
- await service.login("", "");
-
- expect(service.connectionState.getState()).toBe(ConnectionState.REJECTED);
+ expect(service.getLoginState().getState()).toBe(LoginState.LOGIN_FAILED);
});
it("should disconnect after logout", async () => {
- service.connectionState.setState(ConnectionState.CONNECTED);
- mockHttpClient.delete.and.returnValue(of());
+ await service.login(TEST_USER, TEST_PASSWORD);
await service.logout();
- expect(service.connectionState.getState()).toBe(
- ConnectionState.DISCONNECTED
- );
+ expect(service.getLoginState().getState()).toBe(LoginState.LOGGED_OUT);
+ });
+
+ it("should assign the current user after successful login", async () => {
+ await service.login(TEST_USER, TEST_PASSWORD);
+
+ expect(service.getCurrentDBUser()).toEqual({
+ name: dbUser.name,
+ roles: dbUser.roles,
+ });
});
+
+ testSessionServiceImplementation(() => Promise.resolve(service));
});
diff --git a/src/app/core/session/session-service/remote-session.ts b/src/app/core/session/session-service/remote-session.ts
index 7f5cb65560..181b1562a4 100644
--- a/src/app/core/session/session-service/remote-session.ts
+++ b/src/app/core/session/session-service/remote-session.ts
@@ -19,71 +19,45 @@ import PouchDB from "pouchdb-browser";
import { AppConfig } from "../../app-config/app-config";
import { Injectable } from "@angular/core";
-import { StateHandler } from "../session-states/state-handler";
-import { ConnectionState } from "../session-states/connection-state.enum";
-import { HttpClient } from "@angular/common/http";
+import { HttpClient, HttpErrorResponse } from "@angular/common/http";
+import { DatabaseUser } from "./local-user";
+import { SessionService } from "./session.service";
+import { LoginState } from "../session-states/login-state.enum";
+import { Database } from "../../database/database";
+import { User } from "../../user/user";
+import { PouchDatabase } from "../../database/pouch-database";
+import { LoggingService } from "../../logging/logging.service";
/**
* Responsibilities:
* - Hold the remote DB
- * - Handle auth
+ * - Handle auth against CouchDB
* - provide "am i online"-info
*/
@Injectable()
-export class RemoteSession {
+export class RemoteSession extends SessionService {
+ // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
+ readonly UNAUTHORIZED_STATUS_CODE = 401;
/** remote (!) database PouchDB */
- public database: PouchDB.Database;
-
- /** state of the remote connection */
- public connectionState = new StateHandler(ConnectionState.DISCONNECTED);
+ public pouchDB: PouchDB.Database;
+ private readonly database: Database;
+ private currentDBUser: DatabaseUser;
/**
- * Create a RemoteSession and set up connection to the remote database CouchDB server configured in AppConfig.
+ * Create a RemoteSession and set up connection to the remote CouchDB server configured in AppConfig.
*/
- constructor(private httpClient: HttpClient) {
- const thisRemoteSession = this;
- this.database = new PouchDB(
+ constructor(
+ private httpClient: HttpClient,
+ private loggingService: LoggingService
+ ) {
+ super();
+ this.pouchDB = new PouchDB(
AppConfig.settings.database.remote_url + AppConfig.settings.database.name,
{
- ajax: {
- rejectUnauthorized: false,
- timeout: 60000,
- },
- // TODO remove connection state and this code
- fetch(url, opts) {
- const req = fetch(url, opts);
- req.then((result) => {
- if (
- thisRemoteSession.connectionState.getState() ===
- ConnectionState.OFFLINE
- ) {
- thisRemoteSession.connectionState.setState(
- ConnectionState.CONNECTED
- );
- }
- return result;
- });
- req.catch((error) => {
- // fetch will throw on network errors, giving us a chance to check the online status
- // if we are offline at the start, this will already be set on login, so we need not check that initial condition here
- // do not set offline on AbortErrors, as these are fine:
- // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Exceptions
- if (
- error.name !== "AbortError" &&
- thisRemoteSession.connectionState.getState() ===
- ConnectionState.CONNECTED
- ) {
- thisRemoteSession.connectionState.setState(
- ConnectionState.OFFLINE
- );
- }
- throw error;
- });
- return req;
- },
skip_setup: true,
- } as PouchDB.Configuration.RemoteDatabaseConfiguration
+ }
);
+ this.database = new PouchDatabase(this.pouchDB, this.loggingService);
}
/**
@@ -91,29 +65,33 @@ export class RemoteSession {
* @param username Username
* @param password Password
*/
- public async login(
- username: string,
- password: string
- ): Promise {
+ public async login(username: string, password: string): Promise {
try {
- await this.httpClient
+ const response = await this.httpClient
.post(
`${AppConfig.settings.database.remote_url}_session`,
{ name: username, password: password },
{ withCredentials: true }
)
.toPromise();
- this.connectionState.setState(ConnectionState.CONNECTED);
- return ConnectionState.CONNECTED;
+ this.assignDatabaseUser(response);
+ this.getLoginState().setState(LoginState.LOGGED_IN);
} catch (error) {
- if (error.name === "unauthorized" || error.name === "forbidden") {
- this.connectionState.setState(ConnectionState.REJECTED);
- return ConnectionState.REJECTED;
+ const httpError = error as HttpErrorResponse;
+ if (httpError?.status === this.UNAUTHORIZED_STATUS_CODE) {
+ this.getLoginState().setState(LoginState.LOGIN_FAILED);
} else {
- this.connectionState.setState(ConnectionState.OFFLINE);
- return ConnectionState.OFFLINE;
+ this.getLoginState().setState(LoginState.UNAVAILABLE);
}
}
+ return this.getLoginState().getState();
+ }
+
+ private assignDatabaseUser(couchDBResponse: any) {
+ this.currentDBUser = {
+ name: couchDBResponse.name,
+ roles: couchDBResponse.roles,
+ };
}
/**
@@ -125,6 +103,28 @@ export class RemoteSession {
withCredentials: true,
})
.toPromise();
- this.connectionState.setState(ConnectionState.DISCONNECTED);
+ this.currentDBUser = undefined;
+ this.getLoginState().setState(LoginState.LOGGED_OUT);
+ }
+
+ getCurrentDBUser(): DatabaseUser {
+ return this.currentDBUser;
+ }
+
+ checkPassword(username: string, password: string): boolean {
+ // Cannot be checked against CouchDB due to cookie-auth
+ throw Error("Can't check password in remote session");
+ }
+
+ getCurrentUser(): User {
+ throw Error("Can't get user entity in remote session");
+ }
+
+ getDatabase(): Database {
+ return this.database;
+ }
+
+ sync(): Promise {
+ return Promise.reject(new Error("Cannot sync remote session"));
}
}
diff --git a/src/app/core/session/session-service/session.service.spec.ts b/src/app/core/session/session-service/session.service.spec.ts
index 46a9bf1866..7485f36f94 100644
--- a/src/app/core/session/session-service/session.service.spec.ts
+++ b/src/app/core/session/session-service/session.service.spec.ts
@@ -16,14 +16,14 @@
*/
import { LoginState } from "../session-states/login-state.enum";
-import { SyncState } from "../session-states/sync-state.enum";
-import { ConnectionState } from "../session-states/connection-state.enum";
import { SessionService } from "./session.service";
-import { Database } from "../../database/database";
-import { User } from "../../user/user";
+import { SyncState } from "../session-states/sync-state.enum";
+export const TEST_USER = "test";
+export const TEST_PASSWORD = "pass";
/**
* Default tests for testing basic functionality of any SessionService implementation.
+ * The session has to be setup, so TEST_USER and TEST_PASSWORD are (the only) valid credentials
*
* @example
describe("TestSessionService", async () => {
@@ -38,21 +38,14 @@ export function testSessionServiceImplementation(
sessionSetupFunction: () => Promise
) {
let sessionService: SessionService;
- const TEST_USER = "test";
- const TEST_PASSWORD = "pass";
beforeEach(async () => {
sessionService = await sessionSetupFunction();
- await saveUser(sessionService.getDatabase(), TEST_USER, TEST_PASSWORD);
- });
-
- afterEach(async () => {
- await sessionService.getDatabase().destroy();
});
it("has the correct initial state", () => {
- expectNotToBeLoggedIn(sessionService, LoginState.LOGGED_OUT);
- expect(sessionService.getDatabase()).toBeInstanceOf(Database);
+ expect(sessionService.getSyncState().getState()).toBe(SyncState.UNSYNCED);
+ expectNotToBeLoggedIn(LoginState.LOGGED_OUT);
});
it("succeeds login", async () => {
@@ -63,31 +56,26 @@ export function testSessionServiceImplementation(
expect(sessionService.getLoginState().getState())
.withContext("unexpected LoginState")
.toEqual(LoginState.LOGGED_IN);
- expect(sessionService.getSyncState().getState())
- .withContext("unexpected SyncState")
- .toEqual(SyncState.UNSYNCED);
- expect(sessionService.getConnectionState().getState())
- .withContext("unexpected ConnectionState")
- .toEqual(ConnectionState.OFFLINE);
expect(sessionService.isLoggedIn())
.withContext("unexpected isLoggedIn")
.toBeTrue();
- expect(sessionService.getCurrentUser()?.name).toBe(TEST_USER);
+ expect(sessionService.getCurrentDBUser().name).toBe(TEST_USER);
});
it("fails login with wrong password", async () => {
const loginResult = await sessionService.login(TEST_USER, "");
expect(loginResult).toEqual(LoginState.LOGIN_FAILED);
- expectNotToBeLoggedIn(sessionService, LoginState.LOGIN_FAILED);
+ expectNotToBeLoggedIn(LoginState.LOGIN_FAILED);
});
it("fails login with wrong/non-existing username", async () => {
const loginResult = await sessionService.login("other", TEST_PASSWORD);
- expect(loginResult).toEqual(LoginState.LOGIN_FAILED);
- expectNotToBeLoggedIn(sessionService, LoginState.LOGIN_FAILED);
+ // The LocalSession returns LoginState.UNAVAILABLE for unknown users because they might be available remote
+ const failedStates = [LoginState.LOGIN_FAILED, LoginState.UNAVAILABLE];
+ expect(failedStates).toContain(loginResult);
});
it("logs out and resets states", async () => {
@@ -95,50 +83,26 @@ export function testSessionServiceImplementation(
expect(loginResult).toEqual(LoginState.LOGGED_IN);
await sessionService.logout();
- expectNotToBeLoggedIn(sessionService, LoginState.LOGGED_OUT);
+ expectNotToBeLoggedIn(LoginState.LOGGED_OUT);
});
-}
-/**
- * Destroy and rebuild the PouchDB database of the given name
- * and create a User entity with the given credentials.
- *
- * @param database The database
- * @param testUsername Username of the entity to be set up after resetting the database
- * @param testPassword Password of the new user entity
- */
-async function saveUser(
- database: Database,
- testUsername: string,
- testPassword: string
-) {
- const testUser = new User(testUsername);
- testUser.name = testUsername;
- testUser.setNewPassword(testPassword);
- await database.put(testUser);
-}
+ /**
+ * Check all states of the session to be "logged out".
+ * @param expectedLoginState The expected LoginState (failed or simply logged out)
+ */
+ function expectNotToBeLoggedIn(
+ expectedLoginState:
+ | LoginState.LOGGED_OUT
+ | LoginState.LOGIN_FAILED
+ | LoginState.UNAVAILABLE
+ ) {
+ expect(sessionService.getLoginState().getState())
+ .withContext("unexpected LoginState")
+ .toEqual(expectedLoginState);
-/**
- * Check all states of the session to be "logged out".
- * @param session The SessionService whose state should be checked
- * @param expectedLoginState The expected LoginState (failed or simply logged out)
- */
-function expectNotToBeLoggedIn(
- session: SessionService,
- expectedLoginState: LoginState.LOGGED_OUT | LoginState.LOGIN_FAILED
-) {
- expect(session.getLoginState().getState())
- .withContext("unexpected LoginState")
- .toEqual(expectedLoginState);
- expect(session.getSyncState().getState())
- .withContext("unexpected SyncState")
- .toEqual(SyncState.UNSYNCED);
- expect(session.getConnectionState().getState())
- .withContext("unexpected ConnectionState")
- .toEqual(ConnectionState.DISCONNECTED);
-
- expect(session.isLoggedIn())
- .withContext("unexpected isLoggedIn")
- .toEqual(false);
- expect(session.getCurrentUser()).not.toBeDefined();
+ expect(sessionService.isLoggedIn())
+ .withContext("unexpected isLoggedIn")
+ .toEqual(false);
+ expect(sessionService.getCurrentDBUser()).not.toBeDefined();
+ }
}
diff --git a/src/app/core/session/session-service/session.service.ts b/src/app/core/session/session-service/session.service.ts
index 48563071f2..759994118e 100644
--- a/src/app/core/session/session-service/session.service.ts
+++ b/src/app/core/session/session-service/session.service.ts
@@ -17,10 +17,10 @@
import { LoginState } from "../session-states/login-state.enum";
import { Database } from "../../database/database";
-import { ConnectionState } from "../session-states/connection-state.enum";
import { SyncState } from "../session-states/sync-state.enum";
import { User } from "../../user/user";
import { StateHandler } from "../session-states/state-handler";
+import { DatabaseUser } from "./local-user";
/**
* A session manages user authentication and database connection for the app.
@@ -36,6 +36,11 @@ import { StateHandler } from "../session-states/state-handler";
* Providers are set up in a way that you will get the correct implementation during runtime.
*/
export abstract class SessionService {
+ /** StateHandler for login state changes */
+ private loginState = new StateHandler(LoginState.LOGGED_OUT);
+ /** StateHandler for sync state changes */
+ private syncState = new StateHandler(SyncState.UNSYNCED);
+
/**
* Authenticate a user.
* @param username
@@ -50,28 +55,43 @@ export abstract class SessionService {
/**
* Get the currently logged in user (or undefined).
+ * @deprecated use getCurrentDBUser instead
*/
abstract getCurrentUser(): User;
/**
- * Get the session status - whether a user is authenticated currently.
+ * Get the current user according to the new format
*/
- abstract isLoggedIn(): boolean;
+ abstract getCurrentDBUser(): DatabaseUser;
/**
- * Get the state of the session.
+ * Check a password if its valid
+ * @param username the username for which the password should be checked
+ * @param password the password to be checked
+ * @returns boolean true if the password is correct, false otherwise
+ */
+ abstract checkPassword(username: string, password: string): boolean;
+
+ /**
+ * Get the session status - whether a user is authenticated currently.
*/
- abstract getLoginState(): StateHandler;
+ public isLoggedIn(): boolean {
+ return this.getLoginState().getState() === LoginState.LOGGED_IN;
+ }
/**
- * Get the state of the connection to the remote server.
+ * Get the state of the session.
*/
- abstract getConnectionState(): StateHandler;
+ public getLoginState(): StateHandler {
+ return this.loginState;
+ }
/**
* Get the state of the synchronization with the remote server.
*/
- abstract getSyncState(): StateHandler;
+ public getSyncState(): StateHandler {
+ return this.syncState;
+ }
/**
* Start a synchronization process.
diff --git a/src/app/core/session/session-service/synced-session.service.spec.ts b/src/app/core/session/session-service/synced-session.service.spec.ts
index 88f5ef8cc1..9899d078ce 100644
--- a/src/app/core/session/session-service/synced-session.service.spec.ts
+++ b/src/app/core/session/session-service/synced-session.service.spec.ts
@@ -18,353 +18,290 @@
import { SyncedSessionService } from "./synced-session.service";
import { AlertService } from "../../alerts/alert.service";
import { LoginState } from "../session-states/login-state.enum";
-import { SyncState } from "../session-states/sync-state.enum";
-import { ConnectionState } from "../session-states/connection-state.enum";
import { AppConfig } from "../../app-config/app-config";
import { LocalSession } from "./local-session";
import { RemoteSession } from "./remote-session";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
import { SessionType } from "../session-type";
-import { fakeAsync, tick } from "@angular/core/testing";
+import { fakeAsync, flush, TestBed, tick } from "@angular/core/testing";
+import { User } from "../../user/user";
+import { HttpClient, HttpErrorResponse } from "@angular/common/http";
+import { LoggingService } from "../../logging/logging.service";
+import { of, throwError } from "rxjs";
+import { MatSnackBarModule } from "@angular/material/snack-bar";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { DatabaseUser } from "./local-user";
+import {
+ TEST_PASSWORD,
+ TEST_USER,
+ testSessionServiceImplementation,
+} from "./session.service.spec";
describe("SyncedSessionService", () => {
- const snackBarMock = { openFromComponent: () => {} } as any;
- const alertService = new AlertService(snackBarMock);
- const entitySchemaService = new EntitySchemaService();
let sessionService: SyncedSessionService;
+ let localSession: LocalSession;
+ let remoteSession: RemoteSession;
+ let localLoginSpy: jasmine.Spy<
+ (username: string, password: string) => Promise
+ >;
+ let remoteLoginSpy: jasmine.Spy<
+ (username: string, password: string) => Promise
+ >;
+ let dbUser: DatabaseUser;
+ let syncSpy: jasmine.Spy<() => Promise>;
+ let liveSyncSpy: jasmine.Spy<() => void>;
+ let loadUserSpy: jasmine.Spy<(userId: string) => void>;
+ let mockHttpClient: jasmine.SpyObj;
- xdescribe("Integration Tests", () => {
- let originalTimeout;
-
- beforeEach(function () {
- originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
- jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
+ beforeEach(() => {
+ mockHttpClient = jasmine.createSpyObj(["post", "delete"]);
+ mockHttpClient.delete.and.returnValue(of());
+ TestBed.configureTestingModule({
+ imports: [MatSnackBarModule, NoopAnimationsModule],
+ providers: [
+ EntitySchemaService,
+ AlertService,
+ LoggingService,
+ SyncedSessionService,
+ { provide: HttpClient, useValue: mockHttpClient },
+ ],
});
+ AppConfig.settings = {
+ site_name: "Aam Digital - DEV",
+ session_type: SessionType.mock,
+ database: {
+ name: "integration_tests",
+ remote_url: "https://demo.aam-digital.com/db/",
+ },
+ webdav: { remote_url: "" },
+ };
+ sessionService = TestBed.inject(SyncedSessionService);
- afterEach(function () {
- jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
- });
+ // make private members localSession and remoteSession available in the tests
+ // @ts-ignore
+ localSession = sessionService._localSession;
+ // @ts-ignore
+ remoteSession = sessionService._remoteSession;
- beforeEach(() => {
- AppConfig.settings = {
- site_name: "Aam Digital - DEV",
- session_type: SessionType.synced,
- database: {
- name: "integration_tests",
- remote_url: "https://demo.aam-digital.com/db/",
- },
- };
- sessionService = new SyncedSessionService(
- alertService,
- jasmine.createSpyObj(["error", "warn"]),
- entitySchemaService,
- null
- );
+ // Setting up local and remote session to accept TEST_USER and TEST_PASSWORD as valid credentials
+ dbUser = { name: TEST_USER, roles: ["user_app"] };
+ localSession.saveUser({ name: TEST_USER, roles: [] }, TEST_PASSWORD);
+ mockHttpClient.post.and.callFake((url, body) => {
+ if (body.name === TEST_USER && body.password === TEST_PASSWORD) {
+ return of(dbUser as any);
+ } else {
+ return throwError(
+ new HttpErrorResponse({
+ status: remoteSession.UNAUTHORIZED_STATUS_CODE,
+ })
+ );
+ }
});
- it("has the correct Initial State", () => {
- expect(sessionService.getLoginState().getState()).toEqual(
- LoginState.LOGGED_OUT
- );
- expect(sessionService.getSyncState().getState()).toEqual(
- SyncState.UNSYNCED
- );
- expect(sessionService.getConnectionState().getState()).toEqual(
- ConnectionState.DISCONNECTED
- );
-
- expect(sessionService.isLoggedIn()).toEqual(false);
- expect(sessionService.getCurrentUser()).not.toBeDefined();
- });
+ localLoginSpy = spyOn(localSession, "login").and.callThrough();
+ remoteLoginSpy = spyOn(remoteSession, "login").and.callThrough();
+ syncSpy = spyOn(sessionService, "sync").and.resolveTo();
+ liveSyncSpy = spyOn(sessionService, "liveSyncDeferred");
- it("has the correct state after Login with wrong credentials", async () => {
- const loginState = await sessionService.login("demo", "pass123");
- expect(loginState).toEqual(LoginState.LOGIN_FAILED);
- expect(sessionService.getLoginState().getState()).toEqual(
- LoginState.LOGIN_FAILED
- );
- expect(sessionService.getSyncState().getState()).toEqual(
- SyncState.UNSYNCED
- );
-
- // remote session takes a bit longer than a local login - this throws on successful connection
- await sessionService
- .getConnectionState()
- .waitForChangeTo(ConnectionState.REJECTED, [ConnectionState.CONNECTED]);
-
- expect(sessionService.isLoggedIn()).toEqual(false);
- expect(sessionService.getCurrentUser()).not.toBeDefined();
- });
+ // TODO remove this once User Entity is not needed in session any more
+ loadUserSpy = spyOn(localSession, "loadUser").and.resolveTo();
+ });
- it("has the correct state after Login with non-existing user", async () => {
- const loginState = await sessionService.login("demo123", "pass123");
- expect(loginState).toEqual(LoginState.LOGIN_FAILED);
- expect(sessionService.getLoginState().getState()).toEqual(
- LoginState.LOGIN_FAILED
- );
- expect(sessionService.getSyncState().getState()).toEqual(
- SyncState.UNSYNCED
- );
-
- // remote session takes a bit longer than a local login - this throws on successful connection
- await sessionService
- .getConnectionState()
- .waitForChangeTo(ConnectionState.REJECTED, [ConnectionState.CONNECTED]);
-
- expect(sessionService.isLoggedIn()).toEqual(false);
- expect(sessionService.getCurrentUser()).not.toBeDefined();
- });
+ afterEach(() => {
+ localSession.removeUser(TEST_USER);
+ });
- it("has the correct state after Login with correct credentials", async () => {
- const [loginState] = await Promise.all([
- sessionService.login("demo", "pass"),
- sessionService
- .getSyncState()
- .waitForChangeTo(SyncState.COMPLETED, [SyncState.FAILED]),
- ]);
- expect(loginState).toEqual(LoginState.LOGGED_IN);
- expect(sessionService.getLoginState().getState()).toEqual(
- LoginState.LOGGED_IN
- );
- expect(sessionService.getSyncState().getState()).toEqual(
- SyncState.COMPLETED
- );
- expect(sessionService.getConnectionState().getState()).toEqual(
- ConnectionState.CONNECTED
- );
-
- expect(sessionService.isLoggedIn()).toEqual(true);
- expect(sessionService.getCurrentUser()).toBeDefined();
- });
+ it("Remote and local fail (normal login with wrong password)", fakeAsync(() => {
+ const result = sessionService.login(TEST_USER, "wrongPassword");
+ tick();
- it("has the correct state after Logout", async () => {
- await Promise.all([
- sessionService.login("demo", "pass"),
- sessionService
- .getSyncState()
- .waitForChangeTo(SyncState.COMPLETED, [SyncState.FAILED]),
- ]);
-
- sessionService.logout();
- expect(sessionService.getLoginState().getState()).toEqual(
- LoginState.LOGGED_OUT
- );
- expect(sessionService.getConnectionState().getState()).toEqual(
- ConnectionState.DISCONNECTED
- );
-
- expect(sessionService.isLoggedIn()).toEqual(false);
- expect(sessionService.getCurrentUser()).not.toBeDefined();
- });
- });
+ expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, "wrongPassword");
+ expect(remoteLoginSpy).toHaveBeenCalledWith(TEST_USER, "wrongPassword");
+ expect(syncSpy).not.toHaveBeenCalled();
+ expectAsync(result).toBeResolvedTo(LoginState.LOGIN_FAILED);
+ flush();
+ }));
- // These tests mock the login-methods of local and remote session.
- // We cannot test whether the StateHandlers are in correct state, as these are set in the sub-classes themselves.
- describe("Mocked Tests", () => {
- let localSession: LocalSession;
- let remoteSession: RemoteSession;
-
- beforeEach(() => {
- AppConfig.settings = {
- site_name: "Aam Digital - DEV",
- session_type: SessionType.mock,
- database: {
- name: "integration_tests",
- remote_url: "https://demo.aam-digital.com/db/",
- },
- webdav: { remote_url: "" },
- };
- // setup synced session service
- sessionService = new SyncedSessionService(
- alertService,
- jasmine.createSpyObj(["error", "warn"]),
- entitySchemaService,
- null
- );
- // make private members localSession and remoteSession available in the tests
- localSession = sessionService["_localSession"];
- remoteSession = sessionService["_remoteSession"];
- });
+ it("Remote unavailable, local succeeds (offline)", fakeAsync(() => {
+ failRemoteLogin(true);
- it("behaves correctly when both local and remote session succeed (normal login)", (done) => {
- const localLogin = spyOn(localSession, "login").and.returnValue(
- Promise.resolve(LoginState.LOGGED_IN)
- );
- const remoteLogin = spyOn(remoteSession, "login").and.returnValue(
- Promise.resolve(ConnectionState.CONNECTED)
- );
- const syncSpy = spyOn(sessionService, "sync").and.returnValue(
- Promise.resolve()
- );
- const liveSyncSpy = spyOn(sessionService, "liveSyncDeferred");
- const result = sessionService.login("u", "p");
- setTimeout(async () => {
- // wait for the next event cycle loop --> all Promise handlers are evaluated before this
- // login methods should have been called, the local one twice
- expect(localLogin.calls.allArgs()).toEqual([["u", "p"]]);
- expect(remoteLogin.calls.allArgs()).toEqual([["u", "p"]]);
- // sync should have been triggered
- expect(syncSpy.calls.count()).toEqual(1);
- expect(liveSyncSpy.calls.count()).toEqual(1);
- // result should be correct
- expect(await result).toEqual(LoginState.LOGGED_IN);
- done();
- });
- });
+ const result = sessionService.login(TEST_USER, TEST_PASSWORD);
+ tick();
- it("behaves correctly when both local and remote session reject (normal login with wrong password)", (done) => {
- const localLogin = spyOn(localSession, "login").and.returnValue(
- Promise.resolve(LoginState.LOGIN_FAILED)
- );
- const remoteLogin = spyOn(remoteSession, "login").and.returnValue(
- Promise.resolve(ConnectionState.REJECTED)
- );
- const syncSpy = spyOn(sessionService, "sync").and.returnValue(
- Promise.resolve()
- );
- const result = sessionService.login("u", "p");
- setTimeout(async () => {
- // wait for the next event cycle loop --> all Promise handlers are evaluated before this
- // login methods should have been called, the local one twice
- expect(localLogin.calls.allArgs()).toEqual([["u", "p"]]);
- expect(remoteLogin.calls.allArgs()).toEqual([["u", "p"]]);
- // sync should have been triggered
- expect(syncSpy.calls.count()).toEqual(0);
- // result should be correct
- expect(await result).toEqual(LoginState.LOGIN_FAILED);
- done();
- });
- });
+ expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
+ expect(remoteLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
+ expect(syncSpy).not.toHaveBeenCalled();
+ expectAsync(result).toBeResolvedTo(LoginState.LOGGED_IN);
- it("behaves correctly in the offline scenario", (done) => {
- const localLogin = spyOn(localSession, "login").and.returnValue(
- Promise.resolve(LoginState.LOGGED_IN)
- );
- const remoteLogin = spyOn(remoteSession, "login").and.returnValue(
- Promise.resolve(ConnectionState.OFFLINE)
- );
- const syncSpy = spyOn(sessionService, "sync").and.returnValue(
- Promise.resolve()
- );
- const result = sessionService.login("u", "p");
- setTimeout(async () => {
- // wait for the next event cycle loop --> all Promise handlers are evaluated before this
- // login methods should have been called, the local one twice
- expect(localLogin.calls.allArgs()).toEqual([["u", "p"]]);
- expect(remoteLogin.calls.allArgs()).toEqual([["u", "p"]]);
- // sync should have been triggered
- expect(syncSpy.calls.count()).toEqual(0);
- // result should be correct
- expect(await result).toEqual(LoginState.LOGGED_IN);
- done();
- });
- });
+ sessionService.cancelLoginOfflineRetry();
+ flush();
+ }));
- it("behaves correctly when the local session rejects, but the remote session succeeds (password change, new password)", (done) => {
- const localLogin = spyOn(localSession, "login").and.returnValues(
- Promise.resolve(LoginState.LOGIN_FAILED),
- Promise.resolve(LoginState.LOGGED_IN)
- );
- const remoteLogin = spyOn(remoteSession, "login").and.returnValue(
- Promise.resolve(ConnectionState.CONNECTED)
- );
- const syncSpy = spyOn(sessionService, "sync").and.returnValue(
- Promise.resolve()
- );
- const liveSyncSpy = spyOn(sessionService, "liveSyncDeferred");
- const result = sessionService.login("u", "p");
- setTimeout(async () => {
- // wait for the next event cycle loop --> all Promise handlers are evaluated before this
- // login methods should have been called, the local one twice
- expect(localLogin.calls.allArgs()).toEqual([
- ["u", "p"],
- ["u", "p"],
- ]);
- expect(remoteLogin.calls.allArgs()).toEqual([["u", "p"]]);
- // sync should have been triggered
- expect(syncSpy.calls.count()).toEqual(1);
- expect(liveSyncSpy.calls.count()).toEqual(1);
- // result should be correct: initially the local login failed, so sessionService.login must return loginFailed
- expect(await result).toEqual(LoginState.LOGIN_FAILED);
- done();
- });
- });
+ it("Remote unavailable, local fails (offline, wrong password)", fakeAsync(() => {
+ failRemoteLogin(true);
- it("behaves correctly when the local session logs in, but the remote session rejects (password change, old password", (done) => {
- const localLogin = spyOn(localSession, "login").and.returnValue(
- Promise.resolve(LoginState.LOGGED_IN)
- );
- const localLogout = spyOn(localSession, "logout");
- const remoteLogin = spyOn(remoteSession, "login").and.returnValue(
- Promise.resolve(ConnectionState.REJECTED)
- );
- const syncSpy = spyOn(sessionService, "sync").and.returnValue(
- Promise.resolve()
- );
- const result = sessionService.login("u", "p");
- setTimeout(async () => {
- // wait for the next event cycle loop --> all Promise handlers are evaluated before this
- // login methods should have been called
- expect(localLogin.calls.allArgs()).toEqual([["u", "p"]]);
- expect(remoteLogin.calls.allArgs()).toEqual([["u", "p"]]);
- // sync should not have been triggered
- expect(syncSpy.calls.count()).toEqual(0);
- // logout should have been called
- expect(localLogout.calls.count()).toEqual(1);
- // result should be correct: initially the local login succeeded, so sessionService.login must return loggedIn
- expect(await result).toEqual(LoginState.LOGGED_IN);
- done();
- });
- });
+ const result = sessionService.login(TEST_USER, "wrongPassword");
+ tick();
- it("behaves correctly when the sync fails and the local login succeeds", (done) => {
- const localLogin = spyOn(localSession, "login").and.returnValue(
- Promise.resolve(LoginState.LOGGED_IN)
- );
- const remoteLogin = spyOn(remoteSession, "login").and.returnValue(
- Promise.resolve(ConnectionState.CONNECTED)
- );
- const syncSpy = spyOn(sessionService, "sync").and.returnValue(
- Promise.reject()
- );
- const liveSyncSpy = spyOn(sessionService, "liveSyncDeferred");
- const result = sessionService.login("u", "p");
- setTimeout(async () => {
- // wait for the next event cycle loop --> all Promise handlers are evaluated before this
- // login methods should have been called, the local one twice
- expect(localLogin.calls.allArgs()).toEqual([["u", "p"]]);
- expect(remoteLogin.calls.allArgs()).toEqual([["u", "p"]]);
- // sync should have been triggered
- expect(syncSpy.calls.count()).toEqual(1);
- expect(liveSyncSpy.calls.count()).toEqual(1);
- // result should be correct
- expect(await result).toEqual(LoginState.LOGGED_IN);
- done();
- });
- });
+ expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, "wrongPassword");
+ expect(remoteLoginSpy).toHaveBeenCalledWith(TEST_USER, "wrongPassword");
+ expect(syncSpy).not.toHaveBeenCalled();
+ expectAsync(result).toBeResolvedTo(LoginState.LOGIN_FAILED);
+ tick();
+ }));
- it("behaves correctly when the sync fails and the local login fails", fakeAsync(() => {
- const localLogin = spyOn(localSession, "login").and.resolveTo(
- LoginState.LOGIN_FAILED
- );
- const remoteLogin = spyOn(remoteSession, "login").and.resolveTo(
- ConnectionState.CONNECTED
- );
- const syncSpy = spyOn(sessionService, "sync").and.rejectWith();
- const liveSyncSpy = spyOn(sessionService, "liveSyncDeferred");
- const result = sessionService.login("u", "p");
- tick();
- // login methods should have been called, the local one twice
- expect(localLogin.calls.allArgs()).toEqual([
- ["u", "p"],
- ["u", "p"],
- ]);
- expect(remoteLogin.calls.allArgs()).toEqual([["u", "p"]]);
- // sync should have been triggered
- expect(syncSpy.calls.count()).toEqual(1);
- expect(liveSyncSpy.calls.count()).toEqual(0);
- // result should be correct
- return expectAsync(result).toBeResolvedTo(LoginState.LOGIN_FAILED);
- }));
- });
+ it("Remote succeeds, local fails (password changed and new password entered/new user)", fakeAsync(() => {
+ const newUser = { name: "newUser", roles: ["user_app"] };
+ passRemoteLogin(newUser);
+ spyOn(localSession, "saveUser").and.callThrough();
+
+ const result = sessionService.login(newUser.name, "p");
+ tick();
+
+ expect(localLoginSpy.calls.allArgs()).toEqual([
+ [newUser.name, "p"],
+ [newUser.name, "p"],
+ ]);
+ expect(remoteLoginSpy.calls.allArgs()).toEqual([[newUser.name, "p"]]);
+ expect(syncSpy).toHaveBeenCalledTimes(1);
+ expect(liveSyncSpy).toHaveBeenCalledTimes(1);
+ expectAsync(result).toBeResolvedTo(LoginState.LOGGED_IN);
+ expect(localSession.saveUser).toHaveBeenCalledWith(
+ {
+ name: newUser.name,
+ roles: newUser.roles,
+ },
+ "p"
+ );
+ expect(sessionService.getCurrentDBUser().name).toBe("newUser");
+ expect(sessionService.getCurrentDBUser().roles).toEqual(["user_app"]);
+ tick();
+ localSession.removeUser(newUser.name);
+ }));
+
+ it("Remote fails, local succeeds (Password changes, old password entered)", fakeAsync(() => {
+ failRemoteLogin();
+ spyOn(localSession, "removeUser").and.callThrough();
+
+ const result = sessionService.login(TEST_USER, TEST_PASSWORD);
+ tick();
+
+ // The local user is removed to prohibit further offline login
+ expect(localSession.removeUser).toHaveBeenCalledWith(TEST_USER);
+ // Initially the user is logged in
+ expectAsync(result).toBeResolvedTo(LoginState.LOGGED_IN);
+ // After remote session fails the user is logged out again
+ expect(sessionService.getLoginState().getState()).toBe(
+ LoginState.LOGIN_FAILED
+ );
+ flush();
+ }));
+
+ it("Remote and local succeed, sync fails", fakeAsync(() => {
+ syncSpy.and.rejectWith();
+
+ const login = sessionService.login(TEST_USER, TEST_PASSWORD);
+ tick();
+
+ expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
+ expect(remoteLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
+ expect(syncSpy).toHaveBeenCalled();
+ expect(liveSyncSpy).toHaveBeenCalled();
+ expectAsync(login).toBeResolvedTo(LoginState.LOGGED_IN);
+ flush();
+ }));
+
+ it("Remote succeeds, local fails, sync fails", fakeAsync(() => {
+ passRemoteLogin();
+ syncSpy.and.rejectWith();
+
+ const result = sessionService.login(TEST_USER, "anotherPassword");
+ tick();
+
+ expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, "anotherPassword");
+ expect(localLoginSpy).toHaveBeenCalledTimes(2);
+ expect(remoteLoginSpy).toHaveBeenCalledWith(TEST_USER, "anotherPassword");
+ expect(remoteLoginSpy).toHaveBeenCalledTimes(1);
+ expect(syncSpy).toHaveBeenCalled();
+ expect(liveSyncSpy).not.toHaveBeenCalled();
+ expectAsync(result).toBeResolvedTo(LoginState.LOGIN_FAILED);
+ tick();
+ }));
+
+ it("remote and local unavailable", fakeAsync(() => {
+ failRemoteLogin(true);
+
+ const result = sessionService.login("anotherUser", "anotherPassword");
+ tick();
+
+ expect(localLoginSpy).toHaveBeenCalledWith(
+ "anotherUser",
+ "anotherPassword"
+ );
+ expect(remoteLoginSpy).toHaveBeenCalledWith(
+ "anotherUser",
+ "anotherPassword"
+ );
+ expect(syncSpy).not.toHaveBeenCalled();
+ expectAsync(result).toBeResolvedTo(LoginState.UNAVAILABLE);
+
+ flush();
+ }));
+
+ it("should load the user entity after successful local login", fakeAsync(() => {
+ const testUser = new User(TEST_USER);
+ testUser.name = TEST_USER;
+ const database = sessionService.getDatabase();
+ loadUserSpy.and.callThrough();
+ spyOn(database, "get").and.resolveTo(
+ TestBed.inject(EntitySchemaService).transformEntityToDatabaseFormat(
+ testUser
+ )
+ );
+
+ sessionService.login(TEST_USER, TEST_PASSWORD);
+ tick();
+
+ expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
+ expect(database.get).toHaveBeenCalledWith(testUser._id);
+ expect(sessionService.getCurrentUser()).toEqual(testUser);
+ }));
+
+ it("should update the local user object once connected", fakeAsync(() => {
+ const updatedUser: DatabaseUser = {
+ name: TEST_USER,
+ roles: dbUser.roles.concat("admin"),
+ };
+ passRemoteLogin(updatedUser);
+
+ const result = sessionService.login(TEST_USER, TEST_PASSWORD);
+ tick();
+
+ expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
+ expect(remoteLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
+ expect(syncSpy).toHaveBeenCalledTimes(1);
+ expect(liveSyncSpy).toHaveBeenCalledTimes(1);
+
+ const currentUser = localSession.getCurrentDBUser();
+ expect(currentUser.name).toEqual(TEST_USER);
+ expect(currentUser.roles).toEqual(["user_app", "admin"]);
+ expectAsync(result).toBeResolvedTo(LoginState.LOGGED_IN);
+ tick();
+ }));
+
+ testSessionServiceImplementation(() => Promise.resolve(sessionService));
+
+ function passRemoteLogin(response: DatabaseUser = { name: "", roles: [] }) {
+ mockHttpClient.post.and.returnValue(of(response));
+ }
+
+ function failRemoteLogin(offline = false) {
+ let rejectError;
+ if (!offline) {
+ rejectError = new HttpErrorResponse({
+ status: remoteSession.UNAUTHORIZED_STATUS_CODE,
+ });
+ }
+ mockHttpClient.post.and.returnValue(throwError(rejectError));
+ }
});
diff --git a/src/app/core/session/session-service/synced-session.service.ts b/src/app/core/session/session-service/synced-session.service.ts
index 2e595919e3..5444fe744c 100644
--- a/src/app/core/session/session-service/synced-session.service.ts
+++ b/src/app/core/session/session-service/synced-session.service.ts
@@ -24,12 +24,14 @@ import { RemoteSession } from "./remote-session";
import { LoginState } from "../session-states/login-state.enum";
import { Database } from "../../database/database";
import { PouchDatabase } from "../../database/pouch-database";
-import { ConnectionState } from "../session-states/connection-state.enum";
import { SyncState } from "../session-states/sync-state.enum";
import { User } from "../../user/user";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
import { LoggingService } from "../../logging/logging.service";
import { HttpClient } from "@angular/common/http";
+import PouchDB from "pouchdb-browser";
+import { AppConfig } from "../../app-config/app-config";
+import { DatabaseUser } from "./local-user";
/**
* A synced session creates and manages a LocalSession and a RemoteSession
@@ -40,8 +42,13 @@ import { HttpClient } from "@angular/common/http";
*/
@Injectable()
export class SyncedSessionService extends SessionService {
- private _localSession: LocalSession;
- private _remoteSession: RemoteSession;
+ private readonly LOGIN_RETRY_TIMEOUT = 60000;
+ private readonly POUCHDB_SYNC_BATCH_SIZE = 500;
+
+ private readonly _localSession: LocalSession;
+ private readonly _remoteSession: RemoteSession;
+ private readonly pouchDB: PouchDB.Database;
+ private readonly database: Database;
private _liveSyncHandle: any;
private _liveSyncScheduledHandle: any;
private _offlineRetryLoginScheduleHandle: any;
@@ -53,13 +60,13 @@ export class SyncedSessionService extends SessionService {
private _httpClient: HttpClient
) {
super();
- this._localSession = new LocalSession(this._entitySchemaService);
- this._remoteSession = new RemoteSession(this._httpClient);
- }
-
- /** see {@link SessionService} */
- public isLoggedIn(): boolean {
- return this._localSession.loginState.getState() === LoginState.LOGGED_IN;
+ this.pouchDB = new PouchDB(AppConfig.settings.database.name);
+ this.database = new PouchDatabase(this.pouchDB, this._loggingService);
+ this._localSession = new LocalSession(
+ this.database,
+ this._entitySchemaService
+ );
+ this._remoteSession = new RemoteSession(this._httpClient, _loggingService);
}
/**
@@ -75,124 +82,108 @@ export class SyncedSessionService extends SessionService {
* @param password Password
* @returns a promise resolving with the local LoginState
*/
- public login(username: string, password: string): Promise {
+ public async login(username: string, password: string): Promise {
this.cancelLoginOfflineRetry(); // in case this is running in the background
this.getSyncState().setState(SyncState.UNSYNCED);
- const localLogin = this._localSession.login(username, password);
const remoteLogin = this._remoteSession.login(username, password);
+ const syncPromise = this._remoteSession
+ .getLoginState()
+ .waitForChangeTo(LoginState.LOGGED_IN)
+ .then(() => this.updateLocalUserAndStartSync(password));
- remoteLogin
- .then(async (connectionState: ConnectionState) => {
- // remote connected -- sync!
- if (connectionState === ConnectionState.CONNECTED) {
- const syncPromise = this.sync(); // no liveSync() here, as we can't know when that's finished if there are no changes.
+ const localLoginState = await this._localSession.login(username, password);
- // no matter the result of the non-live sync(), start liveSync() once it is done
- syncPromise
- .then(
- // successful -> start liveSync()
- () => this.liveSyncDeferred(),
- // not successful -> only start a liveSync() to retry, if we are logged in locally
- // otherwise the UI is in a fairly unusable state.
- async () => {
- if ((await localLogin) === LoginState.LOGGED_IN) {
- this.liveSyncDeferred();
- } else {
- // TODO(lh): Alert the AlertService: Your password was changed recently, but there is an issue with sync. Try again later!
- }
- }
- )
- .catch((err) => this._loggingService.error(err));
-
- // asynchronously check if the local login failed --> this happens, when the password was changed at the remote
- localLogin.then(async (loginState: LoginState) => {
- if (loginState === LoginState.LOGIN_FAILED) {
- // in this case: when the sync is completed, retry the local login after the sync
- try {
- await syncPromise;
- } catch (err) {
- this._loggingService.error(err);
- }
- return this._localSession.login(username, password);
- }
- });
-
- return syncPromise;
+ if (localLoginState === LoginState.LOGGED_IN) {
+ remoteLogin.then((loginState) => {
+ if (loginState === LoginState.LOGIN_FAILED) {
+ this.handleRemotePasswordChange(username);
+ }
+ if (loginState === LoginState.UNAVAILABLE) {
+ this.retryLoginWhileOffline(username, password);
}
+ });
+ } else {
+ const remoteLoginState = await remoteLogin;
+ if (remoteLoginState === LoginState.LOGGED_IN) {
+ // New user or password changed
+ await syncPromise;
+ await this._localSession.login(username, password);
+ } else if (
+ remoteLoginState === LoginState.UNAVAILABLE &&
+ localLoginState === LoginState.UNAVAILABLE
+ ) {
+ // Offline with no local user
+ this._localSession.getLoginState().setState(LoginState.UNAVAILABLE);
+ } else {
+ // Password and or username wrong
+ this._localSession.getLoginState().setState(LoginState.LOGIN_FAILED);
+ }
+ }
+ return this.getLoginState().getState();
+ }
- // If we are not connected, we must check (asynchronously), whether the local database is initial
- this._localSession.isInitial().then((isInitial) => {
- if (isInitial) {
- // If we were initial, the local session was waiting for a sync.
- if (connectionState === ConnectionState.REJECTED) {
- // Explicitly fail the login if the Connection was rejected, so the LocalSession knows what's going on
- // additionally, fail sync to resolve deadlock
- this._localSession.loginState.setState(LoginState.LOGIN_FAILED);
- this._localSession.syncState.setState(SyncState.FAILED);
- } else {
- // Explicitly abort the sync to resolve the deadlock
- this._localSession.syncState.setState(SyncState.ABORTED);
- }
- }
- });
+ private handleRemotePasswordChange(username: string) {
+ this._localSession.logout();
+ this._localSession.removeUser(username);
+ this._localSession.getLoginState().setState(LoginState.LOGIN_FAILED);
+ this._alertService.addDanger(
+ $localize`Your password was changed recently. Please retry with your new password!`
+ );
+ }
- // remote rejected but local logged in
- if (connectionState === ConnectionState.REJECTED) {
- if ((await localLogin) === LoginState.LOGGED_IN) {
- // Someone changed the password remotely --> log out and signal failed login
- this._localSession.logout();
- this._localSession.loginState.setState(LoginState.LOGIN_FAILED);
- this._alertService.addDanger(
- $localize`Your password was changed recently. Please retry with your new password!`
- );
- }
- }
+ private retryLoginWhileOffline(username: string, password: string) {
+ this._offlineRetryLoginScheduleHandle = setTimeout(() => {
+ this.login(username, password);
+ }, this.LOGIN_RETRY_TIMEOUT);
+ }
- // offline? retry (unless we are in an initial state)! TODO(lh): Backoff
+ private updateLocalUserAndStartSync(password: string) {
+ // Update local user object
+ const remoteUser = this._remoteSession.getCurrentDBUser();
+ this._localSession.saveUser(remoteUser, password);
+
+ return this.sync()
+ .then(() => this.liveSyncDeferred())
+ .catch(() => {
if (
- connectionState === ConnectionState.OFFLINE &&
- !(await this._localSession.isInitial())
+ this._localSession.getLoginState().getState() === LoginState.LOGGED_IN
) {
- this._offlineRetryLoginScheduleHandle = setTimeout(() => {
- this.login(username, password);
- }, 2000);
+ this.liveSyncDeferred();
}
- })
- .catch((err) => this._loggingService.error(err));
- return localLogin; // the local login is the Promise that counts
+ });
}
/** see {@link SessionService} */
public getCurrentUser(): User {
- return this._localSession.currentUser;
+ return this._localSession.getCurrentUser();
}
- /** see {@link SessionService} */
- public getLoginState() {
- return this._localSession.loginState;
+ public getCurrentDBUser(): DatabaseUser {
+ return this._localSession.getCurrentDBUser();
}
- /** see {@link SessionService} */
- public getConnectionState() {
- return this._remoteSession.connectionState;
+
+ public checkPassword(username: string, password: string): boolean {
+ // This only checks the password against locally saved users
+ return this._localSession.checkPassword(username, password);
}
+
/** see {@link SessionService} */
- public getSyncState() {
- return this._localSession.syncState;
+ public getLoginState() {
+ return this._localSession.getLoginState();
}
/** see {@link SessionService} */
public async sync(): Promise {
- this._localSession.syncState.setState(SyncState.STARTED);
+ this.getSyncState().setState(SyncState.STARTED);
try {
- const result = await this._localSession.database.sync(
- this._remoteSession.database,
- { batch_size: 500 }
- );
- this._localSession.syncState.setState(SyncState.COMPLETED);
+ const result = await this.pouchDB.sync(this._remoteSession.pouchDB, {
+ batch_size: this.POUCHDB_SYNC_BATCH_SIZE,
+ });
+ this.getSyncState().setState(SyncState.COMPLETED);
return result;
} catch (error) {
- this._localSession.syncState.setState(SyncState.FAILED);
+ this.getSyncState().setState(SyncState.FAILED);
throw error; // rethrow, so later Promise-handling lands in .catch, too
}
}
@@ -202,30 +193,33 @@ export class SyncedSessionService extends SessionService {
*/
public liveSync() {
this.cancelLiveSync(); // cancel any liveSync that may have been alive before
- this._localSession.syncState.setState(SyncState.STARTED);
- this._liveSyncHandle = this._localSession.database
- .sync(this._remoteSession.database, {
- live: true,
- retry: true,
- })
+ this.getSyncState().setState(SyncState.STARTED);
+ this._liveSyncHandle = (this.pouchDB.sync(this._remoteSession.pouchDB, {
+ live: true,
+ retry: true,
+ }) as any)
.on("change", (change) => {
// after sync. change has direction and changes with info on errors etc
})
.on("paused", (info) => {
// replication was paused: either because sync is finished or because of a failed sync (mostly due to lost connection). info is empty.
- if (this.getConnectionState().getState() !== ConnectionState.OFFLINE) {
- this._localSession.syncState.setState(SyncState.COMPLETED);
+ if (
+ this._remoteSession.getLoginState().getState() ===
+ LoginState.LOGGED_IN
+ ) {
+ this.getSyncState().setState(SyncState.COMPLETED);
// We might end up here after a failed sync that is not due to offline errors.
// It shouldn't happen too often, as we have an initial non-live sync to catch those situations, but we can't find that out here
}
})
.on("active", (info) => {
// replication was resumed: either because new things to sync or because connection is available again. info contains the direction
- this._localSession.syncState.setState(SyncState.STARTED);
+ this.getSyncState().setState(SyncState.STARTED);
})
.on("error", (err) => {
// totally unhandled error (shouldn't happen)
- this._localSession.syncState.setState(SyncState.FAILED);
+ console.error("sync failed", err);
+ this.getSyncState().setState(SyncState.FAILED);
})
.on("complete", (info) => {
// replication was canceled!
@@ -271,7 +265,7 @@ export class SyncedSessionService extends SessionService {
* als see {@link SessionService}
*/
public getDatabase(): Database {
- return new PouchDatabase(this._localSession.database, this._loggingService);
+ return this.database;
}
/**
diff --git a/src/app/core/session/session-states/connection-state.enum.ts b/src/app/core/session/session-states/connection-state.enum.ts
deleted file mode 100644
index a5c52231c1..0000000000
--- a/src/app/core/session/session-states/connection-state.enum.ts
+++ /dev/null
@@ -1,28 +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 .
- */
-
-/** State of the connection to the remote database */
-export enum ConnectionState {
- /** we are offline and therefor not connected to the remote db */
- OFFLINE,
- /** we tried to login, but it failed, so we are not connected to the remote db */
- REJECTED,
- /** we are intentionally not connected to the remote db */
- DISCONNECTED,
- /** we are connected to the remote db */
- CONNECTED,
-}
diff --git a/src/app/core/session/session-states/login-state.enum.ts b/src/app/core/session/session-states/login-state.enum.ts
index 6bda57a0e3..446b108258 100644
--- a/src/app/core/session/session-states/login-state.enum.ts
+++ b/src/app/core/session/session-states/login-state.enum.ts
@@ -23,4 +23,6 @@ export enum LoginState {
LOGGED_OUT,
/** Successfully logged in */
LOGGED_IN,
+ /** Login is not possible right now */
+ UNAVAILABLE,
}
diff --git a/src/app/core/session/session.service.provider.ts b/src/app/core/session/session.service.provider.ts
index fd86694856..b7940402f7 100644
--- a/src/app/core/session/session.service.provider.ts
+++ b/src/app/core/session/session.service.provider.ts
@@ -23,9 +23,9 @@ import { LoggingService } from "../logging/logging.service";
import { EntitySchemaService } from "../entity/schema/entity-schema.service";
import { LoginState } from "./session-states/login-state.enum";
import { SessionType } from "./session-type";
-import { NewLocalSessionService } from "./session-service/new-local-session.service";
import { PouchDatabase } from "../database/pouch-database";
import { HttpClient } from "@angular/common/http";
+import { LocalSession } from "./session-service/local-session";
/**
* Factory method for Angular DI provider of SessionService.
@@ -41,13 +41,12 @@ export function sessionServiceFactory(
let sessionService: SessionService;
switch (AppConfig.settings.session_type) {
case SessionType.local:
- sessionService = new NewLocalSessionService(
- loggingService,
- entitySchemaService,
+ sessionService = new LocalSession(
PouchDatabase.createWithIndexedDB(
AppConfig.settings.database.name,
loggingService
- )
+ ),
+ entitySchemaService
);
break;
case SessionType.synced:
@@ -59,18 +58,17 @@ export function sessionServiceFactory(
);
break;
default:
- sessionService = new NewLocalSessionService(
- loggingService,
- entitySchemaService,
+ sessionService = new LocalSession(
PouchDatabase.createWithInMemoryDB(
AppConfig.settings.database.name,
loggingService
- )
+ ),
+ entitySchemaService
);
break;
}
- // TODO: requires a configuration or UI option to select OnlineSession: https://github.com/Aam-Digital/ndb-core/issues/434
- // return new OnlineSessionService(alertService, entitySchemaService);
+ // TODO: requires a configuration or UI option to select RemoteSession: https://github.com/Aam-Digital/ndb-core/issues/434
+ // return new RemoteSession(httpClient, loggingService);
updateLoggingServiceWithUserContext(sessionService);
diff --git a/src/app/core/user/demo-user-generator.service.ts b/src/app/core/user/demo-user-generator.service.ts
index a94494d1d9..742c0b8d82 100644
--- a/src/app/core/user/demo-user-generator.service.ts
+++ b/src/app/core/user/demo-user-generator.service.ts
@@ -2,6 +2,7 @@ import { DemoDataGenerator } from "../demo-data/demo-data-generator";
import { Injectable } from "@angular/core";
import { User } from "./user";
import { faker } from "../demo-data/faker";
+import { LocalSession } from "../session/session-service/local-session";
/**
* Generate demo users for the application with its DemoDataModule.
@@ -24,10 +25,6 @@ export class DemoUserGeneratorService extends DemoDataGenerator {
];
}
- constructor() {
- super();
- }
-
/**
* Generate User entities to be loaded by the DemoDataModule.
*/
@@ -35,12 +32,21 @@ export class DemoUserGeneratorService extends DemoDataGenerator {
const users = [];
const demoUser = new User(DemoUserGeneratorService.DEFAULT_USERNAME);
demoUser.name = DemoUserGeneratorService.DEFAULT_USERNAME;
- demoUser.setNewPassword(DemoUserGeneratorService.DEFAULT_PASSWORD);
const demoAdmin = new User("demo-admin");
demoAdmin.name = "demo-admin";
demoAdmin.admin = true;
- demoAdmin.setNewPassword(DemoUserGeneratorService.DEFAULT_PASSWORD);
+
+ // Create temporary session to save users to local storage
+ const tmpLocalSession = new LocalSession();
+ tmpLocalSession.saveUser(
+ { name: demoUser.name, roles: ["user_app"] },
+ DemoUserGeneratorService.DEFAULT_PASSWORD
+ );
+ tmpLocalSession.saveUser(
+ { name: demoAdmin.name, roles: ["user_app", "admin"] },
+ DemoUserGeneratorService.DEFAULT_PASSWORD
+ );
users.push(demoUser, demoAdmin);
diff --git a/src/app/core/user/user-account/user-account.component.html b/src/app/core/user/user-account/user-account.component.html
index 769ca6a858..02dd00ee49 100644
--- a/src/app/core/user/user-account/user-account.component.html
+++ b/src/app/core/user/user-account/user-account.component.html
@@ -27,7 +27,7 @@
matInput="text"
id="username"
type="text"
- [value]="user.name"
+ [value]="username"
disabled
/>
@@ -124,7 +124,7 @@
Password changed successfully.
- Failed to change password.
+ Failed to change password: {{ passwordChangeResult.error }}
Please try again. If the problem persists contact Aam Digital
support.
@@ -149,7 +149,7 @@
diff --git a/src/app/core/user/user-account/user-account.component.spec.ts b/src/app/core/user/user-account/user-account.component.spec.ts
index c287f541f1..3680ed40b0 100644
--- a/src/app/core/user/user-account/user-account.component.spec.ts
+++ b/src/app/core/user/user-account/user-account.component.spec.ts
@@ -26,8 +26,6 @@ import {
import { UserAccountComponent } from "./user-account.component";
import { SessionService } from "../../session/session-service/session.service";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
-import { EntityMapperService } from "../../entity/entity-mapper.service";
-import { User } from "../user";
import { AppConfig } from "../../app-config/app-config";
import { UserAccountService } from "./user-account.service";
import { UserModule } from "../user.module";
@@ -40,10 +38,8 @@ describe("UserAccountComponent", () => {
let fixture: ComponentFixture;
let mockSessionService: jasmine.SpyObj;
- let mockEntityMapper: jasmine.SpyObj;
let mockUserAccountService: jasmine.SpyObj;
let mockLoggingService: jasmine.SpyObj;
- const testUser = new User("");
beforeEach(
waitForAsync(() => {
@@ -53,9 +49,9 @@ describe("UserAccountComponent", () => {
mockSessionService = jasmine.createSpyObj("sessionService", [
"getCurrentUser",
"login",
+ "checkPassword",
]);
- mockSessionService.getCurrentUser.and.returnValue(testUser);
- mockEntityMapper = jasmine.createSpyObj(["save"]);
+ mockSessionService.getCurrentUser.and.returnValue(null);
mockUserAccountService = jasmine.createSpyObj("mockUserAccount", [
"changePassword",
]);
@@ -66,7 +62,6 @@ describe("UserAccountComponent", () => {
imports: [UserModule, NoopAnimationsModule],
providers: [
{ provide: SessionService, useValue: mockSessionService },
- { provide: EntityMapperService, useValue: mockEntityMapper },
{ provide: UserAccountService, useValue: mockUserAccountService },
{ provide: LoggingService, useValue: mockLoggingService },
],
@@ -89,43 +84,50 @@ describe("UserAccountComponent", () => {
});
it("should set error when password is incorrect", () => {
- const user = new User("TestUser");
- user.setNewPassword("testPW");
- component.user = user;
component.passwordForm.get("currentPassword").setValue("wrongPW");
+ mockSessionService.checkPassword.and.returnValue(false);
+
expect(component.passwordForm.get("currentPassword").valid).toBeTrue();
+
component.changePassword();
+
expect(component.passwordForm.get("currentPassword").valid).toBeFalse();
});
it("should set error when password change fails", fakeAsync(() => {
- const user = new User("TestUser");
- user.setNewPassword("testPW");
- component.user = user;
+ component.username = "testUser";
component.passwordForm.get("currentPassword").setValue("testPW");
+ mockSessionService.checkPassword.and.returnValue(true);
mockUserAccountService.changePassword.and.rejectWith(
new Error("pw change error")
);
+
try {
component.changePassword();
tick();
} catch (e) {
// expected to re-throw the error for upstream reporting
}
+
+ expect(mockUserAccountService.changePassword).toHaveBeenCalled();
expect(component.passwordChangeResult.success).toBeFalse();
expect(component.passwordChangeResult.error).toBe("pw change error");
}));
- it("should set success when password change worked", fakeAsync(() => {
- const user = new User("TestUser");
- user.setNewPassword("testPW");
- component.user = user;
+ it("should set success and re-login when password change worked", fakeAsync(() => {
+ component.username = "testUser";
component.passwordForm.get("currentPassword").setValue("testPW");
- mockUserAccountService.changePassword.and.resolveTo(null);
+ component.passwordForm.get("newPassword").setValue("changedPassword");
+ mockSessionService.checkPassword.and.returnValue(true);
+ mockUserAccountService.changePassword.and.resolveTo();
mockSessionService.login.and.resolveTo(null);
+
component.changePassword();
tick();
- tick();
expect(component.passwordChangeResult.success).toBeTrue();
+ expect(mockSessionService.login).toHaveBeenCalledWith(
+ "testUser",
+ "changedPassword"
+ );
}));
});
diff --git a/src/app/core/user/user-account/user-account.component.ts b/src/app/core/user/user-account/user-account.component.ts
index 51d2c78bb3..4580276335 100644
--- a/src/app/core/user/user-account/user-account.component.ts
+++ b/src/app/core/user/user-account/user-account.component.ts
@@ -16,9 +16,7 @@
*/
import { Component, OnInit } from "@angular/core";
-import { User } from "../user";
import { SessionService } from "../../session/session-service/session.service";
-import { EntityMapperService } from "../../entity/entity-mapper.service";
import { WebdavModule } from "../../webdav/webdav.module";
import { UserAccountService } from "./user-account.service";
import { FormBuilder, ValidationErrors, Validators } from "@angular/forms";
@@ -36,7 +34,7 @@ import { SessionType } from "../../session/session-type";
})
export class UserAccountComponent implements OnInit {
/** user to be edited */
- user: User;
+ username: string;
/** whether webdav integration is configured and the cloud settings section should be displayed */
webdavEnabled = WebdavModule.isEnabled;
@@ -67,7 +65,6 @@ export class UserAccountComponent implements OnInit {
);
constructor(
- private entityMapperService: EntityMapperService,
private sessionService: SessionService,
private userAccountService: UserAccountService,
private fb: FormBuilder,
@@ -76,7 +73,7 @@ export class UserAccountComponent implements OnInit {
ngOnInit() {
this.checkIfPasswordChangeAllowed();
- this.user = this.sessionService.getCurrentUser();
+ this.username = this.sessionService.getCurrentUser()?.name;
}
checkIfPasswordChangeAllowed() {
@@ -97,7 +94,8 @@ export class UserAccountComponent implements OnInit {
this.passwordChangeResult = undefined;
const currentPassword = this.passwordForm.get("currentPassword").value;
- if (!this.user.checkPassword(currentPassword)) {
+
+ if (!this.sessionService.checkPassword(this.username, currentPassword)) {
this.passwordForm
.get("currentPassword")
.setErrors({ incorrectPassword: true });
@@ -106,11 +104,9 @@ export class UserAccountComponent implements OnInit {
const newPassword = this.passwordForm.get("newPassword").value;
this.userAccountService
- .changePassword(this.user, currentPassword, newPassword)
- .then(() => this.sessionService.login(this.user.name, newPassword))
- .then(() => {
- this.passwordChangeResult = { success: true };
- })
+ .changePassword(this.username, currentPassword, newPassword)
+ .then(() => this.sessionService.login(this.username, newPassword))
+ .then(() => (this.passwordChangeResult = { success: true }))
.catch((err: Error) => {
this.passwordChangeResult = { success: false, error: err.message };
this.loggingService.error({
diff --git a/src/app/core/user/user-account/user-account.service.spec.ts b/src/app/core/user/user-account/user-account.service.spec.ts
index 1a4d96dc69..52c17c35fc 100644
--- a/src/app/core/user/user-account/user-account.service.spec.ts
+++ b/src/app/core/user/user-account/user-account.service.spec.ts
@@ -1,24 +1,16 @@
import { TestBed } from "@angular/core/testing";
-
import { UserAccountService } from "./user-account.service";
-import { EntityMapperService } from "../../entity/entity-mapper.service";
import { HttpClient } from "@angular/common/http";
-import { User } from "../user";
-import { of } from "rxjs";
+import { of, throwError } from "rxjs";
describe("UserAccountService", () => {
let service: UserAccountService;
- let mockEntityMapper: jasmine.SpyObj;
let mockHttpClient: jasmine.SpyObj;
beforeEach(() => {
- mockEntityMapper = jasmine.createSpyObj("mockEntityMapper", ["save"]);
mockHttpClient = jasmine.createSpyObj("mockHttpClient", ["get", "put"]);
TestBed.configureTestingModule({
- providers: [
- { provide: EntityMapperService, useValue: mockEntityMapper },
- { provide: HttpClient, useValue: mockHttpClient },
- ],
+ providers: [{ provide: HttpClient, useValue: mockHttpClient }],
});
service = TestBed.inject(UserAccountService);
});
@@ -27,39 +19,24 @@ describe("UserAccountService", () => {
expect(service).toBeTruthy();
});
- it("should reject if old password is incorrect", (done) => {
- const user = new User("TestUser");
- user.setNewPassword("testPW");
- service
- .changePassword(user, "wrongPW", "")
- .then(() => fail())
- .catch((err) => {
- expect(err).toBeDefined();
- done();
- });
- });
+ it("should reject if current user cant be fetched", (done) => {
+ mockHttpClient.get.and.returnValue(throwError(new Error()));
- it("should call report error when CouchDB not available", (done) => {
- const user = new User("TestUser");
- user.setNewPassword("testPW");
- mockHttpClient.get.and.throwError(new Error("error"));
service
- .changePassword(user, "testPW", "")
+ .changePassword("username", "wrongPW", "")
.then(() => fail())
.catch((err) => {
- expect(mockHttpClient.get).toHaveBeenCalled();
expect(err).toBeDefined();
done();
});
});
- it("should call report error when new Password cannot be saved", (done) => {
- const user = new User("TestUser");
- user.setNewPassword("testPW");
+ it("should report error when new Password cannot be saved", (done) => {
mockHttpClient.get.and.returnValues(of({}));
- mockHttpClient.put.and.throwError(new Error("error"));
+ mockHttpClient.put.and.returnValue(throwError(new Error()));
+
service
- .changePassword(user, "testPW", "")
+ .changePassword("username", "testPW", "")
.then(() => fail())
.catch((err) => {
expect(mockHttpClient.get).toHaveBeenCalled();
@@ -69,16 +46,12 @@ describe("UserAccountService", () => {
});
});
- it("should return User with new password", (done) => {
- const user = new User("TestUser");
- user.setNewPassword("testPW");
+ it("should not fail if get and put requests are successful", () => {
mockHttpClient.get.and.returnValues(of({}));
mockHttpClient.put.and.returnValues(of({}));
- mockEntityMapper.save.and.resolveTo(true);
- service.changePassword(user, "testPW", "newPW").then((res) => {
- expect(res.checkPassword("testPW")).toBeFalse();
- expect(res.checkPassword("newPW")).toBeTrue();
- done();
- });
+
+ return expectAsync(
+ service.changePassword("username", "testPW", "newPW")
+ ).not.toBeRejected();
});
});
diff --git a/src/app/core/user/user-account/user-account.service.ts b/src/app/core/user/user-account/user-account.service.ts
index cc09522486..4a40920867 100644
--- a/src/app/core/user/user-account/user-account.service.ts
+++ b/src/app/core/user/user-account/user-account.service.ts
@@ -1,72 +1,59 @@
import { Injectable } from "@angular/core";
import { HttpClient, HttpHeaders } from "@angular/common/http";
-import { EntityMapperService } from "../../entity/entity-mapper.service";
-import { User } from "../user";
@Injectable({
providedIn: "root",
})
export class UserAccountService {
private static readonly COUCHDB_USER_ENDPOINT = "/db/_users/org.couchdb.user";
- constructor(
- private http: HttpClient,
- private entityMapper: EntityMapperService
- ) {}
+ constructor(private http: HttpClient) {}
/**
* Function to change the password of a user
- * @param user The user for which the password should be changed
+ * @param username The username for which the password should be changed
* @param oldPassword The current plaintext password of the user
* @param newPassword The new plaintext password of the user
* @return Promise that resolves once the password is changed in _user and the database
*/
public async changePassword(
- user: User,
+ username: string,
oldPassword: string,
newPassword: string
- ): Promise {
- if (!user.checkPassword(oldPassword)) {
- throw new Error("Wrong current password");
- }
-
+ ): Promise {
let userResponse;
try {
- userResponse = await this.getCouchDBUser(user, oldPassword);
+ // TODO due to cookie-auth, the old password is actually not checked
+ userResponse = await this.getCouchDBUser(username, oldPassword);
} catch (e) {
throw new Error("Current password incorrect or server not available");
}
userResponse["password"] = newPassword;
- user.setNewPassword(newPassword);
try {
- await Promise.all([
- this.saveNewPasswordToCouchDB(user, oldPassword, userResponse),
- this.entityMapper.save(user),
- ]);
+ await this.saveNewPasswordToCouchDB(username, oldPassword, userResponse);
} catch (e) {
throw new Error(
"Could not save new password, please contact your system administrator"
);
}
- return user;
}
- private getCouchDBUser(user: User, password: string): Promise {
- const userUrl = UserAccountService.COUCHDB_USER_ENDPOINT + ":" + user.name;
+ private getCouchDBUser(username: string, password: string): Promise {
+ const userUrl = UserAccountService.COUCHDB_USER_ENDPOINT + ":" + username;
const headers: HttpHeaders = new HttpHeaders({
- Authorization: "Basic " + btoa(user.name + ":" + password),
+ Authorization: "Basic " + btoa(username + ":" + password),
});
return this.http.get(userUrl, { headers: headers }).toPromise();
}
private saveNewPasswordToCouchDB(
- user: User,
+ username: string,
oldPassword: string,
- userObj
+ userObj: any
): Promise {
- const userUrl = UserAccountService.COUCHDB_USER_ENDPOINT + ":" + user.name;
+ const userUrl = UserAccountService.COUCHDB_USER_ENDPOINT + ":" + username;
const headers: HttpHeaders = new HttpHeaders({
- Authorization: "Basic " + btoa(user.name + ":" + oldPassword),
+ Authorization: "Basic " + btoa(username + ":" + oldPassword),
});
return this.http.put(userUrl, userObj, { headers: headers }).toPromise();
}
diff --git a/src/app/core/user/user.spec.ts b/src/app/core/user/user.spec.ts
index 731aee6c5b..bdd5f18ec5 100644
--- a/src/app/core/user/user.spec.ts
+++ b/src/app/core/user/user.spec.ts
@@ -53,8 +53,7 @@ describe("User", () => {
name: "tester",
admin: true,
- password: undefined,
- cloudPasswordEnc: undefined,
+ cloudPasswordEnc: "encryptedPassword",
cloudBaseFolder: "/aam-digital/",
paginatorSettingsPageSize: {},
@@ -65,41 +64,20 @@ describe("User", () => {
const entity = new User(id);
entity.name = expectedData.name;
entity.admin = expectedData.admin;
- entity.setNewPassword("pass");
// @ts-ignore
- expectedData.password = entity.password;
- // @ts-ignore
- expectedData.cloudPasswordEnc = entity.cloudPasswordEnc;
+ entity.cloudPasswordEnc = expectedData.cloudPasswordEnc;
const rawData = entitySchemaService.transformEntityToDatabaseFormat(entity);
expect(rawData).toEqual(expectedData);
});
- it("accepts valid password", function () {
- const entityId = "test1";
- const user = new User(entityId);
- const password = "pass";
- user.setNewPassword(password);
-
- expect(user.checkPassword(password)).toBeTruthy();
- });
-
- it("rejects wrong password", function () {
- const entityId = "test1";
- const user = new User(entityId);
- const password = "pass";
- user.setNewPassword(password);
-
- expect(user.checkPassword(password + "x")).toBeFalsy();
- });
-
it("sets cloud passwords", () => {
const user = new User("test1");
- user.setNewPassword("userpwd");
expect(user.cloudPasswordDec).not.toBeDefined();
- expect(user.checkPassword("userpwd")).toBeTrue();
+
user.setCloudPassword("cloudpwd", "userpwd");
+
expect(user.cloudPasswordDec).toEqual("cloudpwd");
expect(user.decryptCloudPassword("userpwd")).toEqual("cloudpwd");
});
diff --git a/src/app/core/user/user.ts b/src/app/core/user/user.ts
index b573454cba..9e170f7e20 100644
--- a/src/app/core/user/user.ts
+++ b/src/app/core/user/user.ts
@@ -35,9 +35,6 @@ export class User extends Entity {
/** whether this user has admin rights */
@DatabaseField() admin: boolean;
- /** password object (encrypted) */
- @DatabaseField() private password: any;
-
/** settings for the mat-paginator for tables
* pageSizeOptions is set in the corresponding html of the component,
* pageSize is stored persistently in the database and
@@ -46,7 +43,7 @@ export class User extends Entity {
@DatabaseField() paginatorSettingsPageSize: any = {};
public paginatorSettingsPageIndex: any = {};
- /** password for webdav account (encrypted with user.password) */
+ /** password for webdav account (encrypted) */
@DatabaseField() private cloudPasswordEnc: any;
/** username for webdav account */
@@ -58,58 +55,6 @@ export class User extends Entity {
/** base folder for webdav, all actions of the app will happen relative to this as the root folder */
@DatabaseField() public cloudBaseFolder: string = "/aam-digital/";
- /**
- * Set a new user password.
- * This will be encrypted before saving.
- *
- * Warning: User password must be identical to the CouchDB user password. Otherwise database sync will fail!
- *
- * @param password The new password to be set
- */
- public setNewPassword(password: string) {
- const cryptKeySize = 256 / 32;
- const cryptIterations = 128;
- const cryptSalt = CryptoJS.lib.WordArray.random(128 / 8).toString();
- const hash = CryptoJS.PBKDF2(password, cryptSalt, {
- keySize: cryptKeySize,
- iterations: cryptIterations,
- }).toString();
-
- this.password = {
- hash: hash,
- salt: cryptSalt,
- iterations: cryptIterations,
- keysize: cryptKeySize,
- };
-
- // update encrypted nextcloud password
- this.cloudPasswordEnc = CryptoJS.AES.encrypt(
- this.cloudPasswordDec,
- password
- ).toString();
- }
-
- /**
- * Check whether the given password is correct.
- * @param givenPassword Password attempted
- */
- public checkPassword(givenPassword: string): boolean {
- // hash the given password string and compare it with the stored hash
- return this.hashPassword(givenPassword) === this.password.hash;
- }
-
- private hashPassword(givenPassword: string): string {
- const options = {
- keySize: this.password.keysize,
- iterations: this.password.iterations,
- };
- return CryptoJS.PBKDF2(
- givenPassword,
- this.password.salt,
- options
- ).toString();
- }
-
/**
* Decrypt the stored cloud password with the user's regular password.
* @param givenPassword The user entity's password (not the webdav cloud password)
@@ -133,13 +78,11 @@ export class User extends Entity {
* @param givenPassword The user entity's password (used for encrypting the cloud password before storage)
*/
public setCloudPassword(blobPassword: string, givenPassword: string) {
- if (this.checkPassword(givenPassword)) {
- this.cloudPasswordDec = blobPassword;
- this.cloudPasswordEnc = CryptoJS.AES.encrypt(
- blobPassword,
- givenPassword
- ).toString();
- }
+ this.cloudPasswordDec = blobPassword;
+ this.cloudPasswordEnc = CryptoJS.AES.encrypt(
+ blobPassword,
+ givenPassword
+ ).toString();
}
/**
diff --git a/src/app/core/webdav/cloud-file-service-user-settings/cloud-file-service-user-settings.component.spec.ts b/src/app/core/webdav/cloud-file-service-user-settings/cloud-file-service-user-settings.component.spec.ts
index 83fd650c6d..69170e49c8 100644
--- a/src/app/core/webdav/cloud-file-service-user-settings/cloud-file-service-user-settings.component.spec.ts
+++ b/src/app/core/webdav/cloud-file-service-user-settings/cloud-file-service-user-settings.component.spec.ts
@@ -8,6 +8,7 @@ import { EntityMapperService } from "../../entity/entity-mapper.service";
import { AlertService } from "../../alerts/alert.service";
import { AppConfig } from "../../app-config/app-config";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { SessionService } from "../../session/session-service/session.service";
describe("CloudFileServiceUserSettingsComponent", () => {
let component: CloudFileServiceUserSettingsComponent;
@@ -15,18 +16,23 @@ describe("CloudFileServiceUserSettingsComponent", () => {
let mockCloudFileService: jasmine.SpyObj;
let mockEntityMapper: jasmine.SpyObj;
+ let mockSessionService: jasmine.SpyObj;
let testUser: User;
beforeEach(
waitForAsync(() => {
testUser = new User("user");
+ testUser.cloudUserName = "cloudUsername";
mockCloudFileService = jasmine.createSpyObj([
"connect",
"checkConnection",
]);
mockEntityMapper = jasmine.createSpyObj("", [
"save",
+ "load",
]);
+ mockEntityMapper.load.and.resolveTo(testUser);
+ mockSessionService = jasmine.createSpyObj(["checkPassword"]);
// @ts-ignore
AppConfig.settings = { webdav: { remote_url: "test-url" } };
@@ -40,32 +46,44 @@ describe("CloudFileServiceUserSettingsComponent", () => {
provide: AlertService,
useValue: jasmine.createSpyObj(["addInfo"]),
},
+ { provide: SessionService, useValue: mockSessionService },
],
}).compileComponents();
})
);
- beforeEach(() => {
- fixture = TestBed.createComponent(CloudFileServiceUserSettingsComponent);
- component = fixture.componentInstance;
- component.user = testUser;
- fixture.detectChanges();
- });
+ beforeEach(
+ waitForAsync(() => {
+ fixture = TestBed.createComponent(CloudFileServiceUserSettingsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ })
+ );
it("should create", () => {
expect(component).toBeTruthy();
});
+ it("should load the user entity on startup", async () => {
+ component.username = "testUser";
+
+ await component.ngOnInit();
+
+ expect(mockEntityMapper.load).toHaveBeenCalledWith(User, "testUser");
+ expect(component.user).toBe(testUser);
+ expect(component.form.get("cloudUser").value).toBe(testUser.cloudUserName);
+ });
+
it("should update cloud-service credentials and check the connection", async () => {
const cloudPwSpy = spyOn(testUser, "setCloudPassword");
- const checkPwSpy = spyOn(testUser, "checkPassword");
- checkPwSpy.and.returnValue(true);
+ mockSessionService.checkPassword.and.returnValue(true);
component.form.controls.cloudUser.setValue("testUser");
component.form.controls.cloudPassword.setValue("testPwd");
component.form.controls.userPassword.setValue("loginPwd");
mockCloudFileService.checkConnection.and.returnValue(Promise.resolve(true));
await component.updateCloudServiceSettings();
+
expect(testUser.cloudUserName).toBe("testUser");
expect(cloudPwSpy).toHaveBeenCalledWith("testPwd", "loginPwd");
expect(mockCloudFileService.connect).toHaveBeenCalled();
@@ -75,8 +93,7 @@ describe("CloudFileServiceUserSettingsComponent", () => {
it("should not save user if cloud-service credentials are incorrect", async () => {
spyOn(testUser, "setCloudPassword");
- const checkPwSpy = spyOn(testUser, "checkPassword");
- checkPwSpy.and.returnValue(true);
+ mockSessionService.checkPassword.and.returnValue(true);
component.form.controls.cloudUser.setValue("testUser");
component.form.controls.cloudPassword.setValue("testPwd");
component.form.controls.userPassword.setValue("loginPwd");
diff --git a/src/app/core/webdav/cloud-file-service-user-settings/cloud-file-service-user-settings.component.ts b/src/app/core/webdav/cloud-file-service-user-settings/cloud-file-service-user-settings.component.ts
index e5e689af4d..d21ff61760 100644
--- a/src/app/core/webdav/cloud-file-service-user-settings/cloud-file-service-user-settings.component.ts
+++ b/src/app/core/webdav/cloud-file-service-user-settings/cloud-file-service-user-settings.component.ts
@@ -5,6 +5,7 @@ import { AppConfig } from "../../app-config/app-config";
import { EntityMapperService } from "../../entity/entity-mapper.service";
import { AlertService } from "../../alerts/alert.service";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
+import { SessionService } from "../../session/session-service/session.service";
/**
* User Profile form to allow the user to set up credentials for a webdav server to be used by the CloudFileService.
@@ -16,7 +17,9 @@ import { FormBuilder, FormGroup, Validators } from "@angular/forms";
})
export class CloudFileServiceUserSettingsComponent implements OnInit {
/** The user for who this form edits data */
- @Input() user: User;
+ @Input() username: string;
+
+ user: User;
/** Webdav server URL */
webdavUrl: string;
@@ -30,26 +33,29 @@ export class CloudFileServiceUserSettingsComponent implements OnInit {
private fb: FormBuilder,
private entityMapperService: EntityMapperService,
private cloudFileService: CloudFileService,
- private alertService: AlertService
- ) {}
-
- ngOnInit() {
- this.webdavUrl = AppConfig.settings.webdav.remote_url;
-
+ private alertService: AlertService,
+ private sessionService: SessionService
+ ) {
this.form = this.fb.group({
- cloudUser: [this.user.cloudUserName, Validators.required],
+ cloudUser: ["", Validators.required],
cloudPassword: ["", Validators.required],
userPassword: ["", Validators.required],
});
}
+ async ngOnInit() {
+ this.webdavUrl = AppConfig.settings.webdav.remote_url;
+ this.user = await this.entityMapperService.load(User, this.username);
+ this.form.get("cloudUser").setValue(this.user.cloudUserName);
+ }
+
/**
* Sets the username and password for the cloud-service, provided the login password is correct
* and saves the user entity.
*/
async updateCloudServiceSettings() {
const password = this.form.controls.userPassword.value;
- if (!this.user.checkPassword(password)) {
+ if (!this.sessionService.checkPassword(this.user.name, password)) {
this.form.controls.userPassword.setErrors({ incorrectPassword: true });
return;
}
diff --git a/src/app/core/webdav/cloud-file-service.service.spec.ts b/src/app/core/webdav/cloud-file-service.service.spec.ts
index a7e6e3c889..525de93fa1 100644
--- a/src/app/core/webdav/cloud-file-service.service.spec.ts
+++ b/src/app/core/webdav/cloud-file-service.service.spec.ts
@@ -77,12 +77,12 @@ describe("CloudFileService", () => {
it(".connect() should connect using credentials saved for user", () => {
const testUser = new User("user");
- testUser.setNewPassword("pass");
testUser.cloudUserName = "testuser";
testUser.setCloudPassword("testuserpass", "pass");
sessionSpy.getCurrentUser.and.returnValue(testUser);
cloudFileService.connect();
+
expect(sessionSpy.getCurrentUser).toHaveBeenCalled();
expect(mockWebdav.createClient).toHaveBeenCalledWith("test-url", {
username: "testuser",
diff --git a/src/locale/messages.de.xlf b/src/locale/messages.de.xlf
index ddd3c8ddfb..136132dc60 100644
--- a/src/locale/messages.de.xlf
+++ b/src/locale/messages.de.xlf
@@ -1303,7 +1303,7 @@
src/app/core/config/config-fix.ts
- 27
+ 28
@@ -1859,12 +1859,16 @@
131
-
-
- Soll wirklich das komplette Backup gelöscht werden? Dadurch werden alle existierenden Aufzeichnungen gelöscht und Aufzeichnungen von der neuen Datei geladen.
+
+
+ Are you sure you want to restore this backup? This will
+ delete all existing records,
+ restoring records from the loaded file.src/app/core/admin/admin/admin.component.ts
- 132
+ 132,130
@@ -1872,7 +1876,7 @@
Backup wiederhergestelltsrc/app/core/admin/admin/admin.component.ts
- 148
+ 146
@@ -1880,7 +1884,7 @@
Neue Daten importieren?src/app/core/admin/admin/admin.component.ts
- 170
+ 168
@@ -1888,7 +1892,7 @@
Soll diese Datei wirklich importiert werden? Das wird alle Aufzeichnungen bla blasrc/app/core/admin/admin/admin.component.ts
- 171
+ 169
@@ -1896,7 +1900,7 @@
Import fertiggestellt?src/app/core/admin/admin/admin.component.ts
- 184
+ 182
@@ -1904,7 +1908,7 @@
Komplette Datenbank löschen?src/app/core/admin/admin/admin.component.ts
- 204
+ 202
@@ -1912,7 +1916,7 @@
Soll wirklich die komplette Datenbank gelöscht werden? Dadurch werden alle Aufzeichnungen in der Datenbank gelöscht!src/app/core/admin/admin/admin.component.ts
- 205
+ 203
@@ -1920,7 +1924,7 @@
Import fertigsrc/app/core/admin/admin/admin.component.ts
- 218
+ 216
@@ -2006,7 +2010,7 @@
Menu itemsrc/app/core/config/config-fix.ts
- 22
+ 23
@@ -2015,7 +2019,7 @@
Menu itemsrc/app/core/config/config-fix.ts
- 32
+ 33
@@ -2024,22 +2028,22 @@
Menu itemsrc/app/core/config/config-fix.ts
- 37
+ 38src/app/core/config/config-fix.ts
- 642
+ 643Anwesenheit Aufzeichnen
+ Record attendance menu item
+ Menu itemsrc/app/core/config/config-fix.ts
- 42
+ 43
- Record attendance menu item
- Menu item
@@ -2047,7 +2051,7 @@
Menu itemsrc/app/core/config/config-fix.ts
- 47
+ 48
@@ -2056,7 +2060,7 @@
Menu itemsrc/app/core/config/config-fix.ts
- 57
+ 58
@@ -2065,7 +2069,7 @@
Menu itemsrc/app/core/config/config-fix.ts
- 62
+ 63
@@ -2074,7 +2078,7 @@
Menu itemsrc/app/core/config/config-fix.ts
- 67
+ 68
@@ -2083,7 +2087,7 @@
Menu itemsrc/app/core/config/config-fix.ts
- 72
+ 73
@@ -2092,7 +2096,7 @@
Menu itemsrc/app/core/config/config-fix.ts
- 77
+ 78
@@ -2101,7 +2105,7 @@
Document statussrc/app/core/config/config-fix.ts
- 99
+ 100
@@ -2110,7 +2114,7 @@
Document statussrc/app/core/config/config-fix.ts
- 103
+ 104
@@ -2119,7 +2123,7 @@
Document statussrc/app/core/config/config-fix.ts
- 107
+ 108
@@ -2128,7 +2132,7 @@
Document statussrc/app/core/config/config-fix.ts
- 111
+ 112
@@ -2137,7 +2141,7 @@
Document statussrc/app/core/config/config-fix.ts
- 115
+ 116
@@ -2146,18 +2150,18 @@
Document statussrc/app/core/config/config-fix.ts
- 119
+ 120Anwesenheit Aufzeichnen
+ record attendance shortcut
+ Dashboard shortcut widgetsrc/app/core/config/config-fix.ts
- 147
+ 148
- record attendance shortcut
- Dashboard shortcut widget
@@ -2173,7 +2177,7 @@
src/app/core/config/config-fix.ts
- 125
+ 126
@@ -2186,7 +2190,7 @@
src/app/core/config/config-fix.ts
- 129
+ 130
@@ -2199,7 +2203,7 @@
src/app/core/config/config-fix.ts
- 133
+ 134
@@ -2283,7 +2287,7 @@
Attendance week dashboard widget labelsrc/app/core/config/config-fix.ts
- 171
+ 172
@@ -2292,7 +2296,7 @@
Attendance week dashboard widget labelsrc/app/core/config/config-fix.ts
- 178
+ 179
@@ -2301,11 +2305,11 @@
Title for notes overviewsrc/app/core/config/config-fix.ts
- 196
+ 197src/app/core/config/config-fix.ts
- 560
+ 561
@@ -2314,11 +2318,11 @@
Translated name of default column groupsrc/app/core/config/config-fix.ts
- 206
+ 207src/app/core/config/config-fix.ts
- 210
+ 211
@@ -2327,29 +2331,29 @@
Translated name of mobile column groupsrc/app/core/config/config-fix.ts
- 207
+ 208src/app/core/config/config-fix.ts
- 220
+ 221src/app/core/config/config-fix.ts
- 390
+ 391src/app/core/config/config-fix.ts
- 449
+ 450assets/help/help.de.md
+ Filename of markdown help page (make sure the filename you enter as a translation actually exists on the server!)src/app/core/config/config-fix.ts
- 261
+ 262
- Filename of markdown help page (make sure the filename you enter as a translation actually exists on the server!)
@@ -2357,7 +2361,7 @@
Title of schools overviewsrc/app/core/config/config-fix.ts
- 273
+ 274
@@ -2366,7 +2370,7 @@
Label for private schools filter - false casesrc/app/core/config/config-fix.ts
- 288
+ 289
@@ -2375,11 +2379,11 @@
Panel titlesrc/app/core/config/config-fix.ts
- 300
+ 301src/app/core/config/config-fix.ts
- 488
+ 489
@@ -2388,7 +2392,7 @@
Panel titlesrc/app/core/config/config-fix.ts
- 324
+ 325
@@ -2397,7 +2401,7 @@
Title children overviewsrc/app/core/config/config-fix.ts
- 339
+ 340
@@ -2406,7 +2410,7 @@
Column label for school attendance of childsrc/app/core/config/config-fix.ts
- 365
+ 366
@@ -2415,7 +2419,7 @@
Column label for coaching attendance of childsrc/app/core/config/config-fix.ts
- 374
+ 375
@@ -2424,11 +2428,11 @@
Translated name of default column groupsrc/app/core/config/config-fix.ts
- 389
+ 390src/app/core/config/config-fix.ts
- 406
+ 407
@@ -2437,7 +2441,7 @@
Column group namesrc/app/core/config/config-fix.ts
- 393
+ 394
@@ -2446,11 +2450,11 @@
Column group namesrc/app/core/config/config-fix.ts
- 434
+ 435src/app/core/config/config-fix.ts
- 569
+ 570
@@ -2459,7 +2463,7 @@
Active children filter label - true casesrc/app/core/config/config-fix.ts
- 464
+ 465
@@ -2468,7 +2472,7 @@
Active children filter label - false casesrc/app/core/config/config-fix.ts
- 465
+ 466
@@ -2477,7 +2481,7 @@
Panel titlesrc/app/core/config/config-fix.ts
- 528
+ 529
@@ -2486,7 +2490,7 @@
Title inside a panelsrc/app/core/config/config-fix.ts
- 531
+ 532
@@ -2495,7 +2499,7 @@
Title inside a panelsrc/app/core/config/config-fix.ts
- 545
+ 546
@@ -2504,7 +2508,7 @@
Panel titlesrc/app/core/config/config-fix.ts
- 551
+ 552
@@ -2513,7 +2517,7 @@
Title inside a panelsrc/app/core/config/config-fix.ts
- 583
+ 584
@@ -2522,7 +2526,7 @@
Panel titlesrc/app/core/config/config-fix.ts
- 589
+ 590
@@ -2531,7 +2535,7 @@
Panel titlesrc/app/core/config/config-fix.ts
- 598
+ 599
@@ -2548,7 +2552,7 @@
src/app/core/config/config-fix.ts
- 620
+ 621
@@ -2557,7 +2561,7 @@
Panel titlesrc/app/core/config/config-fix.ts
- 656
+ 662
@@ -2566,7 +2570,7 @@
Panel titlesrc/app/core/config/config-fix.ts
- 685
+ 691
@@ -2575,7 +2579,7 @@
Name of a reportsrc/app/core/config/config-fix.ts
- 701
+ 707
@@ -2584,7 +2588,7 @@
Label of report querysrc/app/core/config/config-fix.ts
- 705
+ 711
@@ -2593,7 +2597,7 @@
Label of report querysrc/app/core/config/config-fix.ts
- 708
+ 714
@@ -2602,7 +2606,7 @@
Label of report querysrc/app/core/config/config-fix.ts
- 712
+ 718
@@ -2611,7 +2615,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 719
+ 725
@@ -2620,7 +2624,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 722
+ 728
@@ -2629,7 +2633,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 726
+ 732
@@ -2638,7 +2642,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 731
+ 737
@@ -2647,7 +2651,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 734
+ 740
@@ -2656,7 +2660,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 738
+ 744
@@ -2665,7 +2669,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 744
+ 750
@@ -2674,7 +2678,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 749
+ 755
@@ -2683,7 +2687,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 752
+ 758
@@ -2692,7 +2696,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 756
+ 762
@@ -2701,8 +2705,17 @@
Name of a reportsrc/app/core/config/config-fix.ts
- 766
+ 772
+
+
+
+
+ Bericht aller Aktivitäten
+
+ src/app/core/config/config-fix.ts
+ 789
+ Name of a report
@@ -2710,7 +2723,7 @@
Label for phone number of a childsrc/app/core/config/config-fix.ts
- 801
+ 824
@@ -2719,7 +2732,7 @@
Label for the guardians of a childsrc/app/core/config/config-fix.ts
- 808
+ 831
@@ -2728,7 +2741,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 815
+ 838
@@ -2737,7 +2750,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 822
+ 845
@@ -2746,7 +2759,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 830
+ 853
@@ -2755,7 +2768,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 838
+ 861
@@ -2764,7 +2777,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 846
+ 869
@@ -2773,7 +2786,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 854
+ 877
@@ -2782,7 +2795,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 861
+ 884
@@ -2791,7 +2804,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 868
+ 891
@@ -2800,7 +2813,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 875
+ 898
@@ -2809,7 +2822,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 891
+ 914
@@ -2818,7 +2831,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 892
+ 915
@@ -2827,7 +2840,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 900
+ 923
@@ -2836,7 +2849,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 901
+ 924
@@ -2845,7 +2858,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 909
+ 932
@@ -2854,7 +2867,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 910
+ 933
@@ -2863,7 +2876,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 918
+ 941
@@ -2872,7 +2885,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 919
+ 942
@@ -2881,7 +2894,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 927
+ 950
@@ -2890,7 +2903,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 928
+ 951
@@ -2899,7 +2912,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 936
+ 959
@@ -2908,7 +2921,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 937
+ 960
@@ -2917,7 +2930,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 945
+ 968
@@ -2926,7 +2939,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 946
+ 969
@@ -2935,7 +2948,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 954
+ 977
@@ -2944,7 +2957,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 955
+ 978
@@ -2953,7 +2966,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 963
+ 986
@@ -2962,7 +2975,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 964
+ 987
@@ -2971,7 +2984,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 972
+ 995
@@ -2980,7 +2993,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 973
+ 996
@@ -3002,7 +3015,7 @@
src/app/core/config/config-fix.ts
- 52
+ 53
@@ -3098,7 +3111,7 @@
src/app/core/config/config-fix.ts
- 343
+ 344
@@ -3127,11 +3140,11 @@
src/app/core/config/config-fix.ts
- 289
+ 290src/app/core/config/config-fix.ts
- 466
+ 467src/app/core/entity-components/entity-list/filter-generator.service.ts
@@ -3308,7 +3321,7 @@
src/app/core/config/config-fix.ts
- 348
+ 349
@@ -3379,7 +3392,7 @@
src/app/core/config/config-fix.ts
- 794
+ 817
@@ -3410,7 +3423,7 @@
src/app/core/config/config-fix.ts
- 287
+ 288
@@ -3534,7 +3547,7 @@
src/app/core/config/config-fix.ts
- 353
+ 354
@@ -3655,11 +3668,11 @@
src/app/core/config/config-fix.ts
- 358
+ 359src/app/core/config/config-fix.ts
- 475
+ 476
@@ -3734,11 +3747,15 @@
src/app/core/config/config-fix.ts
- 671
+ 677src/app/core/config/config-fix.ts
- 776
+ 782
+
+
+ src/app/core/config/config-fix.ts
+ 799
@@ -3815,7 +3832,7 @@
src/app/core/config/config-fix.ts
- 419
+ 420
@@ -3864,7 +3881,7 @@
src/app/core/config/config-fix.ts
- 383
+ 384
@@ -3890,7 +3907,11 @@
src/app/core/config/config-fix.ts
- 771
+ 777
+
+
+ src/app/core/config/config-fix.ts
+ 794
@@ -4284,7 +4305,7 @@
src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.ts
- 135
+ 138
@@ -4293,7 +4314,7 @@
Save button for formssrc/app/core/entity-components/entity-form/entity-form/entity-form.component.html
- 33
+ 42src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.html
@@ -4306,7 +4327,7 @@
Cancel button for formssrc/app/core/entity-components/entity-form/entity-form/entity-form.component.html
- 43
+ 52src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.html
@@ -4316,11 +4337,11 @@
Bearbeiten
+ Edit button for formssrc/app/core/entity-components/entity-form/entity-form/entity-form.component.html
- 57,61
+ 66
- Edit button for forms
@@ -4353,11 +4374,11 @@
Download als CSV
+ Download list contents as CSVsrc/app/core/entity-components/entity-list/entity-list.component.html
- 76,83
+ 77
- Download list contents as CSV
@@ -4441,7 +4462,7 @@
src/app/core/entity-components/entity-utils/dynamic-form-components/edit-text/edit-text.component.html
- 16
+ 10
@@ -4512,20 +4533,20 @@
Änderungen speichern?
+ Save changes headersrc/app/core/form-dialog/form-dialog.service.ts
- 58
+ 59
- Save changes headerSollen die Änderungen an diesem Eintrag gespeichert werden?
+ Save changes messagesrc/app/core/form-dialog/form-dialog.service.ts
- 59
+ 60
- Save changes message
@@ -4566,7 +4587,7 @@
Neuste Änderungen konnten nicht geladen werden: src/app/core/latest-changes/latest-changes.service.ts
- 128
+ 129
@@ -4659,40 +4680,45 @@
Login button
-
-
- Sie können sich erst nach dem ersten Einloggen ohne Internet einloggen. Versuchen Sie es später erneut
+
+
+ Bitte stellen Sie sicher das Sie eine Internetverbindung haben und versuchen Sie es erneut.
+ LoginErrorsrc/app/core/session/login/login.component.ts
- 71
+ 70
-
-
- Benutzername oder Passwort inkorrekt!
- Das Problem kann auch auftreten, wenn Sie offline sind.
- Bitte verbinden Sie sich mit dem Internet, um die neusten Nutzerdaten zu synchronisieren.
+
+
+ Benutzername und/oder Password inkorrekt
+ LoginErrorsrc/app/core/session/login/login.component.ts
- 78,80
+ 75
-
-
- Benutzername oder Passwort falsch!
+
+
+ Ein unerwarteter Fehler ist aufgetreten.
+ Bitte laden Sie die Seite neu und versuchen Sie es erneut.
+ Wenn Sie weiterhin diese Fehlermeldung sehen, kontaktieren Sie bitte Ihren Systemadministrator.
+ src/app/core/session/login/login.component.ts
- 83
+ 84,87
+ LoginErrorIhr Passwort hat sich vor kurzem geändert. Bitte mit dem neuen Passwort versuchen!src/app/core/session/session-service/synced-session.service.ts
- 145
+ 131
@@ -4921,12 +4947,12 @@
124
-
-
- Passwort konnte nicht geändert werden. Bitte erneut versuchen. Wenn das Problem häufig aufttrit, kontaktieren Sie bitte den Aam Digital Support.
+
+
+ Passwort konnte nicht geändert werden: Please try again. If the problem persists contact Aam Digital support. src/app/core/user/user-account/user-account.component.html
- 127
+ 126,130
@@ -5002,7 +5028,7 @@
Anmeldedaten für den Cloud-Service erfolgreich gespeichtert.src/app/core/webdav/cloud-file-service-user-settings/cloud-file-service-user-settings.component.ts
- 81,80
+ 87
diff --git a/src/locale/messages.xlf b/src/locale/messages.xlf
index 2d35bc6057..e13f5bc8c3 100644
--- a/src/locale/messages.xlf
+++ b/src/locale/messages.xlf
@@ -203,7 +203,11 @@
src/app/core/config/config-fix.ts
- 771
+ 777
+
+
+ src/app/core/config/config-fix.ts
+ 794Events of an attendance
@@ -645,11 +649,11 @@
src/app/core/config/config-fix.ts
- 358
+ 359src/app/core/config/config-fix.ts
- 475
+ 476
@@ -715,11 +719,15 @@
src/app/core/config/config-fix.ts
- 671
+ 677
+
+
+ src/app/core/config/config-fix.ts
+ 782src/app/core/config/config-fix.ts
- 776
+ 799Label for the participants of a recurring activity
@@ -780,11 +788,11 @@
src/app/core/config/config-fix.ts
- 289
+ 290src/app/core/config/config-fix.ts
- 466
+ 467src/app/core/entity-components/entity-list/filter-generator.service.ts
@@ -811,7 +819,7 @@
src/app/core/config/config-fix.ts
- 620
+ 621Child status
@@ -827,7 +835,7 @@
src/app/core/config/config-fix.ts
- 125
+ 126center
@@ -839,7 +847,7 @@
src/app/core/config/config-fix.ts
- 129
+ 130center
@@ -851,7 +859,7 @@
src/app/core/config/config-fix.ts
- 133
+ 134center
@@ -935,7 +943,7 @@
src/app/core/config/config-fix.ts
- 343
+ 344Label for the name of a child
@@ -1019,7 +1027,7 @@
src/app/core/config/config-fix.ts
- 419
+ 420Label for the status of a child
@@ -1067,7 +1075,7 @@
src/app/core/config/config-fix.ts
- 353
+ 354Label for the class of a relation
@@ -1401,7 +1409,7 @@
src/app/core/config/config-fix.ts
- 383
+ 384Table header, Short for Body Mass Index
@@ -1833,7 +1841,7 @@
src/app/core/config/config-fix.ts
- 27
+ 28Label for the children of a note
@@ -1853,7 +1861,7 @@
src/app/core/config/config-fix.ts
- 52
+ 53Label for the actual notes of a note
@@ -2171,7 +2179,7 @@
src/app/core/config/config-fix.ts
- 348
+ 349The age of a child
@@ -2235,7 +2243,7 @@
src/app/core/config/config-fix.ts
- 794
+ 817Label for the address of a school
@@ -2263,7 +2271,7 @@
src/app/core/config/config-fix.ts
- 287
+ 288Label whether school is private
@@ -2631,8 +2639,10 @@
131,130
-
-
+
+
src/app/core/admin/admin/admin.component.ts132,130
@@ -2642,49 +2652,49 @@
src/app/core/admin/admin/admin.component.ts
- 148,147
+ 146,145src/app/core/admin/admin/admin.component.ts
- 170,169
+ 168,167src/app/core/admin/admin/admin.component.ts
- 171,169
+ 169,167src/app/core/admin/admin/admin.component.ts
- 184,183
+ 182,181src/app/core/admin/admin/admin.component.ts
- 204,203
+ 202,201src/app/core/admin/admin/admin.component.ts
- 205,203
+ 203,201src/app/core/admin/admin/admin.component.ts
- 218,217
+ 216,215
@@ -2784,7 +2794,7 @@
src/app/core/config/config-fix.ts
- 22
+ 23Menu item
@@ -2792,7 +2802,7 @@
src/app/core/config/config-fix.ts
- 32
+ 33Menu item
@@ -2800,11 +2810,11 @@
src/app/core/config/config-fix.ts
- 37
+ 38src/app/core/config/config-fix.ts
- 642
+ 643Menu item
@@ -2812,7 +2822,7 @@
src/app/core/config/config-fix.ts
- 42
+ 43Record attendance menu itemMenu item
@@ -2821,7 +2831,7 @@
src/app/core/config/config-fix.ts
- 47
+ 48Menu item
@@ -2829,7 +2839,7 @@
src/app/core/config/config-fix.ts
- 57
+ 58Menu item
@@ -2837,7 +2847,7 @@
src/app/core/config/config-fix.ts
- 62
+ 63Menu item
@@ -2845,7 +2855,7 @@
src/app/core/config/config-fix.ts
- 67
+ 68Menu item
@@ -2853,7 +2863,7 @@
src/app/core/config/config-fix.ts
- 72
+ 73Menu item
@@ -2861,7 +2871,7 @@
src/app/core/config/config-fix.ts
- 77
+ 78Menu item
@@ -2869,7 +2879,7 @@
src/app/core/config/config-fix.ts
- 99
+ 100Document status
@@ -2877,7 +2887,7 @@
src/app/core/config/config-fix.ts
- 103
+ 104Document status
@@ -2885,7 +2895,7 @@
src/app/core/config/config-fix.ts
- 107
+ 108Document status
@@ -2893,7 +2903,7 @@
src/app/core/config/config-fix.ts
- 111
+ 112Document status
@@ -2901,7 +2911,7 @@
src/app/core/config/config-fix.ts
- 115
+ 116Document status
@@ -2909,7 +2919,7 @@
src/app/core/config/config-fix.ts
- 119
+ 120Document status
@@ -2917,7 +2927,7 @@
src/app/core/config/config-fix.ts
- 147
+ 148record attendance shortcutDashboard shortcut widget
@@ -2926,7 +2936,7 @@
src/app/core/config/config-fix.ts
- 171
+ 172Attendance week dashboard widget label
@@ -2934,7 +2944,7 @@
src/app/core/config/config-fix.ts
- 178
+ 179Attendance week dashboard widget label
@@ -2942,11 +2952,11 @@
src/app/core/config/config-fix.ts
- 196
+ 197src/app/core/config/config-fix.ts
- 560
+ 561Title for notes overview
@@ -2954,11 +2964,11 @@
src/app/core/config/config-fix.ts
- 206
+ 207src/app/core/config/config-fix.ts
- 210
+ 211Translated name of default column group
@@ -2966,19 +2976,19 @@
src/app/core/config/config-fix.ts
- 207
+ 208src/app/core/config/config-fix.ts
- 220
+ 221src/app/core/config/config-fix.ts
- 390
+ 391src/app/core/config/config-fix.ts
- 449
+ 450Translated name of mobile column group
@@ -2986,7 +2996,7 @@
src/app/core/config/config-fix.ts
- 261
+ 262Filename of markdown help page (make sure the filename you enter as a translation actually exists on the server!)
@@ -2994,7 +3004,7 @@
src/app/core/config/config-fix.ts
- 273
+ 274Title of schools overview
@@ -3002,7 +3012,7 @@
src/app/core/config/config-fix.ts
- 288
+ 289Label for private schools filter - false case
@@ -3010,11 +3020,11 @@
src/app/core/config/config-fix.ts
- 300
+ 301src/app/core/config/config-fix.ts
- 488
+ 489Panel title
@@ -3022,7 +3032,7 @@
src/app/core/config/config-fix.ts
- 324
+ 325Panel title
@@ -3030,7 +3040,7 @@
src/app/core/config/config-fix.ts
- 339
+ 340Title children overview
@@ -3038,7 +3048,7 @@
src/app/core/config/config-fix.ts
- 365
+ 366Column label for school attendance of child
@@ -3046,7 +3056,7 @@
src/app/core/config/config-fix.ts
- 374
+ 375Column label for coaching attendance of child
@@ -3054,11 +3064,11 @@
src/app/core/config/config-fix.ts
- 389
+ 390src/app/core/config/config-fix.ts
- 406
+ 407Translated name of default column group
@@ -3066,7 +3076,7 @@
src/app/core/config/config-fix.ts
- 393
+ 394Column group name
@@ -3074,11 +3084,11 @@
src/app/core/config/config-fix.ts
- 434
+ 435src/app/core/config/config-fix.ts
- 569
+ 570Column group name
@@ -3086,7 +3096,7 @@
src/app/core/config/config-fix.ts
- 464
+ 465Active children filter label - true case
@@ -3094,7 +3104,7 @@
src/app/core/config/config-fix.ts
- 465
+ 466Active children filter label - false case
@@ -3102,7 +3112,7 @@
src/app/core/config/config-fix.ts
- 528
+ 529Panel title
@@ -3110,7 +3120,7 @@
src/app/core/config/config-fix.ts
- 531
+ 532Title inside a panel
@@ -3118,7 +3128,7 @@
src/app/core/config/config-fix.ts
- 545
+ 546Title inside a panel
@@ -3126,7 +3136,7 @@
src/app/core/config/config-fix.ts
- 551
+ 552Panel title
@@ -3134,7 +3144,7 @@
src/app/core/config/config-fix.ts
- 583
+ 584Title inside a panel
@@ -3142,7 +3152,7 @@
src/app/core/config/config-fix.ts
- 589
+ 590Panel title
@@ -3150,7 +3160,7 @@
src/app/core/config/config-fix.ts
- 598
+ 599Panel title
@@ -3158,7 +3168,7 @@
src/app/core/config/config-fix.ts
- 656
+ 662Panel title
@@ -3166,7 +3176,7 @@
src/app/core/config/config-fix.ts
- 685
+ 691Panel title
@@ -3174,7 +3184,7 @@
src/app/core/config/config-fix.ts
- 701
+ 707Name of a report
@@ -3182,7 +3192,7 @@
src/app/core/config/config-fix.ts
- 705
+ 711Label of report query
@@ -3190,7 +3200,7 @@
src/app/core/config/config-fix.ts
- 708
+ 714Label of report query
@@ -3198,7 +3208,7 @@
src/app/core/config/config-fix.ts
- 712
+ 718Label of report query
@@ -3206,7 +3216,7 @@
src/app/core/config/config-fix.ts
- 719
+ 725Label for report query
@@ -3214,7 +3224,7 @@
src/app/core/config/config-fix.ts
- 722
+ 728Label for report query
@@ -3222,7 +3232,7 @@
src/app/core/config/config-fix.ts
- 726
+ 732Label for report query
@@ -3230,7 +3240,7 @@
src/app/core/config/config-fix.ts
- 731
+ 737Label for report query
@@ -3238,7 +3248,7 @@
src/app/core/config/config-fix.ts
- 734
+ 740Label for report query
@@ -3246,7 +3256,7 @@
src/app/core/config/config-fix.ts
- 738
+ 744Label for report query
@@ -3254,7 +3264,7 @@
src/app/core/config/config-fix.ts
- 744
+ 750Label for report query
@@ -3262,7 +3272,7 @@
src/app/core/config/config-fix.ts
- 749
+ 755Label for report query
@@ -3270,7 +3280,7 @@
src/app/core/config/config-fix.ts
- 752
+ 758Label for report query
@@ -3278,7 +3288,7 @@
src/app/core/config/config-fix.ts
- 756
+ 762Label for report query
@@ -3286,7 +3296,15 @@
src/app/core/config/config-fix.ts
- 766
+ 772
+
+ Name of a report
+
+
+
+
+ src/app/core/config/config-fix.ts
+ 789Name of a report
@@ -3294,7 +3312,7 @@
src/app/core/config/config-fix.ts
- 801
+ 824Label for phone number of a child
@@ -3302,7 +3320,7 @@
src/app/core/config/config-fix.ts
- 808
+ 831Label for the guardians of a child
@@ -3310,7 +3328,7 @@
src/app/core/config/config-fix.ts
- 815
+ 838Label for a child attribute
@@ -3318,7 +3336,7 @@
src/app/core/config/config-fix.ts
- 822
+ 845Label for a child attribute
@@ -3326,7 +3344,7 @@
src/app/core/config/config-fix.ts
- 830
+ 853Label for a child attribute
@@ -3334,7 +3352,7 @@
src/app/core/config/config-fix.ts
- 838
+ 861Label for a child attribute
@@ -3342,7 +3360,7 @@
src/app/core/config/config-fix.ts
- 846
+ 869Label for a child attribute
@@ -3350,7 +3368,7 @@
src/app/core/config/config-fix.ts
- 854
+ 877Label for a child attribute
@@ -3358,7 +3376,7 @@
src/app/core/config/config-fix.ts
- 861
+ 884Label for a child attribute
@@ -3366,7 +3384,7 @@
src/app/core/config/config-fix.ts
- 868
+ 891Label for a child attribute
@@ -3374,7 +3392,7 @@
src/app/core/config/config-fix.ts
- 875
+ 898Label for a child attribute
@@ -3382,7 +3400,7 @@
src/app/core/config/config-fix.ts
- 891
+ 914Label for a child attribute
@@ -3390,7 +3408,7 @@
src/app/core/config/config-fix.ts
- 892
+ 915Description for a child attribute
@@ -3398,7 +3416,7 @@
src/app/core/config/config-fix.ts
- 900
+ 923Label for a child attribute
@@ -3406,7 +3424,7 @@
src/app/core/config/config-fix.ts
- 901
+ 924Description for a child attribute
@@ -3414,7 +3432,7 @@
src/app/core/config/config-fix.ts
- 909
+ 932Label for a child attribute
@@ -3422,7 +3440,7 @@
src/app/core/config/config-fix.ts
- 910
+ 933Description for a child attribute
@@ -3430,7 +3448,7 @@
src/app/core/config/config-fix.ts
- 918
+ 941Label for a child attribute
@@ -3438,7 +3456,7 @@
src/app/core/config/config-fix.ts
- 919
+ 942Description for a child attribute
@@ -3446,7 +3464,7 @@
src/app/core/config/config-fix.ts
- 927
+ 950Label for a child attribute
@@ -3454,7 +3472,7 @@
src/app/core/config/config-fix.ts
- 928
+ 951Description for a child attribute
@@ -3462,7 +3480,7 @@
src/app/core/config/config-fix.ts
- 936
+ 959Label for a child attribute
@@ -3470,7 +3488,7 @@
src/app/core/config/config-fix.ts
- 937
+ 960Description for a child attribute
@@ -3478,7 +3496,7 @@
src/app/core/config/config-fix.ts
- 945
+ 968Label for a child attribute
@@ -3486,7 +3504,7 @@
src/app/core/config/config-fix.ts
- 946
+ 969Description for a child attribute
@@ -3494,7 +3512,7 @@
src/app/core/config/config-fix.ts
- 954
+ 977Label for a child attribute
@@ -3502,7 +3520,7 @@
src/app/core/config/config-fix.ts
- 955
+ 978Description for a child attribute
@@ -3510,7 +3528,7 @@
src/app/core/config/config-fix.ts
- 963
+ 986Label for a child attribute
@@ -3518,7 +3536,7 @@
src/app/core/config/config-fix.ts
- 964
+ 987Description for a child attribute
@@ -3526,7 +3544,7 @@
src/app/core/config/config-fix.ts
- 972
+ 995Label for a child attribute
@@ -3534,7 +3552,7 @@
src/app/core/config/config-fix.ts
- 973
+ 996Description for a child attribute
@@ -3823,7 +3841,7 @@
src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.ts
- 135,134
+ 138,137Deleted Entity information
@@ -3831,7 +3849,7 @@
src/app/core/entity-components/entity-form/entity-form/entity-form.component.html
- 33,39
+ 42,48src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.html
@@ -3843,7 +3861,7 @@
src/app/core/entity-components/entity-form/entity-form/entity-form.component.html
- 43,48
+ 52,57src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.html
@@ -3855,7 +3873,7 @@
src/app/core/entity-components/entity-form/entity-form/entity-form.component.html
- 57,61
+ 66,70Edit button for forms
@@ -3880,7 +3898,7 @@
src/app/core/entity-components/entity-list/entity-list.component.html
- 58,62
+ 58,61Add New Button
@@ -3888,7 +3906,7 @@
src/app/core/entity-components/entity-list/entity-list.component.html
- 76,83
+ 77,84Download list contents as CSV
@@ -3965,7 +3983,7 @@
src/app/core/entity-components/entity-utils/dynamic-form-components/edit-text/edit-text.component.html
- 16,17
+ 10,11Error message for any input
@@ -4030,7 +4048,7 @@
src/app/core/form-dialog/form-dialog.service.ts
- 58
+ 59Save changes header
@@ -4038,7 +4056,7 @@
src/app/core/form-dialog/form-dialog.service.ts
- 59
+ 60Save changes message
@@ -4076,7 +4094,7 @@
src/app/core/latest-changes/latest-changes.service.ts
- 128
+ 129
@@ -4130,34 +4148,38 @@
Login button
-
-
+
+
src/app/core/session/login/login.component.ts
- 71
+ 70
+ LoginError
-
-
+
+
src/app/core/session/login/login.component.ts
- 78,80
+ 75
+ LoginError
-
-
+
+
src/app/core/session/login/login.component.ts
- 83
+ 84,87
+ LoginErrorsrc/app/core/session/session-service/synced-session.service.ts
- 145,144
+ 131,130
@@ -4403,11 +4425,11 @@
124,125
-
-
+
+
src/app/core/user/user-account/user-account.component.html
- 127,130
+ 126,130
@@ -4474,7 +4496,7 @@
src/app/core/webdav/cloud-file-service-user-settings/cloud-file-service-user-settings.component.ts
- 81,80
+ 87,86
From dce708197cd566649522224051814711f117e8c7 Mon Sep 17 00:00:00 2001
From: Simon <33730997+TheSlimvReal@users.noreply.github.com>
Date: Tue, 17 Aug 2021 19:43:32 +0300
Subject: [PATCH 19/34] docs: updated authentication concept (#948)
---
.../concepts/session-system.md | 214 +++++++-----------
doc/images/login-flow-new-user.svg | 3 +
doc/images/session-uml.svg | 3 +
3 files changed, 90 insertions(+), 130 deletions(-)
create mode 100644 doc/images/login-flow-new-user.svg
create mode 100644 doc/images/session-uml.svg
diff --git a/doc/compodoc_sources/concepts/session-system.md b/doc/compodoc_sources/concepts/session-system.md
index 8f8e7d302f..03df05fdb2 100644
--- a/doc/compodoc_sources/concepts/session-system.md
+++ b/doc/compodoc_sources/concepts/session-system.md
@@ -4,143 +4,97 @@ This document aims at describing the architecture of the offline-first session h
+- [Components](#components)
- [State](#state)
-- [Login Flow](#login-flow)
-- [Class Diagrams](#class-diagrams)
-- [Sequence Diagrams for Login](#sequence-diagrams-for-login)
- - [Usual Flows](#usual-flows)
- - [Correct Password](#correct-password)
- - [Wrong Password](#wrong-password)
- - [Offline Flows](#offline-flows)
- - [Correct Password](#correct-password-1)
- - [Wrong Password](#wrong-password-1)
- - [Password Changed](#password-changed)
- - [Use old password](#use-old-password)
- - [Use new password](#use-new-password)
- - [Sync Failed Flows](#sync-failed-flows)
- - [Local Login succeeded](#local-login-succeeded)
- - [Local Login failed](#local-login-failed)
- - [Initial Sync Flows](#initial-sync-flows)
- - [Online with correct password](#online-with-correct-password)
- - [Online with wrong password](#online-with-wrong-password)
- - [Offline](#offline)
- - [Other sync failures](#other-sync-failures)
+- [Database User](#database-user)
+ - [Local User](#local-user)
+- [Session Services](#session-services)
+ - [Local Session](#local-session)
+ - [Remote Session](#remote-session)
+ - [Synced Session](#synced-session)
-There are some modules involved in Session Handling:
+## Components
-- **Database**: Contains an abstraction over database with a common interface, the implementation of that interface for PouchDB (`PouchDatabase`), and another implementation providing mock data (`MockDatabase`).
-- **Entity**: Contains an EntityMapper to be used with the database abstraction. Other modules may choose to implement the abstract class `Entity` and add annotations of the `EntitySchemaService` in order to be saved in the database.
-- **User**: Users are one such entity, providing additional methods to check its hashed password stored in the database.
-- **Session**: Contains the services holding everything together: `SessionService` provides an abstract class specifying the interface of an actual implementation (its not an interface, as interfaces will be optimized by the TypeScript compiler and can't be used for injection - which is necessary in our case.) There are also two such implementations: `MockSessionService` implements a local session backed by a `MockDatabase` to be used during development and unit tests, `SyncedSessionService` the production version with sync backed by `PouchDatabase`.
+The following diagram show which components are involved in managing the session.
+![class diagram](../../images/session-uml.svg).
-## State
-
-Lets talk about state for a second. With an offline-first synced session, there is lots of state involved needing to be synced. The following diagrams provide an overview on state and transitions:
-
-![State and State Transitions](../../images/session_state.png)
-
-- **LoginState**: Central state of the application: Is the user logged in or not? One can go directly from `loginFailed` to `loggedIn` by just entering the correct credentials at the UI after a failed attempt.
-- **ConnectionState**: ConnectionState describes the connection to the remote database. `disconnected` describes that no connection shall be established, i.e. before login and after logout. The remote database may reject a login (i.e. due to wrong credentials), causing state `rejected`. Additionally, the connection state may switch between `offline` and `connected`, when the user's browser goes offline or online after connection was established at least once.
-- **SyncState**: SyncState describes the state of database synchronization. Initially, that state is `unsynced`. The synchronization started after a successful connection to the remote database may change that state.
-
-To keep track of those states, a helper class was implemented. `StateEnum` is the (TypeScript-)Enumeration containing the states that are described by a given `StateHandler`.
-
-![StateHandler Helper](../../images/state_handler.png)
-
-LoginState depends primarily on the local database, as a login must be possible in case the user is offline. ConnectionState depends only on the remote database. While SyncState is conceptually somewhere _between_ the two, it is also associated with the local database, as the local login depends on the synchronization, when there is no database available (i.e. when the application is first started in a fresh browser).
-
-## Login Flow
-
-_Note: Please refer to the code in the `SyncedSessionService` for details - there are lots of comments to make clear, what is happening when. Feel free to also take a look at the sequence-diagrams below that capture the flow in various scenarios_
-
-- Login is attempted at both the local and the remote database in parallel. The result of the local login is returned as promise.
-- The local login will wait for the first initial synchronization of the local database in case it was empty, so users are available for offline login.
-- The login at the remote database will return the ConnectionState as a promise. The following situations are possible:
- 1. The user may be `offline`. In this case, the remote login will be queued for retry at a later point in time. If the local login went through, this won't affect the user's ability to work with the application. If the local database is empty (and the local login is therefor waiting for a first login), the SyncedSessionService will set the SyncState to `failed` so the local login won't be pending forever, but can be aborted by the local session.
- 2. The remote database may connect successfully (state `connected`), in which case the SyncedSessionService will start a synchronization. If the local database was empty, the local login waits until this synchronization is finished. After a successful sync, a liveSync will be queued for starting at the next tick (not immediately, as listeners to the EventChangedStream may not be notified due to the way browsers handle promise execution in Microtasks).
- 3. When the remote database rejects connection (state `rejected`), but the local login was successful, we have inconsistent authentication due to changed passwords. In this case, the login will be failed by the SyncedSessionService. Keep in mind that the user has been able to work with his old password until the rejection from the remote database is present.
-
-## Class Diagrams
-
-The local session depends on the user entity being loaded from the database. Doing this the standard way with the EntityMapperService would result in a circular dependency, which is why that specific part of code must be duplicated in the LocalSession.
-
-The following diagram shows most classes and services in the session. The `MockSessionService` and the abstract base class `SessionService`, as well as the `EntitySchemaService` used in the `LocalSession` to construct the the `User` are omitted for layout-reasons. The `databaseProvider` is created by the Session module and depends on the `SyncedSessionService` (or `MockSessionService`, depending on app configuration)-
-
-![class diagrams](../../images/session_classes.png)
-
-
-## Sequence Diagrams for Login
-
-### Usual Flows
-Local database is present, remote password was not changed.
-
-#### Correct Password
-
-![](../../images/new-session-flows/normal__right_pwd.svg)
-
-#### Wrong Password
-
-![](../../images/new-session-flows/normal__wrong_pwd.svg)
-
-### Offline Flows
-Local Database is present, but we are offline
+There are three implementations of the `SessionService` interface: [LocalSession](#local-session), [RemoteSession](#remote-session) and the [SyncedSession](#synced-session).
+They are described in more detail in the [SessionServices](#session-services) section.
-#### Correct Password
-
-![](../../images/new-session-flows/offline__right_pwd.svg)
-
-#### Wrong Password
-
-![](../../images/new-session-flows/offline__wrong_pwd.svg)
-
-We must retry with wait here, as we might be in a situation where the remote password changed and we should actually be able to log in. See these flows for details.
-
-### Password Changed
-Local Database is present, but we changed the password and password state is inconsistent between local and remote.
-
-#### Use old password
-Works locally but not on the remote.
-
-![](../../images/new-session-flows/pwd_changed__old_pwd.svg)
-
-#### Use new password
-Works on the remote but not locally.
-
-![](../../images/new-session-flows/pwd_changed__new_pwd.svg)
-
-### Sync Failed Flows
-So the remote session connected, but for some reason other than being offline the sync fails. I don't know how, but this might happen.
-
-#### Local Login succeeded
-Easiest case. Just start the liveSync and hope everything works out eventually. There should be some sync-indicator listening to the sync state to make the user aware that something is going wrong in the background.
-
-![](../../images/new-session-flows/sync_failed__local_login_success.svg)
-
-#### Local Login failed
-This is most probably a changed password case. However, as the sync failed, we cannot log the user in locally, so we have to keep the login failed. We also don't start a liveSync here, as it confuses the hell out of the UI to be not logged in but have a running (and intermittently failing) liveSync here. We might want to revisit this behavior, though.
-
-![](../../images/new-session-flows/sync_failed__local_login_failure.svg)
-
-### Initial Sync Flows
-The local database is initial. We must wait for a first sync before we can log anyone in.
-
-#### Online with correct password
-
-![](../../images/new-session-flows/initial__online_correct_pwd.svg)
-
-#### Online with wrong password
-
-![](../../images/new-session-flows/initial__online_wrong_pwd.svg)
-
-#### Offline
-We can't have the local login pending for too long. We also don't want the login explicitly failed (resulting in wrong password messages), so we just switch back to logged off.
-
-![](../../images/new-session-flows/initial__offline.svg)
+## State
-#### Other sync failures
-We don't know what to do in this case. We can't have the local login pending forever. We also don't want the login explicitly failed (resulting in wrong password messages), so we just switch back to logged off.
+Lets talk about state for a second. With an offline-first synced session, there is lots of state involved needing to be synced. The following enums are used to define the possible states.
+
+- **LoginState** At the beginning or after logout the state is `LOGGED_OUT`. After successful login, the state changes to `LOGGED_IN`. Entering wrong credentials will result in `LOGIN_FAILED`. If login is not possible at the moment (due to bad or no internet) the state is `UNAVAILABLE`.
+- **SyncState** At the beginning or without internet the state is `UNSYNCED`. Once the user logs in the state changes to `STARTED`. If no problems occur, the state will change to `COMPLETED`, once all data is synced. If an error occurs the state changes to `FAILED`.
+
+## Database User
+The `DatabaseUser` interface mirrors parts of the user documents as it is used by CouchDB (see [here](https://docs.couchdb.org/en/stable/intro/security.html?highlight=_users#users-documents)).
+The most important properties are the `name` which is equivalent to the username and the `roles` which is an array of roles the user has.
+The `name` can be used to assign e.g. notes to a user or find the `User` entity in the database.
+The roles are used to check whether the user has permissions to visit certain pages or edit certain entities.
+
+### Local User
+The `LocalUser` interface is used by the [LocalSession](#local-session) to store user information in the local storage.
+Additionally to the fields of the `DatabaseUser` interface it holds the encrypted password together with information how the password is encrypted.
+This can be used to verify the password later on.
+
+## Session Services
+The `SessionService` interface provides methods for `login`, `logout` and user access.
+These methods need to be implemented to create a working session.
+
+### Local Session
+The `LocalSession` manages a session without internet. It can be used for demo purposes or when no network connection is available.
+The session loads `LocalUser` objects from the local storage and uses the encrypted password to validate login credentials.
+It provides an additional method to save `DatabaseUser` objects together with the password to the local storage.
+If the password matches, it returns `LoginState.LOGGED_IN`.
+If the username matches an existing username but the password is wrong, it returns `LoginState.LOGIN_FAILED`.
+If the username does not match any saved users, it returns `LoginState.UNAVAILABLE`.
+
+### Remote Session
+The `RemoteSession` directly authenticates against a CouchDB instance.
+It uses the [_session](https://docs.couchdb.org/en/stable/api/server/authn.html?highlight=session#cookie-authentication) endpoint of the CouchDB API for cookie-authentication.
+Once successfully logged in, a cookie is stored and sent with any further request to CouchDB.
+This cookie is valid for 10 minutes and renewed by CouchDB if regular requests occur.
+If the password matches, it returns `LoginState.LOGGED_IN`.
+If CouchDB returns are `401` error (unauthorized), it returns `LoginState.LOGIN_FAILED`.
+If the requests fails with an error message other thatn `401`, it returns `LoginState.UNAVAILABLE`.
+
+### Synced Session
+The `SyncedSession` combines the `LocalSession` and the `RemoteSession`.
+It starts the login workflow of both the `RemoteSession` and the `LocalSession` at the same time.
+It first only waits for the `LocalSession` because this is faster and also works without internet.
+Only if the local login fails the `SyncedSession` waits for the remote login.
+If the remote login succeeds, the returned user object is saved through the `LocalSession` to allow a local login next time.
+
+The following table shows all possible return values of the local and the remote session and the handling of the synced session.
+The error message is shown in the `LoginComponent`
+
+| Remote Login | Local Login | Synced Login | Error Message |
+| --- | --- | --- | --- |
+| LOGGED_IN | LOGGED_IN | login + sync | - |
+| LOGGED_IN | LOGIN_FAILED | sync + login | - |
+| LOGGED_IN | UNAVAILABLE | sync + login | - |
+| LOGIN_FAILED | LOGGED_IN | login -> logout | - |
+| LOGIN_FAILED | LOGIN_FAILED | LOGIN_FAILED | Username and/or password incorrect |
+| LOGIN_FAILED | UNAVAILABLE | LOGIN_FAILED | Username and/or password incorrect |
+| UNAVAILABLE | LOGGED_IN | login + retry | - |
+| UNAVAILABLE | LOGIN_FAILED | LOGIN_FAILED | Username and/or password incorrect |
+| UNAVAILABLE | UNAVAILABLE | UNAVAILABLE | Please connect to the internet and try again |
+
+To illustrate this table, the following flow-diagram shows what happens in the case `RemoteLogin: LOGGED_IN` and `LocalLogin: LoginFailed`.
+This case happens when the password of a user has been changed on the server, but not locally and the users logs in with the new password.
+
+![class diagram](../../images/login-flow-new-user.svg).
+
+The flow starts by the user entering a username and a password in the `LoginComponent`.
+First the user credentials are validated against the local session.
+This fails possible because the saved password does not match the user-entered one.
+Then the synced session waits for the remote login to finish.
+In this case the remote login succeeds and the `DatabaseUser` object can be retrieved from the remote session.
+The synced session then stores the `DatabaseUser` in the local session and after that tries to log in against the local session.
+Now that the `DatabaseUser` has been saved with the new password the local log in succeeds and returns `LoginState.LOGGED_IN`.
-![](../../images/new-session-flows/initial__sync_failed.svg)
diff --git a/doc/images/login-flow-new-user.svg b/doc/images/login-flow-new-user.svg
new file mode 100644
index 0000000000..b3fe0304cf
--- /dev/null
+++ b/doc/images/login-flow-new-user.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/doc/images/session-uml.svg b/doc/images/session-uml.svg
new file mode 100644
index 0000000000..2990eb416a
--- /dev/null
+++ b/doc/images/session-uml.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
From 10334434be4900f8106ea456697ef5daafa6cb98 Mon Sep 17 00:00:00 2001
From: Simon <33730997+TheSlimvReal@users.noreply.github.com>
Date: Tue, 17 Aug 2021 20:00:26 +0300
Subject: [PATCH 20/34] docs: added documentation for the infrastructure of Aam
Digital (#949)
---
build/Dockerfile | 2 +-
.../concepts/infrastructure.md | 42 +++++++++++++++++++
doc/compodoc_sources/summary.json | 4 ++
doc/images/CICD-Flow.svg | 3 ++
4 files changed, 50 insertions(+), 1 deletion(-)
create mode 100644 doc/compodoc_sources/concepts/infrastructure.md
create mode 100644 doc/images/CICD-Flow.svg
diff --git a/build/Dockerfile b/build/Dockerfile
index 6a94431dc4..5d531d8101 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -2,7 +2,7 @@
# To use it only Docker needs to be installed locally
# Run the following commands from the root folder to build, run and kill the application
# >> docker build -f build/Dockerfile -t aam/digital:latest .
-# >> docker run -p=80:80 aam/digital:latest aam-digital
+# >> docker run -p=80:80 aam/digital:latest
# >> docker kill aam-digital
FROM node:15.11.0-alpine3.13 as builder
WORKDIR /app
diff --git a/doc/compodoc_sources/concepts/infrastructure.md b/doc/compodoc_sources/concepts/infrastructure.md
new file mode 100644
index 0000000000..7b9ec7e820
--- /dev/null
+++ b/doc/compodoc_sources/concepts/infrastructure.md
@@ -0,0 +1,42 @@
+# Infrastructure
+Aam Digital is an Angular application and can be deployed on a simple webserver.
+To do so we are using Docker and some other tools.
+
+The following diagram show the actions that are performed when new code changes are published.
+
+![CI/CD Flow](../../images/CICD-Flow.svg)
+
+## Dockerfile
+The Dockerfile can be found in [/build/Dockerfile](https://github.com/Aam-Digital/ndb-core/blob/master/build/Dockerfile).
+It provides a stable environment where the application is build, tested and packaged.
+It consists of two stages.
+The first stage is build on top of a `node` image and builds the application for production usage and optionally runs tests and uploads the test coverage.
+The second stage is build on top of a `nginx` image and only copies the files from the previous stage, which are necessary for deploying the application.
+This process makes the final image as small as possible.
+`nginx` is used to run a lightweight webserver.
+The configuration for `nginx` can be found in [/build/default.conf](https://github.com/Aam-Digital/ndb-core/blob/master/build/default.conf).
+
+## Pull Requests
+Whenever a new pull request is created, GitHub creates a new app on Heroku and posts the link to this app in the PR.
+For each new commit of a PR, GitHub then builds the Docker image for this branch.
+This includes building the application in production mode, linting the sourcecode and running the tests.
+If all these stages pass, GitHub will upload the newly created image to Heroku.
+Once this is done, a version of Aam Digital with the new changes included can be visited through the posted link.
+
+## Master Updates
+After approving a PR and merging it into the master, semantic release automatically creates a new tag for this change.
+For each new tag a tagged Docker image is uploaded to [DockerHub](https://hub.docker.com/r/aamdigital/ndb-server).
+
+## Deploying Aam Digital
+The Docker image from DockerHub can then be downloaded and run via Docker using the following command:
+> docker pull aamdigital/ndb-server && docker run -p=80:80 aamdigital/ndb-server
+
+However, this will only run Aam Digital in demo-mode.
+To run Aam Digital with a real database, a new `config.json` file has to be mounted into the image.
+This should have a structure equal to `config.default.json` ([/src/assets/config.default.json](https://github.com/Aam-Digital/ndb-core/blob/master/src/assets/config.default.json)).
+It holds the information about where to find the remote database, what the name of the app is and in which mode the app is being run.
+
+The [ndb-setup](https://github.com/Aam-Digital/ndb-setup) project provides a clear workflow for how to deploy a new instance of Aam Digital and CouchDB.
+It extends the Docker image with a `docker-compose.yml` file which handles all the dependencies.
+For further information checkout the [README](https://github.com/Aam-Digital/ndb-setup/blob/master/README.md).
+
diff --git a/doc/compodoc_sources/summary.json b/doc/compodoc_sources/summary.json
index 5e48207cb1..96eb113951 100644
--- a/doc/compodoc_sources/summary.json
+++ b/doc/compodoc_sources/summary.json
@@ -122,6 +122,10 @@
{
"title": "Documentation Structure",
"file": "concepts/documentation-structure.md"
+ },
+ {
+ "title": "Infrastructure",
+ "file": "concepts/infrastructure.md"
}
]
},
diff --git a/doc/images/CICD-Flow.svg b/doc/images/CICD-Flow.svg
new file mode 100644
index 0000000000..c9c10b23ec
--- /dev/null
+++ b/doc/images/CICD-Flow.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
From ae1b6176e7f5cc381b34b73e88c0169125ff343c Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Thu, 19 Aug 2021 17:05:22 +0200
Subject: [PATCH 21/34] feat(*): allow exports from multiple related entities
in one row and multiple rows for one entity (#938)
Co-authored-by: Simon
---
doc/compodoc_sources/how-to-guides/exports.md | 99 +++++++++
doc/compodoc_sources/summary.json | 4 +
.../activity-list.component.spec.ts | 4 +-
.../children-list.component.spec.ts | 4 +-
.../notes-manager.component.spec.ts | 4 +-
.../schools-list.component.spec.ts | 4 +-
.../admin/services/backup.service.spec.ts | 8 +-
src/app/core/config/config-fix.ts | 6 +-
.../entity-list/entity-list.component.spec.ts | 4 +-
.../export-data-button.component.spec.ts | 21 +-
.../export-data-button.component.ts | 11 +-
.../export-service/export-column-config.ts | 19 +-
.../export-service/export.service.spec.ts | 204 ++++++++++++++++--
.../export/export-service/export.service.ts | 168 ++++++++++++---
src/app/features/reporting/query.service.ts | 46 ++--
.../reporting/reporting.service.spec.ts | 21 +-
.../features/reporting/reporting.service.ts | 14 +-
.../reporting/reporting.component.spec.ts | 11 +-
.../reporting/reporting.component.ts | 5 +-
19 files changed, 545 insertions(+), 112 deletions(-)
create mode 100644 doc/compodoc_sources/how-to-guides/exports.md
diff --git a/doc/compodoc_sources/how-to-guides/exports.md b/doc/compodoc_sources/how-to-guides/exports.md
new file mode 100644
index 0000000000..ca200b93e8
--- /dev/null
+++ b/doc/compodoc_sources/how-to-guides/exports.md
@@ -0,0 +1,99 @@
+# Exports
+
+The list views like `/child`, `/school`, `/note` ,... have a button to export data.
+Clicking this button will create a downloadable `.csv` file.
+The format of this file can be adjusted through the configuration for the list view.
+
+## Config Format
+
+The configuration of the export format is part of the configuration of the [EntityListComponent](../../interfaces/EntityListConfig.html).
+
+E.g.
+```json
+ "view:child": {
+ "component": "ChildrenList",
+ "config": {
+ "exportConfig": [
+ { "label": "Child", "query": ".name" },
+ {
+ "query": ":getRelated(ChildSchoolRelation, childId)",
+ "subQueries": [
+ { "label": "School Name", "query": ".schoolId:toEntities(School).name" },
+ { "label": "From", "query": ".start" },
+ { "label": "To", "query": ".end" }
+ ]
+
+ }
+ ],
+ "title": "Children List",
+ "columns": [...],
+ "columnGroups": {...},
+ "filters": [...]
+ }
+ },
+```
+
+## Configuring a Export
+
+The structure of the exports is according to the [ExportColumnConfig](../../interfaces/ExportColumnConfig.html).
+
+The `label` property is optional and provides a title for this column.
+If nothing is provided the query (without dots) is used (e.g. for `"query": ".name"` the label will be `name`).
+
+The `query` has to be a valid [JSON-Query](https://github.com/auditassistant/json-query#queries).
+The query will be run on each entity of the list page that is visited (e.g. on each child).
+For further information about the query-syntax read the [Reports Guide](reports.md) which uses the same query language.
+
+The `subQueries` is optional and expects an array of `ExportColumnConfigs`.
+The queries of the `subQueries` will be run for each result of the parent query.
+In the example above, this would mean that for each `ChildSchoolRelation` the name of the school, the from- and the to-date will be exported.
+If `subQueries` is defined, each object of the parent query will lead to one row in the report.
+In the example above this would mean if there are `n` children and each of them has `m` child-school-relations then the final report will have `n*m` rows and the columns `Child`, `School Name`, `From`, `To` (`Child` from the first query and the others from the sub-queries).
+In case `subQueries` is defined, the result of the parent query will not be visible in the export.
+If the results of the parent query is wanted in the report, a simple sub-query can be added that returns all values of the parent query (`query: "[*]"`).
+
+## Example Output
+
+Using the config from above with the following data:
+
+```typescript
+const child1 = {
+ _id: "Child:1",
+ name: "Peter"
+}
+const child2 = {
+ _id: "Child:2",
+ name: "Anna"
+}
+const relation1 = {
+ _id: "ChildSchoolRelation:1",
+ schoolId: "1",
+ childId: "1",
+ start: "01/01/2020",
+ end: "01/01/2021"
+}
+const relation2 = {
+ _id: "ChildSchoolRelation:2",
+ schoolId: "1",
+ childId: "1",
+ start: "01/01/2021",
+}
+const relation3 = {
+ _id: "ChildSchoolRelation:3",
+ schoolId: "1",
+ childId: "1",
+ start: "01/01/2021",
+}
+const school = {
+ _id: "School:1",
+ name: "High School"
+}
+```
+
+Would create a `.csv` file according to the following table:
+
+| Child | School Name | From | To |
+| --- | --- | --- | --- |
+| Peter | High School | 01/01/2020 | 01/01/2021 |
+| Peter | High School | 01/01/2021 | |
+| Anna | High School | 01/01/2021 | |
diff --git a/doc/compodoc_sources/summary.json b/doc/compodoc_sources/summary.json
index 96eb113951..999a21f374 100644
--- a/doc/compodoc_sources/summary.json
+++ b/doc/compodoc_sources/summary.json
@@ -81,6 +81,10 @@
"title": "Create a Report",
"file": "how-to-guides/reports.md"
},
+ {
+ "title": "Format Data Export",
+ "file": "how-to-guides/exports.md"
+ },
{
"title": "Build Localizable Components",
"file": "how-to-guides/build-localizable-components.md"
diff --git a/src/app/child-dev-project/attendance/activity-list/activity-list.component.spec.ts b/src/app/child-dev-project/attendance/activity-list/activity-list.component.spec.ts
index bb35b0c600..a84c139460 100644
--- a/src/app/child-dev-project/attendance/activity-list/activity-list.component.spec.ts
+++ b/src/app/child-dev-project/attendance/activity-list/activity-list.component.spec.ts
@@ -7,11 +7,11 @@ import { ActivatedRoute } from "@angular/router";
import { of } from "rxjs";
import { AttendanceModule } from "../attendance.module";
import { SessionService } from "../../../core/session/session-service/session.service";
-import { BackupService } from "../../../core/admin/services/backup.service";
import { Angulartics2Module } from "angulartics2";
import { EntityListConfig } from "../../../core/entity-components/entity-list/EntityListConfig";
import { User } from "../../../core/user/user";
import { mockEntityMapper } from "../../../core/entity/mock-entity-mapper-service";
+import { ExportService } from "../../../core/export/export-service/export.service";
describe("ActivityListComponent", () => {
let component: ActivityListComponent;
@@ -36,7 +36,7 @@ describe("ActivityListComponent", () => {
provide: SessionService,
useValue: { getCurrentUser: () => new User() },
},
- { provide: BackupService, useValue: {} },
+ { provide: ExportService, useValue: {} },
{
provide: ActivatedRoute,
useValue: {
diff --git a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts
index 0d563e7c6f..9bb8fc0876 100644
--- a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts
+++ b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts
@@ -23,7 +23,7 @@ import { User } from "../../../core/user/user";
import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { School } from "../../schools/model/school";
import { LoggingService } from "../../../core/logging/logging.service";
-import { BackupService } from "../../../core/admin/services/backup.service";
+import { ExportService } from "../../../core/export/export-service/export.service";
describe("ChildrenListComponent", () => {
let component: ChildrenListComponent;
@@ -117,7 +117,7 @@ describe("ChildrenListComponent", () => {
provide: LoggingService,
useValue: jasmine.createSpyObj(["warn"]),
},
- { provide: BackupService, useValue: {} },
+ { provide: ExportService, useValue: {} },
],
}).compileComponents();
})
diff --git a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts
index af8c4ce902..5fe7388f77 100644
--- a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts
+++ b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts
@@ -30,8 +30,8 @@ import { By } from "@angular/platform-browser";
import { EntityListComponent } from "../../../core/entity-components/entity-list/entity-list.component";
import { EventNote } from "../../attendance/model/event-note";
import { BehaviorSubject } from "rxjs";
-import { BackupService } from "../../../core/admin/services/backup.service";
import { UpdatedEntity } from "../../../core/entity/model/entity-update";
+import { ExportService } from "../../../core/export/export-service/export.service";
describe("NotesManagerComponent", () => {
let component: NotesManagerComponent;
@@ -122,7 +122,7 @@ describe("NotesManagerComponent", () => {
{ provide: FormDialogService, useValue: dialogMock },
{ provide: ActivatedRoute, useValue: routeMock },
{ provide: ConfigService, useValue: mockConfigService },
- { provide: BackupService, useValue: {} },
+ { provide: ExportService, useValue: {} },
],
}).compileComponents();
});
diff --git a/src/app/child-dev-project/schools/schools-list/schools-list.component.spec.ts b/src/app/child-dev-project/schools/schools-list/schools-list.component.spec.ts
index d3cd0c6486..4c9b690aa9 100644
--- a/src/app/child-dev-project/schools/schools-list/schools-list.component.spec.ts
+++ b/src/app/child-dev-project/schools/schools-list/schools-list.component.spec.ts
@@ -17,7 +17,7 @@ import { School } from "../model/school";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { EntityListConfig } from "../../../core/entity-components/entity-list/EntityListConfig";
import { User } from "../../../core/user/user";
-import { BackupService } from "../../../core/admin/services/backup.service";
+import { ExportService } from "../../../core/export/export-service/export.service";
describe("SchoolsListComponent", () => {
let component: SchoolsListComponent;
@@ -67,7 +67,7 @@ describe("SchoolsListComponent", () => {
{ provide: ActivatedRoute, useValue: routeMock },
{ provide: SessionService, useValue: mockSessionService },
{ provide: EntityMapperService, useValue: mockEntityMapper },
- { provide: BackupService, useValue: {} },
+ { provide: ExportService, useValue: {} },
],
}).compileComponents();
})
diff --git a/src/app/core/admin/services/backup.service.spec.ts b/src/app/core/admin/services/backup.service.spec.ts
index e8ec8af91e..4dccfa5d8c 100644
--- a/src/app/core/admin/services/backup.service.spec.ts
+++ b/src/app/core/admin/services/backup.service.spec.ts
@@ -4,6 +4,7 @@ import { BackupService } from "./backup.service";
import { Database } from "../../database/database";
import { PouchDatabase } from "../../database/pouch-database";
import { ExportService } from "../../export/export-service/export.service";
+import { QueryService } from "../../../features/reporting/query.service";
describe("BackupService", () => {
let db: PouchDatabase;
@@ -12,7 +13,12 @@ describe("BackupService", () => {
beforeEach(() => {
db = PouchDatabase.createWithInMemoryDB();
TestBed.configureTestingModule({
- providers: [BackupService, { provide: Database, useValue: db }],
+ providers: [
+ BackupService,
+ ExportService,
+ { provide: QueryService, useValue: { queryData: () => [] } },
+ { provide: Database, useValue: db },
+ ],
});
service = TestBed.inject(BackupService);
diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts
index 9e552dc78e..ffcdd138ef 100644
--- a/src/app/core/config/config-fix.ts
+++ b/src/app/core/config/config-fix.ts
@@ -647,9 +647,9 @@ export const defaultJsonConfig = {
"assignedTo"
],
"exportConfig": [
- { label: "Title", key: "title" },
- { label: "Type", key: "type" },
- { label: "Assigned users", key: "assignedTo" }
+ { label: "Title", query: "title" },
+ { label: "Type", query: "type" },
+ { label: "Assigned users", query: "assignedTo" }
]
}
},
diff --git a/src/app/core/entity-components/entity-list/entity-list.component.spec.ts b/src/app/core/entity-components/entity-list/entity-list.component.spec.ts
index 87497a7b35..421c3e4127 100644
--- a/src/app/core/entity-components/entity-list/entity-list.component.spec.ts
+++ b/src/app/core/entity-components/entity-list/entity-list.component.spec.ts
@@ -13,7 +13,6 @@ import { ChildrenListComponent } from "../../../child-dev-project/children/child
import { Child } from "../../../child-dev-project/children/model/child";
import { ConfigService } from "../../config/config.service";
import { LoggingService } from "../../logging/logging.service";
-import { BackupService } from "../../admin/services/backup.service";
import { EntityListModule } from "./entity-list.module";
import { Angulartics2Module } from "angulartics2";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
@@ -21,6 +20,7 @@ import { DatabaseField } from "../../entity/database-field.decorator";
import { ReactiveFormsModule } from "@angular/forms";
import { AttendanceService } from "../../../child-dev-project/attendance/attendance.service";
import { ExportModule } from "../../export/export.module";
+import { ExportService } from "../../export/export-service/export.service";
describe("EntityListComponent", () => {
let component: EntityListComponent;
@@ -116,7 +116,7 @@ describe("EntityListComponent", () => {
{ provide: ConfigService, useValue: mockConfigService },
{ provide: EntityMapperService, useValue: mockEntityMapper },
{ provide: LoggingService, useValue: mockLoggingService },
- { provide: BackupService, useValue: {} },
+ { provide: ExportService, useValue: {} },
{ provide: EntitySchemaService, useValue: mockEntitySchemaService },
{ provide: AttendanceService, useValue: mockAttendanceService },
],
diff --git a/src/app/core/export/export-data-button/export-data-button.component.spec.ts b/src/app/core/export/export-data-button/export-data-button.component.spec.ts
index 717573cb4a..c6b87efb56 100644
--- a/src/app/core/export/export-data-button/export-data-button.component.spec.ts
+++ b/src/app/core/export/export-data-button/export-data-button.component.spec.ts
@@ -1,18 +1,24 @@
-import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
-import { BackupService } from "../../admin/services/backup.service";
+import {
+ ComponentFixture,
+ fakeAsync,
+ TestBed,
+ tick,
+ waitForAsync,
+} from "@angular/core/testing";
+import { ExportService } from "../export-service/export.service";
import { ExportDataButtonComponent } from "./export-data-button.component";
describe("ExportDataComponent", () => {
let component: ExportDataButtonComponent;
let fixture: ComponentFixture;
- let mockBackupService: jasmine.SpyObj;
+ let mockExportService: jasmine.SpyObj;
beforeEach(
waitForAsync(() => {
- mockBackupService = jasmine.createSpyObj(["createJson", "createCsv"]);
+ mockExportService = jasmine.createSpyObj(["createJson", "createCsv"]);
TestBed.configureTestingModule({
declarations: [ExportDataButtonComponent],
- providers: [{ provide: BackupService, useValue: mockBackupService }],
+ providers: [{ provide: ExportService, useValue: mockExportService }],
}).compileComponents();
})
);
@@ -27,7 +33,7 @@ describe("ExportDataComponent", () => {
expect(component).toBeTruthy();
});
- it("opens download link when pressing button", () => {
+ it("opens download link when pressing button", fakeAsync(() => {
const link = document.createElement("a");
const clickSpy = spyOn(link, "click");
// Needed to later reset the createElement function, otherwise subsequent calls result in an error
@@ -39,8 +45,9 @@ describe("ExportDataComponent", () => {
expect(clickSpy.calls.count()).toBe(0);
button.click();
+ tick();
expect(clickSpy.calls.count()).toBe(1);
// reset createElement otherwise results in: 'an Error was thrown after all'
document.createElement = oldCreateElement;
- });
+ }));
});
diff --git a/src/app/core/export/export-data-button/export-data-button.component.ts b/src/app/core/export/export-data-button/export-data-button.component.ts
index 716c9f9c43..af8d80d16d 100644
--- a/src/app/core/export/export-data-button/export-data-button.component.ts
+++ b/src/app/core/export/export-data-button/export-data-button.component.ts
@@ -34,8 +34,8 @@ export class ExportDataButtonComponent {
/**
* Trigger the download of the export file.
*/
- exportData() {
- const blobData = this.getFormattedBlobData();
+ async exportData() {
+ const blobData = await this.getFormattedBlobData();
const link = this.createDownloadLink(blobData);
link.click();
}
@@ -50,14 +50,17 @@ export class ExportDataButtonComponent {
return link;
}
- private getFormattedBlobData(): Blob {
+ private async getFormattedBlobData(): Promise {
let result = "";
switch (this.format.toLowerCase()) {
case "json":
result = this.exportService.createJson(this.data); // TODO: support exportConfig for json format
return new Blob([result], { type: "application/json" });
case "csv":
- result = this.exportService.createCsv(this.data, this.exportConfig);
+ result = await this.exportService.createCsv(
+ this.data,
+ this.exportConfig
+ );
return new Blob([result], { type: "text/csv" });
default:
console.warn("Not supported format:", this.format);
diff --git a/src/app/core/export/export-service/export-column-config.ts b/src/app/core/export/export-service/export-column-config.ts
index 86abc7c53a..edc439d239 100644
--- a/src/app/core/export/export-service/export-column-config.ts
+++ b/src/app/core/export/export-service/export-column-config.ts
@@ -5,10 +5,23 @@ export interface ExportColumnConfig {
/**
* label shown in the header row.
*
- * If not specified, key is used.
+ * If not specified, query is used.
*/
label?: string;
- /** property key to access the value for this column from the object to be exported */
- key: string;
+ /** The query to access the value for this column from the object to be exported */
+ query: string;
+
+ /**
+ * One or more sub queries to expand one column into multiple columns and/or rows.
+ *
+ * The queries in the subQueries are executed based on the result(s) from the parent query where:
+ * each object in the parent query result will lead to its own row in the final export (extending one object into multiple export rows);
+ * each query in the subQueries will lead to one (or recursively more) columns in the export rows.
+ *
+ * e.g. `{ query: ".participants:toEntities(Child)", subQueries: [ {query: "name"}, {query: "phone"} ] }`
+ * => parent query (not output in export): [{..child1}, {..child2}]
+ * => overall result: two export rows: [{ name: "child1", phone: "123"}, {name: "child2", phone: "567"}]
+ */
+ subQueries?: ExportColumnConfig[];
}
diff --git a/src/app/core/export/export-service/export.service.spec.ts b/src/app/core/export/export-service/export.service.spec.ts
index ed14ff51ce..85c046b3df 100644
--- a/src/app/core/export/export-service/export.service.spec.ts
+++ b/src/app/core/export/export-service/export.service.spec.ts
@@ -5,18 +5,48 @@ import { ConfigurableEnumValue } from "../../configurable-enum/configurable-enum
import { DatabaseField } from "../../entity/database-field.decorator";
import { DatabaseEntity } from "../../entity/database-entity.decorator";
import { Entity } from "../../entity/model/entity";
+import { QueryService } from "../../../features/reporting/query.service";
+import { EntityMapperService } from "../../entity/entity-mapper.service";
+import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
+import { ChildrenService } from "../../../child-dev-project/children/children.service";
+import { AttendanceService } from "../../../child-dev-project/attendance/attendance.service";
+import { DatabaseIndexingService } from "../../entity/database-indexing/database-indexing.service";
+import { Database } from "../../database/database";
+import { PouchDatabase } from "../../database/pouch-database";
+import { Note } from "../../../child-dev-project/notes/model/note";
+import { Child } from "../../../child-dev-project/children/model/child";
+import { School } from "../../../child-dev-project/schools/model/school";
+import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation";
+import { ExportColumnConfig } from "./export-column-config";
+import { defaultAttendanceStatusTypes } from "../../config/default-config/default-attendance-status-types";
describe("ExportService", () => {
let service: ExportService;
+ let db: PouchDatabase;
beforeEach(() => {
+ db = PouchDatabase.createWithInMemoryDB("export-service-tests");
+
TestBed.configureTestingModule({
- providers: [ExportService],
+ providers: [
+ ExportService,
+ QueryService,
+ EntityMapperService,
+ EntitySchemaService,
+ ChildrenService,
+ AttendanceService,
+ DatabaseIndexingService,
+ { provide: Database, useValue: db },
+ ],
});
service = TestBed.inject(ExportService);
});
+ afterEach(async () => {
+ await db.destroy();
+ });
+
it("should be created", () => {
expect(service).toBeTruthy();
});
@@ -50,21 +80,21 @@ describe("ExportService", () => {
expect(rows[0].split(ExportService.SEPARATOR_COL)).toHaveSize(3);
});
- it("should create a csv string correctly", () => {
- class TestClass {
- _id;
- _rev;
+ it("should create a csv string correctly", async () => {
+ class TestClass extends Entity {
propOne;
propTwo;
}
const test = new TestClass();
- test._id = 1;
- test._rev = 2;
+ test._id = "1";
+ test._rev = "2";
test.propOne = "first";
test.propTwo = "second";
const expected =
- '"_id","_rev","propOne","propTwo"\r\n"1","2","first","second"';
- const result = service.createCsv([test]);
+ '"_id","_rev","propOne","propTwo"' +
+ ExportService.SEPARATOR_ROW +
+ '"1","2","first","second"';
+ const result = await service.createCsv([test]);
expect(result).toEqual(expected);
});
@@ -81,9 +111,12 @@ describe("ExportService", () => {
const csvResult = await service.createCsv([testObject1, testObject2]);
- expect(csvResult).toBe(
- '"name","age","extra"\r\n"foo","12",\r\n"bar","15","true"'
- );
+ const resultRows = csvResult.split(ExportService.SEPARATOR_ROW);
+ expect(resultRows).toEqual([
+ '"name","age","extra"',
+ '"foo","12",""',
+ '"bar","15","true"',
+ ]);
});
it("should export only properties mentioned in config", async () => {
@@ -99,10 +132,11 @@ describe("ExportService", () => {
const csvResult = await service.createCsv(
[testObject1, testObject2],
- [{ label: "test name", key: "name" }]
+ [{ label: "test name", query: ".name" }]
);
- expect(csvResult).toBe('"test name"\r\n"foo"\r\n"bar"');
+ const resultRows = csvResult.split(ExportService.SEPARATOR_ROW);
+ expect(resultRows).toEqual(['"test name"', '"foo"', '"bar"']);
});
it("should transform object properties to their label for export", async () => {
@@ -129,9 +163,149 @@ describe("ExportService", () => {
const rows = csvExport.split(ExportService.SEPARATOR_ROW);
expect(rows).toHaveSize(1 + 1); // includes 1 header line
const columnValues = rows[1].split(ExportService.SEPARATOR_COL);
- expect(columnValues).toHaveSize(3 + 1);
+ expect(columnValues).toHaveSize(3 + 1); // Properties + _id
expect(columnValues).toContain('"' + testEnumValue.label + '"');
expect(columnValues).toContain(new Date(testDate).toISOString());
expect(columnValues).toContain('"true"');
});
+
+ it("should load fields from related entity for joint export", async () => {
+ const child1 = await createChildInDB("John");
+ const child2 = await createChildInDB("Jane");
+ const school1 = await createSchoolInDB("School with student", [child1]);
+ const school2 = await createSchoolInDB("School without student", []);
+ const school3 = await createSchoolInDB("School with multiple students", [
+ child1,
+ child2,
+ ]);
+
+ const query1 =
+ ":getRelated(ChildSchoolRelation, schoolId).childId:toEntities(Child).name";
+ const exportConfig: ExportColumnConfig[] = [
+ { label: "school name", query: ".name" },
+ { label: "child name", query: query1 },
+ ];
+ const result1 = await service.createCsv(
+ [school1, school2, school3],
+ exportConfig
+ );
+ const resultRows = result1.split(ExportService.SEPARATOR_ROW);
+ expect(resultRows).toEqual([
+ `"${exportConfig[0].label}","${exportConfig[1].label}"`,
+ '"School with student","John"',
+ '"School without student",""',
+ jasmine.stringMatching(
+ // order of student names is somehow random "Jane,John" or "John,Jane"
+ /"School with multiple students","(Jane,John|John,Jane)"/
+ ),
+ ]);
+ });
+
+ it("should roll out export to one row for each related entity", async () => {
+ const child1 = await createChildInDB("John");
+ const child2 = await createChildInDB("Jane");
+ const child3 = await createChildInDB("Jack");
+ const noteA = await createNoteInDB("A", [child1, child2]);
+ const noteB = await createNoteInDB("B", [child1, child3]);
+
+ const exportConfig: ExportColumnConfig[] = [
+ { label: "note", query: ".subject" },
+ {
+ query: ".children:toEntities(Child)",
+ subQueries: [{ label: "participant", query: ".name" }],
+ },
+ ];
+ const result1 = await service.createCsv([noteA, noteB], exportConfig);
+ const resultRows = result1.split(ExportService.SEPARATOR_ROW);
+ expect(resultRows).toEqual([
+ `"${exportConfig[0].label}","${exportConfig[1].subQueries[0].label}"`,
+ '"A","John"',
+ '"A","Jane"',
+ '"B","John"',
+ '"B","Jack"',
+ ]);
+ });
+
+ it("should export attendance status for each note participant", async () => {
+ const child1 = await createChildInDB("present kid");
+ const child2 = await createChildInDB("absent kid");
+ const child3 = await createChildInDB("unknown kid");
+ const note = await createNoteInDB(
+ "Note 1",
+ [child1, child2, child3],
+ ["PRESENT", "ABSENT"]
+ );
+
+ const exportConfig: ExportColumnConfig[] = [
+ { label: "note", query: ".subject" },
+ {
+ query: ":getAttendanceArray",
+ subQueries: [
+ {
+ label: "participant",
+ query: ".participant:toEntities(Child).name",
+ },
+ {
+ label: "status",
+ query: ".status._status.id",
+ },
+ ],
+ },
+ ];
+
+ const result = await service.createCsv([note], exportConfig);
+
+ const resultRows = result.split(ExportService.SEPARATOR_ROW);
+ expect(resultRows).toEqual([
+ `"${exportConfig[0].label}","participant","status"`,
+ '"Note 1","present kid","PRESENT"',
+ '"Note 1","absent kid","ABSENT"',
+ '"Note 1","unknown kid",""',
+ ]);
+ });
+
+ async function createChildInDB(name: string): Promise {
+ const child = new Child();
+ child.name = name;
+ await db.put(child);
+ return child;
+ }
+
+ async function createNoteInDB(
+ subject: string,
+ children: Child[] = [],
+ attendanceStatus: string[] = []
+ ): Promise {
+ const note = new Note();
+ note.subject = subject;
+ note.children = children.map((child) => child.getId());
+
+ for (let i = 0; i < attendanceStatus.length; i++) {
+ note.getAttendance(
+ note.children[i]
+ ).status = defaultAttendanceStatusTypes.find(
+ (s) => s.id === attendanceStatus[i]
+ );
+ }
+ await db.put(note);
+ return note;
+ }
+
+ async function createSchoolInDB(
+ schoolName: string,
+ students: Child[] = []
+ ): Promise {
+ const school = new School();
+ school.name = schoolName;
+ await db.put(school);
+
+ for (const child of students) {
+ const childSchoolRel = new ChildSchoolRelation();
+ childSchoolRel.childId = child.getId();
+ childSchoolRel.schoolId = school.getId();
+ await db.put(childSchoolRel);
+ }
+
+ return school;
+ }
});
diff --git a/src/app/core/export/export-service/export.service.ts b/src/app/core/export/export-service/export.service.ts
index 71a59d5662..e695da1ff0 100644
--- a/src/app/core/export/export-service/export.service.ts
+++ b/src/app/core/export/export-service/export.service.ts
@@ -2,6 +2,7 @@ import { Injectable } from "@angular/core";
import { Papa } from "ngx-papaparse";
import { entityListSortingAccessor } from "../../entity-components/entity-subrecord/entity-subrecord/sorting-accessor";
import { ExportColumnConfig } from "./export-column-config";
+import { QueryService } from "../../../features/reporting/query.service";
/**
* Prepare data for export in csv format.
@@ -15,7 +16,7 @@ export class ExportService {
/** CSV column/field separator */
static readonly SEPARATOR_COL = ",";
- constructor(private papa: Papa) {}
+ constructor(private papa: Papa, private queryService: QueryService) {}
/**
* Creates a JSON string of the given data.
@@ -34,34 +35,151 @@ export class ExportService {
* @param config (Optional) config specifying which fields should be exported
* @returns string a valid CSV string of the input data
*/
- createCsv(data: any[], config?: ExportColumnConfig[]): string {
- const allFields = new Set();
- const exportableData = [];
-
- data.forEach((element: any) => {
- const exportableObj = {};
-
- const currentRowConfig =
- config ??
- Object.keys(element).map((key) => ({ key: key } as ExportColumnConfig));
- for (const columnConfig of currentRowConfig) {
- const label = columnConfig.label ?? columnConfig.key;
- const value = entityListSortingAccessor(element, columnConfig.key);
- if (value?.toString().match(/\[object.*\]/) !== null) {
- // skip object values that cannot be converted to a meaningful string
- continue;
- }
-
- exportableObj[label] = value;
- allFields.add(label);
- }
+ async createCsv(data: any[], config?: ExportColumnConfig[]): Promise {
+ if (!config) {
+ config = this.generateExportConfigFromData(data);
+ }
+
+ const flattenedExportRows: ExportRow[] = [];
+ for (const dataRow of data) {
+ const extendedExportableRows = await this.generateExportRows(
+ dataRow,
+ config
+ );
+ flattenedExportRows.push(...extendedExportableRows);
+ }
- exportableData.push(exportableObj);
+ // Apply entitySortingDataAccessor to transform values into human readable format
+ const readableExportRow = flattenedExportRows.map((row) => {
+ const readableRow = {};
+ Object.keys(row).forEach((key) => {
+ readableRow[key] = entityListSortingAccessor(row, key);
+ });
+ return readableRow;
});
return this.papa.unparse(
- { data: exportableData, fields: [...allFields] },
- { quotes: true, header: true }
+ { data: readableExportRow },
+ { quotes: true, header: true, newline: ExportService.SEPARATOR_ROW }
);
}
+
+ /**
+ * Infer a column export config from the given data.
+ * Includes all properties of the data objects,
+ * if different objects are in the data a config for the superset across all objects' properties is returned.
+ *
+ * @param data objects to be exported, each object can have different properties
+ * @private
+ */
+ private generateExportConfigFromData(data: Object[]): ExportColumnConfig[] {
+ const uniqueKeys = new Set();
+ data.forEach((obj) =>
+ Object.keys(obj).forEach((key) => uniqueKeys.add(key))
+ );
+
+ const columnConfigs: ExportColumnConfig[] = [];
+ uniqueKeys.forEach((key) => columnConfigs.push({ query: "." + key }));
+
+ return columnConfigs;
+ }
+
+ /**
+ * Generate one or more export row objects from the given data object and config.
+ * @param object A single data object to be exported as one or more export row objects
+ * @param config
+ * @returns array of one or more export row objects (as simple {key: value})
+ * @private
+ */
+ private async generateExportRows(
+ object: Object,
+ config: ExportColumnConfig[]
+ ): Promise {
+ let exportRows: ExportRow[] = [{}];
+ for (const exportColumnConfig of config) {
+ const partialExportObjects: ExportRow[] = await this.getExportRowsForColumn(
+ object,
+ exportColumnConfig
+ );
+
+ exportRows = this.mergePartialExportRows(
+ exportRows,
+ partialExportObjects
+ );
+ }
+ return exportRows;
+ }
+
+ /**
+ * Generate one or more (partial) export row objects from a single property of the data object
+ * @param object
+ * @param exportColumnConfig
+ * @private
+ */
+ private async getExportRowsForColumn(
+ object: Object,
+ exportColumnConfig: ExportColumnConfig
+ ): Promise {
+ const label =
+ exportColumnConfig.label ?? exportColumnConfig.query.replace(".", "");
+ const value = await this.getValueForQuery(exportColumnConfig, object);
+
+ if (!exportColumnConfig.subQueries) {
+ const result = {};
+ result[label] = value;
+ return [result];
+ } else {
+ const additionalRows: ExportRow[] = [];
+ for (const v of value) {
+ const addRows = await this.generateExportRows(
+ v,
+ exportColumnConfig.subQueries
+ );
+ additionalRows.push(...addRows);
+ }
+ return additionalRows;
+ }
+ }
+
+ private async getValueForQuery(
+ exportColumnConfig: ExportColumnConfig,
+ object: Object
+ ): Promise {
+ const value = await this.queryService.queryData(
+ exportColumnConfig.query,
+ null,
+ null,
+ [object]
+ );
+
+ if (!exportColumnConfig.subQueries && value.length === 1) {
+ // queryData() always returns an array, simple queries should be a direct value however
+ return value[0];
+ }
+ return value;
+ }
+
+ /**
+ * Combine two arrays of export row objects.
+ * Every additional row is merge with every row of the first array (combining properties),
+ * resulting in n*m export rows.
+ *
+ * @param exportRows
+ * @param additionalExportRows
+ * @private
+ */
+ private mergePartialExportRows(
+ exportRows: ExportRow[],
+ additionalExportRows: ExportRow[]
+ ): ExportRow[] {
+ const rowsOfRows: ExportRow[][] = additionalExportRows.map((addRow) =>
+ exportRows.map((row) => Object.assign({}, row, addRow))
+ );
+ // return flattened array
+ return rowsOfRows.reduce((acc, rowOfRows) => acc.concat(rowOfRows), []);
+ }
+}
+
+interface ExportRow {
+ [key: string]: any;
}
diff --git a/src/app/features/reporting/query.service.ts b/src/app/features/reporting/query.service.ts
index 3451e5ab5c..b651b528f6 100644
--- a/src/app/features/reporting/query.service.ts
+++ b/src/app/features/reporting/query.service.ts
@@ -9,6 +9,7 @@ import { EntityMapperService } from "../../core/entity/entity-mapper.service";
import { ChildSchoolRelation } from "../../child-dev-project/children/model/childSchoolRelation";
import { ChildrenService } from "../../child-dev-project/children/children.service";
import { AttendanceService } from "../../child-dev-project/attendance/attendance.service";
+import { EventAttendance } from "../../child-dev-project/attendance/model/event-attendance";
const jsonQuery = require("json-query");
@@ -41,13 +42,19 @@ export class QueryService {
query: string,
from: Date = null,
to: Date = null,
- data: any = this.entities
+ data?: any
): Promise {
- if (!data || (data === this.entities && from < this.dataAvailableFrom)) {
+ if (from === null) {
+ from = new Date(0);
+ }
+ if (from < this.dataAvailableFrom || !this.dataAvailableFrom) {
await this.loadData(from);
- this.dataAvailableFrom = from;
+ }
+
+ if (!data) {
data = this.entities;
}
+
return jsonQuery([query, from, to], {
data: data,
locals: {
@@ -60,12 +67,13 @@ export class QueryService {
filterByObjectAttribute: this.filterByObjectAttribute,
getIds: this.getIds,
getParticipantsWithAttendance: this.getParticipantsWithAttendance,
+ getAttendanceArray: this.getAttendanceArray,
addEntities: this.addEntities.bind(this),
},
}).value;
}
- private async loadData(from?: Date): Promise {
+ private async loadData(from: Date): Promise {
const entityClasses: [EntityConstructor, () => Promise][] = [
[Child, () => this.entityMapper.loadType(Child)],
[School, () => this.entityMapper.loadType(School)],
@@ -74,17 +82,10 @@ export class QueryService {
ChildSchoolRelation,
() => this.entityMapper.loadType(ChildSchoolRelation),
],
- [
- Note,
- () => this.childrenService.getNotesInTimespan(from ?? new Date(0)),
- ],
+ [Note, () => this.childrenService.getNotesInTimespan(from)],
[
EventNote,
- () =>
- this.attendanceService.getEventsOnDate(
- from ?? new Date(0),
- new Date()
- ),
+ () => this.attendanceService.getEventsOnDate(from, new Date()),
],
];
@@ -99,6 +100,8 @@ export class QueryService {
}
await Promise.all(dataPromises);
+
+ this.dataAvailableFrom = from;
}
private setEntities(
@@ -158,6 +161,10 @@ export class QueryService {
* @returns a list of entity objects
*/
toEntities(ids: string[], entityPrefix?: string): Entity[] {
+ if (!ids) {
+ return [];
+ }
+
if (entityPrefix) {
ids = this.addPrefix(ids, entityPrefix);
}
@@ -265,6 +272,19 @@ export class QueryService {
return attendedChildren;
}
+ getAttendanceArray(
+ events: Note[]
+ ): { participant: string; status: EventAttendance }[] {
+ return events
+ .map((e) =>
+ e.children.map((childId) => ({
+ participant: childId,
+ status: e.getAttendance(childId),
+ }))
+ )
+ .reduce((acc, val) => acc.concat(val), []);
+ }
+
/**
* Adds all entities of the given type to the input array
* @param entities the array before
diff --git a/src/app/features/reporting/reporting.service.spec.ts b/src/app/features/reporting/reporting.service.spec.ts
index 81f20ea596..a3e51e7f24 100644
--- a/src/app/features/reporting/reporting.service.spec.ts
+++ b/src/app/features/reporting/reporting.service.spec.ts
@@ -36,7 +36,6 @@ describe("ReportingService", () => {
{ label: "muslims", query: muslimsQuery },
],
};
- service.setAggregations([childDisaggregation]);
const baseData = [new School()];
mockQueryService.queryData.and.returnValues(
Promise.resolve(baseData),
@@ -44,7 +43,7 @@ describe("ReportingService", () => {
Promise.resolve([new School(), new School()])
);
- const report = await service.calculateReport();
+ const report = await service.calculateReport([childDisaggregation]);
expect(mockQueryService.queryData.calls.allArgs()).toEqual([
[baseQuery, undefined, undefined, undefined],
[christiansQuery, undefined, undefined, baseData],
@@ -68,9 +67,8 @@ describe("ReportingService", () => {
query: baseQueryString,
aggregations: [{ label: "tests", query: subjectQueryString }],
};
- service.setAggregations([disaggregation]);
- await service.calculateReport(firstDate, secondDate);
+ await service.calculateReport([disaggregation], firstDate, secondDate);
expect(mockQueryService.queryData.calls.allArgs()).toEqual([
[baseQueryString, firstDate, secondDate, undefined],
[subjectQueryString, firstDate, secondDate, undefined],
@@ -103,7 +101,6 @@ describe("ReportingService", () => {
{ label: "Normal aggregation", query: normalAggregation },
],
};
- service.setAggregations([aggregation]);
const baseData = [new School(), new School()];
const nestedData = [new ChildSchoolRelation()];
@@ -117,7 +114,7 @@ describe("ReportingService", () => {
return Promise.resolve([new School()]);
}
});
- const result = await service.calculateReport();
+ const result = await service.calculateReport([aggregation]);
expect(mockQueryService.queryData.calls.allArgs()).toEqual([
[baseQuery, undefined, undefined, undefined],
[nestedBaseQuery, undefined, undefined, baseData],
@@ -174,8 +171,7 @@ describe("ReportingService", () => {
label: "Total # of children",
};
- service.setAggregations([groupByAggregation]);
- const result = await service.calculateReport();
+ const result = await service.calculateReport([groupByAggregation]);
expect(result).toEqual([
{
@@ -221,8 +217,7 @@ describe("ReportingService", () => {
],
};
- service.setAggregations([groupByAggregation]);
- const result = await service.calculateReport();
+ const result = await service.calculateReport([groupByAggregation]);
expect(result).toEqual([
{
@@ -305,8 +300,7 @@ describe("ReportingService", () => {
groupBy: ["gender", "religion", "center"],
label: "Total # of children",
};
- service.setAggregations([groupByAggregation]);
- const result = await service.calculateReport();
+ const result = await service.calculateReport([groupByAggregation]);
expect(result).toEqual([
{
@@ -549,8 +543,7 @@ describe("ReportingService", () => {
},
],
};
- service.setAggregations([nestedGroupBy]);
- const result = await service.calculateReport();
+ const result = await service.calculateReport([nestedGroupBy]);
expect(result).toEqual([
{
diff --git a/src/app/features/reporting/reporting.service.ts b/src/app/features/reporting/reporting.service.ts
index 019dbf6077..2c8aa5065e 100644
--- a/src/app/features/reporting/reporting.service.ts
+++ b/src/app/features/reporting/reporting.service.ts
@@ -13,20 +13,19 @@ export interface Aggregation {
providedIn: "root",
})
export class ReportingService {
- private aggregations: Aggregation[] = [];
private fromDate: Date;
private toDate: Date;
constructor(private queryService: QueryService) {}
- public setAggregations(aggregations: Aggregation[]) {
- this.aggregations = aggregations;
- }
-
- public calculateReport(from?: Date, to?: Date): Promise {
+ public calculateReport(
+ aggregations: Aggregation[],
+ from?: Date,
+ to?: Date
+ ): Promise {
this.fromDate = from;
this.toDate = to;
- return this.calculateAggregations(this.aggregations);
+ return this.calculateAggregations(aggregations);
}
private async calculateAggregations(
@@ -43,6 +42,7 @@ export class ReportingService {
this.toDate,
data
);
+
if (aggregation.label) {
const newRow = {
header: {
diff --git a/src/app/features/reporting/reporting/reporting.component.spec.ts b/src/app/features/reporting/reporting/reporting.component.spec.ts
index e837a9aabd..0a8bb44a39 100644
--- a/src/app/features/reporting/reporting/reporting.component.spec.ts
+++ b/src/app/features/reporting/reporting/reporting.component.spec.ts
@@ -39,10 +39,7 @@ describe("ReportingComponent", () => {
};
beforeEach(async () => {
- mockReportingService = jasmine.createSpyObj([
- "setAggregations",
- "calculateReport",
- ]);
+ mockReportingService = jasmine.createSpyObj(["calculateReport"]);
mockReportingService.calculateReport.and.resolveTo([]);
await TestBed.configureTestingModule({
declarations: [ReportingComponent],
@@ -84,8 +81,10 @@ describe("ReportingComponent", () => {
tick();
expect(component.loading).toBeFalse();
- expect(mockReportingService.setAggregations).toHaveBeenCalledWith(
- testReport.aggregationDefinitions
+ expect(mockReportingService.calculateReport).toHaveBeenCalledWith(
+ testReport.aggregationDefinitions,
+ undefined,
+ jasmine.any(Date)
);
}));
diff --git a/src/app/features/reporting/reporting/reporting.component.ts b/src/app/features/reporting/reporting/reporting.component.ts
index 8f0415f52c..30c3ef15f0 100644
--- a/src/app/features/reporting/reporting/reporting.component.ts
+++ b/src/app/features/reporting/reporting/reporting.component.ts
@@ -45,14 +45,11 @@ export class ReportingComponent implements OnInit {
async calculateResults() {
this.loading = true;
- this.reportingService.setAggregations(
- this.selectedReport.aggregationDefinitions
- );
-
// Add one day because to date is exclusive
const dayAfterToDate = moment(this.toDate).add(1, "day").toDate();
this.results = await this.reportingService.calculateReport(
+ this.selectedReport.aggregationDefinitions,
this.fromDate,
dayAfterToDate
);
From 780d50457ec72589d74bdc9a7643d8e7865139f8 Mon Sep 17 00:00:00 2001
From: Simon <33730997+TheSlimvReal@users.noreply.github.com>
Date: Mon, 23 Aug 2021 12:45:43 +0300
Subject: [PATCH 22/34] fix: object unsubscribe errors
---
src/app/app.component.ts | 2 +-
.../children-bmi-dashboard.component.ts | 6 ++-
.../children-count-dashboard.component.ts | 4 +-
.../children-list/children-list.component.ts | 13 +++---
.../recent-attendance-blocks.component.ts | 45 ++++++++++---------
src/app/core/admin/admin/admin.component.ts | 33 +++++++++-----
.../entity-details.component.ts | 13 ++++--
.../entity-list/entity-list.component.ts | 37 ++++++++-------
.../entity-subrecord.component.ts | 37 ++++++++-------
.../form-dialog-wrapper.component.ts | 21 ++++++---
.../changelog/changelog.component.ts | 7 ++-
.../navigation/navigation.component.ts | 8 ++--
12 files changed, 140 insertions(+), 86 deletions(-)
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index efd81c283f..f1ad3a8aff 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -67,8 +67,8 @@ export class AppComponent implements OnInit {
.then(() => configService.loadConfig(entityMapper))
.then(() => router.navigate([], { relativeTo: this.activatedRoute }));
// These functions will be executed whenever a new config is available
- configService.configUpdates.subscribe(() => routerService.initRouting());
configService.configUpdates.subscribe(() => {
+ routerService.initRouting();
entityConfigService.addConfigAttributes(Child);
entityConfigService.addConfigAttributes(School);
entityConfigService.addConfigAttributes(RecurringActivity);
diff --git a/src/app/child-dev-project/children/children-bmi-dashboard/children-bmi-dashboard.component.ts b/src/app/child-dev-project/children/children-bmi-dashboard/children-bmi-dashboard.component.ts
index 651f7931e1..4a2cfb34dd 100644
--- a/src/app/child-dev-project/children/children-bmi-dashboard/children-bmi-dashboard.component.ts
+++ b/src/app/child-dev-project/children/children-bmi-dashboard/children-bmi-dashboard.component.ts
@@ -6,12 +6,14 @@ import { take } from "rxjs/operators";
import { ChildrenService } from "../children.service";
import { Child } from "../model/child";
import { WarningLevel } from "../../../core/entity/model/warning-level";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
interface BmiRow {
childId: string;
bmi: number;
}
+@UntilDestroy()
@Component({
selector: "app-children-bmi-dashboard",
templateUrl: "./children-bmi-dashboard.component.html",
@@ -30,7 +32,7 @@ export class ChildrenBmiDashboardComponent
ngOnInit(): void {
this.childrenService
.getChildren()
- .pipe(take(1))
+ .pipe(untilDestroyed(this), take(1))
.subscribe((results) => {
this.filterBMI(results);
});
@@ -44,7 +46,7 @@ export class ChildrenBmiDashboardComponent
children.forEach((child) => {
this.childrenService
.getHealthChecksOfChild(child.getId())
- .pipe()
+ .pipe(untilDestroyed(this))
.subscribe((results) => {
/** get latest HealthCheck */
if (results.length > 0) {
diff --git a/src/app/child-dev-project/children/children-count-dashboard/children-count-dashboard.component.ts b/src/app/child-dev-project/children/children-count-dashboard/children-count-dashboard.component.ts
index 53cd8eec46..45e229dde0 100644
--- a/src/app/child-dev-project/children/children-count-dashboard/children-count-dashboard.component.ts
+++ b/src/app/child-dev-project/children/children-count-dashboard/children-count-dashboard.component.ts
@@ -5,7 +5,9 @@ import { OnInitDynamicComponent } from "../../../core/view/dynamic-components/on
import { take } from "rxjs/operators";
import { ConfigurableEnumValue } from "../../../core/configurable-enum/configurable-enum.interface";
import { Child } from "../model/child";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+@UntilDestroy()
@Component({
selector: "app-children-count-dashboard",
templateUrl: "./children-count-dashboard.component.html",
@@ -37,7 +39,7 @@ export class ChildrenCountDashboardComponent
ngOnInit() {
this.childrenService
.getChildren()
- .pipe(take(1)) // only take the initial result, no need for updated details
+ .pipe(untilDestroyed(this), take(1)) // only take the initial result, no need for updated details
.subscribe((results) => {
this.updateCounts(results);
});
diff --git a/src/app/child-dev-project/children/children-list/children-list.component.ts b/src/app/child-dev-project/children/children-list/children-list.component.ts
index 59cf49a2bc..c35efd288b 100644
--- a/src/app/child-dev-project/children/children-list/children-list.component.ts
+++ b/src/app/child-dev-project/children/children-list/children-list.component.ts
@@ -1,7 +1,7 @@
import { Component, OnInit, ViewChild } from "@angular/core";
import { Child } from "../model/child";
import { ActivatedRoute, Router } from "@angular/router";
-import { UntilDestroy } from "@ngneat/until-destroy";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { ChildrenService } from "../children.service";
import { FilterSelectionOption } from "../../../core/filter/filter-selection/filter-selection";
import {
@@ -45,10 +45,13 @@ export class ChildrenListComponent implements OnInit {
this.route.data.subscribe(
(config: EntityListConfig) => (this.listConfig = config)
);
- this.childrenService.getChildren().subscribe((children) => {
- this.childrenList = children;
- this.addPrebuiltFilters();
- });
+ this.childrenService
+ .getChildren()
+ .pipe(untilDestroyed(this))
+ .subscribe((children) => {
+ this.childrenList = children;
+ this.addPrebuiltFilters();
+ });
}
routeTo(route: string) {
diff --git a/src/app/child-dev-project/children/children-list/recent-attendance-blocks/recent-attendance-blocks.component.ts b/src/app/child-dev-project/children/children-list/recent-attendance-blocks/recent-attendance-blocks.component.ts
index f8fe7ca17a..9a23307cc2 100644
--- a/src/app/child-dev-project/children/children-list/recent-attendance-blocks/recent-attendance-blocks.component.ts
+++ b/src/app/child-dev-project/children/children-list/recent-attendance-blocks/recent-attendance-blocks.component.ts
@@ -6,12 +6,14 @@ import { ViewPropertyConfig } from "../../../../core/entity-components/entity-li
import { ActivityAttendance } from "../../../attendance/model/activity-attendance";
import { AttendanceService } from "../../../attendance/attendance.service";
import moment from "moment";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
/**
* This component lists attendance blocks for a child for recent months filtered by institutions.
* The child object the institution needs to be provided.
* It also implements a flexible layout to display less attendance blocks on a smaller layout.
*/
+@UntilDestroy()
@Component({
selector: "app-recent-attendance-blocks",
template: `
@@ -33,27 +35,30 @@ export class RecentAttendanceBlocksComponent implements OnInitDynamicComponent {
private attendanceService: AttendanceService,
private media: MediaObserver
) {
- this.media.asObservable().subscribe((change: MediaChange[]) => {
- switch (change[0].mqAlias) {
- case "xs":
- case "sm": {
- this.maxAttendanceBlocks = 1;
- break;
+ this.media
+ .asObservable()
+ .pipe(untilDestroyed(this))
+ .subscribe((change: MediaChange[]) => {
+ switch (change[0].mqAlias) {
+ case "xs":
+ case "sm": {
+ this.maxAttendanceBlocks = 1;
+ break;
+ }
+ case "md": {
+ this.maxAttendanceBlocks = 2;
+ break;
+ }
+ case "lg": {
+ this.maxAttendanceBlocks = 3;
+ break;
+ }
+ case "xl": {
+ this.maxAttendanceBlocks = 6;
+ break;
+ }
}
- case "md": {
- this.maxAttendanceBlocks = 2;
- break;
- }
- case "lg": {
- this.maxAttendanceBlocks = 3;
- break;
- }
- case "xl": {
- this.maxAttendanceBlocks = 6;
- break;
- }
- }
- });
+ });
}
async onInitFromDynamicConfig(config: ViewPropertyConfig) {
diff --git a/src/app/core/admin/admin/admin.component.ts b/src/app/core/admin/admin/admin.component.ts
index 90422c7f54..cefa00081d 100644
--- a/src/app/core/admin/admin/admin.component.ts
+++ b/src/app/core/admin/admin/admin.component.ts
@@ -13,10 +13,12 @@ import { AttendanceMigrationService } from "../../../child-dev-project/attendanc
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 { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
/**
* Admin GUI giving administrative users different options/actions.
*/
+@UntilDestroy()
@Component({
selector: "app-admin",
templateUrl: "./admin.component.html",
@@ -149,10 +151,13 @@ export class AdminComponent implements OnInit {
duration: 8000,
}
);
- snackBarRef.onAction().subscribe(async () => {
- await this.backupService.clearDatabase();
- await this.backupService.importJson(restorePoint, true);
- });
+ snackBarRef
+ .onAction()
+ .pipe(untilDestroyed(this))
+ .subscribe(async () => {
+ await this.backupService.clearDatabase();
+ await this.backupService.importJson(restorePoint, true);
+ });
});
}
@@ -185,10 +190,13 @@ export class AdminComponent implements OnInit {
duration: 8000,
}
);
- snackBarRef.onAction().subscribe(async () => {
- await this.backupService.clearDatabase();
- await this.backupService.importJson(restorePoint, true);
- });
+ snackBarRef
+ .onAction()
+ .pipe(untilDestroyed(this))
+ .subscribe(async () => {
+ await this.backupService.clearDatabase();
+ await this.backupService.importJson(restorePoint, true);
+ });
});
}
@@ -219,9 +227,12 @@ export class AdminComponent implements OnInit {
duration: 8000,
}
);
- snackBarRef.onAction().subscribe(async () => {
- await this.backupService.importJson(restorePoint, true);
- });
+ snackBarRef
+ .onAction()
+ .pipe(untilDestroyed(this))
+ .subscribe(async () => {
+ await this.backupService.importJson(restorePoint, true);
+ });
});
}
}
diff --git a/src/app/core/entity-components/entity-details/entity-details.component.ts b/src/app/core/entity-components/entity-details/entity-details.component.ts
index 838c2c74d9..7b1a23c294 100644
--- a/src/app/core/entity-components/entity-details/entity-details.component.ts
+++ b/src/app/core/entity-components/entity-details/entity-details.component.ts
@@ -21,6 +21,7 @@ import {
} from "../../permissions/entity-permissions.service";
import { User } from "../../user/user";
import { Note } from "../../../child-dev-project/notes/model/note";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
export const ENTITY_MAP: Map> = new Map<
string,
@@ -41,6 +42,7 @@ export const ENTITY_MAP: Map> = new Map<
* Any component from the DYNAMIC_COMPONENT_MAP can be used as a subcomponent.
* The subcomponents will be provided with the Entity object and the creating new status, as well as it's static config.
*/
+@UntilDestroy()
@Component({
selector: "app-entity-details",
templateUrl: "./entity-details.component.html",
@@ -141,10 +143,13 @@ export class EntityDetailsComponent {
"Undo",
{ duration: 8000 }
);
- snackBarRef.onAction().subscribe(() => {
- this.entityMapperService.save(this.entity, true);
- this.router.navigate([currentUrl]);
- });
+ snackBarRef
+ .onAction()
+ .pipe(untilDestroyed(this))
+ .subscribe(() => {
+ this.entityMapperService.save(this.entity, true);
+ this.router.navigate([currentUrl]);
+ });
}
});
}
diff --git a/src/app/core/entity-components/entity-list/entity-list.component.ts b/src/app/core/entity-components/entity-list/entity-list.component.ts
index 6b1be6613f..36d69b0a91 100644
--- a/src/app/core/entity-components/entity-list/entity-list.component.ts
+++ b/src/app/core/entity-components/entity-list/entity-list.component.ts
@@ -24,6 +24,7 @@ import { EntitySubrecordComponent } from "../entity-subrecord/entity-subrecord/e
import { FilterGeneratorService } from "./filter-generator.service";
import { FilterComponentSettings } from "./filter-component.settings";
import { entityFilterPredicate } from "./filter-predicate";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
/**
* This component allows to create a full blown table with pagination, filtering, searching and grouping.
@@ -32,6 +33,7 @@ import { entityFilterPredicate } from "./filter-predicate";
* The columns can be any kind of component.
* The column components will be provided with the Entity object, the id for this column, as well as its static config.
*/
+@UntilDestroy()
@Component({
selector: "app-entity-list",
templateUrl: "./entity-list.component.html",
@@ -71,23 +73,26 @@ export class EntityListComponent
) {}
ngOnInit() {
- this.media.asObservable().subscribe((change: MediaChange[]) => {
- switch (change[0].mqAlias) {
- case "xs":
- case "sm": {
- this.displayColumnGroup(this.mobileColumnGroup);
- break;
+ this.media
+ .asObservable()
+ .pipe(untilDestroyed(this))
+ .subscribe((change: MediaChange[]) => {
+ switch (change[0].mqAlias) {
+ case "xs":
+ case "sm": {
+ this.displayColumnGroup(this.mobileColumnGroup);
+ break;
+ }
+ case "md": {
+ this.displayColumnGroup(this.defaultColumnGroup);
+ break;
+ }
+ case "lg":
+ case "xl": {
+ break;
+ }
}
- case "md": {
- this.displayColumnGroup(this.defaultColumnGroup);
- break;
- }
- case "lg":
- case "xl": {
- break;
- }
- }
- });
+ });
}
ngAfterViewInit() {
diff --git a/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts b/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts
index 04fb5a627d..f0dce36555 100644
--- a/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts
+++ b/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.component.ts
@@ -247,11 +247,14 @@ export class EntitySubrecordComponent implements OnChanges {
duration: 8000,
}
);
- snackBarRef.onAction().subscribe(() => {
- this._entityMapper.save(row.record, true);
- this.records.unshift(row.record);
- this.initFormGroups();
- });
+ snackBarRef
+ .onAction()
+ .pipe(untilDestroyed(this))
+ .subscribe(() => {
+ this._entityMapper.save(row.record, true);
+ this.records.unshift(row.record);
+ this.initFormGroups();
+ });
}
});
}
@@ -301,16 +304,20 @@ export class EntitySubrecordComponent implements OnChanges {
.map((col) => [Object.assign({}, col)]);
dialogRef.componentInstance.entity = entity;
dialogRef.componentInstance.editing = true;
- dialogRef.componentInstance.onSave.subscribe((updatedEntity: T) => {
- dialogRef.close();
- // Trigger the change detection
- const rowIndex = this.recordsDataSource.data.findIndex(
- (row) => row.record === entity
- );
- this.recordsDataSource.data[rowIndex] = { record: updatedEntity };
- this.recordsDataSource._updateChangeSubscription();
- });
- dialogRef.componentInstance.onCancel.subscribe(() => dialogRef.close());
+ dialogRef.componentInstance.onSave
+ .pipe(untilDestroyed(this))
+ .subscribe((updatedEntity: T) => {
+ dialogRef.close();
+ // Trigger the change detection
+ const rowIndex = this.recordsDataSource.data.findIndex(
+ (row) => row.record === entity
+ );
+ this.recordsDataSource.data[rowIndex] = { record: updatedEntity };
+ this.recordsDataSource._updateChangeSubscription();
+ });
+ dialogRef.componentInstance.onCancel
+ .pipe(untilDestroyed(this))
+ .subscribe(() => dialogRef.close());
}
/**
diff --git a/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.ts b/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.ts
index fe6551140f..741b80cdf5 100644
--- a/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.ts
+++ b/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.ts
@@ -14,6 +14,7 @@ import { MatSnackBar } from "@angular/material/snack-bar";
import { Router } from "@angular/router";
import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service";
import { OperationType } from "../../permissions/entity-permissions.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
/**
* Use `` in your form templates to handle the saving and resetting of the edited entity.
@@ -31,6 +32,7 @@ import { OperationType } from "../../permissions/entity-permissions.service";
*/
+@UntilDestroy()
@Component({
selector: "app-form-dialog-wrapper",
templateUrl: "./form-dialog-wrapper.component.html",
@@ -83,9 +85,11 @@ export class FormDialogWrapperComponent implements AfterViewInit {
) {}
ngAfterViewInit() {
- this.contentForm.form.statusChanges.subscribe(() => {
- this.matDialogRef.disableClose = this.isFormDirty;
- });
+ this.contentForm.form.statusChanges
+ .pipe(untilDestroyed(this))
+ .subscribe(() => {
+ this.matDialogRef.disableClose = this.isFormDirty;
+ });
}
/**
@@ -139,10 +143,13 @@ export class FormDialogWrapperComponent implements AfterViewInit {
"Undo",
{ duration: 8000 }
);
- snackBarRef.onAction().subscribe(() => {
- this.entityMapper.save(this.entity, true);
- this.router.navigate([currentUrl]);
- });
+ snackBarRef
+ .onAction()
+ .pipe(untilDestroyed(this))
+ .subscribe(() => {
+ this.entityMapper.save(this.entity, true);
+ this.router.navigate([currentUrl]);
+ });
}
});
}
diff --git a/src/app/core/latest-changes/changelog/changelog.component.ts b/src/app/core/latest-changes/changelog/changelog.component.ts
index a6c5082787..70484faafb 100644
--- a/src/app/core/latest-changes/changelog/changelog.component.ts
+++ b/src/app/core/latest-changes/changelog/changelog.component.ts
@@ -26,12 +26,14 @@ import { Changelog } from "../changelog";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { isObservable, Observable } from "rxjs";
import { LatestChangesService } from "../latest-changes.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
/**
* Display information from the changelog for the latest version.
*
* This component is used as content of a dialog.
*/
+@UntilDestroy()
@Component({
templateUrl: "./changelog.component.html",
styleUrls: ["./changelog.component.scss"],
@@ -63,7 +65,9 @@ export class ChangelogComponent implements OnInit {
ngOnInit(): void {
if (this.data && isObservable(this.data)) {
- this.data.subscribe((changelog) => (this.changelogs = changelog));
+ this.data
+ .pipe(untilDestroyed(this))
+ .subscribe((changelog) => (this.changelogs = changelog));
}
}
@@ -83,6 +87,7 @@ export class ChangelogComponent implements OnInit {
.tag_name;
this.latestChangesService
.getChangelogsBeforeVersion(lastDisplayedVersion, 1)
+ .pipe(untilDestroyed(this))
.subscribe((additionalChangelog) => {
this.changelogs.push(...additionalChangelog);
diff --git a/src/app/core/navigation/navigation/navigation.component.ts b/src/app/core/navigation/navigation/navigation.component.ts
index b94d7af93b..312cbbb360 100644
--- a/src/app/core/navigation/navigation/navigation.component.ts
+++ b/src/app/core/navigation/navigation/navigation.component.ts
@@ -22,10 +22,12 @@ import { NavigationMenuConfig } from "../navigation-menu-config.interface";
import { RouterService } from "../../view/dynamic-routing/router.service";
import { ViewConfig } from "../../view/dynamic-routing/view-config.interface";
import { ConfigService } from "../../config/config.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
/**
* Main app menu listing.
*/
+@UntilDestroy()
@Component({
selector: "app-navigation",
templateUrl: "./navigation.component.html",
@@ -41,9 +43,9 @@ export class NavigationComponent {
private adminGuard: AdminGuard,
private configService: ConfigService
) {
- this.configService.configUpdates.subscribe(() =>
- this.initMenuItemsFromConfig()
- );
+ this.configService.configUpdates
+ .pipe(untilDestroyed(this))
+ .subscribe(() => this.initMenuItemsFromConfig());
}
/**
From dec9d78f50e0775cc1bc155f2ecef2671e23a3ca Mon Sep 17 00:00:00 2001
From: Simon <33730997+TheSlimvReal@users.noreply.github.com>
Date: Mon, 23 Aug 2021 15:16:42 +0300
Subject: [PATCH 23/34] feat: Using user-roles to manage permissions in the app
---
src/app/app.component.ts | 15 +--
src/app/app.routing.ts | 4 +-
.../aser-component/aser.component.spec.ts | 12 +-
...ivity-attendance-section.component.spec.ts | 21 ++--
.../activity-attendance-section.stories.ts | 7 +-
.../activity-list.component.spec.ts | 13 +--
.../activity-list/activity-list.component.ts | 3 +-
.../add-day-attendance.component.spec.ts | 26 ++---
.../roll-call-setup.component.spec.ts | 30 ++---
.../roll-call-setup.component.ts | 10 +-
.../roll-call-setup.stories.ts | 9 +-
.../roll-call/roll-call.stories.ts | 6 +
.../attendance-calendar.stories.ts | 8 +-
.../attendance-details.component.spec.ts | 14 +--
.../attendance-details.stories.ts | 10 +-
.../attendance-week-dashboard.stories.ts | 5 +
.../children-list.component.spec.ts | 27 ++---
.../children-list/children-list.component.ts | 3 +-
.../educational-material.component.spec.ts | 37 +++---
.../health-checkup.component.spec.ts | 46 ++++----
.../health-checkup.stories.ts | 8 +-
.../note-details.component.spec.ts | 11 +-
.../note-details/note-details.stories.ts | 14 ++-
.../notes-manager.component.spec.ts | 72 +++++-------
.../notes-manager/notes-manager.component.ts | 11 +-
.../notes-migration.service.spec.ts | 8 +-
.../notes-of-child.component.spec.ts | 19 ++--
.../notes-of-child.component.ts | 2 +-
.../previous-schools.component.spec.ts | 14 +--
.../previous-schools.stories.ts | 5 +-
.../children-overview.component.spec.ts | 29 ++---
.../schools-list.component.spec.ts | 22 ++--
.../schools-list/schools-list.component.ts | 3 +-
src/app/core/admin/admin.guard.spec.ts | 30 -----
src/app/core/admin/admin.guard.ts | 41 -------
src/app/core/admin/admin.module.ts | 3 +-
src/app/core/admin/admin/admin.component.html | 3 +
.../core/admin/admin/admin.component.spec.ts | 5 +
src/app/core/admin/admin/admin.component.ts | 4 +-
.../admin/user-list/user-list.component.html | 9 --
.../user-list/user-list.component.spec.ts | 37 ------
.../admin/user-list/user-list.component.ts | 31 +----
.../core/analytics/analytics.service.spec.ts | 16 +--
src/app/core/analytics/analytics.service.ts | 19 ++--
src/app/core/config/config-fix.ts | 6 +-
.../dashboard/dashboard.component.spec.ts | 10 +-
.../dashboard/dashboard.component.ts | 9 +-
src/app/core/database/pouch-database.ts | 10 +-
.../entity-details.component.spec.ts | 47 ++++----
.../entity-details.component.ts | 7 +-
.../entity-form/entity-form.component.spec.ts | 14 +--
.../entity-form/entity-form.stories.ts | 6 -
.../entity-list/entity-list.component.spec.ts | 12 +-
.../entity-list/entity-list.stories.ts | 5 -
.../entity-subrecord.component.spec.ts | 33 ++----
.../entity-subrecord.stories.ts | 6 -
.../list-paginator.component.spec.ts | 23 ++--
.../list-paginator.component.ts | 21 +++-
.../edit-photo/edit-photo.component.html | 2 +-
.../edit-photo/edit-photo.component.spec.ts | 16 ++-
.../edit-photo/edit-photo.component.ts | 15 ++-
src/app/core/entity/entity-config.service.ts | 4 +-
.../form-dialog-wrapper.component.spec.ts | 25 ++---
.../markdown-page.component.spec.ts | 3 +-
.../markdown-page/markdown-page.component.ts | 4 +-
.../navigation/navigation.component.spec.ts | 56 ++++++---
.../navigation/navigation.component.ts | 22 ++--
.../entity-permissions.service.spec.ts | 43 ++++---
.../permissions/entity-permissions.service.ts | 20 +++-
.../permissions-migration.service.spec.ts | 68 +++++++++++
.../permissions-migration.service.ts | 39 +++++++
.../core/permissions/permissions.module.ts | 2 +
.../core/permissions/user-role.guard.spec.ts | 58 ++++++++++
src/app/core/permissions/user-role.guard.ts | 26 +++++
.../logged-in-guard/logged-in.guard.spec.ts | 56 ---------
.../logged-in-guard/logged-in.guard.ts | 35 ------
src/app/core/session/mock-session.module.ts | 57 ++++++++++
.../session-service/local-session.spec.ts | 20 ++--
.../session/session-service/local-session.ts | 45 ++------
.../session-service/remote-session.spec.ts | 12 +-
.../session/session-service/remote-session.ts | 17 +--
.../session-service/session.service.spec.ts | 10 +-
.../session-service/session.service.ts | 27 ++---
.../synced-session.service.spec.ts | 34 +-----
.../session-service/synced-session.service.ts | 68 +++++------
.../session-states/session-utils.spec.ts | 31 +++++
.../session/session-states/session-utils.ts | 19 ++++
.../session-states/state-handler.spec.ts | 69 ------------
.../session/session-states/state-handler.ts | 106 ------------------
src/app/core/session/session.module.ts | 3 +-
.../core/session/session.service.provider.ts | 27 ++---
.../sync-status/sync-status.component.spec.ts | 15 ++-
.../sync-status/sync-status.component.ts | 12 +-
.../primary-action.component.spec.ts | 10 +-
.../primary-action.component.ts | 2 +-
src/app/core/ui/ui/ui.component.spec.ts | 12 +-
.../core/user/demo-user-generator.service.ts | 5 +-
.../user-account.component.spec.ts | 5 +-
src/app/core/user/user.spec.ts | 2 -
src/app/core/user/user.ts | 18 ---
.../dynamic-routing/router.service.spec.ts | 47 ++++++--
.../view/dynamic-routing/router.service.ts | 26 +++--
.../dynamic-routing/view-config.interface.ts | 37 +++++-
.../webdav/cloud-file-service.service.spec.ts | 84 ++++++++------
.../core/webdav/cloud-file-service.service.ts | 21 ++--
.../historical-data.component.spec.ts | 18 +--
.../reporting/reporting.component.spec.ts | 7 +-
.../reporting/reporting.component.ts | 13 ++-
src/app/utils/performance-tests.spec.ts | 5 +-
109 files changed, 1051 insertions(+), 1258 deletions(-)
delete mode 100644 src/app/core/admin/admin.guard.spec.ts
delete mode 100644 src/app/core/admin/admin.guard.ts
create mode 100644 src/app/core/permissions/permissions-migration.service.spec.ts
create mode 100644 src/app/core/permissions/permissions-migration.service.ts
create mode 100644 src/app/core/permissions/user-role.guard.spec.ts
create mode 100644 src/app/core/permissions/user-role.guard.ts
delete mode 100644 src/app/core/session/logged-in-guard/logged-in.guard.spec.ts
delete mode 100644 src/app/core/session/logged-in-guard/logged-in.guard.ts
create mode 100644 src/app/core/session/mock-session.module.ts
create mode 100644 src/app/core/session/session-states/session-utils.spec.ts
create mode 100644 src/app/core/session/session-states/session-utils.ts
delete mode 100644 src/app/core/session/session-states/state-handler.spec.ts
delete mode 100644 src/app/core/session/session-states/state-handler.ts
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index f1ad3a8aff..924d1e4be8 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -33,15 +33,16 @@ import { School } from "./child-dev-project/schools/model/school";
import { HistoricalEntityData } from "./features/historical-data/historical-entity-data";
import { Note } from "./child-dev-project/notes/model/note";
import { EventNote } from "./child-dev-project/attendance/model/event-note";
+import { waitForChangeTo } from "./core/session/session-states/session-utils";
-/**
- * Component as the main entry point for the app.
- * Actual logic and UI structure is defined in other modules.
- */
@Component({
selector: "app-root",
template: "",
})
+/**
+ * Component as the main entry point for the app.
+ * Actual logic and UI structure is defined in other modules.
+ */
export class AppComponent implements OnInit {
constructor(
private viewContainerRef: ViewContainerRef, // need this small hack in order to catch application root view container ref
@@ -61,9 +62,9 @@ export class AppComponent implements OnInit {
// TODO fix this with https://github.com/Aam-Digital/ndb-core/issues/595
configService.loadConfig(entityMapper);
// Reload config once the database is synced
- sessionService
- .getSyncState()
- .waitForChangeTo(SyncState.COMPLETED)
+ sessionService.syncState
+ .pipe(waitForChangeTo(SyncState.COMPLETED))
+ .toPromise()
.then(() => configService.loadConfig(entityMapper))
.then(() => router.navigate([], { relativeTo: this.activatedRoute }));
// These functions will be executed whenever a new config is available
diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts
index def391917b..44d57f4c67 100644
--- a/src/app/app.routing.ts
+++ b/src/app/app.routing.ts
@@ -22,7 +22,6 @@ import { SchoolsListComponent } from "./child-dev-project/schools/schools-list/s
import { UserAccountComponent } from "./core/user/user-account/user-account.component";
import { ChildrenListComponent } from "./child-dev-project/children/children-list/children-list.component";
import { AdminComponent } from "./core/admin/admin/admin.component";
-import { AdminGuard } from "./core/admin/admin.guard";
import { NotesManagerComponent } from "./child-dev-project/notes/notes-manager/notes-manager.component";
import { AddDayAttendanceComponent } from "./child-dev-project/attendance/add-day-attendance/add-day-attendance.component";
import { AttendanceManagerComponent } from "./child-dev-project/attendance/attendance-manager/attendance-manager.component";
@@ -32,6 +31,7 @@ import { EntityDetailsComponent } from "./core/entity-components/entity-details/
import { ConflictResolutionListComponent } from "./conflict-resolution/conflict-resolution-list/conflict-resolution-list.component";
import { ActivityListComponent } from "./child-dev-project/attendance/activity-list/activity-list.component";
import { ReportingComponent } from "./features/reporting/reporting/reporting.component";
+import { UserRoleGuard } from "./core/permissions/user-role.guard";
export const COMPONENT_MAP = {
Dashboard: DashboardComponent,
@@ -57,7 +57,7 @@ export const routes: Routes = [
// routes are added dynamically by the RouterService
{
path: "admin/conflicts",
- canActivate: [AdminGuard],
+ canActivate: [UserRoleGuard],
loadChildren: () =>
import("./conflict-resolution/conflict-resolution.module").then(
(m) => m["ConflictResolutionModule"]
diff --git a/src/app/child-dev-project/aser/aser-component/aser.component.spec.ts b/src/app/child-dev-project/aser/aser-component/aser.component.spec.ts
index 8dfd519a04..e3e3d3aed8 100644
--- a/src/app/child-dev-project/aser/aser-component/aser.component.spec.ts
+++ b/src/app/child-dev-project/aser/aser-component/aser.component.spec.ts
@@ -2,7 +2,6 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { AserComponent } from "./aser.component";
import { FormsModule } from "@angular/forms";
import { ChildrenService } from "../../children/children.service";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { Child } from "../../children/model/child";
import { DatePipe } from "@angular/common";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
@@ -13,8 +12,7 @@ import { RouterTestingModule } from "@angular/router/testing";
import { EntitySubrecordModule } from "../../../core/entity-components/entity-subrecord/entity-subrecord.module";
import { MatSnackBarModule } from "@angular/material/snack-bar";
import { EntityFormService } from "../../../core/entity-components/entity-form/entity-form.service";
-import { SessionService } from "../../../core/session/session-service/session.service";
-import { User } from "../../../core/user/user";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("AserComponent", () => {
let component: AserComponent;
@@ -28,11 +26,9 @@ describe("AserComponent", () => {
return of([]);
},
};
- let mockEntityMapper: jasmine.SpyObj;
beforeEach(
waitForAsync(() => {
- mockEntityMapper = jasmine.createSpyObj(["save"]);
TestBed.configureTestingModule({
declarations: [AserComponent],
imports: [
@@ -43,16 +39,12 @@ describe("AserComponent", () => {
RouterTestingModule,
EntitySubrecordModule,
MatSnackBarModule,
+ MockSessionModule.withState(),
],
providers: [
EntityFormService,
DatePipe,
{ provide: ChildrenService, useValue: mockChildrenService },
- { provide: EntityMapperService, useValue: mockEntityMapper },
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
],
}).compileComponents();
})
diff --git a/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.spec.ts b/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.spec.ts
index cdb4eb99ec..cf0078ade1 100644
--- a/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.spec.ts
+++ b/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.spec.ts
@@ -2,7 +2,6 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { ActivityAttendanceSectionComponent } from "./activity-attendance-section.component";
import { AttendanceService } from "../attendance.service";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { DatePipe, PercentPipe } from "@angular/common";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { RecurringActivity } from "../model/recurring-activity";
@@ -12,9 +11,7 @@ import { defaultAttendanceStatusTypes } from "../../../core/config/default-confi
import { AttendanceLogicalStatus } from "../model/attendance-status";
import { AttendanceModule } from "../attendance.module";
import { MatNativeDateModule } from "@angular/material/core";
-import { SessionService } from "../../../core/session/session-service/session.service";
-import { User } from "../../../core/user/user";
-import { mockEntityMapper } from "../../../core/entity/mock-entity-mapper-service";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("ActivityAttendanceSectionComponent", () => {
let component: ActivityAttendanceSectionComponent;
@@ -33,21 +30,17 @@ describe("ActivityAttendanceSectionComponent", () => {
"getActivityAttendances",
]);
mockAttendanceService.getActivityAttendances.and.resolveTo(testRecords);
-
TestBed.configureTestingModule({
- imports: [AttendanceModule, NoopAnimationsModule, MatNativeDateModule],
+ imports: [
+ AttendanceModule,
+ NoopAnimationsModule,
+ MatNativeDateModule,
+ MockSessionModule.withState(),
+ ],
providers: [
{ provide: AttendanceService, useValue: mockAttendanceService },
- {
- provide: EntityMapperService,
- useValue: mockEntityMapper(),
- },
DatePipe,
PercentPipe,
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
],
}).compileComponents();
})
diff --git a/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.stories.ts b/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.stories.ts
index 05093ee680..e880dd80df 100644
--- a/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.stories.ts
+++ b/src/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.stories.ts
@@ -5,7 +5,6 @@ import { ActivityAttendanceSectionComponent } from "./activity-attendance-sectio
import { AttendanceModule } from "../attendance.module";
import { FontAwesomeIconsModule } from "../../../core/icons/font-awesome-icons.module";
import { RouterTestingModule } from "@angular/router/testing";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { AttendanceService } from "../attendance.service";
import {
ActivityAttendance,
@@ -18,6 +17,7 @@ import { of } from "rxjs";
import { Child } from "../../children/model/child";
import { EntitySubrecordModule } from "../../../core/entity-components/entity-subrecord/entity-subrecord.module";
import { Angulartics2Module } from "angulartics2";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
const demoActivity = RecurringActivity.create("Coaching Batch C");
const attendanceRecords = [
@@ -78,13 +78,10 @@ export default {
RouterTestingModule,
MatNativeDateModule,
Angulartics2Module.forRoot(),
+ MockSessionModule.withState(),
],
declarations: [],
providers: [
- {
- provide: EntityMapperService,
- useValue: { save: () => Promise.resolve() },
- },
{
provide: AttendanceService,
useValue: {
diff --git a/src/app/child-dev-project/attendance/activity-list/activity-list.component.spec.ts b/src/app/child-dev-project/attendance/activity-list/activity-list.component.spec.ts
index a84c139460..70522fe1c7 100644
--- a/src/app/child-dev-project/attendance/activity-list/activity-list.component.spec.ts
+++ b/src/app/child-dev-project/attendance/activity-list/activity-list.component.spec.ts
@@ -2,16 +2,13 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { ActivityListComponent } from "./activity-list.component";
import { RouterTestingModule } from "@angular/router/testing";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { ActivatedRoute } from "@angular/router";
import { of } from "rxjs";
import { AttendanceModule } from "../attendance.module";
-import { SessionService } from "../../../core/session/session-service/session.service";
import { Angulartics2Module } from "angulartics2";
import { EntityListConfig } from "../../../core/entity-components/entity-list/EntityListConfig";
-import { User } from "../../../core/user/user";
-import { mockEntityMapper } from "../../../core/entity/mock-entity-mapper-service";
import { ExportService } from "../../../core/export/export-service/export.service";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("ActivityListComponent", () => {
let component: ActivityListComponent;
@@ -29,18 +26,14 @@ describe("ActivityListComponent", () => {
AttendanceModule,
RouterTestingModule,
Angulartics2Module.forRoot(),
+ MockSessionModule.withState(),
],
providers: [
- { provide: EntityMapperService, useValue: mockEntityMapper([]) },
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
{ provide: ExportService, useValue: {} },
{
provide: ActivatedRoute,
useValue: {
- data: of(mockConfig),
+ data: of({ config: mockConfig }),
queryParams: of({}),
},
},
diff --git a/src/app/child-dev-project/attendance/activity-list/activity-list.component.ts b/src/app/child-dev-project/attendance/activity-list/activity-list.component.ts
index 935a9b964b..2d82e3e202 100644
--- a/src/app/child-dev-project/attendance/activity-list/activity-list.component.ts
+++ b/src/app/child-dev-project/attendance/activity-list/activity-list.component.ts
@@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { RecurringActivity } from "../model/recurring-activity";
import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { EntityListConfig } from "../../../core/entity-components/entity-list/EntityListConfig";
+import { RouteData } from "../../../core/view/dynamic-routing/view-config.interface";
@Component({
selector: "app-activity-list",
@@ -29,7 +30,7 @@ export class ActivityListComponent implements OnInit {
async ngOnInit() {
this.route.data.subscribe(
- (config: EntityListConfig) => (this.listConfig = config)
+ (data: RouteData) => (this.listConfig = data.config)
);
this.entities = await this.entityMapper.loadType(
RecurringActivity
diff --git a/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.spec.ts b/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.spec.ts
index e31123a424..21ce2be0b6 100644
--- a/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.spec.ts
+++ b/src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.spec.ts
@@ -7,41 +7,32 @@ import { AttendanceModule } from "../attendance.module";
import { RouterTestingModule } from "@angular/router/testing";
import { ChildrenService } from "../../children/children.service";
import { of } from "rxjs";
-import { SessionService } from "../../../core/session/session-service/session.service";
-import { User } from "../../../core/user/user";
import { MatNativeDateModule } from "@angular/material/core";
import { AttendanceService } from "../attendance.service";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("AddDayAttendanceComponent", () => {
let component: AddDayAttendanceComponent;
let fixture: ComponentFixture;
- let mockEntityService: jasmine.SpyObj;
let mockChildrenService: jasmine.SpyObj;
beforeEach(
waitForAsync(() => {
- mockEntityService = jasmine.createSpyObj("mockEntityService", [
- "save",
- "loadType",
- ]);
- mockEntityService.save.and.resolveTo();
- mockEntityService.loadType.and.resolveTo([]);
-
mockChildrenService = jasmine.createSpyObj("mockChildrenService", [
"getChildren",
]);
mockChildrenService.getChildren.and.returnValue(of([]));
TestBed.configureTestingModule({
- imports: [AttendanceModule, RouterTestingModule, MatNativeDateModule],
+ imports: [
+ AttendanceModule,
+ RouterTestingModule,
+ MatNativeDateModule,
+ MockSessionModule.withState(),
+ ],
providers: [
- { provide: EntityMapperService, useValue: mockEntityService },
{ provide: ChildrenService, useValue: mockChildrenService },
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User("") },
- },
{
provide: AttendanceService,
useValue: { getEventsOnDate: () => Promise.resolve([]) },
@@ -63,9 +54,10 @@ describe("AddDayAttendanceComponent", () => {
it("should save event to db after finishing roll call", () => {
component.event = Note.create(new Date());
+ const saveEntitySpy = spyOn(TestBed.inject(EntityMapperService), "save");
component.saveRollCallResult(component.event);
- expect(mockEntityService.save).toHaveBeenCalledWith(component.event);
+ expect(saveEntitySpy).toHaveBeenCalledWith(component.event);
});
});
diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.spec.ts b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.spec.ts
index 1e0bf57d66..f02f0ab753 100644
--- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.spec.ts
+++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.spec.ts
@@ -7,31 +7,23 @@ import {
import { RollCallSetupComponent } from "./roll-call-setup.component";
import { EntityMapperService } from "../../../../core/entity/entity-mapper.service";
-import { SessionService } from "../../../../core/session/session-service/session.service";
-import { User } from "../../../../core/user/user";
import { RecurringActivity } from "../../model/recurring-activity";
import { ChildrenService } from "../../../children/children.service";
import { AttendanceModule } from "../../attendance.module";
import { MatNativeDateModule } from "@angular/material/core";
import { AttendanceService } from "../../attendance.service";
import { EventNote } from "../../model/event-note";
+import { MockSessionModule } from "../../../../core/session/mock-session.module";
+import { TEST_USER } from "../../../../core/session/session-service/session.service.spec";
describe("RollCallSetupComponent", () => {
let component: RollCallSetupComponent;
let fixture: ComponentFixture;
- const user = new User("test-user");
- let mockEntityService: jasmine.SpyObj;
let mockChildrenService: jasmine.SpyObj;
let mockAttendanceService: jasmine.SpyObj;
beforeEach(() => {
- mockEntityService = jasmine.createSpyObj("mockEntityService", [
- "save",
- "loadType",
- ]);
- mockEntityService.loadType.and.resolveTo([]);
-
mockChildrenService = jasmine.createSpyObj(["queryRelationsOf"]);
mockChildrenService.queryRelationsOf.and.resolveTo([]);
mockAttendanceService = jasmine.createSpyObj([
@@ -42,13 +34,12 @@ describe("RollCallSetupComponent", () => {
TestBed.configureTestingModule({
declarations: [RollCallSetupComponent],
- imports: [AttendanceModule, MatNativeDateModule],
+ imports: [
+ AttendanceModule,
+ MatNativeDateModule,
+ MockSessionModule.withState(),
+ ],
providers: [
- { provide: EntityMapperService, useValue: mockEntityService },
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => user },
- },
{ provide: ChildrenService, useValue: mockChildrenService },
{ provide: AttendanceService, useValue: mockAttendanceService },
],
@@ -71,13 +62,14 @@ describe("RollCallSetupComponent", () => {
RecurringActivity.create("act 2"),
];
mockAttendanceService.createEventForActivity.and.resolveTo(new EventNote());
+ const entityMapper = TestBed.inject(EntityMapperService);
+ spyOn(entityMapper, "loadType").and.resolveTo(testActivities);
- mockEntityService.loadType.and.resolveTo(testActivities);
component.ngOnInit();
flush();
expect(component.existingEvents.length).toBe(2);
- expect(component.existingEvents[0].authors).toEqual([user.getId()]);
- expect(component.existingEvents[1].authors).toEqual([user.getId()]);
+ expect(component.existingEvents[0].authors).toEqual([TEST_USER]);
+ expect(component.existingEvents[1].authors).toEqual([TEST_USER]);
}));
});
diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
index f9bb35215b..55ec7da715 100644
--- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
+++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
@@ -48,7 +48,7 @@ export class RollCallSetupComponent implements OnInit {
);
this.visibleActivities = this.allActivities.filter((a) =>
- a.assignedTo.includes(this.sessionService.getCurrentUser().getId())
+ a.assignedTo.includes(this.sessionService.getCurrentUser().name)
);
if (this.visibleActivities.length === 0) {
this.visibleActivities = this.allActivities.filter(
@@ -99,7 +99,7 @@ export class RollCallSetupComponent implements OnInit {
activity,
this.date
)) as NoteForActivitySetup;
- event.authors = [this.sessionService.getCurrentUser().getId()];
+ event.authors = [this.sessionService.getCurrentUser().name];
event.isNewFromActivity = true;
return event;
}
@@ -119,9 +119,7 @@ export class RollCallSetupComponent implements OnInit {
score += 1;
}
- if (
- assignedUsers.includes(this.sessionService.getCurrentUser().getId())
- ) {
+ if (assignedUsers.includes(this.sessionService.getCurrentUser().name)) {
score += 2;
}
@@ -135,7 +133,7 @@ export class RollCallSetupComponent implements OnInit {
createOneTimeEvent() {
const newNote = Note.create(new Date());
- newNote.authors = [this.sessionService.getCurrentUser().getId()];
+ newNote.authors = [this.sessionService.getCurrentUser().name];
this.formDialog
.openDialog(NoteDetailsComponent, newNote)
diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.stories.ts b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.stories.ts
index c1a7c05e33..474c9dec48 100644
--- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.stories.ts
+++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.stories.ts
@@ -30,10 +30,9 @@ import { ActivityCardComponent } from "../../activity-card/activity-card.compone
import { MatTooltipModule } from "@angular/material/tooltip";
import { FontAwesomeIconsModule } from "../../../../core/icons/font-awesome-icons.module";
import { DemoActivityGeneratorService } from "../../demo-data/demo-activity-generator.service";
-import { SessionService } from "../../../../core/session/session-service/session.service";
-import { User } from "../../../../core/user/user";
import { FormDialogModule } from "../../../../core/form-dialog/form-dialog.module";
import { PouchDatabase } from "../../../../core/database/pouch-database";
+import { SessionService } from "../../../../core/session/session-service/session.service";
const demoEvents: Note[] = [
Note.create(new Date(), "Class 5a Parents Meeting"),
@@ -107,7 +106,11 @@ export default {
ChildPhotoService,
{
provide: SessionService,
- useValue: { getCurrentUser: () => new User("demo") },
+ useValue: {
+ getCurrentUser: () => {
+ return { name: "username" };
+ },
+ },
},
],
}),
diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.stories.ts b/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.stories.ts
index 541388e076..c9a3120981 100644
--- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.stories.ts
+++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.stories.ts
@@ -10,6 +10,8 @@ import { Note } from "../../../notes/model/note";
import { FlexLayoutModule } from "@angular/flex-layout";
import { ChildrenService } from "../../../children/children.service";
import { of } from "rxjs";
+import { EntityMapperService } from "../../../../core/entity/entity-mapper.service";
+import { mockEntityMapper } from "../../../../core/entity/mock-entity-mapper-service";
export default {
title: "Attendance/Views/RollCall",
@@ -24,6 +26,10 @@ export default {
],
declarations: [ChildBlockComponent],
providers: [
+ {
+ provide: EntityMapperService,
+ useValue: mockEntityMapper(),
+ },
{
provide: ChildrenService,
useValue: {
diff --git a/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.stories.ts b/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.stories.ts
index 33eafa86fd..7c03354a24 100644
--- a/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.stories.ts
+++ b/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.stories.ts
@@ -11,6 +11,8 @@ import { Note } from "../../notes/model/note";
import moment from "moment";
import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { FormDialogModule } from "../../../core/form-dialog/form-dialog.module";
+import { AttendanceService } from "../attendance.service";
+import { mockEntityMapper } from "../../../core/entity/mock-entity-mapper-service";
const demoEvents: Note[] = [
generateEventWithAttendance(
@@ -63,7 +65,11 @@ export default {
providers: [
{
provide: EntityMapperService,
- useValue: { save: () => Promise.resolve() },
+ useValue: mockEntityMapper(),
+ },
+ {
+ provide: AttendanceService,
+ useValue: null,
},
],
}),
diff --git a/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.spec.ts b/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.spec.ts
index ba927833dd..ee0535915a 100644
--- a/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.spec.ts
+++ b/src/app/child-dev-project/attendance/attendance-details/attendance-details.component.spec.ts
@@ -9,16 +9,13 @@ import {
} from "../model/activity-attendance";
import { AttendanceLogicalStatus } from "../model/attendance-status";
import { RecurringActivity } from "../model/recurring-activity";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { AttendanceModule } from "../attendance.module";
import { EntitySubrecordModule } from "../../../core/entity-components/entity-subrecord/entity-subrecord.module";
import { MatNativeDateModule } from "@angular/material/core";
import { MatDialogRef } from "@angular/material/dialog";
-import { EMPTY } from "rxjs";
import { EventNote } from "../model/event-note";
import { AttendanceService } from "../attendance.service";
-import { SessionService } from "../../../core/session/session-service/session.service";
-import { User } from "../../../core/user/user";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("AttendanceDetailsComponent", () => {
let component: AttendanceDetailsComponent;
@@ -52,9 +49,6 @@ describe("AttendanceDetailsComponent", () => {
]);
entity.activity = RecurringActivity.create("Test Activity");
- const mockEntityMapperService = jasmine.createSpyObj(["receiveUpdates"]);
- mockEntityMapperService.receiveUpdates.and.returnValue(EMPTY);
-
TestBed.configureTestingModule({
imports: [
AttendanceModule,
@@ -63,15 +57,11 @@ describe("AttendanceDetailsComponent", () => {
Angulartics2Module.forRoot(),
RouterTestingModule,
MatNativeDateModule,
+ MockSessionModule.withState(),
],
providers: [
- { provide: EntityMapperService, useValue: mockEntityMapperService },
{ provide: MatDialogRef, useValue: {} },
{ provide: AttendanceService, useValue: mockAttendanceService },
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
],
}).compileComponents();
})
diff --git a/src/app/child-dev-project/attendance/attendance-details/attendance-details.stories.ts b/src/app/child-dev-project/attendance/attendance-details/attendance-details.stories.ts
index f6d1f3f3fb..c31efea87e 100644
--- a/src/app/child-dev-project/attendance/attendance-details/attendance-details.stories.ts
+++ b/src/app/child-dev-project/attendance/attendance-details/attendance-details.stories.ts
@@ -10,13 +10,14 @@ import { AttendanceDetailsComponent } from "./attendance-details.component";
import { AttendanceModule } from "../attendance.module";
import { RouterTestingModule } from "@angular/router/testing";
import { FormDialogModule } from "../../../core/form-dialog/form-dialog.module";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { Angulartics2Module } from "angulartics2";
import { FontAwesomeIconsModule } from "../../../core/icons/font-awesome-icons.module";
import { MatNativeDateModule } from "@angular/material/core";
import { EntitySubrecordModule } from "../../../core/entity-components/entity-subrecord/entity-subrecord.module";
import { MatDialogRef } from "@angular/material/dialog";
import { NotesModule } from "../../notes/notes.module";
+import { AttendanceService } from "../attendance.service";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
const demoActivity = RecurringActivity.create("Coaching Batch C");
const activityAttendance = ActivityAttendance.create(new Date("2020-01-01"), [
@@ -68,14 +69,13 @@ export default {
MatNativeDateModule,
NotesModule,
Angulartics2Module.forRoot(),
+ MockSessionModule.withState(),
],
declarations: [],
providers: [
{
- provide: EntityMapperService,
- useValue: {
- loadType: () => Promise.resolve([]),
- },
+ provide: AttendanceService,
+ useValue: null,
},
{ provide: MatDialogRef, useValue: {} },
],
diff --git a/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.stories.ts b/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.stories.ts
index 16856723c9..48184e1883 100644
--- a/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.stories.ts
+++ b/src/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.stories.ts
@@ -13,6 +13,7 @@ import { RouterTestingModule } from "@angular/router/testing";
import { Angulartics2Module } from "angulartics2";
import { Database } from "../../../../core/database/database";
import { PouchDatabase } from "../../../../core/database/pouch-database";
+import { SessionService } from "../../../../core/session/session-service/session.service";
const child1 = Child.create("Jack");
const child2 = Child.create("Jane");
@@ -62,6 +63,10 @@ export default {
Angulartics2Module.forRoot(),
],
providers: [
+ {
+ provide: SessionService,
+ useValue: null,
+ },
{
provide: Database,
useValue: PouchDatabase.createWithData([
diff --git a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts
index 9bb8fc0876..aa760c0867 100644
--- a/src/app/child-dev-project/children/children-list/children-list.component.spec.ts
+++ b/src/app/child-dev-project/children/children-list/children-list.component.spec.ts
@@ -8,7 +8,6 @@ import {
import { ChildrenListComponent } from "./children-list.component";
import { ChildrenService } from "../children.service";
import { RouterTestingModule } from "@angular/router/testing";
-import { SessionService } from "../../../core/session/session-service/session.service";
import { of } from "rxjs";
import { ActivatedRoute, Router } from "@angular/router";
import { ChildrenModule } from "../children.module";
@@ -19,11 +18,11 @@ import {
EntityListConfig,
PrebuiltFilterConfig,
} from "../../../core/entity-components/entity-list/EntityListConfig";
-import { User } from "../../../core/user/user";
import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { School } from "../../schools/model/school";
import { LoggingService } from "../../../core/logging/logging.service";
import { ExportService } from "../../../core/export/export-service/export.service";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("ChildrenListComponent", () => {
let component: ChildrenListComponent;
@@ -76,20 +75,15 @@ describe("ChildrenListComponent", () => {
],
};
const routeMock = {
- data: of(routeData),
+ data: of({ config: routeData }),
queryParams: of({}),
};
const mockChildrenService: jasmine.SpyObj = jasmine.createSpyObj(
["getChildren"]
);
- const mockEntityMapper: jasmine.SpyObj = jasmine.createSpyObj(
- ["loadType", "save"]
- );
+
beforeEach(
waitForAsync(() => {
- mockEntityMapper.loadType.and.resolveTo([]);
- const mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
- mockSessionService.getCurrentUser.and.returnValue(new User("test1"));
mockChildrenService.getChildren.and.returnValue(of([]));
TestBed.configureTestingModule({
declarations: [ChildrenListComponent],
@@ -98,20 +92,13 @@ describe("ChildrenListComponent", () => {
ChildrenModule,
RouterTestingModule,
Angulartics2Module.forRoot(),
+ MockSessionModule.withState(),
],
providers: [
{
provide: ChildrenService,
useValue: mockChildrenService,
},
- {
- provide: EntityMapperService,
- useValue: mockEntityMapper,
- },
- {
- provide: SessionService,
- useValue: mockSessionService,
- },
{ provide: ActivatedRoute, useValue: routeMock },
{
provide: LoggingService,
@@ -157,10 +144,14 @@ describe("ChildrenListComponent", () => {
firstSchool.name = "A Test";
const secondSchool = new School("test");
secondSchool.name = "Test";
+ const entityMapper = TestBed.inject(EntityMapperService);
+ entityMapper.save(firstSchool);
+ entityMapper.save(secondSchool);
+ tick();
- mockEntityMapper.loadType.and.resolveTo([secondSchool, firstSchool]);
component.ngOnInit();
tick();
+
const schoolFilter = component.listConfig.filters.find(
(f) => f.id === "school"
) as PrebuiltFilterConfig;
diff --git a/src/app/child-dev-project/children/children-list/children-list.component.ts b/src/app/child-dev-project/children/children-list/children-list.component.ts
index c35efd288b..585291fe04 100644
--- a/src/app/child-dev-project/children/children-list/children-list.component.ts
+++ b/src/app/child-dev-project/children/children-list/children-list.component.ts
@@ -12,6 +12,7 @@ import { EntityMapperService } from "../../../core/entity/entity-mapper.service"
import { School } from "../../schools/model/school";
import { LoggingService } from "../../../core/logging/logging.service";
import { EntityListComponent } from "../../../core/entity-components/entity-list/entity-list.component";
+import { RouteData } from "../../../core/view/dynamic-routing/view-config.interface";
@UntilDestroy()
@Component({
@@ -43,7 +44,7 @@ export class ChildrenListComponent implements OnInit {
ngOnInit() {
this.route.data.subscribe(
- (config: EntityListConfig) => (this.listConfig = config)
+ (data: RouteData) => (this.listConfig = data.config)
);
this.childrenService
.getChildren()
diff --git a/src/app/child-dev-project/educational-material/educational-material-component/educational-material.component.spec.ts b/src/app/child-dev-project/educational-material/educational-material-component/educational-material.component.spec.ts
index dd8ee5335e..71f5eb8679 100644
--- a/src/app/child-dev-project/educational-material/educational-material-component/educational-material.component.spec.ts
+++ b/src/app/child-dev-project/educational-material/educational-material-component/educational-material.component.spec.ts
@@ -7,37 +7,34 @@ import { DatePipe } from "@angular/common";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { of } from "rxjs";
import { ChildrenModule } from "../../children/children.module";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
-import { mockEntityMapper } from "../../../core/entity/mock-entity-mapper-service";
-import { SessionService } from "../../../core/session/session-service/session.service";
-import { User } from "../../../core/user/user";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("EducationalMaterialComponent", () => {
let component: EducationalMaterialComponent;
let fixture: ComponentFixture;
-
- const mockChildrenService = {
- getChild: () => {
- return of([new Child("22")]);
- },
- getEducationalMaterialsOfChild: () => {
- return of([]);
- },
- };
+ let mockChildrenService: jasmine.SpyObj;
+ const child = new Child("22");
beforeEach(
waitForAsync(() => {
+ mockChildrenService = jasmine.createSpyObj([
+ "getChild",
+ "getEducationalMaterialsOfChild",
+ ]);
+ mockChildrenService.getChild.and.returnValue(of(child));
+ mockChildrenService.getEducationalMaterialsOfChild.and.returnValue(
+ of([])
+ );
TestBed.configureTestingModule({
declarations: [EducationalMaterialComponent],
- imports: [ChildrenModule, NoopAnimationsModule],
+ imports: [
+ ChildrenModule,
+ NoopAnimationsModule,
+ MockSessionModule.withState(),
+ ],
providers: [
DatePipe,
{ provide: ChildrenService, useValue: mockChildrenService },
- { provide: EntityMapperService, useValue: mockEntityMapper([]) },
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
],
}).compileComponents();
})
@@ -46,7 +43,7 @@ describe("EducationalMaterialComponent", () => {
beforeEach(() => {
fixture = TestBed.createComponent(EducationalMaterialComponent);
component = fixture.componentInstance;
- component.child = new Child("22");
+ component.child = child;
fixture.detectChanges();
});
diff --git a/src/app/child-dev-project/health-checkup/health-checkup-component/health-checkup.component.spec.ts b/src/app/child-dev-project/health-checkup/health-checkup-component/health-checkup.component.spec.ts
index accdb3736f..78b55d1fab 100644
--- a/src/app/child-dev-project/health-checkup/health-checkup-component/health-checkup.component.spec.ts
+++ b/src/app/child-dev-project/health-checkup/health-checkup-component/health-checkup.component.spec.ts
@@ -5,46 +5,40 @@ import { of } from "rxjs";
import { Child } from "../../children/model/child";
import { DatePipe } from "@angular/common";
import { ChildrenService } from "../../children/children.service";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { AlertService } from "../../../core/alerts/alert.service";
import { ChildrenModule } from "../../children/children.module";
-import { SessionService } from "../../../core/session/session-service/session.service";
-import { User } from "../../../core/user/user";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("HealthCheckupComponent", () => {
let component: HealthCheckupComponent;
let fixture: ComponentFixture;
-
- const mockChildrenService = {
- getChild: () => {
- return of([new Child("22")]);
- },
- getEducationalMaterialsOfChild: () => {
- return of([]);
- },
- getHealthChecksOfChild: () => {
- return of([]);
- },
- };
- const mockEntityMapper = jasmine.createSpyObj("mockEntityMapper", [
- "save",
- "remove",
- ]);
+ let mockChildrenService: jasmine.SpyObj;
+ const child = new Child();
beforeEach(
waitForAsync(() => {
+ mockChildrenService = jasmine.createSpyObj([
+ "getChild",
+ "getEducationalMaterialsOfChild",
+ "getHealthChecksOfChild",
+ ]);
+ mockChildrenService.getChild.and.returnValue(of(child));
+ mockChildrenService.getEducationalMaterialsOfChild.and.returnValue(
+ of([])
+ );
+ mockChildrenService.getHealthChecksOfChild.and.returnValue(of([]));
+
TestBed.configureTestingModule({
- imports: [ChildrenModule, NoopAnimationsModule],
+ imports: [
+ ChildrenModule,
+ NoopAnimationsModule,
+ MockSessionModule.withState(),
+ ],
providers: [
DatePipe,
{ provide: ChildrenService, useValue: mockChildrenService },
- { provide: EntityMapperService, useValue: mockEntityMapper },
AlertService,
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
],
}).compileComponents();
})
@@ -53,7 +47,7 @@ describe("HealthCheckupComponent", () => {
beforeEach(() => {
fixture = TestBed.createComponent(HealthCheckupComponent);
component = fixture.componentInstance;
- component.child = new Child("22");
+ component.child = child;
fixture.detectChanges();
});
diff --git a/src/app/child-dev-project/health-checkup/health-checkup-component/health-checkup.stories.ts b/src/app/child-dev-project/health-checkup/health-checkup-component/health-checkup.stories.ts
index 63fdf0566d..0ca30e366b 100644
--- a/src/app/child-dev-project/health-checkup/health-checkup-component/health-checkup.stories.ts
+++ b/src/app/child-dev-project/health-checkup/health-checkup-component/health-checkup.stories.ts
@@ -2,12 +2,12 @@ import { Story, Meta } from "@storybook/angular/types-6-0";
import { moduleMetadata } from "@storybook/angular";
import { HealthCheckupComponent } from "./health-checkup.component";
import { ChildrenModule } from "../../children/children.module";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { ChildrenService } from "../../children/children.service";
import { HealthCheck } from "../model/health-check";
import moment from "moment";
import { Child } from "../../children/model/child";
import { of } from "rxjs";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
const hc1 = new HealthCheck();
hc1.date = new Date();
@@ -27,13 +27,9 @@ export default {
component: HealthCheckupComponent,
decorators: [
moduleMetadata({
- imports: [ChildrenModule],
+ imports: [ChildrenModule, MockSessionModule.withState()],
declarations: [],
providers: [
- {
- provide: EntityMapperService,
- useValue: { save: () => Promise.resolve() },
- },
{
provide: ChildrenService,
useValue: { getHealthChecksOfChild: () => of([hc1, hc2, hc3]) },
diff --git a/src/app/child-dev-project/notes/note-details/note-details.component.spec.ts b/src/app/child-dev-project/notes/note-details/note-details.component.spec.ts
index 977c9eaf65..73bea300ff 100644
--- a/src/app/child-dev-project/notes/note-details/note-details.component.spec.ts
+++ b/src/app/child-dev-project/notes/note-details/note-details.component.spec.ts
@@ -10,10 +10,7 @@ import { RouterTestingModule } from "@angular/router/testing";
import { Angulartics2Module } from "angulartics2";
import { MatDialogRef } from "@angular/material/dialog";
import { defaultAttendanceStatusTypes } from "../../../core/config/default-config/default-attendance-status-types";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
-import { mockEntityMapper } from "../../../core/entity/mock-entity-mapper-service";
-import { SessionService } from "../../../core/session/session-service/session.service";
-import { User } from "../../../core/user/user";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
function generateTestNote(forChildren: Child[]) {
const testNote = Note.create(new Date(), "test note");
@@ -59,15 +56,11 @@ describe("NoteDetailsComponent", () => {
RouterTestingModule,
MatNativeDateModule,
Angulartics2Module.forRoot(),
+ MockSessionModule.withState(),
],
providers: [
{ provide: MatDialogRef, useValue: dialogRefMock },
- { provide: EntityMapperService, useValue: mockEntityMapper() },
{ provide: ChildrenService, useValue: mockChildrenService },
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
],
}).compileComponents();
fixture = TestBed.createComponent(NoteDetailsComponent);
diff --git a/src/app/child-dev-project/notes/note-details/note-details.stories.ts b/src/app/child-dev-project/notes/note-details/note-details.stories.ts
index fe7590407d..2490add812 100644
--- a/src/app/child-dev-project/notes/note-details/note-details.stories.ts
+++ b/src/app/child-dev-project/notes/note-details/note-details.stories.ts
@@ -6,10 +6,13 @@ import { NotesModule } from "../notes.module";
import { NoteDetailsComponent } from "./note-details.component";
import { Note } from "../model/note";
import { RouterTestingModule } from "@angular/router/testing";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { MatNativeDateModule } from "@angular/material/core";
import { Angulartics2Module } from "angulartics2";
import { Child } from "../../children/model/child";
+import { MatDialogRef } from "@angular/material/dialog";
+import { ChildrenService } from "../../children/children.service";
+import { of } from "rxjs";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
const demoChildren: Child[] = [Child.create("Joe"), Child.create("Jane")];
@@ -25,8 +28,15 @@ export default {
Angulartics2Module.forRoot(),
ConfigurableEnumModule,
NotesModule,
+ MockSessionModule.withState(),
+ ],
+ providers: [
+ { provide: MatDialogRef, useValue: {} },
+ {
+ provide: ChildrenService,
+ useValue: { getChild: () => of(new Child()) },
+ },
],
- providers: [{ provide: EntityMapperService, useValue: {} }],
}),
],
} as Meta;
diff --git a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts
index 5fe7388f77..24e4501b34 100644
--- a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts
+++ b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.spec.ts
@@ -11,12 +11,10 @@ import {
} from "@angular/core/testing";
import { NotesModule } from "../notes.module";
import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
-import { SessionService } from "../../../core/session/session-service/session.service";
import { RouterTestingModule } from "@angular/router/testing";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
import { ActivatedRoute, Router } from "@angular/router";
import { of, Subject } from "rxjs";
-import { User } from "../../../core/user/user";
import { Note } from "../model/note";
import { Angulartics2Module } from "angulartics2";
import { NoteDetailsComponent } from "../note-details/note-details.component";
@@ -32,12 +30,13 @@ import { EventNote } from "../../attendance/model/event-note";
import { BehaviorSubject } from "rxjs";
import { UpdatedEntity } from "../../../core/entity/model/entity-update";
import { ExportService } from "../../../core/export/export-service/export.service";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("NotesManagerComponent", () => {
let component: NotesManagerComponent;
let fixture: ComponentFixture;
- let mockEntityMapper: jasmine.SpyObj;
+ let entityMapper: EntityMapperService;
let mockNoteObservable: Subject>;
let mockEventNoteObservable: Subject>;
const dialogMock: jasmine.SpyObj = jasmine.createSpyObj(
@@ -76,7 +75,7 @@ describe("NotesManagerComponent", () => {
};
const routeMock = {
- data: new BehaviorSubject(routeData),
+ data: new BehaviorSubject({ config: routeData }),
queryParams: of({}),
};
@@ -96,35 +95,31 @@ describe("NotesManagerComponent", () => {
"getConfig",
]);
mockConfigService.getConfig.and.returnValue(testInteractionTypes);
-
- mockEntityMapper = jasmine.createSpyObj([
- "loadType",
- "receiveUpdates",
- "save",
- ]);
- mockEntityMapper.loadType.and.resolveTo([]);
mockNoteObservable = new Subject>();
mockEventNoteObservable = new Subject>();
- mockEntityMapper.receiveUpdates.and.callFake((entityType) =>
- (entityType as any) === Note
- ? (mockNoteObservable as any)
- : (mockEventNoteObservable as any)
- );
- const mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
- mockSessionService.getCurrentUser.and.returnValue(new User("test1"));
TestBed.configureTestingModule({
declarations: [],
- imports: [NotesModule, RouterTestingModule, Angulartics2Module.forRoot()],
+ imports: [
+ NotesModule,
+ RouterTestingModule,
+ Angulartics2Module.forRoot(),
+ MockSessionModule.withState(),
+ ],
providers: [
- { provide: SessionService, useValue: mockSessionService },
- { provide: EntityMapperService, useValue: mockEntityMapper },
{ provide: FormDialogService, useValue: dialogMock },
{ provide: ActivatedRoute, useValue: routeMock },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: ExportService, useValue: {} },
],
}).compileComponents();
+
+ entityMapper = TestBed.inject(EntityMapperService);
+ spyOn(entityMapper, "receiveUpdates").and.callFake((entityType) =>
+ (entityType as any) === Note
+ ? (mockNoteObservable as any)
+ : (mockEventNoteObservable as any)
+ );
});
beforeEach(async () => {
@@ -207,53 +202,38 @@ describe("NotesManagerComponent", () => {
note.category = testInteractionTypes[0];
const eventNote = EventNote.create(new Date("2020-01-01"), "test event");
eventNote.category = testInteractionTypes[0];
- mockEntityMapper.loadType.and.callFake(loadTypeFake([note], [eventNote]));
+ await entityMapper.save(note);
+ await entityMapper.save(eventNote);
component.includeEventNotes = true;
await component.updateIncludeEvents();
expect(component.notes).toEqual([note, eventNote]);
- expect(mockEntityMapper.loadType).toHaveBeenCalledWith(Note);
- expect(mockEntityMapper.loadType).toHaveBeenCalledWith(EventNote);
component.includeEventNotes = false;
await component.updateIncludeEvents();
expect(component.notes).toEqual([note]);
- expect(mockEntityMapper.loadType.calls.mostRecent().args).toEqual([Note]);
});
- it("loads initial list including EventNotes if set in config", fakeAsync(async () => {
+ it("loads initial list including EventNotes if set in config", fakeAsync(() => {
const note = Note.create(new Date("2020-01-01"), "test note");
note.category = testInteractionTypes[0];
const eventNote = EventNote.create(new Date("2020-01-01"), "test event");
eventNote.category = testInteractionTypes[0];
- mockEntityMapper.loadType.and.callFake(loadTypeFake([note], [eventNote]));
+ entityMapper.save(note);
+ entityMapper.save(eventNote);
+ tick();
- routeMock.data.next(
- Object.assign(
+ routeMock.data.next({
+ config: Object.assign(
{ includeEventNotes: true } as NotesManagerConfig,
routeData
- )
- );
+ ),
+ });
flush();
expect(component.notes).toEqual([note, eventNote]);
- expect(mockEntityMapper.loadType).toHaveBeenCalledWith(Note);
- expect(mockEntityMapper.loadType).toHaveBeenCalledWith(EventNote);
}));
});
-
-function loadTypeFake(notes: Note[], eventNotes: EventNote[]) {
- return (type) => {
- switch (type) {
- case Note:
- return Promise.resolve(notes);
- case EventNote:
- return Promise.resolve(eventNotes);
- default:
- return Promise.resolve([]);
- }
- };
-}
diff --git a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
index 3c4dba238e..ef2cd1fdf7 100644
--- a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
+++ b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.ts
@@ -15,6 +15,7 @@ import { EntityListConfig } from "../../../core/entity-components/entity-list/En
import { EventNote } from "../../attendance/model/event-note";
import { EntityConstructor } from "../../../core/entity/model/entity";
import { WarningLevel } from "../../../core/entity/model/warning-level";
+import { RouteData } from "../../../core/view/dynamic-routing/view-config.interface";
/**
* additional config specifically for NotesManagerComponent
@@ -84,12 +85,12 @@ export class NotesManagerComponent implements OnInit {
async ngOnInit() {
this.route.data.subscribe(
- async (config: EntityListConfig & NotesManagerConfig) => {
- this.config = config;
+ async (data: RouteData) => {
+ this.config = data.config;
this.addPrebuiltFilters();
- this.includeEventNotes = config.includeEventNotes;
- this.showEventNotesToggle = config.showEventNotesToggle;
+ this.includeEventNotes = data.config.includeEventNotes;
+ this.showEventNotesToggle = data.config.showEventNotesToggle;
this.notes = await this.loadEntities();
}
);
@@ -165,7 +166,7 @@ export class NotesManagerComponent implements OnInit {
addNoteClick() {
const newNote = new Note(Date.now().toString());
newNote.date = new Date();
- newNote.authors = [this.sessionService.getCurrentUser().getId()];
+ newNote.authors = [this.sessionService.getCurrentUser().name];
this.showDetails(newNote);
}
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
index 3075578cf9..f93905f879 100644
--- 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
@@ -4,7 +4,10 @@ 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 } from "../../../core/entity/mock-entity-mapper-service";
+import {
+ mockEntityMapper,
+ MockEntityMapperService,
+} from "../../../core/entity/mock-entity-mapper-service";
import { AlertService } from "../../../core/alerts/alert.service";
function legacyNote(author: string): Note {
@@ -29,9 +32,10 @@ describe("NotesMigrationService", () => {
const Johanna = createUser("Johanna");
const users = [Peter, Ursula, Jens, Angela, Albrecht, Johanna];
- const entityMapper = mockEntityMapper([]);
+ let entityMapper: MockEntityMapperService;
beforeEach(() => {
+ entityMapper = mockEntityMapper([]);
TestBed.configureTestingModule({
providers: [
{
diff --git a/src/app/child-dev-project/notes/notes-of-child/notes-of-child.component.spec.ts b/src/app/child-dev-project/notes/notes-of-child/notes-of-child.component.spec.ts
index 119418f736..2faebf75bd 100644
--- a/src/app/child-dev-project/notes/notes-of-child/notes-of-child.component.spec.ts
+++ b/src/app/child-dev-project/notes/notes-of-child/notes-of-child.component.spec.ts
@@ -5,20 +5,12 @@ import { MatNativeDateModule } from "@angular/material/core";
import { ChildrenService } from "../../children/children.service";
import { DatePipe } from "@angular/common";
import { Note } from "../model/note";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
-import { SessionService } from "../../../core/session/session-service/session.service";
import { Child } from "../../children/model/child";
-import { User } from "../../../core/user/user";
import { RouterTestingModule } from "@angular/router/testing";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
const allChildren: Array = [];
-const mockedSessionService = {
- getCurrentUser(): User {
- return new User("1");
- },
-};
-
describe("NotesOfChildComponent", () => {
let component: NotesOfChildComponent;
let fixture: ComponentFixture;
@@ -30,12 +22,15 @@ describe("NotesOfChildComponent", () => {
"getNotesOfChild",
]);
TestBed.configureTestingModule({
- imports: [NotesModule, MatNativeDateModule, RouterTestingModule],
+ imports: [
+ NotesModule,
+ MatNativeDateModule,
+ RouterTestingModule,
+ MockSessionModule.withState(),
+ ],
providers: [
{ provide: ChildrenService, useValue: mockChildrenService },
- { provide: SessionService, useValue: mockedSessionService },
{ provide: DatePipe, useValue: new DatePipe("medium") },
- { provide: EntityMapperService, useValue: {} },
],
}).compileComponents();
});
diff --git a/src/app/child-dev-project/notes/notes-of-child/notes-of-child.component.ts b/src/app/child-dev-project/notes/notes-of-child/notes-of-child.component.ts
index 1fd54cadd3..7faf799bdb 100644
--- a/src/app/child-dev-project/notes/notes-of-child/notes-of-child.component.ts
+++ b/src/app/child-dev-project/notes/notes-of-child/notes-of-child.component.ts
@@ -72,7 +72,7 @@ export class NotesOfChildComponent
generateNewRecordFactory() {
// define values locally because "this" is a different scope after passing a function as input to another component
- const user = this.sessionService.getCurrentUser().getId();
+ const user = this.sessionService.getCurrentUser().name;
const childId = this.child.getId();
return () => {
diff --git a/src/app/child-dev-project/previous-schools/previous-schools.component.spec.ts b/src/app/child-dev-project/previous-schools/previous-schools.component.spec.ts
index e77bdd03fb..5075efa83f 100644
--- a/src/app/child-dev-project/previous-schools/previous-schools.component.spec.ts
+++ b/src/app/child-dev-project/previous-schools/previous-schools.component.spec.ts
@@ -8,7 +8,6 @@ import {
import { PreviousSchoolsComponent } from "./previous-schools.component";
import { ChildrenService } from "../children/children.service";
-import { EntityMapperService } from "../../core/entity/entity-mapper.service";
import { ChildrenModule } from "../children/children.module";
import { RouterTestingModule } from "@angular/router/testing";
import { ConfirmationDialogModule } from "../../core/confirmation-dialog/confirmation-dialog.module";
@@ -17,15 +16,13 @@ import { Child } from "../children/model/child";
import { PanelConfig } from "../../core/entity-components/entity-details/EntityDetailsConfig";
import { ChildSchoolRelation } from "../children/model/childSchoolRelation";
import moment from "moment";
-import { SessionService } from "../../core/session/session-service/session.service";
-import { User } from "../../core/user/user";
+import { MockSessionModule } from "../../core/session/mock-session.module";
describe("PreviousSchoolsComponent", () => {
let component: PreviousSchoolsComponent;
let fixture: ComponentFixture;
let mockChildrenService: jasmine.SpyObj;
- let mockEntityMapper: jasmine.SpyObj;
const testChild = new Child("22");
@@ -35,22 +32,17 @@ describe("PreviousSchoolsComponent", () => {
mockChildrenService.getSchoolRelationsFor.and.resolveTo([
new ChildSchoolRelation(),
]);
- mockEntityMapper = jasmine.createSpyObj(["loadType"]);
- mockEntityMapper.loadType.and.resolveTo([]);
+
TestBed.configureTestingModule({
declarations: [PreviousSchoolsComponent],
imports: [
RouterTestingModule,
ChildrenModule,
ConfirmationDialogModule,
+ MockSessionModule.withState(),
],
providers: [
{ provide: ChildrenService, useValue: mockChildrenService },
- { provide: EntityMapperService, useValue: mockEntityMapper },
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
],
}).compileComponents();
})
diff --git a/src/app/child-dev-project/previous-schools/previous-schools.stories.ts b/src/app/child-dev-project/previous-schools/previous-schools.stories.ts
index a11716940b..7c4f330c96 100644
--- a/src/app/child-dev-project/previous-schools/previous-schools.stories.ts
+++ b/src/app/child-dev-project/previous-schools/previous-schools.stories.ts
@@ -22,10 +22,7 @@ import { LocalSession } from "../../core/session/session-service/local-session";
const database = PouchDatabase.createWithInMemoryDB();
const schemaService = new EntitySchemaService();
const entityMapper = new EntityMapperService(database, schemaService);
-const sessionService = new LocalSession(
- database,
- schemaService
-);
+const sessionService = new LocalSession(database);
const child = new Child("testChild");
const school1 = new School("1");
diff --git a/src/app/child-dev-project/schools/children-overview/children-overview.component.spec.ts b/src/app/child-dev-project/schools/children-overview/children-overview.component.spec.ts
index ec51660e8b..fcc2a2584e 100644
--- a/src/app/child-dev-project/schools/children-overview/children-overview.component.spec.ts
+++ b/src/app/child-dev-project/schools/children-overview/children-overview.component.spec.ts
@@ -13,33 +13,26 @@ import { SchoolsService } from "../schools.service";
import { RouterTestingModule } from "@angular/router/testing";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { Router } from "@angular/router";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
-import { SessionService } from "../../../core/session/session-service/session.service";
-import { User } from "../../../core/user/user";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("ChildrenOverviewComponent", () => {
let component: ChildrenOverviewComponent;
let fixture: ComponentFixture;
- const schoolsService: jasmine.SpyObj = jasmine.createSpyObj(
- "schoolsService",
- ["getChildrenForSchool"]
- );
+ let mockSchoolsService: jasmine.SpyObj;
beforeEach(
waitForAsync(() => {
- const mockSessionService = jasmine.createSpyObj([
- "getCurrentUser",
- ]);
- mockSessionService.getCurrentUser.and.returnValue(new User());
+ mockSchoolsService = jasmine.createSpyObj(["getChildrenForSchool"]);
TestBed.configureTestingModule({
declarations: [],
- imports: [SchoolsModule, RouterTestingModule, NoopAnimationsModule],
- providers: [
- { provide: SchoolsService, useValue: schoolsService },
- { provide: EntityMapperService, useValue: {} },
- { provide: SessionService, useValue: mockSessionService },
+ imports: [
+ SchoolsModule,
+ RouterTestingModule,
+ NoopAnimationsModule,
+ MockSessionModule.withState(),
],
+ providers: [{ provide: SchoolsService, useValue: mockSchoolsService }],
}).compileComponents();
})
);
@@ -59,11 +52,11 @@ describe("ChildrenOverviewComponent", () => {
const child1 = new Child("c1");
const child2 = new Child("c2");
const config = { entity: school };
- schoolsService.getChildrenForSchool.and.resolveTo([child1, child2]);
+ mockSchoolsService.getChildrenForSchool.and.resolveTo([child1, child2]);
component.onInitFromDynamicConfig(config);
- expect(schoolsService.getChildrenForSchool).toHaveBeenCalledWith(
+ expect(mockSchoolsService.getChildrenForSchool).toHaveBeenCalledWith(
school.getId()
);
tick();
diff --git a/src/app/child-dev-project/schools/schools-list/schools-list.component.spec.ts b/src/app/child-dev-project/schools/schools-list/schools-list.component.spec.ts
index 4c9b690aa9..87ed645a40 100644
--- a/src/app/child-dev-project/schools/schools-list/schools-list.component.spec.ts
+++ b/src/app/child-dev-project/schools/schools-list/schools-list.component.spec.ts
@@ -6,9 +6,7 @@ import {
waitForAsync,
} from "@angular/core/testing";
import { SchoolsListComponent } from "./schools-list.component";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { ActivatedRoute, Router } from "@angular/router";
-import { SessionService } from "../../../core/session/session-service/session.service";
import { of } from "rxjs";
import { SchoolsModule } from "../schools.module";
import { RouterTestingModule } from "@angular/router/testing";
@@ -16,8 +14,9 @@ import { Angulartics2Module } from "angulartics2";
import { School } from "../model/school";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { EntityListConfig } from "../../../core/entity-components/entity-list/EntityListConfig";
-import { User } from "../../../core/user/user";
import { ExportService } from "../../../core/export/export-service/export.service";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
+import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
describe("SchoolsListComponent", () => {
let component: SchoolsListComponent;
@@ -44,17 +43,12 @@ describe("SchoolsListComponent", () => {
};
const routeMock = {
- data: of(routeData),
+ data: of({ config: routeData }),
queryParams: of({}),
};
- let mockEntityMapper: jasmine.SpyObj;
beforeEach(
waitForAsync(() => {
- const mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
- mockSessionService.getCurrentUser.and.returnValue(new User("test1"));
- mockEntityMapper = jasmine.createSpyObj(["loadType", "save"]);
- mockEntityMapper.loadType.and.resolveTo([]);
TestBed.configureTestingModule({
declarations: [],
imports: [
@@ -62,11 +56,10 @@ describe("SchoolsListComponent", () => {
RouterTestingModule,
Angulartics2Module.forRoot(),
NoopAnimationsModule,
+ MockSessionModule.withState(),
],
providers: [
{ provide: ActivatedRoute, useValue: routeMock },
- { provide: SessionService, useValue: mockSessionService },
- { provide: EntityMapperService, useValue: mockEntityMapper },
{ provide: ExportService, useValue: {} },
],
}).compileComponents();
@@ -86,10 +79,13 @@ describe("SchoolsListComponent", () => {
it("should load the schools", fakeAsync(() => {
const school1 = new School("s1");
const school2 = new School("s2");
- mockEntityMapper.loadType.and.resolveTo([school1, school2]);
+ const loadTypeSpy = spyOn(TestBed.inject(EntityMapperService), "loadType");
+ loadTypeSpy.and.resolveTo([school1, school2]);
+
component.ngOnInit();
tick();
- expect(mockEntityMapper.loadType).toHaveBeenCalledWith(School);
+
+ expect(loadTypeSpy).toHaveBeenCalledWith(School);
expect(component.schoolList).toEqual([school1, school2]);
}));
diff --git a/src/app/child-dev-project/schools/schools-list/schools-list.component.ts b/src/app/child-dev-project/schools/schools-list/schools-list.component.ts
index bce3a672e6..f5aa33c9ac 100644
--- a/src/app/child-dev-project/schools/schools-list/schools-list.component.ts
+++ b/src/app/child-dev-project/schools/schools-list/schools-list.component.ts
@@ -5,6 +5,7 @@ import { UntilDestroy } from "@ngneat/until-destroy";
import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
import { EntityListComponent } from "../../../core/entity-components/entity-list/entity-list.component";
import { EntityListConfig } from "../../../core/entity-components/entity-list/EntityListConfig";
+import { RouteData } from "../../../core/view/dynamic-routing/view-config.interface";
@UntilDestroy()
@Component({
@@ -34,7 +35,7 @@ export class SchoolsListComponent implements OnInit {
ngOnInit() {
this.route.data.subscribe(
- (config: EntityListConfig) => (this.listConfig = config)
+ (data: RouteData) => (this.listConfig = data.config)
);
this.entityMapper
.loadType(School)
diff --git a/src/app/core/admin/admin.guard.spec.ts b/src/app/core/admin/admin.guard.spec.ts
deleted file mode 100644
index bea48a535e..0000000000
--- a/src/app/core/admin/admin.guard.spec.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { TestBed, inject } from "@angular/core/testing";
-
-import { AdminGuard } from "./admin.guard";
-import { SessionService } from "../session/session-service/session.service";
-import { User } from "../user/user";
-
-describe("AdminGuard", () => {
- let mockSessionService;
-
- beforeEach(() => {
- const testUser = new User("");
- testUser.admin = true;
- mockSessionService = {
- getCurrentUser: () => {
- return testUser;
- },
- };
-
- TestBed.configureTestingModule({
- providers: [
- AdminGuard,
- { provide: SessionService, useValue: mockSessionService },
- ],
- });
- });
-
- it("should ...", inject([AdminGuard], (guard: AdminGuard) => {
- expect(guard).toBeTruthy();
- }));
-});
diff --git a/src/app/core/admin/admin.guard.ts b/src/app/core/admin/admin.guard.ts
deleted file mode 100644
index 0677183403..0000000000
--- a/src/app/core/admin/admin.guard.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Injectable } from "@angular/core";
-import {
- CanActivate,
- ActivatedRouteSnapshot,
- RouterStateSnapshot,
-} from "@angular/router";
-import { Observable } from "rxjs";
-import { SessionService } from "../session/session-service/session.service";
-
-/**
- * Guard checking for the currently logged in user's admin permissions.
- */
-@Injectable({
- providedIn: "root",
-})
-export class AdminGuard implements CanActivate {
- constructor(private _sessionService: SessionService) {}
-
- /**
- * Whether the currently logged in user (if any) has administrative rights.
- */
- public isAdmin(): boolean {
- if (this._sessionService.isLoggedIn()) {
- return this._sessionService.getCurrentUser().isAdmin();
- }
- }
-
- /**
- * Allows activation (i.e. returns true) only if a user with admin rights is currently logged in.
- * (used by Angular Routing system when added to certain routes)
- *
- * @param next The next route navigated to if allowed (provided by Angular Routing)
- * @param state The current state (provided by Angular Routing)
- */
- canActivate(
- next: ActivatedRouteSnapshot,
- state: RouterStateSnapshot
- ): Observable | Promise | boolean {
- return this.isAdmin();
- }
-}
diff --git a/src/app/core/admin/admin.module.ts b/src/app/core/admin/admin.module.ts
index 539e699e8d..03612692ef 100644
--- a/src/app/core/admin/admin.module.ts
+++ b/src/app/core/admin/admin.module.ts
@@ -5,7 +5,6 @@ import { MatButtonModule } from "@angular/material/button";
import { MatSnackBarModule } from "@angular/material/snack-bar";
import { BrowserModule } from "@angular/platform-browser";
import { AlertsModule } from "../alerts/alerts.module";
-import { AdminGuard } from "./admin.guard";
import { EntityModule } from "../entity/entity.module";
import { HttpClientModule } from "@angular/common/http";
import { ChildPhotoUpdateService } from "./services/child-photo-update.service";
@@ -43,6 +42,6 @@ import { MatTooltipModule } from "@angular/material/tooltip";
MatTooltipModule,
],
declarations: [AdminComponent, UserListComponent],
- providers: [AdminGuard, ChildPhotoUpdateService, BackupService],
+ providers: [ChildPhotoUpdateService, BackupService],
})
export class AdminModule {}
diff --git a/src/app/core/admin/admin/admin.component.html b/src/app/core/admin/admin/admin.component.html
index 8ee2ada567..a5e51a6403 100644
--- a/src/app/core/admin/admin/admin.component.html
+++ b/src/app/core/admin/admin/admin.component.html
@@ -43,6 +43,9 @@
Utility Functions
+
diff --git a/src/app/core/admin/admin/admin.component.spec.ts b/src/app/core/admin/admin/admin.component.spec.ts
index 51f19e6583..ec87335b87 100644
--- a/src/app/core/admin/admin/admin.component.spec.ts
+++ b/src/app/core/admin/admin/admin.component.spec.ts
@@ -23,6 +23,7 @@ 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";
describe("AdminComponent", () => {
let component: AdminComponent;
@@ -119,6 +120,10 @@ describe("AdminComponent", () => {
provide: ChildrenMigrationService,
useValue: {},
},
+ {
+ provide: PermissionsMigrationService,
+ useValue: {},
+ },
],
}).compileComponents();
})
diff --git a/src/app/core/admin/admin/admin.component.ts b/src/app/core/admin/admin/admin.component.ts
index cefa00081d..219fa50b27 100644
--- a/src/app/core/admin/admin/admin.component.ts
+++ b/src/app/core/admin/admin/admin.component.ts
@@ -13,6 +13,7 @@ import { AttendanceMigrationService } from "../../../child-dev-project/attendanc
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";
/**
@@ -45,7 +46,8 @@ export class AdminComponent implements OnInit {
public attendanceMigration: AttendanceMigrationService,
public notesMigration: NotesMigrationService,
public childrenMigrationService: ChildrenMigrationService,
- public configMigrationService: ConfigMigrationService
+ public configMigrationService: ConfigMigrationService,
+ public permissionsMigrationService: PermissionsMigrationService
) {}
ngOnInit() {
diff --git a/src/app/core/admin/user-list/user-list.component.html b/src/app/core/admin/user-list/user-list.component.html
index 01cd4b8bed..fe3e97e7af 100644
--- a/src/app/core/admin/user-list/user-list.component.html
+++ b/src/app/core/admin/user-list/user-list.component.html
@@ -7,15 +7,6 @@
Username
{{ user.name }}
-
-
Admin
-
-
-
-
Details
diff --git a/src/app/core/admin/user-list/user-list.component.spec.ts b/src/app/core/admin/user-list/user-list.component.spec.ts
index e9f235580b..e96ba7c872 100644
--- a/src/app/core/admin/user-list/user-list.component.spec.ts
+++ b/src/app/core/admin/user-list/user-list.component.spec.ts
@@ -4,14 +4,12 @@ import { UserListComponent } from "./user-list.component";
import { EntityMapperService } from "../../entity/entity-mapper.service";
import { AdminModule } from "../admin.module";
import { User } from "../../user/user";
-import { SessionService } from "../../session/session-service/session.service";
describe("UserListComponent", () => {
let component: UserListComponent;
let fixture: ComponentFixture;
let mockEntityMapper: jasmine.SpyObj;
- let mockSessionService: jasmine.SpyObj;
let testUsers: User[];
beforeEach(
@@ -23,15 +21,10 @@ describe("UserListComponent", () => {
]);
mockEntityMapper.loadType.and.returnValue(Promise.resolve(testUsers));
- mockSessionService = jasmine.createSpyObj("mockSessionService", [
- "getCurrentUser",
- ]);
-
TestBed.configureTestingModule({
imports: [AdminModule],
providers: [
{ provide: EntityMapperService, useValue: mockEntityMapper },
- { provide: SessionService, useValue: mockSessionService },
],
}).compileComponents();
})
@@ -46,34 +39,4 @@ describe("UserListComponent", () => {
it("should create", () => {
expect(component).toBeTruthy();
});
-
- it("should makeAdmin and save if user has admin rights", async () => {
- const currentUser = new User("tester");
- currentUser.setAdmin(true);
- mockSessionService.getCurrentUser.and.returnValue(currentUser);
-
- await component.makeAdmin(testUsers[0], true);
- expect(testUsers[0].isAdmin()).toBeTruthy();
- expect(mockEntityMapper.save).toHaveBeenCalledWith(testUsers[0]);
- });
-
- it("should not makeAdmin if user has no admin rights", async () => {
- const currentUser = new User("tester");
- currentUser.setAdmin(false);
- mockSessionService.getCurrentUser.and.returnValue(currentUser);
-
- await component.makeAdmin(testUsers[0], true);
- expect(testUsers[0].isAdmin()).toBeFalsy();
- expect(mockEntityMapper.save).not.toHaveBeenCalled();
- });
-
- it("should not let you remove your own admin rights", async () => {
- const currentUser = new User("1");
- currentUser.setAdmin(true);
- mockSessionService.getCurrentUser.and.returnValue(currentUser);
-
- await component.makeAdmin(currentUser, false);
- expect(currentUser.isAdmin()).toBeTruthy();
- expect(mockEntityMapper.save).not.toHaveBeenCalled();
- });
});
diff --git a/src/app/core/admin/user-list/user-list.component.ts b/src/app/core/admin/user-list/user-list.component.ts
index c361d957d4..3eec63ba11 100644
--- a/src/app/core/admin/user-list/user-list.component.ts
+++ b/src/app/core/admin/user-list/user-list.component.ts
@@ -2,7 +2,6 @@ import { User } from "../../user/user";
import { EntityMapperService } from "../../entity/entity-mapper.service";
import { MatTableDataSource } from "@angular/material/table";
import { Component, OnInit } from "@angular/core";
-import { SessionService } from "../../session/session-service/session.service";
/**
* Display all available users.
@@ -14,17 +13,14 @@ import { SessionService } from "../../session/session-service/session.service";
})
export class UserListComponent implements OnInit {
/** displayed columns for the list table in the template */
- displayedColumns = ["id", "name", "admin", "details"];
+ displayedColumns = ["id", "name", "details"];
/** datasource for the list table in the template */
dataSource = new MatTableDataSource();
/** additional technical details of a user mapped to the user id as key */
debugDetails = new Map();
- constructor(
- private entityMapperService: EntityMapperService,
- private sessionService: SessionService
- ) {}
+ constructor(private entityMapperService: EntityMapperService) {}
async ngOnInit() {
await this.loadData();
@@ -36,27 +32,4 @@ export class UserListComponent implements OnInit {
this.debugDetails.set(user.getId(), JSON.stringify(user))
);
}
-
- /**
- * Change the admin role of the given user and save the entity.
- *
- * This requires the currently logged in user to be admin.
- *
- * @param user The user to be updated
- * @param admin Whether to assign or remove admin role to the given user
- */
- async makeAdmin(user: User, admin: boolean) {
- if (!this.sessionService.getCurrentUser().isAdmin()) {
- this.loadData();
- return;
- }
- if (this.sessionService.getCurrentUser().getId() === user.getId()) {
- // do not change own user to avoid removing your own admin rights by accident
- this.loadData();
- return;
- }
-
- user.setAdmin(admin);
- await this.entityMapperService.save(user);
- }
}
diff --git a/src/app/core/analytics/analytics.service.spec.ts b/src/app/core/analytics/analytics.service.spec.ts
index df8a2d9768..04cd58094c 100644
--- a/src/app/core/analytics/analytics.service.spec.ts
+++ b/src/app/core/analytics/analytics.service.spec.ts
@@ -3,22 +3,18 @@ import { TestBed } from "@angular/core/testing";
import { AnalyticsService } from "./analytics.service";
import { Angulartics2Module } from "angulartics2";
import { RouterTestingModule } from "@angular/router/testing";
-import { SessionService } from "../session/session-service/session.service";
-import { StateHandler } from "../session/session-states/state-handler";
-import { LoginState } from "../session/session-states/login-state.enum";
+import { MockSessionModule } from "../session/mock-session.module";
describe("AnalyticsService", () => {
let service: AnalyticsService;
beforeEach(() => {
- const mockSessionService = jasmine.createSpyObj(["getLoginState"]);
- mockSessionService.getLoginState.and.returnValue(
- new StateHandler(LoginState.LOGGED_OUT)
- );
-
TestBed.configureTestingModule({
- imports: [Angulartics2Module.forRoot(), RouterTestingModule],
- providers: [{ provide: SessionService, useValue: mockSessionService }],
+ imports: [
+ Angulartics2Module.forRoot(),
+ RouterTestingModule,
+ MockSessionModule.withState(),
+ ],
});
service = TestBed.inject(AnalyticsService);
});
diff --git a/src/app/core/analytics/analytics.service.ts b/src/app/core/analytics/analytics.service.ts
index 3424704e27..f592773b5d 100644
--- a/src/app/core/analytics/analytics.service.ts
+++ b/src/app/core/analytics/analytics.service.ts
@@ -17,7 +17,7 @@ const md5 = require("md5");
})
export class AnalyticsService {
private static getUserHash(username: string) {
- return md5(AppConfig.settings.site_name + username);
+ return md5(AppConfig.settings?.site_name + username);
}
constructor(
@@ -46,16 +46,13 @@ export class AnalyticsService {
}
private subscribeToUserChanges() {
- this.sessionService
- .getLoginState()
- .getStateChangedStream()
- .subscribe((newState) => {
- if (newState.toState === LoginState.LOGGED_IN) {
- this.setUser(this.sessionService.getCurrentUser().name);
- } else {
- this.setUser(undefined);
- }
- });
+ this.sessionService.loginState.subscribe((newState) => {
+ if (newState === LoginState.LOGGED_IN) {
+ this.setUser(this.sessionService.getCurrentUser().name);
+ } else {
+ this.setUser(undefined);
+ }
+ });
}
/**
diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts
index ffcdd138ef..f7e1028e1a 100644
--- a/src/app/core/config/config-fix.ts
+++ b/src/app/core/config/config-fix.ts
@@ -245,15 +245,15 @@ export const defaultJsonConfig = {
},
"view:admin": {
"component": "Admin",
- "requiresAdmin": true
+ "permittedUserRoles": ["admin_app"]
},
"view:users": {
"component": "UserList",
- "requiresAdmin": true
+ "permittedUserRoles": ["admin_app"]
},
"view:admin/conflicts": {
"component": "ConflictResolution",
- "requiresAdmin": true,
+ "permittedUserRoles": ["admin_app"],
"lazyLoaded": true
},
"view:help": {
diff --git a/src/app/core/dashboard/dashboard/dashboard.component.spec.ts b/src/app/core/dashboard/dashboard/dashboard.component.spec.ts
index 663d615985..3570e82ad8 100644
--- a/src/app/core/dashboard/dashboard/dashboard.component.spec.ts
+++ b/src/app/core/dashboard/dashboard/dashboard.component.spec.ts
@@ -3,15 +3,19 @@ import { DashboardComponent } from "./dashboard.component";
import { ActivatedRoute } from "@angular/router";
import { BehaviorSubject } from "rxjs";
import { ProgressDashboardComponent } from "../../../child-dev-project/progress-dashboard-widget/progress-dashboard/progress-dashboard.component";
+import { RouteData } from "../../view/dynamic-routing/view-config.interface";
+import { DynamicComponentConfig } from "../../view/dynamic-components/dynamic-component-config.interface";
describe("DashboardComponent", () => {
let component: DashboardComponent;
let fixture: ComponentFixture;
- let mockRouteData: BehaviorSubject;
+ let mockRouteData: BehaviorSubject<
+ RouteData<{ widgets: DynamicComponentConfig[] }>
+ >;
beforeEach(() => {
- mockRouteData = new BehaviorSubject({ widgets: [] });
+ mockRouteData = new BehaviorSubject({ config: { widgets: [] } });
TestBed.configureTestingModule({
declarations: [DashboardComponent, ProgressDashboardComponent],
@@ -44,7 +48,7 @@ describe("DashboardComponent", () => {
],
};
- mockRouteData.next(testDashboardConfig);
+ mockRouteData.next({ config: testDashboardConfig });
expect(component.widgets).toEqual(testDashboardConfig.widgets);
});
diff --git a/src/app/core/dashboard/dashboard/dashboard.component.ts b/src/app/core/dashboard/dashboard/dashboard.component.ts
index 4c8bf99af8..46d64eaa9b 100644
--- a/src/app/core/dashboard/dashboard/dashboard.component.ts
+++ b/src/app/core/dashboard/dashboard/dashboard.component.ts
@@ -18,6 +18,7 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { DynamicComponentConfig } from "../../view/dynamic-components/dynamic-component-config.interface";
+import { RouteData } from "../../view/dynamic-routing/view-config.interface";
@Component({
selector: "app-dashboard",
@@ -30,8 +31,10 @@ export class DashboardComponent implements OnInit {
constructor(private activatedRoute: ActivatedRoute) {}
ngOnInit() {
- this.activatedRoute.data.subscribe((config) => {
- this.widgets = config.widgets;
- });
+ this.activatedRoute.data.subscribe(
+ (data: RouteData<{ widgets: DynamicComponentConfig[] }>) => {
+ this.widgets = data.config.widgets;
+ }
+ );
}
}
diff --git a/src/app/core/database/pouch-database.ts b/src/app/core/database/pouch-database.ts
index c26f7af92b..8ed94efb5b 100644
--- a/src/app/core/database/pouch-database.ts
+++ b/src/app/core/database/pouch-database.ts
@@ -29,9 +29,15 @@ import { PerformanceAnalysisLogging } from "../../utils/performance-analysis-log
* should be implemented in the abstract {@link Database}.
*/
export class PouchDatabase extends Database {
- static async createWithData(data: any[]): Promise {
+ /**
+ * Creates a PouchDB in-memory instance in which the passed documents are saved.
+ * The functions returns immediately but the documents are saved asynchronously.
+ * In tests use `tick()` or `waitForAsync()` to prevent accessing documents before they are saved.
+ * @param data an array of documents
+ */
+ static createWithData(data: any[]): PouchDatabase {
const instance = PouchDatabase.createWithInMemoryDB();
- await Promise.all(data.map((doc) => instance.put(doc)));
+ data.forEach((doc) => instance.put(doc, true));
return instance;
}
diff --git a/src/app/core/entity-components/entity-details/entity-details.component.spec.ts b/src/app/core/entity-components/entity-details/entity-details.component.spec.ts
index 5ca121ed6c..e8d97e5f23 100644
--- a/src/app/core/entity-components/entity-details/entity-details.component.spec.ts
+++ b/src/app/core/entity-components/entity-details/entity-details.component.spec.ts
@@ -12,14 +12,13 @@ import { ActivatedRoute, Router } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { MatSnackBar } from "@angular/material/snack-bar";
import { EntityDetailsConfig, PanelConfig } from "./EntityDetailsConfig";
-import { EntityMapperService } from "../../entity/entity-mapper.service";
-import { User } from "../../user/user";
-import { SessionService } from "../../session/session-service/session.service";
import { ChildrenModule } from "../../../child-dev-project/children/children.module";
import { Child } from "../../../child-dev-project/children/model/child";
import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service";
import { EntityPermissionsService } from "../../permissions/entity-permissions.service";
import { ChildrenService } from "../../../child-dev-project/children/children.service";
+import { MockEntityMapperService } from "../../entity/mock-entity-mapper-service";
+import { MockSessionModule } from "../../session/mock-session.module";
describe("EntityDetailsComponent", () => {
let component: EntityDetailsComponent;
@@ -55,16 +54,15 @@ describe("EntityDetailsComponent", () => {
routeObserver = observer;
observer.next({ get: () => "new" });
}),
- data: of(routeConfig),
+ data: of({ config: routeConfig }),
};
const mockEntityPermissionsService: jasmine.SpyObj = jasmine.createSpyObj(
["userIsPermitted"]
);
- let mockEntityMapper: jasmine.SpyObj;
- let mockSessionService: jasmine.SpyObj;
let mockChildrenService: jasmine.SpyObj;
+ let mockedEntityMapper: MockEntityMapperService;
beforeEach(
waitForAsync(() => {
@@ -74,28 +72,23 @@ describe("EntityDetailsComponent", () => {
]);
mockChildrenService.getSchoolRelationsFor.and.resolveTo([]);
mockChildrenService.getAserResultsOfChild.and.returnValue(of([]));
- mockEntityMapper = jasmine.createSpyObj([
- "loadType",
- "load",
- "remove",
- "save",
- ]);
- mockEntityMapper.loadType.and.resolveTo([]);
- mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
- mockSessionService.getCurrentUser.and.returnValue(new User("Test-User"));
TestBed.configureTestingModule({
- imports: [ChildrenModule, MatNativeDateModule, RouterTestingModule],
+ imports: [
+ ChildrenModule,
+ MatNativeDateModule,
+ RouterTestingModule,
+ MockSessionModule.withState(),
+ ],
providers: [
{ provide: ActivatedRoute, useValue: mockedRoute },
{
provide: EntityPermissionsService,
useValue: mockEntityPermissionsService,
},
- { provide: EntityMapperService, useValue: mockEntityMapper },
- { provide: SessionService, useValue: mockSessionService },
{ provide: ChildrenService, useValue: mockChildrenService },
],
}).compileComponents();
+ mockedEntityMapper = TestBed.inject(MockEntityMapperService);
})
);
@@ -111,7 +104,7 @@ describe("EntityDetailsComponent", () => {
it("sets the panels config with child and creating status", fakeAsync(() => {
const testChild = new Child("Test-Child");
- mockEntityMapper.load.and.resolveTo(testChild);
+ mockedEntityMapper.add(testChild);
component.creatingNew = false;
routeObserver.next({ get: () => testChild.getId() });
tick();
@@ -127,10 +120,13 @@ describe("EntityDetailsComponent", () => {
it("should load the correct child on startup", fakeAsync(() => {
const testChild = new Child("Test-Child");
- mockEntityMapper.load.and.returnValue(Promise.resolve(testChild));
+ mockedEntityMapper.add(testChild);
+ spyOn(mockedEntityMapper, "load").and.callThrough();
+
routeObserver.next({ get: () => testChild.getId() });
tick();
- expect(mockEntityMapper.load).toHaveBeenCalledWith(
+
+ expect(mockedEntityMapper.load).toHaveBeenCalledWith(
Child,
testChild.getId()
);
@@ -147,17 +143,20 @@ describe("EntityDetailsComponent", () => {
const router = fixture.debugElement.injector.get(Router);
const dialogReturn: any = { afterClosed: () => of(true) };
spyOn(dialogRef, "openDialog").and.returnValue(dialogReturn);
- mockEntityMapper.remove.and.returnValue(Promise.resolve());
+ spyOn(mockedEntityMapper, "remove").and.resolveTo();
+ spyOn(mockedEntityMapper, "save").and.resolveTo();
spyOn(component, "navigateBack");
const snackBarReturn: any = { onAction: () => of({}) };
spyOn(snackBar, "open").and.returnValue(snackBarReturn);
spyOn(router, "navigate");
+
component.removeEntity();
tick();
+
expect(dialogRef.openDialog).toHaveBeenCalled();
- expect(mockEntityMapper.remove).toHaveBeenCalledWith(testChild);
+ expect(mockedEntityMapper.remove).toHaveBeenCalledWith(testChild);
expect(snackBar.open).toHaveBeenCalled();
- expect(mockEntityMapper.save).toHaveBeenCalledWith(testChild, true);
+ expect(mockedEntityMapper.save).toHaveBeenCalledWith(testChild, true);
expect(router.navigate).toHaveBeenCalled();
}));
diff --git a/src/app/core/entity-components/entity-details/entity-details.component.ts b/src/app/core/entity-components/entity-details/entity-details.component.ts
index 7b1a23c294..4610e02036 100644
--- a/src/app/core/entity-components/entity-details/entity-details.component.ts
+++ b/src/app/core/entity-components/entity-details/entity-details.component.ts
@@ -22,6 +22,7 @@ import {
import { User } from "../../user/user";
import { Note } from "../../../child-dev-project/notes/model/note";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { RouteData } from "../../view/dynamic-routing/view-config.interface";
export const ENTITY_MAP: Map> = new Map<
string,
@@ -67,9 +68,9 @@ export class EntityDetailsComponent {
private confirmationDialog: ConfirmationDialogService,
private permissionService: EntityPermissionsService
) {
- this.route.data.subscribe((config: EntityDetailsConfig) => {
- this.config = config;
- this.classNamesWithIcon = "fa fa-" + config.icon + " fa-fw";
+ this.route.data.subscribe((data: RouteData) => {
+ this.config = data.config;
+ this.classNamesWithIcon = "fa fa-" + data.config.icon + " fa-fw";
this.route.paramMap.subscribe((params) =>
this.loadEntity(params.get("id"))
);
diff --git a/src/app/core/entity-components/entity-form/entity-form/entity-form.component.spec.ts b/src/app/core/entity-components/entity-form/entity-form/entity-form.component.spec.ts
index 8daaf1657d..5ab6dc1017 100644
--- a/src/app/core/entity-components/entity-form/entity-form/entity-form.component.spec.ts
+++ b/src/app/core/entity-components/entity-form/entity-form/entity-form.component.spec.ts
@@ -3,10 +3,7 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { EntityFormComponent } from "./entity-form.component";
import { ChildPhotoService } from "../../../../child-dev-project/children/child-photo-service/child-photo.service";
import { Entity } from "../../../entity/model/entity";
-import { EntityMapperService } from "../../../entity/entity-mapper.service";
-import { User } from "../../../user/user";
import { RouterTestingModule } from "@angular/router/testing";
-import { SessionService } from "../../../session/session-service/session.service";
import { ConfigService } from "../../../config/config.service";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { AlertService } from "../../../alerts/alert.service";
@@ -17,15 +14,14 @@ import { EntityFormModule } from "../entity-form.module";
import { FormBuilder } from "@angular/forms";
import { MatSnackBarModule } from "@angular/material/snack-bar";
import { EntityFormService } from "../entity-form.service";
+import { MockSessionModule } from "../../../session/mock-session.module";
describe("EntityFormComponent", () => {
let component: EntityFormComponent;
let fixture: ComponentFixture;
let mockChildPhotoService: jasmine.SpyObj;
- let mockSessionService: jasmine.SpyObj;
let mockConfigService: jasmine.SpyObj;
- let mockEntityMapper: jasmine.SpyObj;
let mockEntitySchemaService: jasmine.SpyObj;
const testChild = new Child("Test Name");
@@ -37,12 +33,7 @@ describe("EntityFormComponent", () => {
"setImage",
"getImage",
]);
- mockSessionService = jasmine.createSpyObj({
- getCurrentUser: new User("test-user"),
- });
mockConfigService = jasmine.createSpyObj(["getConfig"]);
- mockEntityMapper = jasmine.createSpyObj(["save"]);
- mockEntityMapper.save.and.resolveTo();
mockEntitySchemaService = jasmine.createSpyObj([
"getComponent",
"registerSchemaDatatype",
@@ -55,13 +46,12 @@ describe("EntityFormComponent", () => {
NoopAnimationsModule,
RouterTestingModule,
MatSnackBarModule,
+ MockSessionModule.withState(),
],
providers: [
FormBuilder,
AlertService,
- { provide: EntityMapperService, useValue: mockEntityMapper },
{ provide: ChildPhotoService, useValue: mockChildPhotoService },
- { provide: SessionService, useValue: mockSessionService },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: EntitySchemaService, useValue: mockEntitySchemaService },
],
diff --git a/src/app/core/entity-components/entity-form/entity-form/entity-form.stories.ts b/src/app/core/entity-components/entity-form/entity-form/entity-form.stories.ts
index 969df3188f..e0113312fb 100644
--- a/src/app/core/entity-components/entity-form/entity-form/entity-form.stories.ts
+++ b/src/app/core/entity-components/entity-form/entity-form/entity-form.stories.ts
@@ -3,10 +3,8 @@ import { Meta, Story } from "@storybook/angular/types-6-0";
import { EntityMapperService } from "../../../entity/entity-mapper.service";
import { AlertService } from "../../../alerts/alert.service";
import { ChildPhotoService } from "../../../../child-dev-project/children/child-photo-service/child-photo.service";
-import { SessionService } from "../../../session/session-service/session.service";
import { Child } from "../../../../child-dev-project/children/model/child";
import { RouterTestingModule } from "@angular/router/testing";
-import { User } from "../../../user/user";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { EntityPermissionsService } from "../../../permissions/entity-permissions.service";
import { ChildrenModule } from "../../../../child-dev-project/children/children.module";
@@ -46,10 +44,6 @@ export default {
useValue: { addDanger: () => null, addInfo: () => null },
},
{ provide: ChildPhotoService, useValue: { canSetImage: () => true } },
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
{
provide: EntityPermissionsService,
useValue: { userIsPermitted: () => true },
diff --git a/src/app/core/entity-components/entity-list/entity-list.component.spec.ts b/src/app/core/entity-components/entity-list/entity-list.component.spec.ts
index 421c3e4127..028c9771de 100644
--- a/src/app/core/entity-components/entity-list/entity-list.component.spec.ts
+++ b/src/app/core/entity-components/entity-list/entity-list.component.spec.ts
@@ -6,9 +6,6 @@ import { RouterTestingModule } from "@angular/router/testing";
import { SimpleChange } from "@angular/core";
import { BooleanFilterConfig, EntityListConfig } from "./EntityListConfig";
import { Entity } from "../../entity/model/entity";
-import { EntityMapperService } from "../../entity/entity-mapper.service";
-import { User } from "../../user/user";
-import { SessionService } from "../../session/session-service/session.service";
import { ChildrenListComponent } from "../../../child-dev-project/children/children-list/children-list.component";
import { Child } from "../../../child-dev-project/children/model/child";
import { ConfigService } from "../../config/config.service";
@@ -21,6 +18,7 @@ import { ReactiveFormsModule } from "@angular/forms";
import { AttendanceService } from "../../../child-dev-project/attendance/attendance.service";
import { ExportModule } from "../../export/export.module";
import { ExportService } from "../../export/export-service/export.service";
+import { MockSessionModule } from "../../session/mock-session.module";
describe("EntityListComponent", () => {
let component: EntityListComponent;
@@ -72,18 +70,13 @@ describe("EntityListComponent", () => {
};
let mockConfigService: jasmine.SpyObj;
let mockLoggingService: jasmine.SpyObj;
- let mockSessionService: jasmine.SpyObj;
- let mockEntityMapper: jasmine.SpyObj;
let mockEntitySchemaService: jasmine.SpyObj;
let mockAttendanceService: jasmine.SpyObj;
beforeEach(
waitForAsync(() => {
- mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
- mockSessionService.getCurrentUser.and.returnValue(new User("test1"));
mockConfigService = jasmine.createSpyObj(["getConfig"]);
mockLoggingService = jasmine.createSpyObj(["warn"]);
- mockEntityMapper = jasmine.createSpyObj(["save"]);
mockEntitySchemaService = jasmine.createSpyObj([
"getComponent",
"registerSchemaDatatype",
@@ -109,12 +102,11 @@ describe("EntityListComponent", () => {
RouterTestingModule.withRoutes([
{ path: "child", component: ChildrenListComponent },
]),
+ MockSessionModule.withState(),
],
providers: [
DatePipe,
- { provide: SessionService, useValue: mockSessionService },
{ provide: ConfigService, useValue: mockConfigService },
- { provide: EntityMapperService, useValue: mockEntityMapper },
{ provide: LoggingService, useValue: mockLoggingService },
{ provide: ExportService, useValue: {} },
{ provide: EntitySchemaService, useValue: mockEntitySchemaService },
diff --git a/src/app/core/entity-components/entity-list/entity-list.stories.ts b/src/app/core/entity-components/entity-list/entity-list.stories.ts
index 8070ab70a3..b3ab22d03f 100644
--- a/src/app/core/entity-components/entity-list/entity-list.stories.ts
+++ b/src/app/core/entity-components/entity-list/entity-list.stories.ts
@@ -4,7 +4,6 @@ import { EntityListComponent } from "./entity-list.component";
import { EntityListModule } from "./entity-list.module";
import { Child } from "../../../child-dev-project/children/model/child";
import { DemoChildGenerator } from "../../../child-dev-project/children/demo-data-generators/demo-child-generator.service";
-import { SessionService } from "../../session/session-service/session.service";
import { User } from "../../user/user";
import { RouterTestingModule } from "@angular/router/testing";
import { BackupService } from "../../admin/services/backup.service";
@@ -31,10 +30,6 @@ export default {
],
providers: [
DatePipe,
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => user },
- },
{ provide: BackupService, useValue: {} },
{
provide: EntityMapperService,
diff --git a/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts b/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts
index 13984588c1..636d84da77 100644
--- a/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts
+++ b/src/app/core/entity-components/entity-subrecord/entity-subrecord/entity-subrecord.component.spec.ts
@@ -30,37 +30,27 @@ import { ConfirmationDialogService } from "../../../confirmation-dialog/confirma
import { MatSnackBar } from "@angular/material/snack-bar";
import { genders } from "../../../../child-dev-project/children/model/genders";
import { LoggingService } from "../../../logging/logging.service";
-import { SessionService } from "../../../session/session-service/session.service";
-import { User } from "../../../user/user";
+import { MockSessionModule } from "../../../session/mock-session.module";
describe("EntitySubrecordComponent", () => {
let component: EntitySubrecordComponent;
let fixture: ComponentFixture>;
- let mockEntityMapper: jasmine.SpyObj;
+ let entityMapper: EntityMapperService;
beforeEach(
waitForAsync(() => {
- mockEntityMapper = jasmine.createSpyObj(["remove", "save"]);
- mockEntityMapper.save.and.resolveTo();
- const mockSessionService = jasmine.createSpyObj([
- "getCurrentUser",
- ]);
- mockSessionService.getCurrentUser.and.returnValue(new User());
-
TestBed.configureTestingModule({
imports: [
EntitySubrecordModule,
RouterTestingModule,
MatNativeDateModule,
NoopAnimationsModule,
+ MockSessionModule.withState(),
],
- providers: [
- DatePipe,
- PercentPipe,
- { provide: EntityMapperService, useValue: mockEntityMapper },
- { provide: SessionService, useValue: mockSessionService },
- ],
+ providers: [DatePipe, PercentPipe],
}).compileComponents();
+
+ entityMapper = TestBed.inject(EntityMapperService);
})
);
@@ -251,7 +241,7 @@ describe("EntitySubrecordComponent", () => {
});
it("should correctly save changes to an entity", fakeAsync(() => {
- mockEntityMapper.save.and.resolveTo();
+ spyOn(entityMapper, "save").and.resolveTo();
const fb = TestBed.inject(FormBuilder);
const child = new Child();
child.name = "Old Name";
@@ -264,7 +254,7 @@ describe("EntitySubrecordComponent", () => {
component.save(tableRow);
tick();
- expect(mockEntityMapper.save).toHaveBeenCalledWith(tableRow.record);
+ expect(entityMapper.save).toHaveBeenCalledWith(tableRow.record);
expect(tableRow.record.name).toBe("New Name");
expect(tableRow.record.gender).toBe(genders[2]);
expect(tableRow.formGroup.disabled).toBeTrue();
@@ -303,7 +293,8 @@ describe("EntitySubrecordComponent", () => {
spyOn(snackbarService, "open").and.returnValue({
onAction: () => snackbarObservable,
} as any);
- mockEntityMapper.remove.and.resolveTo();
+ spyOn(entityMapper, "remove").and.resolveTo();
+ spyOn(entityMapper, "save").and.resolveTo();
const child = new Child();
component.records = [child];
@@ -313,11 +304,11 @@ describe("EntitySubrecordComponent", () => {
dialogObservable.next(true);
tick();
- expect(mockEntityMapper.remove).toHaveBeenCalledWith(child);
+ expect(entityMapper.remove).toHaveBeenCalledWith(child);
expect(component.records).toEqual([]);
snackbarObservable.next();
- expect(mockEntityMapper.save).toHaveBeenCalledWith(child, true);
+ expect(entityMapper.save).toHaveBeenCalledWith(child, true);
expect(component.records).toEqual([child]);
flush();
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 9c3d9a260a..59c3294371 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
@@ -20,8 +20,6 @@ import { ChildrenService } from "../../../../child-dev-project/children/children
import { of } from "rxjs";
import * as faker from "faker";
import { EntityPermissionsService } from "../../../permissions/entity-permissions.service";
-import { SessionService } from "../../../session/session-service/session.service";
-import { User } from "../../../user/user";
const configService = new ConfigService();
const schemaService = new EntitySchemaService();
@@ -77,10 +75,6 @@ export default {
provide: EntityPermissionsService,
useValue: { userIsPermitted: () => true },
},
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
],
}),
],
diff --git a/src/app/core/entity-components/entity-subrecord/list-paginator/list-paginator.component.spec.ts b/src/app/core/entity-components/entity-subrecord/list-paginator/list-paginator.component.spec.ts
index b6c91e38a8..aad7eb4089 100644
--- a/src/app/core/entity-components/entity-subrecord/list-paginator/list-paginator.component.spec.ts
+++ b/src/app/core/entity-components/entity-subrecord/list-paginator/list-paginator.component.spec.ts
@@ -9,30 +9,22 @@ import {
import { ListPaginatorComponent } from "./list-paginator.component";
import { EntityListModule } from "../../entity-list/entity-list.module";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
-import { SessionService } from "../../../session/session-service/session.service";
-import { EntityMapperService } from "../../../entity/entity-mapper.service";
import { MatTableDataSource } from "@angular/material/table";
-import { User } from "../../../user/user";
import { PageEvent } from "@angular/material/paginator";
+import { MockSessionModule } from "../../../session/mock-session.module";
+import { EntityMapperService } from "../../../entity/entity-mapper.service";
describe("ListPaginatorComponent", () => {
let component: ListPaginatorComponent;
let fixture: ComponentFixture>;
- let mockEntityMapper: jasmine.SpyObj;
- let mockSessionService: jasmine.SpyObj;
-
beforeEach(
waitForAsync(() => {
- mockEntityMapper = jasmine.createSpyObj(["save"]);
- mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
- mockSessionService.getCurrentUser.and.returnValue(new User());
-
TestBed.configureTestingModule({
- imports: [EntityListModule, NoopAnimationsModule],
- providers: [
- { provide: SessionService, useValue: mockSessionService },
- { provide: EntityMapperService, useValue: mockEntityMapper },
+ imports: [
+ EntityListModule,
+ NoopAnimationsModule,
+ MockSessionModule.withState(),
],
}).compileComponents();
})
@@ -51,11 +43,12 @@ describe("ListPaginatorComponent", () => {
it("should save pagination settings in the user entity", fakeAsync(() => {
component.idForSavingPagination = "table-id";
+ const saveEntitySpy = spyOn(TestBed.inject(EntityMapperService), "save");
component.onPaginateChange({ pageSize: 20, pageIndex: 1 } as PageEvent);
tick();
- expect(mockEntityMapper.save).toHaveBeenCalledWith(component.user);
+ expect(saveEntitySpy).toHaveBeenCalledWith(component.user);
expect(component.user.paginatorSettingsPageSize["table-id"]).toEqual(20);
expect(component.user.paginatorSettingsPageIndex["table-id"]).toEqual(1);
}));
diff --git a/src/app/core/entity-components/entity-subrecord/list-paginator/list-paginator.component.ts b/src/app/core/entity-components/entity-subrecord/list-paginator/list-paginator.component.ts
index 7b4b84097a..71b51c97fc 100644
--- a/src/app/core/entity-components/entity-subrecord/list-paginator/list-paginator.component.ts
+++ b/src/app/core/entity-components/entity-subrecord/list-paginator/list-paginator.component.ts
@@ -40,9 +40,7 @@ export class ListPaginatorComponent
constructor(
private sessionService: SessionService,
private entityMapperService: EntityMapperService
- ) {
- this.user = this.sessionService.getCurrentUser();
- }
+ ) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.hasOwnProperty("idForSavingPagination")) {
@@ -92,7 +90,9 @@ export class ListPaginatorComponent
this.updateUserPaginationSettings();
}
- private applyUserPaginationSettings() {
+ private async applyUserPaginationSettings() {
+ await this.ensureUserIsLoaded();
+
const pageSize = this.user.paginatorSettingsPageSize[
this.idForSavingPagination
];
@@ -109,7 +109,9 @@ export class ListPaginatorComponent
this.currentPageIndex;
}
- private updateUserPaginationSettings() {
+ private async updateUserPaginationSettings() {
+ await this.ensureUserIsLoaded();
+
// save "all" as -1
const sizeToBeSaved = this.showingAll ? -1 : this.pageSize;
@@ -126,7 +128,14 @@ export class ListPaginatorComponent
] = sizeToBeSaved;
if (hasChangesToBeSaved) {
- this.entityMapperService.save(this.user);
+ await this.entityMapperService.save(this.user);
+ }
+ }
+
+ private async ensureUserIsLoaded() {
+ if (!this.user) {
+ const currentUser = this.sessionService.getCurrentUser();
+ this.user = await this.entityMapperService.load(User, currentUser.name);
}
}
}
diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html
index e12719c6b8..d76b805c00 100644
--- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html
+++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html
@@ -6,7 +6,7 @@
/>
No photo set
{
let component: EditPhotoComponent;
@@ -14,11 +13,10 @@ describe("EditPhotoComponent", () => {
beforeEach(async () => {
mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
- mockSessionService.getCurrentUser.and.returnValue(new User());
await TestBed.configureTestingModule({
imports: [EntityDetailsModule, NoopAnimationsModule],
- declarations: [EditPhotoComponent],
providers: [{ provide: SessionService, useValue: mockSessionService }],
+ declarations: [EditPhotoComponent],
}).compileComponents();
});
@@ -42,4 +40,16 @@ describe("EditPhotoComponent", () => {
expect(component.formControl.value.path).toBe("new_file.name");
});
+
+ it("should allow editing photos when the user is admin", () => {
+ mockSessionService.getCurrentUser.and.returnValue({
+ name: "User",
+ roles: ["admin_app"],
+ });
+ expect(component.editPhotoAllowed).toBeFalse();
+
+ component.ngOnInit();
+
+ expect(component.editPhotoAllowed).toBeTrue();
+ });
});
diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.ts
index 472bffef3c..453b2f54c3 100644
--- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.ts
+++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.ts
@@ -1,21 +1,26 @@
-import { Component } from "@angular/core";
+import { Component, OnInit } from "@angular/core";
import { EditComponent } from "../edit-component";
import { Photo } from "../../../../../child-dev-project/children/child-photo-service/photo";
import { BehaviorSubject } from "rxjs";
import { ChildPhotoService } from "../../../../../child-dev-project/children/child-photo-service/child-photo.service";
import { SessionService } from "../../../../session/session-service/session.service";
-import { User } from "../../../../user/user";
@Component({
selector: "app-edit-photo",
templateUrl: "./edit-photo.component.html",
styleUrls: ["./edit-photo.component.scss"],
})
-export class EditPhotoComponent extends EditComponent {
- user: User;
+export class EditPhotoComponent extends EditComponent implements OnInit {
+ editPhotoAllowed = false;
+
constructor(private sessionService: SessionService) {
super();
- this.user = this.sessionService.getCurrentUser();
+ }
+
+ ngOnInit() {
+ if (this.sessionService.getCurrentUser()?.roles?.includes("admin_app")) {
+ this.editPhotoAllowed = true;
+ }
}
changeFilename(path: string) {
diff --git a/src/app/core/entity/entity-config.service.ts b/src/app/core/entity/entity-config.service.ts
index 1c44fd0863..64987d8f62 100644
--- a/src/app/core/entity/entity-config.service.ts
+++ b/src/app/core/entity/entity-config.service.ts
@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core";
-import { Entity } from "./model/entity";
+import { Entity, EntityConstructor } from "./model/entity";
import { ConfigService } from "../config/config.service";
import { EntitySchemaField } from "./schema/entity-schema-field";
import { addPropertySchema } from "./database-field.decorator";
@@ -26,7 +26,7 @@ export class EntityConfigService {
}
}
- public getEntityConfig(entityType: typeof Entity): EntityConfig {
+ public getEntityConfig(entityType: EntityConstructor): EntityConfig {
const configName =
EntityConfigService.PREFIX_ENTITY_CONFIG + entityType.ENTITY_TYPE;
return this.configService.getConfig(configName);
diff --git a/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.spec.ts b/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.spec.ts
index a9fb1589ea..78c77dbb58 100644
--- a/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.spec.ts
+++ b/src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.spec.ts
@@ -8,35 +8,28 @@ import { RouterTestingModule } from "@angular/router/testing";
import { MatDialogRef } from "@angular/material/dialog";
import { Subject } from "rxjs";
import { MatSnackBarModule } from "@angular/material/snack-bar";
-import { User } from "../../user/user";
-import { SessionService } from "../../session/session-service/session.service";
+import { MockSessionModule } from "../../session/mock-session.module";
describe("FormDialogWrapperComponent", () => {
let component: FormDialogWrapperComponent;
let fixture: ComponentFixture;
- let mockEntityMapper: jasmine.SpyObj;
+ let saveEntitySpy: jasmine.Spy;
beforeEach(
waitForAsync(() => {
- mockEntityMapper = jasmine.createSpyObj("mockEntityMapper", ["save"]);
-
TestBed.configureTestingModule({
imports: [
FormDialogModule,
Angulartics2Module.forRoot(),
RouterTestingModule,
MatSnackBarModule,
+ MockSessionModule.withState(),
],
- providers: [
- { provide: EntityMapperService, useValue: mockEntityMapper },
- { provide: MatDialogRef, useValue: {} },
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
- ],
+ providers: [{ provide: MatDialogRef, useValue: {} }],
}).compileComponents();
+
+ saveEntitySpy = spyOn(TestBed.inject(EntityMapperService), "save");
})
);
@@ -71,7 +64,7 @@ describe("FormDialogWrapperComponent", () => {
await component.save();
- expect(mockEntityMapper.save).toHaveBeenCalledWith(testEntity);
+ expect(saveEntitySpy).toHaveBeenCalledWith(testEntity);
});
it("should allow aborting save", async () => {
@@ -83,7 +76,7 @@ describe("FormDialogWrapperComponent", () => {
await component.save();
expect(component.beforeSave).toHaveBeenCalledWith(testEntity);
- expect(mockEntityMapper.save).not.toHaveBeenCalled();
+ expect(saveEntitySpy).not.toHaveBeenCalled();
});
it("should save entity as transformed by beforeSave", async () => {
@@ -96,6 +89,6 @@ describe("FormDialogWrapperComponent", () => {
await component.save();
expect(component.beforeSave).toHaveBeenCalledWith(testEntity);
- expect(mockEntityMapper.save).toHaveBeenCalledWith(transformedEntity);
+ expect(saveEntitySpy).toHaveBeenCalledWith(transformedEntity);
});
});
diff --git a/src/app/core/markdown-page/markdown-page/markdown-page.component.spec.ts b/src/app/core/markdown-page/markdown-page/markdown-page.component.spec.ts
index 99201f4ea4..319155f320 100644
--- a/src/app/core/markdown-page/markdown-page/markdown-page.component.spec.ts
+++ b/src/app/core/markdown-page/markdown-page/markdown-page.component.spec.ts
@@ -4,12 +4,13 @@ import { MarkdownPageComponent } from "./markdown-page.component";
import { ActivatedRoute } from "@angular/router";
import { BehaviorSubject } from "rxjs";
import { MarkdownPageConfig } from "../MarkdownPageConfig";
+import { RouteData } from "../../view/dynamic-routing/view-config.interface";
describe("HowToComponent", () => {
let component: MarkdownPageComponent;
let fixture: ComponentFixture;
- let mockRouteData: BehaviorSubject<{ config: MarkdownPageConfig }>;
+ let mockRouteData: BehaviorSubject>;
beforeEach(
waitForAsync(() => {
diff --git a/src/app/core/markdown-page/markdown-page/markdown-page.component.ts b/src/app/core/markdown-page/markdown-page/markdown-page.component.ts
index b0498a3002..fae2ed87a6 100644
--- a/src/app/core/markdown-page/markdown-page/markdown-page.component.ts
+++ b/src/app/core/markdown-page/markdown-page/markdown-page.component.ts
@@ -18,6 +18,7 @@
import { Component, Input, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { MarkdownPageConfig } from "../MarkdownPageConfig";
+import { RouteData } from "../../view/dynamic-routing/view-config.interface";
/**
* Display markdown formatted page that is dynamically loaded based on the file defined in config.
@@ -35,7 +36,8 @@ export class MarkdownPageComponent implements OnInit {
ngOnInit() {
this.route.data.subscribe(
- (config: MarkdownPageConfig) => (this.markdownFile = config.markdownFile)
+ (data: RouteData) =>
+ (this.markdownFile = data.config.markdownFile)
);
}
}
diff --git a/src/app/core/navigation/navigation/navigation.component.spec.ts b/src/app/core/navigation/navigation/navigation.component.spec.ts
index 206e93ffe6..e37ded3260 100644
--- a/src/app/core/navigation/navigation/navigation.component.spec.ts
+++ b/src/app/core/navigation/navigation/navigation.component.spec.ts
@@ -23,27 +23,28 @@ import { MenuItem } from "../menu-item";
import { MatDividerModule } from "@angular/material/divider";
import { MatIconModule } from "@angular/material/icon";
import { MatListModule } from "@angular/material/list";
-import { RouterService } from "../../view/dynamic-routing/router.service";
import { ConfigService } from "../../config/config.service";
import { BehaviorSubject } from "rxjs";
import { Config } from "../../config/config";
-import { AdminGuard } from "../../admin/admin.guard";
+import { UserRoleGuard } from "../../permissions/user-role.guard";
+import { ActivatedRouteSnapshot } from "@angular/router";
describe("NavigationComponent", () => {
let component: NavigationComponent;
let fixture: ComponentFixture;
let mockConfigService: jasmine.SpyObj;
- const mockConfigUpdated = new BehaviorSubject(null);
- let mockAdminGuard: jasmine.SpyObj;
+ let mockConfigUpdated: BehaviorSubject;
+ let mockUserRoleGuard: jasmine.SpyObj;
beforeEach(
waitForAsync(() => {
+ mockConfigUpdated = new BehaviorSubject(null);
mockConfigService = jasmine.createSpyObj(["getConfig"]);
mockConfigService.getConfig.and.returnValue({ items: [] });
mockConfigService.configUpdates = mockConfigUpdated;
- mockAdminGuard = jasmine.createSpyObj(["isAdmin"]);
- mockAdminGuard.isAdmin.and.returnValue(false);
+ mockUserRoleGuard = jasmine.createSpyObj(["canActivate"]);
+ mockUserRoleGuard.canActivate.and.returnValue(true);
TestBed.configureTestingModule({
imports: [
@@ -54,7 +55,7 @@ describe("NavigationComponent", () => {
],
declarations: [NavigationComponent],
providers: [
- { provide: AdminGuard, useValue: mockAdminGuard },
+ { provide: UserRoleGuard, useValue: mockUserRoleGuard },
{ provide: ConfigService, useValue: mockConfigService },
],
}).compileComponents();
@@ -78,7 +79,11 @@ describe("NavigationComponent", () => {
{ name: "Children", icon: "child", link: "/child" },
],
};
- mockConfigService.getConfig.and.returnValue(testConfig);
+ mockConfigService.getConfig.and.returnValues(
+ testConfig,
+ undefined,
+ undefined
+ );
mockConfigUpdated.next(null);
const items = component.menuItems;
@@ -95,19 +100,34 @@ describe("NavigationComponent", () => {
{ name: "Children", icon: "child", link: "/child" },
],
};
-
- mockConfigService.getConfig.and.callFake((id) => {
- switch (id) {
- case RouterService.PREFIX_VIEW_CONFIG + "dashboard":
- return { requiresAdmin: true } as any;
- case RouterService.PREFIX_VIEW_CONFIG + "child":
- return { requiresAdmin: false } as any;
- default:
- return testConfig;
+ mockConfigService.getConfig.and.returnValues(
+ testConfig,
+ { permittedUserRoles: ["admin"] },
+ undefined
+ );
+ mockUserRoleGuard.canActivate.and.callFake(
+ (route: ActivatedRouteSnapshot) => {
+ switch (route.routeConfig.path) {
+ case "dashboard":
+ return false;
+ case "child":
+ return true;
+ default:
+ return false;
+ }
}
- });
+ );
+
mockConfigUpdated.next(null);
+ expect(mockUserRoleGuard.canActivate).toHaveBeenCalledWith({
+ routeConfig: { path: "dashboard" },
+ data: { permittedUserRoles: ["admin"] },
+ } as any);
+ expect(mockUserRoleGuard.canActivate).toHaveBeenCalledWith({
+ routeConfig: { path: "child" },
+ data: { permittedUserRoles: undefined },
+ } as any);
expect(component.menuItems).toEqual([
new MenuItem("Children", "child", "/child"),
]);
diff --git a/src/app/core/navigation/navigation/navigation.component.ts b/src/app/core/navigation/navigation/navigation.component.ts
index 312cbbb360..c645d66a4c 100644
--- a/src/app/core/navigation/navigation/navigation.component.ts
+++ b/src/app/core/navigation/navigation/navigation.component.ts
@@ -17,12 +17,14 @@
import { Component } from "@angular/core";
import { MenuItem } from "../menu-item";
-import { AdminGuard } from "../../admin/admin.guard";
import { NavigationMenuConfig } from "../navigation-menu-config.interface";
-import { RouterService } from "../../view/dynamic-routing/router.service";
-import { ViewConfig } from "../../view/dynamic-routing/view-config.interface";
import { ConfigService } from "../../config/config.service";
+import { UserRoleGuard } from "../../permissions/user-role.guard";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import {
+ PREFIX_VIEW_CONFIG,
+ ViewConfig,
+} from "../../view/dynamic-routing/view-config.interface";
/**
* Main app menu listing.
@@ -40,7 +42,7 @@ export class NavigationComponent {
public menuItems: MenuItem[] = [];
constructor(
- private adminGuard: AdminGuard,
+ private userRoleGuard: UserRoleGuard,
private configService: ConfigService
) {
this.configService.configUpdates
@@ -69,9 +71,13 @@ export class NavigationComponent {
* Check whether the user has the required rights
*/
private checkMenuItemPermissions(link: string): boolean {
- const viewConfig = this.configService.getConfig(
- RouterService.PREFIX_VIEW_CONFIG + link.replace(/^\//, "")
- );
- return !viewConfig?.requiresAdmin || this.adminGuard.isAdmin();
+ const configPath = link.replace(/^\//, "");
+ const userRoles = this.configService.getConfig(
+ PREFIX_VIEW_CONFIG + configPath
+ )?.permittedUserRoles;
+ return this.userRoleGuard.canActivate({
+ routeConfig: { path: configPath },
+ data: { permittedUserRoles: userRoles },
+ } as any);
}
}
diff --git a/src/app/core/permissions/entity-permissions.service.spec.ts b/src/app/core/permissions/entity-permissions.service.spec.ts
index 6c7eebbfe7..b28eaa4f73 100644
--- a/src/app/core/permissions/entity-permissions.service.spec.ts
+++ b/src/app/core/permissions/entity-permissions.service.spec.ts
@@ -5,19 +5,18 @@ import {
OperationType,
} from "./entity-permissions.service";
import { SessionService } from "../session/session-service/session.service";
-import { User } from "../user/user";
import { Entity } from "../entity/model/entity";
import { EntityConfigService } from "../entity/entity-config.service";
describe("EntityPermissionsService", () => {
let service: EntityPermissionsService;
- const mockConfigService: jasmine.SpyObj = jasmine.createSpyObj(
- ["getEntityConfig"]
- );
- const mockSessionService: jasmine.SpyObj = jasmine.createSpyObj(
- ["getCurrentUser"]
- );
+ let mockConfigService: jasmine.SpyObj;
+ let mockSessionService: jasmine.SpyObj;
+
beforeEach(() => {
+ mockConfigService = jasmine.createSpyObj(["getEntityConfig"]);
+ mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
+
TestBed.configureTestingModule({
providers: [
{ provide: EntityConfigService, useValue: mockConfigService },
@@ -33,7 +32,9 @@ describe("EntityPermissionsService", () => {
it("should give permission if nothing is defined", () => {
mockConfigService.getEntityConfig.and.returnValue(null);
+
const permitted = service.userIsPermitted(Entity, OperationType.CREATE);
+
expect(permitted).toBeTrue();
});
@@ -41,29 +42,37 @@ describe("EntityPermissionsService", () => {
mockConfigService.getEntityConfig.and.returnValue({
permissions: { update: ["admin"] },
});
+
const permitted = service.userIsPermitted(Entity, OperationType.CREATE);
+
expect(permitted).toBeTrue();
});
- it("should not give user create permission when only admin is specified", () => {
- const noAdmin = new User();
- noAdmin.setAdmin(false);
- mockSessionService.getCurrentUser.and.returnValue(noAdmin);
+ it("should not give permission if user does not have any required role", () => {
+ mockSessionService.getCurrentUser.and.returnValue({
+ name: "noAdminUser",
+ roles: ["user_app"],
+ });
mockConfigService.getEntityConfig.and.returnValue({
- permissions: { create: ["admin"] },
+ permissions: { create: ["admin_app"] },
});
+
const permitted = service.userIsPermitted(Entity, OperationType.CREATE);
+
expect(permitted).toBeFalse();
});
- it("should give admin create permission when admin is specified", () => {
- const admin = new User();
- admin.setAdmin(true);
- mockSessionService.getCurrentUser.and.returnValue(admin);
+ it("should give permission when user has a required role", () => {
+ mockSessionService.getCurrentUser.and.returnValue({
+ name: "adminUser",
+ roles: ["user_app", "admin_app"],
+ });
mockConfigService.getEntityConfig.and.returnValue({
- permissions: { create: ["admin"] },
+ permissions: { create: ["admin_app"] },
});
+
const permitted = service.userIsPermitted(Entity, OperationType.CREATE);
+
expect(permitted).toBeTrue();
});
});
diff --git a/src/app/core/permissions/entity-permissions.service.ts b/src/app/core/permissions/entity-permissions.service.ts
index f13e914a0b..a85c69649d 100644
--- a/src/app/core/permissions/entity-permissions.service.ts
+++ b/src/app/core/permissions/entity-permissions.service.ts
@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core";
-import { Entity } from "../entity/model/entity";
+import { Entity, EntityConstructor } from "../entity/model/entity";
import { SessionService } from "../session/session-service/session.service";
import { EntityConfigService } from "../entity/entity-config.service";
@@ -13,21 +13,31 @@ export enum OperationType {
@Injectable({
providedIn: "root",
})
+/**
+ * This service manages the permissions of the currently logged in user for reading, updating, creating and deleting
+ * entities.
+ */
export class EntityPermissionsService {
constructor(
private entityConfigService: EntityConfigService,
private sessionService: SessionService
) {}
+ /**
+ * This method checks if the current user is permitted to perform the given operation on the given entity
+ * @param entity the constructor of the entity for which the permission is checked
+ * @param operation the operation for which the permission is checked
+ */
public userIsPermitted(
- entity: typeof Entity,
+ entity: EntityConstructor,
operation: OperationType
): boolean {
+ const currentUser = this.sessionService.getCurrentUser();
const entityConfig = this.entityConfigService.getEntityConfig(entity);
if (entityConfig?.permissions && entityConfig.permissions[operation]) {
- return (
- entityConfig.permissions[operation].includes("admin") &&
- this.sessionService.getCurrentUser().isAdmin()
+ // Check if the user has a role that is permitted for this operation
+ return entityConfig.permissions[operation].some((role) =>
+ currentUser.roles.includes(role)
);
}
return true;
diff --git a/src/app/core/permissions/permissions-migration.service.spec.ts b/src/app/core/permissions/permissions-migration.service.spec.ts
new file mode 100644
index 0000000000..dc1ab0f940
--- /dev/null
+++ b/src/app/core/permissions/permissions-migration.service.spec.ts
@@ -0,0 +1,68 @@
+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
new file mode 100644
index 0000000000..1e3d4182d6
--- /dev/null
+++ b/src/app/core/permissions/permissions-migration.service.ts
@@ -0,0 +1,39 @@
+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/core/permissions/permissions.module.ts b/src/app/core/permissions/permissions.module.ts
index 863550e705..959eed96a9 100644
--- a/src/app/core/permissions/permissions.module.ts
+++ b/src/app/core/permissions/permissions.module.ts
@@ -3,11 +3,13 @@ import { CommonModule } from "@angular/common";
import { DisableEntityOperationDirective } from "./disable-entity-operation.directive";
import { DisabledWrapperComponent } from "./disabled-wrapper/disabled-wrapper.component";
import { MatTooltipModule } from "@angular/material/tooltip";
+import { UserRoleGuard } from "./user-role.guard";
@NgModule({
declarations: [DisableEntityOperationDirective, DisabledWrapperComponent],
imports: [CommonModule, MatTooltipModule],
exports: [DisableEntityOperationDirective],
entryComponents: [DisabledWrapperComponent],
+ providers: [UserRoleGuard],
})
export class PermissionsModule {}
diff --git a/src/app/core/permissions/user-role.guard.spec.ts b/src/app/core/permissions/user-role.guard.spec.ts
new file mode 100644
index 0000000000..d86bea47d3
--- /dev/null
+++ b/src/app/core/permissions/user-role.guard.spec.ts
@@ -0,0 +1,58 @@
+import { TestBed } from "@angular/core/testing";
+
+import { UserRoleGuard } from "./user-role.guard";
+import { SessionService } from "../session/session-service/session.service";
+import { DatabaseUser } from "../session/session-service/local-user";
+
+describe("UserRoleGuard", () => {
+ let guard: UserRoleGuard;
+ let mockSessionService: jasmine.SpyObj;
+ const normalUser: DatabaseUser = { name: "normalUser", roles: ["user_app"] };
+ const adminUser: DatabaseUser = {
+ name: "admin",
+ roles: ["admin", "user_app"],
+ };
+
+ beforeEach(() => {
+ mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: SessionService, useValue: mockSessionService },
+ UserRoleGuard,
+ ],
+ });
+ guard = TestBed.inject(UserRoleGuard);
+ });
+
+ it("should be created", () => {
+ expect(guard).toBeTruthy();
+ });
+
+ it("should return true if current user is allowed", () => {
+ mockSessionService.getCurrentUser.and.returnValue(adminUser);
+
+ const result = guard.canActivate({
+ routeConfig: { path: "url" },
+ data: { permittedUserRoles: ["admin"] },
+ } as any);
+
+ expect(result).toBeTrue();
+ });
+
+ it("should return false for a user without permissions", () => {
+ mockSessionService.getCurrentUser.and.returnValue(normalUser);
+
+ const result = guard.canActivate({
+ routeConfig: { path: "url" },
+ data: { permittedUserRoles: ["admin"] },
+ } as any);
+
+ expect(result).toBeFalse();
+ });
+
+ it("should return true if no config is set", () => {
+ const result = guard.canActivate({ routeConfig: { path: "url" } } as any);
+
+ expect(result).toBeTrue();
+ });
+});
diff --git a/src/app/core/permissions/user-role.guard.ts b/src/app/core/permissions/user-role.guard.ts
new file mode 100644
index 0000000000..ad2c0eb3c4
--- /dev/null
+++ b/src/app/core/permissions/user-role.guard.ts
@@ -0,0 +1,26 @@
+import { Injectable } from "@angular/core";
+import { ActivatedRouteSnapshot, CanActivate } from "@angular/router";
+import { SessionService } from "../session/session-service/session.service";
+import { RouteData } from "../view/dynamic-routing/view-config.interface";
+
+@Injectable()
+/**
+ * A guard that checks the roles of the current user against the permissions which are saved in the route data.
+ */
+export class UserRoleGuard implements CanActivate {
+ constructor(private sessionService: SessionService) {}
+
+ canActivate(route: ActivatedRouteSnapshot): boolean {
+ const routeData: RouteData = route.data;
+ const user = this.sessionService.getCurrentUser();
+ if (routeData?.permittedUserRoles?.length > 0) {
+ // Check if user has a role which is in the list of permitted roles
+ return routeData.permittedUserRoles.some((role) =>
+ user?.roles.includes(role)
+ );
+ } else {
+ // No config set => all users are allowed
+ return true;
+ }
+ }
+}
diff --git a/src/app/core/session/logged-in-guard/logged-in.guard.spec.ts b/src/app/core/session/logged-in-guard/logged-in.guard.spec.ts
deleted file mode 100644
index 177d974bb0..0000000000
--- a/src/app/core/session/logged-in-guard/logged-in.guard.spec.ts
+++ /dev/null
@@ -1,56 +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 { TestBed, inject } from "@angular/core/testing";
-
-import { LoggedInGuard } from "./logged-in.guard";
-import { SessionService } from "../session-service/session.service";
-
-describe("LoggedInGuard", () => {
- let mockSessionService: jasmine.SpyObj;
-
- beforeEach(() => {
- mockSessionService = jasmine.createSpyObj(["isLoggedIn"]);
-
- TestBed.configureTestingModule({
- providers: [
- LoggedInGuard,
- { provide: SessionService, useValue: mockSessionService },
- ],
- });
- });
-
- it("should be created", inject([LoggedInGuard], (guard: LoggedInGuard) => {
- expect(guard).toBeTruthy();
- }));
-
- it("should prevent access when logged out", inject(
- [LoggedInGuard],
- (guard: LoggedInGuard) => {
- mockSessionService.isLoggedIn.and.returnValue(false);
- expect(guard.canActivate()).toBeFalsy();
- }
- ));
-
- it("should allow access when logged out", inject(
- [LoggedInGuard],
- (guard: LoggedInGuard) => {
- mockSessionService.isLoggedIn.and.returnValue(true);
- expect(guard.canActivate()).toBeTruthy();
- }
- ));
-});
diff --git a/src/app/core/session/logged-in-guard/logged-in.guard.ts b/src/app/core/session/logged-in-guard/logged-in.guard.ts
deleted file mode 100644
index d9927aac19..0000000000
--- a/src/app/core/session/logged-in-guard/logged-in.guard.ts
+++ /dev/null
@@ -1,35 +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 { Injectable } from "@angular/core";
-import { CanActivate } from "@angular/router";
-import { SessionService } from "../session-service/session.service";
-
-/**
- * Angular guard to prevent routing if no user is currently logged in.
- */
-@Injectable()
-export class LoggedInGuard implements CanActivate {
- constructor(private _sessionService: SessionService) {}
-
- /**
- * Allow if a user is logged in currently.
- */
- canActivate() {
- return this._sessionService.isLoggedIn();
- }
-}
diff --git a/src/app/core/session/mock-session.module.ts b/src/app/core/session/mock-session.module.ts
new file mode 100644
index 0000000000..72a9ae57d7
--- /dev/null
+++ b/src/app/core/session/mock-session.module.ts
@@ -0,0 +1,57 @@
+import { ModuleWithProviders, NgModule } from "@angular/core";
+import { LocalSession } from "./session-service/local-session";
+import { SessionService } from "./session-service/session.service";
+import {
+ TEST_PASSWORD,
+ TEST_USER,
+} from "./session-service/session.service.spec";
+import { LoginState } from "./session-states/login-state.enum";
+import { EntityMapperService } from "../entity/entity-mapper.service";
+import {
+ mockEntityMapper,
+ MockEntityMapperService,
+} from "../entity/mock-entity-mapper-service";
+import { User } from "../user/user";
+
+/**
+ * A simple module that can be imported in test files or stories to have mock implementations of the SessionService
+ * and the EntityMapper. To use it put `imports: [MockSessionModule.withState()]` into the module definition of the
+ * test or the story.
+ * The static method automatically initializes the SessionService and the EntityMapper with a demo user using the
+ * TEST_USER and TEST_PASSWORD constants. On default the user will also be logged in. This behavior can be changed
+ * by passing a different state to the method e.g. `MockSessionModule.withState(LoginState.LOGGED_OUT)`.
+ *
+ * This module provides the services `SessionService` `EntityMapperService` and `MockEntityMapperService`.
+ * The later two refer to the same service but injecting the `MockEntityMapperService` allows to access further methods.
+ */
+@NgModule()
+export class MockSessionModule {
+ static withState(
+ loginState = LoginState.LOGGED_IN
+ ): ModuleWithProviders {
+ const mockedEntityMapper = mockEntityMapper([new User(TEST_USER)]);
+ return {
+ ngModule: MockSessionModule,
+ providers: [
+ {
+ provide: SessionService,
+ useValue: createLocalSession(loginState === LoginState.LOGGED_IN),
+ },
+ { provide: EntityMapperService, useValue: mockedEntityMapper },
+ { provide: MockEntityMapperService, useValue: mockedEntityMapper },
+ ],
+ };
+ }
+}
+
+function createLocalSession(andLogin?: boolean): SessionService {
+ const localSession = new LocalSession(null);
+ localSession.saveUser(
+ { name: TEST_USER, roles: ["user_app"] },
+ TEST_PASSWORD
+ );
+ if (andLogin === true) {
+ localSession.login(TEST_USER, TEST_PASSWORD);
+ }
+ return localSession;
+}
diff --git a/src/app/core/session/session-service/local-session.spec.ts b/src/app/core/session/session-service/local-session.spec.ts
index 898660c71d..ffc70d47f3 100644
--- a/src/app/core/session/session-service/local-session.spec.ts
+++ b/src/app/core/session/session-service/local-session.spec.ts
@@ -39,7 +39,7 @@ describe("LocalSessionService", () => {
remote_url: "https://demo.aam-digital.com/db/",
},
};
- localSession = new LocalSession();
+ localSession = new LocalSession(null);
});
beforeEach(() => {
@@ -70,33 +70,29 @@ describe("LocalSessionService", () => {
});
it("should login a previously saved user with correct password", async () => {
- expect(localSession.getLoginState().getState()).toBe(LoginState.LOGGED_OUT);
+ expect(localSession.loginState.value).toBe(LoginState.LOGGED_OUT);
await localSession.login(TEST_USER, TEST_PASSWORD);
- expect(localSession.getLoginState().getState()).toBe(LoginState.LOGGED_IN);
+ expect(localSession.loginState.value).toBe(LoginState.LOGGED_IN);
});
it("should fail login with correct username but wrong password", async () => {
await localSession.login(TEST_USER, "wrong password");
- expect(localSession.getLoginState().getState()).toBe(
- LoginState.LOGIN_FAILED
- );
+ expect(localSession.loginState.value).toBe(LoginState.LOGIN_FAILED);
});
it("should fail login with wrong username", async () => {
await localSession.login("wrongUsername", TEST_PASSWORD);
- expect(localSession.getLoginState().getState()).toBe(
- LoginState.UNAVAILABLE
- );
+ expect(localSession.loginState.value).toBe(LoginState.UNAVAILABLE);
});
it("should assign current user after successful login", async () => {
await localSession.login(TEST_USER, TEST_PASSWORD);
- const currentUser = localSession.getCurrentDBUser();
+ const currentUser = localSession.getCurrentUser();
expect(currentUser.name).toBe(TEST_USER);
expect(currentUser.roles).toEqual(testUser.roles);
@@ -107,9 +103,7 @@ describe("LocalSessionService", () => {
await localSession.login(TEST_USER, TEST_PASSWORD);
- expect(localSession.getLoginState().getState()).toBe(
- LoginState.UNAVAILABLE
- );
+ expect(localSession.loginState.value).toBe(LoginState.UNAVAILABLE);
expect(localSession.getCurrentUser()).toBeUndefined();
});
diff --git a/src/app/core/session/session-service/local-session.ts b/src/app/core/session/session-service/local-session.ts
index de982d8a52..28680b824a 100644
--- a/src/app/core/session/session-service/local-session.ts
+++ b/src/app/core/session/session-service/local-session.ts
@@ -23,8 +23,6 @@ import {
passwordEqualsEncrypted,
} from "./local-user";
import { Database } from "../../database/database";
-import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
-import { User } from "../../user/user";
import { SessionService } from "./session.service";
/**
@@ -35,15 +33,8 @@ import { SessionService } from "./session.service";
@Injectable()
export class LocalSession extends SessionService {
private currentDBUser: DatabaseUser;
- /**
- * @deprecated instead use currentUser
- */
- private currentUserEntity: User;
- constructor(
- private database?: Database,
- private entitySchemaService?: EntitySchemaService
- ) {
+ constructor(private database: Database) {
super();
}
@@ -58,15 +49,14 @@ export class LocalSession extends SessionService {
if (user) {
if (passwordEqualsEncrypted(password, user.encryptedPassword)) {
this.currentDBUser = user;
- this.currentUserEntity = await this.loadUser(username);
- this.getLoginState().setState(LoginState.LOGGED_IN);
+ this.loginState.next(LoginState.LOGGED_IN);
} else {
- this.getLoginState().setState(LoginState.LOGIN_FAILED);
+ this.loginState.next(LoginState.LOGIN_FAILED);
}
} else {
- this.getLoginState().setState(LoginState.UNAVAILABLE);
+ this.loginState.next(LoginState.UNAVAILABLE);
}
- return this.getLoginState().getState();
+ return this.loginState.value;
}
/**
@@ -82,7 +72,7 @@ export class LocalSession extends SessionService {
};
window.localStorage.setItem(localUser.name, JSON.stringify(localUser));
// Update when already logged in
- if (this.getCurrentDBUser()?.name === localUser.name) {
+ if (this.getCurrentUser()?.name === localUser.name) {
this.currentDBUser = localUser;
}
}
@@ -96,16 +86,12 @@ export class LocalSession extends SessionService {
window.localStorage.removeItem(username);
}
- public getCurrentUser(): User {
- return this.currentUserEntity;
- }
-
public checkPassword(username: string, password: string): boolean {
const user: LocalUser = JSON.parse(window.localStorage.getItem(username));
return user && passwordEqualsEncrypted(password, user.encryptedPassword);
}
- public getCurrentDBUser(): DatabaseUser {
+ public getCurrentUser(): DatabaseUser {
return this.currentDBUser;
}
@@ -114,22 +100,7 @@ export class LocalSession extends SessionService {
*/
public logout() {
this.currentDBUser = undefined;
- this.currentUserEntity = undefined;
- this.getLoginState().setState(LoginState.LOGGED_OUT);
- }
-
- /**
- * TODO remove once admin information is migrated to new format (CouchDB)
- * Helper to get a User Entity from the Database without needing the EntityMapperService
- * @param userId Id of the User to be loaded
- */
- public async loadUser(userId: string): Promise {
- if (this.database && this.entitySchemaService) {
- const user = new User("");
- const userData = await this.database.get("User:" + userId);
- this.entitySchemaService.loadDataIntoEntity(user, userData);
- return user;
- }
+ this.loginState.next(LoginState.LOGGED_OUT);
}
getDatabase(): Database {
diff --git a/src/app/core/session/session-service/remote-session.spec.ts b/src/app/core/session/session-service/remote-session.spec.ts
index 274375a547..a0db30a37a 100644
--- a/src/app/core/session/session-service/remote-session.spec.ts
+++ b/src/app/core/session/session-service/remote-session.spec.ts
@@ -54,12 +54,12 @@ describe("RemoteSessionService", () => {
});
it("should be connected after successful login", async () => {
- expect(service.getLoginState().getState()).toBe(LoginState.LOGGED_OUT);
+ expect(service.loginState.value).toBe(LoginState.LOGGED_OUT);
await service.login(TEST_USER, TEST_PASSWORD);
expect(mockHttpClient.post).toHaveBeenCalled();
- expect(service.getLoginState().getState()).toBe(LoginState.LOGGED_IN);
+ expect(service.loginState.value).toBe(LoginState.LOGGED_IN);
});
it("should be unavailable if requests fails with error other than 401", async () => {
@@ -69,13 +69,13 @@ describe("RemoteSessionService", () => {
await service.login(TEST_USER, TEST_PASSWORD);
- expect(service.getLoginState().getState()).toBe(LoginState.UNAVAILABLE);
+ expect(service.loginState.value).toBe(LoginState.UNAVAILABLE);
});
it("should be rejected if login is unauthorized", async () => {
await service.login(TEST_USER, "wrongPassword");
- expect(service.getLoginState().getState()).toBe(LoginState.LOGIN_FAILED);
+ expect(service.loginState.value).toBe(LoginState.LOGIN_FAILED);
});
it("should disconnect after logout", async () => {
@@ -83,13 +83,13 @@ describe("RemoteSessionService", () => {
await service.logout();
- expect(service.getLoginState().getState()).toBe(LoginState.LOGGED_OUT);
+ expect(service.loginState.value).toBe(LoginState.LOGGED_OUT);
});
it("should assign the current user after successful login", async () => {
await service.login(TEST_USER, TEST_PASSWORD);
- expect(service.getCurrentDBUser()).toEqual({
+ expect(service.getCurrentUser()).toEqual({
name: dbUser.name,
roles: dbUser.roles,
});
diff --git a/src/app/core/session/session-service/remote-session.ts b/src/app/core/session/session-service/remote-session.ts
index 181b1562a4..7d8d69dccd 100644
--- a/src/app/core/session/session-service/remote-session.ts
+++ b/src/app/core/session/session-service/remote-session.ts
@@ -24,7 +24,6 @@ import { DatabaseUser } from "./local-user";
import { SessionService } from "./session.service";
import { LoginState } from "../session-states/login-state.enum";
import { Database } from "../../database/database";
-import { User } from "../../user/user";
import { PouchDatabase } from "../../database/pouch-database";
import { LoggingService } from "../../logging/logging.service";
@@ -75,16 +74,16 @@ export class RemoteSession extends SessionService {
)
.toPromise();
this.assignDatabaseUser(response);
- this.getLoginState().setState(LoginState.LOGGED_IN);
+ this.loginState.next(LoginState.LOGGED_IN);
} catch (error) {
const httpError = error as HttpErrorResponse;
if (httpError?.status === this.UNAUTHORIZED_STATUS_CODE) {
- this.getLoginState().setState(LoginState.LOGIN_FAILED);
+ this.loginState.next(LoginState.LOGIN_FAILED);
} else {
- this.getLoginState().setState(LoginState.UNAVAILABLE);
+ this.loginState.next(LoginState.UNAVAILABLE);
}
}
- return this.getLoginState().getState();
+ return this.loginState.value;
}
private assignDatabaseUser(couchDBResponse: any) {
@@ -104,10 +103,10 @@ export class RemoteSession extends SessionService {
})
.toPromise();
this.currentDBUser = undefined;
- this.getLoginState().setState(LoginState.LOGGED_OUT);
+ this.loginState.next(LoginState.LOGGED_OUT);
}
- getCurrentDBUser(): DatabaseUser {
+ getCurrentUser(): DatabaseUser {
return this.currentDBUser;
}
@@ -116,10 +115,6 @@ export class RemoteSession extends SessionService {
throw Error("Can't check password in remote session");
}
- getCurrentUser(): User {
- throw Error("Can't get user entity in remote session");
- }
-
getDatabase(): Database {
return this.database;
}
diff --git a/src/app/core/session/session-service/session.service.spec.ts b/src/app/core/session/session-service/session.service.spec.ts
index 7485f36f94..2d3f2add2b 100644
--- a/src/app/core/session/session-service/session.service.spec.ts
+++ b/src/app/core/session/session-service/session.service.spec.ts
@@ -44,7 +44,7 @@ export function testSessionServiceImplementation(
});
it("has the correct initial state", () => {
- expect(sessionService.getSyncState().getState()).toBe(SyncState.UNSYNCED);
+ expect(sessionService.syncState.value).toBe(SyncState.UNSYNCED);
expectNotToBeLoggedIn(LoginState.LOGGED_OUT);
});
@@ -53,14 +53,14 @@ export function testSessionServiceImplementation(
expect(loginResult).toEqual(LoginState.LOGGED_IN);
- expect(sessionService.getLoginState().getState())
+ expect(sessionService.loginState.value)
.withContext("unexpected LoginState")
.toEqual(LoginState.LOGGED_IN);
expect(sessionService.isLoggedIn())
.withContext("unexpected isLoggedIn")
.toBeTrue();
- expect(sessionService.getCurrentDBUser().name).toBe(TEST_USER);
+ expect(sessionService.getCurrentUser().name).toBe(TEST_USER);
});
it("fails login with wrong password", async () => {
@@ -96,13 +96,13 @@ export function testSessionServiceImplementation(
| LoginState.LOGIN_FAILED
| LoginState.UNAVAILABLE
) {
- expect(sessionService.getLoginState().getState())
+ expect(sessionService.loginState.value)
.withContext("unexpected LoginState")
.toEqual(expectedLoginState);
expect(sessionService.isLoggedIn())
.withContext("unexpected isLoggedIn")
.toEqual(false);
- expect(sessionService.getCurrentDBUser()).not.toBeDefined();
+ expect(sessionService.getCurrentUser()).not.toBeDefined();
}
}
diff --git a/src/app/core/session/session-service/session.service.ts b/src/app/core/session/session-service/session.service.ts
index 759994118e..38d4ae22f4 100644
--- a/src/app/core/session/session-service/session.service.ts
+++ b/src/app/core/session/session-service/session.service.ts
@@ -18,9 +18,8 @@
import { LoginState } from "../session-states/login-state.enum";
import { Database } from "../../database/database";
import { SyncState } from "../session-states/sync-state.enum";
-import { User } from "../../user/user";
-import { StateHandler } from "../session-states/state-handler";
import { DatabaseUser } from "./local-user";
+import { BehaviorSubject } from "rxjs";
/**
* A session manages user authentication and database connection for the app.
@@ -37,9 +36,9 @@ import { DatabaseUser } from "./local-user";
*/
export abstract class SessionService {
/** StateHandler for login state changes */
- private loginState = new StateHandler(LoginState.LOGGED_OUT);
+ private _loginState = new BehaviorSubject(LoginState.LOGGED_OUT);
/** StateHandler for sync state changes */
- private syncState = new StateHandler(SyncState.UNSYNCED);
+ private _syncState = new BehaviorSubject(SyncState.UNSYNCED);
/**
* Authenticate a user.
@@ -54,15 +53,9 @@ export abstract class SessionService {
abstract logout();
/**
- * Get the currently logged in user (or undefined).
- * @deprecated use getCurrentDBUser instead
+ * Get the current user according to the CouchDB format
*/
- abstract getCurrentUser(): User;
-
- /**
- * Get the current user according to the new format
- */
- abstract getCurrentDBUser(): DatabaseUser;
+ abstract getCurrentUser(): DatabaseUser;
/**
* Check a password if its valid
@@ -76,21 +69,21 @@ export abstract class SessionService {
* Get the session status - whether a user is authenticated currently.
*/
public isLoggedIn(): boolean {
- return this.getLoginState().getState() === LoginState.LOGGED_IN;
+ return this.loginState.value === LoginState.LOGGED_IN;
}
/**
* Get the state of the session.
*/
- public getLoginState(): StateHandler {
- return this.loginState;
+ public get loginState(): BehaviorSubject {
+ return this._loginState;
}
/**
* Get the state of the synchronization with the remote server.
*/
- public getSyncState(): StateHandler {
- return this.syncState;
+ public get syncState(): BehaviorSubject {
+ return this._syncState;
}
/**
diff --git a/src/app/core/session/session-service/synced-session.service.spec.ts b/src/app/core/session/session-service/synced-session.service.spec.ts
index 9899d078ce..c92a139ac6 100644
--- a/src/app/core/session/session-service/synced-session.service.spec.ts
+++ b/src/app/core/session/session-service/synced-session.service.spec.ts
@@ -24,7 +24,6 @@ import { RemoteSession } from "./remote-session";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
import { SessionType } from "../session-type";
import { fakeAsync, flush, TestBed, tick } from "@angular/core/testing";
-import { User } from "../../user/user";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { LoggingService } from "../../logging/logging.service";
import { of, throwError } from "rxjs";
@@ -50,7 +49,6 @@ describe("SyncedSessionService", () => {
let dbUser: DatabaseUser;
let syncSpy: jasmine.Spy<() => Promise>;
let liveSyncSpy: jasmine.Spy<() => void>;
- let loadUserSpy: jasmine.Spy<(userId: string) => void>;
let mockHttpClient: jasmine.SpyObj;
beforeEach(() => {
@@ -102,9 +100,6 @@ describe("SyncedSessionService", () => {
remoteLoginSpy = spyOn(remoteSession, "login").and.callThrough();
syncSpy = spyOn(sessionService, "sync").and.resolveTo();
liveSyncSpy = spyOn(sessionService, "liveSyncDeferred");
-
- // TODO remove this once User Entity is not needed in session any more
- loadUserSpy = spyOn(localSession, "loadUser").and.resolveTo();
});
afterEach(() => {
@@ -173,8 +168,8 @@ describe("SyncedSessionService", () => {
},
"p"
);
- expect(sessionService.getCurrentDBUser().name).toBe("newUser");
- expect(sessionService.getCurrentDBUser().roles).toEqual(["user_app"]);
+ expect(sessionService.getCurrentUser().name).toBe("newUser");
+ expect(sessionService.getCurrentUser().roles).toEqual(["user_app"]);
tick();
localSession.removeUser(newUser.name);
}));
@@ -191,9 +186,7 @@ describe("SyncedSessionService", () => {
// Initially the user is logged in
expectAsync(result).toBeResolvedTo(LoginState.LOGGED_IN);
// After remote session fails the user is logged out again
- expect(sessionService.getLoginState().getState()).toBe(
- LoginState.LOGIN_FAILED
- );
+ expect(sessionService.loginState.value).toBe(LoginState.LOGIN_FAILED);
flush();
}));
@@ -248,25 +241,6 @@ describe("SyncedSessionService", () => {
flush();
}));
- it("should load the user entity after successful local login", fakeAsync(() => {
- const testUser = new User(TEST_USER);
- testUser.name = TEST_USER;
- const database = sessionService.getDatabase();
- loadUserSpy.and.callThrough();
- spyOn(database, "get").and.resolveTo(
- TestBed.inject(EntitySchemaService).transformEntityToDatabaseFormat(
- testUser
- )
- );
-
- sessionService.login(TEST_USER, TEST_PASSWORD);
- tick();
-
- expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
- expect(database.get).toHaveBeenCalledWith(testUser._id);
- expect(sessionService.getCurrentUser()).toEqual(testUser);
- }));
-
it("should update the local user object once connected", fakeAsync(() => {
const updatedUser: DatabaseUser = {
name: TEST_USER,
@@ -282,7 +256,7 @@ describe("SyncedSessionService", () => {
expect(syncSpy).toHaveBeenCalledTimes(1);
expect(liveSyncSpy).toHaveBeenCalledTimes(1);
- const currentUser = localSession.getCurrentDBUser();
+ const currentUser = localSession.getCurrentUser();
expect(currentUser.name).toEqual(TEST_USER);
expect(currentUser.roles).toEqual(["user_app", "admin"]);
expectAsync(result).toBeResolvedTo(LoginState.LOGGED_IN);
diff --git a/src/app/core/session/session-service/synced-session.service.ts b/src/app/core/session/session-service/synced-session.service.ts
index 5444fe744c..57160c79a3 100644
--- a/src/app/core/session/session-service/synced-session.service.ts
+++ b/src/app/core/session/session-service/synced-session.service.ts
@@ -25,13 +25,13 @@ import { LoginState } from "../session-states/login-state.enum";
import { Database } from "../../database/database";
import { PouchDatabase } from "../../database/pouch-database";
import { SyncState } from "../session-states/sync-state.enum";
-import { User } from "../../user/user";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
import { LoggingService } from "../../logging/logging.service";
import { HttpClient } from "@angular/common/http";
import PouchDB from "pouchdb-browser";
import { AppConfig } from "../../app-config/app-config";
import { DatabaseUser } from "./local-user";
+import { waitForChangeTo } from "../session-states/session-utils";
/**
* A synced session creates and manages a LocalSession and a RemoteSession
@@ -62,10 +62,7 @@ export class SyncedSessionService extends SessionService {
super();
this.pouchDB = new PouchDB(AppConfig.settings.database.name);
this.database = new PouchDatabase(this.pouchDB, this._loggingService);
- this._localSession = new LocalSession(
- this.database,
- this._entitySchemaService
- );
+ this._localSession = new LocalSession(this.database);
this._remoteSession = new RemoteSession(this._httpClient, _loggingService);
}
@@ -84,17 +81,18 @@ export class SyncedSessionService extends SessionService {
*/
public async login(username: string, password: string): Promise {
this.cancelLoginOfflineRetry(); // in case this is running in the background
- this.getSyncState().setState(SyncState.UNSYNCED);
+ this.syncState.next(SyncState.UNSYNCED);
const remoteLogin = this._remoteSession.login(username, password);
- const syncPromise = this._remoteSession
- .getLoginState()
- .waitForChangeTo(LoginState.LOGGED_IN)
+ const syncPromise = this._remoteSession.loginState
+ .pipe(waitForChangeTo(LoginState.LOGGED_IN))
+ .toPromise()
.then(() => this.updateLocalUserAndStartSync(password));
const localLoginState = await this._localSession.login(username, password);
if (localLoginState === LoginState.LOGGED_IN) {
+ this.loginState.next(LoginState.LOGGED_IN);
remoteLogin.then((loginState) => {
if (loginState === LoginState.LOGIN_FAILED) {
this.handleRemotePasswordChange(username);
@@ -108,25 +106,29 @@ export class SyncedSessionService extends SessionService {
if (remoteLoginState === LoginState.LOGGED_IN) {
// New user or password changed
await syncPromise;
- await this._localSession.login(username, password);
+ const localLoginRetry = await this._localSession.login(
+ username,
+ password
+ );
+ this.loginState.next(localLoginRetry);
} else if (
remoteLoginState === LoginState.UNAVAILABLE &&
localLoginState === LoginState.UNAVAILABLE
) {
// Offline with no local user
- this._localSession.getLoginState().setState(LoginState.UNAVAILABLE);
+ this.loginState.next(LoginState.UNAVAILABLE);
} else {
// Password and or username wrong
- this._localSession.getLoginState().setState(LoginState.LOGIN_FAILED);
+ this.loginState.next(LoginState.LOGIN_FAILED);
}
}
- return this.getLoginState().getState();
+ return this.loginState.value;
}
private handleRemotePasswordChange(username: string) {
this._localSession.logout();
this._localSession.removeUser(username);
- this._localSession.getLoginState().setState(LoginState.LOGIN_FAILED);
+ this.loginState.next(LoginState.LOGIN_FAILED);
this._alertService.addDanger(
$localize`Your password was changed recently. Please retry with your new password!`
);
@@ -140,50 +142,38 @@ export class SyncedSessionService extends SessionService {
private updateLocalUserAndStartSync(password: string) {
// Update local user object
- const remoteUser = this._remoteSession.getCurrentDBUser();
+ const remoteUser = this._remoteSession.getCurrentUser();
this._localSession.saveUser(remoteUser, password);
return this.sync()
.then(() => this.liveSyncDeferred())
.catch(() => {
- if (
- this._localSession.getLoginState().getState() === LoginState.LOGGED_IN
- ) {
+ if (this._localSession.loginState.value === LoginState.LOGGED_IN) {
this.liveSyncDeferred();
}
});
}
- /** see {@link SessionService} */
- public getCurrentUser(): User {
+ public getCurrentUser(): DatabaseUser {
return this._localSession.getCurrentUser();
}
- public getCurrentDBUser(): DatabaseUser {
- return this._localSession.getCurrentDBUser();
- }
-
public checkPassword(username: string, password: string): boolean {
// This only checks the password against locally saved users
return this._localSession.checkPassword(username, password);
}
- /** see {@link SessionService} */
- public getLoginState() {
- return this._localSession.getLoginState();
- }
-
/** see {@link SessionService} */
public async sync(): Promise {
- this.getSyncState().setState(SyncState.STARTED);
+ this.syncState.next(SyncState.STARTED);
try {
const result = await this.pouchDB.sync(this._remoteSession.pouchDB, {
batch_size: this.POUCHDB_SYNC_BATCH_SIZE,
});
- this.getSyncState().setState(SyncState.COMPLETED);
+ this.syncState.next(SyncState.COMPLETED);
return result;
} catch (error) {
- this.getSyncState().setState(SyncState.FAILED);
+ this.syncState.next(SyncState.FAILED);
throw error; // rethrow, so later Promise-handling lands in .catch, too
}
}
@@ -193,7 +183,7 @@ export class SyncedSessionService extends SessionService {
*/
public liveSync() {
this.cancelLiveSync(); // cancel any liveSync that may have been alive before
- this.getSyncState().setState(SyncState.STARTED);
+ this.syncState.next(SyncState.STARTED);
this._liveSyncHandle = (this.pouchDB.sync(this._remoteSession.pouchDB, {
live: true,
retry: true,
@@ -203,23 +193,20 @@ export class SyncedSessionService extends SessionService {
})
.on("paused", (info) => {
// replication was paused: either because sync is finished or because of a failed sync (mostly due to lost connection). info is empty.
- if (
- this._remoteSession.getLoginState().getState() ===
- LoginState.LOGGED_IN
- ) {
- this.getSyncState().setState(SyncState.COMPLETED);
+ if (this._remoteSession.loginState.value === LoginState.LOGGED_IN) {
+ this.syncState.next(SyncState.COMPLETED);
// We might end up here after a failed sync that is not due to offline errors.
// It shouldn't happen too often, as we have an initial non-live sync to catch those situations, but we can't find that out here
}
})
.on("active", (info) => {
// replication was resumed: either because new things to sync or because connection is available again. info contains the direction
- this.getSyncState().setState(SyncState.STARTED);
+ this.syncState.next(SyncState.STARTED);
})
.on("error", (err) => {
// totally unhandled error (shouldn't happen)
console.error("sync failed", err);
- this.getSyncState().setState(SyncState.FAILED);
+ this.syncState.next(SyncState.FAILED);
})
.on("complete", (info) => {
// replication was canceled!
@@ -275,6 +262,7 @@ export class SyncedSessionService extends SessionService {
public logout() {
this.cancelLoginOfflineRetry();
this.cancelLiveSync();
+ this.loginState.next(LoginState.LOGGED_OUT);
this._localSession.logout();
this._remoteSession.logout();
}
diff --git a/src/app/core/session/session-states/session-utils.spec.ts b/src/app/core/session/session-states/session-utils.spec.ts
new file mode 100644
index 0000000000..a66f9e3964
--- /dev/null
+++ b/src/app/core/session/session-states/session-utils.spec.ts
@@ -0,0 +1,31 @@
+import { of } from "rxjs";
+import { waitForChangeTo } from "./session-utils";
+import { fail } from "assert";
+
+describe("session-utils", () => {
+ it("(waitForChangeTo) Should only emit elements in a stream when the given condition is false", (done) => {
+ const stream = of("A", "B", "C", "D");
+ stream.pipe(waitForChangeTo("D")).subscribe(
+ (next) => {
+ expect(next).toBe("D");
+ },
+ (error) => {
+ fail(error);
+ },
+ () => {
+ done();
+ }
+ );
+ });
+
+ it("(waitForChangeTo) should complete when condition is met but further elements are in the pipeline", (done) => {
+ const stream = of("A", "B", "C", "D");
+ stream.pipe(waitForChangeTo("B")).subscribe(
+ (next) => {
+ expect(next).toBe("B");
+ },
+ (error) => fail(error),
+ () => done()
+ );
+ });
+});
diff --git a/src/app/core/session/session-states/session-utils.ts b/src/app/core/session/session-states/session-utils.ts
new file mode 100644
index 0000000000..3f0286fd9f
--- /dev/null
+++ b/src/app/core/session/session-states/session-utils.ts
@@ -0,0 +1,19 @@
+import { MonoTypeOperatorFunction, pipe } from "rxjs";
+import { first, skipWhile } from "rxjs/operators";
+
+/**
+ * Waits until the state of a source observable is equal to the given
+ * state, then emit that state. All other states are discarded.
+ * After the state has changed to the desired state, subsequent states
+ * are let through. if this desire is not intended, use the {@link filter} function
+ * @param state The state to wait on
+ * otherwise continues to emit values from the source observable
+ */
+export function waitForChangeTo(
+ state: State
+): MonoTypeOperatorFunction {
+ return pipe(
+ skipWhile((nextState) => nextState !== state),
+ first()
+ );
+}
diff --git a/src/app/core/session/session-states/state-handler.spec.ts b/src/app/core/session/session-states/state-handler.spec.ts
deleted file mode 100644
index a351e653ce..0000000000
--- a/src/app/core/session/session-states/state-handler.spec.ts
+++ /dev/null
@@ -1,69 +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 { waitForAsync } from "@angular/core/testing";
-import { StateHandler } from "./state-handler";
-
-enum TestState {
- test1,
- test2,
- test3,
- test4,
-}
-
-describe("StateHandler", () => {
- it("is initiated with the correct Initial State", () => {
- const handler = new StateHandler(TestState.test1);
- expect(handler.getState()).toEqual(TestState.test1);
- });
- it("changes the state when setting the state", () => {
- const handler = new StateHandler(TestState.test1);
- handler.setState(TestState.test2);
- expect(handler.getState()).toEqual(TestState.test2);
- });
- it("emits an event when setting the state", (done) => {
- const handler = new StateHandler(TestState.test1);
- handler.getStateChangedStream().subscribe((stateChangeEvent) => {
- expect(stateChangeEvent.fromState).toEqual(TestState.test1);
- expect(stateChangeEvent.toState).toEqual(TestState.test2);
- done();
- });
- handler.setState(TestState.test2);
- });
- it(
- "waits for the state to change to a specific value",
- waitForAsync(() => {
- const handler = new StateHandler(TestState.test1);
- handler.waitForChangeTo(TestState.test2).then(() => {
- expect(handler.getState()).toEqual(TestState.test2);
- });
- handler.setState(TestState.test3);
- handler.setState(TestState.test2);
- })
- );
- it(
- "fails waiting for the state to change to a specific value if specified",
- waitForAsync(() => {
- const handler = new StateHandler(TestState.test1);
- handler.waitForChangeTo(TestState.test2, [TestState.test3]).catch(() => {
- expect(handler.getState()).toEqual(TestState.test3);
- });
- handler.setState(TestState.test4);
- handler.setState(TestState.test3);
- })
- );
-});
diff --git a/src/app/core/session/session-states/state-handler.ts b/src/app/core/session/session-states/state-handler.ts
deleted file mode 100644
index 00ad6159fc..0000000000
--- a/src/app/core/session/session-states/state-handler.ts
+++ /dev/null
@@ -1,106 +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 { EventEmitter } from "@angular/core";
-
-/**
- * Interface of state transition events.
- */
-export interface StateChangedEvent {
- /** previous state before change */
- fromState: StateEnum;
-
- /** new state after change */
- toState: StateEnum;
-}
-
-/**
- * Utility class supporting generic state transitions by emitting change events
- * and allowing to wait for a certain state transition.
- */
-export class StateHandler {
- private state: StateEnum;
- private stateChanged: EventEmitter<
- StateChangedEvent
- > = new EventEmitter>();
-
- /**
- * Create a StateHandler helper.
- * @param defaultState Optional initial state.
- */
- constructor(defaultState?: StateEnum) {
- this.state = defaultState;
- }
-
- /**
- * Get the current state.
- */
- public getState(): StateEnum {
- return this.state;
- }
-
- /**
- * Change to the given new state.
- * The state handler ensures an event is emitted.
- * @param state The new state
- */
- public setState(state: StateEnum): StateHandler {
- const oldState = this.state;
- this.state = state;
- this.stateChanged.emit({
- fromState: oldState,
- toState: this.state,
- });
- return this;
- }
-
- /**
- * Subscribe to all state change events.
- */
- public getStateChangedStream(): EventEmitter> {
- return this.stateChanged;
- }
-
- /**
- * Subscribe to a certain state transition.
- * @param toState The state for which to wait for and resolve the Promise.
- * @param failOnStates Optional array of states for which the reject the Promise.
- */
- public waitForChangeTo(
- toState: StateEnum,
- failOnStates?: StateEnum[]
- ): Promise> {
- return new Promise((resolve, reject) => {
- if (this.getState() === toState) {
- resolve({ fromState: undefined, toState });
- return;
- } else if (failOnStates && failOnStates.includes(this.getState())) {
- reject({ fromState: undefined, toState: this.getState() });
- return;
- }
- const subscription = this.getStateChangedStream().subscribe((change) => {
- if (change.toState === toState) {
- subscription.unsubscribe(); // only once
- resolve(change);
- } else if (failOnStates && failOnStates.includes(change.toState)) {
- subscription.unsubscribe(); // only once
- reject(change);
- }
- });
- });
- }
-}
diff --git a/src/app/core/session/session.module.ts b/src/app/core/session/session.module.ts
index b0cbe57f4d..1d672449cd 100644
--- a/src/app/core/session/session.module.ts
+++ b/src/app/core/session/session.module.ts
@@ -21,7 +21,6 @@ import { LoginComponent } from "./login/login.component";
import { FormsModule } from "@angular/forms";
import { EntityModule } from "../entity/entity.module";
import { AlertsModule } from "../alerts/alerts.module";
-import { LoggedInGuard } from "./logged-in-guard/logged-in.guard";
import { sessionServiceProvider } from "./session.service.provider";
import { databaseServiceProvider } from "../database/database.service.provider";
import { UserModule } from "../user/user.module";
@@ -54,6 +53,6 @@ import { HttpClientModule } from "@angular/common/http";
],
declarations: [LoginComponent],
exports: [LoginComponent],
- providers: [LoggedInGuard, sessionServiceProvider, databaseServiceProvider],
+ providers: [sessionServiceProvider, databaseServiceProvider],
})
export class SessionModule {}
diff --git a/src/app/core/session/session.service.provider.ts b/src/app/core/session/session.service.provider.ts
index b7940402f7..631fb7c693 100644
--- a/src/app/core/session/session.service.provider.ts
+++ b/src/app/core/session/session.service.provider.ts
@@ -45,8 +45,7 @@ export function sessionServiceFactory(
PouchDatabase.createWithIndexedDB(
AppConfig.settings.database.name,
loggingService
- ),
- entitySchemaService
+ )
);
break;
case SessionType.synced:
@@ -62,8 +61,7 @@ export function sessionServiceFactory(
PouchDatabase.createWithInMemoryDB(
AppConfig.settings.database.name,
loggingService
- ),
- entitySchemaService
+ )
);
break;
}
@@ -78,18 +76,15 @@ export function sessionServiceFactory(
function updateLoggingServiceWithUserContext(sessionService: SessionService) {
// update the user context for remote error logging
// cannot subscribe within LoggingService itself because of cyclic dependencies, therefore doing this here
- sessionService
- .getLoginState()
- .getStateChangedStream()
- .subscribe((newState) => {
- if (newState.toState === LoginState.LOGGED_IN) {
- LoggingService.setLoggingContextUser(
- sessionService.getCurrentUser().name
- );
- } else {
- LoggingService.setLoggingContextUser(undefined);
- }
- });
+ sessionService.loginState.subscribe((newState) => {
+ if (newState === LoginState.LOGGED_IN) {
+ LoggingService.setLoggingContextUser(
+ sessionService.getCurrentUser().name
+ );
+ } else {
+ LoggingService.setLoggingContextUser(undefined);
+ }
+ });
}
/**
diff --git a/src/app/core/sync-status/sync-status/sync-status.component.spec.ts b/src/app/core/sync-status/sync-status/sync-status.component.spec.ts
index 148a1ec777..bd40731e00 100644
--- a/src/app/core/sync-status/sync-status/sync-status.component.spec.ts
+++ b/src/app/core/sync-status/sync-status/sync-status.component.spec.ts
@@ -25,7 +25,6 @@ import { DatabaseIndexingService } from "../../entity/database-indexing/database
import { BehaviorSubject } from "rxjs";
import { take } from "rxjs/operators";
import { BackgroundProcessState } from "../background-process-state.interface";
-import { StateHandler } from "../../session/session-states/state-handler";
import { SyncStatusModule } from "../sync-status.module";
describe("SyncStatusComponent", () => {
@@ -33,7 +32,6 @@ describe("SyncStatusComponent", () => {
let fixture: ComponentFixture;
let mockSessionService: jasmine.SpyObj;
- const syncState: StateHandler = new StateHandler();
let mockIndexingService;
const DATABASE_SYNCING_STATE: BackgroundProcessState = {
@@ -47,9 +45,10 @@ describe("SyncStatusComponent", () => {
beforeEach(
waitForAsync(() => {
- mockSessionService = jasmine.createSpyObj(["getSyncState", "isLoggedIn"]);
+ mockSessionService = jasmine.createSpyObj(["isLoggedIn"], {
+ syncState: new BehaviorSubject(SyncState.UNSYNCED),
+ });
mockSessionService.isLoggedIn.and.returnValue(false);
- mockSessionService.getSyncState.and.returnValue(syncState);
mockIndexingService = { indicesRegistered: new BehaviorSubject([]) };
TestBed.configureTestingModule({
@@ -75,14 +74,14 @@ describe("SyncStatusComponent", () => {
});
it("should open dialog without error", async () => {
- syncState.setState(SyncState.STARTED);
+ mockSessionService.syncState.next(SyncState.STARTED);
fixture.detectChanges();
await fixture.whenStable();
// @ts-ignore
expect(component.dialogRef).toBeDefined();
- syncState.setState(SyncState.COMPLETED);
+ mockSessionService.syncState.next(SyncState.COMPLETED);
// @ts-ignore
component.dialogRef.close();
@@ -91,7 +90,7 @@ describe("SyncStatusComponent", () => {
});
it("should update backgroundProcesses details on sync", async () => {
- syncState.setState(SyncState.STARTED);
+ mockSessionService.syncState.next(SyncState.STARTED);
fixture.detectChanges();
await fixture.whenStable();
@@ -99,7 +98,7 @@ describe("SyncStatusComponent", () => {
await component.backgroundProcesses.pipe(take(1)).toPromise()
).toEqual([DATABASE_SYNCING_STATE]);
- syncState.setState(SyncState.COMPLETED);
+ mockSessionService.syncState.next(SyncState.COMPLETED);
fixture.detectChanges();
await fixture.whenStable();
diff --git a/src/app/core/sync-status/sync-status/sync-status.component.ts b/src/app/core/sync-status/sync-status/sync-status.component.ts
index 27c22addaf..02d4b0c136 100644
--- a/src/app/core/sync-status/sync-status/sync-status.component.ts
+++ b/src/app/core/sync-status/sync-status/sync-status.component.ts
@@ -25,7 +25,6 @@ import { DatabaseIndexingService } from "../../entity/database-indexing/database
import { BackgroundProcessState } from "../background-process-state.interface";
import { BehaviorSubject } from "rxjs";
import { debounceTime } from "rxjs/operators";
-import { StateChangedEvent } from "../../session/session-states/state-handler";
import { LoggingService } from "../../logging/logging.service";
/**
@@ -67,14 +66,13 @@ export class SyncStatusComponent implements OnInit {
this.handleIndexingState(indicesStatus)
);
- this.sessionService
- .getSyncState()
- .getStateChangedStream()
- .subscribe((state) => this.handleSyncState(state));
+ this.sessionService.syncState.subscribe((state) =>
+ this.handleSyncState(state)
+ );
}
- private handleSyncState(state: StateChangedEvent) {
- switch (state.toState) {
+ private handleSyncState(state: SyncState) {
+ switch (state) {
case SyncState.STARTED:
this.syncInProgress = true;
if (!this.sessionService.isLoggedIn() && !this.dialogRef) {
diff --git a/src/app/core/ui/primary-action/primary-action.component.spec.ts b/src/app/core/ui/primary-action/primary-action.component.spec.ts
index fe0de1ae55..df3e55c4df 100644
--- a/src/app/core/ui/primary-action/primary-action.component.spec.ts
+++ b/src/app/core/ui/primary-action/primary-action.component.spec.ts
@@ -3,20 +3,14 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { PrimaryActionComponent } from "./primary-action.component";
import { MatButtonModule } from "@angular/material/button";
import { MatDialogModule } from "@angular/material/dialog";
-import { SessionService } from "../../session/session-service/session.service";
import { FormDialogModule } from "../../form-dialog/form-dialog.module";
import { PermissionsModule } from "../../permissions/permissions.module";
+import { MockSessionModule } from "../../session/mock-session.module";
describe("PrimaryActionComponent", () => {
let component: PrimaryActionComponent;
let fixture: ComponentFixture;
- const mockSessionService = {
- getCurrentUser: () => {
- return { name: "tester" };
- },
- };
-
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [PrimaryActionComponent],
@@ -25,8 +19,8 @@ describe("PrimaryActionComponent", () => {
MatButtonModule,
FormDialogModule,
PermissionsModule,
+ MockSessionModule.withState(),
],
- providers: [{ provide: SessionService, useValue: mockSessionService }],
}).compileComponents();
});
diff --git a/src/app/core/ui/primary-action/primary-action.component.ts b/src/app/core/ui/primary-action/primary-action.component.ts
index 45f36ce68b..5b640ba00f 100644
--- a/src/app/core/ui/primary-action/primary-action.component.ts
+++ b/src/app/core/ui/primary-action/primary-action.component.ts
@@ -35,7 +35,7 @@ export class PrimaryActionComponent {
private createNewNote() {
const newNote = new Note(Date.now().toString());
newNote.date = new Date();
- newNote.authors = [this.sessionService.getCurrentUser().getId()];
+ newNote.authors = [this.sessionService.getCurrentUser().name];
return newNote;
}
}
diff --git a/src/app/core/ui/ui/ui.component.spec.ts b/src/app/core/ui/ui/ui.component.spec.ts
index 96aca1c54b..e475399552 100644
--- a/src/app/core/ui/ui/ui.component.spec.ts
+++ b/src/app/core/ui/ui/ui.component.spec.ts
@@ -29,7 +29,6 @@ import { BehaviorSubject, of } from "rxjs";
import { ApplicationInitStatus } from "@angular/core";
import { UiModule } from "../ui.module";
import { Angulartics2Module } from "angulartics2";
-import { StateHandler } from "../../session/session-states/state-handler";
import { SyncState } from "../../session/session-states/sync-state.enum";
import { ConfigService } from "../../config/config.service";
@@ -40,13 +39,10 @@ describe("UiComponent", () => {
beforeEach(
waitForAsync(() => {
const mockSwUpdate = { available: of(), checkForUpdate: () => {} };
- const mockSession: jasmine.SpyObj = jasmine.createSpyObj([
- "isLoggedIn",
- "logout",
- "getDatabase",
- "getSyncState",
- ]);
- mockSession.getSyncState.and.returnValue(new StateHandler());
+ const mockSession: jasmine.SpyObj = jasmine.createSpyObj(
+ ["isLoggedIn", "logout", "getDatabase"],
+ { syncState: new BehaviorSubject(SyncState.UNSYNCED) }
+ );
const mockConfig = jasmine.createSpyObj(["getConfig"]);
mockConfig.configUpdates = new BehaviorSubject({} as any);
diff --git a/src/app/core/user/demo-user-generator.service.ts b/src/app/core/user/demo-user-generator.service.ts
index 742c0b8d82..0717c04ce8 100644
--- a/src/app/core/user/demo-user-generator.service.ts
+++ b/src/app/core/user/demo-user-generator.service.ts
@@ -35,16 +35,15 @@ export class DemoUserGeneratorService extends DemoDataGenerator {
const demoAdmin = new User("demo-admin");
demoAdmin.name = "demo-admin";
- demoAdmin.admin = true;
// Create temporary session to save users to local storage
- const tmpLocalSession = new LocalSession();
+ const tmpLocalSession = new LocalSession(null);
tmpLocalSession.saveUser(
{ name: demoUser.name, roles: ["user_app"] },
DemoUserGeneratorService.DEFAULT_PASSWORD
);
tmpLocalSession.saveUser(
- { name: demoAdmin.name, roles: ["user_app", "admin"] },
+ { name: demoAdmin.name, roles: ["user_app", "admin_app"] },
DemoUserGeneratorService.DEFAULT_PASSWORD
);
diff --git a/src/app/core/user/user-account/user-account.component.spec.ts b/src/app/core/user/user-account/user-account.component.spec.ts
index 3680ed40b0..d10694f38a 100644
--- a/src/app/core/user/user-account/user-account.component.spec.ts
+++ b/src/app/core/user/user-account/user-account.component.spec.ts
@@ -51,7 +51,10 @@ describe("UserAccountComponent", () => {
"login",
"checkPassword",
]);
- mockSessionService.getCurrentUser.and.returnValue(null);
+ mockSessionService.getCurrentUser.and.returnValue({
+ name: "TestUser",
+ roles: [],
+ });
mockUserAccountService = jasmine.createSpyObj("mockUserAccount", [
"changePassword",
]);
diff --git a/src/app/core/user/user.spec.ts b/src/app/core/user/user.spec.ts
index bdd5f18ec5..66ee248840 100644
--- a/src/app/core/user/user.spec.ts
+++ b/src/app/core/user/user.spec.ts
@@ -52,7 +52,6 @@ describe("User", () => {
_id: ENTITY_TYPE + ":" + id,
name: "tester",
- admin: true,
cloudPasswordEnc: "encryptedPassword",
cloudBaseFolder: "/aam-digital/",
paginatorSettingsPageSize: {},
@@ -63,7 +62,6 @@ describe("User", () => {
const entity = new User(id);
entity.name = expectedData.name;
- entity.admin = expectedData.admin;
// @ts-ignore
entity.cloudPasswordEnc = expectedData.cloudPasswordEnc;
diff --git a/src/app/core/user/user.ts b/src/app/core/user/user.ts
index 9e170f7e20..c3531d59a0 100644
--- a/src/app/core/user/user.ts
+++ b/src/app/core/user/user.ts
@@ -32,9 +32,6 @@ export class User extends Entity {
/** username used for login and identification */
@DatabaseField() name: string;
- /** whether this user has admin rights */
- @DatabaseField() admin: boolean;
-
/** settings for the mat-paginator for tables
* pageSizeOptions is set in the corresponding html of the component,
* pageSize is stored persistently in the database and
@@ -85,21 +82,6 @@ export class User extends Entity {
).toString();
}
- /**
- * Check admin rights of the user.
- */
- public isAdmin(): boolean {
- return this.admin || false;
- }
-
- /**
- * Change this user's admin status
- * @param admin New admin status to be set
- */
- public setAdmin(admin: boolean) {
- this.admin = admin;
- }
-
toString(): string {
return this.name;
}
diff --git a/src/app/core/view/dynamic-routing/router.service.spec.ts b/src/app/core/view/dynamic-routing/router.service.spec.ts
index a34ca59a23..c81f000c8d 100644
--- a/src/app/core/view/dynamic-routing/router.service.spec.ts
+++ b/src/app/core/view/dynamic-routing/router.service.spec.ts
@@ -3,7 +3,6 @@ import { TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { ChildrenListComponent } from "../../../child-dev-project/children/children-list/children-list.component";
-import { AdminGuard } from "../../admin/admin.guard";
import { AdminComponent } from "../../admin/admin/admin.component";
import { ConfigService } from "../../config/config.service";
import { LoggingService } from "../../logging/logging.service";
@@ -11,6 +10,7 @@ import { LoggingService } from "../../logging/logging.service";
import { RouterService } from "./router.service";
import { EntityDetailsComponent } from "../../entity-components/entity-details/entity-details.component";
import { ViewConfig } from "./view-config.interface";
+import { UserRoleGuard } from "../../permissions/user-role.guard";
class TestComponent extends Component {}
@@ -49,23 +49,32 @@ describe("RouterService", () => {
it("should generate and add routes from config for router config", () => {
const testViewConfig = { foo: 1 };
- const testViewConfigs = [
+ const testViewConfigs: ViewConfig[] = [
{ _id: "view:child", component: "ChildrenList" },
{
_id: "view:child/:id",
component: "EntityDetails",
config: testViewConfig,
},
- { _id: "view:admin", component: "Admin", requiresAdmin: true },
+ {
+ _id: "view:admin",
+ component: "Admin",
+ permittedUserRoles: ["user_app"],
+ },
];
const expectedRoutes = [
- { path: "child", component: ChildrenListComponent },
+ { path: "child", component: ChildrenListComponent, data: {} },
{
path: "child/:id",
component: EntityDetailsComponent,
- data: testViewConfig,
+ data: { config: testViewConfig },
+ },
+ {
+ path: "admin",
+ component: AdminComponent,
+ canActivate: [UserRoleGuard],
+ data: { permittedUserRoles: ["user_app"] },
},
- { path: "admin", component: AdminComponent, canActivate: [AdminGuard] },
];
const router = TestBed.inject(Router);
@@ -83,7 +92,7 @@ describe("RouterService", () => {
{ _id: "view:other", component: "EntityDetails" },
];
const expectedRoutes = [
- { path: "child", component: ChildrenListComponent },
+ { path: "child", component: ChildrenListComponent, data: {} },
{ path: "other", component: TestComponent },
];
@@ -113,10 +122,30 @@ describe("RouterService", () => {
const router = TestBed.inject(Router);
expect(router.config.find((r) => r.path === "child").data).toEqual({
- foo: 1,
+ config: { foo: 1 },
});
expect(router.config.find((r) => r.path === "child2").data).toEqual({
- foo: 2,
+ config: { foo: 2 },
});
});
+
+ it("should add the user role guard if userIsPermitted is set", () => {
+ const testViewConfigs: ViewConfig[] = [
+ { _id: "view:admin", component: "Admin", permittedUserRoles: ["admin"] },
+ ];
+ const expectedRoutes = [
+ {
+ path: "admin",
+ component: AdminComponent,
+ canActivate: [UserRoleGuard],
+ data: { permittedUserRoles: ["admin"] },
+ },
+ ];
+ const router = TestBed.inject(Router);
+ spyOn(router, "resetConfig");
+
+ service.reloadRouting(testViewConfigs);
+
+ expect(router.resetConfig).toHaveBeenCalledWith(expectedRoutes);
+ });
});
diff --git a/src/app/core/view/dynamic-routing/router.service.ts b/src/app/core/view/dynamic-routing/router.service.ts
index 6e48641e89..65fb6689fb 100644
--- a/src/app/core/view/dynamic-routing/router.service.ts
+++ b/src/app/core/view/dynamic-routing/router.service.ts
@@ -1,10 +1,14 @@
import { Injectable } from "@angular/core";
import { Route, Router } from "@angular/router";
import { COMPONENT_MAP } from "../../../app.routing";
-import { AdminGuard } from "../../admin/admin.guard";
import { ConfigService } from "../../config/config.service";
import { LoggingService } from "../../logging/logging.service";
-import { ViewConfig } from "./view-config.interface";
+import {
+ PREFIX_VIEW_CONFIG,
+ RouteData,
+ ViewConfig,
+} from "./view-config.interface";
+import { UserRoleGuard } from "../../permissions/user-role.guard";
/**
* The RouterService dynamically sets up Angular routing from config loaded through the {@link ConfigService}.
@@ -16,8 +20,6 @@ import { ViewConfig } from "./view-config.interface";
providedIn: "root",
})
export class RouterService {
- static readonly PREFIX_VIEW_CONFIG = "view:";
-
constructor(
private configService: ConfigService,
private router: Router,
@@ -29,7 +31,7 @@ export class RouterService {
*/
initRouting() {
const viewConfigs = this.configService.getAllConfigs(
- RouterService.PREFIX_VIEW_CONFIG
+ PREFIX_VIEW_CONFIG
);
this.reloadRouting(viewConfigs, this.router.config, true);
}
@@ -79,19 +81,25 @@ export class RouterService {
}
private generateRouteFromConfig(view: ViewConfig): Route {
- const path = view._id.substring(RouterService.PREFIX_VIEW_CONFIG.length); // remove prefix to get actual path
+ const path = view._id.substring(PREFIX_VIEW_CONFIG.length); // remove prefix to get actual path
const route: Route = {
path: path,
component: COMPONENT_MAP[view.component],
};
- if (view.requiresAdmin) {
- route.canActivate = [AdminGuard];
+
+ const routeData: RouteData = {};
+
+ if (view.permittedUserRoles) {
+ route.canActivate = [UserRoleGuard];
+ routeData.permittedUserRoles = view.permittedUserRoles;
}
+
if (view.config) {
- route.data = view.config;
+ routeData.config = view.config;
}
+ route.data = routeData;
return route;
}
}
diff --git a/src/app/core/view/dynamic-routing/view-config.interface.ts b/src/app/core/view/dynamic-routing/view-config.interface.ts
index 84af1a4888..95a097f80c 100644
--- a/src/app/core/view/dynamic-routing/view-config.interface.ts
+++ b/src/app/core/view/dynamic-routing/view-config.interface.ts
@@ -12,8 +12,12 @@ export interface ViewConfig {
*/
component: string;
- /** whether users need admin rights to access this view */
- requiresAdmin?: boolean;
+ /**
+ * Allows to restrict the route to the given list of user roles.
+ * If set, the route can only be visited by users which have a role which is in the list.
+ * If not set, all logged in users can vist the route.
+ */
+ permittedUserRoles?: string[];
/** optional object providing any kind of config to be interpreted by the component for this view */
config?: any;
@@ -26,3 +30,32 @@ export interface ViewConfig {
*/
lazyLoaded?: boolean;
}
+
+/**
+ * The prefix which is used to find the ViewConfig's in the config file
+ */
+export const PREFIX_VIEW_CONFIG = "view:";
+
+/**
+ * This interface is set on the `data` property of the route.
+ * It contains static data which are used to build components and manage permissions.
+ * The generic type defines the interface for the component specific configuration.
+ *
+ * It can be accessed through the activated route:
+ * ```
+ * constructor(private route: ActivatedRoute) {
+ * this.route.data.subscribe(routeData: RouteData => { ...what to do with the data })'
+ * }
+ * ```
+ */
+export interface RouteData {
+ /**
+ * If the `UserRoleGuard` is used for the route, this array holds the information which roles can access the route.
+ */
+ permittedUserRoles?: string[];
+
+ /**
+ * The component specific configuration.
+ */
+ config?: T;
+}
diff --git a/src/app/core/webdav/cloud-file-service.service.spec.ts b/src/app/core/webdav/cloud-file-service.service.spec.ts
index 525de93fa1..80de3a6cd7 100644
--- a/src/app/core/webdav/cloud-file-service.service.spec.ts
+++ b/src/app/core/webdav/cloud-file-service.service.spec.ts
@@ -1,4 +1,4 @@
-import { TestBed } from "@angular/core/testing";
+import { fakeAsync, TestBed, tick } from "@angular/core/testing";
import { CloudFileService } from "./cloud-file-service.service";
import { SessionService } from "../session/session-service/session.service";
import { User } from "../user/user";
@@ -6,16 +6,23 @@ import { AppConfig } from "../app-config/app-config";
import { SessionType } from "../session/session-type";
import { WebdavWrapperService } from "./webdav-wrapper.service";
import { WebDAVClient } from "webdav";
+import { EntityMapperService } from "../entity/entity-mapper.service";
+import {
+ mockEntityMapper,
+ MockEntityMapperService,
+} from "../entity/mock-entity-mapper-service";
describe("CloudFileService", () => {
let cloudFileService: CloudFileService;
- let sessionSpy: jasmine.SpyObj;
+ let mockSessionService: jasmine.SpyObj;
+ let mockedEntityMapper: MockEntityMapperService;
let clientSpy: jasmine.SpyObj;
const BASE_PATH = "base-path/";
let mockWebdav: jasmine.SpyObj;
+ let testUser: User;
- beforeEach(() => {
+ beforeEach(fakeAsync(() => {
AppConfig.settings = {
site_name: "",
session_type: SessionType.mock,
@@ -26,7 +33,17 @@ describe("CloudFileService", () => {
webdav: { remote_url: "test-url" },
};
- sessionSpy = jasmine.createSpyObj("SessionService", ["getCurrentUser"]);
+ mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
+ mockSessionService.getCurrentUser.and.returnValue({
+ name: "user",
+ roles: [],
+ });
+ testUser = new User("user");
+ testUser.cloudUserName = "testuser";
+ testUser.setCloudPassword("testuserpass", "pass");
+ testUser.cloudBaseFolder = BASE_PATH;
+
+ mockedEntityMapper = mockEntityMapper([testUser]);
clientSpy = jasmine.createSpyObj("client", [
"getDirectoryContents",
"createDirectory",
@@ -34,56 +51,52 @@ describe("CloudFileService", () => {
"putFileContents",
"deleteFile",
]);
- mockWebdav = jasmine.createSpyObj(["createClient"]);
-
+ mockWebdav = jasmine.createSpyObj(["createClient", "deleteFile"]);
+ mockWebdav.createClient.and.returnValue(clientSpy);
TestBed.configureTestingModule({
providers: [
CloudFileService,
- { provide: SessionService, useValue: sessionSpy },
+ { provide: SessionService, useValue: mockSessionService },
{ provide: WebdavWrapperService, useValue: mockWebdav },
+ { provide: EntityMapperService, useValue: mockedEntityMapper },
],
});
cloudFileService = TestBed.inject(CloudFileService);
- cloudFileService["client"] = clientSpy;
- cloudFileService["basePath"] = BASE_PATH;
- });
+ tick();
+ mockWebdav.createClient.calls.reset();
+ }));
- it(".connect() should check user existance and call webdav.createClient()", () => {
- sessionSpy.getCurrentUser.and.returnValue(new User("user"));
+ it(".connect() should check user existence and call webdav.createClient()", async () => {
+ await cloudFileService.connect("user", "pass");
- cloudFileService.connect("user", "pass");
- expect(sessionSpy.getCurrentUser).toHaveBeenCalled();
+ expect(mockSessionService.getCurrentUser).toHaveBeenCalled();
expect(mockWebdav.createClient).toHaveBeenCalledWith("test-url", {
username: "user",
password: "pass",
});
});
- it(".connect() should abort if appconfig for webdav is not set", () => {
+ it(".connect() should abort if appconfig for webdav is not set", async () => {
AppConfig.settings.webdav = null;
- sessionSpy.getCurrentUser.and.returnValue(new User("user"));
- cloudFileService.connect("user", "pass");
- expect(mockWebdav.createClient).not.toHaveBeenCalled();
+ await cloudFileService.connect("user", "pass");
+
+ expect(mockWebdav.createClient).not.toHaveBeenCalledTimes(1);
});
- it(".connect() should abort if credentials are passed and not configured for user", () => {
- sessionSpy.getCurrentUser.and.returnValue(new User("user"));
+ it(".connect() should abort if credentials are not passed and not configured for user", async () => {
+ spyOn(mockedEntityMapper, "load").and.resolveTo(new User());
+
+ await cloudFileService.connect();
- cloudFileService.connect();
expect(mockWebdav.createClient).not.toHaveBeenCalled();
});
- it(".connect() should connect using credentials saved for user", () => {
- const testUser = new User("user");
- testUser.cloudUserName = "testuser";
- testUser.setCloudPassword("testuserpass", "pass");
- sessionSpy.getCurrentUser.and.returnValue(testUser);
-
- cloudFileService.connect();
+ it(".connect() should connect using credentials saved for user", async () => {
+ await cloudFileService.connect();
- expect(sessionSpy.getCurrentUser).toHaveBeenCalled();
+ expect(mockSessionService.getCurrentUser).toHaveBeenCalled();
expect(mockWebdav.createClient).toHaveBeenCalledWith("test-url", {
username: "testuser",
password: "testuserpass",
@@ -91,18 +104,17 @@ describe("CloudFileService", () => {
});
it(".checkConnection() should try to create and delete a file", async () => {
- spyOn(cloudFileService, "doesFileExist").and.returnValue(
- new Promise((resolve) => {
- resolve(true);
- })
- );
+ spyOn(cloudFileService, "doesFileExist").and.resolveTo(true);
+
await cloudFileService.checkConnection();
+
expect(clientSpy.putFileContents).toHaveBeenCalled();
expect(clientSpy.deleteFile).toHaveBeenCalled();
});
it(".getDir() should call webdav.getDirectoryContents()", () => {
cloudFileService.getDir("testDir");
+
expect(clientSpy.getDirectoryContents).toHaveBeenCalledWith(
BASE_PATH + "testDir"
);
@@ -110,19 +122,19 @@ describe("CloudFileService", () => {
it("should create dir", () => {
cloudFileService.createDir("testDir");
+
expect(clientSpy.createDirectory).toHaveBeenCalledWith(
BASE_PATH + "testDir"
);
});
- it("should check file existance", async () => {
+ it("should check file existence", async () => {
clientSpy.getDirectoryContents.and.resolveTo({
basename: "filename",
} as any);
expect(await cloudFileService.doesFileExist("filename")).toBe(true);
expect(clientSpy.getDirectoryContents).toHaveBeenCalledWith(BASE_PATH);
-
expect(await cloudFileService.doesFileExist("nonexistant")).toBe(false);
});
diff --git a/src/app/core/webdav/cloud-file-service.service.ts b/src/app/core/webdav/cloud-file-service.service.ts
index 30f5686926..c6268fc085 100644
--- a/src/app/core/webdav/cloud-file-service.service.ts
+++ b/src/app/core/webdav/cloud-file-service.service.ts
@@ -4,6 +4,8 @@ import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
import { SessionService } from "../session/session-service/session.service";
import { WebdavWrapperService } from "./webdav-wrapper.service";
import { WebDAVClient } from "webdav";
+import { EntityMapperService } from "../entity/entity-mapper.service";
+import { User } from "../user/user";
/**
* Connect and access a remote cloud file system like Nextcloud
@@ -30,7 +32,8 @@ export class CloudFileService {
constructor(
private domSanitizer: DomSanitizer,
private sessionService: SessionService,
- private webdav: WebdavWrapperService
+ private webdav: WebdavWrapperService,
+ private entityMapper: EntityMapperService
) {
this.connect();
}
@@ -44,6 +47,7 @@ export class CloudFileService {
username: string = null,
password: string = null
): Promise {
+ const currentUser = this.sessionService.getCurrentUser();
if (
!CloudFileService.WEBDAV_ENABLED ||
!this.sessionService.getCurrentUser()
@@ -52,13 +56,12 @@ export class CloudFileService {
}
this.reset();
-
- const currentUser = this.sessionService.getCurrentUser();
- this.basePath = currentUser.cloudBaseFolder;
+ const userEntity = await this.entityMapper.load(User, currentUser.name);
+ this.basePath = userEntity.cloudBaseFolder;
if (username === null && password == null) {
- username = currentUser.cloudUserName;
- password = currentUser.cloudPasswordDec;
+ username = userEntity.cloudUserName;
+ password = userEntity.cloudPasswordDec;
}
if (!username || !password) {
@@ -147,8 +150,8 @@ export class CloudFileService {
* creates new directory
* @param path path to directory to be created, without leading slash; e.g. 'new-folder'
*/
- public async createDir(path: string) {
- this.client.createDirectory(this.basePath + path);
+ public createDir(path: string) {
+ return this.client.createDirectory(this.basePath + path);
}
/**
@@ -156,7 +159,7 @@ export class CloudFileService {
* @param file The file to be stored
* @param filePath the filename and path to which the file will be uploaded, no leading slash
*/
- public async uploadFile(file: any, filePath: string): Promise {
+ public uploadFile(file: any, filePath: string): Promise {
return this.client.putFileContents(
this.basePath + filePath,
file
diff --git a/src/app/features/historical-data/historical-data/historical-data.component.spec.ts b/src/app/features/historical-data/historical-data/historical-data.component.spec.ts
index 36f7ad3b3f..1c1e144ae4 100644
--- a/src/app/features/historical-data/historical-data/historical-data.component.spec.ts
+++ b/src/app/features/historical-data/historical-data/historical-data.component.spec.ts
@@ -13,9 +13,7 @@ import { HistoricalEntityData } from "../historical-entity-data";
import moment from "moment";
import { DatePipe } from "@angular/common";
import { HistoricalDataService } from "../historical-data.service";
-import { EntityMapperService } from "../../../core/entity/entity-mapper.service";
-import { SessionService } from "../../../core/session/session-service/session.service";
-import { User } from "../../../core/user/user";
+import { MockSessionModule } from "../../../core/session/mock-session.module";
describe("HistoricalDataComponent", () => {
let component: HistoricalDataComponent;
@@ -28,18 +26,14 @@ describe("HistoricalDataComponent", () => {
await TestBed.configureTestingModule({
declarations: [HistoricalDataComponent],
- imports: [HistoricalDataModule, NoopAnimationsModule],
+ imports: [
+ HistoricalDataModule,
+ NoopAnimationsModule,
+ MockSessionModule.withState(),
+ ],
providers: [
{ provide: HistoricalDataService, useValue: mockHistoricalDataService },
- {
- provide: EntityMapperService,
- useValue: jasmine.createSpyObj(["save", "remove"]),
- },
DatePipe,
- {
- provide: SessionService,
- useValue: { getCurrentUser: () => new User() },
- },
],
}).compileComponents();
});
diff --git a/src/app/features/reporting/reporting/reporting.component.spec.ts b/src/app/features/reporting/reporting/reporting.component.spec.ts
index 0a8bb44a39..1bca8dedf5 100644
--- a/src/app/features/reporting/reporting/reporting.component.spec.ts
+++ b/src/app/features/reporting/reporting/reporting.component.spec.ts
@@ -19,11 +19,12 @@ import {
ReportConfig,
ReportingComponentConfig,
} from "./reporting-component-config";
+import { RouteData } from "../../../core/view/dynamic-routing/view-config.interface";
describe("ReportingComponent", () => {
let component: ReportingComponent;
let fixture: ComponentFixture;
- const mockRouteData = new Subject();
+ const mockRouteData = new Subject>();
let mockReportingService: jasmine.SpyObj;
const testReport: ReportConfig = {
@@ -60,7 +61,7 @@ describe("ReportingComponent", () => {
fixture = TestBed.createComponent(ReportingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
- mockRouteData.next({ aggregationDefinitions: {} });
+ mockRouteData.next({ config: { reports: [] } });
});
it("should create", () => {
@@ -71,7 +72,7 @@ describe("ReportingComponent", () => {
const aggregationConfig: ReportingComponentConfig = {
reports: [testReport],
};
- mockRouteData.next(aggregationConfig);
+ mockRouteData.next({ config: aggregationConfig });
expect(component.loading).toBeFalsy();
diff --git a/src/app/features/reporting/reporting/reporting.component.ts b/src/app/features/reporting/reporting/reporting.component.ts
index 30c3ef15f0..6dd0780fb6 100644
--- a/src/app/features/reporting/reporting/reporting.component.ts
+++ b/src/app/features/reporting/reporting/reporting.component.ts
@@ -11,6 +11,7 @@ import {
ReportingComponentConfig,
} from "./reporting-component-config";
import moment from "moment";
+import { RouteData } from "../../../core/view/dynamic-routing/view-config.interface";
@Component({
selector: "app-reporting",
@@ -34,12 +35,14 @@ export class ReportingComponent implements OnInit {
) {}
ngOnInit() {
- this.activatedRoute.data.subscribe((config: ReportingComponentConfig) => {
- this.availableReports = config.reports;
- if (this.availableReports?.length === 1) {
- this.selectedReport = this.availableReports[0];
+ this.activatedRoute.data.subscribe(
+ (data: RouteData) => {
+ this.availableReports = data.config.reports;
+ if (this.availableReports?.length === 1) {
+ this.selectedReport = this.availableReports[0];
+ }
}
- });
+ );
}
async calculateResults() {
diff --git a/src/app/utils/performance-tests.spec.ts b/src/app/utils/performance-tests.spec.ts
index a5f6bbaa4b..eddf66c1e7 100644
--- a/src/app/utils/performance-tests.spec.ts
+++ b/src/app/utils/performance-tests.spec.ts
@@ -23,6 +23,7 @@ import { SyncState } from "../core/session/session-states/sync-state.enum";
import moment from "moment";
import { ChildrenService } from "../child-dev-project/children/children.service";
import { deleteDB } from "idb";
+import { waitForChangeTo } from "../core/session/session-states/session-utils";
const TEST_REMOTE_DATABASE_URL = "http://dev.aam-digital.com/db/";
// WARNING - do not check in credentials into public git repository
@@ -71,7 +72,9 @@ xdescribe("Performance Tests", () => {
TEST_REMOTE_DATABASE_USER,
TEST_REMOTE_DATABASE_PASSWORD
);
- await session.getSyncState().waitForChangeTo(SyncState.COMPLETED);
+ await session.syncState
+ .pipe(waitForChangeTo(SyncState.COMPLETED))
+ .toPromise();
syncTimer.stop();
console.log("sync time", syncTimer.getDuration());
From 85e0a2c40920fb61b89bd533a11125d99e68e398 Mon Sep 17 00:00:00 2001
From: Snyk bot
Date: Sun, 29 Aug 2021 13:43:15 +0200
Subject: [PATCH 24/34] fix: upgrade @sentry/browser from 6.10.0 to 6.11.0
(#957)
---
package-lock.json | 127 +++++++++++++++++++++++-----------------------
package.json | 2 +-
2 files changed, 65 insertions(+), 64 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index ef1312e5f5..e0a5ac1395 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",
@@ -23,7 +24,7 @@
"@angular/router": "^11.2.12",
"@angular/service-worker": "^11.2.12",
"@ngneat/until-destroy": "^8.1.1",
- "@sentry/browser": "^6.10.0",
+ "@sentry/browser": "^6.11.0",
"angulartics2": "^10.0.0",
"crypto-js": "^4.1.1",
"deep-object-diff": "^1.1.0",
@@ -4018,13 +4019,13 @@
}
},
"node_modules/@sentry/browser": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.10.0.tgz",
- "integrity": "sha512-H0Blgp8f8bomebkkGWIgxHVjabtQAlsKJDiFXBg7gIc75YcarRxwH0R3hMog1/h8mmv4CGGUsy5ljYW6jsNnvA==",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.11.0.tgz",
+ "integrity": "sha512-Qr2QRA0t5/S9QQqxzYKvM9W8prvmiWuldfwRX4hubovXzcXLgUi4WK0/H612wSbYZ4dNAEcQbtlxFWJNN4wxdg==",
"dependencies": {
- "@sentry/core": "6.10.0",
- "@sentry/types": "6.10.0",
- "@sentry/utils": "6.10.0",
+ "@sentry/core": "6.11.0",
+ "@sentry/types": "6.11.0",
+ "@sentry/utils": "6.11.0",
"tslib": "^1.9.3"
},
"engines": {
@@ -4037,14 +4038,14 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/core": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.10.0.tgz",
- "integrity": "sha512-5KlxHJlbD7AMo+b9pMGkjxUOfMILtsqCtGgI7DMvZNfEkdohO8QgUY+hPqr540kmwArFS91ipQYWhqzGaOhM3Q==",
- "dependencies": {
- "@sentry/hub": "6.10.0",
- "@sentry/minimal": "6.10.0",
- "@sentry/types": "6.10.0",
- "@sentry/utils": "6.10.0",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.11.0.tgz",
+ "integrity": "sha512-09TB+f3pqEq8LFahFWHO6I/4DxHo+NcS52OkbWMDqEi6oNZRD7PhPn3i14LfjsYVv3u3AESU8oxSEGbFrr2UjQ==",
+ "dependencies": {
+ "@sentry/hub": "6.11.0",
+ "@sentry/minimal": "6.11.0",
+ "@sentry/types": "6.11.0",
+ "@sentry/utils": "6.11.0",
"tslib": "^1.9.3"
},
"engines": {
@@ -4057,12 +4058,12 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/hub": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.10.0.tgz",
- "integrity": "sha512-MV8wjhWiFAXZAhmj7Ef5QdBr2IF93u8xXiIo2J+dRZ7eVa4/ZszoUiDbhUcl/TPxczaw4oW2a6tINBNFLzXiig==",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.11.0.tgz",
+ "integrity": "sha512-pT9hf+ZJfVFpoZopoC+yJmFNclr4NPqPcl2cgguqCHb69DklD1NxgBNWK8D6X05qjnNFDF991U6t1mxP9HrGuw==",
"dependencies": {
- "@sentry/types": "6.10.0",
- "@sentry/utils": "6.10.0",
+ "@sentry/types": "6.11.0",
+ "@sentry/utils": "6.11.0",
"tslib": "^1.9.3"
},
"engines": {
@@ -4075,12 +4076,12 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/minimal": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.10.0.tgz",
- "integrity": "sha512-yarm046UgUFIBoxqnBan2+BEgaO9KZCrLzsIsmALiQvpfW92K1lHurSawl5W6SR7wCYBnNn7CPvPE/BHFdy4YA==",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.11.0.tgz",
+ "integrity": "sha512-XkZ7qrdlGp4IM/gjGxf1Q575yIbl5RvPbg+WFeekpo16Ufvzx37Mr8c2xsZaWosISVyE6eyFpooORjUlzy8EDw==",
"dependencies": {
- "@sentry/hub": "6.10.0",
- "@sentry/types": "6.10.0",
+ "@sentry/hub": "6.11.0",
+ "@sentry/types": "6.11.0",
"tslib": "^1.9.3"
},
"engines": {
@@ -4093,19 +4094,19 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/types": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.10.0.tgz",
- "integrity": "sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw==",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.11.0.tgz",
+ "integrity": "sha512-gm5H9eZhL6bsIy/h3T+/Fzzz2vINhHhqd92CjHle3w7uXdTdFV98i2pDpErBGNTSNzbntqOMifYEB5ENtZAvcg==",
"engines": {
"node": ">=6"
}
},
"node_modules/@sentry/utils": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.10.0.tgz",
- "integrity": "sha512-F9OczOcZMFtazYVZ6LfRIe65/eOfQbiAedIKS0li4npuMz0jKYRbxrjd/U7oLiNQkPAp4/BujU4m1ZIwq6a+tg==",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.11.0.tgz",
+ "integrity": "sha512-IOvyFHcnbRQxa++jO+ZUzRvFHEJ1cZjrBIQaNVc0IYF0twUOB5PTP6joTcix38ldaLeapaPZ9LGfudbvYvxkdg==",
"dependencies": {
- "@sentry/types": "6.10.0",
+ "@sentry/types": "6.11.0",
"tslib": "^1.9.3"
},
"engines": {
@@ -32090,13 +32091,13 @@
}
},
"@sentry/browser": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.10.0.tgz",
- "integrity": "sha512-H0Blgp8f8bomebkkGWIgxHVjabtQAlsKJDiFXBg7gIc75YcarRxwH0R3hMog1/h8mmv4CGGUsy5ljYW6jsNnvA==",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.11.0.tgz",
+ "integrity": "sha512-Qr2QRA0t5/S9QQqxzYKvM9W8prvmiWuldfwRX4hubovXzcXLgUi4WK0/H612wSbYZ4dNAEcQbtlxFWJNN4wxdg==",
"requires": {
- "@sentry/core": "6.10.0",
- "@sentry/types": "6.10.0",
- "@sentry/utils": "6.10.0",
+ "@sentry/core": "6.11.0",
+ "@sentry/types": "6.11.0",
+ "@sentry/utils": "6.11.0",
"tslib": "^1.9.3"
},
"dependencies": {
@@ -32108,14 +32109,14 @@
}
},
"@sentry/core": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.10.0.tgz",
- "integrity": "sha512-5KlxHJlbD7AMo+b9pMGkjxUOfMILtsqCtGgI7DMvZNfEkdohO8QgUY+hPqr540kmwArFS91ipQYWhqzGaOhM3Q==",
- "requires": {
- "@sentry/hub": "6.10.0",
- "@sentry/minimal": "6.10.0",
- "@sentry/types": "6.10.0",
- "@sentry/utils": "6.10.0",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.11.0.tgz",
+ "integrity": "sha512-09TB+f3pqEq8LFahFWHO6I/4DxHo+NcS52OkbWMDqEi6oNZRD7PhPn3i14LfjsYVv3u3AESU8oxSEGbFrr2UjQ==",
+ "requires": {
+ "@sentry/hub": "6.11.0",
+ "@sentry/minimal": "6.11.0",
+ "@sentry/types": "6.11.0",
+ "@sentry/utils": "6.11.0",
"tslib": "^1.9.3"
},
"dependencies": {
@@ -32127,12 +32128,12 @@
}
},
"@sentry/hub": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.10.0.tgz",
- "integrity": "sha512-MV8wjhWiFAXZAhmj7Ef5QdBr2IF93u8xXiIo2J+dRZ7eVa4/ZszoUiDbhUcl/TPxczaw4oW2a6tINBNFLzXiig==",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.11.0.tgz",
+ "integrity": "sha512-pT9hf+ZJfVFpoZopoC+yJmFNclr4NPqPcl2cgguqCHb69DklD1NxgBNWK8D6X05qjnNFDF991U6t1mxP9HrGuw==",
"requires": {
- "@sentry/types": "6.10.0",
- "@sentry/utils": "6.10.0",
+ "@sentry/types": "6.11.0",
+ "@sentry/utils": "6.11.0",
"tslib": "^1.9.3"
},
"dependencies": {
@@ -32144,12 +32145,12 @@
}
},
"@sentry/minimal": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.10.0.tgz",
- "integrity": "sha512-yarm046UgUFIBoxqnBan2+BEgaO9KZCrLzsIsmALiQvpfW92K1lHurSawl5W6SR7wCYBnNn7CPvPE/BHFdy4YA==",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.11.0.tgz",
+ "integrity": "sha512-XkZ7qrdlGp4IM/gjGxf1Q575yIbl5RvPbg+WFeekpo16Ufvzx37Mr8c2xsZaWosISVyE6eyFpooORjUlzy8EDw==",
"requires": {
- "@sentry/hub": "6.10.0",
- "@sentry/types": "6.10.0",
+ "@sentry/hub": "6.11.0",
+ "@sentry/types": "6.11.0",
"tslib": "^1.9.3"
},
"dependencies": {
@@ -32161,16 +32162,16 @@
}
},
"@sentry/types": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.10.0.tgz",
- "integrity": "sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw=="
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.11.0.tgz",
+ "integrity": "sha512-gm5H9eZhL6bsIy/h3T+/Fzzz2vINhHhqd92CjHle3w7uXdTdFV98i2pDpErBGNTSNzbntqOMifYEB5ENtZAvcg=="
},
"@sentry/utils": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.10.0.tgz",
- "integrity": "sha512-F9OczOcZMFtazYVZ6LfRIe65/eOfQbiAedIKS0li4npuMz0jKYRbxrjd/U7oLiNQkPAp4/BujU4m1ZIwq6a+tg==",
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.11.0.tgz",
+ "integrity": "sha512-IOvyFHcnbRQxa++jO+ZUzRvFHEJ1cZjrBIQaNVc0IYF0twUOB5PTP6joTcix38ldaLeapaPZ9LGfudbvYvxkdg==",
"requires": {
- "@sentry/types": "6.10.0",
+ "@sentry/types": "6.11.0",
"tslib": "^1.9.3"
},
"dependencies": {
diff --git a/package.json b/package.json
index 0cb56b6dec..4ae4fd5de9 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"@angular/router": "^11.2.12",
"@angular/service-worker": "^11.2.12",
"@ngneat/until-destroy": "^8.1.1",
- "@sentry/browser": "^6.10.0",
+ "@sentry/browser": "^6.11.0",
"angulartics2": "^10.0.0",
"crypto-js": "^4.1.1",
"deep-object-diff": "^1.1.0",
From 4206a167cebd294fc31ff05e8cb2f147edbcb7f5 Mon Sep 17 00:00:00 2001
From: kirtijadhav <83791155+kirtijadhav@users.noreply.github.com>
Date: Sun, 29 Aug 2021 15:08:36 +0200
Subject: [PATCH 25/34] fix: Added progress bar in attendance roll-call
---
.../roll-call/roll-call.component.html | 8 ++++++++
.../roll-call/roll-call.component.scss | 10 ++++++++++
2 files changed, 18 insertions(+)
diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.component.html b/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.component.html
index 13562e0f02..0314ee7eca 100644
--- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.component.html
+++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call/roll-call.component.html
@@ -1,6 +1,14 @@
0">
+
+
+
{{ currentIndex }} / {{ entries.length }}
+
Date: Sun, 29 Aug 2021 17:42:16 +0200
Subject: [PATCH 26/34] test: Added utility function for performance testing
(#958)
---
src/app/utils/performance-tests.spec.ts | 180 +++++++++++-------------
1 file changed, 84 insertions(+), 96 deletions(-)
diff --git a/src/app/utils/performance-tests.spec.ts b/src/app/utils/performance-tests.spec.ts
index eddf66c1e7..f4b2e47bdf 100644
--- a/src/app/utils/performance-tests.spec.ts
+++ b/src/app/utils/performance-tests.spec.ts
@@ -1,96 +1,101 @@
-/*
- * 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 { TestBed, waitForAsync } from "@angular/core/testing";
import { SessionService } from "../core/session/session-service/session.service";
-import { AppConfig } from "../core/app-config/app-config";
import { AppModule } from "../app.module";
-import { SyncState } from "../core/session/session-states/sync-state.enum";
import moment from "moment";
-import { ChildrenService } from "../child-dev-project/children/children.service";
-import { deleteDB } from "idb";
-import { waitForChangeTo } from "../core/session/session-states/session-utils";
+import { LoggingService } from "../core/logging/logging.service";
+import { Database } from "../core/database/database";
+import { DemoDataService } from "../core/demo-data/demo-data.service";
+import { PouchDatabase } from "../core/database/pouch-database";
+import { LocalSession } from "app/core/session/session-service/local-session";
-const TEST_REMOTE_DATABASE_URL = "http://dev.aam-digital.com/db/";
-// WARNING - do not check in credentials into public git repository
-const TEST_REMOTE_DATABASE_USER = "[edit before running test]";
-const TEST_REMOTE_DATABASE_PASSWORD = "[edit before running test]";
-
-/**
- * These performance tests are actually integration tests that interact with a remote database.
- *
- * You need to enable CORS for the tests to run by editing karma.conf.js replacing `browsers: ['Chrome'],` with the following:
-browsers: ['Chrome_without_security'],
-customLaunchers:{
- Chrome_without_security:{
- base: 'Chrome',
- flags: ['--disable-web-security']
- }
-},
- */
xdescribe("Performance Tests", () => {
- beforeEach(
- waitForAsync(() => {
- TestBed.configureTestingModule({
- imports: [AppModule],
- }).compileComponents();
+ let mockDatabase: PouchDatabase;
- AppConfig.settings = {
- database: {
- name: "app",
- remote_url: TEST_REMOTE_DATABASE_URL,
- timeout: 60000,
- useTemporaryDatabase: false,
- },
- } as any;
+ beforeEach(async () => {
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000;
- jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000;
+ const loggingService = new LoggingService();
+ // Uncomment this line to run performance tests with the InBrowser database.
+ // mockDatabase = PouchDatabase.createWithIndexedDB(
+ mockDatabase = PouchDatabase.createWithInMemoryDB(
+ "performance_db",
+ loggingService
+ );
+ const mockSessionService = new LocalSession(mockDatabase);
+
+ await TestBed.configureTestingModule({
+ imports: [AppModule],
+ providers: [
+ { provide: Database, useValue: mockDatabase },
+ { provide: SessionService, useValue: mockSessionService },
+ { provide: LoggingService, useValue: loggingService },
+ ],
+ }).compileComponents();
+ const demoDataService = TestBed.inject(DemoDataService);
+ const setup = new Timer();
+ await demoDataService.publishDemoData();
+ console.log("finished publishing demo data", setup.getDuration());
+ });
+
+ afterEach(
+ waitForAsync(() => {
+ return mockDatabase.destroy();
})
);
- it("sync initial and indexing", async () => {
- // delete previously synced database; uncomment this to start with a clean state and test an initial sync.
- // await deleteAllIndexedDB(db => true);
-
- const session = TestBed.inject(SessionService);
- const syncTimer = new Timer(true);
- await session.login(
- TEST_REMOTE_DATABASE_USER,
- TEST_REMOTE_DATABASE_PASSWORD
+ it("basic test example", async () => {
+ await comparePerformance(
+ (num) => new Promise((resolve) => setTimeout(() => resolve(num), 100)),
+ (num) => Promise.resolve(num),
+ "Basic performance test example",
+ [10, 20, 30]
);
- await session.syncState
- .pipe(waitForChangeTo(SyncState.COMPLETED))
- .toPromise();
- syncTimer.stop();
- console.log("sync time", syncTimer.getDuration());
-
- // delete index views from previous test runs; comment this to test queries on existing indices
- // await deleteAllIndexedDB(db => db.includes("mrview"));
-
- const childrenService = TestBed.inject(ChildrenService);
- const indexTimer = new Timer(true);
- await childrenService.createDatabaseIndices();
- indexTimer.stop();
- console.log("indexing time", indexTimer.getDuration());
-
- expect(indexTimer.getDuration()).toBe(0); // display indexing time as failed assertion; see console for details
});
});
+async function comparePerformance(
+ currentFunction: (val?: V) => Promise,
+ improvedFunction: (val?: V) => Promise,
+ description: string,
+ input?: V[]
+) {
+ const diffs: number[] = [];
+ if (input) {
+ for (const el of input) {
+ const diff = await getExecutionDiff(
+ () => currentFunction(el),
+ () => improvedFunction(el)
+ );
+ diffs.push(diff);
+ }
+ const avgDiff = diffs.reduce((sum, cur) => sum + cur, 0) / diffs.length;
+ fail("<" + description + "> Average improvement: " + avgDiff + "ms");
+ } else {
+ const diff = await getExecutionDiff(currentFunction, improvedFunction);
+ fail("<" + description + "> Execution time improvement " + diff + "ms");
+ }
+}
+
+async function getExecutionDiff(
+ currentFunction: () => Promise,
+ improvedFunction: () => Promise
+): Promise {
+ const currentTimer = new Timer();
+ const currentResult = await currentFunction();
+ const currentDuration = currentTimer.getDuration();
+ const improvedTimer = new Timer();
+ const improvedResult = await improvedFunction();
+ const improvedDuration = improvedTimer.getDuration();
+ expect(improvedResult).toEqual(
+ currentResult,
+ "current " +
+ JSON.stringify(currentResult) +
+ " improved " +
+ JSON.stringify(improvedResult)
+ );
+ return currentDuration - improvedDuration;
+}
+
/**
* Utility class to calculate duration of an action.
*/
@@ -98,7 +103,7 @@ class Timer {
private startTime;
private stopTime;
- constructor(start: boolean) {
+ constructor(start: boolean = true) {
if (start) {
this.start();
}
@@ -117,20 +122,3 @@ class Timer {
return -this.startTime.diff(this.stopTime ?? moment(), "milliseconds");
}
}
-
-/**
- * Delete all indexedDB databases in the browser matching the given filter.
- * @param filterFun Filter function taking a database name and returning true if this should be deleted.
- */
-export async function deleteAllIndexedDB(
- filterFun: (dbName: string) => boolean
-): Promise {
- // @ts-ignore
- const databases = await indexedDB.databases();
- for (const db of databases) {
- if (filterFun(db.name)) {
- console.log("deleting indexedDB", db.name);
- await deleteDB(db.name);
- }
- }
-}
From fe55523d2b5483a42213caa12eaca0245b038638 Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Mon, 30 Aug 2021 17:40:57 +0200
Subject: [PATCH 27/34] docs(storybook): fix and clean up storybook (#959)
---
.../roll-call-setup.component.spec.ts | 6 +-
.../dashboard-shortcut-widget.stories.ts | 2 +-
src/app/core/session/mock-session.module.ts | 7 +-
.../session-service/local-session.spec.ts | 7 +-
.../session-service/remote-session.spec.ts | 7 +-
.../session-service/session.service.spec.ts | 3 +-
.../synced-session.service.spec.ts | 7 +-
.../language-select.stories.ts | 17 +-
.../reporting/reporting/reporting.stories.ts | 13 +-
src/storybook-examples/Button.stories.ts | 39 ----
src/storybook-examples/Header.stories.ts | 31 ---
.../Introduction.stories.mdx | 207 ------------------
src/storybook-examples/Page.stories.ts | 36 ---
.../assets/code-brackets.svg | 1 -
src/storybook-examples/assets/colors.svg | 1 -
src/storybook-examples/assets/comments.svg | 1 -
src/storybook-examples/assets/direction.svg | 1 -
src/storybook-examples/assets/flow.svg | 1 -
src/storybook-examples/assets/plugin.svg | 1 -
src/storybook-examples/assets/repo.svg | 1 -
src/storybook-examples/assets/stackalt.svg | 1 -
src/storybook-examples/button.component.ts | 53 -----
src/storybook-examples/button.css | 30 ---
src/storybook-examples/header.component.ts | 63 ------
src/storybook-examples/header.css | 26 ---
src/storybook-examples/page.component.ts | 92 --------
src/storybook-examples/page.css | 69 ------
27 files changed, 38 insertions(+), 685 deletions(-)
delete mode 100644 src/storybook-examples/Button.stories.ts
delete mode 100644 src/storybook-examples/Header.stories.ts
delete mode 100644 src/storybook-examples/Introduction.stories.mdx
delete mode 100644 src/storybook-examples/Page.stories.ts
delete mode 100644 src/storybook-examples/assets/code-brackets.svg
delete mode 100644 src/storybook-examples/assets/colors.svg
delete mode 100644 src/storybook-examples/assets/comments.svg
delete mode 100644 src/storybook-examples/assets/direction.svg
delete mode 100644 src/storybook-examples/assets/flow.svg
delete mode 100644 src/storybook-examples/assets/plugin.svg
delete mode 100644 src/storybook-examples/assets/repo.svg
delete mode 100644 src/storybook-examples/assets/stackalt.svg
delete mode 100644 src/storybook-examples/button.component.ts
delete mode 100644 src/storybook-examples/button.css
delete mode 100644 src/storybook-examples/header.component.ts
delete mode 100644 src/storybook-examples/header.css
delete mode 100644 src/storybook-examples/page.component.ts
delete mode 100644 src/storybook-examples/page.css
diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.spec.ts b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.spec.ts
index f02f0ab753..126adea656 100644
--- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.spec.ts
+++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.spec.ts
@@ -13,8 +13,10 @@ import { AttendanceModule } from "../../attendance.module";
import { MatNativeDateModule } from "@angular/material/core";
import { AttendanceService } from "../../attendance.service";
import { EventNote } from "../../model/event-note";
-import { MockSessionModule } from "../../../../core/session/mock-session.module";
-import { TEST_USER } from "../../../../core/session/session-service/session.service.spec";
+import {
+ MockSessionModule,
+ TEST_USER,
+} from "../../../../core/session/mock-session.module";
describe("RollCallSetupComponent", () => {
let component: RollCallSetupComponent;
diff --git a/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget/dashboard-shortcut-widget.stories.ts b/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget/dashboard-shortcut-widget.stories.ts
index 798ee0579c..65d4aeaa43 100644
--- a/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget/dashboard-shortcut-widget.stories.ts
+++ b/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget/dashboard-shortcut-widget.stories.ts
@@ -8,7 +8,7 @@ import { DashboardShortcutWidgetComponent } from "./dashboard-shortcut-widget.co
import { MenuItem } from "../../navigation/menu-item";
export default {
- title: "ShortcutDashboardWidget",
+ title: "Core/ShortcutDashboardWidget",
component: DashboardShortcutWidgetComponent,
decorators: [
moduleMetadata({
diff --git a/src/app/core/session/mock-session.module.ts b/src/app/core/session/mock-session.module.ts
index 72a9ae57d7..34feba5f82 100644
--- a/src/app/core/session/mock-session.module.ts
+++ b/src/app/core/session/mock-session.module.ts
@@ -1,10 +1,6 @@
import { ModuleWithProviders, NgModule } from "@angular/core";
import { LocalSession } from "./session-service/local-session";
import { SessionService } from "./session-service/session.service";
-import {
- TEST_PASSWORD,
- TEST_USER,
-} from "./session-service/session.service.spec";
import { LoginState } from "./session-states/login-state.enum";
import { EntityMapperService } from "../entity/entity-mapper.service";
import {
@@ -13,6 +9,9 @@ import {
} from "../entity/mock-entity-mapper-service";
import { User } from "../user/user";
+export const TEST_USER = "test";
+export const TEST_PASSWORD = "pass";
+
/**
* A simple module that can be imported in test files or stories to have mock implementations of the SessionService
* and the EntityMapper. To use it put `imports: [MockSessionModule.withState()]` into the module definition of the
diff --git a/src/app/core/session/session-service/local-session.spec.ts b/src/app/core/session/session-service/local-session.spec.ts
index ffc70d47f3..b4947b66cb 100644
--- a/src/app/core/session/session-service/local-session.spec.ts
+++ b/src/app/core/session/session-service/local-session.spec.ts
@@ -20,11 +20,8 @@ import { LocalSession } from "./local-session";
import { SessionType } from "../session-type";
import { passwordEqualsEncrypted, DatabaseUser, LocalUser } from "./local-user";
import { LoginState } from "../session-states/login-state.enum";
-import {
- TEST_PASSWORD,
- TEST_USER,
- testSessionServiceImplementation,
-} from "./session.service.spec";
+import { testSessionServiceImplementation } from "./session.service.spec";
+import { TEST_PASSWORD, TEST_USER } from "../mock-session.module";
describe("LocalSessionService", () => {
let localSession: LocalSession;
diff --git a/src/app/core/session/session-service/remote-session.spec.ts b/src/app/core/session/session-service/remote-session.spec.ts
index a0db30a37a..af80b6111d 100644
--- a/src/app/core/session/session-service/remote-session.spec.ts
+++ b/src/app/core/session/session-service/remote-session.spec.ts
@@ -5,13 +5,10 @@ import { of, throwError } from "rxjs";
import { AppConfig } from "../../app-config/app-config";
import { SessionType } from "../session-type";
import { LoggingService } from "../../logging/logging.service";
-import {
- TEST_PASSWORD,
- TEST_USER,
- testSessionServiceImplementation,
-} from "./session.service.spec";
+import { testSessionServiceImplementation } from "./session.service.spec";
import { DatabaseUser } from "./local-user";
import { LoginState } from "../session-states/login-state.enum";
+import { TEST_PASSWORD, TEST_USER } from "../mock-session.module";
describe("RemoteSessionService", () => {
let service: RemoteSession;
diff --git a/src/app/core/session/session-service/session.service.spec.ts b/src/app/core/session/session-service/session.service.spec.ts
index 2d3f2add2b..661d678ae7 100644
--- a/src/app/core/session/session-service/session.service.spec.ts
+++ b/src/app/core/session/session-service/session.service.spec.ts
@@ -18,9 +18,8 @@
import { LoginState } from "../session-states/login-state.enum";
import { SessionService } from "./session.service";
import { SyncState } from "../session-states/sync-state.enum";
+import { TEST_PASSWORD, TEST_USER } from "../mock-session.module";
-export const TEST_USER = "test";
-export const TEST_PASSWORD = "pass";
/**
* Default tests for testing basic functionality of any SessionService implementation.
* The session has to be setup, so TEST_USER and TEST_PASSWORD are (the only) valid credentials
diff --git a/src/app/core/session/session-service/synced-session.service.spec.ts b/src/app/core/session/session-service/synced-session.service.spec.ts
index c92a139ac6..2b0cac5839 100644
--- a/src/app/core/session/session-service/synced-session.service.spec.ts
+++ b/src/app/core/session/session-service/synced-session.service.spec.ts
@@ -30,11 +30,8 @@ import { of, throwError } from "rxjs";
import { MatSnackBarModule } from "@angular/material/snack-bar";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { DatabaseUser } from "./local-user";
-import {
- TEST_PASSWORD,
- TEST_USER,
- testSessionServiceImplementation,
-} from "./session.service.spec";
+import { TEST_PASSWORD, TEST_USER } from "../mock-session.module";
+import { testSessionServiceImplementation } from "./session.service.spec";
describe("SyncedSessionService", () => {
let sessionService: SyncedSessionService;
diff --git a/src/app/core/translation/language-selector/language-select.stories.ts b/src/app/core/translation/language-selector/language-select.stories.ts
index f112e40b57..89755a3dac 100644
--- a/src/app/core/translation/language-selector/language-select.stories.ts
+++ b/src/app/core/translation/language-selector/language-select.stories.ts
@@ -2,12 +2,18 @@ import { moduleMetadata } from "@storybook/angular";
import { Meta, Story } from "@storybook/angular/types-6-0";
import { LanguageSelectComponent } from "./language-select.component";
import { TranslationModule } from "../translation.module";
+import { RouterTestingModule } from "@angular/router/testing";
+import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
export default {
- title: "UI/LanguageSelect",
+ title: "Core/LanguageSelect",
decorators: [
moduleMetadata({
- imports: [TranslationModule],
+ imports: [
+ TranslationModule,
+ RouterTestingModule,
+ BrowserAnimationsModule,
+ ],
}),
],
} as Meta;
@@ -17,4 +23,9 @@ const Template: Story = (args) => ({
props: args,
});
-export const Primary = Template.bind({});
+export const Primary = Template.bind({
+ availableLocales: [
+ { locale: "de", regionCode: "de" },
+ { locale: "en-US", regionCode: "us" },
+ ],
+});
diff --git a/src/app/features/reporting/reporting/reporting.stories.ts b/src/app/features/reporting/reporting/reporting.stories.ts
index 2239764188..d8bf698538 100644
--- a/src/app/features/reporting/reporting/reporting.stories.ts
+++ b/src/app/features/reporting/reporting/reporting.stories.ts
@@ -8,9 +8,9 @@ import { ReportingService } from "../reporting.service";
import { MatNativeDateModule } from "@angular/material/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { FontAwesomeIconsModule } from "../../../core/icons/font-awesome-icons.module";
-import { BackupService } from "../../../core/admin/services/backup.service";
import { ReportingModule } from "../reporting.module";
import { genders } from "../../../child-dev-project/children/model/genders";
+import { ExportService } from "../../../core/export/export-service/export.service";
const reportingService = {
calculateReport: () => {
@@ -184,7 +184,7 @@ const reportingService = {
};
export default {
- title: "Child Dev Project/Reporting",
+ title: "Features/Reporting",
component: ReportingComponent,
decorators: [
moduleMetadata({
@@ -196,10 +196,15 @@ export default {
FontAwesomeIconsModule,
],
providers: [
- { provide: ActivatedRoute, useValue: { data: of({}) } },
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ data: of({ config: { reports: [{ title: "Dummy Report" }] } }),
+ },
+ },
{ provide: ReportingService, useValue: reportingService },
{
- provide: BackupService,
+ provide: ExportService,
useValue: { createJson: () => {}, createCsv: () => {} },
},
],
diff --git a/src/storybook-examples/Button.stories.ts b/src/storybook-examples/Button.stories.ts
deleted file mode 100644
index feba4f551f..0000000000
--- a/src/storybook-examples/Button.stories.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
-import { Story, Meta } from '@storybook/angular/types-6-0';
-import Button from './button.component';
-
-export default {
- title: 'Example/Button',
- component: Button,
- argTypes: {
- backgroundColor: { control: 'color' },
- },
-} as Meta;
-
-const Template: Story
diff --git a/src/app/child-dev-project/schools/demo-school-generator.service.ts b/src/app/child-dev-project/schools/demo-school-generator.service.ts
index 284d1710ef..a880efad5e 100644
--- a/src/app/child-dev-project/schools/demo-school-generator.service.ts
+++ b/src/app/child-dev-project/schools/demo-school-generator.service.ts
@@ -30,7 +30,7 @@ export class DemoSchoolGenerator extends DemoDataGenerator {
for (let i = 1; i <= this.config.count; i++) {
const school = new School(String(i));
- school.medium = faker.random.arrayElement([
+ school["language"] = faker.random.arrayElement([
"Hindi",
"English",
"Bengali",
@@ -41,27 +41,16 @@ export class DemoSchoolGenerator extends DemoDataGenerator {
faker.random.arrayElement([
$localize`:School demo name that is prepended to a name:School`,
$localize`:School demo name that is prepended to a name:High School`,
- school.medium + " Medium",
+ school["language"] + " Language",
]);
- school.address = faker.address.streetAddress();
- school.phone = faker.phone.phoneNumberFormat();
- school.privateSchool = faker.datatype.boolean();
- school.upToClass = faker.random.arrayElement([8, 10, 12]);
- school.academicBoard = faker.random.arrayElement([
- "CBSE",
- "ICSE",
- "WBBSE",
- ]);
- school.timing = faker.random.arrayElement([
+ school["address"] = faker.address.streetAddress();
+ school["phone"] = faker.phone.phoneNumberFormat();
+ school["privateSchool"] = faker.datatype.boolean();
+ school["timing"] = faker.random.arrayElement([
$localize`:School demo timing:6 a.m. - 11 a.m.`,
$localize`:School demo timing:11 a.m. - 4 p.m.`,
$localize`:School demo timing:6:30-11:00 and 11:30-16:00`,
]);
- school.workingDays = faker.random.arrayElement([
- $localize`:School demo working days:Mon - Fri`,
- $localize`:School demo working days:Mon - Fri`,
- $localize`:School demo working days:Mon - Sat`,
- ]);
data.push(school);
}
diff --git a/src/app/child-dev-project/schools/model/school.spec.ts b/src/app/child-dev-project/schools/model/school.spec.ts
index f8c8ee235d..dc33704c88 100644
--- a/src/app/child-dev-project/schools/model/school.spec.ts
+++ b/src/app/child-dev-project/schools/model/school.spec.ts
@@ -52,16 +52,6 @@ describe("School Entity", () => {
_id: "School:" + id,
name: "Max",
- address: "Muster",
- medium: "English",
- remarks: "None",
- website: "www.google.com",
- privateSchool: true,
- phone: "911",
- upToClass: 10,
- academicBoard: "XY",
- timing: "9-5",
- workingDays: "Mon-Fri",
searchIndices: [],
};
@@ -69,16 +59,6 @@ describe("School Entity", () => {
const entity = new School(id);
entity.name = expectedData.name;
- entity.address = expectedData.address;
- entity.medium = expectedData.medium;
- entity.remarks = expectedData.remarks;
- entity.website = expectedData.website;
- entity.privateSchool = expectedData.privateSchool;
- entity.phone = expectedData.phone;
- entity.upToClass = expectedData.upToClass;
- entity.academicBoard = expectedData.academicBoard;
- entity.timing = expectedData.timing;
- entity.workingDays = expectedData.workingDays;
const rawData = entitySchemaService.transformEntityToDatabaseFormat(entity);
diff --git a/src/app/child-dev-project/schools/model/school.ts b/src/app/child-dev-project/schools/model/school.ts
index 140cdf7244..14c985f6b8 100644
--- a/src/app/child-dev-project/schools/model/school.ts
+++ b/src/app/child-dev-project/schools/model/school.ts
@@ -13,45 +13,6 @@ export class School extends Entity {
required: true,
})
name: string = "";
- @DatabaseField({
- label: $localize`:Label for the address of a school:Address`,
- })
- address: string = "";
- @DatabaseField({ label: $localize`:Label for the medium of a school:Medium` })
- medium: string = "";
- @DatabaseField({
- label: $localize`:Label for the remarks of a school:Remarks`,
- })
- remarks: string = "";
- @DatabaseField({
- label: $localize`:Label for the website of a school:Website`,
- })
- website: string = "";
- @DatabaseField({
- label: $localize`:Label whether school is private:Private School`,
- })
- privateSchool: boolean;
- @DatabaseField({
- label: $localize`:Label for the contact number of a school:Contact Number`,
- })
- phone: string = "";
- @DatabaseField({
- label: $localize`:Label up to which class a school is teaching:Teaching up to class`,
- })
- upToClass: number;
- @DatabaseField({
- label: $localize`:Label for the academic board of a school:Board`,
- })
- academicBoard: string = "";
- @DatabaseField({
- label: $localize`:Label for the times of a school:School Timing`,
- })
- timing: string = "";
- @DatabaseField({
- label: $localize`:Label for the working days of a school:Working Days`,
- editComponent: "EditLongText",
- })
- workingDays: string = "";
public toString() {
return this.name;
diff --git a/src/app/child-dev-project/schools/school-block/school-block.component.html b/src/app/child-dev-project/schools/school-block/school-block.component.html
index 6215759680..17e267adfb 100644
--- a/src/app/child-dev-project/schools/school-block/school-block.component.html
+++ b/src/app/child-dev-project/schools/school-block/school-block.component.html
@@ -9,6 +9,6 @@
(mouseenter)="showTooltip()"
(mouseleave)="hideTooltip()"
>
-
i18n
mat-stroked-button
class="feedback-button"
+ target="_blank"
+ rel="noopener"
href="mailto:info@aam-digital.com?subject=Feature-Request%20{{
featureId
}}"
diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts
index 08d3b3d98d..3576c81813 100644
--- a/src/app/core/config/config-fix.ts
+++ b/src/app/core/config/config-fix.ts
@@ -299,15 +299,10 @@ export const defaultJsonConfig = {
"title": $localize`:Title of schools overview:Schools List`,
"columns": [
"name",
- "medium",
"privateSchool",
- "academicBoard",
- "upToClass"
+ "language"
],
"filters": [
- {
- "id": "medium"
- },
{
"id": "privateSchool",
"true": $localize`:Label for private schools filter - true case:Private School`,
@@ -330,17 +325,21 @@ export const defaultJsonConfig = {
"component": "Form",
"config": {
"cols": [
- ["name"],
- ["medium"],
- ["privateSchool"],
- ["academicBoard"],
- ["phone"],
- ["address"],
- ["website"],
- ["timing"],
- ["workingDays"],
- ["upToClass"],
- ["remarks"]
+ [
+ "name",
+ "privateSchool"
+ ],
+ [
+ "address",
+ "phone"
+ ],
+ [
+ "language",
+ "timing",
+ ],
+ [
+ "remarks"
+ ]
]
}
}
@@ -457,12 +456,7 @@ export const defaultJsonConfig = {
"name",
"center",
"status",
- "admissionDate",
- "has_aadhar",
- "has_kanyashree",
- "has_bankAccount",
- "has_rationCard",
- "has_BplCard"
+ "admissionDate"
]
},
{
@@ -474,7 +468,6 @@ export const defaultJsonConfig = {
"health_BMI",
"health_bloodGroup",
"health_lastDentalCheckup",
- "health_lastDeworming",
"gender",
"age",
"dateOfBirth"
@@ -531,29 +524,18 @@ export const defaultJsonConfig = {
[
"name",
"projectNumber",
- "center",
- "status"
+ "admissionDate",
],
[
"dateOfBirth",
"gender",
- "motherTongue",
- "religion"
- ],
- [
- "admissionDate",
- "has_aadhar",
- "has_kanyashree",
- "has_bankAccount",
- "has_rationCard",
- "has_BplCard"
+ "motherTongue"
],
[
+ "center",
+ "status",
"address",
- "phone",
- "guardianName",
- "preferredTimeForGuardianMeeting"
- ]
+ ],
]
}
}
@@ -609,8 +591,7 @@ export const defaultJsonConfig = {
"config": {
"cols": [
["health_bloodGroup"],
- ["health_lastDentalCheckup"],
- ["health_lastDeworming"]
+ ["health_lastDentalCheckup"]
]
}
},
@@ -812,7 +793,7 @@ export const defaultJsonConfig = {
"aggregations": [
{
"query": `:getParticipantsWithAttendance(PRESENT):unique:addPrefix(${Child.ENTITY_TYPE}):toEntities`,
- "groupBy": ["gender", "religion"],
+ "groupBy": ["gender"],
"label": $localize`:Label for a report query:Participants`
}
]
@@ -852,92 +833,90 @@ export const defaultJsonConfig = {
}
},
{
- "name": "phone",
+ "name": "health_bloodGroup",
"schema": {
dataType: "string",
- label: $localize`:Label for phone number of a child:Phone No.`
+ label: $localize`:Label for a child attribute:Blood Group`
}
},
{
- "name": "guardianName",
+ "name": "religion",
"schema": {
dataType: "string",
- label: $localize`:Label for the guardians of a child:Guardians`
+ label: $localize`:Label for the religion of a child:Religion`
}
},
{
- "name": "preferredTimeForGuardianMeeting",
+ "name": "motherTongue",
"schema": {
dataType: "string",
- label: $localize`:Label for a child attribute:Preferred time for guardians meeting` }
+ label: $localize`:Label for the mother tongue of a child:Mother Tongue`
+ }
},
{
- "name": "has_aadhar",
+ "name": "health_lastDentalCheckup",
"schema": {
- dataType: "configurable-enum",
- innerDataType: "document-status",
- label: $localize`:Label for a child attribute:Aadhar`
+ dataType: "Date",
+ label: $localize`:Label for a child attribute:Last Dental Check-Up`
}
},
+ ]
+ },
+ "entity:School": {
+ "permissions": {
+ },
+ "attributes": [
{
- "name": "has_bankAccount",
+ "name": "name",
"schema": {
- dataType: "configurable-enum",
- innerDataType: "document-status",
- label: $localize`:Label for a child attribute:Bank Account`
+ dataType: "string",
+ label: $localize`:Label for the name of a school:Name`
}
},
{
- "name": "has_kanyashree",
+ "name": "privateSchool",
"schema": {
- dataType: "configurable-enum",
- innerDataType: "document-status",
- label: $localize`:Label for a child attribute:Kanyashree`
+ dataType: "boolean",
+ label: $localize`:Label for if a school is a private school:Private School`
}
},
{
- "name": "has_rationCard",
+ "name": "language",
"schema": {
- dataType: "configurable-enum",
- innerDataType: "document-status",
- label: $localize`:Label for a child attribute:Ration Card`
+ dataType: "string",
+ label: $localize`:Label for the language of a school:Language`
}
},
{
- "name": "has_BplCard",
+ "name": "address",
"schema": {
- dataType: "configurable-enum",
- innerDataType: "document-status",
- label: $localize`:Label for a child attribute:BPL Card`
+ dataType: "string",
+ label: $localize`:Label for the address of a school:Address`
}
},
{
- "name": "health_bloodGroup",
+ "name": "phone",
"schema": {
dataType: "string",
- label: $localize`:Label for a child attribute:Blood Group`
+ label: $localize`:Label for the phone number of a school:Phone Number`
}
},
{
- "name": "health_lastDentalCheckup",
+ "name": "timing",
"schema": {
- dataType: "Date",
- label: $localize`:Label for a child attribute:Last Dental Check-Up`
+ dataType: "string",
+ label: $localize`:Label for the timing of a school:School Timing`
}
},
{
- "name": "health_lastDeworming",
+ "name": "remarks",
"schema": {
- dataType: "Date",
- label: $localize`:Label for a child attribute:Last De-Worming`
+ dataType: "string",
+ label: $localize`:Label for the remarks for a school:Remarks`
}
}
]
},
- "entity:School": {
- "permissions": {
- }
- },
"entity:HistoricalEntityData": {
"attributes": [
{
diff --git a/src/app/core/entity-components/entity-list/filter-generator.service.spec.ts b/src/app/core/entity-components/entity-list/filter-generator.service.spec.ts
index 8bb9111864..17b3ef0a76 100644
--- a/src/app/core/entity-components/entity-list/filter-generator.service.spec.ts
+++ b/src/app/core/entity-components/entity-list/filter-generator.service.spec.ts
@@ -11,23 +11,30 @@ import { defaultInteractionTypes } from "../../config/default-config/default-int
import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation";
import { Child } from "../../../child-dev-project/children/model/child";
import moment from "moment";
+import { EntityConfigService } from "app/core/entity/entity-config.service";
describe("FilterGeneratorService", () => {
let service: FilterGeneratorService;
- let mockConfigService: jasmine.SpyObj;
let mockEntityMapper: jasmine.SpyObj;
- beforeEach(() => {
- mockConfigService = jasmine.createSpyObj(["getConfig"]);
- mockEntityMapper = jasmine.createSpyObj(["loadType"]);
+ beforeEach(async () => {
+ mockEntityMapper = jasmine.createSpyObj(["loadType", "load"]);
+ mockEntityMapper.load.and.rejectWith();
TestBed.configureTestingModule({
providers: [
- { provide: ConfigService, useValue: mockConfigService },
+ ConfigService,
{ provide: EntityMapperService, useValue: mockEntityMapper },
LoggingService,
+ EntityConfigService,
],
});
service = TestBed.inject(FilterGeneratorService);
+ const configService = TestBed.inject(ConfigService);
+ const entityConfigService = TestBed.inject(EntityConfigService);
+ const entityMapper = TestBed.inject(EntityMapperService);
+ await configService.loadConfig(entityMapper);
+ entityConfigService.addConfigAttributes(School);
+ entityConfigService.addConfigAttributes(Child);
});
it("should be created", () => {
@@ -59,7 +66,8 @@ describe("FilterGeneratorService", () => {
});
it("should create a configurable enum filter", async () => {
- mockConfigService.getConfig.and.returnValue(defaultInteractionTypes);
+ const getConfigSpy = spyOn(TestBed.inject(ConfigService), "getConfig");
+ getConfigSpy.and.returnValue(defaultInteractionTypes);
const interactionTypes = defaultInteractionTypes.map((it) => {
return { key: it.id, label: it.label };
});
@@ -120,11 +128,11 @@ describe("FilterGeneratorService", () => {
it("should create filters with all possible options on default", async () => {
const child1 = new Child();
- child1.religion = "muslim";
+ child1["religion"] = "muslim";
const child2 = new Child();
- child2.religion = "christian";
+ child2["religion"] = "christian";
const child3 = new Child();
- child3.religion = "muslim";
+ child3["religion"] = "muslim";
const schema = Child.schema.get("religion");
const filter = (
diff --git a/src/app/features/reporting/query.service.spec.ts b/src/app/features/reporting/query.service.spec.ts
index 5aa89d4d09..e489dbe1d1 100644
--- a/src/app/features/reporting/query.service.spec.ts
+++ b/src/app/features/reporting/query.service.spec.ts
@@ -20,6 +20,8 @@ import { Database } from "../../core/database/database";
import { ConfigurableEnumModule } from "../../core/configurable-enum/configurable-enum.module";
import { Note } from "../../child-dev-project/notes/model/note";
import { genders } from "../../child-dev-project/children/model/genders";
+import { EntityConfigService } from "app/core/entity/entity-config.service";
+import { ConfigService } from "app/core/config/config.service";
describe("QueryService", () => {
let service: QueryService;
@@ -47,7 +49,7 @@ describe("QueryService", () => {
let todayEventWithoutSchool: EventNote;
let twoDaysAgoEventWithoutRelation: EventNote;
- beforeEach(() => {
+ beforeEach(async () => {
database = PouchDatabase.createWithInMemoryDB();
TestBed.configureTestingModule({
imports: [ConfigurableEnumModule],
@@ -57,32 +59,40 @@ describe("QueryService", () => {
ChildrenService,
AttendanceService,
DatabaseIndexingService,
+ ConfigService,
+ EntityConfigService,
{ provide: Database, useValue: database },
],
});
service = TestBed.inject(QueryService);
+ const configService = TestBed.inject(ConfigService);
+ const entityConfigService = TestBed.inject(EntityConfigService);
+ const entityMapper = TestBed.inject(EntityMapperService);
+ await configService.loadConfig(entityMapper);
+ entityConfigService.addConfigAttributes(School);
+ entityConfigService.addConfigAttributes(Child);
});
beforeEach(async () => {
const entityMapper = TestBed.inject(EntityMapperService);
maleChristianChild = new Child("maleChristianChild");
maleChristianChild.gender = genders[1];
- maleChristianChild.religion = "christian";
+ maleChristianChild["religion"] = "christian";
await entityMapper.save(maleChristianChild);
femaleChristianChild = new Child("femaleChristianChild");
femaleChristianChild.gender = genders[2];
- femaleChristianChild.religion = "christian";
+ femaleChristianChild["religion"] = "christian";
await entityMapper.save(femaleChristianChild);
femaleMuslimChild = new Child("femaleMuslimChild");
femaleMuslimChild.gender = genders[2];
- femaleMuslimChild.religion = "muslim";
+ femaleMuslimChild["religion"] = "muslim";
await entityMapper.save(femaleMuslimChild);
maleChild = new Child("maleChild");
maleChild.gender = genders[1];
await entityMapper.save(maleChild);
privateSchool = new School("privateSchool");
- privateSchool.privateSchool = true;
+ privateSchool["privateSchool"] = true;
await entityMapper.save(privateSchool);
normalSchool = new School("normalSchool");
await entityMapper.save(normalSchool);
diff --git a/src/app/features/reporting/reporting.service.spec.ts b/src/app/features/reporting/reporting.service.spec.ts
index a3e51e7f24..54dddebb71 100644
--- a/src/app/features/reporting/reporting.service.spec.ts
+++ b/src/app/features/reporting/reporting.service.spec.ts
@@ -275,19 +275,19 @@ describe("ReportingService", () => {
const barabazar = centersUnique.find((c) => c.id === "barabazar");
const maleChristianAlipore = new Child();
maleChristianAlipore.gender = genders[1];
- maleChristianAlipore.religion = "christian";
+ maleChristianAlipore["religion"] = "christian";
maleChristianAlipore.center = alipore;
const maleMuslimAlipore = new Child();
maleMuslimAlipore.gender = genders[1];
- maleMuslimAlipore.religion = "muslim";
+ maleMuslimAlipore["religion"] = "muslim";
maleMuslimAlipore.center = alipore;
const femaleChristianBarabazar = new Child();
femaleChristianBarabazar.gender = genders[2];
- femaleChristianBarabazar.religion = "christian";
+ femaleChristianBarabazar["religion"] = "christian";
femaleChristianBarabazar.center = barabazar;
const femaleChristianAlipore = new Child();
femaleChristianAlipore.gender = genders[2];
- femaleChristianAlipore.religion = "christian";
+ femaleChristianAlipore["religion"] = "christian";
femaleChristianAlipore.center = alipore;
mockQueryService.queryData.and.resolveTo([
femaleChristianAlipore,
@@ -516,13 +516,13 @@ describe("ReportingService", () => {
it("should allow multiple groupBy's", async () => {
const femaleMuslim = new Child();
femaleMuslim.gender = genders[2];
- femaleMuslim.religion = "muslim";
+ femaleMuslim["religion"] = "muslim";
const femaleChristian = new Child();
femaleChristian.gender = genders[2];
- femaleChristian.religion = "christian";
+ femaleChristian["religion"] = "christian";
const maleMuslim = new Child();
maleMuslim.gender = genders[1];
- maleMuslim.religion = "muslim";
+ maleMuslim["religion"] = "muslim";
mockQueryService.queryData.and.resolveTo([
femaleChristian,
femaleMuslim,
diff --git a/src/app/features/reporting/reporting/reporting.component.spec.ts b/src/app/features/reporting/reporting/reporting.component.spec.ts
index 1bca8dedf5..ad5c404507 100644
--- a/src/app/features/reporting/reporting/reporting.component.spec.ts
+++ b/src/app/features/reporting/reporting/reporting.component.spec.ts
@@ -141,7 +141,7 @@ describe("ReportingComponent", () => {
{
header: {
label: "Total # of schools",
- groupedBy: [{ property: "medium", value: "" }],
+ groupedBy: [{ property: "language", value: "" }],
result: 2,
},
subRows: [],
@@ -149,7 +149,7 @@ describe("ReportingComponent", () => {
{
header: {
label: "Total # of schools",
- groupedBy: [{ property: "medium", value: "Hindi" }],
+ groupedBy: [{ property: "language", value: "Hindi" }],
result: 1,
},
subRows: [],
@@ -187,7 +187,7 @@ describe("ReportingComponent", () => {
{ label: `Total # of events (${coachingClass.label})`, result: 1 },
{ label: `Total # of events (${schoolClass.label})`, result: 2 },
{ label: "Total # of schools", result: 3 },
- { label: `Total # of schools (without medium)`, result: 2 },
+ { label: `Total # of schools (without language)`, result: 2 },
{ label: `Total # of schools (Hindi)`, result: 1 },
{ label: "Total # of schools", result: 2 },
{ label: `Total # of schools (privateSchool)`, result: 1 },
diff --git a/src/assets/help/help.de.md b/src/assets/help/help.de.md
index bb56562b20..1aa590c6b5 100644
--- a/src/assets/help/help.de.md
+++ b/src/assets/help/help.de.md
@@ -5,4 +5,4 @@ Haben Sie Fragen oder technische Probleme? Kontaktieren Sie uns:
- [WhatsApp](https://wa.me/491776181407)
- [Telegram](https://telegram.me/SebastianLeidig)
-_Wir freuen uns von Ihnen zu hören und zu helfen!_
+_Wir freuen uns darauf, von Ihnen zu hören und Ihnen zu helfen!_
diff --git a/src/locale/messages.de.xlf b/src/locale/messages.de.xlf
index 12ac9bf22f..9a388345ef 100644
--- a/src/locale/messages.de.xlf
+++ b/src/locale/messages.de.xlf
@@ -15,7 +15,7 @@
- Auch unzugehörige anzeigen
+ Auch Aktivitäten ohne diese:n Schüler:in anzeigen show unrelated attendance-entries for an activity that are not
linked to the child of interestslider
@@ -55,7 +55,7 @@
- Widerkehrende Aktivität - Zeichne einen Tag auf
+ Widerkehrende Aktivität - Nehme Anwesenheit für einen Tag aufInforms the user that this is a recurring eventRecurring-event
@@ -82,8 +82,8 @@
-
- Anwesenheiten aufzeichnen:
+
+ Anwesenheiten aufnehmen: src/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.html8,11
@@ -142,7 +142,7 @@
- Wähle Event aus
+ Wähle ein Event aussrc/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.ts19,18
@@ -151,7 +151,7 @@
- Zeichne Event auf für den
+ Nimm ein Event auf für denRecord an event for a particular date that is to be
inputtedEvent-Record label
@@ -182,7 +182,7 @@
- Zeichne Anwesenheit auf
+ Anwesenheiten aufnehmen Start recording the attendance of a child at
an eventRecord Attendance button
@@ -440,7 +440,7 @@
- Anwesenheit aufzeichnen
+ Anwesenheiten aufnehmensrc/app/child-dev-project/attendance/attendance-manager/attendance-manager.component.html22,23
@@ -449,7 +449,7 @@
- Tages-Anwesenheit aufzeichnen
+ Tages-Anwesenheit aufnehmensrc/app/child-dev-project/attendance/attendance-manager/attendance-manager.component.html26,27
@@ -458,7 +458,7 @@
- Auswählen einer Gruppe von Kindern, um die Anwesenheit einzeln aufzuzeichnen.
+ Eine Gruppe von Schüler:innen auswählen, um die Anwesenheit einzeln aufzunehmen.Record attendance contentsrc/app/child-dev-project/attendance/attendance-manager/attendance-manager.component.html
@@ -467,7 +467,7 @@
- Anwesenheit für eine Event aufzeichnen
+ Anwesenheit für eine Event aufnehmen Record attendance buttonsrc/app/child-dev-project/attendance/attendance-manager/attendance-manager.component.html
@@ -476,7 +476,7 @@
- Monatliche Anwesenheit aufzeichnen
+ Monatliche Anwesenheit aufnehmensrc/app/child-dev-project/attendance/attendance-manager/attendance-manager.component.html49,50
@@ -512,7 +512,7 @@
- Analysen und Vergleiche von Anwesenheiten für alle Kinder. Die Detailseite der Kinder gibt Auskunft über die Anwesenheit des jeweiligen Kindes.
+ Analysen und Vergleiche von Anwesenheiten für alle Schüler:innen. Die Detailseite der Schüler:innen gibt Auskunft über die Anwesenheit des/der jeweiligen Schülerin/Schülers. Analyze attendance contentsrc/app/child-dev-project/attendance/attendance-manager/attendance-manager.component.html
@@ -660,7 +660,7 @@
- Kopie (Einzellinie, klein)
+ Heft (Einzellinien, klein)src/app/child-dev-project/educational-material/model/materials.ts50
@@ -669,7 +669,7 @@
- Kopie (Einzellinie, groß)
+ Heft (Einzellinie, groß)src/app/child-dev-project/educational-material/model/materials.ts54
@@ -678,7 +678,7 @@
- Kopie (vier linien)
+ Heft (vier linien)src/app/child-dev-project/educational-material/model/materials.ts58
@@ -687,7 +687,7 @@
- Kopie (quadratisch)
+ Heft (quadratisch)src/app/child-dev-project/educational-material/model/materials.ts62
@@ -696,7 +696,7 @@
- Kopie (blanko)
+ Heft (blanko)src/app/child-dev-project/educational-material/model/materials.ts66
@@ -705,7 +705,7 @@
- Kopie (liniert)
+ Heft (liniert)src/app/child-dev-project/educational-material/model/materials.ts70
@@ -714,7 +714,7 @@
- Kopie (Zeichnung)
+ Heft (Zeichnen)src/app/child-dev-project/educational-material/model/materials.ts74
@@ -723,7 +723,7 @@
- Kopie (praktisch)
+ Heft (praktisch)src/app/child-dev-project/educational-material/model/materials.ts78
@@ -750,7 +750,7 @@
- Projekt-Unterlagen
+ Projektunterlagensrc/app/child-dev-project/educational-material/model/materials.ts90
@@ -759,7 +759,7 @@
- Sammelalbum
+ Bastelbuchsrc/app/child-dev-project/educational-material/model/materials.ts94
@@ -768,7 +768,7 @@
- Prüfungstafel
+ Prüfungsbögensrc/app/child-dev-project/educational-material/model/materials.ts98
@@ -786,7 +786,7 @@
- Schul-Uniform
+ Schuluniformsrc/app/child-dev-project/educational-material/model/materials.ts107
@@ -795,7 +795,7 @@
- Schul-Schuhe
+ Schulschuhesrc/app/child-dev-project/educational-material/model/materials.ts112
@@ -804,7 +804,7 @@
- Sport-Kleid
+ Sportkleidungsrc/app/child-dev-project/educational-material/model/materials.ts117
@@ -813,7 +813,7 @@
- Sport-Schuhe
+ Sportschuhesrc/app/child-dev-project/educational-material/model/materials.ts122
@@ -822,7 +822,7 @@
- Regenmantel
+ Regenjackesrc/app/child-dev-project/educational-material/model/materials.ts127
@@ -849,7 +849,7 @@
- Kinder ohne neuen Eintrag
+ Schüler:innen ohne neuen Eintrag Subtitle informing the user that these are the children without
recent reportsSubtitle
@@ -887,7 +887,7 @@
- Kinder mit neuem Eintrag
+ Schüler:innen mit neuem Eintrag Subtitle informing the user that these are the children with
recent reportsSubtitle
@@ -924,7 +924,7 @@
Our regular monthly meeting. Find the agenda and minutes in our meeting folder.
- Monatliches Treffen. Die Agenda und das Protokoll können in unserem Treffen Ordner gefunden werden.
+ Monatliches Treffen. Die Agenda und das Protokoll befinden sich in unserem "Treffen"-Ordner.
src/app/child-dev-project/notes/demo-data/notes_group-stories.ts
@@ -946,7 +946,7 @@
- Treffen mit Kindern
+ Treffen mit Schüler:innensrc/app/child-dev-project/notes/demo-data/notes_group-stories.ts24
@@ -959,7 +959,7 @@
- Drogenprevention
+ Drogenpräventionstrainingsrc/app/child-dev-project/notes/demo-data/notes_group-stories.ts40
@@ -971,7 +971,7 @@
Expert conducted a two day workshop on drug prevention.
- Ein Experte führte einen zwei-Tages Drogenpreventionstraining durch.
+ Es wurde ein zweitägiges Drogenpräventionstraining durchgeführt.
src/app/child-dev-project/notes/demo-data/notes_group-stories.ts
@@ -994,8 +994,8 @@
Children are taking care of housework. Told her to see doctor. We should follow up next week.
- Habe die Familie besucht nachdem wir erfahren hatten, dass die Mutter ernsthaft erkankt ist. Sie kann nicht ausfstehen.
- Die Kinder übernehmen die Hausarbeiten. Habe ihr gesagt, sie soll den Arzt besuchen. Wir sollten nächste Woche nachfragen.
+ Habe die Familie besucht, nachdem wir erfahren hatten, dass die Mutter ernsthaft erkankt ist. Sie kann nicht ausfstehen.
+ Die Kinder übernehmen die Hausarbeiten. Habe ihr gesagt, dass sie zu einem Arzt gehen solle. Wir sollten nächste Woche nachfragen.
src/app/child-dev-project/notes/demo-data/notes_individual-stories.ts
@@ -1029,7 +1029,7 @@
- Nachfragen Schulabwesenheit
+ Nachfragen zur Schulabwesenheitsrc/app/child-dev-project/notes/demo-data/notes_individual-stories.ts26
@@ -1041,7 +1041,7 @@
Called to ask for reason about absence. Mother made excuses but promised to send the child tomorrow.
- Habe Mutter angerufen, wieso Kind abwesend war. Mutter hatte Entschuldigungen, versprach aber das Kind morgen in die Schule zu schicken.
+ Habe die Mutter angerufen und gefragt, wieso das Kind nicht in der Schule war. Die Mutter hatte nannte Gründe hierfür, versprach aber, das Kind morgen wieder in die Schule zu schicken.
src/app/child-dev-project/notes/demo-data/notes_individual-stories.ts
@@ -1097,7 +1097,7 @@
- Schule ist glücklich mit Fortschritt
+ Schule ist zufrieden mit Fortschrittsrc/app/child-dev-project/notes/demo-data/notes_individual-stories.ts51
@@ -1121,7 +1121,7 @@
- Muss sich mehr für die Schule bemühen
+ Muss sich in der Schule mehr anstrengensrc/app/child-dev-project/notes/demo-data/notes_individual-stories.ts60
@@ -1134,8 +1134,8 @@
We should consider arranging an extra class for him. Discuss next social worker meeting.
- Besprach den Fortschritt des Kindes mit dem Lehrer. Das Kind gehört weiterhin zu den schwächeren Schüler:innen und benötigt Untersützung.
- Vielleicht benötigt das Kind Nachhilfe. Wird im nächsten Sozialarbeitertreffen besprochen.
+ Ich habe mit dem Lehrer über die Entwicklung des Kindes gesprochen. Es gehört weiterhin zu den schwächeren Schüler:innen und benötigt Untersützung,
+ vielleicht auch Nachhilfe. Das werden wir beim nächsten Sozialarbeiter:innen-Treffen besprechen.
src/app/child-dev-project/notes/demo-data/notes_individual-stories.ts
@@ -1158,8 +1158,8 @@
Need to follow up with the child and discuss the matter.
- Rektor hat heute angerufen. Eines unserer Kinder war in einer Schlägerei verwickelt und ist für nächste Woche von der Schule ausgeschlossen.
- Sollte mit dem Kind besprochen werden.
+ Der Rektor hat heute angerufen. Das Kind war in eine Schlägerei verwickelt und ist für nächste Woche von der Schule ausgeschlossen.
+ Darüber sollte mit dem Kind gesprochen werden.
src/app/child-dev-project/notes/demo-data/notes_individual-stories.ts
@@ -1182,8 +1182,8 @@
After home visits and discussion in our team we decided to refer them to a special support programme.
- Seit der Vater seine Arbeit verloren hatte hat die Familie große Schwierigkeiten zu überleben.
- Nach Hausbesuchen und Diskussionen mit dem Team haben wir uns dazu entschieden die Familie an das spezielle Unterstützungsprogramm zu verweisen.
+ Seitdem der Vater arbeitslos geworden ist, hat die Familie große finanzielle Schwierigkeiten.
+ Nach Hausbesuchen und Diskussionen mit dem Team haben wir uns dazu entschieden, die Familie an das spezielle Unterstützungsprogramm zu verweisen.
src/app/child-dev-project/notes/demo-data/notes_individual-stories.ts
@@ -1193,7 +1193,7 @@
- Möglichkeit sitzenzubleiben
+ Versetzungsgefährdetsrc/app/child-dev-project/notes/demo-data/notes_individual-stories.ts87
@@ -1207,9 +1207,9 @@
and she promised to attend school regularly.
- Das Kind ist dieses Jahr in der Schule durchgefallen da es oft fehlte.
+ Das Kind ist dieses Jahr in der Schule durchgefallen, da es oft fehlte.
Nach langen Diskussionen mit dem Kind und den Eltern wurde beschlossen, dass das Kind die Klasse wiederholt.
- Es verspricht die Schule nun regelmäßig zu besuchen.
+ Es hat versprochen, nun regelmäßig zur Schule zu gehen.
src/app/child-dev-project/notes/demo-data/notes_individual-stories.ts
@@ -1232,8 +1232,8 @@
Discussed with him - there are a lot of problems in the family currently.
- Die Lehrerin hat uns benachrichtigt, dass das Kind die letzten Tage sehr unaufmerksam im Unterricht ist.
- Habe mit dem Kind darüber gesprochen - es hat zurzeit sehr viele Probleme in der Familie.
+ Die Lehrerin hat uns Bescheid gegeben, dass das Kind in den letzten Tagen im Unterricht sehr unaufmerksam war.
+ Habe mit dem Kind darüber gesprochen: Es gibt derzeit sehr viele Probleme in der Familie.
src/app/child-dev-project/notes/demo-data/notes_individual-stories.ts
@@ -1256,8 +1256,8 @@
Did counselling session with her.
- Das Kind ignorierte die Anweisungen des Lehrers und störte im Unterricht.
- Habe mich mit dem Kind über dieses Problem unterhalten.
+ Das Kind hat nicht auf die Lehrerin gehört und im Unterricht gestört.
+ Ich habe mit dem Kind über dieses Problem gesprochen.
src/app/child-dev-project/notes/demo-data/notes_individual-stories.ts
@@ -1276,7 +1276,7 @@
- Abwesend ohne Entschuldigung
+ Unentschuldigt Abwesendsrc/app/child-dev-project/notes/demo-data/remarks.ts3
@@ -1294,7 +1294,7 @@
- Kinder
+ Schüler:innenLabel for the children of a notesrc/app/child-dev-project/notes/model/note.ts
@@ -1324,7 +1324,7 @@
- beinhaltet Kinder ohne Notiz
+ beinhaltet Schüler:innen ohne NotizSpaces in front of the variables are added automaticallyTooltip
@@ -1480,14 +1480,12 @@
4,6
-
-
- Besucht derzeit Klasse an
+
+
+ Besucht derzeit die Klasse der Schule src/app/child-dev-project/previous-schools/previous-schools.component.html
- 9,18
+ 9,16Context 'currently
attending class at school'
@@ -1726,7 +1724,7 @@
- Kinder zu neuem Format migrieren
+ Schüler:innen zu neuem Format migrieren Data migration for photossrc/app/core/admin/admin/admin.component.html
@@ -1998,7 +1996,7 @@
Das ist mein Anwendungsfall src/app/core/coming-soon/coming-soon/coming-soon.component.html
- 46
+ 47
@@ -2038,14 +2036,12 @@
src/app/core/config/config-fix.ts
- 652
+ 623
- Anwesenheit Aufzeichnen
- Record attendance menu item
- Menu item
+ Anwesenheiten aufnehmensrc/app/core/config/config-fix.ts43
@@ -2053,7 +2049,7 @@
- Anwesenheit Verwalten
+ Anwesenheiten verwaltenMenu itemsrc/app/core/config/config-fix.ts
@@ -2161,9 +2157,7 @@
- Anwesenheit Aufzeichnen
- record attendance shortcut
- Dashboard shortcut widget
+ Anwesenheiten aufnehmensrc/app/core/config/config-fix.ts148
@@ -2315,7 +2309,7 @@
src/app/core/config/config-fix.ts
- 570
+ 542
@@ -2345,17 +2339,16 @@
src/app/core/config/config-fix.ts
- 400
+ 389src/app/core/config/config-fix.ts
- 459
+ 442assets/help/help.de.md
- Filename of markdown help page (make sure the filename you enter as a translation actually exists on the server!)src/app/core/config/config-fix.ts262
@@ -2363,7 +2356,7 @@
- Schulen Ãœbersicht
+ Liste der SchulenTitle of schools overviewsrc/app/core/config/config-fix.ts
@@ -2376,7 +2369,7 @@
Label for private schools filter - false casesrc/app/core/config/config-fix.ts
- 289
+ 283
@@ -2385,29 +2378,29 @@
Panel titlesrc/app/core/config/config-fix.ts
- 301
+ 295src/app/core/config/config-fix.ts
- 498
+ 481
- Schüler
+ Schüler:innenPanel titlesrc/app/core/config/config-fix.ts
- 325
+ 323
- Kinder Ãœbersicht
+ Liste der Schüler:innenTitle children overviewsrc/app/core/config/config-fix.ts
- 349
+ 338
@@ -2416,7 +2409,7 @@
Column label for school attendance of childsrc/app/core/config/config-fix.ts
- 375
+ 364
@@ -2425,7 +2418,7 @@
Column label for coaching attendance of childsrc/app/core/config/config-fix.ts
- 384
+ 373
@@ -2434,11 +2427,11 @@
Translated name of default column groupsrc/app/core/config/config-fix.ts
- 399
+ 388src/app/core/config/config-fix.ts
- 416
+ 405
@@ -2447,7 +2440,7 @@
Column group namesrc/app/core/config/config-fix.ts
- 403
+ 392
@@ -2456,11 +2449,11 @@
Column group namesrc/app/core/config/config-fix.ts
- 444
+ 428src/app/core/config/config-fix.ts
- 579
+ 551
@@ -2469,7 +2462,7 @@
Active children filter label - true casesrc/app/core/config/config-fix.ts
- 474
+ 457
@@ -2478,7 +2471,7 @@
Active children filter label - false casesrc/app/core/config/config-fix.ts
- 475
+ 458
@@ -2487,7 +2480,7 @@
Panel titlesrc/app/core/config/config-fix.ts
- 538
+ 510
@@ -2496,34 +2489,34 @@
Title inside a panelsrc/app/core/config/config-fix.ts
- 541
+ 513
- ASER Ergebnisse
+ Abschneiden beim "ASER"-TestTitle inside a panelsrc/app/core/config/config-fix.ts
- 555
+ 527
- Anwsenheit
+ AnwesenheitPanel titlesrc/app/core/config/config-fix.ts
- 561
+ 533
- Gewicht & Größe Messungen
+ Größe & Gewicht Title inside a panelsrc/app/core/config/config-fix.ts
- 593
+ 564
@@ -2532,21 +2525,21 @@
Panel titlesrc/app/core/config/config-fix.ts
- 599
+ 570
- Einschätzungen
+ BeurteilungenPanel titlesrc/app/core/config/config-fix.ts
- 608
+ 579
- Ausstieg
+ Ausscheiden aus dem ProjektChild statussrc/app/child-dev-project/children/demo-data-generators/demo-child-generator.service.ts
@@ -2558,7 +2551,7 @@
src/app/core/config/config-fix.ts
- 630
+ 601
@@ -2567,34 +2560,34 @@
Panel titlesrc/app/core/config/config-fix.ts
- 671
+ 637
- Events & Attendance
+ Aktivitäten & AnwesenheitPanel titlesrc/app/core/config/config-fix.ts
- 700
+ 666
- Basis Bericht
+ BasisberichtName of a reportsrc/app/core/config/config-fix.ts
- 716
+ 682
- Kinder
+ Alle Schüler:innenLabel of report querysrc/app/core/config/config-fix.ts
- 720
+ 686
@@ -2603,16 +2596,16 @@
Label of report querysrc/app/core/config/config-fix.ts
- 723
+ 689
- Weibliche Kinder
+ Weibliche SchülerinnenLabel of report querysrc/app/core/config/config-fix.ts
- 727
+ 693
@@ -2621,7 +2614,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 734
+ 700
@@ -2630,7 +2623,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 737
+ 703
@@ -2639,7 +2632,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 741
+ 707
@@ -2648,7 +2641,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 746
+ 712
@@ -2657,7 +2650,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 749
+ 715
@@ -2666,7 +2659,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 753
+ 719
@@ -2675,7 +2668,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 759
+ 725
@@ -2684,7 +2677,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 764
+ 730
@@ -2693,7 +2686,7 @@
Label for report querysrc/app/core/config/config-fix.ts
- 767
+ 733
@@ -2702,97 +2695,16 @@
Label for report querysrc/app/core/config/config-fix.ts
- 771
+ 737
- Event Bericht
+ EventberichtName of a reportsrc/app/core/config/config-fix.ts
- 781
-
-
-
-
- Bericht aller Aktivitäten
- Name of a report
-
- src/app/core/config/config-fix.ts
- 798
-
-
-
-
- Telefonnummer
- Label for phone number of a child
-
- src/app/core/config/config-fix.ts
- 833
-
-
-
-
- Vormünder
- Label for the guardians of a child
-
- src/app/core/config/config-fix.ts
- 840
-
-
-
-
- Bevorzugte Zeit für Treffen mit Vormündern
- Label for a child attribute
-
- src/app/core/config/config-fix.ts
- 847
-
-
-
-
- Aadhar
- Label for a child attribute
-
- src/app/core/config/config-fix.ts
- 854
-
-
-
-
- Bankkonto
- Label for a child attribute
-
- src/app/core/config/config-fix.ts
- 862
-
-
-
-
- Kanyashree
- Label for a child attribute
-
- src/app/core/config/config-fix.ts
- 870
-
-
-
-
- Rationskarte
- Label for a child attribute
-
- src/app/core/config/config-fix.ts
- 878
-
-
-
-
- BPL-Karte
- Label for a child attribute
-
- src/app/core/config/config-fix.ts
- 886
+ 747
@@ -2801,7 +2713,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 893
+ 782
@@ -2810,16 +2722,25 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 900
+ 803
-
-
- Letzte Entwurmung
- Label for a child attribute
+
+
+ Sprache
+ Label for the language of a schoolsrc/app/core/config/config-fix.ts
- 907
+ 830
+
+
+
+
+ Telefonnummer
+ Label for the phone number of a school
+
+ src/app/core/config/config-fix.ts
+ 844
@@ -2828,7 +2749,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 923
+ 870
@@ -2837,7 +2758,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 924
+ 871
@@ -2846,7 +2767,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 932
+ 879
@@ -2855,7 +2776,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 933
+ 880
@@ -2864,16 +2785,16 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 941
+ 888
- Das Kind interagiert mit andern Kindern im Unterricht.
+ Das Kind interagiert mit andern Schüler:innen im Unterricht.Description for a child attributesrc/app/core/config/config-fix.ts
- 942
+ 889
@@ -2882,7 +2803,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 950
+ 897
@@ -2891,7 +2812,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 951
+ 898
@@ -2900,7 +2821,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 959
+ 906
@@ -2909,7 +2830,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 960
+ 907
@@ -2918,7 +2839,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 968
+ 915
@@ -2927,7 +2848,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 969
+ 916
@@ -2936,7 +2857,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 977
+ 924
@@ -2945,7 +2866,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 978
+ 925
@@ -2954,7 +2875,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 986
+ 933
@@ -2963,7 +2884,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 987
+ 934
@@ -2972,7 +2893,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 995
+ 942
@@ -2981,7 +2902,7 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 996
+ 943
@@ -2990,7 +2911,7 @@
Label for a child attributesrc/app/core/config/config-fix.ts
- 1004
+ 951
@@ -2999,12 +2920,12 @@
Description for a child attributesrc/app/core/config/config-fix.ts
- 1005
+ 952
- zeichne Anwesenheit auf
+ Anwesenheiten aufnehmenOne of the stages while recording child-attendancessrc/app/child-dev-project/attendance/add-day-attendance/add-day-attendance.component.ts
@@ -3117,12 +3038,16 @@
src/app/core/config/config-fix.ts
- 353
+ 342
+
+
+ src/app/core/config/config-fix.ts
+ 816
- Projekt Nummer
+ Projektnummersrc/app/child-dev-project/children/model/child.ts44
@@ -3146,11 +3071,11 @@
src/app/core/config/config-fix.ts
- 290
+ 284src/app/core/config/config-fix.ts
- 476
+ 459src/app/core/entity-components/entity-list/filter-generator.service.ts
@@ -3168,15 +3093,15 @@
Vermerk
+ Label for the remarks of a ASER resultsrc/app/child-dev-project/aser/model/aser.ts74
- src/app/child-dev-project/schools/model/school.ts
- 23
+ src/app/core/config/config-fix.ts
+ 858
- Label for the remarks of a ASER result
@@ -3229,7 +3154,7 @@
- Liest Buchstaben
+ Kann Buchstaben lsensrc/app/child-dev-project/aser/model/readingLevels.ts14
@@ -3238,7 +3163,7 @@
- Liest Wörter
+ Kann Wörter lesensrc/app/child-dev-project/aser/model/readingLevels.ts18
@@ -3247,7 +3172,7 @@
- Liest Sätze
+ Kann Sätze lesensrc/app/child-dev-project/aser/model/readingLevels.ts22
@@ -3256,7 +3181,7 @@
- Liest Absätze
+ Kann Absätze lesensrc/app/child-dev-project/aser/model/readingLevels.ts26
@@ -3293,29 +3218,29 @@
Muttersprache
+ Label for the mother tongue of a child
- src/app/child-dev-project/children/model/child.ts
- 56
+ src/app/core/config/config-fix.ts
+ 796
- Label for the mother tongue of a childGeschlecht
+ Label for the gender of a childsrc/app/child-dev-project/children/model/child.ts
- 61
+ 58
- Label for the gender of a childReligion
+ Label for the religion of a child
- src/app/child-dev-project/children/model/child.ts
- 66
+ src/app/core/config/config-fix.ts
+ 789
- Label for the religion of a child
@@ -3327,7 +3252,7 @@
src/app/core/config/config-fix.ts
- 358
+ 347
@@ -3342,140 +3267,64 @@
6 - 11 Uhr
+ School demo timingsrc/app/child-dev-project/schools/demo-school-generator.service.ts
- 56
+ 50
- School demo timing11 - 16 Uhr
+ School demo timingsrc/app/child-dev-project/schools/demo-school-generator.service.ts
- 57
+ 51
- School demo timing6:30-11:00 und 11:30-16:00
-
- src/app/child-dev-project/schools/demo-school-generator.service.ts
- 58
- School demo timing
-
-
-
- Mo - Frsrc/app/child-dev-project/schools/demo-school-generator.service.ts
- 61
-
-
- src/app/child-dev-project/schools/demo-school-generator.service.ts
- 62
-
- School demo working days
-
-
-
- Mo - Sa
-
- src/app/child-dev-project/schools/demo-school-generator.service.ts
- 63
+ 52
- School demo working daysAdresse
- Label for the address of a school
-
- src/app/child-dev-project/schools/model/school.ts
- 17
-
+ Label for the address of a childsrc/app/core/config/config-fix.ts
- 826
-
-
-
-
- Medium
-
- src/app/child-dev-project/schools/model/school.ts
- 20
+ 775
- Label for the medium of a school
-
-
-
- Webseite
- src/app/child-dev-project/schools/model/school.ts
- 27
+ src/app/core/config/config-fix.ts
+ 837
- Label for the website of a school
- Private Schule
- Label whether school is private
-
- src/app/child-dev-project/schools/model/school.ts
- 31
-
+ Privatschule
+ Label for private schools filter - true casesrc/app/core/config/config-fix.ts
- 288
-
-
-
-
- Kontaktnummer
-
- src/app/child-dev-project/schools/model/school.ts
- 35
-
- Label for the contact number of a school
-
-
-
- Unterichtet bis zu Klasse
-
- src/app/child-dev-project/schools/model/school.ts
- 39
+ 282
- Label up to which class a school is teaching
-
-
-
- Vorstand
- src/app/child-dev-project/schools/model/school.ts
- 43
+ src/app/core/config/config-fix.ts
+ 823
- Label for the academic board of a school
- Schulzeiten
+ Unterrichtszeiten
+ Label for the timing of a school
- src/app/child-dev-project/schools/model/school.ts
- 47
-
- Label for the times of a school
-
-
-
- Arbeitstage
-
- src/app/child-dev-project/schools/model/school.ts
- 51
+ src/app/core/config/config-fix.ts
+ 851
- Label for the working days of a school
@@ -3553,7 +3402,7 @@
src/app/core/config/config-fix.ts
- 363
+ 352
@@ -3674,11 +3523,11 @@
src/app/core/config/config-fix.ts
- 368
+ 357src/app/core/config/config-fix.ts
- 485
+ 468
@@ -3691,7 +3540,7 @@
- Kinder, die mehr als einen Tag abwesend waren
+ Schüler:innen, die mehr als einen Tag abwesend warensrc/app/child-dev-project/attendance/dashboard-widgets/attendance-week-dashboard/attendance-week-dashboard.component.html8,11
@@ -3753,15 +3602,11 @@
src/app/core/config/config-fix.ts
- 686
-
-
- src/app/core/config/config-fix.ts
- 791
+ 652src/app/core/config/config-fix.ts
- 808
+ 757
@@ -3794,7 +3639,7 @@
- Kinder mit ungesundem BMI
+ Schüler:innen mit ungesundem BMI src/app/child-dev-project/children/children-bmi-dashboard/children-bmi-dashboard.component.html8,9
@@ -3803,7 +3648,7 @@
- Kinder
+ Schüler:innensrc/app/child-dev-project/children/children-count-dashboard/children-count-dashboard.component.html8,9
@@ -3812,70 +3657,70 @@
- Zentrum
+ Schulzentrum
+ Label for the center of a childsrc/app/child-dev-project/children/model/child.ts
- 73
+ 66
- Label for the center of a child
- Aufnahme
+ Aufnahmedatum
+ Label for the admission date of a childsrc/app/child-dev-project/children/model/child.ts
- 77
+ 70
- Label for the admission date of a child
- Status
+ Status im Projekt Label for the status of a childsrc/app/child-dev-project/children/model/child.ts
- 81
+ 74src/app/core/config/config-fix.ts
- 429
+ 418
- Ausstiegsdatum
+ Datum des Ausscheidens
+ Label for the dropout date of a childsrc/app/child-dev-project/children/model/child.ts
- 86
+ 79
- Label for the dropout date of a child
- Ausstiegstyp
+ Grund des Ausscheidens
+ Label for the type of dropout of a childsrc/app/child-dev-project/children/model/child.ts
- 90
+ 83
- Label for the type of dropout of a child
- Ausstiegsbemerkungen
+ Bermekungen zum Ausscheiden
+ Label for the remarks about a dropout of a childsrc/app/child-dev-project/children/model/child.ts
- 94
+ 87
- Label for the remarks about a dropout of a childFoto Dateiname
+ Label for the filename of a photo of a childsrc/app/child-dev-project/children/model/child.ts
- 106
+ 99
- Label for the filename of a photo of a child
@@ -3896,7 +3741,7 @@
src/app/core/config/config-fix.ts
- 393
+ 382
@@ -3914,7 +3759,7 @@
- Events
+ AktivitätenEvents of an attendancesrc/app/child-dev-project/attendance/activity-attendance-section/activity-attendance-section.component.ts
@@ -3922,11 +3767,7 @@
src/app/core/config/config-fix.ts
- 786
-
-
- src/app/core/config/config-fix.ts
- 803
+ 752
@@ -4079,7 +3920,7 @@
- Treffen der Kinder
+ Treffen mit den Schüler:innensrc/app/core/config/default-config/default-interaction-types.ts57
@@ -4320,7 +4161,7 @@
src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.ts
- 142
+ 138
@@ -4415,7 +4256,7 @@
- Zeige alle
+ Alle anzeigen src/app/core/entity-components/entity-subrecord/list-paginator/list-paginator.component.html7,8
@@ -4424,7 +4265,7 @@
- Dieses Feld kann nicht editiert werden. Änbdern Sie stattdessen das Geburtsdatum.
+ Dieses Feld kann nicht verändert werden. Ändern Sie stattdessen das Geburtsdatum.Tooltip for the disabled age fieldsrc/app/core/entity-components/entity-utils/dynamic-form-components/edit-age/edit-age.component.html
@@ -4729,7 +4570,7 @@
Ihr Passwort hat sich vor kurzem geändert. Bitte mit dem neuen Passwort versuchen!src/app/core/session/session-service/synced-session.service.ts
- 133
+ 147
diff --git a/src/locale/messages.xlf b/src/locale/messages.xlf
index e1d34b343e..8b24c22be9 100644
--- a/src/locale/messages.xlf
+++ b/src/locale/messages.xlf
@@ -65,8 +65,8 @@
74
- src/app/child-dev-project/schools/model/school.ts
- 23
+ src/app/core/config/config-fix.ts
+ 858Label for the remarks of a ASER result
@@ -203,11 +203,7 @@
src/app/core/config/config-fix.ts
- 786
-
-
- src/app/core/config/config-fix.ts
- 803
+ 752Events of an attendance
@@ -648,11 +644,11 @@
src/app/core/config/config-fix.ts
- 368
+ 357src/app/core/config/config-fix.ts
- 485
+ 468
@@ -718,15 +714,11 @@
src/app/core/config/config-fix.ts
- 686
-
-
- src/app/core/config/config-fix.ts
- 791
+ 652src/app/core/config/config-fix.ts
- 808
+ 757Label for the participants of a recurring activity
@@ -787,11 +779,11 @@
src/app/core/config/config-fix.ts
- 290
+ 284src/app/core/config/config-fix.ts
- 476
+ 459src/app/core/entity-components/entity-list/filter-generator.service.ts
@@ -818,7 +810,7 @@
src/app/core/config/config-fix.ts
- 630
+ 601Child status
@@ -942,7 +934,11 @@
src/app/core/config/config-fix.ts
- 353
+ 342
+
+
+ src/app/core/config/config-fix.ts
+ 816Label for the name of a child
@@ -978,35 +974,19 @@
Short label for the date of birth
-
-
-
- src/app/child-dev-project/children/model/child.ts
- 56
-
- Label for the mother tongue of a child
- src/app/child-dev-project/children/model/child.ts
- 61
+ 58Label for the gender of a child
-
-
-
- src/app/child-dev-project/children/model/child.ts
- 66
-
- Label for the religion of a child
- src/app/child-dev-project/children/model/child.ts
- 73
+ 66Label for the center of a child
@@ -1014,7 +994,7 @@
src/app/child-dev-project/children/model/child.ts
- 77
+ 70Label for the admission date of a child
@@ -1022,11 +1002,11 @@
src/app/child-dev-project/children/model/child.ts
- 81
+ 74src/app/core/config/config-fix.ts
- 429
+ 418Label for the status of a child
@@ -1034,7 +1014,7 @@
src/app/child-dev-project/children/model/child.ts
- 86
+ 79Label for the dropout date of a child
@@ -1042,7 +1022,7 @@
src/app/child-dev-project/children/model/child.ts
- 90
+ 83Label for the type of dropout of a child
@@ -1050,7 +1030,7 @@
src/app/child-dev-project/children/model/child.ts
- 94
+ 87Label for the remarks about a dropout of a child
@@ -1058,7 +1038,7 @@
src/app/child-dev-project/children/model/child.ts
- 106
+ 99Label for the filename of a photo of a child
@@ -1082,7 +1062,7 @@
src/app/core/config/config-fix.ts
- 363
+ 352Label for the class of a relation
@@ -1416,7 +1396,7 @@
src/app/core/config/config-fix.ts
- 393
+ 382Table header, Short for Body Mass Index
@@ -2042,8 +2022,8 @@
4,6
-
-
+
+
src/app/child-dev-project/previous-schools/previous-schools.component.html9,16
@@ -2194,7 +2174,7 @@
src/app/core/config/config-fix.ts
- 358
+ 347The age of a child
@@ -2210,7 +2190,7 @@
src/app/child-dev-project/schools/demo-school-generator.service.ts
- 56
+ 50School demo timing
@@ -2218,7 +2198,7 @@
src/app/child-dev-project/schools/demo-school-generator.service.ts
- 57
+ 51School demo timing
@@ -2226,110 +2206,10 @@
src/app/child-dev-project/schools/demo-school-generator.service.ts
- 58
+ 52School demo timing
-
-
-
- src/app/child-dev-project/schools/demo-school-generator.service.ts
- 61
-
-
- src/app/child-dev-project/schools/demo-school-generator.service.ts
- 62
-
- School demo working days
-
-
-
-
- src/app/child-dev-project/schools/demo-school-generator.service.ts
- 63
-
- School demo working days
-
-
-
-
- src/app/child-dev-project/schools/model/school.ts
- 17
-
-
- src/app/core/config/config-fix.ts
- 826
-
- Label for the address of a school
-
-
-
-
- src/app/child-dev-project/schools/model/school.ts
- 20
-
- Label for the medium of a school
-
-
-
-
- src/app/child-dev-project/schools/model/school.ts
- 27
-
- Label for the website of a school
-
-
-
-
- src/app/child-dev-project/schools/model/school.ts
- 31
-
-
- src/app/core/config/config-fix.ts
- 288
-
- Label whether school is private
-
-
-
-
- src/app/child-dev-project/schools/model/school.ts
- 35
-
- Label for the contact number of a school
-
-
-
-
- src/app/child-dev-project/schools/model/school.ts
- 39
-
- Label up to which class a school is teaching
-
-
-
-
- src/app/child-dev-project/schools/model/school.ts
- 43
-
- Label for the academic board of a school
-
-
-
-
- src/app/child-dev-project/schools/model/school.ts
- 47
-
- Label for the times of a school
-
-
-
-
- src/app/child-dev-project/schools/model/school.ts
- 51
-
- Label for the working days of a school
-
@@ -2787,7 +2667,7 @@
src/app/core/coming-soon/coming-soon/coming-soon.component.html
- 46,47
+ 47,48
@@ -2822,7 +2702,7 @@
src/app/core/config/config-fix.ts
- 652
+ 623Menu item
@@ -2964,7 +2844,7 @@
src/app/core/config/config-fix.ts
- 570
+ 542Title for notes overview
@@ -2992,11 +2872,11 @@
src/app/core/config/config-fix.ts
- 400
+ 389src/app/core/config/config-fix.ts
- 459
+ 442Translated name of mobile column group
@@ -3016,11 +2896,23 @@
Title of schools overview
+
+
+
+ src/app/core/config/config-fix.ts
+ 282
+
+
+ src/app/core/config/config-fix.ts
+ 823
+
+ Label for private schools filter - true case
+ src/app/core/config/config-fix.ts
- 289
+ 283Label for private schools filter - false case
@@ -3028,11 +2920,11 @@
src/app/core/config/config-fix.ts
- 301
+ 295src/app/core/config/config-fix.ts
- 498
+ 481Panel title
@@ -3040,7 +2932,7 @@
src/app/core/config/config-fix.ts
- 325
+ 323Panel title
@@ -3048,7 +2940,7 @@
src/app/core/config/config-fix.ts
- 349
+ 338Title children overview
@@ -3056,7 +2948,7 @@
src/app/core/config/config-fix.ts
- 375
+ 364Column label for school attendance of child
@@ -3064,7 +2956,7 @@
src/app/core/config/config-fix.ts
- 384
+ 373Column label for coaching attendance of child
@@ -3072,11 +2964,11 @@
src/app/core/config/config-fix.ts
- 399
+ 388src/app/core/config/config-fix.ts
- 416
+ 405Translated name of default column group
@@ -3084,7 +2976,7 @@
src/app/core/config/config-fix.ts
- 403
+ 392Column group name
@@ -3092,11 +2984,11 @@
src/app/core/config/config-fix.ts
- 444
+ 428src/app/core/config/config-fix.ts
- 579
+ 551Column group name
@@ -3104,7 +2996,7 @@
src/app/core/config/config-fix.ts
- 474
+ 457Active children filter label - true case
@@ -3112,7 +3004,7 @@
src/app/core/config/config-fix.ts
- 475
+ 458Active children filter label - false case
@@ -3120,7 +3012,7 @@
src/app/core/config/config-fix.ts
- 538
+ 510Panel title
@@ -3128,7 +3020,7 @@
src/app/core/config/config-fix.ts
- 541
+ 513Title inside a panel
@@ -3136,7 +3028,7 @@
src/app/core/config/config-fix.ts
- 555
+ 527Title inside a panel
@@ -3144,7 +3036,7 @@
src/app/core/config/config-fix.ts
- 561
+ 533Panel title
@@ -3152,7 +3044,7 @@
src/app/core/config/config-fix.ts
- 593
+ 564Title inside a panel
@@ -3160,7 +3052,7 @@
src/app/core/config/config-fix.ts
- 599
+ 570Panel title
@@ -3168,7 +3060,7 @@
src/app/core/config/config-fix.ts
- 608
+ 579Panel title
@@ -3176,7 +3068,7 @@
src/app/core/config/config-fix.ts
- 671
+ 637Panel title
@@ -3184,7 +3076,7 @@
src/app/core/config/config-fix.ts
- 700
+ 666Panel title
@@ -3192,7 +3084,7 @@
src/app/core/config/config-fix.ts
- 716
+ 682Name of a report
@@ -3200,7 +3092,7 @@
src/app/core/config/config-fix.ts
- 720
+ 686Label of report query
@@ -3208,7 +3100,7 @@
src/app/core/config/config-fix.ts
- 723
+ 689Label of report query
@@ -3216,7 +3108,7 @@
src/app/core/config/config-fix.ts
- 727
+ 693Label of report query
@@ -3224,7 +3116,7 @@
src/app/core/config/config-fix.ts
- 734
+ 700Label for report query
@@ -3232,7 +3124,7 @@
src/app/core/config/config-fix.ts
- 737
+ 703Label for report query
@@ -3240,7 +3132,7 @@
src/app/core/config/config-fix.ts
- 741
+ 707Label for report query
@@ -3248,7 +3140,7 @@
src/app/core/config/config-fix.ts
- 746
+ 712Label for report query
@@ -3256,7 +3148,7 @@
src/app/core/config/config-fix.ts
- 749
+ 715Label for report query
@@ -3264,7 +3156,7 @@
src/app/core/config/config-fix.ts
- 753
+ 719Label for report query
@@ -3272,7 +3164,7 @@
src/app/core/config/config-fix.ts
- 759
+ 725Label for report query
@@ -3280,7 +3172,7 @@
src/app/core/config/config-fix.ts
- 764
+ 730Label for report query
@@ -3288,7 +3180,7 @@
src/app/core/config/config-fix.ts
- 767
+ 733Label for report query
@@ -3296,7 +3188,7 @@
src/app/core/config/config-fix.ts
- 771
+ 737Label for report query
@@ -3304,111 +3196,83 @@
src/app/core/config/config-fix.ts
- 781
+ 747Name of a report
-
-
-
- src/app/core/config/config-fix.ts
- 798
-
- Name of a report
-
-
-
-
- src/app/core/config/config-fix.ts
- 833
-
- Label for phone number of a child
-
-
-
-
- src/app/core/config/config-fix.ts
- 840
-
- Label for the guardians of a child
-
-
-
+
+
src/app/core/config/config-fix.ts
- 847
+ 775
- Label for a child attribute
-
-
-
src/app/core/config/config-fix.ts
- 854
+ 837
- Label for a child attribute
+ Label for the address of a child
-
-
+
+
src/app/core/config/config-fix.ts
- 862
+ 782Label for a child attribute
-
-
+
+
src/app/core/config/config-fix.ts
- 870
+ 789
- Label for a child attribute
+ Label for the religion of a child
-
-
+
+
src/app/core/config/config-fix.ts
- 878
+ 796
- Label for a child attribute
+ Label for the mother tongue of a child
-
-
+
+
src/app/core/config/config-fix.ts
- 886
+ 803Label for a child attribute
-
-
+
+
src/app/core/config/config-fix.ts
- 893
+ 830
- Label for a child attribute
+ Label for the language of a school
-
-
+
+
src/app/core/config/config-fix.ts
- 900
+ 844
- Label for a child attribute
+ Label for the phone number of a school
-
-
+
+
src/app/core/config/config-fix.ts
- 907
+ 851
- Label for a child attribute
+ Label for the timing of a schoolsrc/app/core/config/config-fix.ts
- 923
+ 870Label for a child attribute
@@ -3416,7 +3280,7 @@
src/app/core/config/config-fix.ts
- 924
+ 871Description for a child attribute
@@ -3424,7 +3288,7 @@
src/app/core/config/config-fix.ts
- 932
+ 879Label for a child attribute
@@ -3432,7 +3296,7 @@
src/app/core/config/config-fix.ts
- 933
+ 880Description for a child attribute
@@ -3440,7 +3304,7 @@
src/app/core/config/config-fix.ts
- 941
+ 888Label for a child attribute
@@ -3448,7 +3312,7 @@
src/app/core/config/config-fix.ts
- 942
+ 889Description for a child attribute
@@ -3456,7 +3320,7 @@
src/app/core/config/config-fix.ts
- 950
+ 897Label for a child attribute
@@ -3464,7 +3328,7 @@
src/app/core/config/config-fix.ts
- 951
+ 898Description for a child attribute
@@ -3472,7 +3336,7 @@
src/app/core/config/config-fix.ts
- 959
+ 906Label for a child attribute
@@ -3480,7 +3344,7 @@
src/app/core/config/config-fix.ts
- 960
+ 907Description for a child attribute
@@ -3488,7 +3352,7 @@
src/app/core/config/config-fix.ts
- 968
+ 915Label for a child attribute
@@ -3496,7 +3360,7 @@
src/app/core/config/config-fix.ts
- 969
+ 916Description for a child attribute
@@ -3504,7 +3368,7 @@
src/app/core/config/config-fix.ts
- 977
+ 924Label for a child attribute
@@ -3512,7 +3376,7 @@
src/app/core/config/config-fix.ts
- 978
+ 925Description for a child attribute
@@ -3520,7 +3384,7 @@
src/app/core/config/config-fix.ts
- 986
+ 933Label for a child attribute
@@ -3528,7 +3392,7 @@
src/app/core/config/config-fix.ts
- 987
+ 934Description for a child attribute
@@ -3536,7 +3400,7 @@
src/app/core/config/config-fix.ts
- 995
+ 942Label for a child attribute
@@ -3544,7 +3408,7 @@
src/app/core/config/config-fix.ts
- 996
+ 943Description for a child attribute
@@ -3552,7 +3416,7 @@
src/app/core/config/config-fix.ts
- 1004
+ 951Label for a child attribute
@@ -3560,7 +3424,7 @@
src/app/core/config/config-fix.ts
- 1005
+ 952Description for a child attribute
@@ -3849,7 +3713,7 @@
src/app/core/form-dialog/form-dialog-wrapper/form-dialog-wrapper.component.ts
- 142,141
+ 138,137Deleted Entity information
@@ -4191,7 +4055,7 @@
src/app/core/session/session-service/synced-session.service.ts
- 133,132
+ 147,146
From 0962c866ec61e473dbe27397962abbb923a044de Mon Sep 17 00:00:00 2001
From: Simon <33730997+TheSlimvReal@users.noreply.github.com>
Date: Thu, 2 Sep 2021 08:53:43 +0200
Subject: [PATCH 31/34] feat: added columns to show counted attendance of notes
---
.../notes/model/note.spec.ts | 20 ++++++++
src/app/child-dev-project/notes/model/note.ts | 14 +++++-
...note-attendance-count-block.component.html | 1 +
...note-attendance-count-block.component.scss | 0
...e-attendance-count-block.component.spec.ts | 50 +++++++++++++++++++
.../note-attendance-count-block.component.ts | 44 ++++++++++++++++
.../child-dev-project/notes/notes.module.ts | 2 +
.../default-attendance-status-types.ts | 2 +-
.../entity-subrecord.stories.ts | 29 +++++++++++
src/app/core/view/dynamic-components-map.ts | 2 +
10 files changed, 162 insertions(+), 2 deletions(-)
create mode 100644 src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.html
create mode 100644 src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.scss
create mode 100644 src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.spec.ts
create mode 100644 src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.ts
diff --git a/src/app/child-dev-project/notes/model/note.spec.ts b/src/app/child-dev-project/notes/model/note.spec.ts
index 4d583ac9ea..c33c4c5f83 100644
--- a/src/app/child-dev-project/notes/model/note.spec.ts
+++ b/src/app/child-dev-project/notes/model/note.spec.ts
@@ -235,4 +235,24 @@ describe("Note", () => {
otherNote.removeChild("5");
expect(otherNote.children.length).toBe(note.children.length - 1);
});
+
+ it("should count children with a given attendance", () => {
+ const present = testStatusTypes[0];
+ const absent = testStatusTypes[1];
+ const note = new Note();
+ note.addChild("presentChild");
+ note.getAttendance("presentChild").status = present;
+ note.addChild("lateChild");
+ note.getAttendance("lateChild").status = present;
+ note.addChild("absentChild");
+ note.getAttendance("absentChild").status = absent;
+
+ const presentChildren = note.countWithStatus(
+ AttendanceLogicalStatus.PRESENT
+ );
+ expect(presentChildren).toBe(2);
+
+ const absentChildren = note.countWithStatus(AttendanceLogicalStatus.ABSENT);
+ expect(absentChildren).toBe(1);
+ });
});
diff --git a/src/app/child-dev-project/notes/model/note.ts b/src/app/child-dev-project/notes/model/note.ts
index bc2654071a..dd8b5b71a4 100644
--- a/src/app/child-dev-project/notes/model/note.ts
+++ b/src/app/child-dev-project/notes/model/note.ts
@@ -200,10 +200,22 @@ export class Note extends Entity {
}
}
}
-
return false;
}
+ /**
+ * Counts how many children have the given attendance status.
+ * The status is counted based on the AttendanceLogicalStatus and the `AttendanceStatusType.countAs` attribute
+ * @param status which should be counted
+ * @returns number of children with this status
+ */
+ countWithStatus(status: AttendanceLogicalStatus): number {
+ const attendanceValues = this.childrenAttendance.values();
+ return [...attendanceValues].filter(
+ (attendance) => attendance.status.countAs === status
+ ).length;
+ }
+
/**
* Performs a deep copy of the note copying all simple data
* (such as the date, author, e.t.c.) as well as copying the
diff --git a/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.html b/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.html
new file mode 100644
index 0000000000..2031e9a891
--- /dev/null
+++ b/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.html
@@ -0,0 +1 @@
+
{{ participantsWithStatus }}
diff --git a/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.scss b/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.spec.ts b/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.spec.ts
new file mode 100644
index 0000000000..4c22c4f509
--- /dev/null
+++ b/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.spec.ts
@@ -0,0 +1,50 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+
+import { NoteAttendanceCountBlockComponent } from "./note-attendance-count-block.component";
+import { Note } from "../model/note";
+import { defaultAttendanceStatusTypes } from "../../../core/config/default-config/default-attendance-status-types";
+
+describe("NoteAttendanceBlockCountComponent", () => {
+ let component: NoteAttendanceCountBlockComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [NoteAttendanceCountBlockComponent],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NoteAttendanceCountBlockComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should count the present children", () => {
+ const present = defaultAttendanceStatusTypes.find(
+ (status) => status.id === "PRESENT"
+ );
+ const absent = defaultAttendanceStatusTypes.find(
+ (status) => status.id === "ABSENT"
+ );
+ const note = new Note();
+ note.addChild("presentChild");
+ note.getAttendance("presentChild").status = present;
+ note.addChild("absentChild");
+ note.getAttendance("absentChild").status = absent;
+ note.addChild("anotherPresentChild");
+ note.getAttendance("anotherPresentChild").status = present;
+
+ component.onInitFromDynamicConfig({
+ entity: note,
+ id: "",
+ config: { status: "PRESENT" },
+ });
+
+ expect(component.participantsWithStatus).toBe(2);
+ });
+});
diff --git a/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.ts b/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.ts
new file mode 100644
index 0000000000..d272c4a4f8
--- /dev/null
+++ b/src/app/child-dev-project/notes/note-attendance-block/note-attendance-count-block.component.ts
@@ -0,0 +1,44 @@
+import { Component, Input, OnInit } from "@angular/core";
+import { OnInitDynamicComponent } from "../../../core/view/dynamic-components/on-init-dynamic-component.interface";
+import { ViewPropertyConfig } from "../../../core/entity-components/entity-list/EntityListConfig";
+import { Note } from "../model/note";
+import { AttendanceLogicalStatus } from "../../attendance/model/attendance-status";
+
+@Component({
+ selector: "app-note-attendance-count-block",
+ templateUrl: "./note-attendance-count-block.component.html",
+ styleUrls: ["./note-attendance-count-block.component.scss"],
+})
+/**
+ * Displays the amount of children with a given attendance status at a given note.
+ */
+export class NoteAttendanceCountBlockComponent
+ implements OnInitDynamicComponent, OnInit {
+ /**
+ * The note on which the attendance should be counted.
+ */
+ @Input() note: Note;
+
+ /**
+ * The logical attendance status for which the attendance should be counted.
+ */
+ @Input() attendanceStatus: AttendanceLogicalStatus;
+
+ participantsWithStatus: number;
+
+ constructor() {}
+
+ ngOnInit() {
+ if (this.note) {
+ this.participantsWithStatus = this.note.countWithStatus(
+ this.attendanceStatus
+ );
+ }
+ }
+
+ onInitFromDynamicConfig(config: ViewPropertyConfig) {
+ this.note = config.entity as Note;
+ this.attendanceStatus = config.config.status as AttendanceLogicalStatus;
+ this.ngOnInit();
+ }
+}
diff --git a/src/app/child-dev-project/notes/notes.module.ts b/src/app/child-dev-project/notes/notes.module.ts
index d036122eb4..a8bac70b3d 100644
--- a/src/app/child-dev-project/notes/notes.module.ts
+++ b/src/app/child-dev-project/notes/notes.module.ts
@@ -42,12 +42,14 @@ import { AttendanceModule } from "../attendance/attendance.module";
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { ChildMeetingNoteAttendanceComponent } from "./note-details/child-meeting-attendance/child-meeting-note-attendance.component";
import { EntityUtilsModule } from "../../core/entity-components/entity-utils/entity-utils.module";
+import { NoteAttendanceCountBlockComponent } from "./note-attendance-block/note-attendance-count-block.component";
@NgModule({
declarations: [
NoteDetailsComponent,
NotesManagerComponent,
ChildMeetingNoteAttendanceComponent,
+ NoteAttendanceCountBlockComponent,
],
imports: [
CommonModule,
diff --git a/src/app/core/config/default-config/default-attendance-status-types.ts b/src/app/core/config/default-config/default-attendance-status-types.ts
index f81a995e49..bd266f4276 100644
--- a/src/app/core/config/default-config/default-attendance-status-types.ts
+++ b/src/app/core/config/default-config/default-attendance-status-types.ts
@@ -28,7 +28,7 @@ export const defaultAttendanceStatusTypes: AttendanceStatusType[] = [
{
id: "HOLIDAY",
shortName: "H",
- label: $localize`:Child was on holliday:Holiday`,
+ label: $localize`:Child was on holiday:Holiday`,
style: "attendance-H",
countAs: "IGNORE" as AttendanceLogicalStatus,
},
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 59c3294371..ec6513717d 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
@@ -20,6 +20,8 @@ import { ChildrenService } from "../../../../child-dev-project/children/children
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";
const configService = new ConfigService();
const schemaService = new EntitySchemaService();
@@ -47,6 +49,7 @@ export default {
BrowserAnimationsModule,
MatNativeDateModule,
ChildrenModule,
+ MockSessionModule.withState(),
],
providers: [
{
@@ -98,3 +101,29 @@ Primary.args = {
records: data,
newRecordFactory: () => new Note(),
};
+
+export const WithAttendance = Template.bind({});
+WithAttendance.args = {
+ columns: [
+ { id: "date" },
+ { id: "subject" },
+ { id: "category" },
+ { id: "children" },
+ {
+ id: "present",
+ label: "Present",
+ view: "NoteAttendanceCountBlock",
+ additional: { status: AttendanceLogicalStatus.PRESENT },
+ noSorting: true,
+ },
+ {
+ id: "absent",
+ label: "Absent",
+ view: "NoteAttendanceCountBlock",
+ additional: { status: AttendanceLogicalStatus.ABSENT },
+ noSorting: true,
+ },
+ ],
+ records: data,
+ newRecordFactory: () => new Note(),
+};
diff --git a/src/app/core/view/dynamic-components-map.ts b/src/app/core/view/dynamic-components-map.ts
index d6070cfcda..8ebdee199f 100644
--- a/src/app/core/view/dynamic-components-map.ts
+++ b/src/app/core/view/dynamic-components-map.ts
@@ -40,6 +40,7 @@ import { DisplayPercentageComponent } from "../entity-components/entity-utils/vi
import { DisplayUnitComponent } from "../entity-components/entity-utils/view-components/display-unit/display-unit.component";
import { FormComponent } from "../entity-components/entity-details/form/form.component";
import { EditNumberComponent } from "../entity-components/entity-utils/dynamic-form-components/edit-number/edit-number.component";
+import { NoteAttendanceCountBlockComponent } from "../../child-dev-project/notes/note-attendance-block/note-attendance-count-block.component";
export const DYNAMIC_COMPONENTS_MAP = new Map([
["ChildrenCountDashboard", ChildrenCountDashboardComponent],
@@ -84,4 +85,5 @@ export const DYNAMIC_COMPONENTS_MAP = new Map([
["DisplayPercentage", DisplayPercentageComponent],
["DisplayUnit", DisplayUnitComponent],
["EditNumber", EditNumberComponent],
+ ["NoteAttendanceCountBlock", NoteAttendanceCountBlockComponent],
]);
From dfa81070bf308348c6fa77642908ccb89c65f42c Mon Sep 17 00:00:00 2001
From: Simon <33730997+TheSlimvReal@users.noreply.github.com>
Date: Thu, 2 Sep 2021 17:18:55 +0200
Subject: [PATCH 32/34] fix: expand participation field to use full width
(#963)
closes #962
---
.../entity-form/entity-form.component.html | 20 ++++++++++---------
.../entity-form/entity-form.component.scss | 16 +++++++++++++--
2 files changed, 25 insertions(+), 11 deletions(-)
diff --git a/src/app/core/entity-components/entity-form/entity-form/entity-form.component.html b/src/app/core/entity-components/entity-form/entity-form/entity-form.component.html
index 98eb8e2c3e..56149c6537 100644
--- a/src/app/core/entity-components/entity-form/entity-form/entity-form.component.html
+++ b/src/app/core/entity-components/entity-form/entity-form/entity-form.component.html
@@ -6,7 +6,17 @@
fxLayout.sm="row wrap"
>