diff --git a/.github/workflows/pull-request-update.yml b/.github/workflows/pull-request-update.yml index 1888a9d1d7..a205fcff60 100644 --- a/.github/workflows/pull-request-update.yml +++ b/.github/workflows/pull-request-update.yml @@ -28,6 +28,19 @@ jobs: BUILD=${{ false }} cache-from: type=gha cache-to: type=gha,mode=max + - name: Run tests with timezone + uses: docker/build-push-action@v3 + with: + context: ./ + file: ./build/Dockerfile + builder: ${{ steps.buildx.outputs.name }} + target: builder + build-args: | + RUN_TESTS=${{ true }} + TZ=America/Detroit + BUILD=${{ false }} + cache-from: type=gha + cache-to: type=gha,mode=max deploy-prod-image: runs-on: ubuntu-latest steps: diff --git a/build/Dockerfile b/build/Dockerfile index b974d2043e..5407c2439a 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -3,7 +3,7 @@ # Run the following commands from the root folder to build, run and kill the application # >> docker build -f build/Dockerfile -t aam-digital . # >> docker run -p=80:80 aam-digital -FROM node:16.14.2-alpine3.15 as builder +FROM node:16.14.2-alpine3.15 AS builder WORKDIR /app COPY package*.json ./ @@ -32,6 +32,12 @@ RUN if [ "$UPLOAD_COVERAGE" = true ] ; then \ chmod +x ./cc-test-reporter &&\ ./cc-test-reporter before-build ; fi +ARG TZ +RUN if [ -n "${TZ}" ] ; then \ + apk --no-cache add tzdata && \ + cp /usr/share/zoneinfo/Europe/Brussels /etc/localtime && \ + echo "$TZ" > /etc/timezone ; fi + # When set to true, chromium is installed an tests are executed ARG RUN_TESTS=false ARG CHROME_BIN=/usr/bin/chromium-browser @@ -59,7 +65,7 @@ ARG SENTRY_ORG ARG SENTRY_PROJECT RUN if [ "$SENTRY_AUTH_TOKEN" != "" ] ; then \ npm install -g @sentry/cli &&\ - sentry-cli --auth-token=$SENTRY_AUTH_TOKEN releases --org=$SENTRY_ORG --project=$SENTRY_PROJECT files ndb-core@$APP_VERSION upload-sourcemaps dist && \ + sentry-cli --auth-token="$SENTRY_AUTH_TOKEN" releases --org="$SENTRY_ORG" --project="$SENTRY_PROJECT" files "ndb-core@$APP_VERSION" upload-sourcemaps dist && \ rm dist/*.map ; fi ### PROD image diff --git a/src/app/child-dev-project/attendance/attendance.service.spec.ts b/src/app/child-dev-project/attendance/attendance.service.spec.ts index a66a0e7989..923acaae5e 100644 --- a/src/app/child-dev-project/attendance/attendance.service.spec.ts +++ b/src/app/child-dev-project/attendance/attendance.service.spec.ts @@ -41,10 +41,10 @@ describe("AttendanceService", () => { activity1 = RecurringActivity.create("activity 1"); activity2 = RecurringActivity.create("activity 2"); - e1_1 = createEvent(new Date("2020-01-01"), activity1.getId(true)); - e1_2 = createEvent(new Date("2020-01-02"), activity1.getId(true)); - e1_3 = createEvent(new Date("2020-03-02"), activity1.getId(true)); - e2_1 = createEvent(new Date("2020-01-01"), activity2.getId(true)); + e1_1 = createEvent(moment("2020-01-01").toDate(), activity1.getId(true)); + e1_2 = createEvent(moment("2020-01-02").toDate(), activity1.getId(true)); + e1_3 = createEvent(moment("2020-03-02").toDate(), activity1.getId(true)); + e2_1 = createEvent(moment("2020-01-01").toDate(), activity2.getId(true)); TestBed.configureTestingModule({ imports: [DatabaseTestingModule], @@ -69,24 +69,32 @@ describe("AttendanceService", () => { }); it("gets events for a date", async () => { - const actualEvents = await service.getEventsOnDate(new Date("2020-01-01")); + const actualEvents = await service.getEventsOnDate( + moment("2020-01-01").toDate(), + ); expectEntitiesToMatch(actualEvents, [e1_1, e2_1]); }); it("gets events including Notes for a date", async () => { - const note1 = Note.create(new Date("2020-01-01"), "manual event note 1"); + const note1 = Note.create( + moment("2020-01-01").toDate(), + "manual event note 1", + ); note1.addChild("1"); note1.addChild("2"); note1.category = meetingInteractionCategory; await entityMapper.save(note1); - const note2 = Note.create(new Date("2020-01-02"), "manual event note 2"); + const note2 = Note.create( + moment("2020-01-02").toDate(), + "manual event note 2", + ); note2.addChild("1"); note2.category = meetingInteractionCategory; await entityMapper.save(note2); const nonMeetingNote = Note.create( - new Date("2020-01-02"), + moment("2020-01-02").toDate(), "manual event note 3", ); nonMeetingNote.addChild("1"); @@ -94,15 +102,17 @@ describe("AttendanceService", () => { await entityMapper.save(nonMeetingNote); const actualEvents = await service.getEventsOnDate( - new Date("2020-01-01"), - new Date("2020-01-02"), + moment("2020-01-01").toDate(), + moment("2020-01-02").toDate(), ); expectEntitiesToMatch(actualEvents, [e1_1, e1_2, e2_1, note1, note2]); }); it("gets empty array for a date without events", async () => { - const actualEvents = await service.getEventsOnDate(new Date("2007-01-01")); + const actualEvents = await service.getEventsOnDate( + moment("2007-01-01").toDate(), + ); expect(actualEvents).toBeEmpty(); }); @@ -210,7 +220,7 @@ describe("AttendanceService", () => { const childSchoolRelation = new ChildSchoolRelation(); childSchoolRelation.childId = "testChild"; childSchoolRelation.schoolId = "testSchool"; - childSchoolRelation.start = new Date("2020-01-01"); + childSchoolRelation.start = moment("2020-01-01").toDate(); const testActivity = RecurringActivity.create("new activity"); testActivity.linkedGroups.push("testSchool"); @@ -329,10 +339,10 @@ describe("AttendanceService", () => { it("should load the events for a date with date-picker format", async () => { const datePickerDate = new Date( - new Date("2021-04-05").setHours(0, 0, 0, 0), + moment("2021-04-05").toDate().setHours(0, 0, 0, 0), ); const sameDayEvent = EventNote.create( - new Date("2021-04-05"), + moment("2021-04-05").toDate(), "Same Day Event", ); sameDayEvent.category = meetingInteractionCategory; diff --git a/src/app/child-dev-project/attendance/attendance.service.ts b/src/app/child-dev-project/attendance/attendance.service.ts index fe21b9c1ba..9b289f2970 100644 --- a/src/app/child-dev-project/attendance/attendance.service.ts +++ b/src/app/child-dev-project/attendance/attendance.service.ts @@ -32,9 +32,13 @@ export class AttendanceService { by_date: { map: `(doc) => { if (doc._id.startsWith("${EventNote.ENTITY_TYPE}")) { - var d = new Date(doc.date || null); - var dString = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0") - emit(dString); + if (doc.date && doc.date.length === 10) { + emit(doc.date); + } else { + var d = new Date(doc.date || null); + var dString = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); + emit(dString); + } } }`, }, @@ -42,8 +46,13 @@ export class AttendanceService { by_activity: { map: `(doc) => { if (doc._id.startsWith("${EventNote.ENTITY_TYPE}") && doc.relatesTo) { - var d = new Date(doc.date || null); - var dString = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0") + var dString; + if (doc.date && doc.date.length === 10) { + dString = doc.date; + } else { + var d = new Date(doc.date || null); + dString = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); + } emit(doc.relatesTo + "_" + dString); } }`, diff --git a/src/app/child-dev-project/children/children.service.spec.ts b/src/app/child-dev-project/children/children.service.spec.ts index 92565d43ed..371750d700 100644 --- a/src/app/child-dev-project/children/children.service.spec.ts +++ b/src/app/child-dev-project/children/children.service.spec.ts @@ -269,6 +269,22 @@ describe("ChildrenService", () => { res = await service.getNotesRelatedTo(s1.getId(true)); expect(res).toEqual([n1]); }); + + it("should return the correct notes in a timespan", async () => { + const n1 = Note.create(moment("2023-01-01").toDate()); + const n2 = Note.create(moment("2023-01-02").toDate()); + const n3 = Note.create(moment("2023-01-03").toDate()); + const n4 = Note.create(moment("2023-01-03").toDate()); + const n5 = Note.create(moment("2023-01-04").toDate()); + await entityMapper.saveAll([n1, n2, n3, n4, n5]); + + const res = await service.getNotesInTimespan( + moment("2023-01-02"), + moment("2023-01-03"), + ); + + expect(res).toEqual(jasmine.arrayWithExactContents([n2, n3, n4])); + }); }); function generateChildEntities(): Child[] { diff --git a/src/app/child-dev-project/children/children.service.ts b/src/app/child-dev-project/children/children.service.ts index cbad2d6b26..89a281a693 100644 --- a/src/app/child-dev-project/children/children.service.ts +++ b/src/app/child-dev-project/children/children.service.ts @@ -222,9 +222,12 @@ export class ChildrenService { map: `(doc) => { if (!doc._id.startsWith("${Note.ENTITY_TYPE}")) return; if (!Array.isArray(doc.children) || !doc.date) return; - var d = new Date(doc.date || null); - var dString = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0") - emit(dString); + if (doc.date.length === 10) { + emit(doc.date); + } else { + var d = new Date(doc.date || null); + emit(d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0")); + } }`, }, }, @@ -246,12 +249,15 @@ export class ChildrenService { map: `(doc) => { if (!doc._id.startsWith("${Note.ENTITY_TYPE}")) return; if (!Array.isArray(doc.relatedEntities)) return; - - var d = new Date(doc.date || null); - var dateString = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0") - + var dString; + if (doc.date && doc.date.length === 10) { + dString = doc.date; + } else { + var d = new Date(doc.date || null); + dString = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); + } doc.relatedEntities.forEach((relatedEntity) => { - emit([relatedEntity, dateString]); + emit([relatedEntity, dString]); }); }`, }, diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts index 44ca08bc13..b63dc2ae5f 100644 --- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts +++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter-panel/date-range-filter-panel.component.spec.ts @@ -23,7 +23,7 @@ describe("DateRangeFilterPanelComponent", () => { beforeEach(async () => { dateFilter = new DateFilter("test", "Test", defaultDateFilters); dateFilter.selectedOption = "1"; - jasmine.clock().mockDate(new Date("2023-04-08")); + jasmine.clock().mockDate(moment("2023-04-08").toDate()); await TestBed.configureTestingModule({ imports: [MatNativeDateModule], providers: [ @@ -73,8 +73,8 @@ describe("DateRangeFilterPanelComponent", () => { await cells[12].select(); const filterRange = dateFilter.getDateRange(); - expect(filterRange.start).toEqual(new Date("2023-04-08")); - expect(filterRange.end).toEqual(new Date("2023-04-13")); + expect(filterRange.start).toEqual(moment("2023-04-08").toDate()); + expect(filterRange.end).toEqual(moment("2023-04-13").toDate()); }); it("should set the dates selected via the preset options", async () => { diff --git a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts index 03fb26ac5d..69b266e69c 100644 --- a/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts +++ b/src/app/core/basic-datatypes/date/date-range-filter/date-range-filter.component.spec.ts @@ -6,6 +6,7 @@ import { MatNativeDateModule } from "@angular/material/core"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { DateFilter } from "../../../filter/filters/filters"; import { defaultDateFilters } from "./date-range-filter-panel/date-range-filter-panel.component"; +import moment from "moment"; describe("DateRangeFilterComponent", () => { let component: DateRangeFilterComponent; @@ -33,7 +34,7 @@ describe("DateRangeFilterComponent", () => { component.filterConfig = dateFilter; expect(component.dateFilter.getFilter()).toEqual({}); - jasmine.clock().mockDate(new Date("2023-05-18")); + jasmine.clock().mockDate(moment("2023-05-18").toDate()); dateFilter.selectedOption = "0"; component.filterConfig = dateFilter; let expectedDataFilter = { @@ -101,8 +102,8 @@ describe("DateRangeFilterComponent", () => { it("should set the correct date filter when changing the date range manually", () => { component.filterConfig = new DateFilter("test", "test", []); - component.fromDate = new Date("2021-10-28"); - component.toDate = new Date("2024-02-12"); + component.fromDate = moment("2021-10-28").toDate(); + component.toDate = moment("2024-02-12").toDate(); component.dateChangedManually(); diff --git a/src/app/core/basic-datatypes/map/map.datatype.spec.ts b/src/app/core/basic-datatypes/map/map.datatype.spec.ts index e24a2947f4..b811a6f6b9 100644 --- a/src/app/core/basic-datatypes/map/map.datatype.spec.ts +++ b/src/app/core/basic-datatypes/map/map.datatype.spec.ts @@ -20,6 +20,7 @@ import { TestBed, waitForAsync } from "@angular/core/testing"; import { DatabaseField } from "../../entity/database-field.decorator"; import { EntitySchemaService } from "../../entity/schema/entity-schema.service"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; +import moment from "moment"; describe("Schema data type: map", () => { class TestEntity extends Entity { @@ -39,8 +40,8 @@ describe("Schema data type: map", () => { it("converts contained dates to month for saving", () => { const id = "test1"; const entity = new TestEntity(id); - entity.dateMap.set("a", new Date("2020-01-01")); - entity.dateMap.set("b", new Date("1999-01-25")); + entity.dateMap.set("a", moment("2020-01-01").toDate()); + entity.dateMap.set("b", moment("1999-01-25").toDate()); const rawData = entitySchemaService.transformEntityToDatabaseFormat(entity); diff --git a/src/app/core/basic-datatypes/schema-embed/schema-embed.datatype.spec.ts b/src/app/core/basic-datatypes/schema-embed/schema-embed.datatype.spec.ts index bccebe1bdc..530b7b2b5e 100644 --- a/src/app/core/basic-datatypes/schema-embed/schema-embed.datatype.spec.ts +++ b/src/app/core/basic-datatypes/schema-embed/schema-embed.datatype.spec.ts @@ -20,6 +20,7 @@ import { DatabaseField } from "../../entity/database-field.decorator"; import { EntitySchemaService } from "../../entity/schema/entity-schema.service"; import { TestBed, waitForAsync } from "@angular/core/testing"; import { MockedTestingModule } from "../../../utils/mocked-testing.module"; +import moment from "moment"; describe("Schema data type: schema-embed", () => { class InnerClass { @@ -55,7 +56,7 @@ describe("Schema data type: schema-embed", () => { it("applies inner schema transformation for database format", () => { const entity = new TestEntity(); - entity.embedded.value = new Date("2020-01-01"); + entity.embedded.value = moment("2020-01-01").toDate(); const rawData = entitySchemaService.transformEntityToDatabaseFormat(entity); expect(rawData.embedded.value).toEqual("2020-01"); diff --git a/src/app/core/export/download-service/download.service.spec.ts b/src/app/core/export/download-service/download.service.spec.ts index 916e430256..9ae04ea0ce 100644 --- a/src/app/core/export/download-service/download.service.spec.ts +++ b/src/app/core/export/download-service/download.service.spec.ts @@ -7,6 +7,7 @@ import { DatabaseEntity } from "../../entity/database-entity.decorator"; import { Entity } from "../../entity/model/entity"; import { ConfigurableEnumValue } from "../../basic-datatypes/configurable-enum/configurable-enum.interface"; import { DatabaseField } from "../../entity/database-field.decorator"; +import moment from "moment"; describe("DownloadService", () => { let service: DownloadService; @@ -96,7 +97,7 @@ describe("DownloadService", () => { const testEntity = new TestEntity(); testEntity.enumProperty = testEnumValue; - testEntity.dateProperty = new Date(testDate); + testEntity.dateProperty = moment(testDate).toDate(); testEntity.boolProperty = true; const csvExport = await service.createCsv([testEntity]); @@ -112,7 +113,7 @@ describe("DownloadService", () => { it("should export a date as YYYY-MM-dd only", async () => { const dateString = "2021-01-01"; - const dateObject = new Date(dateString); + const dateObject = moment(dateString).toDate(); dateObject.setHours(10, 11, 12); const exportData = [ diff --git a/src/app/core/filter/filter.service.spec.ts b/src/app/core/filter/filter.service.spec.ts index a06f68193a..3e00bf7cbd 100644 --- a/src/app/core/filter/filter.service.spec.ts +++ b/src/app/core/filter/filter.service.spec.ts @@ -6,6 +6,7 @@ import { DataFilter } from "../common-components/entity-subrecord/entity-subreco import { Note } from "../../child-dev-project/notes/model/note"; import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum.service"; import { createTestingConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum-testing"; +import moment from "moment"; describe("FilterService", () => { let service: FilterService; @@ -67,11 +68,11 @@ describe("FilterService", () => { }); it("should support filtering dates with day granularity", () => { - const n1 = Note.create(new Date("2022-01-01")); - const n2 = Note.create(new Date("2022-01-02")); - const n3 = Note.create(new Date("2022-01-03")); - const n4 = Note.create(new Date("2022-01-04")); - const n5 = Note.create(new Date("2022-01-05")); + const n1 = Note.create(moment("2022-01-01").toDate()); + const n2 = Note.create(moment("2022-01-02").toDate()); + const n3 = Note.create(moment("2022-01-03").toDate()); + const n4 = Note.create(moment("2022-01-04").toDate()); + const n5 = Note.create(moment("2022-01-05").toDate()); const notes = [n1, n2, n3, n4, n5]; let predicate = service.getFilterPredicate({ diff --git a/src/app/core/filter/filter.service.ts b/src/app/core/filter/filter.service.ts index b4db1c5201..8f58997b93 100644 --- a/src/app/core/filter/filter.service.ts +++ b/src/app/core/filter/filter.service.ts @@ -3,11 +3,11 @@ import { EntitySchemaField } from "../entity/schema/entity-schema-field"; import { DataFilter } from "../common-components/entity-subrecord/entity-subrecord/entity-subrecord-config"; import { Entity } from "../entity/model/entity"; import { - createFactory, - allParsingInstructions, allInterpreters, - Filter, + allParsingInstructions, compare, + createFactory, + Filter, } from "@ucast/mongo2js"; import moment from "moment"; import { ConfigurableEnumService } from "../basic-datatypes/configurable-enum/configurable-enum.service"; @@ -72,7 +72,7 @@ export class FilterService { value = this.parseConfigurableEnumValue(property, value); } if (property.dataType.includes("date")) { - value = new Date(value); + value = moment(value).toDate(); } newEntity[key] = value; } diff --git a/src/app/core/filter/filters/filters.ts b/src/app/core/filter/filters/filters.ts index 5195388211..ab60a04953 100644 --- a/src/app/core/filter/filters/filters.ts +++ b/src/app/core/filter/filters/filters.ts @@ -61,8 +61,8 @@ export class DateFilter extends Filter { } const dates = this.selectedOption?.split("_"); if (dates?.length == 2) { - const firstDate = new Date(dates[0]); - const secondDate = new Date(dates[1]); + const firstDate = moment(dates[0]).toDate(); + const secondDate = moment(dates[1]).toDate(); return new DateRange( isValidDate(firstDate) ? firstDate : undefined, isValidDate(secondDate) ? secondDate : undefined,