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 4342b1c4..1861491e 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 @@ -840,12 +840,15 @@ public Map handleScheduledObservations(Map row, String observation = ConvertHelper.convert(row.get("observation"), String.class); String performedBy = ConvertHelper.convert(row.get("performedBy"), String.class); String taskid = ConvertHelper.convert(row.get("taskid"), String.class); + String id = ConvertHelper.convert(row.get("Id"), String.class); - // Get observation orders for these tasks + // Get this animal's observation orders for these tasks. An order taskid is shared by every animal + // ordered in the same session, so without the animal filter the entry spills onto those animals too. TableInfo ti = getTableInfo("study", "observationOrdersByDate"); SimpleFilter filter = new SimpleFilter(FieldKey.fromString("taskid"), orderTasks, CompareType.IN); filter.addCondition(FieldKey.fromString("category"), category); filter.addCondition(FieldKey.fromString("date"), scheduledDate); + filter.addCondition(FieldKey.fromString("animalId"), id); TableSelector ts = new TableSelector(ti, PageFlowUtil.set("category","caseId","animalId","area","objectid","type","taskid"), filter, null); ts.setNamedParameters(Map.of("StartDate", scheduledDate, "NumDays", "1")); 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 95eb459a..e8d409e8 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 @@ -114,6 +114,10 @@ public class NIRC_EHRTest extends AbstractGenericEHRTest implements PostgresOnly // 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"; + // Dedicated pair for testScheduledObservationStaysWithItsAnimal; both get their observation order from the + // same entry form, so the two orders share a taskid. Provisioned the same way as the animals above. + private static final String sharedOrderAnimalId = "TESTSHARED8181"; + private static final String sharedOrderOtherAnimalId = "TESTSHARED8282"; 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}; @@ -537,9 +541,42 @@ protected void createTestSubjects() throws Exception getApiHelper().deleteAllRecords("study", "Assignment", new Filter("Id", obsTypeAnimalId)); getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + provisionTestSubject(sharedOrderAnimalId, CAGES[2], pastDate1); + provisionTestSubject(sharedOrderOtherAnimalId, CAGES[2], pastDate1); + primeCaches(); } + // Fully provisions an animal (alive demographics, current housing, active assignment) so the clinical data + // entry forms raise no unknown-animal warnings that would keep the validation banner up. + private void provisionTestSubject(String animalId, String cage, Date housedSince) throws Exception + { + log("Creating test subject " + animalId); + String[] fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; + Object[][] data = new Object[][]{ + {animalId, "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", animalId)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + + fields = new String[]{"Id", "date", "enddate", "room", "cage", "performedby"}; + data = new Object[][]{ + {animalId, housedSince, null, getRooms()[0], cage, 1004} + }; + insertCommand = getApiHelper().prepareInsertCommand("study", "Housing", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "Housing", new Filter("Id", animalId)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + + fields = new String[]{"Id", "date", "enddate", "project", "performedby"}; + data = new Object[][]{ + {animalId, housedSince, null, PROJECTS[0], 1004} + }; + insertCommand = getApiHelper().prepareInsertCommand("study", "Assignment", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "Assignment", new Filter("Id", animalId)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + } + @Override protected String[] getWeightFields() { @@ -966,6 +1003,83 @@ public void testScheduledObservationTaskGrouping() Assert.assertEquals("Expected two entries (one per matching order) for category " + category, Integer.valueOf(2), count)); } + // The observation ordered for both animals of the shared-order pair, and a valid Observation/Score for it. + private static final String SHARED_ORDER_CATEGORY = "Dental/Oral Observations"; + private static final String SHARED_ORDER_OBSERVATION = "Fractured Tooth"; + + @Test + public void testScheduledObservationStaysWithItsAnimal() + { + // An observation order's taskid identifies the ordering session, not the animal, so ordering for + // several animals at once leaves them all with the same order taskid. Recording from the schedule + // opens the form for one animal and passes that taskid to the server, which must still confine the + // entry to the animal the form was opened for. + log("Ordering the same observation for two animals in one submission"); + gotoEnterData(); + waitAndClickAndWait(Locator.linkWithText("Bulk Clinical Entry")); + + Ext4GridRef observationOrders = _helper.getExt4GridForFormSection("Observation Orders"); + addObservationOrderRow(observationOrders, sharedOrderAnimalId); + addObservationOrderRow(observationOrders, sharedOrderOtherAnimalId); + submitForm("Submit Final", "Finalize"); + + List> orders = getObservationOrders(sharedOrderAnimalId, sharedOrderOtherAnimalId); + Assert.assertEquals("Expected one observation order per animal", 2, orders.size()); + + Map orderIdByAnimal = new HashMap<>(); + Set orderTaskIds = new HashSet<>(); + for (Map order : orders) + { + orderIdByAnimal.put(String.valueOf(order.get("Id")), String.valueOf(order.get("objectid"))); + orderTaskIds.add(String.valueOf(order.get("taskid"))); + } + Assert.assertEquals("The two orders should share the submitting form's taskid, which is what puts the " + + "entry at risk of crossing animals", 1, orderTaskIds.size()); + + log("Recording the scheduled observation for " + sharedOrderAnimalId + " only"); + goToEHRFolder(); + waitAndClickAndWait(Locator.linkWithText("Today's Observation Schedule")); + DataRegionTable table = new AnimalHistoryPage<>(getDriver()).getActiveReportDataRegion(); + table.setFilter("Id", "Equals", sharedOrderAnimalId); + Assert.assertEquals("Expected a single scheduled slot for " + sharedOrderAnimalId, 1, table.getDataRowCount()); + + table.link(0, "observationRecord").click(); + switchToWindow(1); + waitForText(sharedOrderAnimalId); + Ext4GridRef observation = _helper.getExt4GridForFormSection("Observations"); + Assert.assertEquals("The form should open with only the animal it was launched for", 1, observation.getRowCount()); + observation.setGridCell(1, "observation", SHARED_ORDER_OBSERVATION); + observation.setGridCellJS(1, "remark", "remark for " + sharedOrderAnimalId); + submitForm("Submit Final", "Finalize"); + + List> entered = getClinicalObservations(sharedOrderAnimalId); + Assert.assertEquals("Expected the single entered observation for " + sharedOrderAnimalId, 1, entered.size()); + Assert.assertEquals("Entered observation has the wrong category", SHARED_ORDER_CATEGORY, String.valueOf(entered.get(0).get("category"))); + Assert.assertEquals("The entry should be filed against the animal's own observation order", + orderIdByAnimal.get(sharedOrderAnimalId), String.valueOf(entered.get(0).get("orderId"))); + + Assert.assertTrue("Recording an observation for " + sharedOrderAnimalId + " must not create one for " + + sharedOrderOtherAnimalId + ", whose order only shares the same taskid", + getClinicalObservations(sharedOrderOtherAnimalId).isEmpty()); + } + + // Appends an observation order row for the animal. SID puts the order on a single 8:00 AM slot, so each + // animal gets exactly one row on today's observation schedule. + private void addObservationOrderRow(Ext4GridRef observationOrders, String animalId) + { + _helper.addRecordToGrid(observationOrders); + int row = observationOrders.getRowCount(); + observationOrders.setGridCell(row, "Id", animalId); + observationOrders.setGridCell(row, "category", SHARED_ORDER_CATEGORY); + observationOrders.setGridCell(row, "frequency", "SID"); + } + + private List> getObservationOrders(String... animalIds) + { + return executeSelectRowCommand("study", "observation_order", ContainerFilter.Current, "/" + getContainerPath(), + List.of(new Filter("Id", String.join(";", animalIds), Filter.Operator.IN))).getRows(); + } + // 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. // confirmWindowTitle is the finalize-confirmation dialog expected on submit: "Finalize" for a brand