Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion nirc_ehr/resources/queries/study/cases.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
error = true;
}

if(!triggerHelper.canCloseCase(row.category)) {
if(!triggerHelper.canCloseCase()) {
EHR.Server.Utils.addError(errors, 'enddate', 'Veterinarian permission required to close a case.', 'ERROR');
error = true;
}
Expand Down
10 changes: 10 additions & 0 deletions nirc_ehr/resources/queries/study/deaths.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ function onUpsert(helper, scriptErrors, row, oldRow) {
//only allow death record to be created if the animal is in the demographics table
if (idMap[row.Id]) {

// Do not allow a death record to be completed while other data for this animal is still in 'Review Required' state.
// Records belonging to this death's own task are excluded, since they move to Completed in the same save.
var reviewRequiredDatasets = null;
if (row.QCStateLabel && row.QCStateLabel.toUpperCase() === 'COMPLETED') {
reviewRequiredDatasets = triggerHelper.getReviewRequiredDatasets(row.Id, row.taskid || null);
}

// check if a death record already exists for this animal
if (idMap[row.Id].calculated_status.toUpperCase() === 'DEAD' && deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'COMPLETED') {
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death record already exists for this animal.', 'ERROR');
Expand Down Expand Up @@ -107,6 +114,9 @@ function onUpsert(helper, scriptErrors, row, oldRow) {
deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'IN PROGRESS') {
EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death/Necropsy data entry is in progress for this animal', 'ERROR');
}
else if (reviewRequiredDatasets) {
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');
}
else if (!helper.isValidateOnly() && row.Id && row.date && row.QCStateLabel.toUpperCase() === 'COMPLETED') {

if (validIds.indexOf(row.id) !== -1) {
Expand Down
15 changes: 15 additions & 0 deletions nirc_ehr/resources/queries/study/departure.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ function onInit(event, helper){

}

function onUpsert(helper, scriptErrors, row, oldRow) {

if (!helper.isETL()) {

// Do not allow a departure record to be completed while other data for this animal is still in 'Review Required' state.
// Records belonging to this departure's own task are excluded, since they move to Completed in the same save.
if (row.QCStateLabel && row.QCStateLabel.toUpperCase() === 'COMPLETED') {
var reviewRequiredDatasets = triggerHelper.getReviewRequiredDatasets(row.Id, row.taskid || null);
if (reviewRequiredDatasets) {
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');
}
}
}
}

EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.AFTER_INSERT, 'study', 'departure', function(helper, scriptErrors, row, oldRow) {

if (row.id) {
Expand Down
9 changes: 9 additions & 0 deletions nirc_ehr/resources/queries/study/studyDataReviewRequired.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
SELECT sd.Id,
sd.Date,
sd.DataSet.Label AS datasetLabel,
sd.taskid,
sd.lsid

FROM study.StudyData sd
INNER JOIN core.QCState qc ON sd.QCState = qc.RowId
WHERE qc.Label = 'Review Required'
5 changes: 0 additions & 5 deletions nirc_ehr/src/org/labkey/nirc_ehr/NIRC_EHRModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@
import org.labkey.api.ehr.demographics.SourceDemographicsProvider;
import org.labkey.api.ehr.history.DefaultAlopeciaDataSource;
import org.labkey.api.ehr.history.DefaultAnimalRecordFlagDataSource;
import org.labkey.api.ehr.history.DefaultClinicalRemarksDataSource;
import org.labkey.api.ehr.history.DefaultNotesDataSource;
import org.labkey.api.ehr.history.DefaultVitalsDataSource;
import org.labkey.api.ehr.security.EHRDataAdminPermission;
import org.labkey.api.ldk.ExtendedSimpleModule;
import org.labkey.api.ldk.buttons.ShowEditUIButton;
Expand Down Expand Up @@ -131,7 +129,6 @@ protected void doStartupAfterSpringConfig(ModuleContext moduleContext)
ehrService.registerDemographicsProvider(new ProtocolAssignmentDemographicsProvider(this));
ehrService.registerDemographicsProvider(new HousingDemographicsProvider(this));
ehrService.registerDemographicsProvider(new CagematesDemographicsProvider(this));
ehrService.registerDemographicsProvider(new CagematesDemographicsProvider(this));
ehrService.registerDemographicsProvider(new ActiveCasesDemographicsProvider(this));
ehrService.registerDemographicsProvider(new ActiveTreatmentsDemographicsProvider(this));
ehrService.registerDemographicsProvider(new SourceDemographicsProvider(this));
Expand All @@ -145,9 +142,7 @@ protected void doStartupAfterSpringConfig(ModuleContext moduleContext)
EHRService.get().registerHistoryDataSource(new DeathDataSource(this));
EHRService.get().registerHistoryDataSource(new DefaultAlopeciaDataSource(this));
EHRService.get().registerHistoryDataSource(new DefaultAnimalRecordFlagDataSource(this));
EHRService.get().registerHistoryDataSource(new DefaultClinicalRemarksDataSource(this));
EHRService.get().registerHistoryDataSource(new DefaultNotesDataSource(this));
EHRService.get().registerHistoryDataSource(new DefaultVitalsDataSource(this));
EHRService.get().registerHistoryDataSource(new DepartureDataSource(this));
EHRService.get().registerHistoryDataSource(new DrugAdminDataSource(this));
EHRService.get().registerHistoryDataSource(new FlagsDataSource(this));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.apache.commons.lang3.time.DateUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.collections.CaseInsensitiveHashMap;
import org.labkey.api.data.ColumnInfo;
import org.labkey.api.data.CompareType;
Expand Down Expand Up @@ -59,6 +60,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

public class NIRC_EHRTriggerHelper
{
Expand Down Expand Up @@ -309,6 +311,23 @@ public boolean deathExists(String id)
return false;
}

/**
* Returns a comma-separated list of dataset labels that still have records in the 'Review Required'
* QC state for the given animal, or null if there are none. Records belonging to {@code taskId} are
* excluded, since they transition to Completed in the same save as the death record itself.
*/
@Nullable
public String getReviewRequiredDatasets(String animalId, @Nullable String taskId)
{
TableInfo ti = getTableInfo("study", "studyDataReviewRequired");
SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id"), animalId);
if (taskId != null)
filter.addCondition(FieldKey.fromString("taskid"), taskId, CompareType.NEQ_OR_NULL);

Set<String> datasets = new TreeSet<>(new TableSelector(ti, Collections.singleton("datasetLabel"), filter, null).getArrayList(String.class));
return datasets.isEmpty() ? null : String.join(", ", datasets);
}

public void upsertWeightRecord(Map<String, Object> row) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException
{
BatchValidationException errors = new BatchValidationException();
Expand Down Expand Up @@ -680,7 +699,7 @@ public long totalRecords(String schemaName, String queryName, String columnName,
return ts.getRowCount();
}

public boolean canCloseCase(String category)
public boolean canCloseCase()
{
if (_container.hasPermission(_user, EHRVeterinarianPermission.class))
return true;
Expand Down
55 changes: 55 additions & 0 deletions nirc_ehr/test/src/org.labkey.test/tests.nirc_ehr/NIRC_EHRTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,61 @@ public void testDeathNecropsyForm() throws IOException, CommandException

}

@Test
public void testDeathAndDepartureBlockedByReviewRequiredData() throws Exception
{
String animalId = "REVREQ1";
String taskId = UUID.randomUUID().toString();

goToEHRFolder();

log("Creating a live animal with a weight record in Review Required state");
getApiHelper().deleteAllRecords("study", "deaths", new Filter("Id", animalId));
getApiHelper().deleteAllRecords("study", "departure", new Filter("Id", animalId));
getApiHelper().deleteAllRecords("study", "weight", new Filter("Id", animalId));
getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", animalId));

String[] demographicsFields = {"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"};
Object[][] demographicsData = {{animalId, "Rhesus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}};
getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), getApiHelper().prepareInsertCommand("study", "demographics", "lsid", demographicsFields, demographicsData), getExtraContext());

// The weight record carries the same taskid used by the death/departure rows below so the exclusion
// scenarios can prove that records belonging to the completing record's own task are excluded from
// the review-required check. Both scenarios are validate-only, so nothing persists between them.
String[] weightInsertFields = {"Id", "date", "weight", "taskid", FIELD_QCSTATELABEL, "performedby"};
Object[][] weightInsertData = {{animalId, new Date(), 8.5, taskId, EHRQCState.REVIEW_REQUIRED.label, 1004}};
getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), getApiHelper().prepareInsertCommand("study", "weight", "lsid", weightInsertFields, weightInsertData), getExtraContext());

String[] deathFields = {"Id", "date", "reason", "deathWeight", "taskid", FIELD_QCSTATELABEL, FIELD_OBJECTID, FIELD_LSID, "_recordid", "performedby"};
verifyCompletionBlockedByReviewRequired("deaths", "Death", deathFields,
new Object[]{animalId, new Date(), "4", 8.5, null, EHRQCState.COMPLETED.label, null, null, "recordID", 1004},
new Object[]{animalId, new Date(), "4", 8.5, taskId, EHRQCState.COMPLETED.label, null, null, "recordID", 1004});

String[] departureFields = {"Id", "date", "destination", "taskid", FIELD_QCSTATELABEL, FIELD_OBJECTID, FIELD_LSID, "_recordid", "performedby"};
verifyCompletionBlockedByReviewRequired("departure", "Departure", departureFields,
new Object[]{animalId, new Date(), "Oregon NPRC", null, EHRQCState.COMPLETED.label, null, null, "recordID", 1004},
new Object[]{animalId, new Date(), "Oregon NPRC", taskId, EHRQCState.COMPLETED.label, null, null, "recordID", 1004});
}

// Asserts that completing a record (death, departure) is blocked while other data for the animal is in
// Review Required state, and that a record whose taskid matches the review-required data is NOT blocked
// (its records move to Completed in the same save). rowWithoutTaskId/rowWithTaskId differ only in taskid.
private void verifyCompletionBlockedByReviewRequired(String queryName, String recordNoun, String[] fields, Object[] rowWithoutTaskId, Object[] rowWithTaskId)
{
// testValidationMessage defaults extraContext.targetQC to 'In Progress', and the global targetQC always
// overrides row-level QCStateLabel (see ehr/security.js normalizeQCState), so it must be forced to
// 'Completed' for the trigger to see a completing record.
Map<String, Object> completedTargetQC = Map.of("targetQC", EHRQCState.COMPLETED.label);

log("Completing a " + queryName + " record while other data is in Review Required state should be blocked");
Map<String, List<String>> expected = new HashMap<>();
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"));
getApiHelper().testValidationMessage(DATA_ADMIN.getEmail(), "study", queryName, fields, new Object[][]{rowWithoutTaskId}, expected, completedTargetQC);

log("Review Required records on the " + queryName + " record's own task should not block completion");
getApiHelper().testValidationMessage(DATA_ADMIN.getEmail(), "study", queryName, fields, new Object[][]{rowWithTaskId}, new HashMap<>(), completedTargetQC);
}

@Test
public void testClinicalCasesWorkflow()
{
Expand Down