From 30ec136d756c776439bc4c08c1a3fec0434d7caa Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 30 Aug 2026 17:03:40 -0600 Subject: [PATCH 1/2] Derive observation type from the observation type's category The Observations form offers every observation type, so it cannot set the observation's type up front and stored everything as Clinical, dropping behavior observations out of the behavior views; the trigger script now derives the type from the selected type's category. The Daily Observations shortcut becomes opt-in per form section so it appears only on the clinical forms. Rows already saved with a mismatched type need a one-time data fix. --- .../queries/study/clinical_observations.js | 7 ++ .../nbri_ehr/buttons/clinicalObsGridButton.js | 59 ------------- .../web/nbri_ehr/model/sources/ObsDefaults.js | 7 ++ .../form/NBRIBehaviorRoundsFormType.java | 2 +- .../form/NBRIBehavioralCasesFormType.java | 2 +- .../form/NBRIBulkBehaviorFormType.java | 4 +- .../form/NBRIBulkClinicalFormType.java | 2 +- .../dataentry/form/NBRICasesFormType.java | 2 +- .../NBRIClinicalObservationsFormType.java | 2 +- .../form/NBRIClinicalRoundsFormType.java | 2 +- .../NBRIClinicalObservationsFormSection.java | 18 ++-- .../NBRIObservationOrdersFormSection.java | 1 - .../nbri_ehr/query/NBRI_EHRTriggerHelper.java | 21 +++++ .../tests.nbri_ehr/NBRI_EHRTest.java | 85 +++++++++++++++++++ 14 files changed, 135 insertions(+), 79 deletions(-) delete mode 100644 nbri_ehr/resources/web/nbri_ehr/buttons/clinicalObsGridButton.js diff --git a/nbri_ehr/resources/queries/study/clinical_observations.js b/nbri_ehr/resources/queries/study/clinical_observations.js index 9e54948..ddafdc5 100644 --- a/nbri_ehr/resources/queries/study/clinical_observations.js +++ b/nbri_ehr/resources/queries/study/clinical_observations.js @@ -45,6 +45,13 @@ function onUpsert(helper, scriptErrors, row, oldRow) { EHR.Server.Utils.addError(scriptErrors, 'remark', "You selected 'Yes' for " + row.category + ", please explain in the Remark", "WARN"); } + // Always derive the type from the observation type's category rather than trusting the incoming value. + // The Observations form leaves it blank because it offers every type; the other forms set it explicitly, + // but their type pickers are filtered to the categories that agree with the value they set, so deriving + // here gives them the same answer. Deriving unconditionally also re-derives when a re-opened draft or a + // saved template carries a type left over from a different category. + row.type = triggerHelper.getObservationTypeCategory(row.category) === 'Behavior' ? 'Behavior' : 'Clinical'; + // Handle scheduled observations if (!helper.isValidateOnly() && row.scheduledDate) { var qc; diff --git a/nbri_ehr/resources/web/nbri_ehr/buttons/clinicalObsGridButton.js b/nbri_ehr/resources/web/nbri_ehr/buttons/clinicalObsGridButton.js deleted file mode 100644 index be6c858..0000000 --- a/nbri_ehr/resources/web/nbri_ehr/buttons/clinicalObsGridButton.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2026 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 - */ -EHR.DataEntryUtils.registerGridButton('NBRI_AUTO_POPULATE_DAILY_OBS', function(config){ - return Ext4.Object.merge({ - text: 'Auto Populate Clinical Obs', - xtype: 'button', - hidden: true, - listeners: { - render: function(btn){ - const id = LABKEY.ActionURL.getParameter('id'); - const caseid = LABKEY.ActionURL.getParameter('caseid'); - const scheduledDate = LABKEY.ActionURL.getParameter('scheduledDate'); - const scheduled = id && caseid && scheduledDate; - - LABKEY.Query.selectRows({ - schemaName: 'ehr', - queryName: 'observation_types', - ignoreFilter: true, - success: function (results) { - var grid = btn.up('gridpanel'); - if (grid?.store?.data?.getCount() === 0) { - if (results?.rows?.length > 0) { - for (var i = 0; i < results.rows.length; i++) { - var row = results.rows[i]; - if (row.value === 'Verified Id?' || row.value === 'Stool' || row.value === 'Activity' || - row.value === 'Appetite' || row.value === 'Hydration' || row.value === 'BCS') { - - var newRecord = grid.store.createModel({}); - newRecord.set({ - category: row.value, - }); - - if (scheduled) { - newRecord.set('Id', id); - newRecord.set('caseid', caseid); - newRecord.set('scheduledDate', scheduledDate); - } - grid.store.add(newRecord); - } - } - - if (scheduled) { - this.addEvents('animalchange'); - this.enableBubble('animalchange'); - this.fireEvent('animalchange', id); - grid.fireEvent('panelDataChange'); - } - } - } - }, - scope: this - }); - } - } - }, config); -}); \ No newline at end of file diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/ObsDefaults.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/ObsDefaults.js index bd9f4ce..3569d57 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/ObsDefaults.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/ObsDefaults.js @@ -6,6 +6,13 @@ EHR.model.DataModelManager.registerMetadata('ObsDefaults', { byQuery: { 'study.clinical_observations': { + // This form offers every observation type, so it can't know the observation's type up front. + // Clearing the default inherited from ClinicalDefaults lets the trigger script derive it + // from the selected type's category. + type: { + hidden: true, + defaultValue: null + }, category: { lookup: { columns: 'value,description', diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBehaviorRoundsFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBehaviorRoundsFormType.java index b85e71a..e969398 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBehaviorRoundsFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBehaviorRoundsFormType.java @@ -46,7 +46,7 @@ public NBRIBehaviorRoundsFormType(DataEntryFormContext ctx, Module owner) new NBRIAnimalDetailsFormSection(), new NBRICaseTemplateFormSection("Case Template", "Case Template", "nbri_ehr-casetemplatepanel", Arrays.asList(ClientDependency.supplierFromPath("nbri_ehr/panel/CaseTemplatePanel.js"))), new NBRICasesFormPanelSection("Behavior Case", ctx, true), - new NBRIClinicalObservationsFormSection(true, "cases"), + new NBRIClinicalObservationsFormSection(null, true, "cases"), new NBRITreatmentGivenFormSection(true, "cases") )); diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBehavioralCasesFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBehavioralCasesFormType.java index 3bcc93a..d0bc2c5 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBehavioralCasesFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBehavioralCasesFormType.java @@ -50,7 +50,7 @@ public NBRIBehavioralCasesFormType(DataEntryFormContext ctx, Module owner) new NBRICaseTemplateFormSection("Case Template", "Case Template", "nbri_ehr-casetemplatepanel", Arrays.asList(ClientDependency.supplierFromPath("nbri_ehr/panel/CaseTemplatePanel.js"))), new NBRICasesFormPanelSection("Behavior Case", ctx, true), new NBRIClinicalRemarksFormPanelSection(true, "cases", "Behavior Assessment", ctx, true), - new NBRIClinicalObservationsFormSection(true, "cases"), + new NBRIClinicalObservationsFormSection(null, true, "cases"), new NBRIObservationOrdersFormSection(null, true, "cases"), new NBRITreatmentGivenFormSection(true, "cases"), new NBRITreatmentOrderFormSection(true, "cases") diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkBehaviorFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkBehaviorFormType.java index 35c87c0..0603876 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkBehaviorFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkBehaviorFormType.java @@ -45,8 +45,8 @@ public NBRIBulkBehaviorFormType(DataEntryFormContext ctx, Module owner) new NBRIClinicalRemarksFormSection("Behavior Assessment", ctx.getContainer().hasPermission(ctx.getUser(), NBRIEHRVetTechPermission.class), ctx.getContainer().hasPermission(ctx.getUser(), EHRVeterinarianPermission.class), ctx.getContainer().hasPermission(ctx.getUser(), AdminPermission.class)), - new NBRIClinicalObservationsFormSection(false, null), - new NBRIObservationOrdersFormSection("NBRI_DAILY_CLINICAL_OBS_ORDERS", false, null), + new NBRIClinicalObservationsFormSection(null, false, null), + new NBRIObservationOrdersFormSection(null, false, null), new NBRITreatmentGivenFormSection(), new NBRITreatmentOrderFormSection() )); diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkClinicalFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkClinicalFormType.java index a4317ff..e53b02e 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkClinicalFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkClinicalFormType.java @@ -51,7 +51,7 @@ public NBRIBulkClinicalFormType(DataEntryFormContext ctx, Module owner) ctx.getContainer().hasPermission(ctx.getUser(), EHRVeterinarianPermission.class), ctx.getContainer().hasPermission(ctx.getUser(), AdminPermission.class)), new NBRIWeightFormSection(true, true), - new NBRIClinicalObservationsFormSection(false, null), + new NBRIClinicalObservationsFormSection("NBRI_DAILY_CLINICAL_OBS", false, null), new NBRIObservationOrdersFormSection("NBRI_DAILY_CLINICAL_OBS_ORDERS", false, null), new NBRIProcedureFormSection(), new NBRIProcedureOrderFormSection(), diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRICasesFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRICasesFormType.java index 04c3292..5360f51 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRICasesFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRICasesFormType.java @@ -58,7 +58,7 @@ public NBRICasesFormType(DataEntryFormContext ctx, Module owner) new NBRICasesFormPanelSection("Clinical Case", ctx, false), new NBRIClinicalRemarksFormPanelSection(true, "cases", "Clinical Remarks", ctx, false), new NBRIWeightFormSection(true, false, true, "cases"), - new NBRIClinicalObservationsFormSection(true, "cases"), + new NBRIClinicalObservationsFormSection("NBRI_DAILY_CLINICAL_OBS", true, "cases"), new NBRIObservationOrdersFormSection(null, true, "cases"), new NBRIProcedureFormSection(true, "cases"), new NBRIProcedureOrderFormSection(true, "cases"), diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIClinicalObservationsFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIClinicalObservationsFormType.java index 9a2094f..609f46c 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIClinicalObservationsFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIClinicalObservationsFormType.java @@ -37,7 +37,7 @@ public NBRIClinicalObservationsFormType(DataEntryFormContext ctx, Module owner) super(ctx, owner, NAME, NAME, "Clinical", Arrays.asList( new NBRITaskFormSection(), new NBRIAnimalDetailsFormSection(), - new NBRIClinicalObservationsFormSection(false, false), + new NBRIClinicalObservationsFormSection("NBRI_DAILY_CLINICAL_OBS", false), new NBRIWeightFormSection(true, true) )); diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIClinicalRoundsFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIClinicalRoundsFormType.java index 2864256..b3ac23b 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIClinicalRoundsFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIClinicalRoundsFormType.java @@ -50,7 +50,7 @@ public NBRIClinicalRoundsFormType(DataEntryFormContext ctx, Module owner) new NBRICaseTemplateFormSection("Case Template", "Case Template", "nbri_ehr-casetemplatepanel", Arrays.asList(ClientDependency.supplierFromPath("nbri_ehr/panel/CaseTemplatePanel.js"))), new NBRICasesFormPanelSection("Clinical Case", ctx, false), new NBRIWeightFormSection(true, false, true, "cases"), - new NBRIClinicalObservationsFormSection(true, "cases"), + new NBRIClinicalObservationsFormSection("NBRI_DAILY_CLINICAL_OBS", true, "cases"), new NBRIProcedureFormSection(true, "cases"), new NBRITreatmentGivenFormSection(true, "cases"), new NBRIVitalsFormSection(true, "cases"), diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIClinicalObservationsFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIClinicalObservationsFormSection.java index b435c34..a9b95fb 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIClinicalObservationsFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIClinicalObservationsFormSection.java @@ -22,24 +22,23 @@ public class NBRIClinicalObservationsFormSection extends BaseFormSection { public static final String LABEL = "Observations"; - private boolean _autoPopulateDailyObs; + private final String _dailyObsOption; - public NBRIClinicalObservationsFormSection(boolean autoPopulateDailyObs, boolean initCollapsed) + public NBRIClinicalObservationsFormSection(String dailyObsOption, boolean initCollapsed) { super("study", "clinical_observations", LABEL, "ehr-clinicalobservationgridpanel", true, initCollapsed, true); - _autoPopulateDailyObs = autoPopulateDailyObs; + _dailyObsOption = dailyObsOption; addClientDependency(ClientDependency.supplierFromPath("ehr/plugin/ClinicalObservationsCellEditing.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/data/ClinicalObservationClientStore.js")); addClientDependency(ClientDependency.supplierFromPath("ehr/grid/ClinicalObservationGridPanel.js")); - addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/buttons/clinicalObsGridButton.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/buttons/addClinicalObsButton.js")); setClientStoreClass("NBRI_EHR.data.ClinicalObservationsClientStore"); } - public NBRIClinicalObservationsFormSection(boolean isChild, String parentQueryName) + public NBRIClinicalObservationsFormSection(String dailyObsOption, boolean isChild, String parentQueryName) { - this(false, true); + this(dailyObsOption, true); if (isChild && null != parentQueryName) { @@ -57,12 +56,9 @@ public List getTbarButtons() { List defaults = super.getTbarButtons(); - if (_autoPopulateDailyObs) + if (_dailyObsOption != null) { - defaults.add("NBRI_AUTO_POPULATE_DAILY_OBS"); - } - else { - defaults.add("NBRI_DAILY_CLINICAL_OBS"); + defaults.add(_dailyObsOption); } return defaults; diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIObservationOrdersFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIObservationOrdersFormSection.java index 9a2171e..82de7de 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIObservationOrdersFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIObservationOrdersFormSection.java @@ -32,7 +32,6 @@ public NBRIObservationOrdersFormSection(String dailyObsOption, boolean initColla _dailyObsOption = dailyObsOption; addClientDependency(ClientDependency.supplierFromPath("ehr/plugin/ClinicalObservationsCellEditing.js")); addClientDependency(ClientDependency.supplierFromPath("ehr/grid/ClinicalObservationGridPanel.js")); - addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/buttons/clinicalObsGridButton.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/buttons/addClinicalObsButton.js")); setClientStoreClass("NBRI_EHR.data.ObsOrdersClientStore"); 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 d88b672..b1ed437 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 @@ -80,6 +80,7 @@ public class NBRI_EHRTriggerHelper private User _user; private static final Logger _log = LogManager.getLogger(NBRI_EHRTriggerHelper.class); private final Map _cachedDrugFormulary = new HashMap<>(); + private final Map _cachedObservationTypeCategories = new HashMap<>(); // Maps an originating observation order's taskid to the task its scheduled observations are grouped under, // for the duration of a single save batch (the same helper instance is reused across rows in the batch). @@ -924,6 +925,26 @@ public void ensureDailyClinicalObservationOrders(String id, String caseid, final } } + /** + * Returns the category of an observation type from ehr.observation_types, or null when the type has no + * category or is not found. Cached for the life of the save batch. + */ + public String getObservationTypeCategory(String observationType) + { + if (observationType == null) + return null; + + if (!_cachedObservationTypeCategories.containsKey(observationType)) + { + TableInfo ti = getTableInfo("ehr", "observation_types"); + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("value"), observationType); + List categories = new TableSelector(ti, Collections.singleton("category"), filter, null).getArrayList(String.class); + _cachedObservationTypeCategories.put(observationType, categories.isEmpty() ? null : categories.get(0)); + } + + return _cachedObservationTypeCategories.get(observationType); + } + // This helper function propagates clinical observations through clinical cases public Map handleScheduledObservations(Map row, String qcstate, String orderTasks) throws SQLException, BatchValidationException, QueryUpdateServiceException, DuplicateKeyException { 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 9fbbae2..b93f92e 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 @@ -111,6 +111,9 @@ public class NBRI_EHRTest extends AbstractGenericEHRTest implements PostgresOnly // 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"; + // Dedicated animal for testObservationTypeDerivedFromCategory; provisioned the same way so the Observations + // form can be submitted final in one step. + private static final String obsTypeAnimalId = "TESTOBSTYPE9191"; // 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. @@ -575,6 +578,32 @@ protected void createTestSubjects() throws Exception getApiHelper().deleteAllRecords("study", "Assignment", new Filter("Id", taskGroupAnimalId)); getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + // Fully provision the observation-type test animal for the same reason. + log("Creating observation type test subject"); + fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; + data = new Object[][]{ + {obsTypeAnimalId, "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", obsTypeAnimalId)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + + fields = new String[]{"Id", "date", "enddate", "room", "cage", "performedby"}; + data = new Object[][]{ + {obsTypeAnimalId, pastDate1, null, getRooms()[0], CAGE_IN_R1, 1004} + }; + insertCommand = getApiHelper().prepareInsertCommand("study", "Housing", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "Housing", new Filter("Id", obsTypeAnimalId)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + + fields = new String[]{"Id", "date", "enddate", "project", "performedby"}; + data = new Object[][]{ + {obsTypeAnimalId, pastDate1, null, PROJECTS[0], 1004} + }; + insertCommand = getApiHelper().prepareInsertCommand("study", "Assignment", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "Assignment", new Filter("Id", obsTypeAnimalId)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + primeCaches(); } @@ -1315,7 +1344,13 @@ public void testScheduledObservationTaskGrouping() Map entriesPerCategory = new HashMap<>(); for (Map row : getClinicalObservations(animalId)) + { entriesPerCategory.merge(String.valueOf(row.get("category")), 1, Integer::sum); + // A scheduled observation takes its type from the originating order, which the daily clinical + // observation orders create as Clinical. + Assert.assertEquals("Scheduled observation for category " + row.get("category") + " should be Clinical", + "Clinical", String.valueOf(row.get("type"))); + } Assert.assertEquals("Expected the six daily observation categories", NBRI_DAILY_OBS_VALUES.size(), entriesPerCategory.size()); entriesPerCategory.forEach((category, count) -> Assert.assertEquals("Expected two entries (one per matching order) for category " + category, Integer.valueOf(2), count)); @@ -1407,6 +1442,56 @@ private int countObservationsForTask(String taskId) return executeSelectRowCommand("study", "clinical_observations", ContainerFilter.Current, "/" + getContainerPath(), List.of(new Filter("taskid", taskId))).getRowCount().intValue(); } + // Two ehr.observation_types values on either side of the derivation: the first has no category, the second + // is categorized as Behavior. Both use a free-text Observation/Score editor, so neither depends on an + // ehr_lookups value list being populated. + private static final String UNCATEGORIZED_OBS_TYPE = "Mass"; + private static final String BEHAVIOR_OBS_TYPE = "General Behavior Observation"; + + @Test + public void testObservationTypeDerivedFromCategory() + { + String animalId = obsTypeAnimalId; + + // The Observations form offers every observation type, so it cannot set the observation's type up + // front; the trigger script derives it from the selected type's category. A type categorized as + // Behavior must be stored as a Behavior observation and everything else as Clinical, otherwise the + // entry drops out of the behavior views (study.behaviorObservations filters on type = 'Behavior'). + log("Entering an uncategorized and a Behavior-categorized observation type on the Observations form"); + gotoEnterData(); + waitAndClickAndWait(Locator.linkWithText("Observations")); + + Ext4GridRef observations = _helper.getExt4GridForFormSection("Observations"); + addObservationRow(observations, animalId, UNCATEGORIZED_OBS_TYPE, "3 cm mass on left arm"); + addObservationRow(observations, animalId, BEHAVIOR_OBS_TYPE, "Pacing observed"); + submitForm("Submit Final", "Finalize"); + + Map typeByCategory = new HashMap<>(); + for (Map row : getClinicalObservations(animalId)) + typeByCategory.put(String.valueOf(row.get("category")), String.valueOf(row.get("type"))); + + Assert.assertEquals("Expected exactly the two entered observations for " + animalId, + Set.of(UNCATEGORIZED_OBS_TYPE, BEHAVIOR_OBS_TYPE), typeByCategory.keySet()); + Assert.assertEquals("An uncategorized observation type should be stored as a Clinical observation", + "Clinical", typeByCategory.get(UNCATEGORIZED_OBS_TYPE)); + Assert.assertEquals("A Behavior-categorized observation type should be stored as a Behavior observation", + "Behavior", typeByCategory.get(BEHAVIOR_OBS_TYPE)); + } + + // Appends a row to an Observations grid and fills in the fields the trigger script needs to accept it: an + // animal, an observation type (the grid's "category"), and an Observation/Score plus remark, since an entry + // with neither raises a WARN that would disable Submit Final. The row index is read back from the grid + // rather than assumed, so this works whether or not the form starts with rows of its own. + private void addObservationRow(Ext4GridRef observations, String animalId, String category, String observation) + { + _helper.addRecordToGrid(observations); + int row = observations.getRowCount(); + observations.setGridCell(row, "Id", animalId); + observations.setGridCell(row, "category", category); + observations.setGridCell(row, "observation", observation); + observations.setGridCellJS(row, "remark", "remark for " + category); + } + @Test public void testObservationBulkEdit() { From d910d040f448d4a2dd82d831f2cfc000073a0df3 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 30 Aug 2026 17:05:26 -0600 Subject: [PATCH 2/2] Match cagemates on active housing rows with a known cage A row with no cage never resolved to a location, so every unresolved row grouped together as each other's cagemates. --- nbri_ehr/resources/queries/study/demographicsCagemates.sql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nbri_ehr/resources/queries/study/demographicsCagemates.sql b/nbri_ehr/resources/queries/study/demographicsCagemates.sql index 0450dc2..4be95d9 100644 --- a/nbri_ehr/resources/queries/study/demographicsCagemates.sql +++ b/nbri_ehr/resources/queries/study/demographicsCagemates.sql @@ -25,10 +25,12 @@ JOIN study.housing h2 -- cagemates. Room is not consulted, since it is derived from this same id and so can never distinguish two rows. ON (h.cage = h2.cage AND h2.Id.demographics.calculated_status = 'Alive' - AND h2.enddateTimeCoalesced >= now() + AND h2.isActive = true AND h2.qcstate.publicdata = true) -WHERE h.enddateTimeCoalesced >= now() +-- a null location never resolved, so the row gets no cagemates rather than grouping with every other unresolved row +WHERE h.cage IS NOT NULL +AND h.isActive = true AND h.qcstate.publicdata = true GROUP BY h.id, h.cage