From 8102a1ff5684d59547b257af49e963aa23aac9a7 Mon Sep 17 00:00:00 2001 From: Joosa Kurvinen Date: Mon, 3 Feb 2025 14:43:22 +0200 Subject: [PATCH 1/4] data model and unit page changes, scheduled removal --- .../UnitAccessControl.tsx | 2 + .../acl-modals/AddAclModal.tsx | 25 +++++- .../acl-modals/EditAclModal.tsx | 25 +++++- .../generated/api-clients/daycare.ts | 3 +- .../lib-common/generated/api-types/daycare.ts | 17 ++++ .../lib-common/generated/api-types/shared.ts | 10 +++ .../defaults/employee/i18n/fi.tsx | 1 + .../daycare/controllers/UnitAclController.kt | 86 ++++++++++++------- .../fi/espoo/evaka/shared/auth/AclQueries.kt | 64 ++++++++++---- .../evaka/shared/dev/DataInitializers.kt | 3 +- .../espoo/evaka/shared/job/ScheduledJobs.kt | 23 +++++ .../db/migration/V493__acl_end_date.sql | 1 + service/src/main/resources/migrations.txt | 1 + 13 files changed, 205 insertions(+), 56 deletions(-) create mode 100644 service/src/main/resources/db/migration/V493__acl_end_date.sql diff --git a/frontend/src/employee-frontend/components/unit/tab-unit-information/UnitAccessControl.tsx b/frontend/src/employee-frontend/components/unit/tab-unit-information/UnitAccessControl.tsx index 2656fec6575..beea917cfb2 100755 --- a/frontend/src/employee-frontend/components/unit/tab-unit-information/UnitAccessControl.tsx +++ b/frontend/src/employee-frontend/components/unit/tab-unit-information/UnitAccessControl.tsx @@ -175,6 +175,7 @@ function AclRow({ )} + {row.endDate?.format()} {isEditable && ( @@ -288,6 +289,7 @@ function AclTable({ {i18n.unit.accessControl.role} {i18n.common.form.name} {unitGroups && {i18n.unit.accessControl.groups}} + {i18n.unit.accessControl.aclEndDate} diff --git a/frontend/src/employee-frontend/components/unit/tab-unit-information/acl-modals/AddAclModal.tsx b/frontend/src/employee-frontend/components/unit/tab-unit-information/acl-modals/AddAclModal.tsx index f9c1f912ab2..5d584f1db75 100644 --- a/frontend/src/employee-frontend/components/unit/tab-unit-information/acl-modals/AddAclModal.tsx +++ b/frontend/src/employee-frontend/components/unit/tab-unit-information/acl-modals/AddAclModal.tsx @@ -9,11 +9,13 @@ import { Action } from 'lib-common/generated/action' import { DaycareGroupResponse } from 'lib-common/generated/api-types/daycare' import { Employee } from 'lib-common/generated/api-types/pis' import { DaycareId, EmployeeId } from 'lib-common/generated/api-types/shared' +import LocalDate from 'lib-common/local-date' import { cancelMutation } from 'lib-components/atoms/buttons/MutateButton' import Combobox from 'lib-components/atoms/dropdowns/Combobox' import Checkbox from 'lib-components/atoms/form/Checkbox' import MultiSelect from 'lib-components/atoms/form/MultiSelect' import { FixedSpaceColumn } from 'lib-components/layout/flex-helpers' +import DatePicker from 'lib-components/molecules/date-picker/DatePicker' import { MutateFormModal } from 'lib-components/molecules/modals/FormModal' import { Label } from 'lib-components/typography' @@ -41,6 +43,7 @@ type FormState = { selectedEmployee: EmployeeOption | null selectedGroups: DaycareGroupResponse[] | null hasStaffOccupancyEffect: boolean | null + endDate: LocalDate | null } export default React.memo(function AddAclModal({ @@ -50,7 +53,7 @@ export default React.memo(function AddAclModal({ employees, permittedActions }: Props) { - const { i18n } = useTranslation() + const { i18n, lang } = useTranslation() const [formData, setFormData] = useState({ role: 'STAFF', @@ -60,7 +63,8 @@ export default React.memo(function AddAclModal({ 'UPSERT_STAFF_OCCUPANCY_COEFFICIENTS' ) ? false - : null + : null, + endDate: null }) const roles: DaycareAclRole[] = useMemo( @@ -118,7 +122,8 @@ export default React.memo(function AddAclModal({ 'UPSERT_STAFF_OCCUPANCY_COEFFICIENTS' ) ? formData.hasStaffOccupancyEffect - : null + : null, + endDate: formData.endDate } } } @@ -185,6 +190,7 @@ export default React.memo(function AddAclModal({ /> )} + {permittedActions.includes('UPSERT_STAFF_OCCUPANCY_COEFFICIENTS') && ( )} + + + + + setFormData((prev) => ({ ...prev, endDate: date })) + } + locale={lang} + minDate={LocalDate.todayInHelsinkiTz()} + /> + ) diff --git a/frontend/src/employee-frontend/components/unit/tab-unit-information/acl-modals/EditAclModal.tsx b/frontend/src/employee-frontend/components/unit/tab-unit-information/acl-modals/EditAclModal.tsx index a2f1e9c74a4..17acdf28aac 100644 --- a/frontend/src/employee-frontend/components/unit/tab-unit-information/acl-modals/EditAclModal.tsx +++ b/frontend/src/employee-frontend/components/unit/tab-unit-information/acl-modals/EditAclModal.tsx @@ -7,9 +7,11 @@ import React, { useState } from 'react' import { Action } from 'lib-common/generated/action' import { DaycareGroupResponse } from 'lib-common/generated/api-types/daycare' import { DaycareAclRow, DaycareId } from 'lib-common/generated/api-types/shared' +import LocalDate from 'lib-common/local-date' import Checkbox from 'lib-components/atoms/form/Checkbox' import MultiSelect from 'lib-components/atoms/form/MultiSelect' import { FixedSpaceColumn } from 'lib-components/layout/flex-helpers' +import DatePicker from 'lib-components/molecules/date-picker/DatePicker' import { MutateFormModal } from 'lib-components/molecules/modals/FormModal' import { Label } from 'lib-components/typography' @@ -30,6 +32,7 @@ interface Props { type FormState = { selectedGroups: DaycareGroupResponse[] | null hasStaffOccupancyEffect: boolean | null + endDate: LocalDate | null } export default React.memo(function EditAclModal({ @@ -39,7 +42,7 @@ export default React.memo(function EditAclModal({ groups, row }: Props) { - const { i18n } = useTranslation() + const { i18n, lang } = useTranslation() const { employee, role } = row const groupOptions = useGroupOptions(groups) @@ -51,7 +54,8 @@ export default React.memo(function EditAclModal({ 'UPSERT_STAFF_OCCUPANCY_COEFFICIENTS' ) ? employee.hasStaffOccupancyEffect - : null + : null, + endDate: row.endDate }) return ( @@ -63,7 +67,8 @@ export default React.memo(function EditAclModal({ employeeId: row.employee.id, body: { groupIds: formData.selectedGroups?.map(({ id }) => id) ?? null, - hasStaffOccupancyEffect: formData.hasStaffOccupancyEffect + hasStaffOccupancyEffect: formData.hasStaffOccupancyEffect, + endDate: formData.endDate } })} resolveLabel={i18n.common.save} @@ -99,6 +104,7 @@ export default React.memo(function EditAclModal({ /> )} + {permittedActions.includes('READ_STAFF_OCCUPANCY_COEFFICIENTS') && ( )} + + + + + setFormData((prev) => ({ ...prev, endDate: date })) + } + locale={lang} + minDate={LocalDate.todayInHelsinkiTz()} + /> + ) diff --git a/frontend/src/employee-frontend/generated/api-clients/daycare.ts b/frontend/src/employee-frontend/generated/api-clients/daycare.ts index 2a722315407..0d6d9385d0d 100644 --- a/frontend/src/employee-frontend/generated/api-clients/daycare.ts +++ b/frontend/src/employee-frontend/generated/api-clients/daycare.ts @@ -54,6 +54,7 @@ import { deserializeJsonCaretakersResponse } from 'lib-common/generated/api-type import { deserializeJsonChildResponse } from 'lib-common/generated/api-types/daycare' import { deserializeJsonClubTerm } from 'lib-common/generated/api-types/daycare' import { deserializeJsonDaycare } from 'lib-common/generated/api-types/daycare' +import { deserializeJsonDaycareAclRow } from 'lib-common/generated/api-types/shared' import { deserializeJsonDaycareGroup } from 'lib-common/generated/api-types/daycare' import { deserializeJsonDaycareResponse } from 'lib-common/generated/api-types/daycare' import { deserializeJsonEmployee } from 'lib-common/generated/api-types/pis' @@ -853,7 +854,7 @@ export async function getDaycareAcl( url: uri`/employee/daycares/${request.unitId}/acl`.toString(), method: 'GET' }) - return json + return json.map(e => deserializeJsonDaycareAclRow(e)) } diff --git a/frontend/src/lib-common/generated/api-types/daycare.ts b/frontend/src/lib-common/generated/api-types/daycare.ts index decac4139ab..c784737111a 100644 --- a/frontend/src/lib-common/generated/api-types/daycare.ts +++ b/frontend/src/lib-common/generated/api-types/daycare.ts @@ -46,6 +46,7 @@ import { deserializeJsonUnitBackupCare } from './backupcare' * Generated from fi.espoo.evaka.daycare.controllers.UnitAclController.AclUpdate */ export interface AclUpdate { + endDate: LocalDate | null groupIds: GroupId[] | null hasStaffOccupancyEffect: boolean | null } @@ -570,6 +571,14 @@ export interface VisitingAddress { } +export function deserializeJsonAclUpdate(json: JsonOf): AclUpdate { + return { + ...json, + endDate: (json.endDate != null) ? LocalDate.parseIso(json.endDate) : null + } +} + + export function deserializeJsonCaretakerAmount(json: JsonOf): CaretakerAmount { return { ...json, @@ -705,6 +714,14 @@ export function deserializeJsonDaycareResponse(json: JsonOf): D } +export function deserializeJsonFullAclInfo(json: JsonOf): FullAclInfo { + return { + ...json, + update: deserializeJsonAclUpdate(json.update) + } +} + + export function deserializeJsonGroupOccupancies(json: JsonOf): GroupOccupancies { return { ...json, diff --git a/frontend/src/lib-common/generated/api-types/shared.ts b/frontend/src/lib-common/generated/api-types/shared.ts index 78487f2df5d..e0a9eab534a 100644 --- a/frontend/src/lib-common/generated/api-types/shared.ts +++ b/frontend/src/lib-common/generated/api-types/shared.ts @@ -5,6 +5,7 @@ // GENERATED FILE: no manual modifications import HelsinkiDateTime from '../../helsinki-date-time' +import LocalDate from '../../local-date' import { Id } from '../../id-type' import { JsonOf } from '../../json' @@ -84,6 +85,7 @@ export type DailyServiceTimeNotificationId = Id<'DailyServiceTimeNotification'> */ export interface DaycareAclRow { employee: DaycareAclRowEmployee + endDate: LocalDate | null groupIds: GroupId[] role: UserRole } @@ -304,6 +306,14 @@ export type VoucherValueDecisionId = Id<'VoucherValueDecision'> export type ChildId = PersonId +export function deserializeJsonDaycareAclRow(json: JsonOf): DaycareAclRow { + return { + ...json, + endDate: (json.endDate != null) ? LocalDate.parseIso(json.endDate) : null + } +} + + export function deserializeJsonHelsinkiDateTimeRange(json: JsonOf): HelsinkiDateTimeRange { return { ...json, diff --git a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx index fa225aa9722..abb073a9e02 100755 --- a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx +++ b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx @@ -2163,6 +2163,7 @@ export const fi = { role: 'Rooli', name: 'Nimi', email: 'Sähköpostiosoite', + aclEndDate: 'Luvitus päättyy', removeConfirmation: 'Haluatko poistaa pääsyoikeuden valitulta henkilöltä?', addDaycareAclModal: { diff --git a/service/src/main/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclController.kt b/service/src/main/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclController.kt index 79fa185460a..2226787a889 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclController.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclController.kt @@ -36,6 +36,7 @@ import fi.espoo.evaka.shared.domain.* import fi.espoo.evaka.shared.security.AccessControl import fi.espoo.evaka.shared.security.Action import java.math.BigDecimal +import java.time.LocalDate import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -118,6 +119,7 @@ class UnitAclController( validateIsPermanentEmployee(it, employeeId) removeDaycareAclForRole( it, + asyncJobRunner, clock.now(), unitId, employeeId, @@ -148,6 +150,7 @@ class UnitAclController( validateIsPermanentEmployee(it, employeeId) removeDaycareAclForRole( it, + asyncJobRunner, clock.now(), unitId, employeeId, @@ -178,6 +181,7 @@ class UnitAclController( validateIsPermanentEmployee(it, employeeId) removeDaycareAclForRole( it, + asyncJobRunner, clock.now(), unitId, employeeId, @@ -206,7 +210,14 @@ class UnitAclController( unitId, ) validateIsPermanentEmployee(it, employeeId) - removeDaycareAclForRole(it, clock.now(), unitId, employeeId, UserRole.STAFF) + removeDaycareAclForRole( + it, + asyncJobRunner, + clock.now(), + unitId, + employeeId, + UserRole.STAFF, + ) } } Audit.UnitAclDelete.log(targetId = AuditId(unitId), objectId = AuditId(employeeId)) @@ -227,15 +238,21 @@ class UnitAclController( val occupancyCoefficientId = db.connect { dbc -> dbc.transaction { tx -> + accessControl.requirePermissionFor( + tx, + user, + clock, + Action.Unit.UPDATE_STAFF_GROUP_ACL, + unitId, + ) + validateIsPermanentEmployee(tx, employeeId) + + if (update.endDate?.isBefore(clock.today()) == true) { + throw BadRequest("End date cannot be in the past") + } + tx.updateAclRowEndDate(unitId, employeeId, update.endDate) + update.groupIds?.let { - accessControl.requirePermissionFor( - tx, - user, - clock, - Action.Unit.UPDATE_STAFF_GROUP_ACL, - unitId, - ) - validateIsPermanentEmployee(tx, employeeId) tx.syncDaycareGroupAcl(unitId, employeeId, it, clock.now()) } @@ -286,7 +303,7 @@ class UnitAclController( val roleAction = getRoleAddAction(aclInfo.role) accessControl.requirePermissionFor(tx, user, clock, roleAction, unitId) validateIsPermanentEmployee(tx, employeeId) - tx.insertDaycareAclRow(unitId, employeeId, aclInfo.role) + tx.insertDaycareAclRow(unitId, employeeId, aclInfo.role, aclInfo.update.endDate) tx.upsertEmployeeMessageAccount(employeeId) aclInfo.update.groupIds?.let { accessControl.requirePermissionFor( @@ -332,7 +349,11 @@ class UnitAclController( data class FullAclInfo(val role: UserRole, val update: AclUpdate) - data class AclUpdate(val groupIds: List?, val hasStaffOccupancyEffect: Boolean?) + data class AclUpdate( + val groupIds: List?, + val hasStaffOccupancyEffect: Boolean?, + val endDate: LocalDate?, + ) fun getRoleAddAction(role: UserRole): Action.Unit = when (role) { @@ -505,7 +526,7 @@ class UnitAclController( Action.Unit.UPDATE_TEMPORARY_EMPLOYEE, unitId, ) - tx.insertDaycareAclRow(unitId, employeeId, UserRole.STAFF) + tx.insertDaycareAclRow(unitId, employeeId, UserRole.STAFF, endDate = null) tx.upsertEmployeeMessageAccount(employeeId) } } @@ -584,7 +605,7 @@ class UnitAclController( throw Forbidden("All groups must be in unit") } - tx.insertDaycareAclRow(unitId, employeeId, UserRole.STAFF) + tx.insertDaycareAclRow(unitId, employeeId, UserRole.STAFF, endDate = null) tx.syncDaycareGroupAcl(unitId, employeeId, input.groupIds, now) tx.upsertEmployeeMessageAccount(employeeId) @@ -652,24 +673,25 @@ class UnitAclController( } } } +} - fun removeDaycareAclForRole( - tx: Database.Transaction, - now: HelsinkiDateTime, - unitId: DaycareId, - employeeId: EmployeeId, - role: UserRole, - ) { - tx.syncDaycareGroupAcl(unitId, employeeId, emptyList()) - tx.deleteDaycareAclRow(unitId, employeeId, role) - deactivatePersonalMessageAccountIfNeeded(tx, employeeId) - - // Delete personal mobile devices after a while, in case the employee is added back to this - // or some other unit - asyncJobRunner.plan( - tx, - listOf(AsyncJob.DeletePersonalDevicesIfNeeded(employeeId)), - runAt = now.plusHours(1), - ) - } +fun removeDaycareAclForRole( + tx: Database.Transaction, + asyncJobRunner: AsyncJobRunner, + now: HelsinkiDateTime, + unitId: DaycareId, + employeeId: EmployeeId, + role: UserRole, +) { + tx.syncDaycareGroupAcl(unitId, employeeId, emptyList()) + tx.deleteDaycareAclRow(unitId, employeeId, role) + deactivatePersonalMessageAccountIfNeeded(tx, employeeId) + + // Delete personal mobile devices after a while, in case the employee is added back to this + // or some other unit + asyncJobRunner.plan( + tx, + listOf(AsyncJob.DeletePersonalDevicesIfNeeded(employeeId)), + runAt = now.plusHours(1), + ) } diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AclQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AclQueries.kt index 9af4a08341a..107d73aba98 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AclQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AclQueries.kt @@ -9,12 +9,14 @@ import fi.espoo.evaka.shared.EmployeeId import fi.espoo.evaka.shared.GroupId import fi.espoo.evaka.shared.db.Database import fi.espoo.evaka.shared.domain.HelsinkiDateTime +import java.time.LocalDate import org.jdbi.v3.core.mapper.Nested data class DaycareAclRow( @Nested val employee: DaycareAclRowEmployee, val role: UserRole, val groupIds: List, + val endDate: LocalDate?, ) data class DaycareAclRowEmployee( @@ -46,7 +48,8 @@ SELECT e.id, CASE WHEN (${bind(includeStaffOccupancy)} IS TRUE) THEN (soc.coefficient IS NOT NULL and soc.coefficient > 0) - ELSE NULL END as hasStaffOccupancyEffect + ELSE NULL END as hasStaffOccupancyEffect, + end_date FROM daycare_acl JOIN employee e on daycare_acl.employee_id = e.id LEFT JOIN (SELECT daycare_id, employee_id, array_agg(dg.id) AS group_ids @@ -77,34 +80,63 @@ fun Database.Transaction.insertDaycareAclRow( daycareId: DaycareId, employeeId: EmployeeId, role: UserRole, -) = - createUpdate { - sql( - """ -INSERT INTO daycare_acl (daycare_id, employee_id, role) -VALUES (${bind(daycareId)}, ${bind(employeeId)}, ${bind(role)}) -ON CONFLICT (daycare_id, employee_id) DO UPDATE SET role = excluded.role + endDate: LocalDate?, +) = execute { + sql( + """ +INSERT INTO daycare_acl (daycare_id, employee_id, role, end_date) +VALUES (${bind(daycareId)}, ${bind(employeeId)}, ${bind(role)}, ${bind(endDate)}) +ON CONFLICT (daycare_id, employee_id) DO UPDATE SET role = excluded.role, end_date = excluded.end_date """ - ) - } - .execute() + ) +} + +fun Database.Transaction.updateAclRowEndDate( + daycareId: DaycareId, + employeeId: EmployeeId, + endDate: LocalDate?, +) = execute { + sql( + """ +UPDATE daycare_acl +SET end_date = ${bind(endDate)} +WHERE daycare_id = ${bind(daycareId)} AND employee_id = ${bind(employeeId)} + """ + ) +} fun Database.Transaction.deleteDaycareAclRow( daycareId: DaycareId, employeeId: EmployeeId, role: UserRole, -) = - createUpdate { - sql( - """ +) = execute { + sql( + """ DELETE FROM daycare_acl WHERE daycare_id = ${bind(daycareId)} AND employee_id = ${bind(employeeId)} AND role = ${bind(role)} + """ + ) +} + +data class EndedDaycareAclRow( + val daycareId: DaycareId, + val employeeId: EmployeeId, + val role: UserRole, +) + +fun Database.Read.getEndedDaycareAclRows(today: LocalDate) = + createQuery { + sql( + """ +SELECT daycare_id, employee_id, role +FROM daycare_acl +WHERE end_date IS NOT NULL AND end_date < ${bind(today)} """ ) } - .execute() + .toList() fun Database.Transaction.syncDaycareGroupAcl( daycareId: DaycareId, diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DataInitializers.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DataInitializers.kt index 7e09d6f479c..dd9d57b6452 100755 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DataInitializers.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DataInitializers.kt @@ -272,6 +272,7 @@ fun Database.Transaction.insert( row: DevEmployee, unitRoles: Map = mapOf(), groupAcl: Map> = mapOf(), + endDate: LocalDate? = null, now: HelsinkiDateTime = HelsinkiDateTime.now(), ) = createUpdate { @@ -288,7 +289,7 @@ RETURNING id .also { employeeId -> upsertEmployeeUser(employeeId) unitRoles.forEach { (daycareId, role) -> - insertDaycareAclRow(daycareId, employeeId, role) + insertDaycareAclRow(daycareId, employeeId, role, endDate) } groupAcl.forEach { (daycareId, groups) -> syncDaycareGroupAcl(daycareId, employeeId, groups, now) diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/job/ScheduledJobs.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/job/ScheduledJobs.kt index bd3dbb98e5a..57a5bb2942d 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/job/ScheduledJobs.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/job/ScheduledJobs.kt @@ -17,6 +17,7 @@ import fi.espoo.evaka.assistanceneed.vouchercoefficient.endOutdatedAssistanceNee import fi.espoo.evaka.attachment.AttachmentService import fi.espoo.evaka.attendance.addMissingStaffAttendanceDepartures import fi.espoo.evaka.calendarevent.CalendarEventNotificationService +import fi.espoo.evaka.daycare.controllers.removeDaycareAclForRole import fi.espoo.evaka.document.childdocument.ChildDocumentService import fi.espoo.evaka.dvv.DvvModificationsBatchRefreshService import fi.espoo.evaka.invoicing.service.FinanceDecisionGenerator @@ -32,9 +33,12 @@ import fi.espoo.evaka.reports.freezeVoucherValueReportRows import fi.espoo.evaka.reservations.MissingHolidayReservationsReminders import fi.espoo.evaka.reservations.MissingReservationsReminders import fi.espoo.evaka.sficlient.SfiMessagesClient +import fi.espoo.evaka.shared.async.AsyncJob +import fi.espoo.evaka.shared.async.AsyncJobRunner import fi.espoo.evaka.shared.async.removeOldAsyncJobs import fi.espoo.evaka.shared.auth.AuthenticatedUser import fi.espoo.evaka.shared.auth.PasswordBlacklist +import fi.espoo.evaka.shared.auth.getEndedDaycareAclRows import fi.espoo.evaka.shared.db.Database import fi.espoo.evaka.shared.db.runSanityChecks import fi.espoo.evaka.shared.domain.EvakaClock @@ -230,6 +234,10 @@ enum class ScheduledJob( ScheduledJobs::importPasswordBlacklists, ScheduledJobSettings(enabled = true, schedule = JobSchedule.daily(LocalTime.of(0, 20))), ), + DeleteEndedAcl( + ScheduledJobs::deleteEndedAcl, + ScheduledJobSettings(enabled = true, schedule = JobSchedule.daily(LocalTime.of(0, 5))), + ), } private val logger = KotlinLogging.logger {} @@ -255,6 +263,7 @@ class ScheduledJobs( private val jamixService: JamixService, private val sfiMessagesClient: SfiMessagesClient?, private val passwordBlacklist: PasswordBlacklist, + private val asyncJobRunner: AsyncJobRunner, env: ScheduledJobsEnv, ) : JobSchedule { override val jobs: List = @@ -500,4 +509,18 @@ WHERE id IN (SELECT id FROM attendances_to_end) evakaEnv.passwordBlacklistDirectory?.let { directory -> passwordBlacklist.importBlacklists(db, Path.of(directory)) } + + fun deleteEndedAcl(db: Database.Connection, clock: EvakaClock) = + db.transaction { tx -> + tx.getEndedDaycareAclRows(clock.today()).forEach { + removeDaycareAclForRole( + tx, + asyncJobRunner, + clock.now(), + it.daycareId, + it.employeeId, + it.role, + ) + } + } } diff --git a/service/src/main/resources/db/migration/V493__acl_end_date.sql b/service/src/main/resources/db/migration/V493__acl_end_date.sql new file mode 100644 index 00000000000..0d6df153e08 --- /dev/null +++ b/service/src/main/resources/db/migration/V493__acl_end_date.sql @@ -0,0 +1 @@ +ALTER TABLE daycare_acl ADD COLUMN end_date date; diff --git a/service/src/main/resources/migrations.txt b/service/src/main/resources/migrations.txt index 366fbcf8e45..7b4adb59df9 100644 --- a/service/src/main/resources/migrations.txt +++ b/service/src/main/resources/migrations.txt @@ -488,3 +488,4 @@ V489__person_email_verification.sql V490__daycare_service_worker_note.sql V491__password_blacklist.sql V492__finance_note.sql +V493__acl_end_date.sql From e2f32c1d9398ca1e3a87d0cbf3ed51b1fd8a92e1 Mon Sep 17 00:00:00 2001 From: Joosa Kurvinen Date: Mon, 3 Feb 2025 16:32:14 +0200 Subject: [PATCH 2/4] admin tool updates --- .../employees/DaycareRolesModal.tsx | 24 ++++++++++++++----- .../components/employees/EmployeePage.tsx | 4 +++- .../src/lib-common/generated/api-types/pis.ts | 19 +++++++++++++++ .../defaults/employee/i18n/fi.tsx | 1 + .../fi/espoo/evaka/pis/EmployeeQueries.kt | 17 +++++++++---- .../pis/controllers/EmployeeController.kt | 7 +++++- 6 files changed, 59 insertions(+), 13 deletions(-) diff --git a/frontend/src/employee-frontend/components/employees/DaycareRolesModal.tsx b/frontend/src/employee-frontend/components/employees/DaycareRolesModal.tsx index f954ed04755..afd6e10cd42 100644 --- a/frontend/src/employee-frontend/components/employees/DaycareRolesModal.tsx +++ b/frontend/src/employee-frontend/components/employees/DaycareRolesModal.tsx @@ -5,7 +5,7 @@ import React from 'react' import { scopedRoles } from 'lib-common/api-types/employee-auth' -import { boolean, string } from 'lib-common/form/fields' +import { boolean, localDate, string } from 'lib-common/form/fields' import { array, object, @@ -25,12 +25,14 @@ import { EmployeeId, UserRole } from 'lib-common/generated/api-types/shared' +import LocalDate from 'lib-common/local-date' import { SelectF } from 'lib-components/atoms/dropdowns/Select' import TreeDropdown, { sortTreeByText, TreeNode } from 'lib-components/atoms/dropdowns/TreeDropdown' import { FixedSpaceColumn } from 'lib-components/layout/flex-helpers' +import { DatePickerF } from 'lib-components/molecules/date-picker/DatePicker' import { MutateFormModal } from 'lib-components/molecules/modals/FormModal' import { Label } from 'lib-components/typography' @@ -54,7 +56,8 @@ const treeNode = (): Form => const form = transformed( object({ daycareTree: array(treeNode()), - role: required(oneOf()) + role: required(oneOf()), + endDate: localDate() }), (res) => { const daycareIds = res.daycareTree @@ -66,7 +69,8 @@ const form = transformed( return ValidationSuccess.of({ daycareIds, - role: res.role + role: res.role, + endDate: res.endDate ?? null }) } ) @@ -80,7 +84,8 @@ export default React.memo(function DaycareRolesModal({ units: Daycare[] onClose: () => void }) { - const { i18n } = useTranslation() + const { i18n, lang } = useTranslation() + const boundForm = useForm( form, () => ({ @@ -113,12 +118,15 @@ export default React.memo(function DaycareRolesModal({ domValue: r, label: i18n.roles.adRoles[r] })) - } + }, + endDate: localDate.fromDate(null, { + minDate: LocalDate.todayInHelsinkiTz() + }) }), i18n.validationErrors ) - const { daycareTree, role } = useFormFields(boundForm) + const { daycareTree, role, endDate } = useFormFields(boundForm) return ( {i18n.employees.editor.unitRoles.role} + + + + ) diff --git a/frontend/src/employee-frontend/components/employees/EmployeePage.tsx b/frontend/src/employee-frontend/components/employees/EmployeePage.tsx index 7163dc9a059..a21fe383c9b 100644 --- a/frontend/src/employee-frontend/components/employees/EmployeePage.tsx +++ b/frontend/src/employee-frontend/components/employees/EmployeePage.tsx @@ -180,16 +180,18 @@ const EmployeePage = React.memo(function EmployeePage({ {i18n.employees.editor.unitRoles.unit} {i18n.employees.editor.unitRoles.role} + {i18n.employees.editor.unitRoles.endDate} - {sortedRoles.map(({ daycareId, daycareName, role }) => ( + {sortedRoles.map(({ daycareId, daycareName, role, endDate }) => ( {daycareName} {i18n.roles.adRoles[role]} + {endDate?.format() ?? '-'} ): DaycareRole { + return { + ...json, + endDate: (json.endDate != null) ? LocalDate.parseIso(json.endDate) : null + } +} + + export function deserializeJsonEmailVerification(json: JsonOf): EmailVerification { return { ...json, @@ -794,6 +804,7 @@ export function deserializeJsonEmployeeWithDaycareRoles(json: JsonOf deserializeJsonDaycareRole(e)), lastLogin: (json.lastLogin != null) ? HelsinkiDateTime.parseIso(json.lastLogin) : null, updated: (json.updated != null) ? HelsinkiDateTime.parseIso(json.updated) : null } @@ -984,3 +995,11 @@ export function deserializeJsonRestrictedDetails(json: JsonOf endDate: (json.endDate != null) ? LocalDate.parseIso(json.endDate) : null } } + + +export function deserializeJsonUpsertEmployeeDaycareRolesRequest(json: JsonOf): UpsertEmployeeDaycareRolesRequest { + return { + ...json, + endDate: (json.endDate != null) ? LocalDate.parseIso(json.endDate) : null + } +} diff --git a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx index abb073a9e02..8531a9609b2 100755 --- a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx +++ b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx @@ -4710,6 +4710,7 @@ export const fi = { title: 'Luvitukset', unit: 'Yksikkö', role: 'Rooli yksikössä', + endDate: 'Luvitus päättyy', deleteConfirm: 'Haluatko poistaa käyttäjän luvituksen?', deleteAll: 'Poista kaikki luvitukset', deleteAllConfirm: 'Haluatko poistaa käyttäjän kaikki luvitukset?', diff --git a/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt index 72cb85ecba3..ef6c66f6988 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt @@ -21,6 +21,7 @@ import fi.espoo.evaka.shared.domain.EvakaClock import fi.espoo.evaka.shared.domain.HelsinkiDateTime import fi.espoo.evaka.shared.domain.NotFound import fi.espoo.evaka.shared.mapToPaged +import java.time.LocalDate import org.jdbi.v3.json.Json data class NewEmployee( @@ -49,7 +50,12 @@ data class EmployeeRoles( val allScopedRoles: Set = setOf(), ) -data class DaycareRole(val daycareId: DaycareId, val daycareName: String, val role: UserRole) +data class DaycareRole( + val daycareId: DaycareId, + val daycareName: String, + val role: UserRole, + val endDate: LocalDate?, +) data class DaycareGroupRole( val daycareId: DaycareId, @@ -266,7 +272,7 @@ SELECT temp_unit.name as temporary_unit_name, employee.roles AS global_roles, ( - SELECT jsonb_agg(jsonb_build_object('daycareId', acl.daycare_id, 'daycareName', d.name, 'role', acl.role)) + SELECT jsonb_agg(jsonb_build_object('daycareId', acl.daycare_id, 'daycareName', d.name, 'role', acl.role, 'endDate', acl.end_date)) FROM daycare_acl acl JOIN daycare d ON acl.daycare_id = d.id WHERE acl.employee_id = employee.id @@ -308,13 +314,14 @@ fun Database.Transaction.upsertEmployeeDaycareRoles( id: EmployeeId, daycareIds: List, role: UserRole, + endDate: LocalDate?, ) { executeBatch(daycareIds) { sql( """ -INSERT INTO daycare_acl (daycare_id, employee_id, role) -VALUES (${bind { daycareId -> daycareId }}, ${bind(id)}, ${bind(role)}) -ON CONFLICT (employee_id, daycare_id) DO UPDATE SET role = ${bind(role)} +INSERT INTO daycare_acl (daycare_id, employee_id, role, end_date) +VALUES (${bind { daycareId -> daycareId }}, ${bind(id)}, ${bind(role)}, ${bind(endDate)}) +ON CONFLICT (employee_id, daycare_id) DO UPDATE SET role = ${bind(role)}, end_date = ${bind(endDate)} """ ) } diff --git a/service/src/main/kotlin/fi/espoo/evaka/pis/controllers/EmployeeController.kt b/service/src/main/kotlin/fi/espoo/evaka/pis/controllers/EmployeeController.kt index 5486c1c44fe..ace344d0112 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/pis/controllers/EmployeeController.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/pis/controllers/EmployeeController.kt @@ -40,6 +40,7 @@ import fi.espoo.evaka.shared.domain.EvakaClock import fi.espoo.evaka.shared.domain.NotFound import fi.espoo.evaka.shared.security.AccessControl import fi.espoo.evaka.shared.security.Action +import java.time.LocalDate import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -150,6 +151,7 @@ class EmployeeController(private val accessControl: AccessControl) { data class UpsertEmployeeDaycareRolesRequest( val daycareIds: List, val role: UserRole, + val endDate: LocalDate?, ) { init { if (!role.isUnitScopedRole()) { @@ -169,6 +171,9 @@ class EmployeeController(private val accessControl: AccessControl) { if (body.daycareIds.isEmpty()) { throw BadRequest("No daycare IDs provided") } + if (body.endDate != null && body.endDate.isBefore(clock.today())) { + throw BadRequest("End date cannot be in the past") + } db.connect { dbc -> dbc.transaction { @@ -180,7 +185,7 @@ class EmployeeController(private val accessControl: AccessControl) { id, ) - it.upsertEmployeeDaycareRoles(id, body.daycareIds, body.role) + it.upsertEmployeeDaycareRoles(id, body.daycareIds, body.role, body.endDate) it.upsertEmployeeMessageAccount(id) } } From 34e3054378a3f78531020d86fc1595be530dd05d Mon Sep 17 00:00:00 2001 From: Joosa Kurvinen Date: Tue, 4 Feb 2025 09:40:00 +0200 Subject: [PATCH 3/4] update tests --- .../UnitAclControllerIntegrationTest.kt | 190 ++++++++++-------- .../EmployeeControllerIntegrationTest.kt | 25 ++- ...EmployeeControllerSearchIntegrationTest.kt | 1 + .../evaka/shared/job/ScheduledJobsTest.kt | 52 +++++ .../fi/espoo/evaka/shared/auth/AclQueries.kt | 2 +- .../espoo/evaka/shared/job/ScheduledJobs.kt | 4 +- 6 files changed, 179 insertions(+), 95 deletions(-) diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclControllerIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclControllerIntegrationTest.kt index 5a574ec4245..e0c5a2c8a2c 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclControllerIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclControllerIntegrationTest.kt @@ -4,9 +4,6 @@ package fi.espoo.evaka.daycare.controllers -import com.github.kittinunf.fuel.core.extensions.jsonBody -import com.github.kittinunf.fuel.core.isSuccessful -import com.github.kittinunf.fuel.jackson.responseObject import fi.espoo.evaka.FullApplicationTest import fi.espoo.evaka.attendance.getOccupancyCoefficientsByUnit import fi.espoo.evaka.pairing.listPersonalDevices @@ -24,7 +21,6 @@ import fi.espoo.evaka.shared.dev.DevDaycareGroup import fi.espoo.evaka.shared.dev.DevEmployee import fi.espoo.evaka.shared.dev.DevPersonalMobileDevice import fi.espoo.evaka.shared.dev.insert -import fi.espoo.evaka.shared.domain.EvakaClock import fi.espoo.evaka.shared.domain.HelsinkiDateTime import fi.espoo.evaka.shared.domain.MockEvakaClock import fi.espoo.evaka.shared.domain.NotFound @@ -60,8 +56,8 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = ) private lateinit var admin: AuthenticatedUser.Employee - private fun getRoleBodyString(body: UnitAclController.FullAclInfo) = - jsonMapper.writeValueAsString(body) + val now = HelsinkiDateTime.of(LocalDate.of(2023, 3, 29), LocalTime.of(8, 37)) + val clock = MockEvakaClock(now) @BeforeEach fun beforeEach() { @@ -98,7 +94,11 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = assertTrue(getAclRows().isEmpty()) insertEmployee( - UnitAclController.AclUpdate(groupIds = null, hasStaffOccupancyEffect = null), + UnitAclController.AclUpdate( + groupIds = null, + hasStaffOccupancyEffect = null, + endDate = null, + ), UserRole.UNIT_SUPERVISOR, testDaycare.id, employee.id, @@ -109,6 +109,7 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = employee = employee, role = UserRole.UNIT_SUPERVISOR, groupIds = emptyList(), + endDate = null, ) ), getAclRows(), @@ -117,8 +118,13 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = deleteSupervisor(testDaycare.id) assertTrue(getAclRows().isEmpty()) + val endDate = now.toLocalDate().plusDays(7) insertEmployee( - UnitAclController.AclUpdate(groupIds = null, hasStaffOccupancyEffect = null), + UnitAclController.AclUpdate( + groupIds = null, + hasStaffOccupancyEffect = null, + endDate = endDate, + ), UserRole.STAFF, testDaycare.id, employee.id, @@ -126,7 +132,12 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = assertEquals( listOf( - DaycareAclRow(employee = employee, role = UserRole.STAFF, groupIds = emptyList()) + DaycareAclRow( + employee = employee, + role = UserRole.STAFF, + groupIds = emptyList(), + endDate = endDate, + ) ), getAclRows(), ) @@ -143,6 +154,7 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = UnitAclController.AclUpdate( groupIds = listOf(testDaycareGroup.id), hasStaffOccupancyEffect = null, + endDate = null, ) insertEmployee(aclUpdate, UserRole.UNIT_SUPERVISOR, testDaycare.id, employee.id) @@ -153,6 +165,7 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = employee = employee, role = UserRole.UNIT_SUPERVISOR, groupIds = listOf(testDaycareGroup.id), + endDate = null, ) ), getAclRows(), @@ -166,7 +179,12 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = fun `add and delete daycare acl with occupancy coefficient`() { assertTrue(getAclRows().isEmpty()) - val aclUpdate = UnitAclController.AclUpdate(groupIds = null, hasStaffOccupancyEffect = true) + val aclUpdate = + UnitAclController.AclUpdate( + groupIds = null, + hasStaffOccupancyEffect = true, + endDate = null, + ) insertEmployee(aclUpdate, UserRole.UNIT_SUPERVISOR, testDaycare.id, employee.id) assertEquals( @@ -184,6 +202,7 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = ), role = UserRole.UNIT_SUPERVISOR, groupIds = emptyList(), + endDate = null, ) ), getAclRows(), @@ -203,10 +222,16 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = } @Test - fun `modify group acl and occupancy coefficient`() { + fun `modify group acl, occupancy coefficient and end date`() { assertTrue(getAclRows().isEmpty()) - val aclUpdate = UnitAclController.AclUpdate(groupIds = null, hasStaffOccupancyEffect = true) + val endDate1 = now.toLocalDate().plusDays(7) + val aclUpdate = + UnitAclController.AclUpdate( + groupIds = null, + hasStaffOccupancyEffect = true, + endDate = endDate1, + ) insertEmployee(aclUpdate, UserRole.UNIT_SUPERVISOR, testDaycare.id, employee.id) assertEquals( @@ -224,15 +249,18 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = ), role = UserRole.UNIT_SUPERVISOR, groupIds = emptyList(), + endDate = endDate1, ) ), getAclRows(), ) + val endDate2 = now.toLocalDate().plusDays(14) val aclModification = UnitAclController.AclUpdate( groupIds = listOf(testDaycareGroup.id), hasStaffOccupancyEffect = false, + endDate = endDate2, ) modifyEmployee(aclModification, testDaycare.id, employee.id) @@ -242,6 +270,7 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = employee = employee, role = UserRole.UNIT_SUPERVISOR, groupIds = listOf(testDaycareGroup.id), + endDate = endDate2, ) ), getAclRows(), @@ -255,7 +284,11 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = fun `supervisor message account`() { assertEquals(MessageAccountState.NO_ACCOUNT, employeeMessageAccountState()) insertEmployee( - UnitAclController.AclUpdate(groupIds = null, hasStaffOccupancyEffect = null), + UnitAclController.AclUpdate( + groupIds = null, + hasStaffOccupancyEffect = null, + endDate = null, + ), UserRole.UNIT_SUPERVISOR, testDaycare.id, employee.id, @@ -263,7 +296,11 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = assertEquals(MessageAccountState.ACTIVE_ACCOUNT, employeeMessageAccountState()) insertEmployee( - UnitAclController.AclUpdate(groupIds = null, hasStaffOccupancyEffect = null), + UnitAclController.AclUpdate( + groupIds = null, + hasStaffOccupancyEffect = null, + endDate = null, + ), UserRole.UNIT_SUPERVISOR, testDaycare2.id, employee.id, @@ -324,10 +361,9 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = @Test fun temporaryEmployeeCrud() { - val dateTime = HelsinkiDateTime.of(LocalDate.of(2023, 3, 29), LocalTime.of(8, 37)) - val clock = MockEvakaClock(dateTime) - assertThat(getTemporaryEmployees(clock, testDaycare.id)).isEmpty() - assertThat(getTemporaryEmployees(clock, testDaycare2.id)).isEmpty() + + assertThat(getTemporaryEmployees(testDaycare.id)).isEmpty() + assertThat(getTemporaryEmployees(testDaycare2.id)).isEmpty() // create val createdTemporary = @@ -346,17 +382,17 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = testDaycare.id, createdTemporary, ) - assertThat(getTemporaryEmployees(clock, testDaycare.id)) + assertThat(getTemporaryEmployees(testDaycare.id)) .extracting({ it.id }, { it.firstName }, { it.lastName }, { it.temporaryInUnitId }) .containsExactly(Tuple(temporaryEmployeeId, "Etu1", "Suku1", testDaycare.id)) - assertThat(getTemporaryEmployees(clock, testDaycare2.id)).isEmpty() - assertThat(getTemporaryEmployee(clock, testDaycare.id, temporaryEmployeeId)) + assertThat(getTemporaryEmployees(testDaycare2.id)).isEmpty() + assertThat(getTemporaryEmployee(testDaycare.id, temporaryEmployeeId)) .isEqualTo(createdTemporary) - assertThrows { getTemporaryEmployee(clock, testDaycare2.id, temporaryEmployeeId) } + assertThrows { getTemporaryEmployee(testDaycare2.id, temporaryEmployeeId) } dbInstance().connect { dbc -> - dbc.transaction { tx -> tx.deactivateInactiveEmployees(dateTime.plusMonths(1)) } + dbc.transaction { tx -> tx.deactivateInactiveEmployees(now.plusMonths(1)) } } - assertThat(getTemporaryEmployee(clock, testDaycare.id, temporaryEmployeeId)) + assertThat(getTemporaryEmployee(testDaycare.id, temporaryEmployeeId)) .isEqualTo(createdTemporary) // update @@ -386,15 +422,15 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = temporaryEmployeeId, updatedTemporary, ) - assertThat(getTemporaryEmployees(clock, testDaycare.id)) + assertThat(getTemporaryEmployees(testDaycare.id)) .extracting({ it.id }, { it.firstName }, { it.lastName }, { it.temporaryInUnitId }) .containsExactly(Tuple(temporaryEmployeeId, "Etu2", "Suku2", testDaycare.id)) - assertThat(getTemporaryEmployee(clock, testDaycare.id, temporaryEmployeeId)) + assertThat(getTemporaryEmployee(testDaycare.id, temporaryEmployeeId)) .isEqualTo(updatedTemporary) dbInstance().connect { dbc -> - dbc.transaction { tx -> tx.deactivateInactiveEmployees(dateTime.plusMonths(1)) } + dbc.transaction { tx -> tx.deactivateInactiveEmployees(now.plusMonths(1)) } } - assertThat(getTemporaryEmployee(clock, testDaycare.id, temporaryEmployeeId)) + assertThat(getTemporaryEmployee(testDaycare.id, temporaryEmployeeId)) .isEqualTo(updatedTemporary) // delete acl @@ -414,10 +450,10 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = testDaycare.id, temporaryEmployeeId, ) - assertThat(getTemporaryEmployees(clock, testDaycare.id)) + assertThat(getTemporaryEmployees(testDaycare.id)) .extracting({ it.id }, { it.firstName }, { it.lastName }, { it.temporaryInUnitId }) .containsExactly(Tuple(temporaryEmployeeId, "Etu2", "Suku2", testDaycare.id)) - assertThat(getTemporaryEmployee(clock, testDaycare.id, temporaryEmployeeId)) + assertThat(getTemporaryEmployee(testDaycare.id, temporaryEmployeeId)) .isEqualTo(updatedTemporary.copy(groupIds = emptySet())) // delete @@ -437,14 +473,12 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = testDaycare.id, temporaryEmployeeId, ) - assertThat(getTemporaryEmployees(clock, testDaycare.id)).isEmpty() - assertThrows { getTemporaryEmployee(clock, testDaycare.id, temporaryEmployeeId) } + assertThat(getTemporaryEmployees(testDaycare.id)).isEmpty() + assertThrows { getTemporaryEmployee(testDaycare.id, temporaryEmployeeId) } } @Test fun temporaryEmployeeCannotBeUpdatedWithPermanentEmployeeApi() { - val clock = - MockEvakaClock(HelsinkiDateTime.of(LocalDate.of(2023, 3, 29), LocalTime.of(8, 37))) val temporaryEmployeeId = unitAclController.createTemporaryEmployee( dbInstance(), @@ -467,7 +501,7 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = clock, testDaycare.id, temporaryEmployeeId, - UnitAclController.AclUpdate(listOf(testDaycareGroup.id), false), + UnitAclController.AclUpdate(listOf(testDaycareGroup.id), false, null), ) } assertThrows { @@ -479,7 +513,7 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = temporaryEmployeeId, UnitAclController.FullAclInfo( UserRole.STAFF, - UnitAclController.AclUpdate(listOf(testDaycareGroup.id), false), + UnitAclController.AclUpdate(listOf(testDaycareGroup.id), false, null), ), ) } @@ -523,8 +557,6 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = @Test fun groupAccessCanBeAddedAndRemoved() { - val clock = - MockEvakaClock(HelsinkiDateTime.of(LocalDate.of(2023, 3, 29), LocalTime.of(8, 37))) val group2 = DevDaycareGroup( daycareId = testDaycare.id, @@ -564,6 +596,7 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = UnitAclController.AclUpdate( groupIds = listOf(testDaycareGroup.id, group2.id), hasStaffOccupancyEffect = null, + endDate = null, ), ) @@ -596,6 +629,7 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = UnitAclController.AclUpdate( groupIds = listOf(unit2Group.id), hasStaffOccupancyEffect = null, + endDate = null, ), ) @@ -634,6 +668,7 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = UnitAclController.AclUpdate( groupIds = listOf(testDaycareGroup.id), hasStaffOccupancyEffect = null, + endDate = null, ), ) @@ -660,7 +695,11 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = val clock = MockEvakaClock(HelsinkiDateTime.of(LocalDate.of(2023, 3, 29), LocalTime.of(8, 37))) insertEmployee( - UnitAclController.AclUpdate(groupIds = null, hasStaffOccupancyEffect = null), + UnitAclController.AclUpdate( + groupIds = null, + hasStaffOccupancyEffect = null, + endDate = null, + ), UserRole.STAFF, testDaycare.id, employee.id, @@ -711,20 +750,13 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = } } - private fun getAclRows(): List { - val (_, res, body) = - http - .get("/employee/daycares/${testDaycare.id}/acl") - .asUser(admin) - .responseObject>(jsonMapper) - assertTrue(res.isSuccessful) - return body.get() - } + private fun getAclRows(): List = + unitAclController.getDaycareAcl(dbInstance(), admin, clock, testDaycare.id) - private fun getTemporaryEmployees(clock: EvakaClock, unitId: DaycareId) = + private fun getTemporaryEmployees(unitId: DaycareId) = unitAclController.getTemporaryEmployees(dbInstance(), admin, clock, unitId) - private fun getTemporaryEmployee(clock: EvakaClock, unitId: DaycareId, employeeId: EmployeeId) = + private fun getTemporaryEmployee(unitId: DaycareId, employeeId: EmployeeId) = unitAclController.getTemporaryEmployee(dbInstance(), admin, clock, unitId, employeeId) private fun getDaycareOccupancyCoefficients(unitId: DaycareId): Map { @@ -733,54 +765,40 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = } } - private fun deleteSupervisor(daycareId: DaycareId) { - val (_, res, _) = - http - .delete("/employee/daycares/$daycareId/supervisors/${employee.id}") - .asUser(admin) - .response() - assertTrue(res.isSuccessful) - } + private fun deleteSupervisor(daycareId: DaycareId) = + unitAclController.deleteUnitSupervisor(dbInstance(), admin, clock, daycareId, employee.id) private fun insertEmployee( update: UnitAclController.AclUpdate, role: UserRole, daycareId: DaycareId, employeeId: EmployeeId, - ) { - val (_, res, _) = - http - .put("/employee/daycares/$daycareId/full-acl/$employeeId") - .asUser(admin) - .jsonBody( - getRoleBodyString(UnitAclController.FullAclInfo(update = update, role = role)) - ) - .response() - assertTrue(res.isSuccessful) - } + ) = + unitAclController.addFullAclForRole( + dbInstance(), + admin, + clock, + daycareId, + employeeId, + UnitAclController.FullAclInfo(role, update), + ) private fun modifyEmployee( update: UnitAclController.AclUpdate, daycareId: DaycareId, employeeId: EmployeeId, - ) { - val (_, res, _) = - http - .put("/employee/daycares/$daycareId/staff/$employeeId/groups") - .asUser(admin) - .jsonBody(jsonMapper.writeValueAsString(update)) - .response() - assertTrue(res.isSuccessful) - } + ) = + unitAclController.updateGroupAclWithOccupancyCoefficient( + dbInstance(), + admin, + clock, + daycareId, + employeeId, + update, + ) - private fun deleteStaff() { - val (_, res, _) = - http - .delete("/employee/daycares/${testDaycare.id}/staff/${employee.id}") - .asUser(admin) - .response() - assertTrue(res.isSuccessful) - } + private fun deleteStaff() = + unitAclController.deleteStaff(dbInstance(), admin, clock, testDaycare.id, employee.id) private enum class MessageAccountState { NO_ACCOUNT, diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerIntegrationTest.kt index 40aa7d1a120..c9a6fe5b92f 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerIntegrationTest.kt @@ -26,6 +26,7 @@ import fi.espoo.evaka.shared.dev.DevEmployee import fi.espoo.evaka.shared.dev.insert import fi.espoo.evaka.shared.domain.HelsinkiDateTime import fi.espoo.evaka.shared.domain.MockEvakaClock +import java.time.LocalDate import java.util.UUID import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -110,17 +111,24 @@ class EmployeeControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach ) } + val endDate = clock.today().plusMonths(9) upsertEmployeeDaycareRoles( employee.id, listOf(daycare2.id, daycare3.id), UserRole.SPECIAL_EDUCATION_TEACHER, + endDate, ) assertEquals( setOf( - DaycareRole(daycare1.id, daycare1.name, UserRole.STAFF), - DaycareRole(daycare2.id, daycare2.name, UserRole.SPECIAL_EDUCATION_TEACHER), - DaycareRole(daycare3.id, daycare3.name, UserRole.SPECIAL_EDUCATION_TEACHER), + DaycareRole(daycare1.id, daycare1.name, UserRole.STAFF, null), + DaycareRole( + daycare2.id, + daycare2.name, + UserRole.SPECIAL_EDUCATION_TEACHER, + endDate, + ), + DaycareRole(daycare3.id, daycare3.name, UserRole.SPECIAL_EDUCATION_TEACHER, endDate), ), getEmployeeDetails(employee.id).daycareRoles.toSet(), ) @@ -157,7 +165,7 @@ class EmployeeControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach val updated = getEmployeeDetails(employee.id) assertEquals( - listOf(DaycareRole(daycare2.id, daycare2.name, UserRole.STAFF)), + listOf(DaycareRole(daycare2.id, daycare2.name, UserRole.STAFF, null)), updated.daycareRoles, ) assertEquals( @@ -230,13 +238,18 @@ class EmployeeControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach fun createEmployee(employee: NewEmployee) = employeeController.createEmployee(dbInstance(), adminUser, clock, employee) - fun upsertEmployeeDaycareRoles(id: EmployeeId, daycareIds: List, role: UserRole) = + fun upsertEmployeeDaycareRoles( + id: EmployeeId, + daycareIds: List, + role: UserRole, + endDate: LocalDate?, + ) = employeeController.upsertEmployeeDaycareRoles( dbInstance(), adminUser, clock, id, - EmployeeController.UpsertEmployeeDaycareRolesRequest(daycareIds, role), + EmployeeController.UpsertEmployeeDaycareRolesRequest(daycareIds, role, endDate), ) fun updateEmployeeGlobalRoles(id: EmployeeId, roles: List) = diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerSearchIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerSearchIntegrationTest.kt index dc6d0ab0401..dd391c2774a 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerSearchIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerSearchIntegrationTest.kt @@ -80,6 +80,7 @@ class EmployeeControllerSearchIntegrationTest : FullApplicationTest(resetDbBefor daycareId = testDaycare.id, daycareName = testDaycare.name, role = UserRole.UNIT_SUPERVISOR, + endDate = null, ) ), supervisor.daycareRoles, diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/job/ScheduledJobsTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/job/ScheduledJobsTest.kt index 6d627b8ac12..8c520782a55 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/job/ScheduledJobsTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/job/ScheduledJobsTest.kt @@ -28,9 +28,13 @@ import fi.espoo.evaka.shared.ChildId import fi.espoo.evaka.shared.auth.AuthenticatedUser import fi.espoo.evaka.shared.auth.CitizenAuthLevel import fi.espoo.evaka.shared.auth.UserRole +import fi.espoo.evaka.shared.auth.getDaycareAclRows +import fi.espoo.evaka.shared.auth.insertDaycareAclRow import fi.espoo.evaka.shared.db.Database import fi.espoo.evaka.shared.dev.DevBackupCare import fi.espoo.evaka.shared.dev.DevPerson +import fi.espoo.evaka.shared.dev.DevDaycare +import fi.espoo.evaka.shared.dev.DevEmployee import fi.espoo.evaka.shared.dev.DevPersonType import fi.espoo.evaka.shared.dev.DevPlacement import fi.espoo.evaka.shared.dev.insert @@ -452,6 +456,54 @@ class ScheduledJobsTest : FullApplicationTest(resetDbBeforeEach = true) { } } + @Test + fun `removeEndedAcl removes rows where end date has passed`() { + val now = HelsinkiDateTime.of(LocalDate.of(2024, 5, 1), LocalTime.of(0, 5)) + val today = now.toLocalDate() + + val daycare1 = DevDaycare(areaId = testArea.id) + val daycare2 = DevDaycare(areaId = testArea.id) + val daycare3 = DevDaycare(areaId = testArea.id) + val daycare4 = DevDaycare(areaId = testArea.id) + val staff = DevEmployee() + db.transaction { tx -> + tx.insert(daycare1) + tx.insert(daycare2) + tx.insert(daycare3) + tx.insert(daycare4) + tx.insert(staff) + tx.insertDaycareAclRow(daycare1.id, staff.id, UserRole.STAFF, endDate = null) + tx.insertDaycareAclRow( + daycare2.id, + staff.id, + UserRole.STAFF, + endDate = today.minusDays(1), + ) + tx.insertDaycareAclRow(daycare3.id, staff.id, UserRole.STAFF, endDate = today) + tx.insertDaycareAclRow( + daycare4.id, + staff.id, + UserRole.STAFF, + endDate = today.plusDays(1), + ) + } + db.read { tx -> + assertEquals(1, tx.getDaycareAclRows(daycare1.id, false).size) + assertEquals(1, tx.getDaycareAclRows(daycare2.id, false).size) + assertEquals(1, tx.getDaycareAclRows(daycare3.id, false).size) + assertEquals(1, tx.getDaycareAclRows(daycare4.id, false).size) + } + + scheduledJobs.removeEndedAcl(db, MockEvakaClock(now)) + + db.read { tx -> + assertEquals(1, tx.getDaycareAclRows(daycare1.id, false).size) + assertEquals(0, tx.getDaycareAclRows(daycare2.id, false).size) + assertEquals(1, tx.getDaycareAclRows(daycare3.id, false).size) + assertEquals(1, tx.getDaycareAclRows(daycare4.id, false).size) + } + } + private fun createExpiredDailyNote(now: Instant) { db.transaction { val sixteenHoursAgo = now - Duration.ofHours(16) diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AclQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AclQueries.kt index 107d73aba98..71f1c3fa4b1 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AclQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AclQueries.kt @@ -80,7 +80,7 @@ fun Database.Transaction.insertDaycareAclRow( daycareId: DaycareId, employeeId: EmployeeId, role: UserRole, - endDate: LocalDate?, + endDate: LocalDate? = null, ) = execute { sql( """ diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/job/ScheduledJobs.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/job/ScheduledJobs.kt index 57a5bb2942d..9294794ac83 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/job/ScheduledJobs.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/job/ScheduledJobs.kt @@ -235,7 +235,7 @@ enum class ScheduledJob( ScheduledJobSettings(enabled = true, schedule = JobSchedule.daily(LocalTime.of(0, 20))), ), DeleteEndedAcl( - ScheduledJobs::deleteEndedAcl, + ScheduledJobs::removeEndedAcl, ScheduledJobSettings(enabled = true, schedule = JobSchedule.daily(LocalTime.of(0, 5))), ), } @@ -510,7 +510,7 @@ WHERE id IN (SELECT id FROM attendances_to_end) passwordBlacklist.importBlacklists(db, Path.of(directory)) } - fun deleteEndedAcl(db: Database.Connection, clock: EvakaClock) = + fun removeEndedAcl(db: Database.Connection, clock: EvakaClock) = db.transaction { tx -> tx.getEndedDaycareAclRows(clock.today()).forEach { removeDaycareAclForRole( From 3d3b858825c03adb21565928414f24964150342d Mon Sep 17 00:00:00 2001 From: Joosa Kurvinen Date: Thu, 6 Feb 2025 12:49:36 +0200 Subject: [PATCH 4/4] format --- .../kotlin/fi/espoo/evaka/shared/job/ScheduledJobsTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/job/ScheduledJobsTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/job/ScheduledJobsTest.kt index 8c520782a55..3d9ca10585d 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/job/ScheduledJobsTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/job/ScheduledJobsTest.kt @@ -32,9 +32,9 @@ import fi.espoo.evaka.shared.auth.getDaycareAclRows import fi.espoo.evaka.shared.auth.insertDaycareAclRow import fi.espoo.evaka.shared.db.Database import fi.espoo.evaka.shared.dev.DevBackupCare -import fi.espoo.evaka.shared.dev.DevPerson import fi.espoo.evaka.shared.dev.DevDaycare import fi.espoo.evaka.shared.dev.DevEmployee +import fi.espoo.evaka.shared.dev.DevPerson import fi.espoo.evaka.shared.dev.DevPersonType import fi.espoo.evaka.shared.dev.DevPlacement import fi.espoo.evaka.shared.dev.insert