From 37afdd0cc1143774d52451ea56d4e5313da709e9 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 23 Aug 2026 15:58:33 -0700 Subject: [PATCH] Tighten birth and conception entry and drop legacy nbri_ehr tables (#20) ## Rationale Make the forms that open and close an animal's record agree on who owns each piece of its demographics, and finish the birth and conception cleanup this branch started with. The birth, arrival and death forms each wrote the animal's birth and death dates for themselves, so the three could disagree, and the death form additionally hand-set a status that the shared EHR recalculation already owns. Separately, the `nbri_ehr` schema still carried tables inherited from the legacy system that nothing reads, and several data entry forms exposed fields that are either derived or closed automatically and so were never meant to be typed by hand. ## Related Pull Requests None. ## Changes - Derive an animal's birth and death dates from its birth and death records, and leave calculated status to the shared EHR recalculation. Adds a drift query for reconciling values the old paths left stale. - Tighten birth and conception entry: conception date is now required, term date and birth condition are gone, and the birth fields the Start with Conception window fills can no longer be edited away from the conception record they came from. - Drop the legacy tables from the `nbri_ehr` schema along with the queries, custom views, lookups and referencing columns that existed only to serve them. Conception is the only table left in the schema. - Let an animal be put in a group as it arrives or is born, rather than through a separate form, and give the EHR roles access to the group membership data so the assignment can be closed when the animal dies. - Block recording a death against an animal whose demographics record is still in data entry, since that would publish unreviewed arrival or birth data. Admins can override. - Record a social code once per animal on the birth and arrival forms, and store it on demographics so it is available for animal selection and search. - Hide the end dates on the project and group assignment forms, which are always closed automatically rather than entered by hand, and rename the Bulk Deaths form to Deaths so it follows the same availability rules as every other data entry form. - Treat animals as cagemates when they share a location, which covers a group pen and a cage alike. - Stop reloading flag values from the production lookups manifest, so values configured at a site are no longer overwritten. --- nbri_ehr/resources/data/birth_condition.tsv | 3 - nbri_ehr/resources/data/editable_lookups.tsv | 3 +- nbri_ehr/resources/data/id_history_type.tsv | 6 - nbri_ehr/resources/data/lookup_sets.tsv | 3 +- nbri_ehr/resources/data/lookupsManifest.tsv | 4 +- .../resources/data/lookupsManifestTest.tsv | 3 +- nbri_ehr/resources/data/social_code.tsv | 6 + .../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 - .../study/animal_group_members.query.xml | 5 +- nbri_ehr/resources/queries/study/arrival.js | 29 +- nbri_ehr/resources/queries/study/birth.js | 32 +- .../resources/queries/study/birth.query.xml | 9 - nbri_ehr/resources/queries/study/deaths.js | 65 +- .../queries/study/demographics.query.xml | 17 +- .../queries/study/demographics/.qview.xml | 1 + .../study/demographics/Search Panel.qview.xml | 90 +++ .../queries/study/demographicsCagemates.sql | 13 +- .../study/diagnosticsBirthDeathDrift.sql | 69 +++ .../study/datasets/datasets_metadata.xml | 9 +- .../postgresql/nbri_ehr-26.001-26.002.sql | 40 ++ nbri_ehr/resources/schemas/nbri_ehr.xml | 572 +----------------- .../model/sources/AnimalGroupMembers.js | 12 +- .../web/nbri_ehr/model/sources/Arrival.js | 13 +- .../web/nbri_ehr/model/sources/Assignment.js | 4 +- .../model/sources/BehaviorDefaults.js | 2 +- .../nbri_ehr/model/sources/BehavioralCase.js | 2 +- .../web/nbri_ehr/model/sources/Birth.js | 49 +- .../nbri_ehr/model/sources/BulkBehavior.js | 2 +- .../nbri_ehr/model/sources/ClinicalCase.js | 4 +- .../model/sources/ClinicalDefaults.js | 4 +- .../web/nbri_ehr/model/sources/Conception.js | 9 +- .../web/nbri_ehr/model/sources/Death.js | 4 +- .../nbri_ehr/model/sources/DeathNecropsy.js | 4 +- .../org/labkey/nbri_ehr/NBRI_EHRModule.java | 4 +- .../dataentry/form/NBRIArrivalFormType.java | 4 + .../dataentry/form/NBRIBirthFormType.java | 6 +- ...thFormType.java => NBRIDeathFormType.java} | 24 +- .../section/NBRIArrivalFormSection.java | 32 +- .../section/NBRIBirthFormSection.java | 2 +- .../nbri_ehr/query/NBRI_EHRTriggerHelper.java | 100 +++ .../test/sampledata/nbriEHRStudyPolicy.xml | 6 + .../study/study/datasets/datasetBirth.tsv | 14 +- .../study/study/datasets/datasetDeaths.tsv | 4 +- .../tests.nbri_ehr/NBRI_EHRTest.java | 189 +++++- 72 files changed, 727 insertions(+), 1765 deletions(-) delete mode 100644 nbri_ehr/resources/data/birth_condition.tsv delete mode 100644 nbri_ehr/resources/data/id_history_type.tsv create mode 100644 nbri_ehr/resources/data/social_code.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/queries/study/demographics/Search Panel.qview.xml create mode 100644 nbri_ehr/resources/queries/study/diagnosticsBirthDeathDrift.sql create mode 100644 nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.001-26.002.sql rename nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/{NBRIBulkDeathFormType.java => NBRIDeathFormType.java} (65%) 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..edde79b 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. @@ -117,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/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..6cd8cba 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 @@ -102,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 9f32d12..19aa5a4 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 @@ -117,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 d64c8ba..8d028e6 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 @@ -114,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/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/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/queries/study/arrival.js b/nbri_ehr/resources/queries/study/arrival.js index 2010ffd..8ee71bb 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) { @@ -40,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(); @@ -132,9 +140,9 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even hasUpdates = true; } - if (row.birth && row.birth !== data.birth) + if (row.socialCode && row.socialCode !== data.socialCode) { - obj.birth = row.birth; + obj.socialCode = row.socialCode; hasUpdates = true; } @@ -171,5 +179,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){ + + // 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) { + 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..b5a34c6 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,25 @@ 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){ + + // 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) { + 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)) { @@ -110,6 +127,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, @@ -147,11 +165,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; @@ -162,6 +175,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; @@ -180,6 +198,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/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/deaths.js b/nbri_ehr/resources/queries/study/deaths.js index abe7bc4..fc9b3e9 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 @@ -85,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. @@ -100,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 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]) { 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') + ').'; } @@ -130,21 +129,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 +161,30 @@ 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){ + + // 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) { + 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/demographics.query.xml b/nbri_ehr/resources/queries/study/demographics.query.xml index f6fafa6..0323b4a 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 @@ -99,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/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' 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/resources/referenceStudy/study/datasets/datasets_metadata.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml index aa2bd6a..14fbe82 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -253,9 +253,6 @@ varchar - - varchar - varchar @@ -498,12 +495,12 @@ varchar - - integer - varchar + + varchar + 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/AnimalGroupMembers.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js index 3678d54..6c56006 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, @@ -43,9 +47,13 @@ EHR.model.DataModelManager.registerMetadata('AnimalGroupMembers', { filterArray: [] } }, - performedBy: { + // 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; + } } } } 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..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 } }, @@ -51,6 +53,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/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/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/Birth.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js index f266f1e..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 } }, @@ -32,12 +34,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 +77,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,17 +95,30 @@ 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: { 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/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/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/resources/web/nbri_ehr/model/sources/Death.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js index 568a410..84eb22a 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', { @@ -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 } }, 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 b76fdbb..6e988db 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 @@ -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/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/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); - } } 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..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,15 +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"))); + + 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; + } } 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..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,8 +37,8 @@ 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("cond"), FieldKey.fromString("breedingType"), FieldKey.fromString("remark"), FieldKey.fromString("performedby") 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..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 @@ -323,6 +323,106 @@ 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. + *

+ * 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. + * + * @param ids animals touched by the current save + * @return rows ready for updateDemographicsRecord(); empty when nothing has drifted + */ + public List> computeDemographicsSync(List ids) + { + // 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); + + 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. 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); + + 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 == null && b == null); + + return !DateUtils.isSameDay(a, b); + } + public boolean upsertWeightRecord(Map row) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException { return upsertWeightRecord(row, true); 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 @@ + + + + + + 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 afaab10..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 @@ -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; @@ -126,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"; @@ -632,9 +633,13 @@ public void testWeightValidation() } @Test - public void testArrivalForm() + 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(); @@ -653,6 +658,12 @@ public void testArrivalForm() 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); @@ -666,6 +677,12 @@ public void testArrivalForm() 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(); @@ -687,11 +704,31 @@ public void testArrivalForm() 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); + + 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", + arrivalBirthDay, getDatasetDay("birth", arrivedAnimal, "date")); + assertEquals("Demographics birth date does not match the birth record", + arrivalBirthDay, getDatasetDay("demographics", arrivedAnimal, "birth")); } @Test @@ -705,6 +742,10 @@ 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)"; + // 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"); @@ -748,6 +789,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); @@ -761,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(); @@ -778,6 +831,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"); @@ -789,11 +844,24 @@ 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); + 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"); @@ -863,7 +931,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); @@ -1454,6 +1521,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); @@ -1479,6 +1554,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() { @@ -1695,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]); @@ -1714,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]); } @@ -1743,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); @@ -1963,7 +2081,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", "cond", "breedingType", "remark", "performedby"); + "Id/demographics/dam", "Id/demographics/sire", "cage", "Id/demographics/socialCode", "type", + "breedingType", "remark", "performedby"); int previousIdx = 0; String previousCol = null; @@ -1981,6 +2100,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();