From 1b8b14fb310f46f52d5e3be47ee093447f8ff5d1 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 28 Jul 2026 21:06:59 -0700 Subject: [PATCH 1/7] Scope cage lookups to the EHR study container in housing tables (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Rationale Scope the housing cage lookups to a single container so housing grids, and the queries built on them, keep working on a server with more than one EHR folder. The cage lookup is keyed by container and location together, but the room and days-in-room columns matched on location alone, so a second EHR folder defining the same cage location makes those subqueries match more than one row and the query returns a database error instead of results. Production installations run a single EHR folder per server, so the effect is limited to test and development environments where several EHR folders coexist. ## Related Pull Requests - LabKey/nircEHRModules, branch `26.7_fb_cage_container_scope` — the identical fix in the NIRC customizer. ## Changes - Scope every cage lookup behind the housing room and days-in-room columns to a single container, preferring the EHR study container and falling back to the current one when it is not configured. - Fix a null dereference that could occur while building the room sort field. --- .../nbri_ehr/table/NBRI_EHRCustomizer.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java index 19fe42d..e8f771c 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java @@ -896,15 +896,17 @@ private void ensureSortColumn(AbstractTableInfo ti, ColumnInfo baseColumn) private void customizeHousingTable(AbstractTableInfo ti) { + // ehr_lookups.cage is unique on (Container, Location), so the cage subqueries below must be container-scoped; + // a second EHR folder defining the same location would otherwise make them return multiple rows. + Container lookupContainer = EHRService.get().getEHRStudyContainer(ti.getUserSchema().getContainer()); + if (lookupContainer == null) + lookupContainer = ti.getUserSchema().getContainer(); // as DefaultEHRCustomizer does + if (ti.getColumn("room") == null && ti.getColumn("cage") != null) { - UserSchema us = getUserSchema(ti, "ehr_lookups"); - if (us != null) - { - SQLFragment roomSql = new SQLFragment("(SELECT room FROM ehr_lookups.cage WHERE location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)"); - ExprColumn roomCol = new ExprColumn(ti, "room", roomSql, JdbcType.VARCHAR, ti.getColumn("cage")); - ti.addColumn(roomCol); - } + SQLFragment roomSql = new SQLFragment("(SELECT room FROM ehr_lookups.cage WHERE Container = ? AND location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)", lookupContainer); + ExprColumn roomCol = new ExprColumn(ti, "room", roomSql, JdbcType.VARCHAR, ti.getColumn("cage")); + ti.addColumn(roomCol); ensureSortColumn(ti, ti.getColumn("room")); } @@ -913,8 +915,8 @@ private void customizeHousingTable(AbstractTableInfo ti) TableInfo realTable = getRealTable(ti); if (realTable != null && realTable.getColumn("participantid") != null && realTable.getColumn("date") != null && realTable.getColumn("enddate") != null) { - SQLFragment roomSql = new SQLFragment(realTable.getSqlDialect().getDateDiff(Calendar.DATE, "{fn curdate()}", "COALESCE((SELECT max(h2.enddate) as d FROM " + realTable.getSelectName() + " h2 LEFT JOIN ehr_lookups.cage cg ON h2.cage = cg.location " + - "WHERE h2.enddate IS NOT NULL AND h2.enddate <= " + ExprColumn.STR_TABLE_ALIAS + ".date AND h2.participantid = " + ExprColumn.STR_TABLE_ALIAS + ".participantid AND cg.room != (SELECT room FROM ehr_lookups.cage WHERE location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)), " + ExprColumn.STR_TABLE_ALIAS + ".date)")); + SQLFragment roomSql = new SQLFragment(realTable.getSqlDialect().getDateDiff(Calendar.DATE, "{fn curdate()}", "COALESCE((SELECT max(h2.enddate) as d FROM " + realTable.getSelectName() + " h2 LEFT JOIN ehr_lookups.cage cg ON h2.cage = cg.location AND cg.Container = ? " + + "WHERE h2.enddate IS NOT NULL AND h2.enddate <= " + ExprColumn.STR_TABLE_ALIAS + ".date AND h2.participantid = " + ExprColumn.STR_TABLE_ALIAS + ".participantid AND cg.room != (SELECT room FROM ehr_lookups.cage WHERE Container = ? AND location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)), " + ExprColumn.STR_TABLE_ALIAS + ".date)"), lookupContainer, lookupContainer); ExprColumn roomCol = new ExprColumn(ti, "daysInRoom", roomSql, JdbcType.INTEGER, realTable.getColumn("participantid"), realTable.getColumn("date"), realTable.getColumn("enddate")); roomCol.setLabel("Days In Room"); ti.addColumn(roomCol); From f8f880d7d473a302661bef70380c7f2ac4ec4ff9 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 28 Jul 2026 21:18:02 -0700 Subject: [PATCH 2/7] Fix Record Treatment link to pass schedule slot date as scheduledDate (#4) ## Rationale The Record Treatment link on study.treatment_order passed the order's start date as the scheduledDate URL parameter, so every treatment recorded through it carried the same scheduledDate regardless of which schedule slot was being recorded. The second recording against an order then tripped the duplicate-treatment trigger in study/drug.js ("A treatment has already been entered for this order for this date and time.") while the treatmentSchedule grid still showed the slot as unrecorded, since its status join compares the computed slot time against the stored scheduledDate. ## Related Pull Requests - https://github.com/LabKey/johnsHopkinsEHRModules/pull/667 (same fix for jhu_ehr) - https://github.com/LabKey/nircEHRModules/pull/730 (same fix for nirc_ehr) ## Changes - Extract the inline Record Treatment display column into TreatmentDisplayColumnFactory with an includeScheduledDate flag; emit scheduledDate only when set, ISO-formatted via DateUtil.formatIsoDateShortTime() instead of Date.toString(); add a null guard on category. - The treatment_order Record Treatment link no longer passes scheduledDate; new customizeTreatmentSchedule() adds a treatmentRecord link column to study.treatmentSchedule that passes the slot's date. - treatmentSchedule.sql: drop the t1.treatmentRecord passthrough column inherited from treatment_order. - treatmentSchedule.query.xml: apply the module customizer via javaCustomizer so the new column is added. --- .../queries/study/treatmentSchedule.query.xml | 1 + .../queries/study/treatmentSchedule.sql | 1 - .../nbri_ehr/table/NBRI_EHRCustomizer.java | 97 +++---------- .../table/TreatmentDisplayColumnFactory.java | 130 ++++++++++++++++++ 4 files changed, 147 insertions(+), 82 deletions(-) create mode 100644 nbri_ehr/src/org/labkey/nbri_ehr/table/TreatmentDisplayColumnFactory.java diff --git a/nbri_ehr/resources/queries/study/treatmentSchedule.query.xml b/nbri_ehr/resources/queries/study/treatmentSchedule.query.xml index 59d1f2e..e6f2432 100644 --- a/nbri_ehr/resources/queries/study/treatmentSchedule.query.xml +++ b/nbri_ehr/resources/queries/study/treatmentSchedule.query.xml @@ -2,6 +2,7 @@ + Treatment Schedule /EHR/treatmentDetails.view?key=${lsid} primaryKey diff --git a/nbri_ehr/resources/queries/study/treatmentSchedule.sql b/nbri_ehr/resources/queries/study/treatmentSchedule.sql index 068cb4e..557e183 100644 --- a/nbri_ehr/resources/queries/study/treatmentSchedule.sql +++ b/nbri_ehr/resources/queries/study/treatmentSchedule.sql @@ -38,7 +38,6 @@ JOIN( timestampdiff('SQL_TSI_DAY', cast(t1.dateOnly AS timestamp), dr.dateOnly) + 1 AS daysElapsed, t1.enddate, t1.code, - t1.treatmentRecord, t1.volume, t1.vol_units, t1.concentration, diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java index e8f771c..38aa173 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java @@ -131,6 +131,11 @@ public void customize(TableInfo table) customizeTreatmentOrder(ti); } + if (matches(ti, "study", "treatmentSchedule")) + { + customizeTreatmentSchedule(ti); + } + if (matches(ti, "study", "prc_order")) { customizeProcedureOrder(ti); @@ -1067,88 +1072,18 @@ private void customizeTreatmentOrder(AbstractTableInfo ti) { WrappedColumn col = new WrappedColumn(ti.getColumn("objectid"), "treatmentRecord"); col.setLabel("Record Treatment"); - col.setDisplayColumnFactory(new DisplayColumnFactory() { - - @Override - public DisplayColumn createRenderer(final ColumnInfo colInfo) - { - return new DataColumn(colInfo){ - - @Override - public void renderGridCellContents(RenderContext ctx, HtmlWriter out) - { - String objectid = (String)getBoundColumn().getValue(ctx); - Date date = (Date)ctx.get("date"); - String caseid = (String)ctx.get("caseid"); - String category = (String)ctx.get("category"); - ActionURL url = new ActionURL("ehr", "dataEntryForm", ti.getUserSchema().getContainer()); - if (!ti.getUserSchema().getContainer().hasPermission(ti.getUserSchema().getUser(), EHRClinicalEntryPermission.class)) - return; - - if (category.equals("Behavior")) - { - if (caseid != null) - { - url.addParameter("formType", "Behavioral Rounds"); - url.addParameter("caseid", caseid); - } - else - { - url.addParameter("formType", "Bulk Behavior Entry"); - } - } - else - { - if (caseid != null) - { - url.addParameter("formType", "Clinical Rounds"); - url.addParameter("caseid", caseid); - } - else - { - url.addParameter("formType", "medicationTreatment"); - } - } - - url.addParameter("treatmentid", objectid); - url.addParameter("scheduledDate", date.toString()); - - String returnUrl = new ActionURL("ehr", "animalHistory", ti.getUserSchema().getContainer()) + "#inputType:none&showReport:0&activeReport:clinMedicationSchedule"; - url.addParameter("returnUrl", returnUrl); - - out.write(LinkBuilder.labkeyLink("Record Treatment", url).target("_blank")); - } - - @Override - public void addQueryFieldKeys(Set keys) - { - super.addQueryFieldKeys(keys); - keys.add(getBoundColumn().getFieldKey()); - keys.add(FieldKey.fromString("date")); - keys.add(FieldKey.fromString("caseid")); - keys.add(FieldKey.fromString("category")); - } - - @Override - public boolean isSortable() - { - return false; - } - - @Override - public boolean isFilterable() - { - return false; - } + col.setDisplayColumnFactory(new TreatmentDisplayColumnFactory(false)); + ti.addColumn(col); + } + } - @Override - public boolean isEditable() - { - return false; - } - }; - } - }); + private void customizeTreatmentSchedule(AbstractTableInfo ti) + { + if (ti.getColumn("treatmentRecord") == null && ti.getColumn("objectid") != null) + { + WrappedColumn col = new WrappedColumn(ti.getColumn("objectid"), "treatmentRecord"); + col.setLabel("Record Treatment"); + col.setDisplayColumnFactory(new TreatmentDisplayColumnFactory(true)); ti.addColumn(col); } } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/TreatmentDisplayColumnFactory.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/TreatmentDisplayColumnFactory.java new file mode 100644 index 0000000..83db906 --- /dev/null +++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/TreatmentDisplayColumnFactory.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.nbri_ehr.table; + +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.DataColumn; +import org.labkey.api.data.DisplayColumn; +import org.labkey.api.data.DisplayColumnFactory; +import org.labkey.api.data.RenderContext; +import org.labkey.api.ehr.security.EHRClinicalEntryPermission; +import org.labkey.api.query.FieldKey; +import org.labkey.api.util.DateUtil; +import org.labkey.api.util.LinkBuilder; +import org.labkey.api.view.ActionURL; +import org.labkey.api.writer.HtmlWriter; + +import java.util.Date; +import java.util.Set; + +/** + * Display column factory for creating Record Treatment links. When includeScheduledDate is set, the row's date is + * passed as the scheduledDate URL parameter, so it should only be set on tables whose date column is the scheduled + * slot being recorded (e.g. treatmentSchedule), not the treatment order's start date. + */ +public class TreatmentDisplayColumnFactory implements DisplayColumnFactory +{ + private final boolean _includeScheduledDate; + + public TreatmentDisplayColumnFactory(boolean includeScheduledDate) + { + _includeScheduledDate = includeScheduledDate; + } + + @Override + public DisplayColumn createRenderer(final ColumnInfo colInfo) + { + return new DataColumn(colInfo){ + + @Override + public void renderGridCellContents(RenderContext ctx, HtmlWriter out) + { + String objectid = (String)getBoundColumn().getValue(ctx); + Date date = (Date)ctx.get("date"); + String caseid = (String)ctx.get("caseid"); + String category = (String)ctx.get("category"); + ActionURL url = new ActionURL("ehr", "dataEntryForm", colInfo.getParentTable().getUserSchema().getContainer()); + if (!colInfo.getParentTable().getUserSchema().getContainer().hasPermission(colInfo.getParentTable().getUserSchema().getUser(), EHRClinicalEntryPermission.class)) + return; + + if (category == null) + return; + + if (category.equals("Behavior")) + { + if (caseid != null) + { + url.addParameter("formType", "Behavioral Rounds"); + url.addParameter("caseid", caseid); + } + else + { + url.addParameter("formType", "Bulk Behavior Entry"); + } + } + else + { + if (caseid != null) + { + url.addParameter("formType", "Clinical Rounds"); + url.addParameter("caseid", caseid); + } + else + { + url.addParameter("formType", "medicationTreatment"); + } + } + + url.addParameter("treatmentid", objectid); + if (_includeScheduledDate && date != null) + url.addParameter("scheduledDate", DateUtil.formatIsoDateShortTime(date)); + + String returnUrl = new ActionURL("ehr", "animalHistory", colInfo.getParentTable().getUserSchema().getContainer()) + "#inputType:none&showReport:0&activeReport:clinMedicationSchedule"; + url.addParameter("returnUrl", returnUrl); + + out.write(LinkBuilder.labkeyLink("Record Treatment", url).target("_blank")); + } + + @Override + public void addQueryFieldKeys(Set keys) + { + super.addQueryFieldKeys(keys); + keys.add(getBoundColumn().getFieldKey()); + keys.add(FieldKey.fromString("date")); + keys.add(FieldKey.fromString("caseid")); + keys.add(FieldKey.fromString("category")); + } + + @Override + public boolean isSortable() + { + return false; + } + + @Override + public boolean isFilterable() + { + return false; + } + + @Override + public boolean isEditable() + { + return false; + } + }; + } +} From 781ef8ed5c4a40713cdcd8773e3856ea260b585a Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sat, 1 Aug 2026 05:46:59 -0700 Subject: [PATCH 3/7] Add birth, arrival and conception lookups and relax entry validation (#6) ## Rationale Adds the fields and lookup codes needed to bulk load historical birth, arrival and conception records into the NBRI EHR. The legacy source system keys its source, species and pregnancy-outcome lookups by short alpha codes rather than the numeric placeholders the module was seeded with, so those lookups have to be recoded before incoming values will resolve. The historical records also predate several of the required-field rules on the current entry forms. ## Changes - Add birth condition, delivery mode and breeding type to the birth record, and an estimated-date flag to conception. - Recode the source, species and pregnancy outcome lookups to the alpha codes used by the source system, and point the arrival and departure lookups at them. - Surface the task and status columns on the conception grid, with the standard status highlighting, and report a conception's offspring alongside its outcome. - Add a Start with Conception action to the birth form that seeds a new birth record from an existing conception, inferring the newborn's species from the dam. - Move project and protocol entry on the arrival and birth forms into dedicated assignment sections, which relaxes animal Id validation for animals that have no demographics record yet. - Relax required-field validation on the arrival and birth entry forms so historical records can be loaded. These rules are left in place as commented-out code so they can be restored once the loads are done. - The conception estimated-date flag is added to the in-flight 26.7 schema script rather than a new one, so servers that already ran it need a bootstrap. --- nbri_ehr/resources/data/birth_condition.tsv | 3 + nbri_ehr/resources/data/breeding_type.tsv | 10 + nbri_ehr/resources/data/delivery_mode.tsv | 4 + nbri_ehr/resources/data/editable_lookups.tsv | 3 + nbri_ehr/resources/data/gender_codes.tsv | 6 +- nbri_ehr/resources/data/lookup_sets.tsv | 4 + nbri_ehr/resources/data/lookupsManifest.tsv | 4 + .../resources/data/lookupsManifestTest.tsv | 3 + nbri_ehr/resources/data/pregnancy_result.tsv | 8 +- nbri_ehr/resources/data/source.tsv | 209 +++++++++--------- nbri_ehr/resources/data/species.tsv | 20 +- nbri_ehr/resources/data/species_codes.tsv | 20 +- nbri_ehr/resources/data/status_codes.tsv | 5 + .../resources/queries/ehr/project.query.xml | 7 + .../queries/nbri_ehr/Conception.query.xml | 74 ++++++- .../nbri_ehr/ConceptionsByDam.query.xml | 8 + .../queries/nbri_ehr/ConceptionsByDam.sql | 5 +- nbri_ehr/resources/queries/study/arrival.js | 1 - .../resources/queries/study/arrival.query.xml | 3 +- nbri_ehr/resources/queries/study/birth.js | 2 +- .../resources/queries/study/birth.query.xml | 41 +++- .../queries/study/demographics.query.xml | 3 - .../queries/study/demographics/.qview.xml | 1 - .../queries/study/demographicsSource.sql | 2 +- .../queries/study/departure.query.xml | 3 +- .../study/protocolAssignment.query.xml | 1 - .../study/datasets/datasets_metadata.xml | 12 +- .../postgresql/nbri_ehr-26.000-26.001.sql | 1 + nbri_ehr/resources/schemas/nbri_ehr.xml | 1 + nbri_ehr/resources/scripts/nbri_triggers.js | 10 +- .../web/nbri_ehr/model/sources/Arrival.js | 41 +--- .../web/nbri_ehr/model/sources/Assignment.js | 4 +- .../web/nbri_ehr/model/sources/Birth.js | 56 +++-- .../web/nbri_ehr/model/sources/Conception.js | 7 + .../window/StartWithConceptionWindow.js | 165 ++++++++++++++ .../dataentry/form/NBRIArrivalFormType.java | 6 + .../dataentry/form/NBRIBirthFormType.java | 8 +- .../section/NBRIBirthFormSection.java | 30 ++- .../nbri_ehr/table/NBRI_EHRCustomizer.java | 2 +- .../tests.nbri_ehr/NBRI_EHRTest.java | 180 ++++++++++++--- 40 files changed, 735 insertions(+), 238 deletions(-) create mode 100644 nbri_ehr/resources/data/birth_condition.tsv create mode 100644 nbri_ehr/resources/data/breeding_type.tsv create mode 100644 nbri_ehr/resources/data/delivery_mode.tsv create mode 100644 nbri_ehr/resources/data/status_codes.tsv create mode 100644 nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js diff --git a/nbri_ehr/resources/data/birth_condition.tsv b/nbri_ehr/resources/data/birth_condition.tsv new file mode 100644 index 0000000..bdb4e26 --- /dev/null +++ b/nbri_ehr/resources/data/birth_condition.tsv @@ -0,0 +1,3 @@ +value title sort_order +L Live 1 +D Dead 2 diff --git a/nbri_ehr/resources/data/breeding_type.tsv b/nbri_ehr/resources/data/breeding_type.tsv new file mode 100644 index 0000000..dea0f78 --- /dev/null +++ b/nbri_ehr/resources/data/breeding_type.tsv @@ -0,0 +1,10 @@ +value title sort_order +A Assigned Breeding Protocol 1 +C Cull 2 +H Held from Mating Activity 3 +M Multi-Male 4 +P Project Breeding 5 +Q Testing as Breeder 6 +S Single Male Harem 7 +T Time-Mated 8 +O Not Assigned 9 \ No newline at end of file diff --git a/nbri_ehr/resources/data/delivery_mode.tsv b/nbri_ehr/resources/data/delivery_mode.tsv new file mode 100644 index 0000000..cea8a58 --- /dev/null +++ b/nbri_ehr/resources/data/delivery_mode.tsv @@ -0,0 +1,4 @@ +value title +V Vaginal +N Surgical-clinical +NX Surgical experimental \ No newline at end of file diff --git a/nbri_ehr/resources/data/editable_lookups.tsv b/nbri_ehr/resources/data/editable_lookups.tsv index 90cc8af..4306a94 100644 --- a/nbri_ehr/resources/data/editable_lookups.tsv +++ b/nbri_ehr/resources/data/editable_lookups.tsv @@ -18,10 +18,12 @@ 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. ehr_lookups blood_tube_volumes Clinical Blood Tube Volumes Used in blood draw datasets. +ehr_lookups breeding_type Colony Management Breeding Type Breeding group assignment codes. ehr_lookups cage_type Colony Management Cage Type Used in cage details. ehr_lookups calculated_status_codes Colony Management Calculated Status Animal status values. ehr_lookups capillary_refill_time Clinical Capillary Refill Times Used clinical observations. @@ -37,6 +39,7 @@ ehr_lookups country_category Colony Management Country Category ehr_lookups daily_enrich_codes Behavior Daily enrichment codes. ehr_lookups data_category Clinical Data Categories Used in datasets. ehr_lookups death_reason Colony Management Death Reason +ehr_lookups delivery_mode Colony Management Delivery Mode ehr_lookups delivery_state Colony Management Delivery State ehr_lookups dental_obs Clinical Dental Observation Types Clinical observation values. ehr_lookups derm_obs Clinical Dermatologic Observation Types Clinical observation values. diff --git a/nbri_ehr/resources/data/gender_codes.tsv b/nbri_ehr/resources/data/gender_codes.tsv index 01c67fb..a55c8a7 100644 --- a/nbri_ehr/resources/data/gender_codes.tsv +++ b/nbri_ehr/resources/data/gender_codes.tsv @@ -1,4 +1,4 @@ code meaning -1 unknown -2 female -3 male \ No newline at end of file +U Unknown +F Female +M Male \ 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 613eb8e..5243772 100644 --- a/nbri_ehr/resources/data/lookup_sets.tsv +++ b/nbri_ehr/resources/data/lookup_sets.tsv @@ -16,8 +16,10 @@ 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 cage_type Cage Type value title capillary_refill_time Capillary Refill Time value card_format Card Format value title @@ -31,6 +33,7 @@ country_category Country Category value title daily_enrich_codes Daily Enrichment Codes value data_category Data Category Field Values value death_reason Death Reason value +delivery_mode Delivery Mode value title delivery_state Delivery State value title dental_obs Dental Observations value derm_obs Dermatologic Observations value @@ -98,6 +101,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 +status_codes Status Code Field Values value title stool_score Stool Score value stool_types Stool Types value tb_obs_score TB Obs Score value diff --git a/nbri_ehr/resources/data/lookupsManifest.tsv b/nbri_ehr/resources/data/lookupsManifest.tsv index defb466..472d51d 100644 --- a/nbri_ehr/resources/data/lookupsManifest.tsv +++ b/nbri_ehr/resources/data/lookupsManifest.tsv @@ -18,10 +18,12 @@ bcs_score behavior_abnormality behavior_mgmt_codes behavior_types +birth_condition blood_draw_reason blood_draw_tube_type blood_sample_type blood_tube_volumes +breeding_type cage_type calculated_status_codes capillary_refill_time @@ -35,6 +37,7 @@ country_category daily_enrich_codes data_category death_reason +delivery_mode delivery_state dental_obs derm_obs @@ -109,6 +112,7 @@ sib_score source snomed skin_problem +status_codes stool_score stool_types tb_obs_score diff --git a/nbri_ehr/resources/data/lookupsManifestTest.tsv b/nbri_ehr/resources/data/lookupsManifestTest.tsv index b2006c8..b8946ef 100644 --- a/nbri_ehr/resources/data/lookupsManifestTest.tsv +++ b/nbri_ehr/resources/data/lookupsManifestTest.tsv @@ -17,10 +17,12 @@ bcs_score behavior_abnormality behavior_mgmt_codes behavior_types +birth_condition blood_draw_reason blood_draw_tube_type blood_sample_type blood_tube_volumes +breeding_type cage_type calculated_status_codes capillary_refill_time @@ -34,6 +36,7 @@ country_category daily_enrich_codes data_category death_reason +delivery_mode delivery_state dental_obs derm_obs diff --git a/nbri_ehr/resources/data/pregnancy_result.tsv b/nbri_ehr/resources/data/pregnancy_result.tsv index 1c615bc..819182e 100644 --- a/nbri_ehr/resources/data/pregnancy_result.tsv +++ b/nbri_ehr/resources/data/pregnancy_result.tsv @@ -1,3 +1,7 @@ value title -1 Stillborn -2 Abort \ No newline at end of file +NT No Tissue +FD Fetal Death +FN Found at necropsy +FX Live, Term, euthanized at birth +ND Live, Died day of birth (lungs inflated) +FL Fetal Delivery, live in Utero \ No newline at end of file diff --git a/nbri_ehr/resources/data/source.tsv b/nbri_ehr/resources/data/source.tsv index 0d893c4..5c3f18a 100644 --- a/nbri_ehr/resources/data/source.tsv +++ b/nbri_ehr/resources/data/source.tsv @@ -1,108 +1,103 @@ code meaning -1 Adv Bioscience Labs -2 Alamogordo PrimateFaclty -3 Alpha Genesis, Inc. -4 Barton West End Farm -5 Battelle Memorial Inst -6 Baylor Research Inst -7 BIOCULTURE (MTIUS) LTD -8 Bioculture US LLC -9 BIOQUAL, Inc. -10 Boehringer Ingelheim -11 Boston University -12 Buckshire Corporation -13 Caribbean Primate Rsrch -14 CBNC -15 CDC -16 Charles River Laboratory -17 Charles River/Reno -18 Chimp Haven -19 China/Guangxi GF Sci Pri -20 Chiron Corp -21 CiToxLab North America -22 Covance Research Prod -23 CR Rsrch Models Houston -24 Ctr Captive Chimp Care -25 Cynologics Ltd -26 DHMRI -27 Duke Univ Medical Ctr -28 Durham Research Center -29 Emory University YPRC -30 Envigo Global Services -31 Guangdong Landau Biotech -32 Hainan, China -33 Harvard Medical School -34 Indonesia (Pt. W. Sat) -35 Johns Hopkins University -36 LC Preclinical Research -37 Lovelace Biomedical -38 LRRI -39 LSU Health Science BR -40 LSU Health Science NO -41 LSU Health Science Shv -42 Mannheimer Foundation -43 Mass. General Hospital -44 MD Anderson Cancer Ctr. -45 Merck & Co (Rahway) -46 Merck & Co (WP) -47 Merck Research Labs WP -48 Nationwide Children's -49 New England NPRC -50 New York University -51 NIAID (Bioqual) -52 NIAID Morgan Island -53 NIAID NIH Animal Ctr -54 NICHD/NIH -55 NIH -56 NIH Animal Center -57 NINDS NIH Animal Ctr -58 Novartis Pharm. Corp. -59 Novartis Vaccines Srl -60 NYU School of Medicine -61 Oregon NPRC -62 Pfizer -63 Pfizer-Andover -64 Pfizer-Pearl River -65 PreLabs -66 Primate Products -67 Primedica Labs -68 PrimGen -69 Primgen CSP -70 RainForest Adventures -71 Republic of Mauritius -72 Rocky Mountain Lab -73 Save the Chimps -74 Siconbrec Inc -75 Sierra Biomedical -76 SNBL-SRC -77 SRI International -78 St. Jude Childrens Rsrch -79 St. Kitts -80 Stanford Univ School Med -81 Stony Brook University -82 SUNY Downstate Medical -83 SW Found Biomed Rsrch -84 Texas Biomed -85 Three Springs Scientific -86 Tulane NPC (Covington) -87 Tulane Univ Medical Ctr -88 Univ Alabama Birmingham -89 Univ of Illinois -90 Univ of Kansas Med Ctr -91 Univ of Maryland -92 Univ of Nebraska Med Ctr -93 Univ of Pittsburgh -94 Univ of Texas at Austin -95 Univ of Washington NPRC -96 Univ of Wisconsin -97 Univ Tex MD Anderson CC -98 USAMRICD -99 USAMRIID -100 Virginia Commonwealth U -101 WakeForest School of Med -102 WaNPRC -103 Worldwide Primates, Inc. -104 WRAIR -105 Wyeth-Ayerst Rsrch (PR) -106 Yale Univ Sch Medicine -107 Yerkes Regional PRC \ No newline at end of file +AAI Asiatic Animal Imports +BIOQUAL Bioqual, Incorporated +BRANDEIS Brandeis University +CA-DPH Calif Dept Public Health Facilities +CA-DOH California State Department of Health +CPRC Carribean Primate Center +CWRU Case Western Reserve University +UCD-CNS Center for Neuroscience, UCD +CR-KL Charles River - Key Lois +CRL Charles River Labs +CRRP Charles River Research Primates Inc +BROOKFIELD Chicago Zoological Park (Brookfield Zoo) +CHILDRENS Childrens Hospital +CHIMR Christ Hospital Inst. for Medical Res. +CSU Colorado State University +CORNELL Cornell University +COULSTON Coulston Foundation +COVANCE Covance Research Products, Inc. +PRIMGEN CSP-Primgen +CUTTER Cutter Lab +DMT Del Mundo Trading +EPZ El Paso Zoological Gardens +ENVIGO Envigo +HAHNEMANN Hahnemann University +HL Hazelton Laboratories +HLA Hazelton Laboratories America Inc. +UNK Institution Unknown +ISU-VDL Iowa State Univ, Vet Diagnostic Lab +JVL Jan Vacek Limited +JHMC Jewish Hospital & Medical Center +JRI Johnson Research Institute +KNLPC Kunming National Laboratory Primate Ctr. +LABS Laboratory Animal Breeders and Services +LABSINDO Labsindo +LEMSIP LEMSIP, New York Medical Center, RDI +LAIR Letterman Army Res Inst-Presidio +LB Litton Bionetics +LLU Loma Linda University +MSU-MI Michigan State University +MSU-MT Montana State University +MPI MPI Research +NASA-ARC NASA-Ames Research Center +NAMRL Naval Aerospace Medical Research Lab +NEPRC New England Primate Research Center +NIHAC NIH Animal Center +ORPRC Oregon Regional Primate Research Center +PARC-SAF Parc Safari African +PPC Perrine Primate Center +PPP Peruvian Primatological Project +PET-FARM Pet Farm +PRIMLAB Primate Laboratory +PPI Primate Products, Incorporated +PRIVATE Private Party +RIEPT Res. Inst. of Exp. Pathology & Therapy +SALK Salk Institute +SFGH San Francisco General Hospital +SNBL Shin Nippon Biomedical Laboratories +SICONBREC Siconbrec +SBM Sierra Biomedical +SKB Smith, Kline, Beecham +SIU Southern Illinois University +SIU-SOM Southern Illinois University Med School +SORI Southern Research Institute +SFRE Southwest Foundation for Res. & Educ. +SWRF Southwest Research Foundation +SWRI Southwest Research Institute +SOPF Stanford Outdoor Primate Facility +SRI Stanford Research Institute +SUNY-SB State Univ of New York at Stony Brook +SXZ Suzhou Xishan Zhongke Lab Animal Co. +SYNTEX Syntex (USA) Incorporated +TARPON Tarpon Zoo +TTUHSC Texas Tech Health Science Center +BROOKS-AFB Texas, Brooks Air Force Base +TPI The Parkinson's Institute +TNPRC Tulane (Delta) Regional Primate Res Ctr +UCB UC Berkeley +UCLA UC Los Angeles +UCR UC Riverside +UCSD UC San Diego +UCSF UC San Francisco Vivarium +UC-ARS Univ of Calif, Animal Resources Service +GPC Univ of Gottingen Primate Center +UH-RAF Univ of Hawaii, Research Animal Facility +OUHSC Univ of Oklahoma Health Sci. Center +UTHSC-H Univ of Texas, Health Sci Ctr, Houston +UCHSC University of Colorado Health Sciences +UCMC University of Colorado Medical Center +UIC University of Illinois at Chicago +UNV University of Nevada +UNM University of New Mexico +UPR University of Puerto Rico +UTSCC University of Texas System Cancer Center +UNK-CN Unknown Institution, China +VBS Valley Biosystems +WFSM Wake Forest School of Medicine +WANPRC Washington Regional Primate Research Ctr +WHMC Wilford Hall Medical Center +WRPRC Wisconsin Regional Primate Research Ctr +WAI Woodward Asiatic Imports +WWP World Wide Primates, Inc. +YPRC Yemassee Primate Research Center diff --git a/nbri_ehr/resources/data/species.tsv b/nbri_ehr/resources/data/species.tsv index c95e4bd..12d3118 100644 --- a/nbri_ehr/resources/data/species.tsv +++ b/nbri_ehr/resources/data/species.tsv @@ -1,7 +1,15 @@ Common Scientific Name Id Prefix Mhc Prefix Max Blood Draw (mL/kg) Max Percent Blood Drawn Blood Reconstitution Interval (Days) Cites Code Date Disabled USDA Gestation -African Green Monkey Chlorocebus aethiops AGM 3.0000 1.0000 7.0000 -Brown-Tufted Capuchin Cebus apella CAP 3.0000 1.0000 7.0000 -Cynomolgus Macaque Macaca fascicularis CYN 3.0000 1.0000 7.0000 -Marmoset 3.0000 1.0000 7.0000 -Pig-Tailed Macaque Macaca nemestrina PIG 3.0000 1.0000 7.0000 -Rhesus Macaque Macaca mulatta RHM 3.0000 1.0000 7.0000 \ No newline at end of file +Rhesus Macaque Macaca Mulatta 3.0000 1.0000 7.0000 +Pig-Tailed Macaque Macaca Nemestrina 3.0000 1.0000 7.0000 +Bonnet Macaque Macaca Radiata 3.0000 1.0000 7.0000 +Olive Baboon Papio Anubis 3.0000 1.0000 7.0000 +Yellow Baboon Papio Cynocephalus 3.0000 1.0000 7.0000 +Squirrel Monkey Saimiri Sciureus 3.0000 1.0000 7.0000 +African Green Monkey Chlorocebus Aethiops 3.0000 1.0000 7.0000 +Domestic Dog Canis Familiaris 3.0000 1.0000 7.0000 +Formosan Rock Macaque Macaca Cyclopis 3.0000 1.0000 7.0000 +Japanese Macaque Macaca Fuscata 3.0000 1.0000 7.000 +Talapoin Monkey Cercopithecus Talapoin 3.0000 1.0000 7.0000 +Sykes' Monkey Cercopithecus M Albogulari 3.0000 1.0000 7.0000 +Dusky Titi Monkey Callicebus Moloch 3.0000 1.0000 7.0000 +Brown-Tufted Capuchin Cebus Apella 3.0000 1.0000 7.0000 \ No newline at end of file diff --git a/nbri_ehr/resources/data/species_codes.tsv b/nbri_ehr/resources/data/species_codes.tsv index df698e7..feb4100 100644 --- a/nbri_ehr/resources/data/species_codes.tsv +++ b/nbri_ehr/resources/data/species_codes.tsv @@ -1,7 +1,15 @@ Code Scientific Name Common Name Description Genus Species Date Disabled -1 Chlorocebus aethiops AGM African Green Monkey 3 -10 Macaca mulatta RHM Rhesus Macaque 3 -18 Marmoset 3 -4 Cebus apella CAP Brown-Tufted Capuchin 3 -7 Macaca fascicularis CYN Cynomolgus Macaque 3 -9 Macaca nemestrina PIG Pig-Tailed Macaque 3 \ No newline at end of file +MMU Macaca Mulatta Rhesus Macaque +MNE Macaca Nemestrina Pig-Tailed Macaque +MRA Macaca Radiata Bonnet Macaque +PAN Papio Anubis Olive Baboon +PCY Papio Cynocephalus Yellow Baboon +SSC Saimiri Sciureus Squirrel Monkey +CAE Chlorocebus Aethiops African Green / Vervet +DOG Canis Familiaris Domestic Dog +MCY Macaca Cyclopis Formosan Rock Macaque +MFU Macaca Fuscata Japanese Macaque +CTA Cercopithecus Talapoin Talapoin Monkey +CMA Cercopithecus M Albogulari Sykes' Monkey +CMO Callicebus Moloch Dusky Titi Monkey +CAP Cebus Apella Brown-Tufted Capuchin \ No newline at end of file diff --git a/nbri_ehr/resources/data/status_codes.tsv b/nbri_ehr/resources/data/status_codes.tsv new file mode 100644 index 0000000..b131086 --- /dev/null +++ b/nbri_ehr/resources/data/status_codes.tsv @@ -0,0 +1,5 @@ +value title +Alive Alive +Dead Dead +Escaped Escaped +Shipped Shipped diff --git a/nbri_ehr/resources/queries/ehr/project.query.xml b/nbri_ehr/resources/queries/ehr/project.query.xml index d2db2ca..5b72f21 100644 --- a/nbri_ehr/resources/queries/ehr/project.query.xml +++ b/nbri_ehr/resources/queries/ehr/project.query.xml @@ -47,6 +47,13 @@ true + + + core + Users + UserId + DisplayName + true diff --git a/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml b/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml index 0eb26a8..7fc6298 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml +++ b/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml @@ -14,16 +14,28 @@ Conception Date - - Conception Date - Conception Term Date + + Estimated + Conception date is estimated rather than observed + true + + Task Id + ALWAYS_OFF + + ehr + tasks + taskid + rowid + + /ehr/dataEntryFormDetails.view?formType=${TaskId/formtype}&taskid=${TaskId} + Status @@ -31,6 +43,62 @@ qcstate rowid + + + + + + FBEC5D + + + + + + FBEC5D + + + + + + FBEC5D + + + + + + FF0000 + + + + + + FBEC5D + + + + + + FBEC5D + + + + + + FF0000 + + + + + + FBEC5D + + + + + + FBEC5D + +
diff --git a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.query.xml b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.query.xml index 866a7da..b315c2b 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.query.xml +++ b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.query.xml @@ -11,9 +11,17 @@ Conception Id + + Estimated + Conception date is estimated rather than observed + Conception Outcome + + Offspring + Animal born from this conception, if a birth record references it +
diff --git a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql index 4554f1d..ae7697f 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql +++ b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql @@ -8,16 +8,19 @@ SELECT c.ConceptId, c.ConceptDate, c.ConceptTermDate, + c.Estimated, c.Sire, CASE WHEN b.conceptId IS NOT NULL THEN 'Live Birth' WHEN po.conceptId IS NOT NULL THEN COALESCE(po.result, 'Unknown') ELSE 'Unknown' END AS conceptionOutcome, + b.offspring, c.Remark, c.QCState AS qcstate FROM Conception c -LEFT JOIN (SELECT DISTINCT b.conceptId FROM study.birth b WHERE b.conceptId IS NOT NULL) b +-- a conception yields at most one birth; the aggregate only guards against duplicates the birth trigger warns about but does not block +LEFT JOIN (SELECT b.conceptId, MAX(b.Id) AS offspring FROM study.birth b WHERE b.conceptId IS NOT NULL GROUP BY b.conceptId) b ON b.conceptId = c.ConceptId LEFT JOIN (SELECT p.conceptId, MAX(p.result.title) AS result FROM study.pregnancy p WHERE p.conceptId IS NOT NULL GROUP BY p.conceptId) po ON po.conceptId = c.ConceptId diff --git a/nbri_ehr/resources/queries/study/arrival.js b/nbri_ehr/resources/queries/study/arrival.js index 251c651..2010ffd 100644 --- a/nbri_ehr/resources/queries/study/arrival.js +++ b/nbri_ehr/resources/queries/study/arrival.js @@ -40,7 +40,6 @@ 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.source = row.sourceFacility || null; if (row.QCStateLabel) { row.qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); diff --git a/nbri_ehr/resources/queries/study/arrival.query.xml b/nbri_ehr/resources/queries/study/arrival.query.xml index ffe2108..0955496 100644 --- a/nbri_ehr/resources/queries/study/arrival.query.xml +++ b/nbri_ehr/resources/queries/study/arrival.query.xml @@ -66,7 +66,8 @@ ehr_lookups source - meaning + code + meaning diff --git a/nbri_ehr/resources/queries/study/birth.js b/nbri_ehr/resources/queries/study/birth.js index e3e5fd0..cd0cce1 100644 --- a/nbri_ehr/resources/queries/study/birth.js +++ b/nbri_ehr/resources/queries/study/birth.js @@ -38,7 +38,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even //when updating a record that already carries this conception id, the existing row accounts for one match var conceptIdThreshold = (oldRow && oldRow.conceptId === row.conceptId) ? 1 : 0; if (triggerHelper.totalRecords('study', 'birth', 'conceptId', row.conceptId) > conceptIdThreshold) { - EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id is already used by another birth record', 'INFO'); + EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id is already used by another birth record', 'WARN'); } if (triggerHelper.totalRecords('study', 'pregnancy', 'conceptId', row.conceptId) > 0) { diff --git a/nbri_ehr/resources/queries/study/birth.query.xml b/nbri_ehr/resources/queries/study/birth.query.xml index 8eccc34..18a5a92 100644 --- a/nbri_ehr/resources/queries/study/birth.query.xml +++ b/nbri_ehr/resources/queries/study/birth.query.xml @@ -8,11 +8,18 @@ - - + Birth Date + + Conception Id + + nbri_ehr + Conception + ConceptId + + Birth Location 80 @@ -25,6 +32,25 @@ cage + + Delivery Mode + false + + ehr_lookups + delivery_mode + value + title + + + + Birth Condition + + ehr_lookups + birth_condition + value + title + + Project @@ -43,12 +69,13 @@ title - - Conception Id + + Breeding Type - nbri_ehr - Conception - ConceptId + ehr_lookups + breeding_type + value + title diff --git a/nbri_ehr/resources/queries/study/demographics.query.xml b/nbri_ehr/resources/queries/study/demographics.query.xml index bff3d17..f6fafa6 100644 --- a/nbri_ehr/resources/queries/study/demographics.query.xml +++ b/nbri_ehr/resources/queries/study/demographics.query.xml @@ -99,9 +99,6 @@ meaning - - Source - CITES diff --git a/nbri_ehr/resources/queries/study/demographics/.qview.xml b/nbri_ehr/resources/queries/study/demographics/.qview.xml index 56f1f23..439cd0a 100644 --- a/nbri_ehr/resources/queries/study/demographics/.qview.xml +++ b/nbri_ehr/resources/queries/study/demographics/.qview.xml @@ -12,7 +12,6 @@ - diff --git a/nbri_ehr/resources/queries/study/demographicsSource.sql b/nbri_ehr/resources/queries/study/demographicsSource.sql index 9de9dfd..6ee341e 100644 --- a/nbri_ehr/resources/queries/study/demographicsSource.sql +++ b/nbri_ehr/resources/queries/study/demographicsSource.sql @@ -17,7 +17,7 @@ SELECT WHEN T1.EarliestArrival IS NULL AND d.birth IS NOT NULL THEN true ELSE false END as fromCenter, - d.source as source, + T2.sourceFacility as source, CASE WHEN T1.EarliestArrival IS NULL AND d.birth IS NOT NULL THEN 'Born at NBRI' diff --git a/nbri_ehr/resources/queries/study/departure.query.xml b/nbri_ehr/resources/queries/study/departure.query.xml index 19845d1..5955592 100644 --- a/nbri_ehr/resources/queries/study/departure.query.xml +++ b/nbri_ehr/resources/queries/study/departure.query.xml @@ -13,7 +13,8 @@ ehr_lookups source - meaning + code + meaning diff --git a/nbri_ehr/resources/queries/study/protocolAssignment.query.xml b/nbri_ehr/resources/queries/study/protocolAssignment.query.xml index ac61d1c..d95eb07 100644 --- a/nbri_ehr/resources/queries/study/protocolAssignment.query.xml +++ b/nbri_ehr/resources/queries/study/protocolAssignment.query.xml @@ -15,7 +15,6 @@ ehr protocol protocol - title diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml index 55e48ea..42317f7 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -224,6 +224,15 @@ varchar + + varchar + + + varchar + + + varchar + @@ -469,9 +478,6 @@ varchar - - varchar - varchar diff --git a/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.000-26.001.sql b/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.000-26.001.sql index 964b91c..866fefc 100644 --- a/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.000-26.001.sql +++ b/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.000-26.001.sql @@ -9,6 +9,7 @@ CREATE TABLE nbri_ehr.Conception ConceptId VARCHAR(100), ConceptDate TIMESTAMP, ConceptTermDate TIMESTAMP, + Estimated BOOLEAN DEFAULT FALSE, Remark TEXT, Dam VARCHAR(100), Sire VARCHAR(100), diff --git a/nbri_ehr/resources/schemas/nbri_ehr.xml b/nbri_ehr/resources/schemas/nbri_ehr.xml index 6d076c8..2bc125d 100644 --- a/nbri_ehr/resources/schemas/nbri_ehr.xml +++ b/nbri_ehr/resources/schemas/nbri_ehr.xml @@ -581,6 +581,7 @@ Date + diff --git a/nbri_ehr/resources/scripts/nbri_triggers.js b/nbri_ehr/resources/scripts/nbri_triggers.js index e4ad95b..190f656 100644 --- a/nbri_ehr/resources/scripts/nbri_triggers.js +++ b/nbri_ehr/resources/scripts/nbri_triggers.js @@ -40,9 +40,15 @@ exports.init = function (EHR) { }); }); + // the arrival and birth forms assign animals that do not have a demographics record yet, so those forms ask for Id validation to be relaxed + function isAllowAnyIdRequested(helper) { + helper.decodeExtraContextProperty('allowAnyId', false); + return helper.getProperty('allowAnyId') === true; // this can be true or an empty object + } + EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'protocolAssignment', function(event, helper) { helper.setScriptOptions({ - allowAnyId: false, + allowAnyId: isAllowAnyIdRequested(helper), requiresStatusRecalc: false, allowDatesInDistantPast: true }); @@ -50,7 +56,7 @@ exports.init = function (EHR) { EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'assignment', function(event, helper) { helper.setScriptOptions({ - allowAnyId: false, + allowAnyId: isAllowAnyIdRequested(helper), requiresStatusRecalc: false, allowDatesInDistantPast: true, skipAssignmentCheck: true, 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 3e2850b..a5e983f 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js @@ -25,7 +25,7 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { byQuery: { 'study.arrival': { 'cage': { - allowBlank: false, + // allowBlank: false, columnConfig: { fixed: true, width: 200 @@ -39,45 +39,28 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { allowBlank: false }, 'Id/demographics/birth': { - allowBlank: false + // allowBlank: false }, 'Id/demographics/gender': { allowBlank: false }, 'Id/demographics/geographic_origin': { - allowBlank: false, + // allowBlank: false, columnConfig: { fixed: true, width: 200 } }, + // project and protocol are entered through the Project Assignment and Protocol Assignment sections project: { - xtype: 'combo', - columnConfig: { - width: 150 - }, - lookup: { - schemaName: 'ehr', - queryName: 'project', - keyColumn: 'project', - columns: 'project,name', - filterArray: [ - LABKEY.Filter.create('isActive', true, LABKEY.Filter.Types.EQUAL), - ] - }, - allowBlank: false + allowBlank: true, + hidden: true, + showInGrid: false }, arrivalProtocol: { - allowBlank: false, - columnConfig: { - width: 200 - }, - lookup: { - schemaName: 'ehr', - queryName: 'activeProtocols', - keyColumn: 'protocol', - columns: 'protocol,title' - }, + allowBlank: true, + hidden: true, + showInGrid: false }, performedby: { hidden: true, @@ -91,14 +74,14 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { }, }, acquisitionType: { - allowBlank: false, + // allowBlank: false, columnConfig: { fixed: true, width: 150 }, }, arrivalType: { - allowBlank: false, + // allowBlank: false, columnConfig: { width: 200 } 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 4db08ad..4d38d3c 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js @@ -37,12 +37,14 @@ EHR.model.DataModelManager.registerMetadata('Assignment', { fixed: true, width: 150 }, + // set displayColumn: ehr.protocol's title column (displayName) is not returned by this query lookup: { schemaName: 'ehr', queryName: 'activeProtocols', keyColumn: 'protocol', + displayColumn: 'protocol', columns: 'protocol,title' - }, + } } } } 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 7202b23..f266f1e 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js @@ -24,58 +24,68 @@ EHR.model.DataModelManager.registerMetadata('Birth', { }, byQuery: { 'study.birth': { + Id: { + allowBlank: false, + nullable: false + }, + date: { + allowBlank: false, + nullable: false + }, 'Id/demographics/species': { allowBlank: false, + nullable: false, columnConfig: { fixed: true, width: 250 } }, 'cage': { - allowBlank: false, + // allowBlank: false, columnConfig: { fixed: true, width: 200 }, }, - project: { - xtype: 'combo', - allowBlank: false, + type: { columnConfig: { - width: 150 + width: 200 }, - lookup: { - schemaName: 'ehr', - queryName: 'project', - keyColumn: 'project', - columns: 'project,name', - filterArray: [ - LABKEY.Filter.create('isActive', true, LABKEY.Filter.Types.EQUAL), - ] - } }, - birthProtocol: { + cond: { columnConfig: { width: 200 }, - allowBlank: false, - lookup: { - schemaName: 'ehr', - queryName: 'activeProtocols', - keyColumn: 'protocol', - columns: 'protocol,title' - }, + }, + // project and protocol are entered through the Project Assignment and Protocol Assignment sections + project: { + allowBlank: true, + hidden: true, + showInGrid: false + }, + birthProtocol: { + allowBlank: true, + hidden: true, + showInGrid: false }, 'Id/demographics/birth': { allowBlank: false }, 'Id/demographics/gender': { - allowBlank: false + allowBlank: false, + nullable: false }, conceptId: { + allowBlank: false, + nullable: false, columnConfig: { width: 150 } + }, + breedingType: { + 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 d862181..949d8cb 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Conception.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Conception.js @@ -35,6 +35,13 @@ EHR.model.DataModelManager.registerMetadata('Conception', { width: 200 }, }, + Estimated: { + xtype: 'checkbox', + defaultValue: false, + columnConfig: { + width: 100 + }, + }, Dam: { xtype: 'ehr-animalfield', lookups: false, diff --git a/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js b/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js new file mode 100644 index 0000000..ebdf711 --- /dev/null +++ b/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * Adds a birth record pre-populated from an existing conception record. + * + * @cfg {Object} targetStore + * @cfg {Object} formConfig + */ +Ext4.define('NBRI_EHR.window.StartWithConceptionWindow', { + extend: 'Ext.window.Window', + + initComponent: function(){ + Ext4.apply(this, { + title: 'Start with Conception', + modal: true, + closeAction: 'destroy', + border: true, + bodyStyle: 'padding: 5px', + width: 400, + defaults: { + border: false, + width: 370 + }, + items: [{ + html: 'Select a conception record. A new birth record will be added using the conception Id, along with the dam, sire and species from that conception.', + style: 'padding-bottom: 10px;' + },{ + xtype: 'labkey-combo', + itemId: 'conceptionField', + fieldLabel: 'Conception Id', + displayField: 'ConceptId', + valueField: 'ConceptId', + forceSelection: true, + queryMode: 'local', + anyMatch: true, + caseSensitive: false, + store: { + type: 'labkey-store', + schemaName: 'nbri_ehr', + queryName: 'Conception', + columns: 'ConceptId,ConceptDate,Dam,Sire', + sort: '-ConceptDate', + autoLoad: true + } + }], + buttons: [{ + text: 'Submit', + scope: this, + handler: this.onSubmit + },{ + text: 'Close', + handler: function(btn){ + btn.up('window').close(); + } + }] + }); + + this.callParent(arguments); + }, + + onSubmit: function(btn){ + var field = this.down('#conceptionField'); + var conceptId = field.getValue(); + if (!conceptId){ + Ext4.Msg.alert('Error', 'Must select a conception Id'); + return; + } + + var record = field.findRecordByValue(conceptId); + if (!record){ + Ext4.Msg.alert('Error', 'Unable to find the conception record for: ' + conceptId); + return; + } + + var dam = record.get('Dam'); + var sire = record.get('Sire'); + + btn.disable(); + this.getSpecies(dam, function(species, speciesError){ + this.addRow(conceptId, dam, sire, species); + btn.enable(); + this.close(); + + // the row is still added so the conception values are not lost, but a blank species would otherwise + // surface only as a bare "Species is required" error with no hint that the copy from the dam failed + if (speciesError){ + Ext4.Msg.alert('Species Not Copied', speciesError + ' Enter the species on the new birth record manually.'); + } + }, this); + }, + + // the species of the offspring is inferred from the dam of the conception. When it cannot be determined the + // callback receives a message explaining why, rather than a null that is indistinguishable from an unset field. + getSpecies: function(dam, callback, scope){ + if (!dam){ + callback.call(scope, null, 'The conception record has no dam, so the species could not be determined.'); + return; + } + + LABKEY.Query.selectRows({ + schemaName: 'study', + queryName: 'demographics', + columns: 'Id,species', + filterArray: [LABKEY.Filter.create('Id', dam, LABKEY.Filter.Types.EQUAL)], + scope: this, + success: function(results){ + var rows = (results && results.rows) || []; + if (!rows.length){ + callback.call(scope, null, 'No demographics record was found for dam ' + dam + '.'); + return; + } + + if (!rows[0].species){ + callback.call(scope, null, 'No species is recorded on the demographics record for dam ' + dam + '.'); + return; + } + + callback.call(scope, rows[0].species); + }, + failure: function(error){ + console.error(error); + callback.call(scope, null, 'Unable to look up the species of dam ' + dam + ': ' + ((error && error.exception) || 'the query failed') + '.'); + } + }); + }, + + addRow: function(conceptId, dam, sire, species){ + this.targetStore.add(this.targetStore.createModel({ + conceptId: conceptId, + 'Id/demographics/dam': dam, + 'Id/demographics/sire': sire, + 'Id/demographics/species': species + })); + } +}); + +EHR.DataEntryUtils.registerGridButton('NBRI_START_WITH_CONCEPTION', function(config){ + return Ext4.Object.merge({ + text: 'Start with Conception', + tooltip: EHR.DataEntryUtils.shouldShowTooltips() ? 'Click to add a birth record populated from an existing conception record' : undefined, + handler: function(btn){ + var grid = btn.up('gridpanel'); + if (!grid.store || !grid.store.hasLoaded()){ + console.log('no store or store hasnt loaded'); + return; + } + + // commit any in-progress cell edit first; the modal window blocks the grid, so an open editor would + // otherwise be abandoned and its pending value lost + var cellEditing = grid.getPlugin(grid.editingPluginId); + if (cellEditing){ + cellEditing.completeEdit(); + } + + Ext4.create('NBRI_EHR.window.StartWithConceptionWindow', { + targetStore: grid.store, + formConfig: grid.formConfig + }).show(); + } + }, config); +}); 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 b79b90b..61fe238 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,8 @@ 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.NBRIProjectAssignmentFormSection; +import org.labkey.nbri_ehr.dataentry.section.NBRIProtocolAssignmentFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRITaskFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIWeightFormSection; @@ -40,13 +42,17 @@ public NBRIArrivalFormType(DataEntryFormContext ctx, Module owner) new NBRITaskFormSection(), new NBRIAnimalDetailsFormSection(), new NBRIArrivalFormSection(), + new NBRIProtocolAssignmentFormSection(true, true, true), + new NBRIProjectAssignmentFormSection(true, true, true), new NBRIWeightFormSection(true, true) )); + addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Assignment.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Arrival.js")); for (FormSection s : getFormSections()) { + s.addConfigSource("Assignment"); 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 ebe9c2b..cbd66ea 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,8 @@ 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.NBRIProjectAssignmentFormSection; +import org.labkey.nbri_ehr.dataentry.section.NBRIProtocolAssignmentFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRITaskFormSection; import java.util.ArrayList; @@ -39,16 +41,20 @@ public NBRIBirthFormType (DataEntryFormContext ctx, Module owner) new NBRIBirthInstructionsFormSection(), new NBRITaskFormSection(), new NBRIAnimalDetailsFormSection(), - new NBRIBirthFormSection() + new NBRIBirthFormSection(), + new NBRIProtocolAssignmentFormSection(true, true, true), + new NBRIProjectAssignmentFormSection(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/Birth.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/window/AddAnimalsWindow.js")); for (FormSection s : getFormSections()) { + s.addConfigSource("Assignment"); s.addConfigSource("Birth"); } } 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 604cbb9..ddecd53 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 @@ -22,15 +22,34 @@ import org.labkey.api.query.FieldKey; import org.labkey.api.view.template.ClientDependency; +import java.util.ArrayList; import java.util.List; public class NBRIBirthFormSection extends NewAnimalFormSection { + // left to right column order of the Births grid; the demographics fields are not on study.birth, so they are added here + private static final List COLUMN_ORDER = List.of( + FieldKey.fromString("Id"), + FieldKey.fromString("date"), + FieldKey.fromString("conceptId"), + FieldKey.fromString("Id/demographics/species"), + FieldKey.fromString("Id/demographics/gender"), + FieldKey.fromString("Id/demographics/dam"), + FieldKey.fromString("Id/demographics/sire"), + FieldKey.fromString("cage"), + FieldKey.fromString("type"), + FieldKey.fromString("cond"), + FieldKey.fromString("breedingType"), + FieldKey.fromString("remark"), + FieldKey.fromString("performedby") + ); + public NBRIBirthFormSection() { super("study", "birth", "Births", false); addClientDependency(ClientDependency.supplierFromPath("ehr/window/FormBulkAddWindow.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/window/FormBulkAddWindow.js")); + addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/window/StartWithConceptionWindow.js")); } @Override @@ -46,14 +65,12 @@ public JSONObject toJSON(DataEntryFormContext ctx, boolean includeFormElements) @Override protected List getFieldKeys(TableInfo ti) { - List keys = super.getFieldKeys(ti); + List ordered = new ArrayList<>(COLUMN_ORDER); - keys.add(2, FieldKey.fromString("Id/demographics/species")); - keys.add(3, FieldKey.fromString("Id/demographics/gender")); - keys.add(4, FieldKey.fromString("Id/demographics/dam")); - keys.add(5, FieldKey.fromString("Id/demographics/sire")); + // anything not explicitly ordered above (hidden and system fields) keeps its default position at the end + super.getFieldKeys(ti).stream().filter(key -> !COLUMN_ORDER.contains(key)).forEach(ordered::add); - return keys; + return ordered; } @Override @@ -68,6 +85,7 @@ public List getTbarButtons() defaultButtons.add(idx, "NBRI_ADDANIMALS"); } defaultButtons.remove("COPYFROMSECTION"); + defaultButtons.addFirst("NBRI_START_WITH_CONCEPTION"); return defaultButtons; } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java index 38aa173..38fd24d 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java @@ -751,7 +751,7 @@ public void doSharedCustomization(AbstractTableInfo ti) { UserSchema us = getEHRUserSchema(ti, "ehr_lookups"); col.setLabel("Species"); - col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "species_codes", "code", "scientific_name")); + col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "species_codes", "code", "common_name")); } if ("protocol".equalsIgnoreCase(col.getName()) && null == col.getFk() && !"protocol".equalsIgnoreCase(ti.getName())) { 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 c7587ae..a06c389 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 @@ -363,13 +363,13 @@ private void addNBRIEhrLinks() @Override protected String getMale() { - return "3"; + return "M"; } @Override protected String getFemale() { - return "2"; + return "F"; } @Test @@ -576,13 +576,24 @@ public void testArrivalForm() arrivals.setGridCell(1, "acquisitionType", "Lab Transfer (Wild Born)"); arrivals.setGridCell(1, "Id", arrivedAnimal); arrivals.setGridCell(1, "cage", "C1"); - arrivals.setGridCell(1, "project", "640991"); - arrivals.setGridCell(1, "arrivalProtocol", "dummyprotocol"); - arrivals.setGridCell(1, "Id/demographics/gender", "female"); + arrivals.setGridCell(1, "Id/demographics/gender", "Female"); arrivals.setGridCell(1, "Id/demographics/geographic_origin", "BRAZIL"); - arrivals.setGridCell(1, "Id/demographics/species", "Macaca nemestrina PIG"); + 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, Inc."); + arrivals.setGridCell(1, "sourceFacility", "Bioqual, Incorporated"); + + Ext4GridRef protocolAssignments = _helper.getExt4GridForFormSection("Protocol Assignment"); + _helper.addRecordToGrid(protocolAssignments); + protocolAssignments.setGridCell(1, "Id", arrivedAnimal); + protocolAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + protocolAssignments.setGridCell(1, "protocol", "dummyprotocol"); + + Ext4GridRef projectAssignments = _helper.getExt4GridForFormSection("Project Assignment"); + _helper.addRecordToGrid(projectAssignments); + projectAssignments.setGridCell(1, "Id", arrivedAnimal); + projectAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + projectAssignments.setGridCell(1, "project", "640991"); + submitForm("Submit Final", "Finalize"); goToSchemaBrowser(); @@ -590,13 +601,19 @@ public void testArrivalForm() table.setFilter("Id", "Equals", arrivedAnimal); CustomizeView view = table.openCustomizeGrid(); view.addColumn("cage"); - view.addColumn("project"); - view.addColumn("arrivalProtocol"); view.applyCustomView(); Assert.assertEquals("Invalid Arrival record", Arrays.asList(arrivedAnimal), table.getRowDataAsText(0, "Id")); Assert.assertEquals("Invalid Arrival record", Arrays.asList("C1"), table.getRowDataAsText(0, "cage")); - Assert.assertEquals("Invalid Arrival record", Arrays.asList("640991"), table.getRowDataAsText(0, "project")); - Assert.assertEquals("Invalid Arrival record", Arrays.asList("dummyprotocol"), table.getRowDataAsText(0, "arrivalProtocol")); + + goToSchemaBrowser(); + table = viewQueryData("study", "assignment"); + table.setFilter("Id", "Equals", arrivedAnimal); + Assert.assertEquals("Invalid project assignment", Arrays.asList("640991"), table.getRowDataAsText(0, "project")); + + goToSchemaBrowser(); + table = viewQueryData("study", "protocolAssignment"); + table.setFilter("Id", "Equals", arrivedAnimal); + Assert.assertEquals("Invalid protocol assignment", Arrays.asList("dummyprotocol"), table.getRowDataAsText(0, "protocol")); verifyRowCreated("study", "birth", arrivedAnimal, 1); verifyRowCreated("study", "assignment", arrivedAnimal, 1); @@ -606,15 +623,24 @@ public void testArrivalForm() } @Test - public void testBirthForm() throws IOException, CommandException + public void testBirthForm() throws Exception { String bornAnimal = "80801"; + String damId = "TESTDAM01"; + String sireId = "TESTSIRE01"; + // demographics.species holds an ehr_lookups.species_codes code; the grids display its common name + String damSpeciesCode = "CAP"; + String damSpecies = "Brown-Tufted Capuchin"; String conceptId = "TESTCONCEPT1"; + String breedingType = "Time-Mated"; LocalDateTime now = LocalDateTime.now(); + log("Creating the dam and sire of the conception"); + createBreedingPair(damId, sireId, damSpeciesCode); + log("Creating conception record"); InsertRowsCommand conception = new InsertRowsCommand("nbri_ehr", "Conception"); - conception.addRow(Map.of("ConceptId", conceptId, "ConceptDate", now.minusDays(160), "Dam", "TEST4551032")); + conception.addRow(Map.of("ConceptId", conceptId, "ConceptDate", now.minusDays(160), "Dam", damId, "Sire", sireId)); conception.execute(getApiHelper().getConnection(), getContainerPath()); gotoEnterData(); @@ -622,15 +648,47 @@ public void testBirthForm() throws IOException, CommandException lockForm(); Ext4GridRef births = _helper.getExt4GridForFormSection("Births"); - _helper.addRecordToGrid(births); + verifyBirthColumnOrder(births); + + log("Starting a birth record from the conception"); + births.clickTbarButton("Start with Conception"); + Window conceptionWindow = new Window.WindowFinder(getDriver()).withTitle("Start with Conception").waitFor(); + Ext4ComboRef conceptionCombo = _ext4Helper.queryOne("window #conceptionField", Ext4ComboRef.class); + Assert.assertNotNull("Conception Id field not found in the Start with Conception window", conceptionCombo); + conceptionCombo.waitForStoreLoad(); + conceptionCombo.setComboByDisplayValue(conceptId); + conceptionWindow.clickButton("Submit", 0); + births.waitForRowCount(1); + + log("Verifying the conception populated the new birth record"); + assertEquals("Conception Id was not copied from the conception", conceptId, births.getFieldValue(1, "conceptId")); + assertEquals("Dam was not copied from the conception", damId, births.getFieldValue(1, "Id/demographics/dam")); + assertEquals("Sire was not copied from the conception", sireId, births.getFieldValue(1, "Id/demographics/sire")); + assertEquals("Species was not copied from the dam of the conception", damSpeciesCode, births.getFieldValue(1, "Id/demographics/species")); + + log("Verifying Conception Id is required"); + births.setGridCellJS(1, "conceptId", null); + waitForFormError("The field: Conception Id is required"); + births.setGridCellJS(1, "conceptId", conceptId); + births.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); births.setGridCell(1, "Id", bornAnimal); births.setGridCell(1, "cage", "C3"); - births.setGridCell(1, "Id/demographics/species", "Cebus apella CAP"); - births.setGridCell(1, "Id/demographics/gender", "female"); - births.setGridCell(1, "project", "795644"); - births.setGridCell(1, "birthProtocol", "protocol101"); - births.setGridCell(1, "conceptId", conceptId); + births.setGridCell(1, "Id/demographics/gender", "Female"); + births.setGridCell(1, "breedingType", breedingType); + + Ext4GridRef protocolAssignments = _helper.getExt4GridForFormSection("Protocol Assignment"); + _helper.addRecordToGrid(protocolAssignments); + protocolAssignments.setGridCell(1, "Id", bornAnimal); + protocolAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + protocolAssignments.setGridCell(1, "protocol", "protocol101"); + + Ext4GridRef projectAssignments = _helper.getExt4GridForFormSection("Project Assignment"); + _helper.addRecordToGrid(projectAssignments); + projectAssignments.setGridCell(1, "Id", bornAnimal); + projectAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + projectAssignments.setGridCell(1, "project", "795644"); + submitForm("Submit Final", "Finalize"); goToSchemaBrowser(); @@ -638,20 +696,39 @@ public void testBirthForm() throws IOException, CommandException table.setFilter("Id", "Equals", bornAnimal); Assert.assertEquals("Invalid Birth record", Arrays.asList(bornAnimal), table.getRowDataAsText(0, "Id")); Assert.assertEquals("Invalid Birth record", Arrays.asList("C3"), table.getRowDataAsText(0, "cage")); - Assert.assertEquals("Invalid Birth record", Arrays.asList("795644"), table.getRowDataAsText(0, "project")); - Assert.assertEquals("Invalid Birth record", Arrays.asList("protocol101"), table.getRowDataAsText(0, "birthProtocol")); Assert.assertEquals("Invalid Birth record", Arrays.asList(conceptId), table.getRowDataAsText(0, "conceptId")); + Assert.assertEquals("Invalid Birth record", Arrays.asList(breedingType), table.getRowDataAsText(0, "breedingType")); + + log("Verifying the dam and sire of the conception reached demographics"); + goToSchemaBrowser(); + table = viewQueryData("study", "demographics"); + table.setFilter("Id", "Equals", bornAnimal); + 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")); + + goToSchemaBrowser(); + table = viewQueryData("study", "assignment"); + table.setFilter("Id", "Equals", bornAnimal); + Assert.assertEquals("Invalid project assignment", Arrays.asList("795644"), table.getRowDataAsText(0, "project")); + + goToSchemaBrowser(); + table = viewQueryData("study", "protocolAssignment"); + table.setFilter("Id", "Equals", bornAnimal); + Assert.assertEquals("Invalid protocol assignment", Arrays.asList("protocol101"), table.getRowDataAsText(0, "protocol")); verifyRowCreated("study", "assignment", bornAnimal, 1); verifyRowCreated("study", "protocolAssignment", bornAnimal, 1); verifyRowCreated("study", "housing", bornAnimal, 1); verifyRowCreated("study", "demographics", bornAnimal, 1); - log("Verifying conception outcome in ConceptionsByDam"); + log("Verifying conception outcome and offspring in ConceptionsByDam"); goToSchemaBrowser(); DataRegionTable report = viewQueryData("nbri_ehr", "ConceptionsByDam"); report.setFilter("ConceptId", "Equals", conceptId); + Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(damId), report.getRowDataAsText(0, "Id")); Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList("Live Birth"), report.getRowDataAsText(0, "conceptionOutcome")); + Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(bornAnimal), report.getRowDataAsText(0, "offspring")); } @Test @@ -659,6 +736,8 @@ public void testPregnancyForm() throws IOException, CommandException { String animalId = "TEST4551032"; String conceptId = "TESTCONCEPT2"; + // a non-live outcome, so ConceptionsByDam reports it rather than falling through to 'Live Birth' + String result = "Fetal Death"; LocalDateTime now = LocalDateTime.now(); log("Creating conception record"); @@ -674,7 +753,7 @@ public void testPregnancyForm() throws IOException, CommandException _helper.addRecordToGrid(outcomes); outcomes.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); outcomes.setGridCell(1, "Id", animalId); - outcomes.setGridCell(1, "result", "Stillborn"); + outcomes.setGridCell(1, "result", result); outcomes.setGridCell(1, "conceptId", conceptId); submitForm("Submit Final", "Finalize"); @@ -682,7 +761,7 @@ public void testPregnancyForm() throws IOException, CommandException DataRegionTable table = viewQueryData("study", "pregnancy"); table.setFilter("Id", "Equals", animalId); Assert.assertEquals("Invalid Pregnancy Outcome record", Arrays.asList(animalId), table.getRowDataAsText(0, "Id")); - Assert.assertEquals("Invalid Pregnancy Outcome record", Arrays.asList("Stillborn"), table.getRowDataAsText(0, "result")); + Assert.assertEquals("Invalid Pregnancy Outcome record", Arrays.asList(result), table.getRowDataAsText(0, "result")); Assert.assertEquals("Invalid Pregnancy Outcome record", Arrays.asList(conceptId), table.getRowDataAsText(0, "conceptId")); log("Verifying conception outcome in ConceptionsByDam"); @@ -690,7 +769,7 @@ public void testPregnancyForm() throws IOException, CommandException DataRegionTable report = viewQueryData("nbri_ehr", "ConceptionsByDam"); report.setFilter("ConceptId", "Equals", conceptId); Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(animalId), report.getRowDataAsText(0, "Id")); - Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList("Stillborn"), report.getRowDataAsText(0, "conceptionOutcome")); + Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(result), report.getRowDataAsText(0, "conceptionOutcome")); } @Test @@ -706,13 +785,20 @@ public void testConceptionForm() lockForm(); Ext4GridRef conceptions = _helper.getExt4GridForFormSection("Conception"); + Assert.assertFalse("Breeding Type describes the birth and should no longer appear on the Conception form", + conceptions.isColumnPresent("breedingType", false)); + _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); - conceptions.setGridCell(1, "Remark", "Conception entry test"); + // Remark renders as a textarea, which Ext4GridRef's cell editor helpers cannot drive: they only recognize + // an as the active editor, so the click that opens the textarea is followed by a retry click that + // the open textarea intercepts. Set it through the store instead. + conceptions.setGridCellJS(1, "Remark", "Conception entry test"); submitForm("Submit Final", "Finalize"); goToSchemaBrowser(); @@ -720,6 +806,7 @@ public void testConceptionForm() table.setFilter("ConceptId", "Equals", conceptId); Assert.assertEquals("Invalid Conception record", Arrays.asList(damId), table.getRowDataAsText(0, "Dam")); Assert.assertEquals("Invalid Conception record", Arrays.asList(sireId), table.getRowDataAsText(0, "Sire")); + Assert.assertEquals("Invalid Conception record", Arrays.asList("true"), table.getRowDataAsText(0, "Estimated")); Assert.assertEquals("Invalid Conception record", Arrays.asList("Conception entry test"), table.getRowDataAsText(0, "Remark")); log("Verifying unmatched conception appears as Unknown in ConceptionsByDam"); @@ -1156,7 +1243,8 @@ public void createSubjectsForDeathForm() throws IOException, CommandException log("Marking an animal departed"); InsertRowsCommand departure = new InsertRowsCommand("study", "departure"); - departure.addRow(Map.of("Id", departedAnimalId, "date", LocalDateTime.now().minusDays(1), "destination", "Oregon NPRC", "performedby", 1004)); + // destination stores an ehr_lookups.source code; the facility name is only the display value + departure.addRow(Map.of("Id", departedAnimalId, "date", LocalDateTime.now().minusDays(1), "destination", "ORPRC", "performedby", 1004)); departure.execute(getApiHelper().getConnection(), getContainerPath()); } @@ -1615,6 +1703,44 @@ private int countLines(File file) throws Exception } } + // Creates the parents of a conception. They need a species from the ehr_lookups.species list because the + // Start with Conception window copies the dam's species onto the newborn, and the reference study's + // demographics data carries placeholder species values that no lookup entry matches. + private void createBreedingPair(String damId, String sireId, String species) throws Exception + { + String[] fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; + Object[][] data = new Object[][]{ + {damId, species, (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {sireId, species, (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} + }; + SimplePostCommand insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", damId + ";" + sireId, Filter.Operator.IN)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + } + + // Asserts the Births columns appear in the expected left to right order. Relative position is checked rather + // than absolute index so that hidden and system columns can come and go without breaking the test. + 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"); + + int previousIdx = 0; + String previousCol = null; + for (String col : expectedOrder) + { + int idx = births.getIndexOfColumn(col, true); + Assert.assertTrue("Births column '" + col + "' should appear to the right of '" + previousCol + "'", idx > previousIdx); + previousIdx = idx; + previousCol = col; + } + } + + private void waitForFormError(String message) + { + waitFor(() -> isTextPresent(message), "Form did not report: " + message, WAIT_FOR_JAVASCRIPT); + } + private void verifyRowCreated(String schema, String query, String animalId, int rowCount) { goToSchemaBrowser(); From a43f6465ae47976e96913fe9fe036f48233f0e8a Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 2 Aug 2026 20:34:42 -0700 Subject: [PATCH 4/7] Add Bulk Deaths form and death type lookup; harden the deaths trigger (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Rationale Historical deaths have to be loaded for the colony, and the existing Death/Necropsy form takes one animal at a time and requires necropsy data alongside the death. This adds an admin-only grid form that records deaths only, and hardens `study/deaths.js` for the multi-row saves that form produces: the duplicate checks dereferenced values that are null for an animal with no prior death record, and a second row for one animal reached the unique constraint on this demographic dataset and surfaced as a database error rather than a validation message. ## Changes - `NBRIBulkDeathFormType`, an admin-only grid form for entering completed historical deaths in bulk, with `Death.js` supplying its grid metadata. - A `type` (Death Type) column on `study.deaths`, backed by a new `ehr_lookups.death_type` lookup of the 13 codes the source data uses. `A` and `X` share the title `Experimental`, so only the code round-trips reliably on import. `DeathNecropsy.js` now requires `type` rather than `reason`, and `deathWeight` becomes optional — not every historical death carries a weight. - `study/deaths.js`: null-safe duplicate checks; a second row for an animal is reported as a validation error, both against the pre-save snapshot and against a helper property tracking animals saved earlier in the same transaction; an Id absent from demographics is now rejected on insert; the weight upsert moved into the success branch and gated on `!isValidateOnly`; removed the dead `validIds` check. - `NBRI_EHRTriggerHelper.upsertWeightRecord` returns whether a row was written and gains an `announceChanges` overload, so a caller writing many weight rows in one transaction suppresses the per-row participant announcement and announces once via `addTableModified`. With no weight entered it deletes any record left by an earlier save rather than blanking it — the weight trigger only WARNs on a null weight and the default threshold filters that, so an emptied record would otherwise survive the save. - The existing-weight lookup is skipped when `taskid` is null, making task-less entry insert-only. A null `taskid` filter flips to `taskid IS NULL` and would match unrelated historical weights for the animal. - `study.departure.destination` resolves through the lookup's `code` column instead of `meaning`, matching how the values are stored. - Removed a stray apostrophe that left `study.aliases` unparseable. - `NBRI_EHRTest` covers the unknown-Id and existing-record rejections in the Death form and asserts through the API that a second death insert returns a validation error rather than a constraint violation. --- nbri_ehr/resources/data/death_type.tsv | 14 +++ nbri_ehr/resources/data/editable_lookups.tsv | 5 +- nbri_ehr/resources/data/lookup_sets.tsv | 1 + nbri_ehr/resources/data/lookupsManifest.tsv | 1 + .../resources/data/lookupsManifestTest.tsv | 1 + nbri_ehr/resources/queries/study/aliases.sql | 3 +- nbri_ehr/resources/queries/study/deaths.js | 91 ++++++++++++------- .../resources/queries/study/deaths.query.xml | 9 ++ .../resources/queries/study/deaths/.qview.xml | 1 + .../study/datasets/datasets_metadata.xml | 3 + .../web/nbri_ehr/model/sources/Death.js | 54 +++++++++++ .../nbri_ehr/model/sources/DeathNecropsy.js | 8 +- .../org/labkey/nbri_ehr/NBRI_EHRModule.java | 1 + .../dataentry/form/NBRIBulkDeathFormType.java | 66 ++++++++++++++ .../nbri_ehr/query/NBRI_EHRTriggerHelper.java | 76 +++++++++++----- .../tests.nbri_ehr/NBRI_EHRTest.java | 18 ++++ 16 files changed, 287 insertions(+), 65 deletions(-) create mode 100644 nbri_ehr/resources/data/death_type.tsv create mode 100644 nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js create mode 100644 nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkDeathFormType.java diff --git a/nbri_ehr/resources/data/death_type.tsv b/nbri_ehr/resources/data/death_type.tsv new file mode 100644 index 0000000..700a88d --- /dev/null +++ b/nbri_ehr/resources/data/death_type.tsv @@ -0,0 +1,14 @@ +value title sort_order +A Experimental 1 +D Spontaneous/Normal 2 +F Fetal 3 +FD Fetal Death 4 +FL Fetal Live 5 +FN Fetal found at necropsy 6 +FX Fetal experimental 7 +K Cull (scheduled) 8 +M Medical cull (non-scheduled) 9 +ND Non-vaginal (C-section) dead 10 +NT Not pregnant at assessment 11 +S Cull 12 +X Experimental 13 \ No newline at end of file diff --git a/nbri_ehr/resources/data/editable_lookups.tsv b/nbri_ehr/resources/data/editable_lookups.tsv index 4306a94..d0ed33a 100644 --- a/nbri_ehr/resources/data/editable_lookups.tsv +++ b/nbri_ehr/resources/data/editable_lookups.tsv @@ -38,9 +38,10 @@ ehr_lookups country Colony Management Country ehr_lookups country_category Colony Management Country Category ehr_lookups daily_enrich_codes Behavior Daily enrichment codes. ehr_lookups data_category Clinical Data Categories Used in datasets. -ehr_lookups death_reason Colony Management Death Reason +ehr_lookups death_reason Colony Management Death Reason +ehr_lookups death_type Colony Management Death Type Death type codes. ehr_lookups delivery_mode Colony Management Delivery Mode -ehr_lookups delivery_state Colony Management Delivery State +ehr_lookups delivery_state Colony Management Delivery State ehr_lookups dental_obs Clinical Dental Observation Types Clinical observation values. ehr_lookups derm_obs Clinical Dermatologic Observation Types Clinical observation values. ehr_lookups digit_amputation Clinical Digit Amputation Clinical observation fixed values. diff --git a/nbri_ehr/resources/data/lookup_sets.tsv b/nbri_ehr/resources/data/lookup_sets.tsv index 5243772..c465f4a 100644 --- a/nbri_ehr/resources/data/lookup_sets.tsv +++ b/nbri_ehr/resources/data/lookup_sets.tsv @@ -33,6 +33,7 @@ country_category Country Category value title daily_enrich_codes Daily Enrichment Codes value data_category Data Category Field Values value death_reason Death Reason value +death_type Death Type value title delivery_mode Delivery Mode value title delivery_state Delivery State value title dental_obs Dental Observations value diff --git a/nbri_ehr/resources/data/lookupsManifest.tsv b/nbri_ehr/resources/data/lookupsManifest.tsv index 472d51d..e7a5d49 100644 --- a/nbri_ehr/resources/data/lookupsManifest.tsv +++ b/nbri_ehr/resources/data/lookupsManifest.tsv @@ -37,6 +37,7 @@ country_category daily_enrich_codes data_category death_reason +death_type delivery_mode delivery_state dental_obs diff --git a/nbri_ehr/resources/data/lookupsManifestTest.tsv b/nbri_ehr/resources/data/lookupsManifestTest.tsv index b8946ef..3e334b8 100644 --- a/nbri_ehr/resources/data/lookupsManifestTest.tsv +++ b/nbri_ehr/resources/data/lookupsManifestTest.tsv @@ -36,6 +36,7 @@ country_category daily_enrich_codes data_category death_reason +death_type delivery_mode delivery_state dental_obs diff --git a/nbri_ehr/resources/queries/study/aliases.sql b/nbri_ehr/resources/queries/study/aliases.sql index a1b279f..4ed9962 100644 --- a/nbri_ehr/resources/queries/study/aliases.sql +++ b/nbri_ehr/resources/queries/study/aliases.sql @@ -15,5 +15,4 @@ 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 +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/deaths.js b/nbri_ehr/resources/queries/study/deaths.js index 79883b4..abe7bc4 100644 --- a/nbri_ehr/resources/queries/study/deaths.js +++ b/nbri_ehr/resources/queries/study/deaths.js @@ -6,7 +6,6 @@ require("ehr/triggers").initScript(this); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); -var validIds = []; var idMap = {}; var deathIdMap = {}; @@ -26,9 +25,7 @@ function onInit(event, helper){ return; for(var i=0; i < results.rows.length; i++) { - validIds.push(results.rows[i]["Id"]["value"]) idMap[results.rows[i]["Id"]["value"]] = {calculated_status: results.rows[i]["calculated_status"]["value"], QCStateLabel: results.rows[i]["QCState/Label"]["value"]}; - // console.log(idMap[results.rows[i]["Id"]["value"]]); } }, failure: function (error) { @@ -84,13 +81,27 @@ function onUpsert(helper, scriptErrors, row, oldRow) { //only allow death record to be created if the animal is in the demographics table if (idMap[row.Id]) { + // deathIdMap has no entry for the animal on initial import, and any of these values can be null + 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; + + // 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. + var deathsInTransaction = helper.getProperty('deathsInTransaction') || {}; + + var errorMsg = null; + // check if a death record already exists for this animal - if (idMap[row.Id].calculated_status.toUpperCase() === 'DEAD' && deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'COMPLETED') { - EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death record already exists for this animal.', 'ERROR'); + if (status === 'DEAD' && priorDeathQCState === 'COMPLETED') { + errorMsg = 'Death record already exists for this animal.'; } // check if the animal is at the center - else if (idMap[row.Id].calculated_status.toUpperCase() === 'SHIPPED') { - EHR.Server.Utils.addError(scriptErrors, 'Id', 'Animal is not at the center.', 'ERROR'); + else if (status === 'SHIPPED') { + errorMsg = 'Animal is not at the center.'; + } + 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'. @@ -98,23 +109,28 @@ function onUpsert(helper, scriptErrors, row, oldRow) { // 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. - else if (row.QCStateLabel.toUpperCase() === 'IN PROGRESS' && - deathIdMap[row.Id] && deathIdMap[row.Id].QCStateLabel && - (deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'REQUEST: PENDING' || - deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'REVIEW REQUIRED')) { - EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death record is pending review for this animal', 'ERROR'); + 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. - else if (oldRow === undefined && row.QCStateLabel.toUpperCase() === 'IN PROGRESS' && - deathIdMap[row.Id] && deathIdMap[row.Id].QCStateLabel && - deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'IN PROGRESS') { - EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death/Necropsy data entry is in progress for this animal', 'ERROR'); + 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. + else if (oldRow === undefined && deathIdMap[row.Id]) { + errorMsg = 'A death record already exists for this animal (' + (deathIdMap[row.Id].QCStateLabel || 'unknown state') + ').'; } - else if (!helper.isValidateOnly() && row.Id && row.date && row.QCStateLabel.toUpperCase() === 'COMPLETED') { - if (validIds.indexOf(row.id) !== -1) { + if (errorMsg) { + EHR.Server.Utils.addError(scriptErrors, 'Id', errorMsg, 'ERROR'); + } + else { + if (!helper.isValidateOnly() && row.Id && row.date && rowQCState === 'COMPLETED') { // update demographics demographicsUpdates.push({ @@ -128,26 +144,33 @@ function onUpsert(helper, scriptErrors, row, oldRow) { helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates); console.log('updated demographics death date for animal: ' + row.Id); } - else { - console.log(row.id + " is not a valid animal id"); + + if (!helper.isValidateOnly() && row.date && row.QCStateLabel && EHR.Server.Security.getQCStateByLabel(row.QCStateLabel).PublicData) { + var qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); + + //add/update weight record + var weightRecord = { + Id: row.Id, + date: row.date, + weight: row.deathWeight, + taskid: row.taskid, + qcstate: qcstate, + performedby: row.performedby + }; + if (triggerHelper.upsertWeightRecord(weightRecord, false)) { + helper.addTableModified('study', 'weight'); + } } - } - if(row.QCStateLabel && EHR.Server.Security.getQCStateByLabel(row.QCStateLabel).PublicData) { - var qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); - - //add/update weight record - var weightRecord = { - Id: row.Id, - date: row.date, - weight: row.deathWeight, - taskid: row.taskid, - qcstate: qcstate, - performedby: row.performedby - }; - triggerHelper.upsertWeightRecord(weightRecord); + // mark only rows that passed, so a duplicate of a failed row reports that row's underlying error + deathsInTransaction[row.Id] = true; + helper.setProperty('deathsInTransaction', deathsInTransaction); } } + // insert-only: updates of existing death records keep their prior behavior + else if (oldRow === undefined) { + EHR.Server.Utils.addError(scriptErrors, 'Id', 'Id not found in the demographics table.', 'ERROR'); + } } } diff --git a/nbri_ehr/resources/queries/study/deaths.query.xml b/nbri_ehr/resources/queries/study/deaths.query.xml index 35c2e6a..e28e2f0 100644 --- a/nbri_ehr/resources/queries/study/deaths.query.xml +++ b/nbri_ehr/resources/queries/study/deaths.query.xml @@ -7,6 +7,15 @@ Death Date + + Death Type + + ehr_lookups + death_type + value + title + + Disposition diff --git a/nbri_ehr/resources/queries/study/deaths/.qview.xml b/nbri_ehr/resources/queries/study/deaths/.qview.xml index fdc063c..56eecec 100644 --- a/nbri_ehr/resources/queries/study/deaths/.qview.xml +++ b/nbri_ehr/resources/queries/study/deaths/.qview.xml @@ -6,6 +6,7 @@ + diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml index 42317f7..bf236b7 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -536,6 +536,9 @@ double + + varchar +
diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js new file mode 100644 index 0000000..568a410 --- /dev/null +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * 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 + * when the same fields render in a form panel. + */ +EHR.model.DataModelManager.registerMetadata('Death', { + allQueries: { + }, + byQuery: { + 'study.deaths': { + qcstate: { + hidden: true + }, + date: { + xtype: 'xdatetime', + editorConfig: { + dateFormat: 'Y-m-d', + timeFormat: 'H:i' + }, + columnConfig: { + width: 160 + } + }, + deathWeight: { + label: 'Weight (kg)', + columnConfig: { + width: 150 + } + }, + type: { + allowBlank: false, + nullable: false, + columnConfig: { + width: 160 + } + }, + reason: { + columnConfig: { + width: 160 + } + }, + remark: { + xtype: 'ehr-remarkfield', + columnConfig: { + width: 200 + } + } + } + } +}); 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 cb988e9..2f7b703 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/DeathNecropsy.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/DeathNecropsy.js @@ -19,13 +19,11 @@ EHR.model.DataModelManager.registerMetadata('DeathNecropsy', { }, }, deathWeight: { - label: 'Weight (kg)', - allowBlank: false, - nullable: false, + label: 'Weight (kg)' }, - reason: { + type: { allowBlank: false, - nullable: false, + nullable: false } }, 'study.necropsy': { 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 5339e54..aa76225 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java @@ -212,6 +212,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(NBRIHousingFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIMedicationTreatmentFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIProjectFormType.class, this)); diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkDeathFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkDeathFormType.java new file mode 100644 index 0000000..5d95343 --- /dev/null +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkDeathFormType.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.nbri_ehr.dataentry.form; + +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; +import org.labkey.nbri_ehr.dataentry.section.NBRITaskFormSection; + +import java.util.Arrays; + +/** + * Admin-only form that records deaths only, as a grid so several animals can be entered at once. + */ +public class NBRIBulkDeathFormType extends NBRIBaseTaskFormType +{ + public static final String NAME = "BulkDeaths"; + public static final String LABEL = "Bulk Deaths"; + + public NBRIBulkDeathFormType(DataEntryFormContext ctx, Module owner) + { + super(ctx, owner, NAME, LABEL, "Colony Management", Arrays.asList( + new NBRITaskFormSection(), + new NBRIAnimalDetailsFormSection(), + new BaseFormSection("study", "deaths", "Deaths", EHRService.FORM_SECTION_LOCATION.Body, true, false) + )); + + addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Death.js")); + + for (FormSection s : getFormSections()) + { + 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/query/NBRI_EHRTriggerHelper.java b/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java index a61a5fc..662ab54 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,7 +323,19 @@ public boolean deathExists(String id) return false; } - public void upsertWeightRecord(Map row) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException + public boolean upsertWeightRecord(Map row) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException + { + return upsertWeightRecord(row, true); + } + + /** + * When announceChanges is false, the nested weight trigger will not announce the modified id + * (skipAnnounceChangedParticipants). Callers must mark study.weight as modified on the outer helper + * (addTableModified) so the single announcement at trigger completion covers it. + * + * @return whether a weight record was written; false when there was nothing to record + */ + public boolean upsertWeightRecord(Map row, boolean announceChanges) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException { BatchValidationException errors = new BatchValidationException(); Date date = ConvertHelper.convert(row.get("date"), Date.class); @@ -331,25 +343,16 @@ public void upsertWeightRecord(Map row) throws QueryUpdateServic TableInfo ti = getTableInfo("study", "weight"); - // If there is already a weight record for this task, update that record - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id"), row.get("Id")); - filter.addCondition(FieldKey.fromString("taskid"), taskId); - TableSelector ts = new TableSelector(ti, PageFlowUtil.set("lsid", "objectid"), filter, null); - boolean updateRecord = ts.exists(); - - Map saveRow = new CaseInsensitiveHashMap<>(); - saveRow.put("Id", row.get("Id")); - saveRow.put("date", date); - saveRow.put("taskid", taskId); - saveRow.put("qcstate", row.get("qcstate")); - saveRow.put("performedby", row.get("performedby")); - if (updateRecord) - { - saveRow.put("objectid", ts.getMap().get("objectid")); - } - else + // If there is already a weight record for this task, update that record. A null taskid filter flips to + // "taskid IS NULL" and would match unrelated historical weights, so task-less entry (e.g. a non-EHR bulk + // import form) is insert-only. + Map existingRecord = null; + if (taskId != null) { - saveRow.put("objectid", new GUID().toString()); + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id"), row.get("Id")); + filter.addCondition(FieldKey.fromString("taskid"), taskId); + TableSelector ts = new TableSelector(ti, PageFlowUtil.set("lsid", "objectid"), filter, null); + existingRecord = ts.getMap(); } Double weight = null; @@ -357,22 +360,51 @@ public void upsertWeightRecord(Map row) throws QueryUpdateServic { weight = ConvertHelper.convert(row.get("weight"), Double.class); } + + Map context = getExtraContext(); + if (!announceChanges) + context.put("skipAnnounceChangedParticipants", true); + + // Weight is optional, so with none entered there is nothing to record. Delete any record left by an earlier + // save rather than blanking it: the weight trigger only WARNs on a null weight and the default threshold + // filters that out, so the emptied record would survive the save. + if (weight == null) + { + if (existingRecord == null) + return false; + + Map keyRow = new CaseInsensitiveHashMap<>(); + keyRow.put("lsid", existingRecord.get("lsid")); + ti.getUpdateService().deleteRows(_user, _container, List.of(keyRow), null, context); + + return true; + } + + Map saveRow = new CaseInsensitiveHashMap<>(); + saveRow.put("Id", row.get("Id")); + saveRow.put("date", date); + saveRow.put("taskid", taskId); + saveRow.put("qcstate", row.get("qcstate")); + saveRow.put("performedby", row.get("performedby")); + saveRow.put("objectid", existingRecord != null ? existingRecord.get("objectid") : new GUID().toString()); saveRow.put("weight", weight); List> rows = new ArrayList<>(); rows.add(saveRow); - if (updateRecord) + if (existingRecord != null) { - ti.getUpdateService().updateRows(_user, _container, rows, null, null, getExtraContext()); + ti.getUpdateService().updateRows(_user, _container, rows, null, null, context); } else { - ti.getUpdateService().insertRows(_user, _container, rows, errors, null, getExtraContext()); + ti.getUpdateService().insertRows(_user, _container, rows, errors, null, context); } if (errors.hasErrors()) throw errors; + + return true; } public void clinicalMoveNotification(final String animalId, final String date) 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 a06c389..1711854 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 @@ -103,6 +103,8 @@ public class NBRI_EHRTest extends AbstractGenericEHRTest implements PostgresOnly private static final String deadAnimalId = "D5454"; private static final String departedAnimalId = "H6767"; private static final String aliveAnimalId = "A4545"; + // never inserted into demographics; exercises the deaths trigger's unknown-Id rejection + private static final String unknownAnimalId = "X9999"; // Dedicated animal for testScheduledObservationTaskGrouping; provisioned (alive, housed, assigned) in // createTestSubjects so the clinical case form raises no warnings that would keep the validation banner up. private static final String taskGroupAnimalId = "TESTGRP9090"; @@ -1272,7 +1274,14 @@ public void testDeathNecropsyForm() throws IOException, CommandException setFormElement(Locator.name("Id"), departedAnimalId); waitForText("Id: ERROR: Animal is not at the center."); + setFormElement(Locator.name("Id"), unknownAnimalId); + waitForText("Id: ERROR: Id not found in the demographics table."); + + setFormElement(Locator.name("Id"), deadAnimalId); + waitForText("Id: ERROR: Death record already exists for this animal."); + setFormElement(Locator.name("Id"), aliveAnimalId); + _ext4Helper.selectComboBoxItem("Death Type:", "Spontaneous/Normal"); _ext4Helper.selectComboBoxItem("Disposition:", "Euthaniasia (project)"); waitForElement(Locator.name("deathWeight")); setFormElement(Locator.name("deathWeight"), "23"); @@ -1281,6 +1290,15 @@ public void testDeathNecropsyForm() throws IOException, CommandException submitForm("Submit Death", "Confirm"); stopImpersonating(); + log("Verify a second death insert is rejected with a validation error, not a unique constraint violation"); + SimplePostCommand duplicateDeath = getApiHelper().prepareInsertCommand("study", "deaths", "lsid", + new String[]{"Id", "date", "reason", "performedby"}, + new Object[][]{{aliveAnimalId, LocalDateTime.now(), "4", 1004}}); + CommandException duplicateError = getApiHelper().doSaveRowsExpectingError(DATA_ADMIN.getEmail(), duplicateDeath, getExtraContext()); + Map> duplicateErrors = getApiHelper().extractErrors(duplicateError.getProperties()); + Assert.assertTrue("Expected duplicate death validation error, got: " + duplicateErrors, + duplicateErrors.getOrDefault("Id", List.of()).contains("ERROR: A death record already exists for this animal (Request: Pending).")); + log("Trigger notifications"); goToEHRFolder(); NotificationAdminPage adminPage = NotificationAdminPage.beginAt(this); From 3250f62c5feae385aef60d09167a3c3544ec9013 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 2 Aug 2026 20:35:19 -0700 Subject: [PATCH 5/7] Weights and species reports (#13) ## Rationale This PR sets up NBRI weight and species reporting. Weight validation never fired, because the weight ranges lookup was keyed on species common names while demographics records hold species codes, so no animal ever matched a range. NBRI's query metadata files also replace the ehr module's rather than merging with them, so several wrapped lookup columns that the ehr-supplied views depend on had silently disappeared from NBRI tables. ## Changes - Key the weight ranges on species codes, and load the species lookups those codes resolve against. - Re-declare the wrapped lookup columns that NBRI's query metadata was dropping so the ehr-supplied views resolve again, and correct a metadata file that named the wrong table. - Convert the test subjects and the reference sample data from placeholder species and sex values to real lookup codes, and update the weight validation expectation to match the new ranges. --- nbri_ehr/resources/data/lookupsManifest.tsv | 2 + nbri_ehr/resources/data/weight_ranges.tsv | 6 ++- .../ehr_lookups/weight_ranges.query.xml | 19 ++++++++++ .../queries/study/assignment.query.xml | 12 ++++++ .../queries/study/chemistryResults.query.xml | 14 ++++++- .../resources/queries/study/housing.query.xml | 13 +++++++ .../resources/queries/study/weight.query.xml | 22 +++++++++++ .../study/datasets/datasetDemographics.tsv | 26 ++++++------- .../study/study/datasets/datasetWeight.tsv | 4 +- .../tests.nbri_ehr/NBRI_EHRTest.java | 37 ++++++++++--------- 10 files changed, 119 insertions(+), 36 deletions(-) create mode 100644 nbri_ehr/resources/queries/ehr_lookups/weight_ranges.query.xml diff --git a/nbri_ehr/resources/data/lookupsManifest.tsv b/nbri_ehr/resources/data/lookupsManifest.tsv index e7a5d49..dae20fa 100644 --- a/nbri_ehr/resources/data/lookupsManifest.tsv +++ b/nbri_ehr/resources/data/lookupsManifest.tsv @@ -112,6 +112,8 @@ routes sib_score source snomed +species +species_codes skin_problem status_codes stool_score diff --git a/nbri_ehr/resources/data/weight_ranges.tsv b/nbri_ehr/resources/data/weight_ranges.tsv index bb2c94f..dca47bd 100644 --- a/nbri_ehr/resources/data/weight_ranges.tsv +++ b/nbri_ehr/resources/data/weight_ranges.tsv @@ -1,3 +1,5 @@ Species Min Weight Max Weight -Cynomolgus 0.0 20.0 -Rhesus 0.0 35.0 \ No newline at end of file +CMO 0.04 2.2 +MCY 0.2 16.0 +MMU 0.2 30.0 +MNE 0.2 30.0 diff --git a/nbri_ehr/resources/queries/ehr_lookups/weight_ranges.query.xml b/nbri_ehr/resources/queries/ehr_lookups/weight_ranges.query.xml new file mode 100644 index 0000000..b5f2f4d --- /dev/null +++ b/nbri_ehr/resources/queries/ehr_lookups/weight_ranges.query.xml @@ -0,0 +1,19 @@ + + + +
+ + + Species + + ehr_lookups + species_codes + code + common_name + + + +
+ +
+ diff --git a/nbri_ehr/resources/queries/study/assignment.query.xml b/nbri_ehr/resources/queries/study/assignment.query.xml index 1cf7a36..df55a2e 100644 --- a/nbri_ehr/resources/queries/study/assignment.query.xml +++ b/nbri_ehr/resources/queries/study/assignment.query.xml @@ -21,6 +21,18 @@ true + + + CoAssignments + false + true + + study + assignmentTotalCoAssigned + lsid + + diff --git a/nbri_ehr/resources/queries/study/chemistryResults.query.xml b/nbri_ehr/resources/queries/study/chemistryResults.query.xml index caa0e9e..154a0ae 100644 --- a/nbri_ehr/resources/queries/study/chemistryResults.query.xml +++ b/nbri_ehr/resources/queries/study/chemistryResults.query.xml @@ -1,7 +1,7 @@ - +
@@ -15,6 +15,18 @@ Type + + + Ref Range + true + false + + study + chemistryRefRange + lsid + +
diff --git a/nbri_ehr/resources/queries/study/housing.query.xml b/nbri_ehr/resources/queries/study/housing.query.xml index eeb5e0d..cfaafa6 100644 --- a/nbri_ehr/resources/queries/study/housing.query.xml +++ b/nbri_ehr/resources/queries/study/housing.query.xml @@ -45,6 +45,19 @@ + + + Total Cagemates + false + true + + study + housingTotalRoommates + lsid + + diff --git a/nbri_ehr/resources/queries/study/weight.query.xml b/nbri_ehr/resources/queries/study/weight.query.xml index d9ee743..ca0dfdc 100644 --- a/nbri_ehr/resources/queries/study/weight.query.xml +++ b/nbri_ehr/resources/queries/study/weight.query.xml @@ -29,6 +29,28 @@ + + + Percent Change + false + true + + study + weightPctChange + lsid + + + + Relative Change + false + true + + study + weightRelChange + lsid + + diff --git a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDemographics.tsv b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDemographics.tsv index 8dc76ff..93872c6 100644 --- a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDemographics.tsv +++ b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDemographics.tsv @@ -1,14 +1,14 @@ objectid Id QCStateLabel date birth death calculated_status gender sire dam species origin performedby -1 44444 Completed -1381d -1381d Alive 1 44442 44443 10 00001 1004 -2 44446 Completed -1406d -1406d Alive 1 44442 44443 10 00003 1004 -3 44445 Completed -1414d -1414d -726d Dead 2 44442 44443 10 00004 1004 -4 TEST6390238 Completed -3923d -3923d Shipped 2 3565069 5250080 9 00002 1004 -5 TEST5904521 Completed -5431d -5431d Shipped 1 8377984 9 00004 1004 -6 TEST3804589 Completed -5806d -5806d Shipped 1 493957 9749422 9 00005 1004 -7 TEST2312318 Completed -8069d -8069d Shipped 1 5748235 8739374 9 00002 1004 -8 TEST1993532 Completed -11808d -11808d -2259d Dead 2 5409336 3784452 9 00003 1004 -9 TEST4551032 Completed -6362d -6362d Alive 1 5030167 8416939 9 00001 1004 -11 44442 Completed -6100d -6100d Alive 1 8377984 10 00004 1004 -12 44443 Completed -6100d -6100d Alive 1 8377984 10 00004 1004 -13 44447 Completed -2600d -2600d Alive 1 8377984 10 00004 1004 -14 8377984 Completed -2600d -2600d Alive 1 8377984 10 00004 1004 +1 44444 Completed -1381d -1381d Alive M 44442 44443 MNE 00001 1004 +2 44446 Completed -1406d -1406d Alive M 44442 44443 MNE 00003 1004 +3 44445 Completed -1414d -1414d -726d Dead F 44442 44443 MNE 00004 1004 +4 TEST6390238 Completed -3923d -3923d Shipped F 3565069 5250080 MMU 00002 1004 +5 TEST5904521 Completed -5431d -5431d Shipped M 8377984 MMU 00004 1004 +6 TEST3804589 Completed -5806d -5806d Shipped M 493957 9749422 CMO 00005 1004 +7 TEST2312318 Completed -8069d -8069d Shipped M 5748235 8739374 MMU 00002 1004 +8 TEST1993532 Completed -11808d -11808d -2259d Dead F 5409336 3784452 MMU 00003 1004 +9 TEST4551032 Completed -6362d -6362d Alive M 5030167 8416939 MMU 00001 1004 +11 44442 Completed -6100d -6100d Alive M 8377984 MNE 00004 1004 +12 44443 Completed -6100d -6100d Alive F 8377984 MNE 00004 1004 +13 44447 Completed -2600d -2600d Alive M 8377984 MNE 00004 1004 +14 8377984 Completed -2600d -2600d Alive F 8377984 MNE 00004 1004 diff --git a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetWeight.tsv b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetWeight.tsv index 75fd789..c47b7f7 100644 --- a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetWeight.tsv +++ b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetWeight.tsv @@ -1,6 +1,6 @@ objectid Id date weight remark QCStateLabel units performedby -1 TEST3804589 -5735d 0.037 vel praesent tincidunt Completed 1004 -2 TEST3804589 -5730d 0.035 erat et convallis Completed 1004 +1 TEST3804589 -5735d 0.040 vel praesent tincidunt Completed 1004 +2 TEST3804589 -5730d 0.040 erat et convallis Completed 1004 3 TEST3804589 -5727d 0.041 egestas pharetra Completed 1004 4 TEST3804589 -5722d 0.045 sed dui suscipit Completed 1004 5 TEST3804589 -5714d 0.058 a aliquet et tempus Completed 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 1711854..7970f13 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 @@ -409,13 +409,15 @@ protected void createTestSubjects() throws Exception //insert into demographics log("Creating test subjects"); + // demographics.species holds an ehr_lookups.species_codes code, and ehr_lookups.weight_ranges is keyed + // on that same code, so weight validation only fires for animals given a real code here. fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; data = new Object[][]{ - {SUBJECTS[0], "Rhesus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {SUBJECTS[1], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {SUBJECTS[2], "Marmoset", (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {SUBJECTS[3], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {SUBJECTS[4], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} + {SUBJECTS[0], "MMU", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {SUBJECTS[1], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {SUBJECTS[2], "CAE", (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {SUBJECTS[3], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {SUBJECTS[4], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} }; insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", StringUtils.join(SUBJECTS, ";"), Filter.Operator.IN)); @@ -423,11 +425,11 @@ protected void createTestSubjects() throws Exception //for simplicity, also create the animals from MORE_ANIMAL_IDS right now data = new Object[][]{ - {MORE_ANIMAL_IDS[0], "Rhesus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {MORE_ANIMAL_IDS[1], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {MORE_ANIMAL_IDS[2], "Marmoset", (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {MORE_ANIMAL_IDS[3], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {MORE_ANIMAL_IDS[4], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} + {MORE_ANIMAL_IDS[0], "MMU", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {MORE_ANIMAL_IDS[1], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {MORE_ANIMAL_IDS[2], "CAE", (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {MORE_ANIMAL_IDS[3], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {MORE_ANIMAL_IDS[4], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} }; insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", StringUtils.join(MORE_ANIMAL_IDS, ";"), Filter.Operator.IN)); @@ -480,7 +482,7 @@ protected void createTestSubjects() throws Exception log("Creating task grouping test subject"); fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; data = new Object[][]{ - {taskGroupAnimalId, "Rhesus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} + {taskGroupAnimalId, "MMU", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} }; insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", taskGroupAnimalId)); @@ -537,7 +539,7 @@ public void testWeightValidation() }; Map> expected = new HashMap<>(); expected.put("weight", Arrays.asList( - "WARN: Weight above the allowable value of 20.0 kg for Cynomolgus", + "WARN: Weight above the allowable value of 30.0 kg for MNE", "INFO: Weight gain of >10%. Last weight 12 kg") ); getApiHelper().testValidationMessage(DATA_ADMIN.getEmail(), "study", "weight", getWeightFields(), data, expected); @@ -1219,9 +1221,9 @@ public void createSubjectsForDeathForm() throws IOException, CommandException getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), getApiHelper().prepareInsertCommand("study", "birth", "lsid", new String[]{"Id", "Date", "gender", "QCStateLabel", "performedby"}, new Object[][]{ - {aliveAnimalId, LocalDateTime.now().minusDays(30), "f", "Completed", 1004}, - {deadAnimalId, LocalDateTime.now().minusDays(30), "m", "Completed", 1004}, - {departedAnimalId, LocalDateTime.now().minusDays(30), "m", "Completed", 1004}, + {aliveAnimalId, LocalDateTime.now().minusDays(30), getFemale(), "Completed", 1004}, + {deadAnimalId, LocalDateTime.now().minusDays(30), getMale(), "Completed", 1004}, + {departedAnimalId, LocalDateTime.now().minusDays(30), getMale(), "Completed", 1004}, } ), getExtraContext()); @@ -1721,9 +1723,8 @@ private int countLines(File file) throws Exception } } - // Creates the parents of a conception. They need a species from the ehr_lookups.species list because the - // Start with Conception window copies the dam's species onto the newborn, and the reference study's - // demographics data carries placeholder species values that no lookup entry matches. + // Creates the parents of a conception. They need an ehr_lookups.species_codes code because the Start with + // Conception window copies the dam's species onto the newborn, and the test asserts the resulting record. private void createBreedingPair(String damId, String sireId, String species) throws Exception { String[] fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; From af3e3e6a01c6238835b7fd1f7565a49a1bca680c Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 2 Aug 2026 20:35:48 -0700 Subject: [PATCH 6/7] Animal flag workflows (#14) ## Rationale Animal record flags were displayed by their short value, which is a terse code rather than something a reader can interpret, so the descriptive text already stored alongside each flag went unused. The flag lookup also keyed on an opaque identifier, which made the underlying data hard to read and maintain. ## Changes - The flags lookup is keyed and displayed by meaningful text rather than an opaque identifier, so flags read as category plus description wherever they appear. - The animal snapshot, the flags data entry form, and the default flags grid all show the description. - Flag categories and values are seeded as module lookups. - Flag records may be future dated. - Sample data references flags by their value. --- nbri_ehr/resources/data/flag_categories.tsv | 11 ++++++++++ nbri_ehr/resources/data/flag_values.tsv | 11 ++++++++++ nbri_ehr/resources/data/lookupsManifest.tsv | 2 ++ .../resources/data/lookupsManifestTest.tsv | 3 +++ .../queries/ehr_lookups/flag_values.query.xml | 13 ++++++++++++ .../resources/queries/study/flags.query.xml | 4 ++-- .../resources/queries/study/flags/.qview.xml | 2 +- nbri_ehr/resources/scripts/nbri_triggers.js | 6 ++++++ .../web/nbri_ehr/model/sources/NBRIDefault.js | 4 ++-- .../web/nbri_ehr/panel/SnapshotPanel.js | 4 ++++ .../ActiveFlagsDemographicsProvider.java | 1 + .../study/study/datasets/datasetFlags.tsv | 20 +++++++++---------- 12 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 nbri_ehr/resources/data/flag_categories.tsv create mode 100644 nbri_ehr/resources/data/flag_values.tsv create mode 100644 nbri_ehr/resources/queries/ehr_lookups/flag_values.query.xml diff --git a/nbri_ehr/resources/data/flag_categories.tsv b/nbri_ehr/resources/data/flag_categories.tsv new file mode 100644 index 0000000..2e815bf --- /dev/null +++ b/nbri_ehr/resources/data/flag_categories.tsv @@ -0,0 +1,11 @@ +Category Description Enforce Single Flag Per Animal? Omit When Displaying Default Flags? Highlight Flags of This Category? Date Disabled +Behavioral Behavioral observations, abnormal-behavior designations and enrichment provided to the animal. false false false +Capture Restraint and handling limitations. false false true +Clinical Clinical conditions, procedures and veterinary designations. false false false +Experimental Experimental treatments, inoculations and implants. false false true +Genetics Genetic background, ancestry, inbreeding and genomic characterization. false true false +Hold Animal held or reserved for a specific project, investigator or shipment. false false false +Reproduction Breeding, contraception, pregnancy, fostering and rearing designations. false false false +Socially housed Social rank, pairing, group-formation and social-housing-exemption designations. false false false +Status Administrative status and availability designations. false false true +Training Training program participation and progress. false false false diff --git a/nbri_ehr/resources/data/flag_values.tsv b/nbri_ehr/resources/data/flag_values.tsv new file mode 100644 index 0000000..8a075a7 --- /dev/null +++ b/nbri_ehr/resources/data/flag_values.tsv @@ -0,0 +1,11 @@ +Category Meaning Description Date Disabled ObjectId +Behavioral Behavioral FLAG 1 Behavioral FLAG 1 +Capture Capture FLAG 1 Capture FLAG 1 +Clinical Clinical FLAG 1 Clinical FLAG 1 +Experimental Experimental FLAG 1 Experimental FLAG 1 +Genetics Genetics FLAG 1 Genetics FLAG 1 +Hold Hold FLAG 1 Hold FLAG 1 +Reproduction Reproduction FLAG 1 Reproduction FLAG 1 +Socially housed Socially housed FLAG 1 Socially housed FLAG 1 +Status Status FLAG 1 Status FLAG 1 +Training Training FLAG 1 Training FLAG 1 diff --git a/nbri_ehr/resources/data/lookupsManifest.tsv b/nbri_ehr/resources/data/lookupsManifest.tsv index dae20fa..9f32d12 100644 --- a/nbri_ehr/resources/data/lookupsManifest.tsv +++ b/nbri_ehr/resources/data/lookupsManifest.tsv @@ -55,6 +55,8 @@ expense_class fecal_score fecal_smear_score feed_assess_types +flag_categories +flag_values gastro_types gender_codes general_obs diff --git a/nbri_ehr/resources/data/lookupsManifestTest.tsv b/nbri_ehr/resources/data/lookupsManifestTest.tsv index 3e334b8..d64c8ba 100644 --- a/nbri_ehr/resources/data/lookupsManifestTest.tsv +++ b/nbri_ehr/resources/data/lookupsManifestTest.tsv @@ -12,6 +12,7 @@ amount_units app_score arrival_type arthritis_types +att_score bandage_observations bcs_score behavior_abnormality @@ -54,6 +55,8 @@ expense_class fecal_score fecal_smear_score feed_assess_types +flag_categories +flag_values gastro_types gender_codes general_obs diff --git a/nbri_ehr/resources/queries/ehr_lookups/flag_values.query.xml b/nbri_ehr/resources/queries/ehr_lookups/flag_values.query.xml new file mode 100644 index 0000000..c6531be --- /dev/null +++ b/nbri_ehr/resources/queries/ehr_lookups/flag_values.query.xml @@ -0,0 +1,13 @@ + + + + + + + Value + + +
+
+
+
diff --git a/nbri_ehr/resources/queries/study/flags.query.xml b/nbri_ehr/resources/queries/study/flags.query.xml index 570a645..00a8119 100644 --- a/nbri_ehr/resources/queries/study/flags.query.xml +++ b/nbri_ehr/resources/queries/study/flags.query.xml @@ -23,8 +23,8 @@ ehr_lookups flag_values - objectid - value + value + Description
diff --git a/nbri_ehr/resources/queries/study/flags/.qview.xml b/nbri_ehr/resources/queries/study/flags/.qview.xml index bf12e57..68dece5 100644 --- a/nbri_ehr/resources/queries/study/flags/.qview.xml +++ b/nbri_ehr/resources/queries/study/flags/.qview.xml @@ -7,7 +7,7 @@ - + diff --git a/nbri_ehr/resources/scripts/nbri_triggers.js b/nbri_ehr/resources/scripts/nbri_triggers.js index 190f656..16b2ddc 100644 --- a/nbri_ehr/resources/scripts/nbri_triggers.js +++ b/nbri_ehr/resources/scripts/nbri_triggers.js @@ -115,6 +115,12 @@ exports.init = function (EHR) { }); }); + EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'flags', function(event, helper) { + helper.setScriptOptions({ + allowFutureDates: true, + }); + }); + EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'treatment_order', function(event, helper) { helper.setScriptOptions({ allowFutureDates: true, diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/NBRIDefault.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/NBRIDefault.js index c5971be..ea19076 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/NBRIDefault.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/NBRIDefault.js @@ -327,7 +327,7 @@ EHR.model.DataModelManager.registerMetadata('Default', { flag: { allowBlank: false, lookup: { - columns: 'objectid,value,category,code', + columns: 'objectid,value,description,category,code', sort: 'category,code,value', filterArray: [LABKEY.Filter.create('datedisabled', null, LABKEY.Filter.Types.ISBLANK)] }, @@ -341,7 +341,7 @@ EHR.model.DataModelManager.registerMetadata('Default', { allowChooseOther: false })], listConfig: { - innerTpl: '{[(values.category ? ("" + LABKEY.Utils.encodeHtml(values.category) + ": ") : "") + LABKEY.Utils.encodeHtml(values.value)]}', + innerTpl: '{[(values.category ? ("" + LABKEY.Utils.encodeHtml(values.category) + ": ") : "") + LABKEY.Utils.encodeHtml(values.description || values.value)]}', getInnerTpl: function () { return this.innerTpl; } diff --git a/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js b/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js index dd59726..bfdb0fd 100644 --- a/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js +++ b/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js @@ -228,6 +228,10 @@ Ext4.define('NBRI_EHR.panel.SnapshotPanel', { toSet['flags'] = values.length ? '' + values.join('
') + '' : null; }, + getFlagDisplayValue: function(row) { + return row['flag/description'] || row['flag/value']; + }, + appendAssignments: function(toSet, results){ toSet['projectAssignment'] = null; diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveFlagsDemographicsProvider.java b/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveFlagsDemographicsProvider.java index d327a44..f012b8a 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveFlagsDemographicsProvider.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveFlagsDemographicsProvider.java @@ -47,6 +47,7 @@ protected Set getFieldKeys() keys.add(FieldKey.fromString("flag")); keys.add(FieldKey.fromString("flag/category")); keys.add(FieldKey.fromString("flag/value")); + keys.add(FieldKey.fromString("flag/description")); keys.add(FieldKey.fromString("performedby")); keys.add(FieldKey.fromString("remark")); diff --git a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetFlags.tsv b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetFlags.tsv index 45004d8..ce33d90 100644 --- a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetFlags.tsv +++ b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetFlags.tsv @@ -1,11 +1,11 @@ objectId Id date enddate flag QCStateLabel performedby -1 TEST3804589 -2187d -2178d 1 Completed 1004 -2 TEST4551032 -2243d -2213d 2 Completed 1004 -3 TEST5904521 -2215d -2186d 3 Completed 1004 -4 TEST1112911 -2215d 4 Completed 1004 -5 TEST1112911 -2215d 5 Completed 1004 -6 44444 -1423d -560d 3 Completed 1004 -7 44444 -1423d -560d 1 Completed 1004 -10 44444 -1208d 3 Completed 1004 -8 44446 -1208d 3 Completed 1004 -9 TSTCP -1423d -560d 3 Completed 1004 +1 TEST3804589 -2187d -2178d Behavioral FLAG 1 Completed 1004 +2 TEST4551032 -2243d -2213d Capture FLAG 1 Completed 1004 +3 TEST5904521 -2215d -2186d Clinical FLAG 1 Completed 1004 +4 TEST1112911 -2215d Experimental FLAG 1 Completed 1004 +5 TEST1112911 -2215d Genetics FLAG 1 Completed 1004 +6 44444 -1423d -560d Clinical FLAG 1 Completed 1004 +7 44444 -1423d -560d Behavioral FLAG 1 Completed 1004 +10 44444 -1208d Clinical FLAG 1 Completed 1004 +8 44446 -1208d Clinical FLAG 1 Completed 1004 +9 TSTCP -1423d -560d Clinical FLAG 1 Completed 1004 From 631b905748176039d5643e6065c5f3dde9e27303 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 2 Aug 2026 20:43:45 -0700 Subject: [PATCH 7/7] Housing and location updates (#12) ## Rationale Remove floors from the NBRI location hierarchy so a location is identified by building, room, and cage alone. Floors are not a meaningful unit of location at NBRI, and requiring one on every room forced entry of a value that carries no information while complicating the derived location keys. This also drops the historicalOther dataset, which is no longer part of the reference study. Keys are derived only when a record is created, so nothing recomputes them for locations that already exist. There is no location data in place yet, so there is nothing to migrate. ## Changes - Rooms are keyed by building and name, and floor is no longer required or shown. Room entry now requires a building, since the derived key depends on it. - Buildings no longer fold the area into their name, so a description identifies a building on its own and a reused one is rejected rather than colliding. - Derived building, room, and cage keys are persisted consistently, and a key too long for its column is rejected with a message naming the parts at fault. Cages resolve to their room. - The cagemates report resolves animals in group pens, which have no cage and are housed against the room alone. Animals with a cage continue to match on the cage, which already identifies its room. - The cagemates report now counts only current occupants. It previously counted every living animal that had ever been housed in the cage, and it now also skips housing records that have not been approved. - Removes the floor-based combined room and floor display columns, and repoints everything that depended on them: the room and floor lookups, the housing views and history, the cage details page, and every view that previously reached the building through a floor. - Room lookups carry a link through to the cage details page, matching how cages already behave. - Location test fixtures build rooms from a building instead of a floor, and cover the room key, the building rules, and cagemates for both group pens and cages recorded without a room. - Removes the historicalOther dataset, its report, and its metadata. --- .../queries/ehr_lookups/buildings.js | 27 +- .../resources/queries/ehr_lookups/cage.js | 24 +- .../queries/ehr_lookups/cage.query.xml | 5 + .../resources/queries/ehr_lookups/rooms.js | 25 +- .../queries/ehr_lookups/rooms.query.xml | 12 +- .../queries/ehr_lookups/rooms/.qview.xml | 2 +- .../queries/nbri_ehr/AnimalReqOrder.query.xml | 2 +- .../study/BehaviorClinRemarks/.qview.xml | 2 +- .../study/ClinicalClinRemarks/.qview.xml | 2 +- .../study/activeBehaviorCases/.qview.xml | 2 +- .../study/activeClinicalCases/.qview.xml | 2 +- .../queries/study/alopecia/.qview.xml | 2 +- .../queries/study/behaviorCases/.qview.xml | 2 +- .../study/behaviorObservations/.qview.xml | 2 +- .../resources/queries/study/blood/.qview.xml | 2 +- .../queries/study/breeder/.qview.xml | 2 +- .../resources/queries/study/cases/.qview.xml | 2 +- .../cases/Active Behavior Cases.qview.xml | 2 +- .../cases/Active Clinical Cases.qview.xml | 2 +- .../study/cases/All Behavior Cases.qview.xml | 2 +- .../study/cases/All Clinical Cases.qview.xml | 2 +- .../queries/study/chemistryResults/.qview.xml | 2 +- .../queries/study/clinicalCases/.qview.xml | 2 +- .../study/clinicalObservations/.qview.xml | 2 +- .../study/clinical_observations/.qview.xml | 2 +- .../Alopecia Scores.qview.xml | 2 +- .../clinical_observations/Behavior.qview.xml | 2 +- .../clinical_observations/Clinical.qview.xml | 2 +- .../clinical_observationsSummary/.qview.xml | 2 +- .../queries/study/clinremarks/.qview.xml | 2 +- .../study/clinremarks/Behavior.qview.xml | 2 +- .../study/clinremarks/Clinical.qview.xml | 2 +- .../queries/study/demographics/.qview.xml | 2 +- .../queries/study/demographicsCagemates.sql | 9 +- .../resources/queries/study/drug/.qview.xml | 2 +- .../queries/study/drug/Behavior.qview.xml | 2 +- .../queries/study/exemptions/.qview.xml | 2 +- .../resources/queries/study/flags/.qview.xml | 2 +- .../queries/study/historicalOther.query.xml | 13 - .../resources/queries/study/housing.query.xml | 3 +- .../queries/study/housing/.qview.xml | 2 +- .../study/housing/Active Housing.qview.xml | 1 + .../resources/queries/study/notes/.qview.xml | 2 +- .../study/observationSchedule/.qview.xml | 4 +- .../study/observation_order/.qview.xml | 2 +- .../Active Behavior Orders.qview.xml | 2 +- .../Active Clinical Orders.qview.xml | 2 +- .../Behavior Orders.qview.xml | 2 +- .../Clinical Orders.qview.xml | 2 +- .../queries/study/pairingSummary/.qview.xml | 2 +- .../pairingSummary/Active Pairing.qview.xml | 2 +- .../pairingSummary/Pairing History.qview.xml | 2 +- .../queries/study/pairings/.qview.xml | 2 +- .../queries/study/physicalExam/.qview.xml | 2 +- .../resources/queries/study/prc/.qview.xml | 2 +- .../queries/study/prcOverdue/.qview.xml | 2 +- .../queries/study/prcSchedule/.qview.xml | 4 +- .../queries/study/prc_order/.qview.xml | 2 +- .../queries/study/prc_order_report/.qview.xml | 2 +- .../study/treatmentSchedule/.qview.xml | 4 +- .../queries/study/treatment_order/.qview.xml | 2 +- .../resources/queries/study/vitals/.qview.xml | 2 +- .../resources/queries/study/weight/.qview.xml | 2 +- .../study/datasets/datasets_manifest.xml | 1 - .../study/datasets/datasets_metadata.xml | 24 -- .../resources/reports/additionalReports.tsv | 1 - nbri_ehr/resources/views/cageDetails.html | 4 +- .../history/NBRIHousingDataSource.java | 4 +- .../nbri_ehr/table/NBRI_EHRCustomizer.java | 68 +---- .../study/study/datasets/datasetHousing.tsv | 18 +- .../tests.nbri_ehr/NBRI_EHRTest.java | 251 ++++++++++++++++-- 71 files changed, 403 insertions(+), 201 deletions(-) delete mode 100644 nbri_ehr/resources/queries/study/historicalOther.query.xml diff --git a/nbri_ehr/resources/queries/ehr_lookups/buildings.js b/nbri_ehr/resources/queries/ehr_lookups/buildings.js index 3fca085..b5e9890 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/buildings.js +++ b/nbri_ehr/resources/queries/ehr_lookups/buildings.js @@ -7,6 +7,19 @@ var LABKEY = require("labkey"); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); +// Width of ehr_lookups.buildings.name. The description it is derived from is a wider column, so it can overrun the +// key; reject it here rather than letting the database raise an unreadable error. +var MAX_NAME_LENGTH = 100; + +// 'name' is not user editable, so it is absent from the incoming row map and the value this script derives has +// nowhere to land. Declaring it managed reserves a slot so the derived key is persisted. +function managedColumns() { + return { + insert: ["name"], + update: ["name"], + }; +} + function onUpsert(row, oldRow, errors){ if (extraContext.dataSource != "etl") { if (!row.description) { @@ -25,7 +38,19 @@ function onUpsert(row, oldRow, errors){ return; } - row.name = row.description + '-' + row.area; + if (row.description.length > MAX_NAME_LENGTH) { + errors['description'] = 'Description is too long: it becomes the building key, which cannot exceed ' + MAX_NAME_LENGTH + ' characters.'; + return; + } + + // The description alone identifies the building now that the area is no longer folded in, so a duplicate + // would collide on the key. Say so here instead of surfacing a constraint violation on a hidden column. + if (triggerHelper.totalRecords("ehr_lookups", "buildings", "name", row.description) > 0) { + errors['description'] = 'A building described as ' + row.description + ' already exists. Building descriptions must be unique.'; + return; + } + + row.name = row.description; } } } diff --git a/nbri_ehr/resources/queries/ehr_lookups/cage.js b/nbri_ehr/resources/queries/ehr_lookups/cage.js index e19a2b7..0e71832 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/cage.js +++ b/nbri_ehr/resources/queries/ehr_lookups/cage.js @@ -7,6 +7,19 @@ var LABKEY = require("labkey"); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); +// Width of ehr_lookups.cage.location. The derived key builds on the room key, which is itself derived, so it can +// overrun the column; reject it here rather than letting the database raise an unreadable error. +var MAX_LOCATION_LENGTH = 100; + +// 'location' is not user editable, so it is absent from the incoming row map and the value this script +// derives has nowhere to land. Declaring it managed reserves a slot so the derived key is persisted. +function managedColumns() { + return { + insert: ["location"], + update: ["location"], + }; +} + function onUpsert(row, oldRow, errors){ if (extraContext.dataSource != "etl") { if (!row.location) { @@ -20,9 +33,16 @@ function onUpsert(row, oldRow, errors){ return; } - row.location = row.room; + let location = row.room; if (row.cage) - row.location += '-' + row.cage; + location += '-' + row.cage; + + if (location.length > MAX_LOCATION_LENGTH) { + errors['cage'] = 'Room and cage are too long: they combine to a ' + location.length + ' character location key, which cannot exceed ' + MAX_LOCATION_LENGTH + '.'; + return; + } + + row.location = location; } } } diff --git a/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml b/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml index 97b1549..3d15c46 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml @@ -9,6 +9,11 @@
Room + + ehr_lookups + rooms + room + true diff --git a/nbri_ehr/resources/queries/ehr_lookups/rooms.js b/nbri_ehr/resources/queries/ehr_lookups/rooms.js index caea28b..18a10a2 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/rooms.js +++ b/nbri_ehr/resources/queries/ehr_lookups/rooms.js @@ -8,6 +8,19 @@ var console = require("console"); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); +// Width of ehr_lookups.rooms.room. The derived key is built from values the user supplies, so it can overrun the +// column; reject it here rather than letting the database raise an unreadable error. +var MAX_ROOM_LENGTH = 100; + +// 'room' is not user editable, so it is absent from the incoming row map and the value this script derives has +// nowhere to land. Declaring it managed reserves a slot so the derived key is persisted. +function managedColumns() { + return { + insert: ["room"], + update: ["room"], + }; +} + function onUpsert(row, oldRow, errors){ if (extraContext.dataSource != "etl") { if (!row.name) { @@ -15,8 +28,8 @@ function onUpsert(row, oldRow, errors){ return; } - if (!row.floor) { - errors['floor'] = 'Floor is required.'; + if (!row.building) { + errors['building'] = 'Building is required.'; return; } @@ -26,7 +39,13 @@ function onUpsert(row, oldRow, errors){ return; } - row.room = row.name + '-' + row.floor; + let room = row.building + '-' + row.name; + if (room.length > MAX_ROOM_LENGTH) { + errors['name'] = 'Building and room name are too long: they combine to a ' + room.length + ' character room key, which cannot exceed ' + MAX_ROOM_LENGTH + '.'; + return; + } + + row.room = room; } } } diff --git a/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml b/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml index dd7b89f..0b522c5 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml @@ -6,8 +6,10 @@ Rooms - + + + /nbri_ehr/cageDetails.view?room=${room} true false false @@ -17,6 +19,14 @@ false false + + + true + false + false + false + diff --git a/nbri_ehr/resources/queries/ehr_lookups/rooms/.qview.xml b/nbri_ehr/resources/queries/ehr_lookups/rooms/.qview.xml index 0bfbf5b..f8c6e5e 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/rooms/.qview.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/rooms/.qview.xml @@ -1,6 +1,6 @@ - + \ 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 index 5c829e1..e282d4d 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrder.query.xml +++ b/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrder.query.xml @@ -96,7 +96,7 @@ ehr_lookups rooms room - name + room diff --git a/nbri_ehr/resources/queries/study/BehaviorClinRemarks/.qview.xml b/nbri_ehr/resources/queries/study/BehaviorClinRemarks/.qview.xml index 4e381e4..243ffad 100644 --- a/nbri_ehr/resources/queries/study/BehaviorClinRemarks/.qview.xml +++ b/nbri_ehr/resources/queries/study/BehaviorClinRemarks/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/ClinicalClinRemarks/.qview.xml b/nbri_ehr/resources/queries/study/ClinicalClinRemarks/.qview.xml index c3ca1ea..86bc194 100644 --- a/nbri_ehr/resources/queries/study/ClinicalClinRemarks/.qview.xml +++ b/nbri_ehr/resources/queries/study/ClinicalClinRemarks/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/activeBehaviorCases/.qview.xml b/nbri_ehr/resources/queries/study/activeBehaviorCases/.qview.xml index b08797c..6026d97 100644 --- a/nbri_ehr/resources/queries/study/activeBehaviorCases/.qview.xml +++ b/nbri_ehr/resources/queries/study/activeBehaviorCases/.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/activeClinicalCases/.qview.xml b/nbri_ehr/resources/queries/study/activeClinicalCases/.qview.xml index b08797c..6026d97 100644 --- a/nbri_ehr/resources/queries/study/activeClinicalCases/.qview.xml +++ b/nbri_ehr/resources/queries/study/activeClinicalCases/.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/alopecia/.qview.xml b/nbri_ehr/resources/queries/study/alopecia/.qview.xml index f889f92..b514468 100644 --- a/nbri_ehr/resources/queries/study/alopecia/.qview.xml +++ b/nbri_ehr/resources/queries/study/alopecia/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/behaviorCases/.qview.xml b/nbri_ehr/resources/queries/study/behaviorCases/.qview.xml index b08797c..6026d97 100644 --- a/nbri_ehr/resources/queries/study/behaviorCases/.qview.xml +++ b/nbri_ehr/resources/queries/study/behaviorCases/.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/behaviorObservations/.qview.xml b/nbri_ehr/resources/queries/study/behaviorObservations/.qview.xml index a48d5a5..5a68b93 100644 --- a/nbri_ehr/resources/queries/study/behaviorObservations/.qview.xml +++ b/nbri_ehr/resources/queries/study/behaviorObservations/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/blood/.qview.xml b/nbri_ehr/resources/queries/study/blood/.qview.xml index 831d6b5..c382d83 100644 --- a/nbri_ehr/resources/queries/study/blood/.qview.xml +++ b/nbri_ehr/resources/queries/study/blood/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/breeder/.qview.xml b/nbri_ehr/resources/queries/study/breeder/.qview.xml index 448bd0f..bba85e1 100644 --- a/nbri_ehr/resources/queries/study/breeder/.qview.xml +++ b/nbri_ehr/resources/queries/study/breeder/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/cases/.qview.xml b/nbri_ehr/resources/queries/study/cases/.qview.xml index 340d770..a2f1fbc 100644 --- a/nbri_ehr/resources/queries/study/cases/.qview.xml +++ b/nbri_ehr/resources/queries/study/cases/.qview.xml @@ -3,7 +3,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/cases/Active Behavior Cases.qview.xml b/nbri_ehr/resources/queries/study/cases/Active Behavior Cases.qview.xml index 8333562..7f83fe9 100644 --- a/nbri_ehr/resources/queries/study/cases/Active Behavior Cases.qview.xml +++ b/nbri_ehr/resources/queries/study/cases/Active Behavior Cases.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/cases/Active Clinical Cases.qview.xml b/nbri_ehr/resources/queries/study/cases/Active Clinical Cases.qview.xml index 6081418..7554046 100644 --- a/nbri_ehr/resources/queries/study/cases/Active Clinical Cases.qview.xml +++ b/nbri_ehr/resources/queries/study/cases/Active Clinical Cases.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/cases/All Behavior Cases.qview.xml b/nbri_ehr/resources/queries/study/cases/All Behavior Cases.qview.xml index 261b1bb..25baf96 100644 --- a/nbri_ehr/resources/queries/study/cases/All Behavior Cases.qview.xml +++ b/nbri_ehr/resources/queries/study/cases/All Behavior Cases.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/cases/All Clinical Cases.qview.xml b/nbri_ehr/resources/queries/study/cases/All Clinical Cases.qview.xml index 73fb0b2..e0dd66f 100644 --- a/nbri_ehr/resources/queries/study/cases/All Clinical Cases.qview.xml +++ b/nbri_ehr/resources/queries/study/cases/All Clinical Cases.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/chemistryResults/.qview.xml b/nbri_ehr/resources/queries/study/chemistryResults/.qview.xml index 02467cb..3cc536b 100644 --- a/nbri_ehr/resources/queries/study/chemistryResults/.qview.xml +++ b/nbri_ehr/resources/queries/study/chemistryResults/.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinicalCases/.qview.xml b/nbri_ehr/resources/queries/study/clinicalCases/.qview.xml index b08797c..6026d97 100644 --- a/nbri_ehr/resources/queries/study/clinicalCases/.qview.xml +++ b/nbri_ehr/resources/queries/study/clinicalCases/.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinicalObservations/.qview.xml b/nbri_ehr/resources/queries/study/clinicalObservations/.qview.xml index 7b1fc1a..0fe5c32 100644 --- a/nbri_ehr/resources/queries/study/clinicalObservations/.qview.xml +++ b/nbri_ehr/resources/queries/study/clinicalObservations/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinical_observations/.qview.xml b/nbri_ehr/resources/queries/study/clinical_observations/.qview.xml index daaa82d..8d99f0c 100644 --- a/nbri_ehr/resources/queries/study/clinical_observations/.qview.xml +++ b/nbri_ehr/resources/queries/study/clinical_observations/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinical_observations/Alopecia Scores.qview.xml b/nbri_ehr/resources/queries/study/clinical_observations/Alopecia Scores.qview.xml index 0a05b82..589eba9 100644 --- a/nbri_ehr/resources/queries/study/clinical_observations/Alopecia Scores.qview.xml +++ b/nbri_ehr/resources/queries/study/clinical_observations/Alopecia Scores.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinical_observations/Behavior.qview.xml b/nbri_ehr/resources/queries/study/clinical_observations/Behavior.qview.xml index f7566f3..6c017a8 100644 --- a/nbri_ehr/resources/queries/study/clinical_observations/Behavior.qview.xml +++ b/nbri_ehr/resources/queries/study/clinical_observations/Behavior.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinical_observations/Clinical.qview.xml b/nbri_ehr/resources/queries/study/clinical_observations/Clinical.qview.xml index adf8cf8..e8cff6b 100644 --- a/nbri_ehr/resources/queries/study/clinical_observations/Clinical.qview.xml +++ b/nbri_ehr/resources/queries/study/clinical_observations/Clinical.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinical_observationsSummary/.qview.xml b/nbri_ehr/resources/queries/study/clinical_observationsSummary/.qview.xml index a48d5a5..5a68b93 100644 --- a/nbri_ehr/resources/queries/study/clinical_observationsSummary/.qview.xml +++ b/nbri_ehr/resources/queries/study/clinical_observationsSummary/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinremarks/.qview.xml b/nbri_ehr/resources/queries/study/clinremarks/.qview.xml index 903f88d..d96725e 100644 --- a/nbri_ehr/resources/queries/study/clinremarks/.qview.xml +++ b/nbri_ehr/resources/queries/study/clinremarks/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinremarks/Behavior.qview.xml b/nbri_ehr/resources/queries/study/clinremarks/Behavior.qview.xml index 24f5a40..007434b 100644 --- a/nbri_ehr/resources/queries/study/clinremarks/Behavior.qview.xml +++ b/nbri_ehr/resources/queries/study/clinremarks/Behavior.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinremarks/Clinical.qview.xml b/nbri_ehr/resources/queries/study/clinremarks/Clinical.qview.xml index 6cb3771..3123f8c 100644 --- a/nbri_ehr/resources/queries/study/clinremarks/Clinical.qview.xml +++ b/nbri_ehr/resources/queries/study/clinremarks/Clinical.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/demographics/.qview.xml b/nbri_ehr/resources/queries/study/demographics/.qview.xml index 439cd0a..d232062 100644 --- a/nbri_ehr/resources/queries/study/demographics/.qview.xml +++ b/nbri_ehr/resources/queries/study/demographics/.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/demographicsCagemates.sql b/nbri_ehr/resources/queries/study/demographicsCagemates.sql index b3464b7..4219925 100644 --- a/nbri_ehr/resources/queries/study/demographicsCagemates.sql +++ b/nbri_ehr/resources/queries/study/demographicsCagemates.sql @@ -22,10 +22,15 @@ SELECT FROM study.housing h JOIN study.housing h2 -ON (h2.Id.demographics.calculated_status = 'Alive' - AND (h.cage = h2.cage)) +-- 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)) + 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 ) t ON (t.id = d.id) diff --git a/nbri_ehr/resources/queries/study/drug/.qview.xml b/nbri_ehr/resources/queries/study/drug/.qview.xml index 3b72268..8489e4d 100644 --- a/nbri_ehr/resources/queries/study/drug/.qview.xml +++ b/nbri_ehr/resources/queries/study/drug/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/drug/Behavior.qview.xml b/nbri_ehr/resources/queries/study/drug/Behavior.qview.xml index a9769cb..1688ad3 100644 --- a/nbri_ehr/resources/queries/study/drug/Behavior.qview.xml +++ b/nbri_ehr/resources/queries/study/drug/Behavior.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/exemptions/.qview.xml b/nbri_ehr/resources/queries/study/exemptions/.qview.xml index 7e4278b..cdd0721 100644 --- a/nbri_ehr/resources/queries/study/exemptions/.qview.xml +++ b/nbri_ehr/resources/queries/study/exemptions/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/flags/.qview.xml b/nbri_ehr/resources/queries/study/flags/.qview.xml index 68dece5..3f15f5b 100644 --- a/nbri_ehr/resources/queries/study/flags/.qview.xml +++ b/nbri_ehr/resources/queries/study/flags/.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/historicalOther.query.xml b/nbri_ehr/resources/queries/study/historicalOther.query.xml deleted file mode 100644 index 37e2754..0000000 --- a/nbri_ehr/resources/queries/study/historicalOther.query.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Type - - -
-
-
-
\ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/housing.query.xml b/nbri_ehr/resources/queries/study/housing.query.xml index cfaafa6..e1a21a3 100644 --- a/nbri_ehr/resources/queries/study/housing.query.xml +++ b/nbri_ehr/resources/queries/study/housing.query.xml @@ -21,7 +21,6 @@ ehr_lookups cage location - cage
@@ -31,7 +30,7 @@ ehr_lookups rooms room - name + room diff --git a/nbri_ehr/resources/queries/study/housing/.qview.xml b/nbri_ehr/resources/queries/study/housing/.qview.xml index bc780a5..5f01093 100644 --- a/nbri_ehr/resources/queries/study/housing/.qview.xml +++ b/nbri_ehr/resources/queries/study/housing/.qview.xml @@ -7,7 +7,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/housing/Active Housing.qview.xml b/nbri_ehr/resources/queries/study/housing/Active Housing.qview.xml index 4fc3eeb..54e07b9 100644 --- a/nbri_ehr/resources/queries/study/housing/Active Housing.qview.xml +++ b/nbri_ehr/resources/queries/study/housing/Active Housing.qview.xml @@ -4,6 +4,7 @@ + diff --git a/nbri_ehr/resources/queries/study/notes/.qview.xml b/nbri_ehr/resources/queries/study/notes/.qview.xml index 6cd9bab..0f3a556 100644 --- a/nbri_ehr/resources/queries/study/notes/.qview.xml +++ b/nbri_ehr/resources/queries/study/notes/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/observationSchedule/.qview.xml b/nbri_ehr/resources/queries/study/observationSchedule/.qview.xml index b91d5d4..628bcd9 100644 --- a/nbri_ehr/resources/queries/study/observationSchedule/.qview.xml +++ b/nbri_ehr/resources/queries/study/observationSchedule/.qview.xml @@ -3,7 +3,7 @@ - + @@ -11,7 +11,7 @@ - + \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/observation_order/.qview.xml b/nbri_ehr/resources/queries/study/observation_order/.qview.xml index c02d2fb..40bb429 100644 --- a/nbri_ehr/resources/queries/study/observation_order/.qview.xml +++ b/nbri_ehr/resources/queries/study/observation_order/.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/observation_order/Active Behavior Orders.qview.xml b/nbri_ehr/resources/queries/study/observation_order/Active Behavior Orders.qview.xml index d4c5c11..c2c3d7e 100644 --- a/nbri_ehr/resources/queries/study/observation_order/Active Behavior Orders.qview.xml +++ b/nbri_ehr/resources/queries/study/observation_order/Active Behavior Orders.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/observation_order/Active Clinical Orders.qview.xml b/nbri_ehr/resources/queries/study/observation_order/Active Clinical Orders.qview.xml index 1638c87..f8bc797 100644 --- a/nbri_ehr/resources/queries/study/observation_order/Active Clinical Orders.qview.xml +++ b/nbri_ehr/resources/queries/study/observation_order/Active Clinical Orders.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/observation_order/Behavior Orders.qview.xml b/nbri_ehr/resources/queries/study/observation_order/Behavior Orders.qview.xml index 343d9b8..ad56df0 100644 --- a/nbri_ehr/resources/queries/study/observation_order/Behavior Orders.qview.xml +++ b/nbri_ehr/resources/queries/study/observation_order/Behavior Orders.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/observation_order/Clinical Orders.qview.xml b/nbri_ehr/resources/queries/study/observation_order/Clinical Orders.qview.xml index 5e1e749..24a0828 100644 --- a/nbri_ehr/resources/queries/study/observation_order/Clinical Orders.qview.xml +++ b/nbri_ehr/resources/queries/study/observation_order/Clinical Orders.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/pairingSummary/.qview.xml b/nbri_ehr/resources/queries/study/pairingSummary/.qview.xml index f0fdcf4..48d82d6 100644 --- a/nbri_ehr/resources/queries/study/pairingSummary/.qview.xml +++ b/nbri_ehr/resources/queries/study/pairingSummary/.qview.xml @@ -6,7 +6,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/pairingSummary/Active Pairing.qview.xml b/nbri_ehr/resources/queries/study/pairingSummary/Active Pairing.qview.xml index 14e0dfe..689f99f 100644 --- a/nbri_ehr/resources/queries/study/pairingSummary/Active Pairing.qview.xml +++ b/nbri_ehr/resources/queries/study/pairingSummary/Active Pairing.qview.xml @@ -9,7 +9,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/pairingSummary/Pairing History.qview.xml b/nbri_ehr/resources/queries/study/pairingSummary/Pairing History.qview.xml index 1a11617..81ee864 100644 --- a/nbri_ehr/resources/queries/study/pairingSummary/Pairing History.qview.xml +++ b/nbri_ehr/resources/queries/study/pairingSummary/Pairing History.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/pairings/.qview.xml b/nbri_ehr/resources/queries/study/pairings/.qview.xml index 26a8731..1a55dfa 100644 --- a/nbri_ehr/resources/queries/study/pairings/.qview.xml +++ b/nbri_ehr/resources/queries/study/pairings/.qview.xml @@ -6,7 +6,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/physicalExam/.qview.xml b/nbri_ehr/resources/queries/study/physicalExam/.qview.xml index caf3fb0..3fb7d6a 100644 --- a/nbri_ehr/resources/queries/study/physicalExam/.qview.xml +++ b/nbri_ehr/resources/queries/study/physicalExam/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/prc/.qview.xml b/nbri_ehr/resources/queries/study/prc/.qview.xml index f5ed643..089bd36 100644 --- a/nbri_ehr/resources/queries/study/prc/.qview.xml +++ b/nbri_ehr/resources/queries/study/prc/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/prcOverdue/.qview.xml b/nbri_ehr/resources/queries/study/prcOverdue/.qview.xml index b8bf120..7957d9f 100644 --- a/nbri_ehr/resources/queries/study/prcOverdue/.qview.xml +++ b/nbri_ehr/resources/queries/study/prcOverdue/.qview.xml @@ -3,7 +3,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/prcSchedule/.qview.xml b/nbri_ehr/resources/queries/study/prcSchedule/.qview.xml index 4604b77..227ae69 100644 --- a/nbri_ehr/resources/queries/study/prcSchedule/.qview.xml +++ b/nbri_ehr/resources/queries/study/prcSchedule/.qview.xml @@ -3,7 +3,7 @@ - + @@ -17,7 +17,7 @@ - + \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/prc_order/.qview.xml b/nbri_ehr/resources/queries/study/prc_order/.qview.xml index b9de6c1..8da38d4 100644 --- a/nbri_ehr/resources/queries/study/prc_order/.qview.xml +++ b/nbri_ehr/resources/queries/study/prc_order/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/prc_order_report/.qview.xml b/nbri_ehr/resources/queries/study/prc_order_report/.qview.xml index 62d20f0..3402400 100644 --- a/nbri_ehr/resources/queries/study/prc_order_report/.qview.xml +++ b/nbri_ehr/resources/queries/study/prc_order_report/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/treatmentSchedule/.qview.xml b/nbri_ehr/resources/queries/study/treatmentSchedule/.qview.xml index 4c57f0b..e773c14 100644 --- a/nbri_ehr/resources/queries/study/treatmentSchedule/.qview.xml +++ b/nbri_ehr/resources/queries/study/treatmentSchedule/.qview.xml @@ -3,7 +3,7 @@ - + @@ -22,7 +22,7 @@ - + \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/treatment_order/.qview.xml b/nbri_ehr/resources/queries/study/treatment_order/.qview.xml index 27608af..2b14b7b 100644 --- a/nbri_ehr/resources/queries/study/treatment_order/.qview.xml +++ b/nbri_ehr/resources/queries/study/treatment_order/.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/vitals/.qview.xml b/nbri_ehr/resources/queries/study/vitals/.qview.xml index f3b831c..b6bc349 100644 --- a/nbri_ehr/resources/queries/study/vitals/.qview.xml +++ b/nbri_ehr/resources/queries/study/vitals/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/weight/.qview.xml b/nbri_ehr/resources/queries/study/weight/.qview.xml index b27ffe4..b6d1a95 100644 --- a/nbri_ehr/resources/queries/study/weight/.qview.xml +++ b/nbri_ehr/resources/queries/study/weight/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml index 1d03f01..a4cd2a4 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml @@ -20,7 +20,6 @@ - diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml index bf236b7..2ae212d 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -626,30 +626,6 @@ - - - - varchar - http://cpas.labkey.com/Study#ParticipantId - - ptid - - - - timestamp - http://cpas.labkey.com/Study#VisitDate - http://cpas.labkey.com/Study#VisitDate - - - varchar - - - varchar - - - Historical Other - Captures miscellaneous historical health and research records for primates that do not fit other specific categories. -
HousingTracks housing assignments and location transfers for primates within the research facility. diff --git a/nbri_ehr/resources/reports/additionalReports.tsv b/nbri_ehr/resources/reports/additionalReports.tsv index 8eddd91..efaa2ad 100644 --- a/nbri_ehr/resources/reports/additionalReports.tsv +++ b/nbri_ehr/resources/reports/additionalReports.tsv @@ -49,5 +49,4 @@ behaviorRemarks Behavior query Behavior Remarks true study BehaviorClinRemarks clinObsBehavior Behavior query Observations true study behaviorObservations date false false qcstate/publicdata This report contains one record for each encounter with each animal, including surergies, exams, procedures, etc. clinremarks Clinical query Clinical Remarks true study ClinicalClinRemarks date false false qcstate/publicdata This report contains the clinical remarks entered about each animal physicalExam Clinical query Exam History True study physicalExam date false false qcstate/publicdata This report displays physical exam data for the selected animal -historicalOther General query Historical True study historicalOther date false false qcstate/publicdata This report displays historical events from legacy systems conceptionsByDam Reproductive Management query Conceptions by Dam true nbri_ehr ConceptionsByDam ConceptDate false false qcstate/publicdata This report displays conception records where the selected animal is the dam \ No newline at end of file diff --git a/nbri_ehr/resources/views/cageDetails.html b/nbri_ehr/resources/views/cageDetails.html index a0c5da2..fde98de 100644 --- a/nbri_ehr/resources/views/cageDetails.html +++ b/nbri_ehr/resources/views/cageDetails.html @@ -42,7 +42,7 @@ schemaName: 'ehr_lookups', queryName: 'cage', filterArray: detailFilterArray, - columns: 'cage,room,room/floor,room/floor/building,room/floor/building/area,outdoor', + columns: 'cage,room,room/building,room/building/area,outdoor', }, title: 'Cage Details', renderTo: 'cageDetails_' + webpart.wrapperDivId, @@ -56,7 +56,7 @@ queryName: 'cage', filterArray: detailFilterArray, sort: 'cage', - columns: 'cage,room,room/floor,room/floor/building,room/floor/building/area,outdoor', + columns: 'cage,room,room/building,room/building/area,outdoor', }).render('cageDetails_' + webpart.wrapperDivId); } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/history/NBRIHousingDataSource.java b/nbri_ehr/src/org/labkey/nbri_ehr/history/NBRIHousingDataSource.java index 91fdf41..0749f01 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/history/NBRIHousingDataSource.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/history/NBRIHousingDataSource.java @@ -36,7 +36,7 @@ public NBRIHousingDataSource(Module module) @Override protected Set getColumnNames() { - return PageFlowUtil.set("Id", "date", "cage/cage", "room/fullRoom", "reason", "remark"); + return PageFlowUtil.set("Id", "date", "cage/cage", "room/room", "reason", "remark"); } @Override @@ -44,7 +44,7 @@ protected String getHtml(Container c, Results rs, boolean redacted) throws SQLEx { StringBuilder sb = new StringBuilder(); - FieldKey room = FieldKey.fromString("room/fullRoom"); + FieldKey room = FieldKey.fromString("room/room"); FieldKey cage = FieldKey.fromString("cage/cage"); String value = "Unknown"; if (rs.hasColumn(cage) && rs.getObject(cage) != null) diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java index 38fd24d..b365680 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java @@ -93,16 +93,6 @@ public void customize(TableInfo table) customizeTasks(ti); } - if (matches(ti, "ehr_lookups", "rooms")) - { - customizeRooms(ti); - } - - if (matches(ti, "ehr_lookups", "floors")) - { - customizeFloors(ti); - } - if (matches(ti, "nbri_ehr", "necropsyTasks")) { addNecropsyReportLink(ti); @@ -581,60 +571,6 @@ public boolean isEditable() } } - private void customizeRooms(AbstractTableInfo ti) - { - ColumnInfo roomCol = ti.getColumn("name"); - ColumnInfo floorCol = ti.getColumn("floor"); - if (roomCol != null && floorCol != null && ti.getColumn("fullRoom") == null) - { - ExprColumn col = new ExprColumn(ti, new FieldKey(null, "fullRoom"), new SQLFragment("##ERROR"), JdbcType.VARCHAR, roomCol, floorCol) { - @Override - public SQLFragment getValueSql(String tableAlias) - { - // Need to subclass and override this function due to issue using ExprColumn.STR_TABLE_ALIAS with extensible columns - SQLFragment sql = new SQLFragment("(SELECT COALESCE(r.room, 'Room N/A') || ', ' || COALESCE(r.floor, 'Floor N/A') || ', ' || COALESCE(r.building, 'Building N/A') \n" + - " FROM (\n" + - " SELECT \n").append(roomCol.getValueSql(tableAlias)); - sql.append(" AS room,\n" + - " ff.name as floor,\n" + - " bb.description as building\n" + - " FROM ehr_lookups.floors ff \n" + - " JOIN ehr_lookups.buildings bb ON ff.building = bb.name\n" + - " WHERE ff.floor =\n").append(floorCol.getValueSql(tableAlias)); - sql.append(" ) r\n" + - " )"); - - return sql; - } - }; - col.setName("fullRoom"); - col.setLabel("Full Room"); - ti.addColumn(col); - } - } - - private void customizeFloors(AbstractTableInfo ti) - { - ColumnInfo floorCol = ti.getColumn("name"); - ColumnInfo bldgCol = ti.getColumn("building"); - if (floorCol != null && bldgCol != null && ti.getColumn("fullFloor") == null) - { - SQLFragment sql = new SQLFragment("(SELECT COALESCE(r.floor, 'Floor N/A') || ', ' || COALESCE(r.building, 'Building N/A') \n" + - " FROM (\n" + - " SELECT " + ExprColumn.STR_TABLE_ALIAS + ".name\n"); - sql.append(" AS floor,\n" + - " bb.description as building\n" + - " FROM ehr_lookups.buildings bb \n" + - " WHERE bb.name = " + ExprColumn.STR_TABLE_ALIAS + ".building\n"); - sql.append(" ) r\n" + - " )"); - - ExprColumn col = new ExprColumn(ti, "fullFloor", sql, JdbcType.VARCHAR, floorCol, bldgCol); - col.setLabel("Full Floor"); - ti.addColumn(col); - } - } - private void customizeTasks(AbstractTableInfo ti) { DetailsURL detailsURL = DetailsURL.fromString("/ehr/dataEntryFormDetails.view?formType=${formtype}&taskid=${taskid}"); @@ -777,7 +713,7 @@ public void doSharedCustomization(AbstractTableInfo ti) { UserSchema us = getEHRUserSchema(ti, "ehr_lookups"); col.setLabel("Room"); - col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "rooms", "room", "fullRoom")); + col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "rooms", "room", "room")); col.setURL(StringExpressionFactory.createURL("/nbri_ehr/cageDetails.view?room=${room}")); } if ("building".equalsIgnoreCase(col.getName()) && !ti.getName().equalsIgnoreCase("buildings")) @@ -790,7 +726,7 @@ public void doSharedCustomization(AbstractTableInfo ti) { UserSchema us = getEHRUserSchema(ti, "ehr_lookups"); col.setLabel("Floor"); - col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "floors", "floor", "fullFloor")); + col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "floors", "floor", "name")); } if ("area".equalsIgnoreCase(col.getName()) && !ti.getName().equalsIgnoreCase("areas")) { diff --git a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetHousing.tsv b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetHousing.tsv index 23969c0..487f797 100644 --- a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetHousing.tsv +++ b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetHousing.tsv @@ -1,9 +1,9 @@ -objectid Id QCStateLabel date enddate cage performedby -1 44444 Completed -1381d -1321d 11 - Rm 202B - ZZA 1004 -2 44444 Completed -1321d 11 - Rm 202B - ZZB 1004 -3 44446 Completed -1406d -1316d 11 - Rm 202B - ZZA 1004 -4 44446 Completed -1316d 11 - Rm 202B - ZZC 1004 -5 TEST4551032 Completed -6362d 11 - Rm 202B - ZZD 1004 -6 44442 Completed -6100d -5920d 11 - Rm 202B - ZZA 1004 -7 44442 Completed -5920d 11 - Rm 202B - ZZC 1004 -8 44443 Completed -6100d 11 - Rm 202B - ZZF 1004 +objectid Id QCStateLabel date enddate room cage performedby +1 44444 Completed -1381d -1321d TestBuilding-R1 TestBuilding-R1-C1 1004 +2 44444 Completed -1321d TestBuilding-R1 TestBuilding-R1-C2 1004 +3 44446 Completed -1406d -1316d TestBuilding-R1 TestBuilding-R1-C1 1004 +4 44446 Completed -1316d TestBuilding-R2 TestBuilding-R2-C3 1004 +5 TEST4551032 Completed -6362d TestBuilding-R3 TestBuilding-R3-C4 1004 +6 44442 Completed -6100d -5920d TestBuilding-R1 TestBuilding-R1-C1 1004 +7 44442 Completed -5920d TestBuilding-R2 TestBuilding-R2-C3 1004 +8 44443 Completed -6100d TestBuilding-R1 TestBuilding-R1-C2 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 7970f13..afaab10 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 @@ -34,6 +34,8 @@ import org.labkey.remoteapi.query.ImportDataCommand; import org.labkey.remoteapi.query.InsertRowsCommand; import org.labkey.remoteapi.query.RowsResponse; +import org.labkey.remoteapi.query.SelectRowsCommand; +import org.labkey.remoteapi.query.SelectRowsResponse; import org.labkey.remoteapi.security.CreateUserResponse; import org.labkey.test.Locator; import org.labkey.test.TestFileUtils; @@ -109,6 +111,28 @@ public class NBRI_EHRTest extends AbstractGenericEHRTest implements PostgresOnly // createTestSubjects so the clinical case form raises no warnings that would keep the validation banner up. private static final String taskGroupAnimalId = "TESTGRP9090"; + // Rooms are keyed by building and name, so every room fixture needs a building to hang off of. + // 'buildings' derives its key from the description, and 'SPF' is one of the areas seeded with the ehr_lookups schema. + private static final String BUILDING_ID = "TestBuilding"; + private static final String BUILDING_AREA = "SPF"; + + // Cage locations seeded by populateLocations, named for the room each one sits in. The cage trigger derives these + // from the room and cage, and housing records key off the location, so these are what belongs in a housing row's + // 'cage' field. datasetHousing.tsv spells the same values out, since a TSV cannot call cageLocation. + private static final String CAGE_IN_R1 = cageLocation("R1", "C1"); + private static final String CAGE_IN_R3 = cageLocation("R3", "C4"); + + // A group pen has no cage, so its location is the room key alone. Created by testGroupPenCagemates. + 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 + // 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"; + private static final String ROOMLESS_CAGE = cageLocation("R1", ROOMLESS_CAGE_NAME); + private static final String[] ROOMLESS_ANIMALS = {"CAGE0001", "CAGE0002"}; + private final String[] weightFields = {"Id", "date", "enddate", "project", "weight", FIELD_QCSTATELABEL, FIELD_OBJECTID, FIELD_LSID, "_recordid", "performedby"}; private final Object[] weightData1 = {getExpectedAnimalIDCasing("TESTSUBJECT1"), EHRClientAPIHelper.DATE_SUBSTITUTION, null, null, "12", EHRQCState.IN_PROGRESS.label, null, null, "_recordID", 1004}; @@ -246,20 +270,62 @@ private void populateFormulary() throws IOException, CommandException RowsResponse saveRowsResponse = insertRowsCommand.execute(getApiHelper().getConnection(), getContainerPath()); } + /** + * The key a room trigger derives for the given room name. Mirrors ehr_lookups/rooms.js. + */ + private static String roomKey(String roomName) + { + return BUILDING_ID + "-" + roomName; + } + + /** + * The location a cage trigger derives for the given room and cage. Mirrors ehr_lookups/cage.js. + */ + private static String cageLocation(String roomName, String cageName) + { + return roomKey(roomName) + "-" + cageName; + } + + /** + * Housing records store the derived room key, so the fixture rooms have to be named by key for the room + * lookups to resolve. The base implementation returns names that match no room in this study. + */ + @Override + protected String[] getRooms() + { + return new String[]{roomKey("R1"), roomKey("R2"), roomKey("R3")}; + } + + @LogMethod + private void populateBuildingRecords() throws Exception + { + InsertRowsCommand insertCmd = new InsertRowsCommand("ehr_lookups", "buildings"); + Map rowMap = new HashMap<>(); + // Supply the derived key rather than relying on the trigger to fill it, matching how the base class seeds rooms. + rowMap.put("name", BUILDING_ID); + rowMap.put("description", BUILDING_ID); + rowMap.put("area", BUILDING_AREA); + insertCmd.addRow(rowMap); + + insertCmd.execute(createDefaultConnection(), getContainerPath()); + } + @Override protected void populateRoomRecords() throws Exception { + populateBuildingRecords(); + InsertRowsCommand insertCmd = new InsertRowsCommand("ehr_lookups", "rooms"); Map rowMap = new HashMap<>(); rowMap.put("name", ROOM_ID); - rowMap.put("floor", "floor1"); + rowMap.put("building", BUILDING_ID); rowMap.put("housingType", 1); rowMap.put("housingCondition", 1); insertCmd.addRow(rowMap); rowMap = new HashMap<>(); rowMap.put("name", ROOM_ID2); - rowMap.put("floor", "floor2"); + rowMap.put("building", BUILDING_ID); rowMap.put("housingType", 1); rowMap.put("housingCondition", 1); insertCmd.addRow(rowMap); @@ -323,19 +389,21 @@ private void enableNotification(String notification) private void populateLocations() throws IOException, CommandException { goToEHRFolder(); + // BUILDING_ID is created by populateRoomRecords, which runs earlier as part of initProject. log("Inserting values in rooms"); InsertRowsCommand roomCmd = new InsertRowsCommand("ehr_lookups", "rooms"); - roomCmd.addRow(Map.of("name", "R1", "floor", "F1")); - roomCmd.addRow(Map.of("name", "R2", "floor", "F2")); - roomCmd.addRow(Map.of("name", "R3", "floor", "F3")); + roomCmd.addRow(Map.of("name", "R1", "building", BUILDING_ID)); + roomCmd.addRow(Map.of("name", "R2", "building", BUILDING_ID)); + roomCmd.addRow(Map.of("name", "R3", "building", BUILDING_ID)); roomCmd.execute(getApiHelper().getConnection(), getContainerPath()); + // 'location' is left out so the cage trigger derives it, exercising the same path production entry takes. log("Inserting values in cage"); InsertRowsCommand cageCmd = new InsertRowsCommand("ehr_lookups", "cage"); - cageCmd.addRow(Map.of("location", "L1", "cage", "C1", "room", "R1")); - cageCmd.addRow(Map.of("location", "L2", "cage", "C2", "room", "R1")); - cageCmd.addRow(Map.of("location", "L3", "cage", "C3", "room", "R2")); - cageCmd.addRow(Map.of("location", "L4", "cage", "C4", "room", "R3")); + cageCmd.addRow(Map.of("cage", "C1", "room", roomKey("R1"))); + cageCmd.addRow(Map.of("cage", "C2", "room", roomKey("R1"))); + cageCmd.addRow(Map.of("cage", "C3", "room", roomKey("R2"))); + cageCmd.addRow(Map.of("cage", "C4", "room", roomKey("R3"))); cageCmd.execute(getApiHelper().getConnection(), getContainerPath()); } @@ -443,10 +511,10 @@ protected void createTestSubjects() throws Exception log("Creating initial housing records"); fields = new String[]{"Id", "date", "enddate", "room", "cage", "performedby"}; data = new Object[][]{ - {SUBJECTS[0], pastDate1, pastDate2, getRooms()[0], CAGES[0], 1004}, - {SUBJECTS[0], pastDate2, null, getRooms()[0], CAGES[0], 1004}, - {SUBJECTS[1], pastDate1, pastDate2, getRooms()[0], CAGES[0], 1004}, - {SUBJECTS[1], pastDate2, null, getRooms()[2], CAGES[2], 1004} + {SUBJECTS[0], pastDate1, pastDate2, getRooms()[0], CAGE_IN_R1, 1004}, + {SUBJECTS[0], pastDate2, null, getRooms()[0], CAGE_IN_R1, 1004}, + {SUBJECTS[1], pastDate1, pastDate2, getRooms()[0], CAGE_IN_R1, 1004}, + {SUBJECTS[1], pastDate2, null, getRooms()[2], CAGE_IN_R3, 1004} }; insertCommand = getApiHelper().prepareInsertCommand("study", "Housing", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "Housing", new Filter("Id", StringUtils.join(SUBJECTS, ";"), Filter.Operator.IN)); @@ -490,7 +558,7 @@ protected void createTestSubjects() throws Exception fields = new String[]{"Id", "date", "enddate", "room", "cage", "performedby"}; data = new Object[][]{ - {taskGroupAnimalId, pastDate1, null, getRooms()[0], CAGES[0], 1004} + {taskGroupAnimalId, pastDate1, null, getRooms()[0], CAGE_IN_R1, 1004} }; insertCommand = getApiHelper().prepareInsertCommand("study", "Housing", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "Housing", new Filter("Id", taskGroupAnimalId)); @@ -1237,7 +1305,7 @@ public void createSubjectsForDeathForm() throws IOException, CommandException project.execute(getApiHelper().getConnection(), getContainerPath()); InsertRowsCommand housing = new InsertRowsCommand("study", "housing"); - housing.addRow(Map.of("Id", aliveAnimalId, "date", LocalDateTime.now().minusDays(10), "cage", "C4", "QCStateLabel", "Completed", "performedby", 1004)); + housing.addRow(Map.of("Id", aliveAnimalId, "date", LocalDateTime.now().minusDays(10), "room", getRooms()[2], "cage", CAGE_IN_R3, "QCStateLabel", "Completed", "performedby", 1004)); housing.execute(getApiHelper().getConnection(), getContainerPath()); log("Marking an animal dead"); @@ -1556,6 +1624,159 @@ public void testCalculatedAgeColumns() assertEquals("Calculated ages are incorrect", Arrays.asList("4.8", "4.0", "58.0"), row.subList(columnCount - 3, columnCount)); } + @Test + public void testRoomKeyDerivation() throws Exception + { + log("Verifying a room derives its key from its building and name"); + SelectRowsCommand selectCmd = new SelectRowsCommand("ehr_lookups", "rooms"); + selectCmd.setColumns(List.of("room", "name", "building")); + selectCmd.addFilter(new Filter("name", "R1")); + SelectRowsResponse response = selectCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + Assert.assertEquals("Expected exactly one room named R1", 1, response.getRows().size()); + Map room = response.getRows().get(0); + Assert.assertEquals("Room key should combine the building and the name", roomKey("R1"), room.get("room")); + Assert.assertEquals("Room should belong to the seeded building", BUILDING_ID, room.get("building")); + } + + @Test + public void testRoomRequiresBuilding() + { + log("Verifying a room cannot be created without a building"); + InsertRowsCommand insertCmd = new InsertRowsCommand("ehr_lookups", "rooms"); + insertCmd.addRow(Map.of("name", "NOBUILDING")); + + try + { + insertCmd.execute(getApiHelper().getConnection(), getContainerPath()); + Assert.fail("Room insert should have been rejected when no building was supplied"); + } + catch (IOException | CommandException e) + { + Assert.assertTrue("Unexpected failure inserting a room without a building: " + e.getMessage(), + e.getMessage() != null && e.getMessage().contains("Building is required")); + } + } + + @Test + public void testDuplicateBuildingRejected() + { + log("Verifying a second building cannot reuse an existing description"); + // The description alone is the building key now, so reusing the seeded building's description would collide. + InsertRowsCommand insertCmd = new InsertRowsCommand("ehr_lookups", "buildings"); + insertCmd.addRow(Map.of("description", BUILDING_ID, "area", BUILDING_AREA)); + + try + { + insertCmd.execute(getApiHelper().getConnection(), getContainerPath()); + Assert.fail("Building insert should have been rejected when the description was already in use"); + } + catch (IOException | CommandException e) + { + Assert.assertTrue("Unexpected failure inserting a duplicate building: " + e.getMessage(), + e.getMessage() != null && e.getMessage().contains("already exists")); + } + } + + @Test + public void testGroupPenCagemates() throws Exception + { + String penRoom = roomKey(PEN_ROOM_NAME); + + log("Creating a group pen, whose location is the room with no cage"); + InsertRowsCommand roomCmd = new InsertRowsCommand("ehr_lookups", "rooms"); + roomCmd.addRow(Map.of("name", PEN_ROOM_NAME, "building", BUILDING_ID)); + roomCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + // With no cage supplied the trigger derives the location as the room key alone. + InsertRowsCommand penCmd = new InsertRowsCommand("ehr_lookups", "cage"); + penCmd.addRow(Map.of("room", penRoom)); + penCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + 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); + + log("Verifying penned animals resolve as each other's cagemates"); + assertCagemates(PEN_ANIMALS[0], 2, PEN_ANIMALS[1]); + } + + @Test + public void testCagematesWithoutRoom() throws Exception + { + log("Creating an unoccupied cage"); + InsertRowsCommand cageCmd = new InsertRowsCommand("ehr_lookups", "cage"); + cageCmd.addRow(Map.of("cage", ROOMLESS_CAGE_NAME, "room", roomKey("R1"))); + cageCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + 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); + + log("Verifying caged animals resolve as each other's cagemates without a room"); + assertCagemates(ROOMLESS_ANIMALS[0], 2, ROOMLESS_ANIMALS[1]); + } + + /** + * Creates living demographics records for the given animals, replacing any left behind by an earlier run. + */ + private void createAliveAnimals(String[] animalIds) throws Exception + { + log("Creating animals " + StringUtils.join(animalIds, ", ")); + String[] fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; + Object[][] data = new Object[animalIds.length][]; + for (int i = 0; i < animalIds.length; i++) + { + data[i] = new Object[]{animalIds[i], "Rhesus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}; + } + + SimplePostCommand insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", StringUtils.join(animalIds, ";"), Filter.Operator.IN)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + } + + /** + * 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. + */ + private void houseAnimals(String[] animalIds, String room, String cage) throws Exception + { + String[] fields = new String[]{"Id", "date", "enddate", "room", "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}; + } + + SimplePostCommand insertCommand = getApiHelper().prepareInsertCommand("study", "Housing", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "Housing", new Filter("Id", StringUtils.join(animalIds, ";"), Filter.Operator.IN)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + } + + /** + * Asserts the cagemates report resolves the expected companions for an animal. A null total means the join matched + * nothing at all, which is reported as its own failure rather than as an unexpected count. + */ + private void assertCagemates(String animalId, int expectedTotal, String expectedCompanion) throws Exception + { + SelectRowsCommand selectCmd = new SelectRowsCommand("study", "demographicsCagemates"); + selectCmd.addFilter(new Filter("Id", animalId)); + SelectRowsResponse response = selectCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + Assert.assertEquals("Expected one cagemates row for " + animalId, 1, response.getRows().size()); + Map cagemates = response.getRows().get(0); + Assert.assertNotNull("Cagemates resolved no location for " + animalId, cagemates.get("total")); + Assert.assertEquals("Unexpected cagemate count for " + animalId, expectedTotal, ((Number) cagemates.get("total")).intValue()); + Assert.assertTrue("Cagemate list should name " + expectedCompanion + ", was: " + cagemates.get("animals"), + String.valueOf(cagemates.get("animals")).contains(expectedCompanion)); + } + @Test public void testLookupPage() throws Exception {