From 6e74e4f44cf1c02b1164aff7c53e56ad543db20b Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 16 Jun 2026 12:33:52 -0600 Subject: [PATCH 1/3] Group scheduled observations by order with a new task per group When clinical observations are entered from the observation schedule and multiple orders match on category and scheduled date/time, group the resulting clinical_observations entries by their originating order's taskid and assign one task per group. The first order group reuses the form's own task; each additional distinct order group gets a freshly created task cloned from the form task. This means no order's existing schedule task id is reused, while the form task is reused for the first group so it is never left empty. Also add the caseId/problemCategory column to the clinical_observations default view. --- .../queries/study/clinical_observations.js | 2 + .../study/clinical_observations/.qview.xml | 1 + .../nirc_ehr/query/NIRC_EHRTriggerHelper.java | 94 ++++++++++++++++++- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/nirc_ehr/resources/queries/study/clinical_observations.js b/nirc_ehr/resources/queries/study/clinical_observations.js index 38f07036..6c50146b 100644 --- a/nirc_ehr/resources/queries/study/clinical_observations.js +++ b/nirc_ehr/resources/queries/study/clinical_observations.js @@ -12,6 +12,7 @@ var triggerHelper = new org.labkey.nirc_ehr.query.NIRC_EHRTriggerHelper(LABKEY.S function onInit(event, helper) { helper.decodeExtraContextProperty('orderTasksInTransaction'); + triggerHelper.clearScheduledObsTaskMap(); } function onUpsert(helper, scriptErrors, row, oldRow) { @@ -67,6 +68,7 @@ function onUpsert(helper, scriptErrors, row, oldRow) { row.orderid = orderData.orderId; row.area = orderData.area; row.type = orderData.type; + row.taskid = orderData.taskId; } } } diff --git a/nirc_ehr/resources/queries/study/clinical_observations/.qview.xml b/nirc_ehr/resources/queries/study/clinical_observations/.qview.xml index a48d5a51..daaa82db 100644 --- a/nirc_ehr/resources/queries/study/clinical_observations/.qview.xml +++ b/nirc_ehr/resources/queries/study/clinical_observations/.qview.xml @@ -14,5 +14,6 @@ + \ No newline at end of file diff --git a/nirc_ehr/src/org/labkey/nirc_ehr/query/NIRC_EHRTriggerHelper.java b/nirc_ehr/src/org/labkey/nirc_ehr/query/NIRC_EHRTriggerHelper.java index 22112ed5..a4b08fd2 100644 --- a/nirc_ehr/src/org/labkey/nirc_ehr/query/NIRC_EHRTriggerHelper.java +++ b/nirc_ehr/src/org/labkey/nirc_ehr/query/NIRC_EHRTriggerHelper.java @@ -40,6 +40,7 @@ import org.labkey.api.util.PageFlowUtil; import org.labkey.nirc_ehr.NIRCOrchardFileGenerator; import org.labkey.nirc_ehr.NIRC_EHRManager; +import org.labkey.nirc_ehr.dataentry.form.NIRCClinicalObservationsFormType; import org.labkey.nirc_ehr.notification.NIRCClinicalMoveNotification; import org.labkey.nirc_ehr.notification.NIRCDeathNotification; import org.labkey.nirc_ehr.notification.NIRCPregnancyOutcomeNotification; @@ -66,6 +67,10 @@ public class NIRC_EHRTriggerHelper private static final Logger _log = LogManager.getLogger(NIRC_EHRTriggerHelper.class); private final Map _cachedDrugFormulary = 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). + private final Map _scheduledObsTaskMap = new HashMap<>(); + private final SimpleDateFormat _dateFormat; public NIRC_EHRTriggerHelper(int userId, String containerId) @@ -798,6 +803,10 @@ public Map handleScheduledObservations(Map row, { Map order = orders[i]; + // Group every entry by the originating order's taskid into a new task per group. + String orderTaskId = ConvertHelper.convert(order.get("taskid"), String.class); + String groupTaskId = resolveGroupTaskId(orderTaskId, taskid, qcstate); + // First order we find will fill out the information in the row passing through the trigger if (i == 0) { @@ -806,6 +815,7 @@ public Map handleScheduledObservations(Map row, triggerOrder.put("area", order.get("area")); triggerOrder.put("orderId", order.get("objectid")); triggerOrder.put("type", order.get("type")); + triggerOrder.put("taskId", groupTaskId); continue; } @@ -822,7 +832,7 @@ public Map handleScheduledObservations(Map row, obsRow.put("performedBy", performedBy); obsRow.put("orderId", order.get("objectid")); obsRow.put("type", order.get("type")); - obsRow.put("taskid", order.get("taskid")); + obsRow.put("taskid", groupTaskId); List> rows = new ArrayList<>(); rows.add(obsRow); @@ -837,6 +847,88 @@ public Map handleScheduledObservations(Map row, return triggerOrder; } + /** + * Resets the per-batch grouping map. Called from the clinical_observations onInit trigger so no + * grouping state can leak between save batches. + */ + public void clearScheduledObsTaskMap() + { + _scheduledObsTaskMap.clear(); + } + + /** + * Resolves the task that a scheduled observation entry should be grouped under, keyed by the + * originating observation order's taskid. The first distinct order taskid seen in a save batch + * reuses the form's own task ({@code formTaskId}); each subsequent distinct order taskid gets a + * freshly created task that clones the form task. This groups all observations that came from the + * same order under one task, with a new task per additional group, while reusing the form's task + * for the first group so it is not left empty. + */ + private String resolveGroupTaskId(String orderTaskId, String formTaskId, String qcstate) throws SQLException, BatchValidationException, QueryUpdateServiceException, DuplicateKeyException + { + // Defensive: an order with no taskid can't be grouped, so fall back to the form's task. + if (orderTaskId == null) + return formTaskId; + + if (_scheduledObsTaskMap.containsKey(orderTaskId)) + return _scheduledObsTaskMap.get(orderTaskId); + + // Reuse the form's already-created task for the first group; create new tasks for the rest. + String groupTaskId = _scheduledObsTaskMap.isEmpty() ? formTaskId : createTaskFromForm(formTaskId, qcstate); + _scheduledObsTaskMap.put(orderTaskId, groupTaskId); + return groupTaskId; + } + + /** + * Creates a new ehr.tasks record for a group of scheduled observations, cloning the form's task + * ({@code formTaskId}) so the new task carries the same title/form/category/etc. Returns the new taskid. + */ + private String createTaskFromForm(String formTaskId, String qcstate) throws SQLException, BatchValidationException, QueryUpdateServiceException, DuplicateKeyException + { + String newTaskId = new GUID().toString(); + TableInfo tasksTi = getTableInfo("ehr", "tasks"); + + Map formTask = null; + if (formTaskId != null) + { + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("taskid"), formTaskId); + formTask = new TableSelector(tasksTi, PageFlowUtil.set("title", "formtype", "category", "qcstate", "assignedto", "duedate", "caseid", "description"), filter, null).getMap(); + } + + Map taskRow = new CaseInsensitiveHashMap<>(); + taskRow.put("taskid", newTaskId); + if (formTask != null) + { + taskRow.put("title", formTask.get("title")); + taskRow.put("formtype", formTask.get("formtype")); + taskRow.put("category", formTask.get("category")); + taskRow.put("qcstate", formTask.get("qcstate")); + taskRow.put("assignedto", formTask.get("assignedto")); + taskRow.put("duedate", formTask.get("duedate")); + taskRow.put("caseid", formTask.get("caseid")); + taskRow.put("description", formTask.get("description")); + } + else + { + // Fallback if the form task is not visible yet: populate the required non-null columns. + taskRow.put("title", "Clinical Observations"); + taskRow.put("category", "task"); + taskRow.put("formtype", NIRCClinicalObservationsFormType.NAME); + taskRow.put("qcstate", qcstate); + taskRow.put("assignedto", _user.getUserId()); + } + + List> rows = new ArrayList<>(); + rows.add(taskRow); + + BatchValidationException errors = new BatchValidationException(); + tasksTi.getUpdateService().insertRows(_user, _container, rows, errors, null, getExtraContext()); + if (errors.hasErrors()) + throw errors; + + return newTaskId; + } + public boolean validateHousing(String id, String cage, Date date) { if (id == null || cage == null || date == null) From a37b7ad58081d566744cf73e8ad910582fd472a1 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 16 Jun 2026 13:58:34 -0600 Subject: [PATCH 2/3] Add NIRC_EHRTest coverage for scheduled observation task grouping Augments testClinicalObservation to assert that observations entered from the schedule for an animal with a single clinical case stay grouped under one task with no empty task left behind. Adds testScheduledObservationTaskGrouping, which creates two concurrent clinical cases for the same animal so each scheduled category matches two orders, then verifies the entries are grouped into two tasks (one per originating order taskid) with two entries per category and no empty task created. --- .../tests.nirc_ehr/NIRC_EHRTest.java | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/nirc_ehr/test/src/org.labkey.test/tests.nirc_ehr/NIRC_EHRTest.java b/nirc_ehr/test/src/org.labkey.test/tests.nirc_ehr/NIRC_EHRTest.java index 85f30273..2074eb4b 100644 --- a/nirc_ehr/test/src/org.labkey.test/tests.nirc_ehr/NIRC_EHRTest.java +++ b/nirc_ehr/test/src/org.labkey.test/tests.nirc_ehr/NIRC_EHRTest.java @@ -78,8 +78,10 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import static org.junit.Assert.assertEquals; @@ -664,6 +666,10 @@ public void testClinicalObservation() Assert.assertEquals("Incorrect rows in Today's Observation Schedule", 4, table.getDataRowCount()); Assert.assertEquals("Incorrect observation title", "Daily Clinical Observations; Lameness", table.getDataAsText(0, "observationList")); Assert.assertEquals("Status is not updated", "", table.getDataAsText(0, "observationStatus")); + + // Capture existing observation-form tasks so we can confirm that entering scheduled + // observations groups them onto the form's task without leaving an empty task behind. + Set obsTasksBefore = getObservationFormTaskIds(); table.link(0, "observationRecord").click(); switchToWindow(1); @@ -694,6 +700,10 @@ public void testClinicalObservation() table = new AnimalHistoryPage<>(getDriver()).getActiveReportDataRegion(); Assert.assertEquals("Status is not updated", "Completed", table.getDataAsText(0, "observationStatus")); + // This animal has a single clinical case, so every scheduled observation belongs to that one + // order group and stays on the form's task: one task group, and no empty task created. + verifyScheduledObservationTaskGrouping(animalId, obsTasksBefore, 1); + log("Closing the case"); goToEHRFolder(); waitAndClickAndWait(Locator.linkWithText("Active Clinical Cases")); @@ -764,6 +774,139 @@ public void testBulkClinicalEntry() Assert.assertEquals("Status is not updated ", "Completed", table.getDataAsText(0, "observationStatus")); } + // The ehr.tasks formtype for the clinical observations data entry form (NIRCClinicalObservationsFormType.NAME). + private static final String NIRC_OBSERVATIONS_FORM_TYPE = "Observations"; + + // Valid Observation/Score values keyed by daily clinical observation category. The Observation/Score + // field is category-dependent, so each value must be legal for its category. + private static final Map NIRC_DAILY_OBS_VALUES = Map.of( + "Activity", "0-1 Extremely Lethargic", + "Appetite", "Normal to low", + "BCS", "2.5", + "Hydration", "10%", + "Stool", "M/F", + "Verified Id?", "No"); + + @Test + public void testScheduledObservationTaskGrouping() + { + String animalId = "TESTGRP9090"; + + // Two concurrent clinical cases for the same animal each generate their own set of daily + // observation orders at the same scheduled slot (today at 8:00 AM). A single schedule entry + // therefore matches two orders per category, each carrying a distinct order taskid. The entered + // observations must be grouped by that order taskid -- the first group reuses the form's own + // task and the second gets a freshly created task -- so the entries end up under exactly two + // tasks with no empty task left behind. + createClinicalCase(animalId); + createClinicalCase(animalId); + + goToEHRFolder(); + waitAndClickAndWait(Locator.linkWithText("Today's Observation Schedule")); + DataRegionTable table = new AnimalHistoryPage<>(getDriver()).getActiveReportDataRegion(); + table.setFilter("Id", "Equals", animalId); + Assert.assertEquals("Both cases' orders should collapse to a single schedule row for " + animalId, 1, table.getDataRowCount()); + + Set obsTasksBefore = getObservationFormTaskIds(); + table.link(0, "observationRecord").click(); + switchToWindow(1); + waitForText(animalId); + enterScheduledObservations(); + + // Each of the six daily categories matched two orders, so two entries per category were created, + // grouped into exactly two tasks (one per originating order taskid) with no empty task. + verifyScheduledObservationTaskGrouping(animalId, obsTasksBefore, 2); + + Map entriesPerCategory = new HashMap<>(); + for (Map row : getClinicalObservations(animalId)) + entriesPerCategory.merge(String.valueOf(row.get("category")), 1, Integer::sum); + Assert.assertEquals("Expected the six daily observation categories", NIRC_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)); + } + + // Creates and finalizes a minimal clinical case for the animal. The case's open date is set to + // yesterday so the auto-generated daily observation orders land on today's observation schedule. + private void createClinicalCase(String animalId) + { + gotoEnterData(); + waitAndClickAndWait(Locator.linkWithText("Clinical Cases")); + Ext4FieldRef problem = _helper.getExt4FieldForFormSection("Clinical Case", "Problem Area"); + problem.clickTrigger(); + problem.setValue("General abnormality"); + _helper.setDataEntryField("openRemark", "Clinical Case for " + animalId); + _helper.setDataEntryField("plan", "Case plan for " + animalId); + _helper.getExt4FieldForFormSection("Clinical Case", "Open Date").setValue(LocalDateTime.now().minusDays(1).format(_dateFormat)); + setFormElement(Locator.name("Id"), animalId); + _helper.setDataEntryField("s", "Subjective for " + animalId); + _helper.setDataEntryField("remark", "Remarks for " + animalId); + submitForm("Submit Final", "Finalize"); + } + + // Fills in the Observations grid opened from the schedule, setting a valid value and remark for each + // category row regardless of the grid's row order, then submits. + private void enterScheduledObservations() + { + Ext4GridRef observation = _helper.getExt4GridForFormSection("Observations"); + int rowCount = observation.getRowCount(); + for (int row = 1; row <= rowCount; row++) + { + String category = String.valueOf(observation.getFieldValue(row, "category")); + String value = NIRC_DAILY_OBS_VALUES.get(category); + if (value != null) + observation.setGridCell(row, "observation", value); + observation.setGridCellJS(row, "remark", "remark for " + category); + } + submitForm("Submit Final", "Finalize"); + } + + // Asserts that the animal's scheduled observations are grouped under the expected number of distinct + // tasks and that no observation-form task created while entering them was left empty. + private void verifyScheduledObservationTaskGrouping(String animalId, Set obsTasksBefore, int expectedTaskGroups) + { + List> obsRows = getClinicalObservations(animalId); + Assert.assertFalse("Expected scheduled clinical observations for " + animalId, obsRows.isEmpty()); + + Set taskIds = new HashSet<>(); + for (Map row : obsRows) + { + Object taskId = row.get("taskid"); + Assert.assertNotNull("A scheduled observation is missing its taskid", taskId); + taskIds.add(String.valueOf(taskId)); + } + Assert.assertEquals("Scheduled observations should be grouped under " + expectedTaskGroups + " task(s)", expectedTaskGroups, taskIds.size()); + + // No empty task: every observation-form task created while entering these observations must carry + // at least one observation. The old behavior abandoned the form's task (leaving it empty) when its + // entries were moved onto freshly created group tasks. + Set newObsTasks = new HashSet<>(getObservationFormTaskIds()); + newObsTasks.removeAll(obsTasksBefore); + Assert.assertFalse("Entering scheduled observations should have created at least one observation task", newObsTasks.isEmpty()); + for (String taskId : newObsTasks) + Assert.assertTrue("An empty observation task was created: " + taskId, countObservationsForTask(taskId) > 0); + } + + private List> getClinicalObservations(String animalId) + { + return executeSelectRowCommand("study", "clinical_observations", List.of(new Filter("Id", animalId))).getRows(); + } + + private Set getObservationFormTaskIds() + { + Set taskIds = new HashSet<>(); + for (Map row : executeSelectRowCommand("ehr", "tasks", List.of(new Filter("formtype", NIRC_OBSERVATIONS_FORM_TYPE))).getRows()) + { + if (row.get("taskid") != null) + taskIds.add(String.valueOf(row.get("taskid"))); + } + return taskIds; + } + + private int countObservationsForTask(String taskId) + { + return executeSelectRowCommand("study", "clinical_observations", List.of(new Filter("taskid", taskId))).getRowCount().intValue(); + } + @Override @Test public void testQuickSearch() From 2d67df9f9646cabea5acc095c26a9c9c46695be0 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Thu, 18 Jun 2026 13:59:06 -0600 Subject: [PATCH 3/3] Fix testScheduledObservationTaskGrouping setup and verification Provision the dedicated task-grouping test animal (alive demographics, current housing, active assignment) in createTestSubjects so the clinical case form raises no unknown-animal warnings that would keep the validation banner from clearing and time out the submit. Acknowledge the 'Similar Case Exists' confirmation when finalizing the second case for the same animal and problem area; createClinicalCase now takes the expected finalize-confirmation window title rather than always assuming 'Finalize'. Query the EHR study folder explicitly (ContainerFilter.Current against getContainerPath) in the observation/task verification helpers, since the study datasets and ehr.tasks are defined there rather than in the project root targeted by the default selectRows overload. --- .../tests.nirc_ehr/NIRC_EHRTest.java | 54 ++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/nirc_ehr/test/src/org.labkey.test/tests.nirc_ehr/NIRC_EHRTest.java b/nirc_ehr/test/src/org.labkey.test/tests.nirc_ehr/NIRC_EHRTest.java index af305c10..8d99ca6b 100644 --- a/nirc_ehr/test/src/org.labkey.test/tests.nirc_ehr/NIRC_EHRTest.java +++ b/nirc_ehr/test/src/org.labkey.test/tests.nirc_ehr/NIRC_EHRTest.java @@ -29,6 +29,7 @@ import org.labkey.remoteapi.CommandException; import org.labkey.remoteapi.SimplePostCommand; import org.labkey.remoteapi.core.SaveModulePropertiesCommand; +import org.labkey.remoteapi.query.ContainerFilter; import org.labkey.remoteapi.query.Filter; import org.labkey.remoteapi.query.ImportDataCommand; import org.labkey.remoteapi.query.InsertRowsCommand; @@ -104,6 +105,9 @@ public class NIRC_EHRTest extends AbstractGenericEHRTest implements PostgresOnly private static final String deadAnimalId = "D5454"; private static final String departedAnimalId = "H6767"; private static final String aliveAnimalId = "A4545"; + // 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"; 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}; @@ -474,6 +478,33 @@ protected void createTestSubjects() throws Exception getApiHelper().deleteAllRecords("study", "Assignment", new Filter("Id", StringUtils.join(SUBJECTS, ";"), Filter.Operator.IN)); getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + // Fully provision the task-grouping test animal (alive demographics, current housing, active assignment) so + // the clinical case form has no unknown-animal warnings to keep the validation banner from clearing. + 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} + }; + insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", taskGroupAnimalId)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + + fields = new String[]{"Id", "date", "enddate", "room", "cage", "performedby"}; + data = new Object[][]{ + {taskGroupAnimalId, pastDate1, null, getRooms()[0], CAGES[0], 1004} + }; + insertCommand = getApiHelper().prepareInsertCommand("study", "Housing", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "Housing", new Filter("Id", taskGroupAnimalId)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + + fields = new String[]{"Id", "date", "enddate", "project", "performedby"}; + data = new Object[][]{ + {taskGroupAnimalId, pastDate1, null, PROJECTS[0], 1004} + }; + insertCommand = getApiHelper().prepareInsertCommand("study", "Assignment", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "Assignment", new Filter("Id", taskGroupAnimalId)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + primeCaches(); } @@ -791,7 +822,7 @@ public void testBulkClinicalEntry() @Test public void testScheduledObservationTaskGrouping() { - String animalId = "TESTGRP9090"; + String animalId = taskGroupAnimalId; // Two concurrent clinical cases for the same animal each generate their own set of daily // observation orders at the same scheduled slot (today at 8:00 AM). A single schedule entry @@ -799,8 +830,11 @@ public void testScheduledObservationTaskGrouping() // observations must be grouped by that order taskid -- the first group reuses the form's own // task and the second gets a freshly created task -- so the entries end up under exactly two // tasks with no empty task left behind. - createClinicalCase(animalId); - createClinicalCase(animalId); + // The first case finalizes through the normal "Finalize Form" confirmation. The second case is for + // the same animal and problem area, so its submission instead raises the "Similar Case Exists" + // confirmation -- acknowledge that one to finalize it. + createClinicalCase(animalId, "Finalize"); + createClinicalCase(animalId, "Similar Case Exists"); goToEHRFolder(); waitAndClickAndWait(Locator.linkWithText("Today's Observation Schedule")); @@ -828,7 +862,9 @@ public void testScheduledObservationTaskGrouping() // Creates and finalizes a minimal clinical case for the animal. The case's open date is set to // yesterday so the auto-generated daily observation orders land on today's observation schedule. - private void createClinicalCase(String animalId) + // confirmWindowTitle is the finalize-confirmation dialog expected on submit: "Finalize" for a brand + // new case, or "Similar Case Exists" when the animal already has an active case for the same problem. + private void createClinicalCase(String animalId, String confirmWindowTitle) { gotoEnterData(); waitAndClickAndWait(Locator.linkWithText("Clinical Cases")); @@ -841,7 +877,7 @@ private void createClinicalCase(String animalId) setFormElement(Locator.name("Id"), animalId); _helper.setDataEntryField("s", "Subjective for " + animalId); _helper.setDataEntryField("remark", "Remarks for " + animalId); - submitForm("Submit Final", "Finalize"); + submitForm("Submit Final", confirmWindowTitle); } // Fills in the Observations grid opened from the schedule, setting a valid value and remark for each @@ -889,13 +925,15 @@ private void verifyScheduledObservationTaskGrouping(String animalId, Set private List> getClinicalObservations(String animalId) { - return executeSelectRowCommand("study", "clinical_observations", List.of(new Filter("Id", animalId))).getRows(); + // study datasets and ehr.tasks are defined in the EHR study folder, not the project root, so query + // that container explicitly rather than relying on the default project-scoped overload. + return executeSelectRowCommand("study", "clinical_observations", ContainerFilter.Current, "/" + getContainerPath(), List.of(new Filter("Id", animalId))).getRows(); } private Set getObservationFormTaskIds() { Set taskIds = new HashSet<>(); - for (Map row : executeSelectRowCommand("ehr", "tasks", List.of(new Filter("formtype", NIRC_OBSERVATIONS_FORM_TYPE))).getRows()) + for (Map row : executeSelectRowCommand("ehr", "tasks", ContainerFilter.Current, "/" + getContainerPath(), List.of(new Filter("formtype", NIRC_OBSERVATIONS_FORM_TYPE))).getRows()) { if (row.get("taskid") != null) taskIds.add(String.valueOf(row.get("taskid"))); @@ -905,7 +943,7 @@ private Set getObservationFormTaskIds() private int countObservationsForTask(String taskId) { - return executeSelectRowCommand("study", "clinical_observations", List.of(new Filter("taskid", taskId))).getRowCount().intValue(); + return executeSelectRowCommand("study", "clinical_observations", ContainerFilter.Current, "/" + getContainerPath(), List.of(new Filter("taskid", taskId))).getRowCount().intValue(); } @Test