+
+
@@ -67,12 +80,16 @@
class="nav-button"
fxFlex
i18n="Skip-Button|Button to skip a step in the roll call"
+ angulartics2On="click" angularticsCategory="Record Attendance" angularticsAction="rollcall_skip"
>
Skip
-
diff --git a/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.component.spec.ts b/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.component.spec.ts
index 97588ae08b..8c5cbf3e30 100644
--- a/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.component.spec.ts
+++ b/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.component.spec.ts
@@ -14,6 +14,7 @@ import { MatNativeDateModule } from "@angular/material/core";
import { mockEntityMapper } from "../../../core/entity/mock-entity-mapper-service";
import { EventNote } from "../model/event-note";
import { AttendanceService } from "../attendance.service";
+import { AnalyticsService } from "../../../core/analytics/analytics.service";
describe("AttendanceCalendarComponent", () => {
let component: AttendanceCalendarComponent;
@@ -35,6 +36,10 @@ describe("AttendanceCalendarComponent", () => {
provide: EntityMapperService,
useValue: mockEntityMapper(),
},
+ {
+ provide: AnalyticsService,
+ useValue: jasmine.createSpyObj(["eventTrack"]),
+ },
{
provide: AttendanceService,
useValue: mockAttendanceService,
diff --git a/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.component.ts b/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.component.ts
index 8d8b739fdc..3a0125113a 100644
--- a/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.component.ts
+++ b/src/app/child-dev-project/attendance/attendance-calendar/attendance-calendar.component.ts
@@ -22,6 +22,7 @@ import { RecurringActivity } from "../model/recurring-activity";
import { applyUpdate } from "../../../core/entity/model/entity-update";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { AttendanceService } from "../attendance.service";
+import { AnalyticsService } from "../../../core/analytics/analytics.service";
@Component({
selector: "app-attendance-calendar",
@@ -47,6 +48,7 @@ export class AttendanceCalendarComponent implements OnChanges {
constructor(
private entityMapper: EntityMapperService,
private formDialog: FormDialogService,
+ private analyticsService: AnalyticsService,
private attendanceService: AttendanceService
) {
this.entityMapper
@@ -148,6 +150,11 @@ export class AttendanceCalendarComponent implements OnChanges {
this.selectedEvent
);
}
+
+ this.analyticsService.eventTrack("calendar_select_date", {
+ category: "Attendance",
+ label: this.selectedEvent ? "with event" : "without event",
+ });
}
this.calendar.updateTodaysDate();
@@ -165,6 +172,10 @@ export class AttendanceCalendarComponent implements OnChanges {
}
await this.entityMapper.save(this.selectedEvent);
+
+ this.analyticsService.eventTrack("calendar_save_event_changes", {
+ category: "Attendance",
+ });
}
createNewEvent() {
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 aa760c0867..caee0ec0b0 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
@@ -77,6 +77,7 @@ describe("ChildrenListComponent", () => {
const routeMock = {
data: of({ config: routeData }),
queryParams: of({}),
+ snapshot: { queryParams: {} },
};
const mockChildrenService: jasmine.SpyObj = jasmine.createSpyObj(
["getChildren"]
diff --git a/src/app/child-dev-project/children/model/childSchoolRelation.ts b/src/app/child-dev-project/children/model/childSchoolRelation.ts
index ac504585b6..565e78f606 100644
--- a/src/app/child-dev-project/children/model/childSchoolRelation.ts
+++ b/src/app/child-dev-project/children/model/childSchoolRelation.ts
@@ -31,11 +31,13 @@ export class ChildSchoolRelation extends Entity {
@DatabaseField({
dataType: "date-only",
label: $localize`:Label for the start date of a relation:From`,
+ description: $localize`:Description of the start date of a relation:The date a child joins a school`,
})
start: Date;
@DatabaseField({
dataType: "date-only",
label: $localize`:Label for the end date of a relation:To`,
+ description: $localize`:Description of the end date of a relation:The date of a child leaving the school`,
})
end: Date;
diff --git a/src/app/child-dev-project/children/model/genders.ts b/src/app/child-dev-project/children/model/genders.ts
index 3dfc448fd3..d1c536efd2 100644
--- a/src/app/child-dev-project/children/model/genders.ts
+++ b/src/app/child-dev-project/children/model/genders.ts
@@ -13,4 +13,8 @@ export const genders: ConfigurableEnumValue[] = [
id: "F",
label: $localize`:Label gender:female`,
},
+ {
+ id: "X",
+ label: $localize`:Label gender:Non-binary/third gender`,
+ },
];
diff --git a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
index 2a0bae28e5..3072db2491 100644
--- a/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
+++ b/src/app/child-dev-project/notes/notes-manager/notes-manager.component.html
@@ -8,6 +8,10 @@
Slider that allows a user to also include events|events are related to a
child
"
+ angulartics2On="click"
+ angularticsCategory="Note"
+ angularticsAction="include_events_toggle"
+ [angularticsLabel]="includeEventNotes ? 'off' : 'on'"
>
Include events
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 24e4501b34..ec2d43d1d7 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
@@ -77,6 +77,7 @@ describe("NotesManagerComponent", () => {
const routeMock = {
data: new BehaviorSubject({ config: routeData }),
queryParams: of({}),
+ snapshot: { queryParams: {} },
};
const testInteractionTypes: InteractionType[] = [
diff --git a/src/app/child-dev-project/schools/children-overview/children-overview.component.html b/src/app/child-dev-project/schools/children-overview/children-overview.component.html
index a404d8954f..160e63f110 100644
--- a/src/app/child-dev-project/schools/children-overview/children-overview.component.html
+++ b/src/app/child-dev-project/schools/children-overview/children-overview.component.html
@@ -1,19 +1,6 @@
-
-
- Add {{ addButtonLabel }}
-
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 d341932c42..054cdbe0fd 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
@@ -9,27 +9,21 @@ import { ChildrenOverviewComponent } from "./children-overview.component";
import { SchoolsModule } from "../schools.module";
import { School } from "../model/school";
import { Child } from "../../children/model/child";
-import { SchoolsService } from "../schools.service";
import { RouterTestingModule } from "@angular/router/testing";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { Router } from "@angular/router";
import { MockSessionModule } from "../../../core/session/mock-session.module";
-import { MatDialog } from "@angular/material/dialog";
-import { EntityFormComponent } from "../../../core/entity-components/entity-form/entity-form/entity-form.component";
-import { EntityFormService } from "../../../core/entity-components/entity-form/entity-form.service";
-import { AlertService } from "../../../core/alerts/alert.service";
-import { EventEmitter } from "@angular/core";
-import { PanelConfig } from "../../../core/entity-components/entity-details/EntityDetailsConfig";
import { ChildSchoolRelation } from "../../children/model/childSchoolRelation";
+import { ChildrenService } from "../../children/children.service";
describe("ChildrenOverviewComponent", () => {
let component: ChildrenOverviewComponent;
let fixture: ComponentFixture;
- let mockSchoolsService: jasmine.SpyObj;
+ let mockChildrenService: jasmine.SpyObj;
beforeEach(
waitForAsync(() => {
- mockSchoolsService = jasmine.createSpyObj(["getChildrenForSchool"]);
+ mockChildrenService = jasmine.createSpyObj(["queryRelationsOf"]);
TestBed.configureTestingModule({
declarations: [],
@@ -39,7 +33,9 @@ describe("ChildrenOverviewComponent", () => {
NoopAnimationsModule,
MockSessionModule.withState(),
],
- providers: [{ provide: SchoolsService, useValue: mockSchoolsService }],
+ providers: [
+ { provide: ChildrenService, useValue: mockChildrenService },
+ ],
}).compileComponents();
})
);
@@ -47,6 +43,7 @@ describe("ChildrenOverviewComponent", () => {
beforeEach(() => {
fixture = TestBed.createComponent(ChildrenOverviewComponent);
component = fixture.componentInstance;
+ component.entity = new School();
fixture.detectChanges();
});
@@ -54,20 +51,21 @@ describe("ChildrenOverviewComponent", () => {
expect(component).toBeTruthy();
});
- it("should load the children for a school", fakeAsync(() => {
+ it("should load the relations for a school", fakeAsync(() => {
const school = new School("s1");
- const child1 = new Child("c1");
- const child2 = new Child("c2");
+ const relation1 = new ChildSchoolRelation("r1");
+ const relation2 = new ChildSchoolRelation("r2");
const config = { entity: school };
- mockSchoolsService.getChildrenForSchool.and.resolveTo([child1, child2]);
+ mockChildrenService.queryRelationsOf.and.resolveTo([relation1, relation2]);
component.onInitFromDynamicConfig(config);
+ tick();
- expect(mockSchoolsService.getChildrenForSchool).toHaveBeenCalledWith(
+ expect(mockChildrenService.queryRelationsOf).toHaveBeenCalledWith(
+ "school",
school.getId()
);
- tick();
- expect(component.children).toEqual([child1, child2]);
+ expect(component.records).toEqual([relation1, relation2]);
}));
it("should route to a child when clicked", () => {
@@ -83,89 +81,12 @@ describe("ChildrenOverviewComponent", () => {
]);
});
- it("should open a dialog when clicking add new child with correct relation", () => {
- component.entity = new School();
- const dialog = TestBed.inject(MatDialog);
- const entityFormService = TestBed.inject(EntityFormService);
- const alertService = TestBed.inject(AlertService);
- const dialogComponent = new EntityFormComponent(
- entityFormService,
- alertService
- );
- spyOn(dialog, "open").and.returnValues({
- componentInstance: dialogComponent,
- } as any);
+ it("should create a relation with the school ID already been set", () => {
+ component.entity = new School("testID");
- component.addChildClick();
+ const newRelation = component.generateNewRecordFactory()();
- expect(dialog.open).toHaveBeenCalled();
- const relation = dialogComponent.entity as ChildSchoolRelation;
- expect(relation.schoolId).toBe(component.entity.getId());
+ expect(newRelation).toBeInstanceOf(ChildSchoolRelation);
+ expect(newRelation.schoolId).toBe("testID");
});
-
- it("should add a newly added child to the list", fakeAsync(() => {
- component.entity = new School();
- const child = new Child();
- const dialog = TestBed.inject(MatDialog);
- const dialogComponent = {
- onSave: new EventEmitter(),
- onCancel: new EventEmitter(),
- };
- spyOn(dialog, "open").and.returnValues({
- componentInstance: dialogComponent,
- close: () => {},
- } as any);
- mockSchoolsService.getChildrenForSchool.and.resolveTo([child]);
-
- component.addChildClick();
- dialogComponent.onSave.emit(undefined);
- tick();
-
- expect(component.children).toContain(child);
- }));
-
- it("should close the dialog when cancel is clicked", fakeAsync(() => {
- component.entity = new School();
- const dialog = TestBed.inject(MatDialog);
- const dialogComponent = {
- onSave: new EventEmitter(),
- onCancel: new EventEmitter(),
- };
- const closeSpy = jasmine.createSpy();
- spyOn(dialog, "open").and.returnValues({
- componentInstance: dialogComponent,
- close: closeSpy,
- } as any);
-
- component.addChildClick();
- dialogComponent.onCancel.emit(undefined);
- tick();
-
- expect(closeSpy).toHaveBeenCalled();
- }));
-
- it("should assign the popup columns from the config", fakeAsync(() => {
- const dialog = TestBed.inject(MatDialog);
- const dialogComponent = {
- columns: [],
- onSave: new EventEmitter(),
- onCancel: new EventEmitter(),
- };
- spyOn(dialog, "open").and.returnValues({
- componentInstance: dialogComponent,
- close: () => {},
- } as any);
- const popupColumns = ["start", "end", "class"];
- const config: PanelConfig = {
- entity: new Child(),
- config: { popupColumns: popupColumns },
- };
-
- component.onInitFromDynamicConfig(config);
- tick();
- component.addChildClick();
- tick();
-
- expect(dialogComponent.columns).toEqual(popupColumns.map((col) => [col]));
- }));
});
diff --git a/src/app/child-dev-project/schools/children-overview/children-overview.component.ts b/src/app/child-dev-project/schools/children-overview/children-overview.component.ts
index 35d60b30b8..373792e99e 100644
--- a/src/app/child-dev-project/schools/children-overview/children-overview.component.ts
+++ b/src/app/child-dev-project/schools/children-overview/children-overview.component.ts
@@ -1,14 +1,12 @@
import { Component } from "@angular/core";
import { OnInitDynamicComponent } from "../../../core/view/dynamic-components/on-init-dynamic-component.interface";
-import { SchoolsService } from "../schools.service";
import { Child } from "../../children/model/child";
import { PanelConfig } from "../../../core/entity-components/entity-details/EntityDetailsConfig";
import { FormFieldConfig } from "../../../core/entity-components/entity-form/entity-form/FormConfig";
import { Router } from "@angular/router";
-import { MatDialog } from "@angular/material/dialog";
-import { EntityFormComponent } from "../../../core/entity-components/entity-form/entity-form/entity-form.component";
import { ChildSchoolRelation } from "../../children/model/childSchoolRelation";
import { Entity } from "../../../core/entity/model/entity";
+import { ChildrenService } from "../../children/children.service";
/**
* This component creates a table containing all children currently attending this school.
@@ -19,47 +17,29 @@ import { Entity } from "../../../core/entity/model/entity";
styleUrls: ["./children-overview.component.scss"],
})
export class ChildrenOverviewComponent implements OnInitDynamicComponent {
- readonly addButtonLabel = ChildSchoolRelation.schema.get("childId").label;
-
columns: FormFieldConfig[] = [
- { id: "projectNumber" },
- { id: "name" },
- {
- id: "schoolClass",
- label: $localize`:The school-class of a child:Class`,
- view: "DisplayText",
- },
- {
- id: "age",
- label: $localize`:The age of a child:Age`,
- view: "DisplayText",
- },
- ];
-
- private popupColumns: (string | FormFieldConfig)[] = [
- "childId",
- "start",
- "end",
+ { id: "childId" },
+ { id: "schoolClass" },
+ { id: "start" },
+ { id: "end" },
+ { id: "result" },
];
- children: Child[] = [];
entity: Entity;
+ records: ChildSchoolRelation[] = [];
constructor(
- private schoolsService: SchoolsService,
- private router: Router,
- private dialog: MatDialog
+ private childrenService: ChildrenService,
+ private router: Router
) {}
async onInitFromDynamicConfig(config: PanelConfig) {
if (config?.config?.columns) {
this.columns = config.config.columns;
}
- if (config?.config?.popupColumns?.length > 0) {
- this.popupColumns = config.config.popupColumns;
- }
this.entity = config.entity;
- this.children = await this.schoolsService.getChildrenForSchool(
+ this.records = await this.childrenService.queryRelationsOf(
+ "school",
this.entity.getId()
);
}
@@ -68,23 +48,11 @@ export class ChildrenOverviewComponent implements OnInitDynamicComponent {
this.router.navigate([`/${child.getType().toLowerCase()}`, child.getId()]);
}
- addChildClick() {
- const dialogRef = this.dialog.open(EntityFormComponent, {
- width: "80%",
- maxHeight: "90vh",
- });
-
- dialogRef.componentInstance.columns = this.popupColumns.map((col) => [col]);
- const newRelation = new ChildSchoolRelation();
- newRelation.schoolId = this.entity.getId();
- dialogRef.componentInstance.entity = newRelation;
- dialogRef.componentInstance.editing = true;
- dialogRef.componentInstance.onSave.subscribe(async () => {
- dialogRef.close();
- this.children = await this.schoolsService.getChildrenForSchool(
- this.entity.getId()
- );
- });
- dialogRef.componentInstance.onCancel.subscribe(() => dialogRef.close());
+ generateNewRecordFactory(): () => ChildSchoolRelation {
+ return () => {
+ const newRelation = new ChildSchoolRelation();
+ newRelation.schoolId = this.entity.getId();
+ return newRelation;
+ };
}
}
diff --git a/src/app/child-dev-project/schools/model/school.ts b/src/app/child-dev-project/schools/model/school.ts
index 14c985f6b8..c17961f7ed 100644
--- a/src/app/child-dev-project/schools/model/school.ts
+++ b/src/app/child-dev-project/schools/model/school.ts
@@ -8,6 +8,12 @@ export class School extends Entity {
return "SchoolBlock";
}
+ static create(params: Partial): School {
+ const school = new School();
+ Object.assign(school, params);
+ return school;
+ }
+
@DatabaseField({
label: $localize`:Label for the name of a school:Name`,
required: true,
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 93c6b9b51e..95be95a110 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
@@ -40,6 +40,7 @@ describe("SchoolsListComponent", () => {
const routeMock = {
data: of({ config: routeData }),
queryParams: of({}),
+ snapshot: { queryParams: {} },
};
beforeEach(
diff --git a/src/app/child-dev-project/schools/schools.module.ts b/src/app/child-dev-project/schools/schools.module.ts
index d7746cea00..f63563c95a 100644
--- a/src/app/child-dev-project/schools/schools.module.ts
+++ b/src/app/child-dev-project/schools/schools.module.ts
@@ -22,7 +22,6 @@ import { MatPaginatorModule } from "@angular/material/paginator";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import { MatSelectModule } from "@angular/material/select";
import { RouterModule } from "@angular/router";
-import { SchoolsService } from "./schools.service";
import { MatTooltipModule } from "@angular/material/tooltip";
import { Angulartics2Module } from "angulartics2";
import { ChildrenOverviewComponent } from "./children-overview/children-overview.component";
@@ -77,7 +76,7 @@ import { EntitySubrecordModule } from "../../core/entity-components/entity-subre
ChildrenOverviewComponent,
],
exports: [SchoolBlockComponent],
- providers: [SchoolsService, DatePipe],
+ providers: [DatePipe],
entryComponents: [SchoolBlockComponent],
})
export class SchoolsModule {}
diff --git a/src/app/child-dev-project/schools/schools.service.ts b/src/app/child-dev-project/schools/schools.service.ts
deleted file mode 100644
index 807bfd9769..0000000000
--- a/src/app/child-dev-project/schools/schools.service.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Injectable } from "@angular/core";
-import { School } from "./model/school";
-import { EntityMapperService } from "../../core/entity/entity-mapper.service";
-import { from, Observable } from "rxjs";
-import { Child } from "../children/model/child";
-import { ChildrenService } from "../children/children.service";
-import { LoggingService } from "../../core/logging/logging.service";
-
-@Injectable()
-export class SchoolsService {
- constructor(
- private entityMapper: EntityMapperService,
- private childrenService: ChildrenService,
- private log: LoggingService
- ) {}
-
- getSchools(): Observable {
- return from(this.entityMapper.loadType(School));
- }
-
- async getChildrenForSchool(schoolId: string): Promise {
- const relations = await this.childrenService.queryRelationsOf(
- "school",
- schoolId
- );
- const children: Child[] = [];
- for (const relation of relations) {
- try {
- children.push(
- await this.childrenService.getChild(relation.childId).toPromise()
- );
- } catch (e) {
- this.log.warn("Could not find child " + relation.childId);
- }
- }
- return children;
- }
-}
diff --git a/src/app/core/analytics/analytics.service.spec.ts b/src/app/core/analytics/analytics.service.spec.ts
index 04cd58094c..b1bc05d7b7 100644
--- a/src/app/core/analytics/analytics.service.spec.ts
+++ b/src/app/core/analytics/analytics.service.spec.ts
@@ -4,17 +4,40 @@ import { AnalyticsService } from "./analytics.service";
import { Angulartics2Module } from "angulartics2";
import { RouterTestingModule } from "@angular/router/testing";
import { MockSessionModule } from "../session/mock-session.module";
+import { ConfigService } from "../config/config.service";
+import { UsageAnalyticsConfig } from "./usage-analytics-config";
+import { Angulartics2Piwik } from "angulartics2/piwik";
+import { AppConfig } from "../app-config/app-config";
+import { IAppConfig } from "../app-config/app-config.model";
describe("AnalyticsService", () => {
let service: AnalyticsService;
+ let mockConfigService: jasmine.SpyObj;
+ let mockAngulartics: jasmine.SpyObj;
+
beforeEach(() => {
+ AppConfig.settings = { site_name: "unit-testing" } as IAppConfig;
+ mockConfigService = jasmine.createSpyObj("mockConfigService", [
+ "getConfig",
+ ]);
+ mockAngulartics = jasmine.createSpyObj("mockAngulartics", [
+ "startTracking",
+ "setUserProperties",
+ "setUsername",
+ ]);
+
TestBed.configureTestingModule({
imports: [
Angulartics2Module.forRoot(),
RouterTestingModule,
MockSessionModule.withState(),
],
+ providers: [
+ AnalyticsService,
+ { provide: ConfigService, useValue: mockConfigService },
+ { provide: Angulartics2Piwik, useValue: mockAngulartics },
+ ],
});
service = TestBed.inject(AnalyticsService);
});
@@ -22,4 +45,58 @@ describe("AnalyticsService", () => {
it("should be created", () => {
expect(service).toBeTruthy();
});
+
+ it("should not track if no url or site_id", () => {
+ mockConfigService.getConfig.and.returnValue({});
+ service.init();
+ expect(mockAngulartics.startTracking).not.toHaveBeenCalled();
+ });
+
+ it("should not track if no usage analytics config", () => {
+ mockConfigService.getConfig.and.returnValue(undefined);
+ service.init();
+ expect(mockAngulartics.startTracking).not.toHaveBeenCalled();
+ });
+
+ it("should track correct site_id", () => {
+ const testAnalyticsConfig: UsageAnalyticsConfig = {
+ site_id: "101",
+ url: "test-endpoint",
+ };
+ mockConfigService.getConfig.and.returnValue(testAnalyticsConfig);
+
+ service.init();
+
+ expect(mockAngulartics.startTracking).toHaveBeenCalledTimes(1);
+ expect(window["_paq"]).toContain([
+ "setSiteId",
+ testAnalyticsConfig.site_id,
+ ]);
+ });
+
+ it("should set tracker url with or without trailing slash", () => {
+ window["_paq"] = [];
+ const testAnalyticsConfig: UsageAnalyticsConfig = {
+ site_id: "101",
+ url: "test-endpoint",
+ };
+ mockConfigService.getConfig.and.returnValue(testAnalyticsConfig);
+ service.init();
+ expect(window["_paq"]).toContain([
+ "setTrackerUrl",
+ testAnalyticsConfig.url + "/matomo.php",
+ ]);
+
+ window["_paq"] = [];
+ const testAnalyticsConfig2: UsageAnalyticsConfig = {
+ site_id: "101",
+ url: "test-endpoint/",
+ };
+ mockConfigService.getConfig.and.returnValue(testAnalyticsConfig2);
+ service.init();
+ expect(window["_paq"]).toContain([
+ "setTrackerUrl",
+ testAnalyticsConfig2.url + "matomo.php",
+ ]);
+ });
});
diff --git a/src/app/core/analytics/analytics.service.ts b/src/app/core/analytics/analytics.service.ts
index f592773b5d..67e92a72ab 100644
--- a/src/app/core/analytics/analytics.service.ts
+++ b/src/app/core/analytics/analytics.service.ts
@@ -2,8 +2,13 @@ import { Injectable } from "@angular/core";
import { Angulartics2Piwik } from "angulartics2/piwik";
import { environment } from "../../../environments/environment";
import { AppConfig } from "../app-config/app-config";
+import { ConfigService } from "../config/config.service";
import { SessionService } from "../session/session-service/session.service";
import { LoginState } from "../session/session-states/login-state.enum";
+import {
+ USAGE_ANALYTICS_CONFIG_ID,
+ UsageAnalyticsConfig,
+} from "./usage-analytics-config";
const md5 = require("md5");
@@ -22,6 +27,7 @@ export class AnalyticsService {
constructor(
private angulartics2Piwik: Angulartics2Piwik,
+ private configService: ConfigService,
private sessionService: SessionService
) {
this.subscribeToUserChanges();
@@ -59,19 +65,22 @@ export class AnalyticsService {
* Set up usage analytics tracking - if the AppConfig specifies the required settings.
*/
init(): void {
- if (!AppConfig.settings.usage_analytics) {
+ const config = this.configService.getConfig(
+ USAGE_ANALYTICS_CONFIG_ID
+ );
+
+ if (!config || !config.url || !config.site_id) {
// do not track
return;
}
- this.setUpMatomo(
- AppConfig.settings.usage_analytics.url,
- AppConfig.settings.usage_analytics.site_id
- );
+ this.setUpMatomo(config.url, config.site_id, config.no_cookies);
- this.angulartics2Piwik.startTracking();
this.setVersion();
this.setOrganization(AppConfig.settings.site_name);
+ this.setUser(undefined);
+
+ this.angulartics2Piwik.startTracking();
}
/**
@@ -82,21 +91,26 @@ export class AnalyticsService {
*
* @param url The URL of the matomo backend
* @param id The id of the Matomo site as which this app will be tracked
+ * @param disableCookies (Optional) flag whether to disable use of cookies to track sessions
* @private
*/
- private setUpMatomo(url: string, id: string) {
+ private setUpMatomo(
+ url: string,
+ id: string,
+ disableCookies: boolean = false
+ ) {
window["_paq"] = window["_paq"] || [];
window["_paq"].push([
"setDocumentTitle",
document.domain + "/" + document.title,
]);
- if (AppConfig.settings.usage_analytics.no_cookies) {
+ if (disableCookies) {
window["_paq"].push(["disableCookies"]);
}
window["_paq"].push(["trackPageView"]);
window["_paq"].push(["enableLinkTracking"]);
(() => {
- const u = url;
+ const u = url.endsWith("/") ? url : url + "/";
window["_paq"].push(["setTrackerUrl", u + "matomo.php"]);
window["_paq"].push(["setSiteId", id]);
const d = document;
@@ -119,7 +133,7 @@ export class AnalyticsService {
action: string,
properties: {
category: string;
- label: string;
+ label?: string;
value?: number;
} = {
category: "no_category",
diff --git a/src/app/core/analytics/usage-analytics-config.ts b/src/app/core/analytics/usage-analytics-config.ts
new file mode 100644
index 0000000000..e53f1773c4
--- /dev/null
+++ b/src/app/core/analytics/usage-analytics-config.ts
@@ -0,0 +1,16 @@
+/**
+ * Interface for the config object in the general application configuration database
+ * to define usage analytics settings.
+ */
+export interface UsageAnalyticsConfig {
+ /** url of the backend to report usage data to */
+ url: string;
+
+ /** the id by which this site is represented in the analytics backend */
+ site_id: string;
+
+ /** do not set any cookies for analytics */
+ no_cookies?: boolean;
+}
+
+export const USAGE_ANALYTICS_CONFIG_ID = "appConfig:usage-analytics";
diff --git a/src/app/core/app-config/app-config.model.ts b/src/app/core/app-config/app-config.model.ts
index 4b5b631481..dade0d20b5 100644
--- a/src/app/core/app-config/app-config.model.ts
+++ b/src/app/core/app-config/app-config.model.ts
@@ -64,20 +64,6 @@ export interface IAppConfig {
remote_url: string;
};
- /**
- * Optional configuration for usage analytics (e.g. Matomo)
- */
- usage_analytics?: {
- /** url of the backend to report usage data to */
- url: string;
-
- /** the id by which this site is represented in the analytics backend */
- site_id: string;
-
- /** do not set any cookies for analytics */
- no_cookies?: boolean;
- };
-
/**
* Optional flag to activate additional debugging output to troubleshoot problems on a user's device
*/
diff --git a/src/app/core/coming-soon/coming-soon/coming-soon.component.spec.ts b/src/app/core/coming-soon/coming-soon/coming-soon.component.spec.ts
index 1da1b94fcf..4a55f7e880 100644
--- a/src/app/core/coming-soon/coming-soon/coming-soon.component.spec.ts
+++ b/src/app/core/coming-soon/coming-soon/coming-soon.component.spec.ts
@@ -52,9 +52,9 @@ describe("ComingSoonComponent", () => {
);
expect(component).toBeTruthy();
- expect(mockAnalytics.eventTrack).toHaveBeenCalledWith("visit", {
+ expect(mockAnalytics.eventTrack).toHaveBeenCalledWith(testFeatureId, {
category: "feature_request",
- label: testFeatureId,
+ label: "visit",
});
});
@@ -63,9 +63,9 @@ describe("ComingSoonComponent", () => {
component.reportFeatureRequest();
- expect(mockAnalytics.eventTrack).toHaveBeenCalledWith("request", {
+ expect(mockAnalytics.eventTrack).toHaveBeenCalledWith(testFeatureId, {
category: "feature_request",
- label: testFeatureId,
+ label: "request",
});
expect(component.requested).toBeTrue();
});
diff --git a/src/app/core/coming-soon/coming-soon/coming-soon.component.ts b/src/app/core/coming-soon/coming-soon/coming-soon.component.ts
index 7c25ee6f0f..9c4cc9f9f8 100644
--- a/src/app/core/coming-soon/coming-soon/coming-soon.component.ts
+++ b/src/app/core/coming-soon/coming-soon/coming-soon.component.ts
@@ -60,9 +60,9 @@ export class ComingSoonComponent implements OnChanges {
}
private track(action: string) {
- this.analyticsService.eventTrack(action, {
+ this.analyticsService.eventTrack(this.featureId, {
category: "feature_request",
- label: this.featureId,
+ label: action,
});
}
diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts
index 3576c81813..708e74f5c3 100644
--- a/src/app/core/config/config-fix.ts
+++ b/src/app/core/config/config-fix.ts
@@ -17,6 +17,10 @@ export const defaultJsonConfig = {
"appConfig": {
displayLanguageSelect: true,
},
+ "appConfig:usage-analytics": {
+ "url": "https://matomo.aam-digital.org",
+ "site_id": "8",
+ },
"navigationMenu": {
"items": [
{
@@ -351,15 +355,6 @@ export const defaultJsonConfig = {
{
"title": "",
"component": "ChildrenOverview",
- "config": {
- "popupColumns": [
- "childId",
- "start",
- "end",
- "schoolClass",
- "result",
- ],
- }
}
]
}
diff --git a/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget.module.ts b/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget.module.ts
index 51d845a407..f62a62ed9e 100644
--- a/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget.module.ts
+++ b/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget.module.ts
@@ -4,10 +4,17 @@ import { DashboardShortcutWidgetComponent } from "./dashboard-shortcut-widget/da
import { MatCardModule } from "@angular/material/card";
import { MatIconModule } from "@angular/material/icon";
import { RouterModule } from "@angular/router";
+import { Angulartics2Module } from "angulartics2";
@NgModule({
declarations: [DashboardShortcutWidgetComponent],
- imports: [CommonModule, MatCardModule, MatIconModule, RouterModule],
+ imports: [
+ CommonModule,
+ MatCardModule,
+ MatIconModule,
+ RouterModule,
+ Angulartics2Module,
+ ],
exports: [DashboardShortcutWidgetComponent],
})
export class DashboardShortcutWidgetModule {}
diff --git a/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget/dashboard-shortcut-widget.component.html b/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget/dashboard-shortcut-widget.component.html
index a36af5c169..79453db24b 100644
--- a/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget/dashboard-shortcut-widget.component.html
+++ b/src/app/core/dashboard-shortcut-widget/dashboard-shortcut-widget/dashboard-shortcut-widget.component.html
@@ -17,6 +17,9 @@
*ngFor="let entry of shortcuts"
class="dashboard-table-row clickable"
[routerLink]="[entry.link]"
+ angulartics2On="click" angularticsCategory="Dashboard"
+ angularticsAction="shortcut-widget_click"
+ [angularticsLabel]="entry.link"
>