From 2b4e5686c70c5de1a33b68b9e380dd669aeb682a Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Fri, 11 Sep 2026 03:59:23 -0700 Subject: [PATCH] Add project, protocol and location to the Rearrival form (#758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Rationale The Rearrival form collects fields that do not apply to a returning animal and omits the ones that do, and a completed rearrival leaves the animal in a departed state. It asks for an acquisition type and a CITES number, though an animal that is coming back was already acquired, and it does not ask which project, which protocol or which location the animal returns to. A departure closes the animal's project assignment, protocol assignment and housing records, so a rearrival has to open them again, and it has to put the animal back to Alive. ## Related Pull Requests - https://github.com/LabKey/ehrModules/pull/1200 — needed alongside this change, since entering a death for an animal that has arrived more than once fails without it. ## Changes - Replace acquisition type and CITES on the Rearrival form with project, protocol and location, each required, as they are on the Arrival form. - Open the project assignment, protocol assignment and housing records for a rearrival, matching what an arrival already does. - Return a rearrived animal to Alive once the rearrival is completed. - Add a Selenium test covering the fields the form collects, the records a rearrival opens and the status change. --- nirc_ehr/resources/queries/study/arrival.js | 72 +++++++++++-------- .../web/nirc_ehr/model/sources/Rearrival.js | 53 ++++++++++---- .../tests.nirc_ehr/NIRC_EHRTest.java | 68 ++++++++++++++++++ 3 files changed, 149 insertions(+), 44 deletions(-) diff --git a/nirc_ehr/resources/queries/study/arrival.js b/nirc_ehr/resources/queries/study/arrival.js index 501da5dc..0bf813bc 100644 --- a/nirc_ehr/resources/queries/study/arrival.js +++ b/nirc_ehr/resources/queries/study/arrival.js @@ -24,41 +24,21 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even helper.registerArrival(row.Id, row.date); - //Insert or update demographic and birth records - if (!row.rearrival && !helper.isETL() && !helper.isGeneratedByServer() && !helper.isValidateOnly()) { - - // this allows demographic records in qcstates other than completed - var extraDemographicsFieldMappings = { - 'taskid': row.taskid, - 'qcstate': helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId() - } - - // null (not undefined) required for call to java trigger helper - row.dam = row['Id/demographics/dam'] || null; - row.sire = row['Id/demographics/sire'] || null; - row.species = row['Id/demographics/species'] || null; - 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; + // Project, protocol and housing records are created for rearrivals too: the animal's departure closed the previous ones. + if (!helper.isETL() && !helper.isGeneratedByServer() && !helper.isValidateOnly()) { if (row.QCStateLabel) { row.qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); } - if (row.birth) { - var birthInfo = { + // A rearrival reuses the existing demographics record, so its status is written directly; drafts get the same interim status an arrival draft does. + // The update refreshes the server-side animal cache, so the records opened below validate against this status rather than the departure's. + if (row.rearrival && row.Id && row.QCStateLabel) { + row.calculated_status = (row.QCStateLabel.toUpperCase() === 'IN PROGRESS' || row.QCStateLabel.toUpperCase() === 'REVIEW REQUIRED') ? 'Alive - In Progress' : 'Alive'; + helper.getJavaHelper().updateDemographicsRecord([{ Id: row.Id, - date: row.birth, - qcstate: row.qcstate, - taskid: row.taskid, - performedby: row.performedby - } - - var birthErrors = triggerHelper.saveBirthRecord(row.Id, birthInfo); - if (birthErrors){ - EHR.Server.Utils.addError(scriptErrors, 'birth', birthErrors, 'ERROR'); - } + calculated_status: row.calculated_status + }]); } if (row.Id && row.date) { @@ -100,6 +80,40 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even EHR.Server.Utils.addError(scriptErrors, 'Id', housingErrors, 'ERROR'); } } + } + + //Insert or update demographic and birth records + if (!row.rearrival && !helper.isETL() && !helper.isGeneratedByServer() && !helper.isValidateOnly()) { + + // this allows demographic records in qcstates other than completed + var extraDemographicsFieldMappings = { + 'taskid': row.taskid, + 'qcstate': helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId() + } + + // null (not undefined) required for call to java trigger helper + row.dam = row['Id/demographics/dam'] || null; + row.sire = row['Id/demographics/sire'] || null; + row.species = row['Id/demographics/species'] || null; + 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.birth) { + var birthInfo = { + Id: row.Id, + date: row.birth, + qcstate: row.qcstate, + taskid: row.taskid, + performedby: row.performedby + } + + var birthErrors = triggerHelper.saveBirthRecord(row.Id, birthInfo); + if (birthErrors){ + EHR.Server.Utils.addError(scriptErrors, 'birth', birthErrors, 'ERROR'); + } + } row.calculated_status = (row.QCStateLabel.toUpperCase() === 'IN PROGRESS' || row.QCStateLabel.toUpperCase() === 'REVIEW REQUIRED') ? 'Alive - In Progress' : 'Alive'; diff --git a/nirc_ehr/resources/web/nirc_ehr/model/sources/Rearrival.js b/nirc_ehr/resources/web/nirc_ehr/model/sources/Rearrival.js index 059847ea..94ae9c4c 100644 --- a/nirc_ehr/resources/web/nirc_ehr/model/sources/Rearrival.js +++ b/nirc_ehr/resources/web/nirc_ehr/model/sources/Rearrival.js @@ -30,11 +30,14 @@ EHR.model.DataModelManager.registerMetadata('Rearrival', { }, }, acquisitionType: { - allowBlank: false, - columnConfig: { - fixed: true, - width: 150 - }, + allowBlank: true, + hidden: true, + showInGrid: false + }, + CITES: { + allowBlank: true, + hidden: true, + showInGrid: false }, arrivalType: { allowBlank: false, @@ -43,20 +46,40 @@ EHR.model.DataModelManager.registerMetadata('Rearrival', { } }, 'cage': { - allowBlank: true, - hidden: true, - showInGrid: false + allowBlank: false, + columnConfig: { + fixed: true, + width: 200 + }, }, project: { - allowBlank: true, - hidden: true, - showInGrid: false + 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 }, arrivalProtocol: { - allowBlank: true, - hidden: true, - showInGrid: false + allowBlank: false, + columnConfig: { + width: 200 + }, + lookup: { + schemaName: 'ehr', + queryName: 'activeProtocols', + keyColumn: 'protocol', + columns: 'protocol,title' + }, }, } } -}); \ No newline at end of file +}); 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 59fe6a13..95eb459a 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 @@ -645,6 +645,74 @@ public void testArrivalForm() verifyOrchardFileGenerated(arrivedAnimal); } + @Test + public void testRearrivalForm() throws Exception + { + String rearrivedAnimal = getExpectedAnimalIDCasing("R7373"); + LocalDateTime now = LocalDateTime.now(); + + goToEHRFolder(); + + log("Seeding an animal that has left the center"); + for (String query : List.of("arrival", "departure", "assignment", "protocolAssignment", "housing", "demographics")) + getApiHelper().deleteAllRecords("study", query, new Filter("Id", rearrivedAnimal)); + + // The departed status is seeded straight onto demographics because a departure does not recalculate + // calculated_status in this module, so inserting one alone would leave the animal alive. + String[] demographicsFields = {"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; + Object[][] demographicsData = {{rearrivedAnimal, "Rhesus", now.minusDays(30).toString(), getMale(), new Date(), "Shipped", UUID.randomUUID().toString(), 1004}}; + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), getApiHelper().prepareInsertCommand("study", "demographics", "lsid", demographicsFields, demographicsData), getExtraContext()); + + InsertRowsCommand departure = new InsertRowsCommand("study", "departure"); + departure.addRow(Map.of("Id", rearrivedAnimal, "date", now.minusDays(2), "destination", "Oregon NPRC", "performedby", 1004)); + departure.execute(getApiHelper().getConnection(), getContainerPath()); + + gotoEnterData(); + waitAndClickAndWait(Locator.linkWithText("Rearrivals")); + lockForm(); + + Ext4GridRef rearrivals = _helper.getExt4GridForFormSection("Rearrivals"); + _helper.addRecordToGrid(rearrivals); + + log("A rearrival collects project, protocol and location, and does not collect acquisition type or CITES"); + Assert.assertTrue("Project should be on the Rearrival form", rearrivals.isColumnPresent("project", true)); + Assert.assertTrue("Protocol should be on the Rearrival form", rearrivals.isColumnPresent("arrivalProtocol", true)); + Assert.assertTrue("Location should be on the Rearrival form", rearrivals.isColumnPresent("cage", true)); + Assert.assertFalse("Acquisition type should not be on the Rearrival form", rearrivals.isColumnPresent("acquisitionType", true)); + Assert.assertFalse("CITES should not be on the Rearrival form", rearrivals.isColumnPresent("CITES", true)); + + rearrivals.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + rearrivals.setGridCell(1, "arrivalType", "Non-quarantine Arrival"); + rearrivals.setGridCell(1, "Id", rearrivedAnimal); + rearrivals.setGridCell(1, "cage", "C1"); + rearrivals.setGridCell(1, "project", "640991"); + rearrivals.setGridCell(1, "arrivalProtocol", "dummyprotocol"); + rearrivals.setGridCell(1, "sourceFacility", "BIOQUAL, Inc."); + submitForm("Submit Final", "Finalize"); + + goToSchemaBrowser(); + DataRegionTable table = viewQueryData("study", "arrival"); + table.setFilter("Id", "Equals", rearrivedAnimal); + CustomizeView view = table.openCustomizeGrid(); + view.addColumn("cage"); + view.addColumn("project"); + view.addColumn("arrivalProtocol"); + view.applyCustomView(); + Assert.assertEquals("Invalid Rearrival record", Arrays.asList("C1"), table.getRowDataAsText(0, "cage")); + Assert.assertEquals("Invalid Rearrival record", Arrays.asList("640991"), table.getRowDataAsText(0, "project")); + Assert.assertEquals("Invalid Rearrival record", Arrays.asList("dummyprotocol"), table.getRowDataAsText(0, "arrivalProtocol")); + + log("The rearrival opens the assignment, protocol assignment and housing records that the departure closed"); + verifyRowCreated("study", "assignment", rearrivedAnimal, 1); + verifyRowCreated("study", "protocolAssignment", rearrivedAnimal, 1); + verifyRowCreated("study", "housing", rearrivedAnimal, 1); + + log("The rearrived animal is alive again"); + List> rows = executeSelectRowCommand("study", "demographics", ContainerFilter.Current, "/" + getContainerPath(), List.of(new Filter("Id", rearrivedAnimal))).getRows(); + assertEquals("Expected one demographics record for the rearrived animal", 1, rows.size()); + assertEquals("Rearrived animal should be alive", "Alive", rows.getFirst().get("calculated_status")); + } + @Test public void testBirthForm() {