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/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 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() {