From b64a02d81cf7d61a866dac061591fd4bb1292bc9 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 9 Aug 2026 06:52:16 -0600 Subject: [PATCH 01/18] Drop legacy nbri_ehr tables and tighten birth and conception entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conception is the only table left in the nbri_ehr schema — the rest were carried over from the legacy system and nothing reads them, so their queries, lookups and referencing columns go too. On the entry side, birth condition and conception term date are gone, conception date is now required, and the fields the Start with Conception window populates are read-only. --- nbri_ehr/resources/data/birth_condition.tsv | 3 - nbri_ehr/resources/data/editable_lookups.tsv | 2 - nbri_ehr/resources/data/id_history_type.tsv | 6 - nbri_ehr/resources/data/lookup_sets.tsv | 2 - nbri_ehr/resources/data/lookupsManifest.tsv | 3 - .../resources/data/lookupsManifestTest.tsv | 2 - .../queries/nbri_ehr/Account.query.xml | 48 -- .../queries/nbri_ehr/AnimalDelivery.query.xml | 86 --- .../nbri_ehr/AnimalDeliveryEsig.query.xml | 36 -- .../queries/nbri_ehr/AnimalReqOrder.query.xml | 135 ----- .../nbri_ehr/AnimalReqOrderEsig.query.xml | 36 -- .../queries/nbri_ehr/AnimalShipment.query.xml | 42 -- .../queries/nbri_ehr/AnimalVendor.query.xml | 29 - .../queries/nbri_ehr/AnimalVendor/.qview.xml | 20 - .../queries/nbri_ehr/CageCard.query.xml | 56 -- .../nbri_ehr/CageCardHistory.query.xml | 65 -- .../queries/nbri_ehr/Conception.query.xml | 4 +- .../queries/nbri_ehr/ConceptionsByDam.sql | 1 - .../queries/nbri_ehr/DeletedRecord.query.xml | 24 - .../queries/nbri_ehr/Department.query.xml | 25 - .../queries/nbri_ehr/IdHistory.query.xml | 29 - .../resources/queries/nbri_ehr/Lot.query.xml | 20 - .../queries/nbri_ehr/ProtocolEsig.query.xml | 31 - .../nbri_ehr/ProtocolProcedures.query.xml | 21 - .../queries/nbri_ehr/ProtocolStress.query.xml | 21 - .../queries/nbri_ehr/QuestionResponse.js | 35 -- .../nbri_ehr/QuestionResponse.query.xml | 38 -- .../queries/nbri_ehr/locationTypes/.qview.xml | 5 - .../queries/nbri_ehr/locations.query.xml | 19 - .../queries/nbri_ehr/locations/.qview.xml | 6 - .../nbri_ehr/locationsMapping.query.xml | 28 - .../nbri_ehr/locationsMapping/.qview.xml | 7 - .../queries/nbri_ehr/staff.query.xml | 128 ---- .../queries/nbri_ehr/stress.query.xml | 20 - nbri_ehr/resources/queries/study/aliases.sql | 4 - .../resources/queries/study/birth.query.xml | 9 - .../queries/study/demographics.query.xml | 8 - .../study/datasets/datasets_metadata.xml | 6 - .../postgresql/nbri_ehr-26.001-26.002.sql | 40 ++ nbri_ehr/resources/schemas/nbri_ehr.xml | 572 +----------------- .../web/nbri_ehr/model/sources/Birth.js | 37 +- .../web/nbri_ehr/model/sources/Conception.js | 9 +- .../org/labkey/nbri_ehr/NBRI_EHRModule.java | 2 +- .../section/NBRIBirthFormSection.java | 1 - .../tests.nbri_ehr/NBRI_EHRTest.java | 3 +- 45 files changed, 76 insertions(+), 1648 deletions(-) delete mode 100644 nbri_ehr/resources/data/birth_condition.tsv delete mode 100644 nbri_ehr/resources/data/id_history_type.tsv delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/Account.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/AnimalDelivery.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/AnimalDeliveryEsig.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrder.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrderEsig.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/AnimalShipment.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/AnimalVendor.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/AnimalVendor/.qview.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/CageCard.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/CageCardHistory.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/DeletedRecord.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/Department.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/IdHistory.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/Lot.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/ProtocolEsig.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/ProtocolProcedures.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/ProtocolStress.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/QuestionResponse.js delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/QuestionResponse.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/locationTypes/.qview.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/locations.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/locations/.qview.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/locationsMapping.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/locationsMapping/.qview.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/staff.query.xml delete mode 100644 nbri_ehr/resources/queries/nbri_ehr/stress.query.xml create mode 100644 nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.001-26.002.sql diff --git a/nbri_ehr/resources/data/birth_condition.tsv b/nbri_ehr/resources/data/birth_condition.tsv deleted file mode 100644 index bdb4e26..0000000 --- a/nbri_ehr/resources/data/birth_condition.tsv +++ /dev/null @@ -1,3 +0,0 @@ -value title sort_order -L Live 1 -D Dead 2 diff --git a/nbri_ehr/resources/data/editable_lookups.tsv b/nbri_ehr/resources/data/editable_lookups.tsv index d0ed33a..700bffe 100644 --- a/nbri_ehr/resources/data/editable_lookups.tsv +++ b/nbri_ehr/resources/data/editable_lookups.tsv @@ -18,7 +18,6 @@ ehr_lookups bcs_score Clinical Body Condition Score Clinical observation fixed v ehr_lookups behavior_abnormality Clinical Behavior Abnormality Clinical observation fixed values. ehr_lookups behavior_mgmt_codes Behavior Behavior Management Codes Behavior observation fixed values. ehr_lookups behavior_types Behavior Behavior Types Behavior observation fixed values. -ehr_lookups birth_condition Colony Management Birth Condition Birth condition values. ehr_lookups blood_draw_reason Clinical Blood Draw Reason Used in blood draw datasets. ehr_lookups blood_draw_tube_type Clinical Blood Draw Tube Type Used in blood draw datasets. ehr_lookups blood_sample_type Clinical Blood Sample Types Used in blood draw datasets. @@ -68,7 +67,6 @@ ehr_lookups geographic_origins Colony Management Geographic Origins Used in demo ehr_lookups hernia_types Clinical Hernia Types Clinical observation fixed values. ehr_lookups housing_reason Housing Housing Move Reason Used in housing dataset for location transfers. ehr_lookups hyd_score Clinical Hydration Score Clinical observation fixed values. -ehr_lookups id_history_type Colony Management Id History Type ehr_lookups incision_score Clinical Incision Status Clinical observation fixed values. ehr_lookups indoor_outdoor Colony Management Indoor/Outdoor Cage details ehr_lookups keyword Clinical Keyword Clinical observation fixed values. diff --git a/nbri_ehr/resources/data/id_history_type.tsv b/nbri_ehr/resources/data/id_history_type.tsv deleted file mode 100644 index ba59b93..0000000 --- a/nbri_ehr/resources/data/id_history_type.tsv +++ /dev/null @@ -1,6 +0,0 @@ -value title -1 Old Tattoo Number -2 ISIS Stud Book Number -3 CITES Number -4 Name -5 Transponder Number \ No newline at end of file diff --git a/nbri_ehr/resources/data/lookup_sets.tsv b/nbri_ehr/resources/data/lookup_sets.tsv index c465f4a..e71f832 100644 --- a/nbri_ehr/resources/data/lookup_sets.tsv +++ b/nbri_ehr/resources/data/lookup_sets.tsv @@ -16,7 +16,6 @@ bcs_score BCS Store value title behavior_abnormality Behavior Abnormality value behavior_mgmt_codes Behavior Management Codes value behavior_types Behavior Types value -birth_condition Birth Condition value title blood_draw_reason Blood Draw Reason value blood_sample_type Blood Sample Types value breeding_type Breeding Type value title @@ -55,7 +54,6 @@ genitourinary_obs Genitourinary Observations value hernia_types Hernia Types value housing_reason Housing Reason value hyd_score Hyd Score value -id_history_type Id History Type value title incision_score Incision Status value indoor_outdoor Indoor/Outdoor value keyword Keyword value title diff --git a/nbri_ehr/resources/data/lookupsManifest.tsv b/nbri_ehr/resources/data/lookupsManifest.tsv index 9f32d12..ccc18c5 100644 --- a/nbri_ehr/resources/data/lookupsManifest.tsv +++ b/nbri_ehr/resources/data/lookupsManifest.tsv @@ -18,7 +18,6 @@ bcs_score behavior_abnormality behavior_mgmt_codes behavior_types -birth_condition blood_draw_reason blood_draw_tube_type blood_sample_type @@ -56,7 +55,6 @@ fecal_score fecal_smear_score feed_assess_types flag_categories -flag_values gastro_types gender_codes general_obs @@ -66,7 +64,6 @@ geographic_origins hernia_types housing_reason hyd_score -id_history_type incision_score indoor_outdoor lameness diff --git a/nbri_ehr/resources/data/lookupsManifestTest.tsv b/nbri_ehr/resources/data/lookupsManifestTest.tsv index d64c8ba..64b5720 100644 --- a/nbri_ehr/resources/data/lookupsManifestTest.tsv +++ b/nbri_ehr/resources/data/lookupsManifestTest.tsv @@ -18,7 +18,6 @@ bcs_score behavior_abnormality behavior_mgmt_codes behavior_types -birth_condition blood_draw_reason blood_draw_tube_type blood_sample_type @@ -66,7 +65,6 @@ geographic_origins hernia_types housing_reason hyd_score -id_history_type incision_score indoor_outdoor lameness diff --git a/nbri_ehr/resources/queries/nbri_ehr/Account.query.xml b/nbri_ehr/resources/queries/nbri_ehr/Account.query.xml deleted file mode 100644 index 7f498db..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/Account.query.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - Account - - - - Department - - nbri_ehr - Department - DepartmentId - Name - - - - Cost Type - - ehr_lookups - cost_type - value - title - - - - Expense Class - - ehr_lookups - expense_class - value - description - - - - Project - - ehr - project - project - displayName - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/AnimalDelivery.query.xml b/nbri_ehr/resources/queries/nbri_ehr/AnimalDelivery.query.xml deleted file mode 100644 index e19c0a8..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/AnimalDelivery.query.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - Animal Delivery - - - - Animal Delivery Id - - - Animal Shipment - - nbri_ehr - AnimalShipment - AnimalShipmentId - AnimalShipmentId - - - - Ship To - - nbri_ehr - ShipTo - ShipToId - Name - - - - Animal Req Order - - nbri_ehr - AnimalReqOrder - AnimalReqOrderId - AnimalReqOrder - - - - Delivery State - - ehr_lookups - delivery_state - value - title - - - - Project - - ehr - project - project - displayName - - - - Bill To Account - - nbri_ehr - Account - AccountId - AccountNumber - - - - Bill To Staff - http://www.labkey.org/types#userId - - - Per Diem Account - - nbri_ehr - Account - AccountId - AccountNumber - - - - Per Diem Staff - http://www.labkey.org/types#userId - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/AnimalDeliveryEsig.query.xml b/nbri_ehr/resources/queries/nbri_ehr/AnimalDeliveryEsig.query.xml deleted file mode 100644 index 0c74ac4..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/AnimalDeliveryEsig.query.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - Animal Delivery ESignature - - - Animal Delivery - - nbri_ehr - AnimalDelivery - AnimalDeliveryId - AnimalDeliveryId - - - - Esig Event - - ehr_lookups - esig_events - value - title - - - - User Profile - http://www.labkey.org/types#userId - - - Esig Date - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrder.query.xml b/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrder.query.xml deleted file mode 100644 index e282d4d..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrder.query.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - Animal Req Order - - - - Animal Vendor - - nbri_ehr - AnimalVendor - AnimalVendorId - VendorName - - - - Requisitioner Staff - http://www.labkey.org/types#userId - - - Req Order Type - - ehr_lookups - req_order_type - value - title - - - - Req Order State - - ehr_lookups - req_order_state - value - title - - - - Bill To Account - - nbri_ehr - Account - AccountId - AccountNumber - - - - Bill To Staff - http://www.labkey.org/types#userId - - - Per Diem Account - - nbri_ehr - Account - AccountId - AccountNumber - - - - Per Diem Staff - http://www.labkey.org/types#userId - - - Submitted By Staff - http://www.labkey.org/types#userId - - - Approved By Staff - http://www.labkey.org/types#userId - - - Project - - ehr - project - project - displayName - - - - Site Cage - /nbri_ehr/cageDetails.view?room=${SiteCage/room}&cage=${SiteCage}& - - ehr_lookups - cage - location - cage - - - - Site Room - /nbri_ehr/cageDetails.view?room=${SiteRoom}& - - ehr_lookups - rooms - room - room - - - - Site Floor - - ehr_lookups - floors - floor - name - - - - Site Building - - ehr_lookups - buildings - name - - - - Site Area - - ehr_lookups - areas - area - - - - Created By Staff - http://www.labkey.org/types#userId - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrderEsig.query.xml b/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrderEsig.query.xml deleted file mode 100644 index 2d8bfb2..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrderEsig.query.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - Animal Req Order ESignature - - - Animal Req Order - - nbri_ehr - AnimalReqOrder - AnimalReqOrderId - AnimalReqOrderId - - - - Esig Event - - ehr_lookups - esig_events - value - title - - - - User Profile - http://www.labkey.org/types#userId - - - Esig Date - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/AnimalShipment.query.xml b/nbri_ehr/resources/queries/nbri_ehr/AnimalShipment.query.xml deleted file mode 100644 index 60aa8de..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/AnimalShipment.query.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - Animal Shipment - - - - Received By Staff - http://www.labkey.org/types#userId - - - Animal Delivery - - nbri_ehr - AnimalDelivery - AnimalDeliveryId - AnimalDeliveryId - - - - Lot Id - - nbri_ehr - lot - lotId - - - - Cost Center - - ehr_lookups - cost_center - value - title - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/AnimalVendor.query.xml b/nbri_ehr/resources/queries/nbri_ehr/AnimalVendor.query.xml deleted file mode 100644 index 426952f..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/AnimalVendor.query.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Animal Vendor - - - Vendor Approval Code - - ehr_lookups - vendor_approval_code - value - title - - - - Vendor Production Location - - ehr_lookups - vendor_production_location - value - title - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/AnimalVendor/.qview.xml b/nbri_ehr/resources/queries/nbri_ehr/AnimalVendor/.qview.xml deleted file mode 100644 index 9691552..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/AnimalVendor/.qview.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/CageCard.query.xml b/nbri_ehr/resources/queries/nbri_ehr/CageCard.query.xml deleted file mode 100644 index e70dfc5..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/CageCard.query.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - Cage Card - - - - Account Staff - http://www.labkey.org/types#userId - - - Account Staff - http://www.labkey.org/types#userId - - - Animal Delivery - - nbri_ehr - AnimalDelivery - AnimalDeliveryId - AnimalDeliveryId - - - - Cost Center - - ehr_lookups - cost_center - value - title - - - - Card Format - - ehr_lookups - card_format - value - title - - - - Census Activity Status - - ehr_lookups - census_activity_status - value - title - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/CageCardHistory.query.xml b/nbri_ehr/resources/queries/nbri_ehr/CageCardHistory.query.xml deleted file mode 100644 index 088f8b4..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/CageCardHistory.query.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - Cage Card History - - - - Account Staff - http://www.labkey.org/types#userId - - - Account Staff - http://www.labkey.org/types#userId - - - Animal Delivery - - nbri_ehr - AnimalDelivery - AnimalDeliveryId - AnimalDeliveryId - - - - Cost Center - - ehr_lookups - cost_center - value - title - - - - Card Format - - ehr_lookups - card_format - value - title - - - - Census Activity Status - - ehr_lookups - census_activity_status - value - title - - - - Account - - nbri_ehr - Account - AccountId - AccountNumber - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml b/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml index 7fc6298..32e2b50 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml +++ b/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml @@ -13,9 +13,7 @@ Conception Date - - - Conception Term Date + true Estimated diff --git a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql index ae7697f..68b07b4 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql +++ b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql @@ -7,7 +7,6 @@ SELECT c.Dam AS Id, c.ConceptId, c.ConceptDate, - c.ConceptTermDate, c.Estimated, c.Sire, CASE diff --git a/nbri_ehr/resources/queries/nbri_ehr/DeletedRecord.query.xml b/nbri_ehr/resources/queries/nbri_ehr/DeletedRecord.query.xml deleted file mode 100644 index 6b894a7..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/DeletedRecord.query.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - Deleted Record - - - Esig Event - - ehr_lookups - esig_events - value - title - - - - ESignature User - http://www.labkey.org/types#userId - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/Department.query.xml b/nbri_ehr/resources/queries/nbri_ehr/Department.query.xml deleted file mode 100644 index 91062cf..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/Department.query.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - Department - - - - Parent Department - - nbri_ehr - Department - DepartmentId - Name - - - - Staff - http://www.labkey.org/types#userId - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/IdHistory.query.xml b/nbri_ehr/resources/queries/nbri_ehr/IdHistory.query.xml deleted file mode 100644 index 686bf4c..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/IdHistory.query.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Id History - - - Id - - study - Animal - Id - Id - - - - Type - - ehr_lookups - id_history_type - Value - Title - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/Lot.query.xml b/nbri_ehr/resources/queries/nbri_ehr/Lot.query.xml deleted file mode 100644 index a5de660..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/Lot.query.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - Animal Lot - - - Animal Shipment - - nbri_ehr - AnimalShipment - AnimalShipmentId - AnimalShipmentId - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/ProtocolEsig.query.xml b/nbri_ehr/resources/queries/nbri_ehr/ProtocolEsig.query.xml deleted file mode 100644 index 366d740..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/ProtocolEsig.query.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - Protocol ESignature - - - - Esig Id - - - Esig Event - - ehr_lookups - esig_events - value - title - - - - User Profile - http://www.labkey.org/types#userId - - - Esig Date - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/ProtocolProcedures.query.xml b/nbri_ehr/resources/queries/nbri_ehr/ProtocolProcedures.query.xml deleted file mode 100644 index 27ee2e0..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/ProtocolProcedures.query.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Protocol Procedures - - - - Procedures - - ehr_lookups - procedures - name - name - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/ProtocolStress.query.xml b/nbri_ehr/resources/queries/nbri_ehr/ProtocolStress.query.xml deleted file mode 100644 index 2b1ca94..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/ProtocolStress.query.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Protocol Stress - - - - Stress - - nbri_ehr - Stress - StressId - Name - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/QuestionResponse.js b/nbri_ehr/resources/queries/nbri_ehr/QuestionResponse.js deleted file mode 100644 index 537630a..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/QuestionResponse.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2026 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 - */ -require("ehr/triggers").initScript(this); - -EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_INSERT, 'nbri_ehr', 'QuestionResponse', function (helper, scriptErrors, row, oldRow) { - - if (row.Response) { - plainTextParts = row.Response.split("\\loch\\af0") - let plainText = ""; - for(let i = 1; i < plainTextParts.length; i++) - { - let chunk = LABKEY.Utils.encodeHtml(plainTextParts[i].split("}")[0]); - chunk = chunk.replace("\\hich\\af0 \\'85", "…"); // ellipsis - chunk = chunk.replace("\\hich\\af0 \\'92", "'"); - chunk = chunk.replace("\\hich\\af0 \\'93", "\""); - chunk = chunk.replace("\\hich\\af0 \\'94", "\""); - chunk = chunk.replace("\\hich\\af0 \\'99", "™"); // trademark - chunk = chunk.replace("\\hich\\af0 \\'ae", "®"); // registered trademark - chunk = chunk.replace("\\hich\\af0 \\'b0", "°"); // Degree sign - chunk = chunk.replace("\\hich\\af0 \\'b5", "µ"); // Micro sign (Greek mu) - chunk = chunk.replace("\\hich\\af0 \\'b7", "·"); // Middle dot - chunk = chunk.replace("\\hich\\af0 \\'ef", "ï"); // i-diaeresis - // TODO: As a general catch all, could convert the hex value in the match to the HTML ascii entity - // Currently this covers all the cases though in the source data - - plainText += chunk.trim(); - plainText += " "; - } - row.Response = plainText.trim(); - } - -}); \ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/QuestionResponse.query.xml b/nbri_ehr/resources/queries/nbri_ehr/QuestionResponse.query.xml deleted file mode 100644 index 218470d..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/QuestionResponse.query.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - Question Response - - - Protocol - - ehr - protocol - objectid - protocol - - - - Question - - nbri_ehr - Question - QuestionId - Text - - - - Questionnaire - - ehr_lookups - questionnaire - value - title - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/locationTypes/.qview.xml b/nbri_ehr/resources/queries/nbri_ehr/locationTypes/.qview.xml deleted file mode 100644 index b1d4016..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/locationTypes/.qview.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/locations.query.xml b/nbri_ehr/resources/queries/nbri_ehr/locations.query.xml deleted file mode 100644 index 3f4fe51..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/locations.query.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - Location Type - - nbri_ehr - locationTypes - locationTypeId - name - - - -
-
-
-
diff --git a/nbri_ehr/resources/queries/nbri_ehr/locations/.qview.xml b/nbri_ehr/resources/queries/nbri_ehr/locations/.qview.xml deleted file mode 100644 index 08af13d..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/locations/.qview.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/locationsMapping.query.xml b/nbri_ehr/resources/queries/nbri_ehr/locationsMapping.query.xml deleted file mode 100644 index d2357c7..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/locationsMapping.query.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - Location - - nbri_ehr - locations - locationId - name - - - - Parent Location - - nbri_ehr - locations - locationId - name - - - -
-
-
-
diff --git a/nbri_ehr/resources/queries/nbri_ehr/locationsMapping/.qview.xml b/nbri_ehr/resources/queries/nbri_ehr/locationsMapping/.qview.xml deleted file mode 100644 index 6496f3b..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/locationsMapping/.qview.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/staff.query.xml b/nbri_ehr/resources/queries/nbri_ehr/staff.query.xml deleted file mode 100644 index c39c37c..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/staff.query.xml +++ /dev/null @@ -1,128 +0,0 @@ - - - - - Staff - - - First Name - - - Last Name - - - Middle Name - - - Display Name - - - Hire Date - - - Last Employ Date - - - Office Phone - - - Office Phone Ext - - - Office Fax - - - Home Phone - - - Beeper Phone - - - Cell Phone - - - Emergency Contact - - - Emergency Phone - - - Home Address 1 - - - Home Address2 - - - Home City - - - Home State - - - Home Zip - - - Home Zip Ext - - - Home Country - - - Office Address 1 - - - Office Address 2 - - - Office City - - - Office State - - - Office Zip - - - Office Zip Ext - - - Office Country - - - Birth Date - - - High School Name - - - College Grad Date - - - College Degree - - - College Major - - - College Name - - - Supervisor - - nbri_ehr - staff - staffId - displayName - - - - Position Name - - - Position Description - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/nbri_ehr/stress.query.xml b/nbri_ehr/resources/queries/nbri_ehr/stress.query.xml deleted file mode 100644 index 590673d..0000000 --- a/nbri_ehr/resources/queries/nbri_ehr/stress.query.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - Stress Levels - - - Regulatory Stress Level - - ehr_lookups - regulatory_stress_levels - value - title - - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/aliases.sql b/nbri_ehr/resources/queries/study/aliases.sql index 4ed9962..e6c16d0 100644 --- a/nbri_ehr/resources/queries/study/aliases.sql +++ b/nbri_ehr/resources/queries/study/aliases.sql @@ -9,10 +9,6 @@ SELECT Id, Id as alias FROM study.Animal where Dataset.Demographics.calculated_status != 'Alive - In Progress' UNION -SELECT Id, - Name as alias -FROM nbri_ehr.IdHistory -UNION SELECT Id, Alias as alias FROM study.alias where Id.demographics.calculated_status != 'Alive - In Progress' \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/birth.query.xml b/nbri_ehr/resources/queries/study/birth.query.xml index 18a5a92..034b7f0 100644 --- a/nbri_ehr/resources/queries/study/birth.query.xml +++ b/nbri_ehr/resources/queries/study/birth.query.xml @@ -42,15 +42,6 @@ title
- - Birth Condition - - ehr_lookups - birth_condition - value - title - - Project diff --git a/nbri_ehr/resources/queries/study/demographics.query.xml b/nbri_ehr/resources/queries/study/demographics.query.xml index f6fafa6..01fdfc9 100644 --- a/nbri_ehr/resources/queries/study/demographics.query.xml +++ b/nbri_ehr/resources/queries/study/demographics.query.xml @@ -82,14 +82,6 @@ Animal Status - - Lot - - nbri_ehr - Lot - LotId - - Origin diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml index 2ae212d..4acb1e6 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -227,9 +227,6 @@ varchar - - varchar - varchar @@ -472,9 +469,6 @@ varchar - - integer - varchar diff --git a/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.001-26.002.sql b/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.001-26.002.sql new file mode 100644 index 0000000..afac7bc --- /dev/null +++ b/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.001-26.002.sql @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +ALTER TABLE nbri_ehr.Conception DROP COLUMN ConceptTermDate; + +-- Drop the tables carried over from the legacy system that nothing in the module reads or writes. Conception is the +-- only table left in the schema afterward. Each DROP also removes that table's primary key and its +-- IX_..._Container index, so no separate DROP INDEX is needed. + +DROP TABLE IF EXISTS nbri_ehr.CageCardHistory; +DROP TABLE IF EXISTS nbri_ehr.CageCard; +DROP TABLE IF EXISTS nbri_ehr.AnimalDeliveryEsig; +DROP TABLE IF EXISTS nbri_ehr.AnimalReqOrderEsig; +DROP TABLE IF EXISTS nbri_ehr.AnimalDelivery; +DROP TABLE IF EXISTS nbri_ehr.AnimalReqOrder; +DROP TABLE IF EXISTS nbri_ehr.Lot; +DROP TABLE IF EXISTS nbri_ehr.AnimalShipment; +DROP TABLE IF EXISTS nbri_ehr.AnimalVendor; +DROP TABLE IF EXISTS nbri_ehr.ShipTo; + +DROP TABLE IF EXISTS nbri_ehr.Account; +DROP TABLE IF EXISTS nbri_ehr.Department; + +DROP TABLE IF EXISTS nbri_ehr.ProtocolStress; +DROP TABLE IF EXISTS nbri_ehr.Stress; +DROP TABLE IF EXISTS nbri_ehr.ProtocolProcedures; +DROP TABLE IF EXISTS nbri_ehr.ProtocolEsig; +DROP TABLE IF EXISTS nbri_ehr.ProtocolUsage; + +DROP TABLE IF EXISTS nbri_ehr.LocationsMapping; +DROP TABLE IF EXISTS nbri_ehr.Locations; +DROP TABLE IF EXISTS nbri_ehr.LocationTypes; + +DROP TABLE IF EXISTS nbri_ehr.QuestionResponse; +DROP TABLE IF EXISTS nbri_ehr.Question; +DROP TABLE IF EXISTS nbri_ehr.DeletedRecord; +DROP TABLE IF EXISTS nbri_ehr.Staff; +DROP TABLE IF EXISTS nbri_ehr.IdHistory; diff --git a/nbri_ehr/resources/schemas/nbri_ehr.xml b/nbri_ehr/resources/schemas/nbri_ehr.xml index 2bc125d..9c11a59 100644 --- a/nbri_ehr/resources/schemas/nbri_ehr.xml +++ b/nbri_ehr/resources/schemas/nbri_ehr.xml @@ -2,573 +2,6 @@ - - Location Types - DETAILED - - - - - - - - - - -
- - - Locations - DETAILED - - - - - - - - - - - -
- - - Locations Mapping - DETAILED - - - - - - - - - - - -
- - - Staff - DETAILED - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - Lot - DETAILED - - - - - - - - - - - -
- - - Animal Shipment - DETAILED - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - Animal Delivery - DETAILED - - - - - - - - - - - - - - - - - - - - - - - - -
- - - Animal Delivery ESignature - DETAILED - - - - - - - - - - - - -
- - - Animal Req Order - DETAILED - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - Animal Req Order ESignature - DETAILED - - - - - - - - - - - - -
- - - Animal Vendor - DETAILED - - - - - - - - - - - - - - - - - - - - - - -
- - - Ship To - DETAILED - - - - - - - - - - - - - - - - -
- - - Protocol ESignature - DETAILED - - - - - - - - - - - - -
- - - Protocol Usage - DETAILED - - - - - - - - - - - - - - - - - - - - - - - - -
- - - Account - DETAILED - - - - - - - - - - - - - - - - -
- - - Department - DETAILED - - - - - - - - - - - - -
- - - Id History - DETAILED - - - - - - - - - - -
- - - Deleted Record - DETAILED - - - - - - - - - - - - - - - -
- - - Question - DETAILED - - - - - - - - - - - -
- - - Question Response - DETAILED - - - - - - - - - - - - -
- - - Protocol Stress - DETAILED - - - - - - - - - - - - -
- - - Stress - DETAILED - - - - - - - - - - - - - -
- - - Protocol Procedures - DETAILED - - - - - - - - - - - -
- - - Cage Card - DETAILED - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - Cage Card History - DETAILED - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ConceptionDETAILED @@ -578,9 +11,6 @@ Date - - Date - @@ -595,4 +25,4 @@
-
\ No newline at end of file + diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js index f266f1e..0b76f92 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js @@ -32,12 +32,35 @@ EHR.model.DataModelManager.registerMetadata('Birth', { allowBlank: false, nullable: false }, + // conception Id, species, dam and sire all come from the conception picked in the Start with Conception + // window, so they are shown but not entered by hand. That window writes to the store directly, which is + // unaffected by these read-only editor settings. 'Id/demographics/species': { allowBlank: false, nullable: false, columnConfig: { fixed: true, - width: 250 + width: 250, + editable: false + }, + formEditorConfig: { + readOnly: true + } + }, + 'Id/demographics/dam': { + columnConfig: { + editable: false + }, + formEditorConfig: { + readOnly: true + } + }, + 'Id/demographics/sire': { + columnConfig: { + editable: false + }, + formEditorConfig: { + readOnly: true } }, 'cage': { @@ -52,11 +75,6 @@ EHR.model.DataModelManager.registerMetadata('Birth', { width: 200 }, }, - cond: { - columnConfig: { - width: 200 - }, - }, // project and protocol are entered through the Project Assignment and Protocol Assignment sections project: { allowBlank: true, @@ -75,11 +93,16 @@ EHR.model.DataModelManager.registerMetadata('Birth', { allowBlank: false, nullable: false }, + // see the note above on the fields the Start with Conception window populates conceptId: { allowBlank: false, nullable: false, columnConfig: { - width: 150 + width: 150, + editable: false + }, + formEditorConfig: { + readOnly: true } }, breedingType: { diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Conception.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Conception.js index 949d8cb..076ce02 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Conception.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Conception.js @@ -24,13 +24,8 @@ EHR.model.DataModelManager.registerMetadata('Conception', { ConceptDate: { xtype: 'datefield', extFormat: LABKEY.extDefaultDateFormat, - columnConfig: { - width: 200 - }, - }, - ConceptTermDate: { - xtype: 'datefield', - extFormat: LABKEY.extDefaultDateFormat, + allowBlank: false, + nullable: false, columnConfig: { width: 200 }, diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java index aa76225..c3ee1d2 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java @@ -80,7 +80,7 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 26.001; + return 26.002; } @Override diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java index ddecd53..10c8ab7 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java @@ -38,7 +38,6 @@ public class NBRIBirthFormSection extends NewAnimalFormSection FieldKey.fromString("Id/demographics/sire"), FieldKey.fromString("cage"), FieldKey.fromString("type"), - FieldKey.fromString("cond"), FieldKey.fromString("breedingType"), FieldKey.fromString("remark"), FieldKey.fromString("performedby") diff --git a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java index afaab10..b11d6ab 100644 --- a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java +++ b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java @@ -863,7 +863,6 @@ public void testConceptionForm() _helper.addRecordToGrid(conceptions); conceptions.setGridCell(1, "ConceptId", conceptId); conceptions.setGridCellJS(1, "ConceptDate", now.minusDays(30).format(_dateFormat)); - conceptions.setGridCellJS(1, "ConceptTermDate", now.plusDays(135).format(_dateFormat)); conceptions.setGridCellJS(1, "Estimated", true); conceptions.setGridCell(1, "Dam", damId); conceptions.setGridCell(1, "Sire", sireId); @@ -1963,7 +1962,7 @@ private void createBreedingPair(String damId, String sireId, String species) thr private void verifyBirthColumnOrder(Ext4GridRef births) { List expectedOrder = List.of("Id", "date", "conceptId", "Id/demographics/species", "Id/demographics/gender", - "Id/demographics/dam", "Id/demographics/sire", "cage", "type", "cond", "breedingType", "remark", "performedby"); + "Id/demographics/dam", "Id/demographics/sire", "cage", "type", "breedingType", "remark", "performedby"); int previousIdx = 0; String previousCol = null; From 8f8c7a97a102bc6c69efdf47c594c7c840a1cba7 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 10 Aug 2026 09:41:44 -0600 Subject: [PATCH 02/18] Derive demographics birth/death from the birth and death records The birth, arrival and death forms each wrote demographics.birth and demographics.death independently, and the death form also hand-set calculated_status. One trigger helper now derives both dates from the saved event records, and status is left to the shared recalc that owns the death/departure precedence. Adds a drift query for reconciling values the old paths left stale. --- nbri_ehr/resources/queries/study/arrival.js | 30 ++++-- nbri_ehr/resources/queries/study/birth.js | 25 ++++- nbri_ehr/resources/queries/study/deaths.js | 44 +++++---- .../study/diagnosticsBirthDeathDrift.sql | 69 +++++++++++++ .../nbri_ehr/query/NBRI_EHRTriggerHelper.java | 97 +++++++++++++++++++ 5 files changed, 236 insertions(+), 29 deletions(-) create mode 100644 nbri_ehr/resources/queries/study/diagnosticsBirthDeathDrift.sql diff --git a/nbri_ehr/resources/queries/study/arrival.js b/nbri_ehr/resources/queries/study/arrival.js index 2010ffd..80761d7 100644 --- a/nbri_ehr/resources/queries/study/arrival.js +++ b/nbri_ehr/resources/queries/study/arrival.js @@ -6,6 +6,13 @@ require("ehr/triggers").initScript(this); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); +var idsToSync = []; + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'Arrival', function(event, helper){ + + // the script scope can outlive a single save, so never inherit ids from a prior one + idsToSync = []; +}); EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'study', 'Arrival', function(helper, scriptErrors, row, oldRow) { @@ -132,12 +139,6 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even hasUpdates = true; } - if (row.birth && row.birth !== data.birth) - { - obj.birth = row.birth; - hasUpdates = true; - } - if (row.sire && row.sire !== data.sire) { obj.sire = row.sire; @@ -171,5 +172,22 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even helper.cacheDemographics(row.Id, row); } } + + if (row.Id && idsToSync.indexOf(row.Id) === -1) { + idsToSync.push(row.Id); + } + } +}); + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'study', 'Arrival', function(event, errors, helper){ + + // Single writer for the denormalized demographics birth date. saveBirthRecord() above wrote the birth record; + // this reads it back so demographics and the event record cannot disagree. + if (!helper.isETL() && idsToSync.length) { + var demographicsUpdates = triggerHelper.computeDemographicsSync(idsToSync); + if (demographicsUpdates.size() > 0) { + helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates); + } + idsToSync = []; } }); \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/birth.js b/nbri_ehr/resources/queries/study/birth.js index cd0cce1..52aa379 100644 --- a/nbri_ehr/resources/queries/study/birth.js +++ b/nbri_ehr/resources/queries/study/birth.js @@ -7,6 +7,7 @@ require("ehr/triggers").initScript(this); EHR.Server.Utils = require("ehr/utils").EHR.Server.Utils; var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); +var idsToSync = []; function onInit(event, helper){ helper.setScriptOptions({ @@ -21,9 +22,24 @@ function onInit(event, helper){ skipAssignmentCheck: true, }); + // the script scope can outlive a single save, so never inherit ids from a prior one + idsToSync = []; + helper.decodeExtraContextProperty('birthsInTransaction'); } +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'study', 'birth', function(event, errors, helper){ + + // Single writer for the denormalized demographics birth date, derived from the saved birth records. + if (!helper.isETL() && idsToSync.length) { + var demographicsUpdates = triggerHelper.computeDemographicsSync(idsToSync); + if (demographicsUpdates.size() > 0) { + helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates); + } + idsToSync = []; + } +}); + EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'study', 'birth', function(helper, scriptErrors, row, oldRow) { if (!oldRow && row.Id && triggerHelper.birthExists(row.Id)) { @@ -147,11 +163,6 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even hasUpdates = true; } - if (obj.birth && obj.birth !== data.birth) { - record.birth = obj.birth; - hasUpdates = true; - } - if (obj.sire && obj.sire !== data.sire) { record.sire = obj.sire; hasUpdates = true; @@ -180,6 +191,10 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even helper.cacheDemographics(row.Id, row); } } + + if (row.Id && idsToSync.indexOf(row.Id) === -1) { + idsToSync.push(row.Id); + } } } }); \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/deaths.js b/nbri_ehr/resources/queries/study/deaths.js index abe7bc4..c7dae52 100644 --- a/nbri_ehr/resources/queries/study/deaths.js +++ b/nbri_ehr/resources/queries/study/deaths.js @@ -8,9 +8,13 @@ require("ehr/triggers").initScript(this); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); var idMap = {}; var deathIdMap = {}; +var idsToSync = []; function onInit(event, helper){ + // the script scope can outlive a single save, so never inherit ids from a prior one + idsToSync = []; + helper.decodeExtraContextProperty('deathsInTransaction'); // Cache valid Ids for check on each row @@ -59,7 +63,6 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even demographicsUpdates.push({ Id: row.Id, death: null, - calculated_status: 'Alive', QCState: helper.getJavaHelper().getQCStateForLabel('Completed').getRowId(), }); @@ -69,8 +72,6 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even function onUpsert(helper, scriptErrors, row, oldRow) { - var demographicsUpdates = []; - if (!helper.isETL()) { //skip other checks so that the admins can update a death record @@ -130,21 +131,6 @@ function onUpsert(helper, scriptErrors, row, oldRow) { EHR.Server.Utils.addError(scriptErrors, 'Id', errorMsg, 'ERROR'); } else { - if (!helper.isValidateOnly() && row.Id && row.date && rowQCState === 'COMPLETED') { - - // update demographics - demographicsUpdates.push({ - Id: row.Id, - death: row.date, - calculated_status: 'Dead', - QCState: helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId() - }); - - console.log('updating demographics death date for animal: ' + row.Id); - helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates); - console.log('updated demographics death date for animal: ' + row.Id); - } - if (!helper.isValidateOnly() && row.date && row.QCStateLabel && EHR.Server.Security.getQCStateByLabel(row.QCStateLabel).PublicData) { var qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); @@ -177,9 +163,31 @@ function onUpsert(helper, scriptErrors, row, oldRow) { EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.AFTER_INSERT, 'study', 'deaths', function(helper, scriptErrors, row, oldRow) { helper.registerDeath(row.Id, row.date); triggerHelper.reportDataChange("study", "deaths", [row.Id]); + + if (row.Id && idsToSync.indexOf(row.Id) === -1) { + idsToSync.push(row.Id); + } +}); + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.AFTER_UPDATE, 'study', 'deaths', function(helper, scriptErrors, row, oldRow) { + if (row.Id && idsToSync.indexOf(row.Id) === -1) { + idsToSync.push(row.Id); + } }); EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'study', 'Deaths', function(event, errors, helper){ + + // Single writer for the denormalized demographics death date. Runs once per save, after the death rows are saved, + // and derives the value from the stored record rather than from the incoming row. calculated_status is left to the + // shared status recalc, which owns the death/departure/re-arrival precedence. + if (!helper.isETL() && idsToSync.length) { + var demographicsUpdates = triggerHelper.computeDemographicsSync(idsToSync); + if (demographicsUpdates.size() > 0) { + helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates); + } + idsToSync = []; + } + var rows = helper.getRows() || []; for (var i = 0; i < rows.length; i++) { var row = rows[i].row; diff --git a/nbri_ehr/resources/queries/study/diagnosticsBirthDeathDrift.sql b/nbri_ehr/resources/queries/study/diagnosticsBirthDeathDrift.sql new file mode 100644 index 0000000..6ee5fd9 --- /dev/null +++ b/nbri_ehr/resources/queries/study/diagnosticsBirthDeathDrift.sql @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +/* + * Reports animals whose demographics birth/death values disagree with the birth and deaths event records that are + * supposed to feed them. + * + * demographics.birth and demographics.death are denormalized copies, written by trigger scripts rather than derived, + * so anything that bypasses those triggers -- ETL loads, admin edits, a partially failed save -- leaves them stale. + * The framework reads the demographics copy (not the event record) for age, lastDayAtCenter and status, so drift here + * is silently wrong data everywhere those appear. + * + * Only public (Completed) event records count as backing, and draft demographics records are excluded, so rows still + * in data entry are not reported. Each animal yields at most one row; the three drift columns are independent and can + * be filtered separately in the grid. + * + * Note: on a container populated by ETL or legacy import, "no completed birth record" can be the common case rather + * than the exception. Filter birthDrift to triage. + */ + +SELECT * FROM ( + SELECT + ids.Id, + dem.calculated_status, + + dem.birth AS demographicsBirth, + b.date AS birthRecordDate, + CASE + WHEN dem.Id IS NULL THEN 'Event record exists with no demographics record' + WHEN dem.birth IS NULL AND b.Id IS NOT NULL THEN 'Birth record exists but demographics birth is empty' + WHEN dem.birth IS NOT NULL AND b.Id IS NULL THEN 'Demographics birth is set with no completed birth record' + WHEN CAST(dem.birth AS DATE) <> CAST(b.date AS DATE) THEN 'Birth dates disagree' + END AS birthDrift, + + dem.death AS demographicsDeath, + d.date AS deathRecordDate, + CASE + WHEN dem.Id IS NULL THEN 'Event record exists with no demographics record' + WHEN dem.death IS NULL AND d.Id IS NOT NULL THEN 'Death record exists but demographics death is empty' + WHEN dem.death IS NOT NULL AND d.Id IS NULL THEN 'Demographics death is set with no completed death record' + WHEN CAST(dem.death AS DATE) <> CAST(d.date AS DATE) THEN 'Death dates disagree' + END AS deathDrift, + + CASE + WHEN dem.Id IS NULL THEN NULL + WHEN d.Id IS NOT NULL AND (dem.calculated_status IS NULL OR dem.calculated_status <> 'Dead') + THEN 'Completed death record but status is not Dead' + WHEN d.Id IS NULL AND dem.calculated_status = 'Dead' + THEN 'Status is Dead with no completed death record' + END AS statusDrift, + + dem.QCState.PublicData AS demographicsIsPublic + + FROM ( + SELECT Id FROM study.demographics + UNION + SELECT Id FROM study.birth + UNION + SELECT Id FROM study.deaths + ) ids + LEFT JOIN study.demographics dem ON ids.Id = dem.Id + LEFT JOIN (SELECT Id, date FROM study.birth WHERE QCState.PublicData = true) b ON ids.Id = b.Id + LEFT JOIN (SELECT Id, date FROM study.deaths WHERE QCState.PublicData = true) d ON ids.Id = d.Id +) t +WHERE (t.demographicsIsPublic = true OR t.demographicsIsPublic IS NULL) + AND (t.birthDrift IS NOT NULL OR t.deathDrift IS NOT NULL OR t.statusDrift IS NOT NULL) diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java b/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java index 662ab54..893c53a 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java @@ -323,6 +323,103 @@ public boolean deathExists(String id) return false; } + /** + * Derives the denormalized birth/death values on study.demographics from the birth and deaths event records, which + * are authoritative, and returns only the animals whose stored values disagree. The result is intended to be handed + * straight to the shared trigger helper's updateDemographicsRecord(), so that lsid resolution and the demographics + * cache recache stay in the single place that already handles them. + *

+ * Only public (Completed) event records count, so a record still in data entry never overwrites a saved value. + *

+ * calculated_status is deliberately absent from the result. It belongs to the shared status recalc, which owns the + * death/departure/re-arrival precedence. + *

+ * Every lookup is set-based - one query per event dataset for the whole id list, not one per animal - because a + * bulk save can pass hundreds of ids and per-animal SQL in a trigger exhausts the script's wall-clock budget. + * + * @param ids animals touched by the current save + * @return rows ready for updateDemographicsRecord(); empty when nothing has drifted + */ + public List> computeDemographicsSync(List ids) + { + if (ids == null || ids.isEmpty()) + return Collections.emptyList(); + + Set idSet = new HashSet<>(ids); + + Map births = getPublicEventDates("birth", idSet); + Map deaths = getPublicEventDates("deaths", idSet); + + List> updates = new ArrayList<>(); + + TableInfo demographics = getTableInfo("study", "demographics"); + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id"), idSet, CompareType.IN); + TableSelector ts = new TableSelector(demographics, PageFlowUtil.set("Id", "birth", "death"), filter, null); + + for (Map current : ts.getMapCollection()) + { + String id = (String)current.get("Id"); + Map update = new CaseInsensitiveHashMap<>(); + + // A public event record always wins - the same rule createDemographicsRecord() already applies to death on + // insert, extended to updates and to birth. The absence of an event record is NOT evidence the stored value + // is wrong: animals loaded by ETL, or acquired before these datasets were in use, legitimately carry a date + // with no event row, so a missing record leaves the value alone. Clearing a value is only ever driven by an + // explicit delete of the event record. + Date birth = births.get(id); + if (birth != null && differsByDay(birth, (Date)current.get("birth"))) + update.put("birth", birth); + + Date death = deaths.get(id); + if (death != null && differsByDay(death, (Date)current.get("death"))) + update.put("death", death); + + if (!update.isEmpty()) + { + update.put("Id", id); + updates.add(update); + } + } + + if (!updates.isEmpty()) + _log.info("Demographics birth/death out of sync with event records for {} animal(s); updating", updates.size()); + + return updates; + } + + /** Most recent public event date per animal for a demographic event dataset, in a single query. */ + private Map getPublicEventDates(String queryName, Set ids) + { + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id"), ids, CompareType.IN); + filter.addCondition(FieldKey.fromString("qcstate/publicdata"), true); + + Map ret = new HashMap<>(); + new TableSelector(getTableInfo("study", queryName), PageFlowUtil.set("Id", "date"), filter, null) + .forEachMap(row -> { + String id = (String)row.get("Id"); + Date date = ConvertHelper.convert(row.get("date"), Date.class); + // birth and deaths are demographic datasets (one row per animal), but tolerate duplicates from a + // legacy load by keeping the latest rather than picking arbitrarily. + if (date != null && (ret.get(id) == null || date.after(ret.get(id)))) + ret.put(id, date); + }); + + return ret; + } + + /** + * Compares to day precision. Event dates are entered with the time stripped, but values that arrived by ETL or + * predate that behavior can carry a time component; treating those as drift would rewrite the whole colony on the + * first save. + */ + private boolean differsByDay(Date a, Date b) + { + if (a == null || b == null) + return a != b; + + return !DateUtils.isSameDay(a, b); + } + public boolean upsertWeightRecord(Map row) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException { return upsertWeightRecord(row, true); From 66014ba7779a5d6f5e0365784951b444bdee8cb6 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 10 Aug 2026 13:10:53 -0600 Subject: [PATCH 03/18] Cover the demographics birth/death sync and align the sample data Adds a test for deleting a death record, which has to hand the status back to the shared recalc rather than assume the animal is alive, and asserts the birth and death dates reach demographics on the arrival, birth and death forms. The birth and deaths sample data disagreed with demographics on nine rows and now matches, leaving only the animals that legitimately have no birth record. --- .../study/study/datasets/datasetBirth.tsv | 14 +-- .../study/study/datasets/datasetDeaths.tsv | 4 +- .../tests.nbri_ehr/NBRI_EHRTest.java | 103 +++++++++++++++++- 3 files changed, 111 insertions(+), 10 deletions(-) diff --git a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetBirth.tsv b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetBirth.tsv index cecdcb7..6ca8a47 100644 --- a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetBirth.tsv +++ b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetBirth.tsv @@ -1,9 +1,9 @@ objectid Id date QCStateLabel performedby -1 44444 -2895d Completed 1004 -2 44445 -2854d Completed 1004 -3 44446 -2703d Completed 1004 +1 44444 -1381d Completed 1004 +2 44445 -1414d Completed 1004 +3 44446 -1406d Completed 1004 4 44447 -2600d Completed 1004 -5 TEST6390238 -5601d Completed 1004 -6 TEST5904521 -5602d Completed 1004 -7 TEST3804589 -2603d Completed 1004 -8 TEST2312318 -3000d Completed 1004 +5 TEST6390238 -3923d Completed 1004 +6 TEST5904521 -5431d Completed 1004 +7 TEST3804589 -5806d Completed 1004 +8 TEST2312318 -8069d Completed 1004 diff --git a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDeaths.tsv b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDeaths.tsv index 0c46745..0a481b8 100644 --- a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDeaths.tsv +++ b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDeaths.tsv @@ -1,3 +1,3 @@ objectid Id date QCStateLabel cause manner remark performedby -1 TEST1993532 -2212d Completed 1 S sed nibh viverra 1004 -2 44445 -723d Completed 2 X labor omnia vincit 1004 +1 TEST1993532 -2259d Completed 1 S sed nibh viverra 1004 +2 44445 -726d Completed 2 X labor omnia vincit 1004 diff --git a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java index b11d6ab..bcd4b33 100644 --- a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java +++ b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java @@ -71,6 +71,7 @@ import java.io.File; import java.io.IOException; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; @@ -632,7 +633,7 @@ public void testWeightValidation() } @Test - public void testArrivalForm() + public void testArrivalForm() throws IOException, CommandException { String arrivedAnimal = "30905"; LocalDateTime now = LocalDateTime.now(); @@ -692,6 +693,13 @@ public void testArrivalForm() verifyRowCreated("study", "protocolAssignment", arrivedAnimal, 1); verifyRowCreated("study", "demographics", arrivedAnimal, 1); verifyRowCreated("study", "housing", arrivedAnimal, 1); + + log("Verifying the birth date reached demographics and agrees with the birth record"); + String arrivalBirthDay = now.minusDays(7).format(_dateFormat); + assertEquals("Birth record does not carry the birth date entered on the arrival form", + arrivalBirthDay, getDatasetDay("birth", arrivedAnimal, "date")); + assertEquals("Demographics birth date does not match the birth record", + arrivalBirthDay, getDatasetDay("demographics", arrivedAnimal, "birth")); } @Test @@ -794,6 +802,13 @@ public void testBirthForm() throws Exception verifyRowCreated("study", "housing", bornAnimal, 1); verifyRowCreated("study", "demographics", bornAnimal, 1); + log("Verifying the birth date reached demographics and agrees with the birth record"); + String bornBirthDay = now.minusDays(1).format(_dateFormat); + assertEquals("Birth record does not carry the date entered on the birth form", + bornBirthDay, getDatasetDay("birth", bornAnimal, "date")); + assertEquals("Demographics birth date does not match the birth record", + bornBirthDay, getDatasetDay("demographics", bornAnimal, "birth")); + log("Verifying conception outcome and offspring in ConceptionsByDam"); goToSchemaBrowser(); DataRegionTable report = viewQueryData("nbri_ehr", "ConceptionsByDam"); @@ -1453,6 +1468,14 @@ public void testDeathNecropsyForm() throws IOException, CommandException goToEHRFolder(); verifyRowCreated("study", "weight", aliveAnimalId, 1); + log("Verify the death date reached demographics and agrees with the death record"); + String finalizedDeathDay = getDatasetDay("deaths", aliveAnimalId, "date"); + Assert.assertNotNull("Death record has no date", finalizedDeathDay); + assertEquals("Demographics death date does not match the death record", + finalizedDeathDay, getDatasetDay("demographics", aliveAnimalId, "death")); + // the waitForText below is a weak check: the necropsy diagnosis on this page is also the text "Dead" + assertEquals("Animal should be Dead once the death is finalized", "Dead", getCalculatedStatus(aliveAnimalId)); + log("Verify animal is marked as dead"); AnimalHistoryPage historyPage = AnimalHistoryPage.beginAt(this); historyPage.searchSingleAnimal(aliveAnimalId); @@ -1478,6 +1501,48 @@ public void testDeathNecropsyForm() throws IOException, CommandException } + /** + * Deleting a death record has to hand the status back to the shared recalc rather than assume the animal is alive: + * an animal that also has a departure is Shipped, not Alive. Also covers demographics.death being cleared. + */ + @Test + public void testDeathDeleteRestoresDepartedStatus() throws Exception + { + String animalId = "DD9001"; + LocalDateTime now = LocalDateTime.now(); + + log("Creating an animal via a birth record"); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), getApiHelper().prepareInsertCommand("study", "birth", "lsid", + new String[]{"Id", "Date", "gender", "QCStateLabel", "performedby"}, + new Object[][]{{animalId, now.minusDays(30), getMale(), "Completed", 1004}} + ), getExtraContext()); + + // the death has to be recorded before the departure: the deaths trigger rejects an animal that has shipped + log("Recording the death"); + InsertRowsCommand deaths = new InsertRowsCommand("study", "deaths"); + deaths.addRow(Map.of("Id", animalId, "date", now.minusDays(10), "reason", "4", "QCStateLabel", "Completed", "performedby", 1004)); + deaths.execute(getApiHelper().getConnection(), getContainerPath()); + + assertEquals("Demographics death date does not match the death record", + now.minusDays(10).format(_dateFormat), getDatasetDay("demographics", animalId, "death")); + assertEquals("Animal should be Dead while the death record exists", "Dead", getCalculatedStatus(animalId)); + + log("Departing the animal, so the deleted death has a departure to fall back to"); + InsertRowsCommand departure = new InsertRowsCommand("study", "departure"); + departure.addRow(Map.of("Id", animalId, "date", now.minusDays(5), "destination", "ORPRC", "QCStateLabel", "Completed", "performedby", 1004)); + departure.execute(getApiHelper().getConnection(), getContainerPath()); + + assertEquals("A death outranks a departure", "Dead", getCalculatedStatus(animalId)); + + log("Deleting the death record"); + getApiHelper().deleteAllRecords("study", "deaths", new Filter("Id", animalId)); + + Assert.assertNull("Demographics death date should be cleared when the death record is deleted", + getDatasetDay("demographics", animalId, "death")); + assertEquals("Deleting the death should fall back to the departure, not to Alive", + "Shipped", getCalculatedStatus(animalId)); + } + @Test public void testClinicalCasesWorkflow() { @@ -1980,6 +2045,42 @@ private void waitForFormError(String message) waitFor(() -> isTextPresent(message), "Form did not report: " + message, WAIT_FOR_JAVASCRIPT); } + /** + * Reads a date field for one animal through the API rather than off a grid, so assertions compare stored values + * instead of formatted display text, and normalizes to the day: event dates are entered with the time stripped, + * but values reaching demographics by other paths can carry a time component. + * + * @return the date as yyyy-MM-dd, or null when the field is empty + */ + private String getDatasetDay(String queryName, String animalId, String column) throws IOException, CommandException + { + Object value = getSingleRowForAnimal(queryName, animalId, List.of("Id", column)).get(column); + if (value == null) + return null; + + if (value instanceof Date) + return _dateFormat.format(((Date)value).toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); + + String text = String.valueOf(value); + return text.length() >= 10 ? text.substring(0, 10) : text; + } + + private String getCalculatedStatus(String animalId) throws IOException, CommandException + { + return (String)getSingleRowForAnimal("demographics", animalId, List.of("Id", "calculated_status")).get("calculated_status"); + } + + private Map getSingleRowForAnimal(String queryName, String animalId, List columns) throws IOException, CommandException + { + SelectRowsCommand select = new SelectRowsCommand("study", queryName); + select.setColumns(columns); + select.addFilter(new Filter("Id", animalId)); + SelectRowsResponse response = select.execute(getApiHelper().getConnection(), getContainerPath()); + + Assert.assertEquals("Expected exactly one study." + queryName + " row for " + animalId, 1, response.getRows().size()); + return response.getRows().get(0); + } + private void verifyRowCreated(String schema, String query, String animalId, int rowCount) { goToSchemaBrowser(); From c44e56021586e961be117b0bb22fa64a8a0d6c4f Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 17 Aug 2026 08:11:10 -0600 Subject: [PATCH 04/18] Record a social code on births and arrivals The code is entered once per animal, on the birth and arrival forms, and stored on demographics so it is available for animal selection and search. A new ehr_lookups.social_code lookup supplies the five codes. --- nbri_ehr/resources/data/editable_lookups.tsv | 1 + nbri_ehr/resources/data/lookup_sets.tsv | 1 + nbri_ehr/resources/data/lookupsManifest.tsv | 1 + .../resources/data/lookupsManifestTest.tsv | 1 + nbri_ehr/resources/data/social_code.tsv | 6 ++ nbri_ehr/resources/queries/study/arrival.js | 7 ++ nbri_ehr/resources/queries/study/birth.js | 6 ++ .../queries/study/demographics.query.xml | 9 ++ .../queries/study/demographics/.qview.xml | 1 + .../study/demographics/Search Panel.qview.xml | 90 +++++++++++++++++++ .../study/datasets/datasets_metadata.xml | 3 + .../web/nbri_ehr/model/sources/Arrival.js | 9 ++ .../web/nbri_ehr/model/sources/Birth.js | 8 ++ .../section/NBRIArrivalFormSection.java | 3 + .../section/NBRIBirthFormSection.java | 1 + .../tests.nbri_ehr/NBRI_EHRTest.java | 28 +++++- 16 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 nbri_ehr/resources/data/social_code.tsv create mode 100644 nbri_ehr/resources/queries/study/demographics/Search Panel.qview.xml diff --git a/nbri_ehr/resources/data/editable_lookups.tsv b/nbri_ehr/resources/data/editable_lookups.tsv index 700bffe..edde79b 100644 --- a/nbri_ehr/resources/data/editable_lookups.tsv +++ b/nbri_ehr/resources/data/editable_lookups.tsv @@ -115,6 +115,7 @@ ehr_lookups respiratory_observations Clinical Respiratory Observations Used in c ehr_lookups routes Clinical Treatment Routes Used in drug and treatment datasets. ehr_lookups sib_score Behavior SIB Score Behavior observation fixed values. ehr_lookups skin_problem Clinical Skin Problems Clinical observation fixed values. +ehr_lookups social_code Colony Management Social Code Rearing and acquisition codes recorded once per animal at birth or arrival. ehr_lookups source Colony Management Source ehr_lookups species Colony Management Species ehr_lookups snomed Research Treatments List of animal treatments. diff --git a/nbri_ehr/resources/data/lookup_sets.tsv b/nbri_ehr/resources/data/lookup_sets.tsv index e71f832..6cd8cba 100644 --- a/nbri_ehr/resources/data/lookup_sets.tsv +++ b/nbri_ehr/resources/data/lookup_sets.tsv @@ -100,6 +100,7 @@ req_order_type Req Order Type value title respiratory_observations Respiratory Observations value title sib_score SIB Score value skin_problem Skin Problem value +social_code Social Code value title status_codes Status Code Field Values value title stool_score Stool Score value stool_types Stool Types value diff --git a/nbri_ehr/resources/data/lookupsManifest.tsv b/nbri_ehr/resources/data/lookupsManifest.tsv index ccc18c5..19aa5a4 100644 --- a/nbri_ehr/resources/data/lookupsManifest.tsv +++ b/nbri_ehr/resources/data/lookupsManifest.tsv @@ -114,6 +114,7 @@ snomed species species_codes skin_problem +social_code status_codes stool_score stool_types diff --git a/nbri_ehr/resources/data/lookupsManifestTest.tsv b/nbri_ehr/resources/data/lookupsManifestTest.tsv index 64b5720..8d028e6 100644 --- a/nbri_ehr/resources/data/lookupsManifestTest.tsv +++ b/nbri_ehr/resources/data/lookupsManifestTest.tsv @@ -112,6 +112,7 @@ respiratory_observations routes sib_score skin_problem +social_code source snomed species diff --git a/nbri_ehr/resources/data/social_code.tsv b/nbri_ehr/resources/data/social_code.tsv new file mode 100644 index 0000000..448cedc --- /dev/null +++ b/nbri_ehr/resources/data/social_code.tsv @@ -0,0 +1,6 @@ +value title +AC Acquired +CC Corn crib rearing/living +FC Field cage rearing/living +MR Mother-rearing (for indoors) +NR Nursery-rearing \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/arrival.js b/nbri_ehr/resources/queries/study/arrival.js index 80761d7..c216e82 100644 --- a/nbri_ehr/resources/queries/study/arrival.js +++ b/nbri_ehr/resources/queries/study/arrival.js @@ -47,6 +47,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even row.birth = row['Id/demographics/birth'] || null; row.gender = row['Id/demographics/gender'] || null; row.geographic_origin = row['Id/demographics/geographic_origin'] || null; + row.socialCode = row['Id/demographics/socialCode'] || null; if (row.QCStateLabel) { row.qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); @@ -139,6 +140,12 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even hasUpdates = true; } + if (row.socialCode && row.socialCode !== data.socialCode) + { + obj.socialCode = row.socialCode; + hasUpdates = true; + } + if (row.sire && row.sire !== data.sire) { obj.sire = row.sire; diff --git a/nbri_ehr/resources/queries/study/birth.js b/nbri_ehr/resources/queries/study/birth.js index 52aa379..d1c1cab 100644 --- a/nbri_ehr/resources/queries/study/birth.js +++ b/nbri_ehr/resources/queries/study/birth.js @@ -126,6 +126,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even species: row['Id/demographics/species'] || null, birth: row.date || null, gender: row['Id/demographics/gender'] || null, + socialCode: row['Id/demographics/socialCode'] || null, taskid: row.taskid, remark: row.remark, QCStateLabel: row.QCStateLabel, @@ -173,6 +174,11 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even hasUpdates = true; } + if (obj.socialCode && obj.socialCode !== data.socialCode) { + record.socialCode = obj.socialCode; + hasUpdates = true; + } + if (obj.performedby && obj.performedby !== data.performedby) { record.performedby = obj.performedby; hasUpdates = true; diff --git a/nbri_ehr/resources/queries/study/demographics.query.xml b/nbri_ehr/resources/queries/study/demographics.query.xml index 01fdfc9..0323b4a 100644 --- a/nbri_ehr/resources/queries/study/demographics.query.xml +++ b/nbri_ehr/resources/queries/study/demographics.query.xml @@ -91,6 +91,15 @@ meaning + + Social Code + + ehr_lookups + social_code + value + title + + CITES diff --git a/nbri_ehr/resources/queries/study/demographics/.qview.xml b/nbri_ehr/resources/queries/study/demographics/.qview.xml index d232062..5f80b01 100644 --- a/nbri_ehr/resources/queries/study/demographics/.qview.xml +++ b/nbri_ehr/resources/queries/study/demographics/.qview.xml @@ -12,6 +12,7 @@ + diff --git a/nbri_ehr/resources/queries/study/demographics/Search Panel.qview.xml b/nbri_ehr/resources/queries/study/demographics/Search Panel.qview.xml new file mode 100644 index 0000000..b7949ad --- /dev/null +++ b/nbri_ehr/resources/queries/study/demographics/Search Panel.qview.xml @@ -0,0 +1,90 @@ + + diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml index 91a2f97..14fbe82 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -498,6 +498,9 @@ varchar + + varchar + varchar diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js index a5e983f..59258f8 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js @@ -51,6 +51,15 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { width: 200 } }, + // the social code is recorded once per animal, at birth or arrival, and lives on demographics + 'Id/demographics/socialCode': { + allowBlank: false, + nullable: false, + columnConfig: { + fixed: true, + width: 200 + } + }, // project and protocol are entered through the Project Assignment and Protocol Assignment sections project: { allowBlank: true, diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js index 0b76f92..d51641f 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js @@ -109,6 +109,14 @@ EHR.model.DataModelManager.registerMetadata('Birth', { columnConfig: { width: 200 } + }, + // the social code is recorded once per animal, at birth or arrival, and lives on demographics + 'Id/demographics/socialCode': { + allowBlank: false, + nullable: false, + columnConfig: { + width: 200 + } } } } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java index 2bd152c..ab2f5d4 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java @@ -49,6 +49,9 @@ protected List getFieldKeys(TableInfo ti) keys.add(10, FieldKey.fromString("Id/demographics/gender")); keys.add(12, FieldKey.fromString("Id/demographics/geographic_origin")); + // the social code sits beside Initial Location, whose index the inserts above have shifted, so find it + keys.add(keys.indexOf(FieldKey.fromString("cage")) + 1, FieldKey.fromString("Id/demographics/socialCode")); + return keys; } } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java index 10c8ab7..326b540 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java @@ -37,6 +37,7 @@ public class NBRIBirthFormSection extends NewAnimalFormSection FieldKey.fromString("Id/demographics/dam"), FieldKey.fromString("Id/demographics/sire"), FieldKey.fromString("cage"), + FieldKey.fromString("Id/demographics/socialCode"), FieldKey.fromString("type"), FieldKey.fromString("breedingType"), FieldKey.fromString("remark"), diff --git a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java index bcd4b33..a73bd87 100644 --- a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java +++ b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java @@ -636,6 +636,8 @@ public void testWeightValidation() public void testArrivalForm() throws IOException, CommandException { String arrivedAnimal = "30905"; + // demographics.socialCode holds an ehr_lookups.social_code code; the grids display its title + String socialCode = "Acquired"; LocalDateTime now = LocalDateTime.now(); gotoEnterData(); @@ -654,6 +656,12 @@ public void testArrivalForm() throws IOException, CommandException arrivals.setGridCell(1, "Id/demographics/species", "Pig-Tailed Macaque"); arrivals.setGridCellJS(1, "Id/demographics/birth", now.minusDays(7).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); arrivals.setGridCell(1, "sourceFacility", "Bioqual, Incorporated"); + arrivals.setGridCell(1, "Id/demographics/socialCode", socialCode); + + log("Verifying Social Code is required"); + arrivals.setGridCellJS(1, "Id/demographics/socialCode", null); + waitForFormError("The field: Social Code is required"); + arrivals.setGridCell(1, "Id/demographics/socialCode", socialCode); Ext4GridRef protocolAssignments = _helper.getExt4GridForFormSection("Protocol Assignment"); _helper.addRecordToGrid(protocolAssignments); @@ -694,6 +702,13 @@ public void testArrivalForm() throws IOException, CommandException verifyRowCreated("study", "demographics", arrivedAnimal, 1); verifyRowCreated("study", "housing", arrivedAnimal, 1); + log("Verifying the social code reached demographics"); + goToSchemaBrowser(); + table = viewQueryData("study", "demographics"); + table.setFilter("Id", "Equals", arrivedAnimal); + Assert.assertEquals("Social code entered on the arrival form did not reach demographics", + Arrays.asList(socialCode), table.getRowDataAsText(0, "socialCode")); + log("Verifying the birth date reached demographics and agrees with the birth record"); String arrivalBirthDay = now.minusDays(7).format(_dateFormat); assertEquals("Birth record does not carry the birth date entered on the arrival form", @@ -713,6 +728,8 @@ public void testBirthForm() throws Exception String damSpecies = "Brown-Tufted Capuchin"; String conceptId = "TESTCONCEPT1"; String breedingType = "Time-Mated"; + // demographics.socialCode holds an ehr_lookups.social_code code; the grids display its title + String socialCode = "Mother-rearing (for indoors)"; LocalDateTime now = LocalDateTime.now(); log("Creating the dam and sire of the conception"); @@ -756,6 +773,12 @@ public void testBirthForm() throws Exception births.setGridCell(1, "cage", "C3"); births.setGridCell(1, "Id/demographics/gender", "Female"); births.setGridCell(1, "breedingType", breedingType); + births.setGridCell(1, "Id/demographics/socialCode", socialCode); + + log("Verifying Social Code is required"); + births.setGridCellJS(1, "Id/demographics/socialCode", null); + waitForFormError("The field: Social Code is required"); + births.setGridCell(1, "Id/demographics/socialCode", socialCode); Ext4GridRef protocolAssignments = _helper.getExt4GridForFormSection("Protocol Assignment"); _helper.addRecordToGrid(protocolAssignments); @@ -786,6 +809,8 @@ public void testBirthForm() throws Exception Assert.assertEquals("Invalid demographics record", Arrays.asList(damId), table.getRowDataAsText(0, "dam")); Assert.assertEquals("Invalid demographics record", Arrays.asList(sireId), table.getRowDataAsText(0, "sire")); Assert.assertEquals("Invalid demographics record", Arrays.asList(damSpecies), table.getRowDataAsText(0, "species")); + Assert.assertEquals("Social code entered on the birth form did not reach demographics", + Arrays.asList(socialCode), table.getRowDataAsText(0, "socialCode")); goToSchemaBrowser(); table = viewQueryData("study", "assignment"); @@ -2027,7 +2052,8 @@ private void createBreedingPair(String damId, String sireId, String species) thr private void verifyBirthColumnOrder(Ext4GridRef births) { List expectedOrder = List.of("Id", "date", "conceptId", "Id/demographics/species", "Id/demographics/gender", - "Id/demographics/dam", "Id/demographics/sire", "cage", "type", "breedingType", "remark", "performedby"); + "Id/demographics/dam", "Id/demographics/sire", "cage", "Id/demographics/socialCode", "type", + "breedingType", "remark", "performedby"); int previousIdx = 0; String previousCol = null; From 9bc7c21319f54128cda4c2b9a4eaa9007fe6eecd Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 17 Aug 2026 11:58:18 -0600 Subject: [PATCH 05/18] Hide the project assignment end date on data entry forms A new project assignment already closes the animal's open one, so the end date is never entered by hand. The arrival and birth key is lowercased to match the actual field name, which it did not match before. --- nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js | 4 +++- nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js | 4 ++-- nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js index 59258f8..af0c748 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js @@ -18,7 +18,9 @@ Ext4.onReady(function() { EHR.model.DataModelManager.registerMetadata('Arrival', { allQueries: { - 'endDate': { + // lowercase to match the key Default.js and Assignment.js use; a differently-cased key shadows theirs entirely + // rather than merging with it + 'enddate': { hidden: true } }, diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js index b055afb..31645f0 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js @@ -7,9 +7,9 @@ EHR.model.DataModelManager.registerMetadata('Assignment', { byQuery: { 'study.assignment': { - // the dataset column is hidden by default; project assignments are ended by entering an end date + // a new project assignment ends the open one automatically, so the end date is never entered here 'enddate': { - hidden: false + hidden: true }, 'project': { xtype: 'combo', diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js index d51641f..de3c560 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js @@ -18,7 +18,9 @@ Ext4.onReady(function() { EHR.model.DataModelManager.registerMetadata('Birth', { allQueries: { - 'endDate': { + // lowercase to match the key Default.js and Assignment.js use; a differently-cased key shadows theirs entirely + // rather than merging with it + 'enddate': { hidden: true } }, From ba9191895a25a414f86b93ff0562e99e530a3604 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 17 Aug 2026 14:23:33 -0600 Subject: [PATCH 06/18] Rename the Bulk Deaths form to Deaths Drops Bulk from the class, form name and label. Also removes the admin-only isAvailable check and the canInsert override, so the form follows the same availability rules as the other data entry forms. --- .../web/nbri_ehr/model/sources/Death.js | 2 +- .../org/labkey/nbri_ehr/NBRI_EHRModule.java | 2 +- ...thFormType.java => NBRIDeathFormType.java} | 24 ++++--------------- 3 files changed, 7 insertions(+), 21 deletions(-) rename nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/{NBRIBulkDeathFormType.java => NBRIDeathFormType.java} (65%) diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js index 568a410..872eaac 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js @@ -4,7 +4,7 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ /** - * Metadata for the grid-based Bulk Deaths form. The columnConfig widths only take effect in the grid; they are ignored + * Metadata for the grid-based Deaths form. The columnConfig widths only take effect in the grid; they are ignored * when the same fields render in a form panel. */ EHR.model.DataModelManager.registerMetadata('Death', { diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java index 2395e6f..6e988db 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java @@ -215,7 +215,7 @@ private void registerDataEntry() EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIBulkClinicalFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIDepartureFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIDeathNecropsyFormType.class, this)); - EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIBulkDeathFormType.class, this)); + EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIDeathFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIHousingFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIMedicationTreatmentFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIProjectFormType.class, this)); diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkDeathFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIDeathFormType.java similarity index 65% rename from nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkDeathFormType.java rename to nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIDeathFormType.java index 5d95343..dba5a7c 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkDeathFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIDeathFormType.java @@ -18,9 +18,7 @@ import org.labkey.api.ehr.EHRService; import org.labkey.api.ehr.dataentry.DataEntryFormContext; import org.labkey.api.ehr.dataentry.FormSection; -import org.labkey.api.ehr.security.EHRCompletedInsertPermission; import org.labkey.api.module.Module; -import org.labkey.api.security.permissions.AdminPermission; import org.labkey.api.view.template.ClientDependency; import org.labkey.nbri_ehr.dataentry.section.BaseFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIAnimalDetailsFormSection; @@ -29,14 +27,14 @@ import java.util.Arrays; /** - * Admin-only form that records deaths only, as a grid so several animals can be entered at once. + * Records deaths only, as a grid so several animals can be entered at once. */ -public class NBRIBulkDeathFormType extends NBRIBaseTaskFormType +public class NBRIDeathFormType extends NBRIBaseTaskFormType { - public static final String NAME = "BulkDeaths"; - public static final String LABEL = "Bulk Deaths"; + public static final String NAME = "Deaths"; + public static final String LABEL = "Deaths"; - public NBRIBulkDeathFormType(DataEntryFormContext ctx, Module owner) + public NBRIDeathFormType(DataEntryFormContext ctx, Module owner) { super(ctx, owner, NAME, LABEL, "Colony Management", Arrays.asList( new NBRITaskFormSection(), @@ -51,16 +49,4 @@ public NBRIBulkDeathFormType(DataEntryFormContext ctx, Module owner) s.addConfigSource("Death"); } } - - @Override - public boolean isAvailable() - { - return super.isAvailable() && getCtx().getContainer().hasPermission(getCtx().getUser(), AdminPermission.class); - } - - @Override - protected boolean canInsert() - { - return EHRService.get().hasPermission("study", "deaths", getCtx().getContainer(), getCtx().getUser(), EHRCompletedInsertPermission.class); - } } From 9466b3008ceb3f3a01ac1ded50e7ace33af2d500 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 17 Aug 2026 14:30:22 -0600 Subject: [PATCH 07/18] Hide the end date on the animal group assignment form A new group membership already closes the animal's open one through datasetsToCloseOnNewEntry, so the end date is never entered by hand. --- .../web/nbri_ehr/model/sources/AnimalGroupMembers.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js index 3678d54..25a5dbd 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js @@ -35,6 +35,10 @@ EHR.model.DataModelManager.registerMetadata('AnimalGroupMembers', { return curDate; } }, + enddate: { + shownInGrid: false, + hidden: true + }, groupId: { allowBlank: false, nullable: false, From c16c67031bf3de1edc469600ad4a3350e13958b8 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 17 Aug 2026 15:56:10 -0600 Subject: [PATCH 08/18] Add a group assignment section to the arrival and birth forms An animal can now be put in a group as it arrives or is born rather than through a separate form. The section allows any Id because neither form's animal has a demographics record yet, and the arrival and birth tests cover it. --- .../dataentry/form/NBRIArrivalFormType.java | 4 +++ .../dataentry/form/NBRIBirthFormType.java | 6 +++- .../tests.nbri_ehr/NBRI_EHRTest.java | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIArrivalFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIArrivalFormType.java index 61fe238..b8d2e6e 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIArrivalFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIArrivalFormType.java @@ -23,6 +23,7 @@ import org.labkey.nbri_ehr.dataentry.section.NBRIAnimalDetailsFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIArrivalFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIArrivalInstructionsFormSection; +import org.labkey.nbri_ehr.dataentry.section.NBRIGroupAssignmentFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIProjectAssignmentFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIProtocolAssignmentFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRITaskFormSection; @@ -44,15 +45,18 @@ public NBRIArrivalFormType(DataEntryFormContext ctx, Module owner) new NBRIArrivalFormSection(), new NBRIProtocolAssignmentFormSection(true, true, true), new NBRIProjectAssignmentFormSection(true, true, true), + new NBRIGroupAssignmentFormSection(true, true, true), new NBRIWeightFormSection(true, true) )); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Assignment.js")); + addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/AnimalGroupMembers.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Arrival.js")); for (FormSection s : getFormSections()) { s.addConfigSource("Assignment"); + s.addConfigSource("AnimalGroupMembers"); s.addConfigSource("Arrival"); } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBirthFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBirthFormType.java index cbd66ea..78be0d6 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBirthFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBirthFormType.java @@ -24,6 +24,7 @@ import org.labkey.nbri_ehr.dataentry.section.NBRIAnimalDetailsFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIBirthFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIBirthInstructionsFormSection; +import org.labkey.nbri_ehr.dataentry.section.NBRIGroupAssignmentFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIProjectAssignmentFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIProtocolAssignmentFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRITaskFormSection; @@ -43,18 +44,21 @@ public NBRIBirthFormType (DataEntryFormContext ctx, Module owner) new NBRIAnimalDetailsFormSection(), new NBRIBirthFormSection(), new NBRIProtocolAssignmentFormSection(true, true, true), - new NBRIProjectAssignmentFormSection(true, true, true) + new NBRIProjectAssignmentFormSection(true, true, true), + new NBRIGroupAssignmentFormSection(true, true, true) )); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/plugin/RowEditor.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/NBRIDefault.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Assignment.js")); + addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/AnimalGroupMembers.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Birth.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/window/AddAnimalsWindow.js")); for (FormSection s : getFormSections()) { s.addConfigSource("Assignment"); + s.addConfigSource("AnimalGroupMembers"); s.addConfigSource("Birth"); } } diff --git a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java index a73bd87..73501bc 100644 --- a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java +++ b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java @@ -638,6 +638,8 @@ public void testArrivalForm() throws IOException, CommandException String arrivedAnimal = "30905"; // demographics.socialCode holds an ehr_lookups.social_code code; the grids display its title String socialCode = "Acquired"; + // animal_group_members.groupId holds an ehr_lookups.breeding_type code; the grids display its title + String animalGroup = "Assigned Breeding Protocol"; LocalDateTime now = LocalDateTime.now(); gotoEnterData(); @@ -675,6 +677,12 @@ public void testArrivalForm() throws IOException, CommandException projectAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); projectAssignments.setGridCell(1, "project", "640991"); + Ext4GridRef groupAssignments = _helper.getExt4GridForFormSection("Group Assignments"); + _helper.addRecordToGrid(groupAssignments); + groupAssignments.setGridCell(1, "Id", arrivedAnimal); + groupAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + groupAssignments.setGridCell(1, "groupId", animalGroup); + submitForm("Submit Final", "Finalize"); goToSchemaBrowser(); @@ -696,9 +704,15 @@ public void testArrivalForm() throws IOException, CommandException table.setFilter("Id", "Equals", arrivedAnimal); Assert.assertEquals("Invalid protocol assignment", Arrays.asList("dummyprotocol"), table.getRowDataAsText(0, "protocol")); + goToSchemaBrowser(); + table = viewQueryData("study", "animal_group_members"); + table.setFilter("Id", "Equals", arrivedAnimal); + Assert.assertEquals("Invalid group assignment", Arrays.asList(animalGroup), table.getRowDataAsText(0, "groupId")); + verifyRowCreated("study", "birth", arrivedAnimal, 1); verifyRowCreated("study", "assignment", arrivedAnimal, 1); verifyRowCreated("study", "protocolAssignment", arrivedAnimal, 1); + verifyRowCreated("study", "animal_group_members", arrivedAnimal, 1); verifyRowCreated("study", "demographics", arrivedAnimal, 1); verifyRowCreated("study", "housing", arrivedAnimal, 1); @@ -730,6 +744,8 @@ public void testBirthForm() throws Exception String breedingType = "Time-Mated"; // demographics.socialCode holds an ehr_lookups.social_code code; the grids display its title String socialCode = "Mother-rearing (for indoors)"; + // animal_group_members.groupId holds an ehr_lookups.breeding_type code; the grids display its title + String animalGroup = "Project Breeding"; LocalDateTime now = LocalDateTime.now(); log("Creating the dam and sire of the conception"); @@ -792,6 +808,12 @@ public void testBirthForm() throws Exception projectAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); projectAssignments.setGridCell(1, "project", "795644"); + Ext4GridRef groupAssignments = _helper.getExt4GridForFormSection("Group Assignments"); + _helper.addRecordToGrid(groupAssignments); + groupAssignments.setGridCell(1, "Id", bornAnimal); + groupAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + groupAssignments.setGridCell(1, "groupId", animalGroup); + submitForm("Submit Final", "Finalize"); goToSchemaBrowser(); @@ -822,8 +844,14 @@ public void testBirthForm() throws Exception table.setFilter("Id", "Equals", bornAnimal); Assert.assertEquals("Invalid protocol assignment", Arrays.asList("protocol101"), table.getRowDataAsText(0, "protocol")); + goToSchemaBrowser(); + table = viewQueryData("study", "animal_group_members"); + table.setFilter("Id", "Equals", bornAnimal); + Assert.assertEquals("Invalid group assignment", Arrays.asList(animalGroup), table.getRowDataAsText(0, "groupId")); + verifyRowCreated("study", "assignment", bornAnimal, 1); verifyRowCreated("study", "protocolAssignment", bornAnimal, 1); + verifyRowCreated("study", "animal_group_members", bornAnimal, 1); verifyRowCreated("study", "housing", bornAnimal, 1); verifyRowCreated("study", "demographics", bornAnimal, 1); From b4724684471d518ec709a07d19bd5b32aa2d61f6 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 18 Aug 2026 05:58:21 -0600 Subject: [PATCH 09/18] Fix metadata field key casing that shadowed shared EHR defaults EHR metadata keys are merged case-sensitively but applied case-insensitively, so performedBy and qcstate replaced the shared performedby and QCState config rather than merging with it. That dropped the performed-by default of the current user, which made Group Assignments fail the submit-final check, along with the QC state initial value and editor config. --- .../web/nbri_ehr/model/sources/AnimalGroupMembers.js | 2 +- .../resources/web/nbri_ehr/model/sources/BehaviorDefaults.js | 2 +- .../resources/web/nbri_ehr/model/sources/BehavioralCase.js | 2 +- nbri_ehr/resources/web/nbri_ehr/model/sources/BulkBehavior.js | 2 +- nbri_ehr/resources/web/nbri_ehr/model/sources/ClinicalCase.js | 4 ++-- .../resources/web/nbri_ehr/model/sources/ClinicalDefaults.js | 4 ++-- nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js | 2 +- .../resources/web/nbri_ehr/model/sources/DeathNecropsy.js | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js index 25a5dbd..11a4ac5 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js @@ -47,7 +47,7 @@ EHR.model.DataModelManager.registerMetadata('AnimalGroupMembers', { filterArray: [] } }, - performedBy: { + performedby: { shownInGrid: false, hidden: true } diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/BehaviorDefaults.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/BehaviorDefaults.js index 6a52bea..0a0d56b 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/BehaviorDefaults.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/BehaviorDefaults.js @@ -46,7 +46,7 @@ EHR.model.DataModelManager.registerMetadata('BehaviorDefaults', { dateFinalized: { hidden: true }, - qcstate: { + QCState: { hidden: true }, performedby: { diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/BehavioralCase.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/BehavioralCase.js index 94d26dc..66bd817 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/BehavioralCase.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/BehavioralCase.js @@ -93,7 +93,7 @@ EHR.model.DataModelManager.registerMetadata('BehavioralCase', { closeRemark: { height: 120 }, - qcstate: { + QCState: { hidden: true }, attachmentFile: { diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/BulkBehavior.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/BulkBehavior.js index 631b913..932b29c 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/BulkBehavior.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/BulkBehavior.js @@ -46,7 +46,7 @@ EHR.model.DataModelManager.registerMetadata('BulkBehavior', { dateFinalized: { hidden: true }, - qcstate: { + QCState: { hidden: true } }, diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/ClinicalCase.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/ClinicalCase.js index 4edd8e4..453bdf9 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/ClinicalCase.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/ClinicalCase.js @@ -93,7 +93,7 @@ EHR.model.DataModelManager.registerMetadata('ClinicalCase', { closeRemark: { height: 120 }, - qcstate: { + QCState: { hidden: true }, attachmentFile: { @@ -150,7 +150,7 @@ EHR.model.DataModelManager.registerMetadata('ClinicalCase', { dateFinalized: { hidden: true }, - qcstate: { + QCState: { hidden: true }, }, diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/ClinicalDefaults.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/ClinicalDefaults.js index 7a7e901..9921492 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/ClinicalDefaults.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/ClinicalDefaults.js @@ -119,7 +119,7 @@ EHR.model.DataModelManager.registerMetadata('ClinicalDefaults', { closeRemark: { height: 120 }, - qcstate: { + QCState: { hidden: true }, attachmentFile: { @@ -167,7 +167,7 @@ EHR.model.DataModelManager.registerMetadata('ClinicalDefaults', { dateFinalized: { hidden: true }, - qcstate: { + QCState: { hidden: true }, }, diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js index 872eaac..84eb22a 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js @@ -12,7 +12,7 @@ EHR.model.DataModelManager.registerMetadata('Death', { }, byQuery: { 'study.deaths': { - qcstate: { + QCState: { hidden: true }, date: { diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/DeathNecropsy.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/DeathNecropsy.js index 2f7b703..6fd8a26 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/DeathNecropsy.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/DeathNecropsy.js @@ -8,7 +8,7 @@ EHR.model.DataModelManager.registerMetadata('DeathNecropsy', { }, byQuery: { 'study.deaths': { - qcstate: { + QCState: { hidden: true }, date: { @@ -90,7 +90,7 @@ EHR.model.DataModelManager.registerMetadata('DeathNecropsy', { category: { hidden: true }, - qcstate: { + QCState: { hidden: true } }, From 96037c3a46b26817b7d83c7663935736d8e83164 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 18 Aug 2026 06:44:39 -0600 Subject: [PATCH 10/18] Block death entry for animals with a non-final demographics record A demographics record still in data entry is provisional, so recording a death against it would publish unreviewed arrival or birth data. Admins can override. --- nbri_ehr/resources/queries/study/deaths.js | 27 ++++++++++------------ 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/nbri_ehr/resources/queries/study/deaths.js b/nbri_ehr/resources/queries/study/deaths.js index c7dae52..b56fa0e 100644 --- a/nbri_ehr/resources/queries/study/deaths.js +++ b/nbri_ehr/resources/queries/study/deaths.js @@ -86,6 +86,7 @@ function onUpsert(helper, scriptErrors, row, oldRow) { var status = idMap[row.Id].calculated_status ? idMap[row.Id].calculated_status.toUpperCase() : null; var priorDeathQCState = deathIdMap[row.Id] && deathIdMap[row.Id].QCStateLabel ? deathIdMap[row.Id].QCStateLabel.toUpperCase() : null; var rowQCState = row.QCStateLabel ? row.QCStateLabel.toUpperCase() : null; + var demographicsQCState = idMap[row.Id].QCStateLabel ? idMap[row.Id].QCStateLabel.toUpperCase() : null; // deathIdMap is a snapshot taken before any row was processed, so it cannot see earlier rows of this same // save. Track them separately: study.deaths is demographic, so a second row for one animal cannot be saved. @@ -101,28 +102,25 @@ function onUpsert(helper, scriptErrors, row, oldRow) { else if (status === 'SHIPPED') { errorMsg = 'Animal is not at the center.'; } + // An in-progress demographics record is provisional; completing it here would publish unreviewed arrival + // or birth data. Admins can override. A null state is not evidence of anything - ETL rows carry one. + else if (demographicsQCState && demographicsQCState !== 'COMPLETED' && !LABKEY.Security.currentUser.isAdmin) { + errorMsg = 'Demographics record for this animal is not final (' + idMap[row.Id].QCStateLabel + '). Complete the arrival or birth record before submitting a death.'; + } else if (deathsInTransaction[row.Id]) { errorMsg = 'This animal is entered more than once. Only one death record per animal can be saved.'; } - // Check if an animal that's being entered is pending any request/review. - // Note 1: When trying to enter a new record for an animal, the QCState = 'IN PROGRESS'. - // Note 2: Upon 'Submit Death', the QCState will get set to 'REQUEST: PENDING', and upon 'Submit Necropsy for Review', - // the QCState will get set to 'Review Required' - this way we can distinguish between the two states in the Death/Necropsy workflow. - // If a user tries to submit a new Death record (identified by QCState = 'IN PROGRESS') for an animal that - // already has a pending request/review status in study.deaths, then below error message will be displayed. + // A new entry starts at 'IN PROGRESS'; 'Submit Death' sets 'REQUEST: PENDING' and 'Submit Necropsy for + // Review' sets 'Review Required', so either prior state means the animal is already in the workflow. else if (rowQCState === 'IN PROGRESS' && (priorDeathQCState === 'REQUEST: PENDING' || priorDeathQCState === 'REVIEW REQUIRED')) { errorMsg = 'Death record is pending review for this animal'; } - // if 'Save Draft' record already exists, it doesn't allow to 'Save Draft' or 'Submit Death' - // on the same animal again - throws an error "duplicate key value violates unique constraint" - // So, added this check to allow 'Save Draft' record to be saved only once. + // A second draft for the same animal would fail on the unique constraint, so catch it here. else if (oldRow === undefined && rowQCState === 'IN PROGRESS' && priorDeathQCState === 'IN PROGRESS') { errorMsg = 'Death/Necropsy data entry is in progress for this animal'; } - // study.deaths is demographic (one row per animal), so any other new row for an animal with an existing - // record would fail on the unique constraint; report it as a validation error instead. Test record - // existence, not QC state: ETL/import-sourced rows can carry a null QCState. + // Catch-all for that constraint. Test existence, not QC state: ETL rows can carry a null QCState. else if (oldRow === undefined && deathIdMap[row.Id]) { errorMsg = 'A death record already exists for this animal (' + (deathIdMap[row.Id].QCStateLabel || 'unknown state') + ').'; } @@ -177,9 +175,8 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'study', 'Deaths', function(event, errors, helper){ - // Single writer for the denormalized demographics death date. Runs once per save, after the death rows are saved, - // and derives the value from the stored record rather than from the incoming row. calculated_status is left to the - // shared status recalc, which owns the death/departure/re-arrival precedence. + // The shared ehr script writes demographics.death from the incoming row; this runs after and re-derives it from + // the stored record, so the event record wins. calculated_status is left to the shared status recalc. if (!helper.isETL() && idsToSync.length) { var demographicsUpdates = triggerHelper.computeDemographicsSync(idsToSync); if (demographicsUpdates.size() > 0) { From cafb3b8fd9b7b5ea21f682c4fc7783550dd684af Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 18 Aug 2026 06:53:22 -0600 Subject: [PATCH 11/18] Scope the demographics QC state check to inserts Re-checking it on update let a later regression of the demographics state trap a death record that was already in the necropsy workflow. --- nbri_ehr/resources/queries/study/deaths.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nbri_ehr/resources/queries/study/deaths.js b/nbri_ehr/resources/queries/study/deaths.js index b56fa0e..fc9b3e9 100644 --- a/nbri_ehr/resources/queries/study/deaths.js +++ b/nbri_ehr/resources/queries/study/deaths.js @@ -102,9 +102,9 @@ function onUpsert(helper, scriptErrors, row, oldRow) { else if (status === 'SHIPPED') { errorMsg = 'Animal is not at the center.'; } - // An in-progress demographics record is provisional; completing it here would publish unreviewed arrival - // or birth data. Admins can override. A null state is not evidence of anything - ETL rows carry one. - else if (demographicsQCState && demographicsQCState !== 'COMPLETED' && !LABKEY.Security.currentUser.isAdmin) { + // An in-progress demographics record is provisional; completing it would publish unreviewed arrival or + // birth data. Insert only, so an in-flight death is not trapped. Admins override. Null is not evidence. + else if (oldRow === undefined && demographicsQCState && demographicsQCState !== 'COMPLETED' && !LABKEY.Security.currentUser.isAdmin) { errorMsg = 'Demographics record for this animal is not final (' + idMap[row.Id].QCStateLabel + '). Complete the arrival or birth record before submitting a death.'; } else if (deathsInTransaction[row.Id]) { From d2150b692d835f40d53cae4b943a0df51d378a4b Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 18 Aug 2026 07:05:18 -0600 Subject: [PATCH 12/18] Clarify demographics birth date ownership comments --- nbri_ehr/resources/queries/study/arrival.js | 4 ++-- nbri_ehr/resources/queries/study/birth.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/nbri_ehr/resources/queries/study/arrival.js b/nbri_ehr/resources/queries/study/arrival.js index c216e82..8ee71bb 100644 --- a/nbri_ehr/resources/queries/study/arrival.js +++ b/nbri_ehr/resources/queries/study/arrival.js @@ -188,8 +188,8 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'study', 'Arrival', function(event, errors, helper){ - // Single writer for the denormalized demographics birth date. saveBirthRecord() above wrote the birth record; - // this reads it back so demographics and the event record cannot disagree. + // Owns updates to the denormalized demographics birth date, read back from the birth record saveBirthRecord() + // just wrote. createDemographicsRecord() seeds it on insert but never overwrites an existing row. if (!helper.isETL() && idsToSync.length) { var demographicsUpdates = triggerHelper.computeDemographicsSync(idsToSync); if (demographicsUpdates.size() > 0) { diff --git a/nbri_ehr/resources/queries/study/birth.js b/nbri_ehr/resources/queries/study/birth.js index d1c1cab..b5a34c6 100644 --- a/nbri_ehr/resources/queries/study/birth.js +++ b/nbri_ehr/resources/queries/study/birth.js @@ -30,7 +30,8 @@ function onInit(event, helper){ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'study', 'birth', function(event, errors, helper){ - // Single writer for the denormalized demographics birth date, derived from the saved birth records. + // Owns updates to the denormalized demographics birth date, derived from the saved birth records. + // createDemographicsRecord() in the shared ehr script seeds it but never overwrites an existing row. if (!helper.isETL() && idsToSync.length) { var demographicsUpdates = triggerHelper.computeDemographicsSync(idsToSync); if (demographicsUpdates.size() > 0) { From 9e661aab906e48d491f5d4820a78c5fcf3375c18 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 18 Aug 2026 07:12:25 -0600 Subject: [PATCH 13/18] Fix demographics sync no-op from Rhino NativeArray isEmpty() A NativeArray inherits an isEmpty() that is always true, so the early-return guard skipped every sync. --- .../nbri_ehr/query/NBRI_EHRTriggerHelper.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java b/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java index 893c53a..ded4d22 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java @@ -334,6 +334,10 @@ public boolean deathExists(String id) * calculated_status is deliberately absent from the result. It belongs to the shared status recalc, which owns the * death/departure/re-arrival precedence. *

+ * Note that a stored value is only ever cleared for death, by the AFTER_DELETE handler on study.deaths. There is no + * equivalent handler on study.birth, so deleting a birth record leaves demographics.birth at its last known value + * and this method will not heal it - a missing event record deliberately leaves the stored value alone. + *

* Every lookup is set-based - one query per event dataset for the whole id list, not one per animal - because a * bulk save can pass hundreds of ids and per-animal SQL in a trigger exhausts the script's wall-clock budget. * @@ -342,7 +346,9 @@ public boolean deathExists(String id) */ public List> computeDemographicsSync(List ids) { - if (ids == null || ids.isEmpty()) + // a JS array arrives as a Rhino NativeArray, whose inherited isEmpty() is always true; use size() instead + //noinspection SizeReplaceableByIsEmpty + if (ids == null || ids.size() == 0) return Collections.emptyList(); Set idSet = new HashSet<>(ids); @@ -361,11 +367,8 @@ public List> computeDemographicsSync(List ids) String id = (String)current.get("Id"); Map update = new CaseInsensitiveHashMap<>(); - // A public event record always wins - the same rule createDemographicsRecord() already applies to death on - // insert, extended to updates and to birth. The absence of an event record is NOT evidence the stored value - // is wrong: animals loaded by ETL, or acquired before these datasets were in use, legitimately carry a date - // with no event row, so a missing record leaves the value alone. Clearing a value is only ever driven by an - // explicit delete of the event record. + // A public event record always wins. A missing one is NOT evidence the stored value is wrong - ETL-loaded + // and pre-dataset animals legitimately carry a date with no event row - so it leaves the value alone. Date birth = births.get(id); if (birth != null && differsByDay(birth, (Date)current.get("birth"))) update.put("birth", birth); @@ -415,7 +418,7 @@ private Map getPublicEventDates(String queryName, Set ids) private boolean differsByDay(Date a, Date b) { if (a == null || b == null) - return a != b; + return !(a == null && b == null); return !DateUtils.isSameDay(a, b); } From 0b3ea908a2767f9de15d7534202a9ecf7e69af8e Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 18 Aug 2026 09:34:29 -0600 Subject: [PATCH 14/18] Anchor arrival form field inserts to named neighbors Hard-coded indices broke whenever the arrival metadata changed which columns the insert view shows. Also reorders the demographics block to species, gender, birth, dam, sire. --- .../section/NBRIArrivalFormSection.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java index ab2f5d4..5707656 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java @@ -20,6 +20,7 @@ import org.labkey.api.ehr.dataentry.DataEntryFormContext; import org.labkey.api.query.FieldKey; +import java.util.ArrayList; import java.util.List; public class NBRIArrivalFormSection extends BaseFormSection @@ -40,18 +41,32 @@ public JSONObject toJSON(DataEntryFormContext ctx, boolean includeFormElements) @Override protected List getFieldKeys(TableInfo ti) { - List keys = super.getFieldKeys(ti); + // super hands back the list registered for study.arrival when there is one, so copy before inserting + List keys = new ArrayList<>(super.getFieldKeys(ti)); - keys.add(6, FieldKey.fromString("Id/demographics/dam")); - keys.add(7, FieldKey.fromString("Id/demographics/sire")); - keys.add(8, FieldKey.fromString("Id/demographics/species")); - keys.add(9, FieldKey.fromString("Id/demographics/birth")); - keys.add(10, FieldKey.fromString("Id/demographics/gender")); - keys.add(12, FieldKey.fromString("Id/demographics/geographic_origin")); + // anchor each insert to a named neighbour - which columns the metadata shows in the insert view decides + // the index of everything after them + keys.addAll(indexOf(keys, "project"), List.of( + FieldKey.fromString("Id/demographics/species"), + FieldKey.fromString("Id/demographics/gender"), + FieldKey.fromString("Id/demographics/birth"), + FieldKey.fromString("Id/demographics/dam"), + FieldKey.fromString("Id/demographics/sire"))); - // the social code sits beside Initial Location, whose index the inserts above have shifted, so find it - keys.add(keys.indexOf(FieldKey.fromString("cage")) + 1, FieldKey.fromString("Id/demographics/socialCode")); + keys.add(indexOf(keys, "project") + 1, FieldKey.fromString("Id/demographics/geographic_origin")); + + // the social code sits beside Initial Location + keys.add(indexOf(keys, "cage") + 1, FieldKey.fromString("Id/demographics/socialCode")); return keys; } + + private int indexOf(List keys, String name) + { + int index = keys.indexOf(FieldKey.fromString(name)); + if (index < 0) + throw new IllegalStateException("Cannot position the arrival form fields: study.arrival has no '" + name + "' field"); + + return index; + } } From ca0229416cd1543917771008a96c7aa0fe52b39e Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 18 Aug 2026 17:05:10 -0600 Subject: [PATCH 15/18] Record a performer on animal group memberships The dataset was configured on two layers not to collect performedBy, but the shared dataset trigger rejects any record saved as Completed without one, so the Group Assignments section failed the Arrival and Birth forms. It stays hidden; the value is now seeded from the current user. --- .../resources/queries/study/animal_group_members.query.xml | 5 +---- .../web/nbri_ehr/model/sources/AnimalGroupMembers.js | 6 +++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/nbri_ehr/resources/queries/study/animal_group_members.query.xml b/nbri_ehr/resources/queries/study/animal_group_members.query.xml index 4cf726e..09a5157 100644 --- a/nbri_ehr/resources/queries/study/animal_group_members.query.xml +++ b/nbri_ehr/resources/queries/study/animal_group_members.query.xml @@ -31,10 +31,7 @@ true - - false - false - + diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js index 11a4ac5..6c56006 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js @@ -47,9 +47,13 @@ EHR.model.DataModelManager.registerMetadata('AnimalGroupMembers', { filterArray: [] } }, + // hidden here but still recorded, so seed it rather than relying on the shared default surviving the merge performedby: { shownInGrid: false, - hidden: true + hidden: true, + getInitialValue: function(v){ + return v || LABKEY.Security.currentUser.id; + } } } } From 6069c2879926329413c4bede04f570d86dcd0d95 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 23 Aug 2026 14:54:23 -0600 Subject: [PATCH 16/18] Grant the EHR roles access to the animal_group_members dataset The study uses per-dataset security, so a dataset with no role assignments is readable only by administrators. Closing an animal's group memberships on death therefore failed for every other user, and took the whole save down with it. --- nbri_ehr/test/sampledata/nbriEHRStudyPolicy.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nbri_ehr/test/sampledata/nbriEHRStudyPolicy.xml b/nbri_ehr/test/sampledata/nbriEHRStudyPolicy.xml index cefe3a8..6dee686 100644 --- a/nbri_ehr/test/sampledata/nbriEHRStudyPolicy.xml +++ b/nbri_ehr/test/sampledata/nbriEHRStudyPolicy.xml @@ -184,5 +184,11 @@ + + + + + + From 9beb27b0ef1c492529a69872c4bd0ccf6414f564 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 23 Aug 2026 14:54:31 -0600 Subject: [PATCH 17/18] House cagemate test animals against a location id Housing has no room of its own: room is derived from the location id and is read-only, so the room these tests posted was discarded and the penned animals landed with no location at all. --- .../tests.nbri_ehr/NBRI_EHRTest.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java index 73501bc..dd7e2c5 100644 --- a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java +++ b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java @@ -127,7 +127,7 @@ public class NBRI_EHRTest extends AbstractGenericEHRTest implements PostgresOnly private static final String PEN_ROOM_NAME = "PEN1"; private static final String[] PEN_ANIMALS = {"PEN0001", "PEN0002"}; - // Housed with a cage but no room, which is how a record entered against a cage alone lands. Every cage seeded by + // Housed against a cage-level location id, as opposed to a pen's room-level one. Every cage seeded by // populateLocations already has occupants from datasetHousing.tsv, so testCagematesWithoutRoom creates its own to // keep the expected cagemate count exact. private static final String ROOMLESS_CAGE_NAME = "C9"; @@ -1812,10 +1812,10 @@ public void testGroupPenCagemates() throws Exception createAliveAnimals(PEN_ANIMALS); - // The cage is deliberately left null: a penned animal is housed against the room, which is the case the - // cagemates query has to bound by room rather than by cage. - log("Housing two animals in the pen, with no cage"); - houseAnimals(PEN_ANIMALS, penRoom, null); + // A pen has no cage name, so the location the cage trigger derived for it is the room key alone. That key is + // the location id the animals are housed against, and sharing it is what makes them cagemates. + log("Housing two animals against the pen's location"); + houseAnimals(PEN_ANIMALS, penRoom); log("Verifying penned animals resolve as each other's cagemates"); assertCagemates(PEN_ANIMALS[0], 2, PEN_ANIMALS[1]); @@ -1831,12 +1831,12 @@ public void testCagematesWithoutRoom() throws Exception createAliveAnimals(ROOMLESS_ANIMALS); - // The cage is a location key that already names its room, so the room is redundant here and nothing requires - // it. Cagemates must still resolve when it is absent. - log("Housing two animals in the same cage, with no room"); - houseAnimals(ROOMLESS_ANIMALS, null, ROOMLESS_CAGE); + // This location id names a cage within a room, the other shape a location takes. Cagemates must resolve for it + // the same way they do for a pen's room-level id. + log("Housing two animals against the same cage location"); + houseAnimals(ROOMLESS_ANIMALS, ROOMLESS_CAGE); - log("Verifying caged animals resolve as each other's cagemates without a room"); + log("Verifying animals sharing a cage location resolve as each other's cagemates"); assertCagemates(ROOMLESS_ANIMALS[0], 2, ROOMLESS_ANIMALS[1]); } @@ -1860,15 +1860,16 @@ private void createAliveAnimals(String[] animalIds) throws Exception /** * Opens a completed housing record for each animal at the given location, replacing any left behind by an earlier - * run. Either the room or the cage may be null, which is how records entered against one alone land. + * run. The location id in 'cage' is the only location housing stores: 'room' is derived from it, is read-only, and + * would be discarded if it were posted here. */ - private void houseAnimals(String[] animalIds, String room, String cage) throws Exception + private void houseAnimals(String[] animalIds, String cage) throws Exception { - String[] fields = new String[]{"Id", "date", "enddate", "room", "cage", "QCStateLabel", "performedby"}; + String[] fields = new String[]{"Id", "date", "enddate", "cage", "QCStateLabel", "performedby"}; Object[][] data = new Object[animalIds.length][]; for (int i = 0; i < animalIds.length; i++) { - data[i] = new Object[]{animalIds[i], new Date(), null, room, cage, EHRQCState.COMPLETED.label, 1004}; + data[i] = new Object[]{animalIds[i], new Date(), null, cage, EHRQCState.COMPLETED.label, 1004}; } SimplePostCommand insertCommand = getApiHelper().prepareInsertCommand("study", "Housing", "lsid", fields, data); From c52207ded1ddcfc70f3745192ccbeeeaafc11f3d Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 23 Aug 2026 15:21:59 -0600 Subject: [PATCH 18/18] Match cagemates on the location id alone The room fallback could never fire: room is derived from the location id, so it is null exactly when the id is, and the branch required both. Dropping it leaves the equality that was already deciding every case. --- .../queries/study/demographicsCagemates.sql | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/nbri_ehr/resources/queries/study/demographicsCagemates.sql b/nbri_ehr/resources/queries/study/demographicsCagemates.sql index 4219925..0450dc2 100644 --- a/nbri_ehr/resources/queries/study/demographicsCagemates.sql +++ b/nbri_ehr/resources/queries/study/demographicsCagemates.sql @@ -5,7 +5,6 @@ */ SELECT d.id, --- t.room, t.cage, t.total, cast(t.animals as varchar(4000)) as animals @@ -14,7 +13,6 @@ FROM study.demographics d LEFT JOIN ( SELECT h.id, --- h.room, h.cage, count(distinct h2.id) as total, group_concat(distinct h2.id, ', ') as animals @@ -22,17 +20,18 @@ SELECT FROM study.housing h JOIN study.housing h2 --- cage holds a location key that already encodes the room, so caged animals match on cage alone. Group/pen rooms have --- no cage, so those fall back to the room, which is only consulted when neither side has a cage. -ON ((h.cage = h2.cage OR (h.cage IS NULL AND h2.cage IS NULL AND h.room = h2.room)) +-- cage is the location id, and it is the only location housing stores: for a caged animal it is the room-and-cage key, +-- for a group pen it is the room key alone. Animals sharing that id are in the same place, which is what makes them +-- cagemates. Room is not consulted, since it is derived from this same id and so can never distinguish two rows. +ON (h.cage = h2.cage AND h2.Id.demographics.calculated_status = 'Alive' AND h2.enddateTimeCoalesced >= now() AND h2.qcstate.publicdata = true) WHERE h.enddateTimeCoalesced >= now() AND h.qcstate.publicdata = true -GROUP BY h.id, h.room, h.cage +GROUP BY h.id, h.cage ) t ON (t.id = d.id) -WHERE d.calculated_status = 'Alive' \ No newline at end of file +WHERE d.calculated_status = 'Alive'