Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notes of schools and authors #1431

Merged
merged 7 commits into from
Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/app/child-dev-project/children/children.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,31 @@ describe("ChildrenService", () => {
const result = await service.queryActiveRelationsOf("child", "3");
expect(result).toEqual(activeRelations);
});

it("should return related notes", async () => {
const c1 = new Child("c1");
const c2 = new Child("c2");
const s1 = new School("s1");
const s2 = new School("s2");
const n1 = new Note("n1");
n1.addChild(c1);
n1.addChild(c2);
n1.schools.push(s1.getId());
const n2 = new Note("n2");
n2.addChild(c1);
const n3 = new Note("n3");
n3.schools.push(s2.getId());
await entityMapper.saveAll([n1, n2, n3]);

let res = await service.getNotesOf(c1.getId(), "children");
expect(res).toEqual([n1, n2]);

res = await service.getNotesOf(s1.getId(), "schools");
expect(res).toEqual([n1]);

res = await service.getNotesOf(s2.getId(), "schools");
expect(res).toEqual([n3]);
});
});

function generateChildEntities(): Child[] {
Expand Down
36 changes: 20 additions & 16 deletions src/app/child-dev-project/children/children.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ChildPhotoService } from "./child-photo-service/child-photo.service";
import moment, { Moment } from "moment";
import { LoggingService } from "../../core/logging/logging.service";
import { DatabaseIndexingService } from "../../core/entity/database-indexing/database-indexing.service";
import { Entity } from "../../core/entity/model/entity";

@Injectable()
export class ChildrenService {
Expand Down Expand Up @@ -122,14 +123,12 @@ export class ChildrenService {
);
}

getNotesOfChild(childId: string): Observable<Note[]> {
const promise = this.dbIndexing.queryIndexDocs(
getNotesOf(entityId: string, noteProperty: string): Promise<Note[]> {
return this.dbIndexing.queryIndexDocs(
Note,
"notes_index/by_child",
childId
`notes_index/by_${noteProperty}`,
entityId
);

return from(promise);
}

/**
Expand Down Expand Up @@ -191,16 +190,6 @@ export class ChildrenService {
const designDoc = {
_id: "_design/notes_index",
views: {
by_child: {
map:
"(doc) => { " +
'if (!doc._id.startsWith("' +
Note.ENTITY_TYPE +
'")) return;' +
"if (!Array.isArray(doc.children)) return;" +
"doc.children.forEach(childId => emit(childId)); " +
"}",
},
note_child_by_date: {
map: `(doc) => {
if (!doc._id.startsWith("${Note.ENTITY_TYPE}")) return;
Expand All @@ -212,10 +201,25 @@ export class ChildrenService {
},
},
};
// creating a by_... view for each of the following properties
["children", "schools", "authors"].forEach(
(prop) =>
(designDoc.views[`by_${prop}`] = this.createNotesByFunction(prop))
);

return this.dbIndexing.createIndex(designDoc);
}

private createNotesByFunction(property: string) {
return {
map: `(doc) => {
if (!doc._id.startsWith("${Note.ENTITY_TYPE}")) return;
if (!Array.isArray(doc.${property})) return;
doc.${property}.forEach(val => emit(val));
}`,
};
}

/**
*
* @param childId should be set in the specific components and is passed by the URL as a parameter
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { NotesOfChildComponent } from "./notes-of-child.component";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from "@angular/core/testing";
import { NotesModule } from "../notes.module";
import { ChildrenService } from "../../children/children.service";
import { Note } from "../model/note";
import { Child } from "../../children/model/child";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";

const allChildren: Array<Note> = [];
import {
MockedTestingModule,
TEST_USER,
} from "../../../utils/mocked-testing.module";
import { ChildSchoolRelation } from "../../children/model/childSchoolRelation";
import { PanelConfig } from "../../../core/entity-components/entity-details/EntityDetailsConfig";
import { Entity } from "../../../core/entity/model/entity";
import { School } from "../../schools/model/school";
import { User } from "../../../core/user/user";
import moment from "moment";

describe("NotesOfChildComponent", () => {
let component: NotesOfChildComponent;
Expand All @@ -15,9 +27,8 @@ describe("NotesOfChildComponent", () => {
let mockChildrenService: jasmine.SpyObj<ChildrenService>;

beforeEach(() => {
mockChildrenService = jasmine.createSpyObj("mockChildrenService", [
"getNotesOfChild",
]);
mockChildrenService = jasmine.createSpyObj(["getNotesOf"]);
mockChildrenService.getNotesOf.and.resolveTo([]);
TestBed.configureTestingModule({
imports: [NotesModule, MockedTestingModule.withState()],
providers: [{ provide: ChildrenService, useValue: mockChildrenService }],
Expand All @@ -27,21 +38,61 @@ describe("NotesOfChildComponent", () => {
beforeEach(async () => {
fixture = TestBed.createComponent(NotesOfChildComponent);
component = fixture.componentInstance;
component.child = new Child("1");
component.entity = new Child("1");
fixture.detectChanges();
});

it("should create", () => {
expect(component).toBeTruthy();
});

it("should load initial notes", async () => {
await fixture.whenStable();
expect(component.records).toEqual(allChildren);
it("should throw an error when a invalid entity is passed", () => {
const config: PanelConfig = { entity: new ChildSchoolRelation() };
expect(() => component.onInitFromDynamicConfig(config)).toThrowError();
});

it("should use the attendance color function when passing a child", () => {
const note = new Note();
spyOn(note, "getColorForId");
const entity = new Child();
component.onInitFromDynamicConfig({ entity });

component.getColor(note);

expect(note.getColorForId).toHaveBeenCalledWith(entity.getId());
});

it("should create a new note", function () {
const newNoteFactory = component.generateNewRecordFactory();
expect(newNoteFactory).toBeDefined();
it("should create a new note and fill it with the appropriate initial value", () => {
let entity: Entity = new Child();
component.onInitFromDynamicConfig({ entity });
let note = component.generateNewRecordFactory()();
expect(note.children).toEqual([entity.getId()]);
expect(note.authors).toEqual([TEST_USER]);

entity = new School();
component.onInitFromDynamicConfig({ entity });
note = component.generateNewRecordFactory()();
expect(note.schools).toEqual([entity.getId()]);
expect(note.authors).toEqual([TEST_USER]);

entity = new User();
component.onInitFromDynamicConfig({ entity });
note = component.generateNewRecordFactory()();
expect(note.authors).toEqual([entity.getId(), TEST_USER]);
});

it("should sort notes by date", fakeAsync(() => {
// No date should come first
const n1 = new Note();
const n2 = new Note();
n2.date = moment().subtract(1, "day").toDate();
const n3 = new Note();
n3.date = moment().subtract(2, "days").toDate();
mockChildrenService.getNotesOf.and.resolveTo([n3, n2, n1]);

component.onInitFromDynamicConfig({ entity: new Child() });
tick();

expect(component.records).toEqual([n1, n2, n3]);
}));
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,29 @@ import { NoteDetailsComponent } from "../note-details/note-details.component";
import { ChildrenService } from "../../children/children.service";
import moment from "moment";
import { SessionService } from "../../../core/session/session-service/session.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { Child } from "../../children/model/child";
import { OnInitDynamicComponent } from "../../../core/view/dynamic-components/on-init-dynamic-component.interface";
import { PanelConfig } from "../../../core/entity-components/entity-details/EntityDetailsConfig";
import { FormFieldConfig } from "../../../core/entity-components/entity-form/entity-form/FormConfig";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
import { DynamicComponent } from "../../../core/view/dynamic-components/dynamic-component.decorator";
import { Entity } from "../../../core/entity/model/entity";

/**
* The component that is responsible for listing the Notes that are related to a certain child
*
* TODO rename this to a more general name as this can also handle notes of schools and notes of authors
*/
@UntilDestroy()
@DynamicComponent("NotesOfChild")
@Component({
selector: "app-notes-of-child",
templateUrl: "./notes-of-child.component.html",
styleUrls: ["./notes-of-child.component.scss"],
})
export class NotesOfChildComponent
implements OnChanges, OnInitDynamicComponent {
@Input() child: Child;
implements OnChanges, OnInitDynamicComponent
{
@Input() entity: Entity;
private noteProperty = "children";
records: Array<Note> = [];

columns: FormFieldConfig[] = [
Expand All @@ -35,6 +37,12 @@ export class NotesOfChildComponent
{ id: "warningLevel", visibleFrom: "md" },
];

/**
* returns the color for a note; passed to the entity subrecord component
* @param note note to get color for
*/
getColor = (note: Note) => note?.getColor();

constructor(
private childrenService: ChildrenService,
private sessionService: SessionService,
Expand All @@ -52,15 +60,29 @@ export class NotesOfChildComponent
this.columns = config.config.columns;
}

this.child = config.entity as Child;
this.entity = config.entity;
const entityType = this.entity.getType();
this.noteProperty = [...Note.schema.keys()].find(
(prop) => Note.schema.get(prop).additional === entityType
);
Comment on lines +63 to +67
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impressive solution 😃 took me a second to even understand why this works

if (!this.noteProperty) {
throw new Error(
`Could not load notes for related entity: "${entityType}"`
);
}

if (this.noteProperty === "children") {
// When displaying notes for a child, use attendance color highlighting
this.getColor = (note: Note) => note?.getColorForId(this.entity.getId());
}

this.initNotesOfChild();
}

private initNotesOfChild() {
this.childrenService
.getNotesOfChild(this.child.getId())
.pipe(untilDestroyed(this))
.subscribe((notes: Note[]) => {
.getNotesOf(this.entity.getId(), this.noteProperty)
.then((notes: Note[]) => {
notes.sort((a, b) => {
if (!a.date && b.date) {
// note without date should be first
Expand All @@ -73,26 +95,25 @@ 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().name;
const childId = this.child.getId();
const entityId = this.entity.getId();

return () => {
const newNote = new Note(Date.now().toString());
newNote.date = new Date();
newNote.addChild(childId);
newNote.authors = [user];
if (this.noteProperty === "children") {
newNote.addChild(entityId);
} else {
newNote[this.noteProperty].push(entityId);
}
if (!newNote.authors.includes(user)) {
newNote.authors.push(user);
}

return newNote;
};
}

/**
* returns the color for a note; passed to the entity subrecord component
* @param note note to get color for
*/
getColor = (note: Note) => note?.getColorForId(this.child.getId());

showNoteDetails(note: Note) {
this.formDialog.openDialog(NoteDetailsComponent, note);
}
Expand Down