-
Notifications
You must be signed in to change notification settings - Fork 7
Workflow: Add Update Sample Status Action #7667
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
labkey-alan
wants to merge
5
commits into
develop
Choose a base branch
from
fb_workflow_sample_status
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3e59705
Add UpdateSampleStatus action type and validation
labkey-alan 51934ab
workflow/Action: Account for removeFromStorage key when validating wo…
labkey-alan fe60182
Action: update validateStatus to check for consumed status when using…
labkey-alan f44a0e1
Action::validateStatus - refactor to make validation logic more robus…
labkey-alan 5114c94
Action: use more specific Exception classes, add helper for status ke…
labkey-alan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,14 +2,18 @@ | |||||
|
|
||||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||||
| import org.apache.commons.lang3.StringUtils; | ||||||
| import org.jetbrains.annotations.Nullable; | ||||||
| import org.json.JSONArray; | ||||||
| import org.json.JSONException; | ||||||
| import org.json.JSONObject; | ||||||
| import org.labkey.api.data.Container; | ||||||
| import org.labkey.api.data.ContainerManager; | ||||||
| import org.labkey.api.data.CreatedModified; | ||||||
| import org.labkey.api.exp.api.ExpProtocol; | ||||||
| import org.labkey.api.exp.api.ExperimentService; | ||||||
| import org.labkey.api.exp.api.SampleTypeService; | ||||||
| import org.labkey.api.exp.query.ExpSchema; | ||||||
| import org.labkey.api.qc.DataState; | ||||||
| import org.labkey.api.qc.SampleStatusService; | ||||||
| import org.labkey.api.util.GUID; | ||||||
|
|
||||||
|
|
@@ -26,6 +30,7 @@ public abstract class Action extends CreatedModified | |||||
| public static final String ASSAY_TYPES_KEY = "assayTypes"; | ||||||
| public static final String NUM_PER_PARENT_KEY = "numPerParent"; | ||||||
| public static final String UPDATE_STATUS_KEY = "updateStatus"; | ||||||
| public static final String REMOVE_FROM_STORAGE_KEY = "removeFromStorage"; | ||||||
| public static final String STATUS_KEY = "sampleStatus"; | ||||||
| protected Long _rowId; | ||||||
| protected int _ordinal; | ||||||
|
|
@@ -126,6 +131,88 @@ public void setInputParameters(JSONObject inputParameters) | |||||
| _inputParameters = inputParameters; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Validates that the status parameters are valid, according to the following rules: | ||||||
| * - If updateKeyRequired is true, then STATUS_KEY and REMOVE_FROM_STORAGE_KEY may only be present if | ||||||
| * UPDATE_STATUS_KEY is true | ||||||
| * - STATUS_KEY is optional, but when present the value must be a valid status | ||||||
| * - REMOVE_FROM_STORAGE_KEY may only be present when STATUS_KEY is also present | ||||||
| * - If REMOVE_STORAGE_KEY is true, then the value for STATUS_KEY must represent a consumed state | ||||||
| */ | ||||||
| private @Nullable String validateStatus(Container container, String prefix, boolean updateKeyRequired) | ||||||
| { | ||||||
| boolean hasStatusKey = _inputParameters.has(STATUS_KEY); | ||||||
| boolean hasUpdateStatusKey = _inputParameters.has(UPDATE_STATUS_KEY); | ||||||
| boolean hasRemoveFromStorageKey = _inputParameters.has(REMOVE_FROM_STORAGE_KEY); | ||||||
|
|
||||||
| boolean updateStatus; | ||||||
| try | ||||||
| { | ||||||
| updateStatus = hasUpdateStatusKey && _inputParameters.getBoolean(UPDATE_STATUS_KEY); | ||||||
| } | ||||||
| catch (JSONException e) | ||||||
| { | ||||||
| return prefix + UPDATE_STATUS_KEY + " must be a boolean."; | ||||||
| } | ||||||
|
|
||||||
| boolean removeFromStorage; | ||||||
| try | ||||||
| { | ||||||
| removeFromStorage = hasRemoveFromStorageKey && _inputParameters.getBoolean(REMOVE_FROM_STORAGE_KEY); | ||||||
| } | ||||||
| catch (JSONException e) | ||||||
| { | ||||||
| return prefix + REMOVE_FROM_STORAGE_KEY + " must be a boolean."; | ||||||
| } | ||||||
|
|
||||||
| // If we're expecting an UPDATE_STATUS_KEY (aliquot / derive / pool actions), then the key must be present and | ||||||
| // set to true in order for us to honor the STATUS_KEY and REMOVE_FROM_STORAGE_KEY. | ||||||
| if (updateKeyRequired && !updateStatus) | ||||||
| { | ||||||
| if (hasStatusKey) | ||||||
| return prefix + STATUS_KEY + " is not allowed for action of type " + _type + " when " + UPDATE_STATUS_KEY + " is false."; | ||||||
|
|
||||||
| if (hasRemoveFromStorageKey) | ||||||
| return prefix + REMOVE_FROM_STORAGE_KEY + " is not allowed for action of type " + _type + " when " + UPDATE_STATUS_KEY + " is false."; | ||||||
| } | ||||||
|
|
||||||
| if (removeFromStorage && !hasStatusKey) | ||||||
| return prefix + STATUS_KEY + " is required for action of type " + _type + " when " + REMOVE_FROM_STORAGE_KEY + " is true."; | ||||||
|
|
||||||
| if (hasStatusKey && container != null) | ||||||
| { | ||||||
| long statusId; | ||||||
| try | ||||||
| { | ||||||
| statusId = _inputParameters.getLong(STATUS_KEY); | ||||||
| } | ||||||
| catch (JSONException e) | ||||||
| { | ||||||
| return prefix + STATUS_KEY + " must be a number."; | ||||||
| } | ||||||
|
|
||||||
| DataState state = SampleStatusService.get().getStateForRowId(container, statusId); | ||||||
|
|
||||||
| if (state == null) | ||||||
| return prefix + "Invalid " + STATUS_KEY + " (" + statusId + ")."; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps:
Suggested change
|
||||||
|
|
||||||
| if (removeFromStorage && !ExpSchema.SampleStateType.Consumed.name().equals(state.getStateType())) | ||||||
| return prefix + STATUS_KEY + " (" + statusId + ") must represent a " + ExpSchema.SampleStateType.Consumed.name() + " state when " + REMOVE_FROM_STORAGE_KEY + " is true."; | ||||||
| } | ||||||
|
|
||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| private static boolean isSampleStatusKey(String key) | ||||||
| { | ||||||
| return key.equals(STATUS_KEY) || key.equals(UPDATE_STATUS_KEY) || key.equals(REMOVE_FROM_STORAGE_KEY); | ||||||
| } | ||||||
|
|
||||||
| private boolean hasAnySampleStatusKey() | ||||||
| { | ||||||
| return _inputParameters != null && (_inputParameters.has(UPDATE_STATUS_KEY) || _inputParameters.has(STATUS_KEY) || _inputParameters.has(REMOVE_FROM_STORAGE_KEY)); | ||||||
| } | ||||||
|
|
||||||
| @JsonIgnore | ||||||
| public List<String> validateInputParameters(int ordinal, Container container) | ||||||
| { | ||||||
|
|
@@ -169,31 +256,57 @@ public List<String> validateInputParameters(int ordinal, Container container) | |||||
| } | ||||||
| else if (_type == WorkflowService.ActionType.AliquotSamples) | ||||||
| { | ||||||
| List<String> messages = new ArrayList<>(); | ||||||
|
|
||||||
| if (_inputParameters == null || !_inputParameters.has(NUM_PER_PARENT_KEY)) | ||||||
| return List.of(prefix + NUM_PER_PARENT_KEY + " is required for action of type " + _type + "."); | ||||||
| messages.add(prefix + NUM_PER_PARENT_KEY + " is required for action of type " + _type + "."); | ||||||
| else | ||||||
| { | ||||||
| try { | ||||||
| int numPerParent = _inputParameters.getInt(NUM_PER_PARENT_KEY); | ||||||
| if (numPerParent < 0) | ||||||
| return List.of(prefix + NUM_PER_PARENT_KEY + " cannot be negative."); | ||||||
| messages.add(prefix + NUM_PER_PARENT_KEY + " cannot be negative."); | ||||||
| } | ||||||
| catch (Exception e) { | ||||||
| return List.of(prefix + NUM_PER_PARENT_KEY + " must be an integer."); | ||||||
| messages.add(prefix + NUM_PER_PARENT_KEY + " must be an integer."); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if (hasAnySampleStatusKey()) | ||||||
| { | ||||||
| String statusMessage = validateStatus(container, prefix, true); | ||||||
| if (statusMessage != null) messages.add(statusMessage); | ||||||
| } | ||||||
|
|
||||||
| return messages; | ||||||
| } | ||||||
| else if (_type == WorkflowService.ActionType.DeriveSamples || _type == WorkflowService.ActionType.PoolSamples) | ||||||
| { | ||||||
| if (_inputParameters == null || _inputParameters.isEmpty()) | ||||||
| return List.of(prefix + "data about sample types and sample counts per parent is required for action of type " + _type + "."); | ||||||
| if (_type == WorkflowService.ActionType.PoolSamples && _inputParameters.length() > 1) | ||||||
| String emptyMessage = prefix + "data about sample types and sample counts per parent is required for action of type " + _type + "."; | ||||||
|
|
||||||
| if (_inputParameters == null) return List.of(emptyMessage); | ||||||
|
|
||||||
| // We can't just check _inputParameters size because it may include sample status keys, so we extract the | ||||||
| // sample type IDs and validate against those. | ||||||
| List<String> sampleTypeIds = new ArrayList<>(); | ||||||
| _inputParameters.keys().forEachRemaining(id -> { | ||||||
| if (isSampleStatusKey(id)) return; | ||||||
| sampleTypeIds.add(id); | ||||||
| }); | ||||||
|
|
||||||
| if (sampleTypeIds.isEmpty()) | ||||||
| return List.of(emptyMessage); | ||||||
|
|
||||||
| if (_type == WorkflowService.ActionType.PoolSamples && sampleTypeIds.size() > 1) | ||||||
| return List.of(prefix + "only one sample type can be specified for action of type " + _type + "."); | ||||||
|
|
||||||
| SampleTypeService sampleTypeService = SampleTypeService.get(); | ||||||
| Set<String> invalidSampleTypeIds = new HashSet<>(); | ||||||
| List<Object> invalidCounts = new ArrayList<>(); | ||||||
| List<String> messages = new ArrayList<>(); | ||||||
|
|
||||||
| _inputParameters.keys().forEachRemaining(id -> { | ||||||
| for (String id : sampleTypeIds) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| if (sampleTypeService.getSampleType(Long.valueOf(id)) == null) | ||||||
|
|
@@ -204,55 +317,53 @@ else if (_type == WorkflowService.ActionType.DeriveSamples || _type == WorkflowS | |||||
| invalidSampleTypeIds.add(id); | ||||||
| } | ||||||
| Object countObj = _inputParameters.get(id); | ||||||
| if ((countObj instanceof String)) | ||||||
| if (countObj instanceof String countStr) | ||||||
| try | ||||||
| { | ||||||
| if (Integer.parseInt((String) countObj) < 0) | ||||||
| if (Integer.parseInt(countStr) < 0) | ||||||
| invalidCounts.add(countObj); | ||||||
| } | ||||||
| catch (NumberFormatException e) | ||||||
| { | ||||||
| invalidCounts.add(countObj); | ||||||
| } | ||||||
| else if (countObj instanceof Integer) | ||||||
| else if (countObj instanceof Integer count) | ||||||
| { | ||||||
| if (((Integer) countObj) < 0) | ||||||
| if (count < 0) | ||||||
| invalidCounts.add(countObj); | ||||||
| } | ||||||
| else | ||||||
| invalidCounts.add(countObj); | ||||||
| }); | ||||||
| List<String> messages = new ArrayList<>(); | ||||||
| } | ||||||
|
|
||||||
| if (!invalidSampleTypeIds.isEmpty()) | ||||||
| messages.add(prefix + "invalid sample type IDs " + invalidSampleTypeIds + "."); | ||||||
| if (!invalidCounts.isEmpty()) | ||||||
| messages.add(prefix + "invalid sample count values " + invalidCounts + "."); | ||||||
|
|
||||||
| if (hasAnySampleStatusKey()) | ||||||
| { | ||||||
| String statusMessage = validateStatus(container, prefix, true); | ||||||
|
|
||||||
| if (statusMessage != null) messages.add(statusMessage); | ||||||
| } | ||||||
|
|
||||||
| return messages; | ||||||
| } | ||||||
| else if (_type == WorkflowService.ActionType.RemoveFromStorage) | ||||||
| else if (_type == WorkflowService.ActionType.RemoveFromStorage || _type == WorkflowService.ActionType.UpdateSampleStatus) | ||||||
| { | ||||||
| if (_inputParameters == null || _inputParameters.isEmpty()) | ||||||
| return Collections.emptyList(); | ||||||
| boolean updateStatus = _inputParameters.getBoolean(UPDATE_STATUS_KEY); | ||||||
| if (updateStatus && !_inputParameters.has(STATUS_KEY)) | ||||||
| return List.of(prefix + STATUS_KEY + " is required for action of type " + _type + " when " + UPDATE_STATUS_KEY + " is true."); | ||||||
| if (!updateStatus && _inputParameters.has(STATUS_KEY)) | ||||||
| return List.of(prefix + STATUS_KEY + " is not allowed for action of type " + _type + " when " + UPDATE_STATUS_KEY + " is false."); | ||||||
| if (updateStatus && container != null) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| long statusId = _inputParameters.getLong(STATUS_KEY); | ||||||
| SampleStatusService sampleStatusService = SampleStatusService.get(); | ||||||
| if (sampleStatusService.getStateForRowId(container, statusId) == null) | ||||||
| return List.of(prefix + "Invalid " + STATUS_KEY + " (" + statusId + ")."); | ||||||
| } | ||||||
| catch (Exception e) | ||||||
| { | ||||||
| return List.of(prefix + "Invalid " + STATUS_KEY + "."); | ||||||
| } | ||||||
| } | ||||||
| } else { | ||||||
|
|
||||||
| boolean updateKeyRequired = _type == WorkflowService.ActionType.RemoveFromStorage; | ||||||
| String statusMessage = validateStatus(container, prefix, updateKeyRequired); | ||||||
|
|
||||||
| if (statusMessage != null) return List.of(statusMessage); | ||||||
|
|
||||||
| return Collections.emptyList(); | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| if (_inputParameters != null && !_inputParameters.isEmpty()) | ||||||
| return List.of(prefix + "input parameters are not allowed for action of type " + _type + "."); | ||||||
| } | ||||||
|
|
||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.