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/arrival.js b/nbri_ehr/resources/queries/study/arrival.js index 2010ffd..c216e82 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){ + + // Single writer for the denormalized demographics birth date. saveBirthRecord() above wrote the birth record; + // this reads it back so demographics and the event record cannot disagree. + if (!helper.isETL() && idsToSync.length) { + var demographicsUpdates = triggerHelper.computeDemographicsSync(idsToSync); + if (demographicsUpdates.size() > 0) { + helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates); + } + idsToSync = []; } }); \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/birth.js b/nbri_ehr/resources/queries/study/birth.js index cd0cce1..d1c1cab 100644 --- a/nbri_ehr/resources/queries/study/birth.js +++ b/nbri_ehr/resources/queries/study/birth.js @@ -7,6 +7,7 @@ require("ehr/triggers").initScript(this); EHR.Server.Utils = require("ehr/utils").EHR.Server.Utils; var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); +var idsToSync = []; function onInit(event, helper){ helper.setScriptOptions({ @@ -21,9 +22,24 @@ function onInit(event, helper){ skipAssignmentCheck: true, }); + // the script scope can outlive a single save, so never inherit ids from a prior one + idsToSync = []; + helper.decodeExtraContextProperty('birthsInTransaction'); } +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'study', 'birth', function(event, errors, helper){ + + // Single writer for the denormalized demographics birth date, derived from the saved birth records. + if (!helper.isETL() && idsToSync.length) { + var demographicsUpdates = triggerHelper.computeDemographicsSync(idsToSync); + if (demographicsUpdates.size() > 0) { + helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates); + } + idsToSync = []; + } +}); + EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'study', 'birth', function(helper, scriptErrors, row, oldRow) { if (!oldRow && row.Id && triggerHelper.birthExists(row.Id)) { @@ -110,6 +126,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even species: row['Id/demographics/species'] || null, birth: row.date || null, gender: row['Id/demographics/gender'] || null, + socialCode: row['Id/demographics/socialCode'] || null, taskid: row.taskid, remark: row.remark, QCStateLabel: row.QCStateLabel, @@ -147,11 +164,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 +174,11 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even hasUpdates = true; } + if (obj.socialCode && obj.socialCode !== data.socialCode) { + record.socialCode = obj.socialCode; + hasUpdates = true; + } + if (obj.performedby && obj.performedby !== data.performedby) { record.performedby = obj.performedby; hasUpdates = true; @@ -180,6 +197,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..c7dae52 100644 --- a/nbri_ehr/resources/queries/study/deaths.js +++ b/nbri_ehr/resources/queries/study/deaths.js @@ -8,9 +8,13 @@ require("ehr/triggers").initScript(this); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); var idMap = {}; var deathIdMap = {}; +var idsToSync = []; function onInit(event, helper){ + // the script scope can outlive a single save, so never inherit ids from a prior one + idsToSync = []; + helper.decodeExtraContextProperty('deathsInTransaction'); // Cache valid Ids for check on each row @@ -59,7 +63,6 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even demographicsUpdates.push({ Id: row.Id, death: null, - calculated_status: 'Alive', QCState: helper.getJavaHelper().getQCStateForLabel('Completed').getRowId(), }); @@ -69,8 +72,6 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even function onUpsert(helper, scriptErrors, row, oldRow) { - var demographicsUpdates = []; - if (!helper.isETL()) { //skip other checks so that the admins can update a death record @@ -130,21 +131,6 @@ function onUpsert(helper, scriptErrors, row, oldRow) { EHR.Server.Utils.addError(scriptErrors, 'Id', errorMsg, 'ERROR'); } else { - if (!helper.isValidateOnly() && row.Id && row.date && rowQCState === 'COMPLETED') { - - // update demographics - demographicsUpdates.push({ - Id: row.Id, - death: row.date, - calculated_status: 'Dead', - QCState: helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId() - }); - - console.log('updating demographics death date for animal: ' + row.Id); - helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates); - console.log('updated demographics death date for animal: ' + row.Id); - } - if (!helper.isValidateOnly() && row.date && row.QCStateLabel && EHR.Server.Security.getQCStateByLabel(row.QCStateLabel).PublicData) { var qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); @@ -177,9 +163,31 @@ function onUpsert(helper, scriptErrors, row, oldRow) { EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.AFTER_INSERT, 'study', 'deaths', function(helper, scriptErrors, row, oldRow) { helper.registerDeath(row.Id, row.date); triggerHelper.reportDataChange("study", "deaths", [row.Id]); + + if (row.Id && idsToSync.indexOf(row.Id) === -1) { + idsToSync.push(row.Id); + } +}); + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.AFTER_UPDATE, 'study', 'deaths', function(helper, scriptErrors, row, oldRow) { + if (row.Id && idsToSync.indexOf(row.Id) === -1) { + idsToSync.push(row.Id); + } }); EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'study', 'Deaths', function(event, errors, helper){ + + // Single writer for the denormalized demographics death date. Runs once per save, after the death rows are saved, + // and derives the value from the stored record rather than from the incoming row. calculated_status is left to the + // shared status recalc, which owns the death/departure/re-arrival precedence. + if (!helper.isETL() && idsToSync.length) { + var demographicsUpdates = triggerHelper.computeDemographicsSync(idsToSync); + if (demographicsUpdates.size() > 0) { + helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates); + } + idsToSync = []; + } + var rows = helper.getRows() || []; for (var i = 0; i < rows.length; i++) { var row = rows[i].row; diff --git a/nbri_ehr/resources/queries/study/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/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..25a5dbd 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/AnimalGroupMembers.js @@ -35,6 +35,10 @@ EHR.model.DataModelManager.registerMetadata('AnimalGroupMembers', { return curDate; } }, + enddate: { + shownInGrid: false, + hidden: true + }, groupId: { allowBlank: false, nullable: false, 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/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/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..872eaac 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js @@ -4,7 +4,7 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ /** - * Metadata for the grid-based Bulk Deaths form. The columnConfig widths only take effect in the grid; they are ignored + * Metadata for the grid-based Deaths form. The columnConfig widths only take effect in the grid; they are ignored * when the same fields render in a form panel. */ EHR.model.DataModelManager.registerMetadata('Death', { diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java index 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..ab2f5d4 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java @@ -49,6 +49,9 @@ protected List getFieldKeys(TableInfo ti) keys.add(10, FieldKey.fromString("Id/demographics/gender")); keys.add(12, FieldKey.fromString("Id/demographics/geographic_origin")); + // the social code sits beside Initial Location, whose index the inserts above have shifted, so find it + keys.add(keys.indexOf(FieldKey.fromString("cage")) + 1, FieldKey.fromString("Id/demographics/socialCode")); + return keys; } } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java index 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..893c53a 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java @@ -323,6 +323,103 @@ public boolean deathExists(String id) return false; } + /** + * Derives the denormalized birth/death values on study.demographics from the birth and deaths event records, which + * are authoritative, and returns only the animals whose stored values disagree. The result is intended to be handed + * straight to the shared trigger helper's updateDemographicsRecord(), so that lsid resolution and the demographics + * cache recache stay in the single place that already handles them. + *

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

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

+ * Every lookup is set-based - one query per event dataset for the whole id list, not one per animal - because a + * bulk save can pass hundreds of ids and per-animal SQL in a trigger exhausts the script's wall-clock budget. + * + * @param ids animals touched by the current save + * @return rows ready for updateDemographicsRecord(); empty when nothing has drifted + */ + public List> computeDemographicsSync(List ids) + { + if (ids == null || ids.isEmpty()) + return Collections.emptyList(); + + Set idSet = new HashSet<>(ids); + + Map births = getPublicEventDates("birth", idSet); + Map deaths = getPublicEventDates("deaths", idSet); + + List> updates = new ArrayList<>(); + + TableInfo demographics = getTableInfo("study", "demographics"); + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id"), idSet, CompareType.IN); + TableSelector ts = new TableSelector(demographics, PageFlowUtil.set("Id", "birth", "death"), filter, null); + + for (Map current : ts.getMapCollection()) + { + String id = (String)current.get("Id"); + Map update = new CaseInsensitiveHashMap<>(); + + // A public event record always wins - the same rule createDemographicsRecord() already applies to death on + // insert, extended to updates and to birth. The absence of an event record is NOT evidence the stored value + // is wrong: animals loaded by ETL, or acquired before these datasets were in use, legitimately carry a date + // with no event row, so a missing record leaves the value alone. Clearing a value is only ever driven by an + // explicit delete of the event record. + Date birth = births.get(id); + if (birth != null && differsByDay(birth, (Date)current.get("birth"))) + update.put("birth", birth); + + Date death = deaths.get(id); + if (death != null && differsByDay(death, (Date)current.get("death"))) + update.put("death", death); + + if (!update.isEmpty()) + { + update.put("Id", id); + updates.add(update); + } + } + + if (!updates.isEmpty()) + _log.info("Demographics birth/death out of sync with event records for {} animal(s); updating", updates.size()); + + return updates; + } + + /** Most recent public event date per animal for a demographic event dataset, in a single query. */ + private Map getPublicEventDates(String queryName, Set ids) + { + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id"), ids, CompareType.IN); + filter.addCondition(FieldKey.fromString("qcstate/publicdata"), true); + + Map ret = new HashMap<>(); + new TableSelector(getTableInfo("study", queryName), PageFlowUtil.set("Id", "date"), filter, null) + .forEachMap(row -> { + String id = (String)row.get("Id"); + Date date = ConvertHelper.convert(row.get("date"), Date.class); + // birth and deaths are demographic datasets (one row per animal), but tolerate duplicates from a + // legacy load by keeping the latest rather than picking arbitrarily. + if (date != null && (ret.get(id) == null || date.after(ret.get(id)))) + ret.put(id, date); + }); + + return ret; + } + + /** + * Compares to day precision. Event dates are entered with the time stripped, but values that arrived by ETL or + * predate that behavior can carry a time component; treating those as drift would rewrite the whole colony on the + * first save. + */ + private boolean differsByDay(Date a, Date b) + { + if (a == null || b == null) + return a != b; + + return !DateUtils.isSameDay(a, b); + } + public boolean upsertWeightRecord(Map row) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException { return upsertWeightRecord(row, true); 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..73501bc 100644 --- a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java +++ b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java @@ -71,6 +71,7 @@ import java.io.File; import java.io.IOException; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; @@ -632,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() { @@ -1963,7 +2080,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 +2099,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();