Skip to content

Commit 7dd950e

Browse files
Merge 26.7 to develop
2 parents 02e66e8 + 717c5a8 commit 7dd950e

7 files changed

Lines changed: 110 additions & 7 deletions

File tree

nirc_ehr/resources/queries/study/cases.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
2222
error = true;
2323
}
2424

25-
if(!triggerHelper.canCloseCase(row.category)) {
25+
if(!triggerHelper.canCloseCase()) {
2626
EHR.Server.Utils.addError(errors, 'enddate', 'Veterinarian permission required to close a case.', 'ERROR');
2727
error = true;
2828
}

nirc_ehr/resources/queries/study/deaths.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ function onUpsert(helper, scriptErrors, row, oldRow) {
8484
//only allow death record to be created if the animal is in the demographics table
8585
if (idMap[row.Id]) {
8686

87+
// Do not allow a death record to be completed while other data for this animal is still in 'Review Required' state.
88+
// Records belonging to this death's own task are excluded, since they move to Completed in the same save.
89+
var reviewRequiredDatasets = null;
90+
if (row.QCStateLabel && row.QCStateLabel.toUpperCase() === 'COMPLETED') {
91+
reviewRequiredDatasets = triggerHelper.getReviewRequiredDatasets(row.Id, row.taskid || null);
92+
}
93+
8794
// check if a death record already exists for this animal
8895
if (idMap[row.Id].calculated_status.toUpperCase() === 'DEAD' && deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'COMPLETED') {
8996
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death record already exists for this animal.', 'ERROR');
@@ -112,6 +119,9 @@ function onUpsert(helper, scriptErrors, row, oldRow) {
112119
deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'IN PROGRESS') {
113120
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death/Necropsy data entry is in progress for this animal', 'ERROR');
114121
}
122+
else if (reviewRequiredDatasets) {
123+
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death record cannot be completed. There is still data in Review Required state for this animal in the following dataset(s): ' + reviewRequiredDatasets, 'ERROR');
124+
}
115125
else if (!helper.isValidateOnly() && row.Id && row.date && row.QCStateLabel.toUpperCase() === 'COMPLETED') {
116126

117127
if (validIds.indexOf(row.id) !== -1) {

nirc_ehr/resources/queries/study/departure.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ function onInit(event, helper){
1515

1616
}
1717

18+
function onUpsert(helper, scriptErrors, row, oldRow) {
19+
20+
if (!helper.isETL()) {
21+
22+
// Do not allow a departure record to be completed while other data for this animal is still in 'Review Required' state.
23+
// Records belonging to this departure's own task are excluded, since they move to Completed in the same save.
24+
if (row.QCStateLabel && row.QCStateLabel.toUpperCase() === 'COMPLETED') {
25+
var reviewRequiredDatasets = triggerHelper.getReviewRequiredDatasets(row.Id, row.taskid || null);
26+
if (reviewRequiredDatasets) {
27+
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Departure record cannot be completed. There is still data in Review Required state for this animal in the following dataset(s): ' + reviewRequiredDatasets, 'ERROR');
28+
}
29+
}
30+
}
31+
}
32+
1833
EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.AFTER_INSERT, 'study', 'departure', function(helper, scriptErrors, row, oldRow) {
1934

2035
if (row.id) {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
SELECT sd.Id,
2+
sd.Date,
3+
sd.DataSet.Label AS datasetLabel,
4+
sd.taskid,
5+
sd.lsid
6+
7+
FROM study.StudyData sd
8+
INNER JOIN core.QCState qc ON sd.QCState = qc.RowId
9+
WHERE qc.Label = 'Review Required'

nirc_ehr/src/org/labkey/nirc_ehr/NIRC_EHRModule.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@
2727
import org.labkey.api.ehr.demographics.SourceDemographicsProvider;
2828
import org.labkey.api.ehr.history.DefaultAlopeciaDataSource;
2929
import org.labkey.api.ehr.history.DefaultAnimalRecordFlagDataSource;
30-
import org.labkey.api.ehr.history.DefaultClinicalRemarksDataSource;
3130
import org.labkey.api.ehr.history.DefaultNotesDataSource;
32-
import org.labkey.api.ehr.history.DefaultVitalsDataSource;
3331
import org.labkey.api.ehr.security.EHRDataAdminPermission;
3432
import org.labkey.api.ldk.ExtendedSimpleModule;
3533
import org.labkey.api.ldk.buttons.ShowEditUIButton;
@@ -131,7 +129,6 @@ protected void doStartupAfterSpringConfig(ModuleContext moduleContext)
131129
ehrService.registerDemographicsProvider(new ProtocolAssignmentDemographicsProvider(this));
132130
ehrService.registerDemographicsProvider(new HousingDemographicsProvider(this));
133131
ehrService.registerDemographicsProvider(new CagematesDemographicsProvider(this));
134-
ehrService.registerDemographicsProvider(new CagematesDemographicsProvider(this));
135132
ehrService.registerDemographicsProvider(new ActiveCasesDemographicsProvider(this));
136133
ehrService.registerDemographicsProvider(new ActiveTreatmentsDemographicsProvider(this));
137134
ehrService.registerDemographicsProvider(new SourceDemographicsProvider(this));
@@ -145,9 +142,7 @@ protected void doStartupAfterSpringConfig(ModuleContext moduleContext)
145142
EHRService.get().registerHistoryDataSource(new DeathDataSource(this));
146143
EHRService.get().registerHistoryDataSource(new DefaultAlopeciaDataSource(this));
147144
EHRService.get().registerHistoryDataSource(new DefaultAnimalRecordFlagDataSource(this));
148-
EHRService.get().registerHistoryDataSource(new DefaultClinicalRemarksDataSource(this));
149145
EHRService.get().registerHistoryDataSource(new DefaultNotesDataSource(this));
150-
EHRService.get().registerHistoryDataSource(new DefaultVitalsDataSource(this));
151146
EHRService.get().registerHistoryDataSource(new DepartureDataSource(this));
152147
EHRService.get().registerHistoryDataSource(new DrugAdminDataSource(this));
153148
EHRService.get().registerHistoryDataSource(new FlagsDataSource(this));

nirc_ehr/src/org/labkey/nirc_ehr/query/NIRC_EHRTriggerHelper.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.apache.commons.lang3.time.DateUtils;
2121
import org.apache.logging.log4j.LogManager;
2222
import org.apache.logging.log4j.Logger;
23+
import org.jetbrains.annotations.Nullable;
2324
import org.labkey.api.collections.CaseInsensitiveHashMap;
2425
import org.labkey.api.data.ColumnInfo;
2526
import org.labkey.api.data.CompareType;
@@ -74,6 +75,7 @@
7475
import java.util.Map;
7576
import java.util.Objects;
7677
import java.util.Set;
78+
import java.util.TreeSet;
7779

7880
public class NIRC_EHRTriggerHelper
7981
{
@@ -324,6 +326,23 @@ public boolean deathExists(String id)
324326
return false;
325327
}
326328

329+
/**
330+
* Returns a comma-separated list of dataset labels that still have records in the 'Review Required'
331+
* QC state for the given animal, or null if there are none. Records belonging to {@code taskId} are
332+
* excluded, since they transition to Completed in the same save as the death record itself.
333+
*/
334+
@Nullable
335+
public String getReviewRequiredDatasets(String animalId, @Nullable String taskId)
336+
{
337+
TableInfo ti = getTableInfo("study", "studyDataReviewRequired");
338+
SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id"), animalId);
339+
if (taskId != null)
340+
filter.addCondition(FieldKey.fromString("taskid"), taskId, CompareType.NEQ_OR_NULL);
341+
342+
Set<String> datasets = new TreeSet<>(new TableSelector(ti, Collections.singleton("datasetLabel"), filter, null).getArrayList(String.class));
343+
return datasets.isEmpty() ? null : String.join(", ", datasets);
344+
}
345+
327346
public void upsertWeightRecord(Map<String, Object> row) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException
328347
{
329348
BatchValidationException errors = new BatchValidationException();
@@ -693,7 +712,7 @@ public long totalRecords(String schemaName, String queryName, String columnName,
693712
return ts.getRowCount();
694713
}
695714

696-
public boolean canCloseCase(String category)
715+
public boolean canCloseCase()
697716
{
698717
if (_container.hasPermission(_user, EHRVeterinarianPermission.class))
699718
return true;

nirc_ehr/test/src/org.labkey.test/tests.nirc_ehr/NIRC_EHRTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,61 @@ public void testDeathNecropsyForm() throws IOException, CommandException
12251225

12261226
}
12271227

1228+
@Test
1229+
public void testDeathAndDepartureBlockedByReviewRequiredData() throws Exception
1230+
{
1231+
String animalId = "REVREQ1";
1232+
String taskId = UUID.randomUUID().toString();
1233+
1234+
goToEHRFolder();
1235+
1236+
log("Creating a live animal with a weight record in Review Required state");
1237+
getApiHelper().deleteAllRecords("study", "deaths", new Filter("Id", animalId));
1238+
getApiHelper().deleteAllRecords("study", "departure", new Filter("Id", animalId));
1239+
getApiHelper().deleteAllRecords("study", "weight", new Filter("Id", animalId));
1240+
getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", animalId));
1241+
1242+
String[] demographicsFields = {"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"};
1243+
Object[][] demographicsData = {{animalId, "Rhesus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}};
1244+
getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), getApiHelper().prepareInsertCommand("study", "demographics", "lsid", demographicsFields, demographicsData), getExtraContext());
1245+
1246+
// The weight record carries the same taskid used by the death/departure rows below so the exclusion
1247+
// scenarios can prove that records belonging to the completing record's own task are excluded from
1248+
// the review-required check. Both scenarios are validate-only, so nothing persists between them.
1249+
String[] weightInsertFields = {"Id", "date", "weight", "taskid", FIELD_QCSTATELABEL, "performedby"};
1250+
Object[][] weightInsertData = {{animalId, new Date(), 8.5, taskId, EHRQCState.REVIEW_REQUIRED.label, 1004}};
1251+
getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), getApiHelper().prepareInsertCommand("study", "weight", "lsid", weightInsertFields, weightInsertData), getExtraContext());
1252+
1253+
String[] deathFields = {"Id", "date", "reason", "deathWeight", "taskid", FIELD_QCSTATELABEL, FIELD_OBJECTID, FIELD_LSID, "_recordid", "performedby"};
1254+
verifyCompletionBlockedByReviewRequired("deaths", "Death", deathFields,
1255+
new Object[]{animalId, new Date(), "4", 8.5, null, EHRQCState.COMPLETED.label, null, null, "recordID", 1004},
1256+
new Object[]{animalId, new Date(), "4", 8.5, taskId, EHRQCState.COMPLETED.label, null, null, "recordID", 1004});
1257+
1258+
String[] departureFields = {"Id", "date", "destination", "taskid", FIELD_QCSTATELABEL, FIELD_OBJECTID, FIELD_LSID, "_recordid", "performedby"};
1259+
verifyCompletionBlockedByReviewRequired("departure", "Departure", departureFields,
1260+
new Object[]{animalId, new Date(), "Oregon NPRC", null, EHRQCState.COMPLETED.label, null, null, "recordID", 1004},
1261+
new Object[]{animalId, new Date(), "Oregon NPRC", taskId, EHRQCState.COMPLETED.label, null, null, "recordID", 1004});
1262+
}
1263+
1264+
// Asserts that completing a record (death, departure) is blocked while other data for the animal is in
1265+
// Review Required state, and that a record whose taskid matches the review-required data is NOT blocked
1266+
// (its records move to Completed in the same save). rowWithoutTaskId/rowWithTaskId differ only in taskid.
1267+
private void verifyCompletionBlockedByReviewRequired(String queryName, String recordNoun, String[] fields, Object[] rowWithoutTaskId, Object[] rowWithTaskId)
1268+
{
1269+
// testValidationMessage defaults extraContext.targetQC to 'In Progress', and the global targetQC always
1270+
// overrides row-level QCStateLabel (see ehr/security.js normalizeQCState), so it must be forced to
1271+
// 'Completed' for the trigger to see a completing record.
1272+
Map<String, Object> completedTargetQC = Map.of("targetQC", EHRQCState.COMPLETED.label);
1273+
1274+
log("Completing a " + queryName + " record while other data is in Review Required state should be blocked");
1275+
Map<String, List<String>> expected = new HashMap<>();
1276+
expected.put("Id", Collections.singletonList("ERROR: " + recordNoun + " record cannot be completed. There is still data in Review Required state for this animal in the following dataset(s): Weight"));
1277+
getApiHelper().testValidationMessage(DATA_ADMIN.getEmail(), "study", queryName, fields, new Object[][]{rowWithoutTaskId}, expected, completedTargetQC);
1278+
1279+
log("Review Required records on the " + queryName + " record's own task should not block completion");
1280+
getApiHelper().testValidationMessage(DATA_ADMIN.getEmail(), "study", queryName, fields, new Object[][]{rowWithTaskId}, new HashMap<>(), completedTargetQC);
1281+
}
1282+
12281283
@Test
12291284
public void testClinicalCasesWorkflow()
12301285
{

0 commit comments

Comments
 (0)