Repository: EdinburghGenomics/clarity_scripts

Issue:
Route samples after "Awaiting User Response" to Remove Step
At the end of "Awaiting User Response" artifacts that do not have UDF "Proceed To SeqLab" == true
Should be routed and run through the Remove from workflow step.
this can be done in `assign_workflow_preseqlab.py` or another independent script

Discussion:

Patch:
diff --git a/scripts/assign_workflow_preseqlab.py b/scripts/assign_workflow_preseqlab.py
index e539d68..5652fbb 100644
--- a/scripts/assign_workflow_preseqlab.py
+++ b/scripts/assign_workflow_preseqlab.py
@@ -1,15 +1,41 @@
 #!/usr/bin/env python
+import platform
+
+import time
+from pyclarity_lims.entities import Step
+from requests import HTTPError
+
 from EPPs.common import StepEPP, get_workflow_stage, find_newest_artifact_originating_from
 
 
 class AssignWorkflowPreSeqLab(StepEPP):
+
+    @staticmethod
+    def _finish_step(step, try_count=1):
+        """
+        This function will try to advance a step three time waiting for a ongoing program to finish.
+        It waits for 5 seconds in between each try
+        """
+        try:
+            step.get(force=True)
+            step.advance()
+        except HTTPError as e:
+            if try_count < 3 and str(e) == '400: Cannot advance a step that has an external program queued, ' \
+                                           'running or pending acknowledgement':
+                # wait for whatever needs to happen to happen
+                time.sleep(5)
+                AssignWorkflowPreSeqLab._finish_step(step, try_count=try_count + 1)
+            else:
+                raise e
+
     def _run(self):
-        artifacts_to_route = set()
+        artifacts_to_route_to_seq_lab = set()
+        artifacts_to_route_to_remove = set()
         for art in self.artifacts:
             sample = art.samples[0]
             if sample.udf.get("Proceed To SeqLab") and not sample.udf.get("2D Barcode"):
                 # checks to see if sample is in plate or fluidX tube
-                artifacts_to_route.add(sample.artifact)
+                artifacts_to_route_to_seq_lab.add(sample.artifact)
 
             elif sample.udf.get("Proceed To SeqLab") and sample.udf.get("2D Barcode"):
                 artifact = find_newest_artifact_originating_from(
@@ -17,12 +43,37 @@ class AssignWorkflowPreSeqLab(StepEPP):
                     process_type="FluidX Transfer From Rack Into Plate EG 1.0 ST",
                     sample_name=sample.name
                 )
-                artifacts_to_route.add(artifact)
+                artifacts_to_route_to_seq_lab.add(artifact)
+
+            else:
+                artifacts_to_route_to_remove.add(sample.artifact)
 
-        if artifacts_to_route:
+        if artifacts_to_route_to_seq_lab:
             # Only route artifacts if there are any
             stage = get_workflow_stage(self.lims, "PreSeqLab EG 6.0", "Sequencing Plate Preparation EG 2.0")
-            self.lims.route_artifacts(list(artifacts_to_route), stage_uri=stage.uri)
+            self.lims.route_artifacts(list(artifacts_to_route_to_seq_lab), stage_uri=stage.uri)
+
+        if artifacts_to_route_to_remove:
+            stage = get_workflow_stage(self.lims, "Remove From Processing EG 1.0 WF", "Remove From Processing EG 1.0 ST")
+            self.lims.route_artifacts(list(artifacts_to_route_to_remove), stage_uri=stage.uri)
+
+            # Create new step with the routed artifacts
+            s = Step.create(self.lims, protocol_step=stage.step, inputs=artifacts_to_route_to_remove,
+                            container_type_name='Tube')
+
+            url = 'https://%s/clarity/work-complete/%s' % (platform.node(), self.process.id.split('-')[1])
+            s.details.udf['Reason for removal from processing:'] = 'Failed QC. See step %s' % url
+            s.details.put()
+
+            # Move from "Record detail" window to the "Next Step"
+            s.advance()
+
+            for next_action in s.actions.next_actions:
+                next_action['action'] = 'complete'
+            s.actions.put()
+
+            # Complete the step
+            self._finish_step(s)
 
 
 if __name__ == "__main__":


Test patch:
diff --git a/tests/test_assign_to_workflow_preseqlab.py b/tests/test_assign_to_workflow_preseqlab.py
index de2ef85..ff58eff 100644
--- a/tests/test_assign_to_workflow_preseqlab.py
+++ b/tests/test_assign_to_workflow_preseqlab.py
@@ -1,3 +1,6 @@
+import pytest
+from requests import HTTPError
+
 from scripts.assign_workflow_preseqlab import AssignWorkflowPreSeqLab
 from tests.test_common import TestEPP, fake_artifact
 from unittest.mock import Mock, patch, PropertyMock
@@ -7,7 +10,8 @@ def fake_all_inputs(unique=False, resolve=False):
     """Return a list of mocked artifacts which contain samples which contain artifacts ... Simple!"""
     return (
         Mock(samples=[Mock(artifact=fake_artifact('a1'), id='s1', udf={'Proceed To SeqLab': True})]),
-        Mock(samples=[Mock(artifact=fake_artifact('a2'), id='s2', udf={'Proceed To SeqLab': True, '2D Barcode': 'fluidX1'})])
+        Mock(samples=[Mock(artifact=fake_artifact('a2'), id='s2', udf={'Proceed To SeqLab': True, '2D Barcode': 'fluidX1'})]),
+        Mock(samples=[Mock(artifact=fake_artifact('a3'), id='s3', udf={'Proceed To SeqLab': False})])
     )
 
 
@@ -16,22 +20,69 @@ class TestAssignWorkflowPreSeqLab(TestEPP):
         self.patched_process = patch.object(
             AssignWorkflowPreSeqLab,
             'process',
-            new_callable=PropertyMock(return_value=Mock(all_inputs=fake_all_inputs))
+            new_callable=PropertyMock(return_value=Mock(id='24-1234', all_inputs=fake_all_inputs))
         )
+        self.workflow_stage = Mock(uri='a_uri')
         self.patched_get_workflow_stage = patch(
             'scripts.assign_workflow_preseqlab.get_workflow_stage',
-            return_value=Mock(uri='a_uri')
+            return_value=self.workflow_stage
         )
         self.patch_find_art = patch(
             'scripts.assign_workflow_preseqlab.find_newest_artifact_originating_from',
             return_value=Mock(id='fx2')
         )
+        self.mocked_step = Mock(details=Mock(udf={}), actions=Mock(next_actions=[{}]))
+        self.patch_Step_create = patch('scripts.assign_workflow_preseqlab.Step.create', return_value=self.mocked_step)
         self.epp = AssignWorkflowPreSeqLab(self.default_argv)
 
     def test_assign(self):
-        with self.patched_get_workflow_stage as pws, self.patched_lims, self.patched_process, self.patch_find_art:
+        with self.patched_get_workflow_stage as pws, self.patched_lims, self.patched_process, self.patch_find_art, \
+             self.patch_Step_create as psc:
             self.epp._run()
 
-            pws.assert_called_with(self.epp.lims, 'PreSeqLab EG 6.0', 'Sequencing Plate Preparation EG 2.0')
-            assert sorted([a.id for a in self.epp.lims.route_artifacts.call_args[0][0]]) == ['a1', 'fx2']
-            assert self.epp.lims.route_artifacts.call_args[1] == {'stage_uri': 'a_uri'}
+            pws.assert_any_call(self.epp.lims, 'PreSeqLab EG 6.0', 'Sequencing Plate Preparation EG 2.0')
+            first_call = self.epp.lims.route_artifacts.call_args_list[0]
+            assert sorted([a.id for a in first_call[0][0]]) == ['a1', 'fx2']
+            assert first_call[1] == {'stage_uri': 'a_uri'}
+
+            pws.assert_any_call(self.epp.lims, "Remove From Processing EG 1.0 WF", "Remove From Processing EG 1.0 ST")
+            second_call = self.epp.lims.route_artifacts.call_args_list[1]
+            assert sorted([a.id for a in second_call[0][0]]) == ['a3']
+            assert second_call[1] == {'stage_uri': 'a_uri'}
+            # Test the Step creation
+            psc.assert_called_with(
+                self.epp.lims,
+                inputs=set([self.epp.artifacts[2].samples[0].artifact]),
+                protocol_step=self.workflow_stage.step,
+                container_type_name='Tube'
+            )
+            # Test udf in details
+            assert 'Reason for removal from processing:' in self.mocked_step.details.udf
+            self.mocked_step.details.put.assert_called_with()
+            # Test next actions has been set.
+            assert self.mocked_step.actions.next_actions == [{'action': 'complete'}]
+            self.mocked_step.actions.put.assert_called_with()
+
+            # Test advance has been called.
+            assert self.mocked_step.advance.call_count == 2
+
+    def test_finish_step(self):
+        # Successful attempt
+        step = Mock()
+        self.epp._finish_step(step)
+        assert step.get.call_count == 1
+        assert step.advance.call_count == 1
+
+        # 3 failed attempts before raising
+        step = Mock(
+            advance=Mock(side_effect=HTTPError('400: Cannot advance a step that has an external program queued, '
+                                               'running or pending acknowledgement'))
+        )
+        with patch('time.sleep'):
+            with pytest.raises(HTTPError):
+                self.epp._finish_step(step)
+        assert step.get.call_count == 3
+        assert step.advance.call_count == 3
+
+
+