diff --git a/Pipfile.lock b/Pipfile.lock index 9a099c52..82b1d9a5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -783,7 +783,7 @@ "spiffworkflow": { "editable": true, "git": "https://github.com/sartography/SpiffWorkflow.git", - "ref": "f7df6cfdc1487251a798930069a8a11a80fa30af" + "ref": "c3dc94deba2890a10d3b2b05d4a0dee54c83ed69" }, "sqlalchemy": { "hashes": [ diff --git a/crc/api.yml b/crc/api.yml index 868eaff1..0d556c0f 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -669,7 +669,35 @@ paths: capital_assyria: Assur responses: '201': - description: Returns the updated workflow with the task completed. + description: Returns the updated workflow with this task as the current task. + content: + application/json: + schema: + $ref: "#/components/schemas/Workflow" + /workflow/{workflow_id}/task/{task_id}/set_token: + parameters: + - name: workflow_id + in: path + required: true + description: The id of the workflow + schema: + type: integer + format: int32 + - name: task_id + in: path + required: true + description: The id of the task + schema: + type: string + format: uuid + put: + operationId: crc.api.workflow.set_current_task + summary: Attempts to make the given task the Current Active Task + tags: + - Workflows and Tasks + responses: + '201': + description: Returns the updated workflow with this task as the current task. content: application/json: schema: diff --git a/crc/api/workflow.py b/crc/api/workflow.py index 67c70fca..7b3dad9e 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -121,12 +121,28 @@ def delete(workflow_id): session.query(WorkflowModel).filter_by(id=workflow_id).delete() session.commit() +def set_current_task(workflow_id, task_id): + workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first() + processor = WorkflowProcessor(workflow_model) + task_id = uuid.UUID(task_id) + task = processor.bpmn_workflow.get_task(task_id) + task.reset_token(reset_data=False) # we could optionally clear the previous data. + workflow_model.bpmn_workflow_json = processor.serialize() + session.add(workflow_model) + session.commit() + + workflow_api_model = __get_workflow_api_model(processor) + update_workflow_stats(workflow_model, workflow_api_model) + return WorkflowApiSchema().dump(workflow_api_model) def update_task(workflow_id, task_id, body): workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first() processor = WorkflowProcessor(workflow_model) task_id = uuid.UUID(task_id) task = processor.bpmn_workflow.get_task(task_id) + if task.state != task.READY: + raise ApiError("invalid_state", "You may not update a task unless it is in the READY state. " + "Consider calling a token reset to make this task Ready.") task.update_data(body) processor.complete_task(task) processor.do_engine_steps() diff --git a/tests/test_study_api.py b/tests/test_study_api.py index 72e88914..ee1e6660 100644 --- a/tests/test_study_api.py +++ b/tests/test_study_api.py @@ -41,13 +41,23 @@ class TestStudyApi(BaseTest): study = session.query(StudyModel).first() self.assertIsNotNone(study) + @patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies @patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs - def test_get_study(self, mock_docs): + @patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details + @patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies + def test_get_study(self, mock_studies, mock_details, mock_docs, mock_investigators): """Generic test, but pretty detailed, in that the study should return a categorized list of workflows This starts with out loading the example data, to show that all the bases are covered from ground 0.""" + # Mock Protocol Builder responses + studies_response = self.protocol_builder_response('user_studies.json') + mock_studies.return_value = ProtocolBuilderStudySchema(many=True).loads(studies_response) + details_response = self.protocol_builder_response('study_details.json') + mock_details.return_value = json.loads(details_response) docs_response = self.protocol_builder_response('required_docs.json') mock_docs.return_value = json.loads(docs_response) + investigators_response = self.protocol_builder_response('investigators.json') + mock_investigators.return_value = json.loads(investigators_response) new_study = self.add_test_study() new_study = session.query(StudyModel).filter_by(id=new_study["id"]).first() @@ -114,10 +124,11 @@ class TestStudyApi(BaseTest): self.assertEqual(study.title, json_data['title']) self.assertEqual(study.protocol_builder_status.name, json_data['protocol_builder_status']) + @patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies @patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs @patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details @patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies - def test_get_all_studies(self, mock_studies, mock_details, mock_docs): + def test_get_all_studies(self, mock_studies, mock_details, mock_docs, mock_investigators): self.load_example_data() s = StudyModel( id=54321, # This matches one of the ids from the study_details_json data. @@ -136,6 +147,8 @@ class TestStudyApi(BaseTest): mock_details.return_value = json.loads(details_response) docs_response = self.protocol_builder_response('required_docs.json') mock_docs.return_value = json.loads(docs_response) + investigators_response = self.protocol_builder_response('investigators.json') + mock_investigators.return_value = json.loads(investigators_response) # Make the api call to get all studies api_response = self.app.get('/v1.0/study', headers=self.logged_in_headers(), content_type="application/json") @@ -167,12 +180,21 @@ class TestStudyApi(BaseTest): self.assertEqual(len(json_data), num_db_studies_after) self.assertEqual(num_open + num_active + num_incomplete + num_abandoned, num_db_studies_after) - + @patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies @patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs - def test_get_single_study(self, mock_docs): + @patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details + @patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies + def test_get_single_study(self, mock_studies, mock_details, mock_docs, mock_investigators): + # Mock Protocol Builder responses + studies_response = self.protocol_builder_response('user_studies.json') + mock_studies.return_value = ProtocolBuilderStudySchema(many=True).loads(studies_response) + details_response = self.protocol_builder_response('study_details.json') + mock_details.return_value = json.loads(details_response) docs_response = self.protocol_builder_response('required_docs.json') mock_docs.return_value = json.loads(docs_response) + investigators_response = self.protocol_builder_response('investigators.json') + mock_investigators.return_value = json.loads(investigators_response) self.load_example_data() study = session.query(StudyModel).first() diff --git a/tests/test_tasks_api.py b/tests/test_tasks_api.py index e3534c3b..2290a61a 100644 --- a/tests/test_tasks_api.py +++ b/tests/test_tasks_api.py @@ -3,6 +3,7 @@ import os from unittest.mock import patch from crc import session, app +from crc.api.common import ApiError from crc.models.api_models import WorkflowApiSchema, MultiInstanceType from crc.models.file import FileModelSchema, LookupDataSchema from crc.models.stats import WorkflowStatsModel, TaskEventModel @@ -23,7 +24,7 @@ class TestTasksApi(BaseTest): self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id) return workflow_api - def complete_form(self, workflow, task, dict_data): + def complete_form(self, workflow, task, dict_data, error_code = None): if isinstance(task, dict): task_id = task["id"] else: @@ -32,6 +33,10 @@ class TestTasksApi(BaseTest): headers=self.logged_in_headers(), content_type="application/json", data=json.dumps(dict_data)) + if error_code: + self.assert_failure(rv, error_code=error_code) + return + self.assert_success(rv) json_data = json.loads(rv.get_data(as_text=True)) @@ -329,3 +334,28 @@ class TestTasksApi(BaseTest): workflow_api = self.complete_form(workflow, task, {"name": "Dan"}) self.assertEquals(WorkflowStatus.complete, workflow_api.status) + def test_update_task_resets_token(self): + self.load_example_data() + workflow = self.create_workflow('exclusive_gateway') + + # Start the workflow. + tasks = self.get_workflow_api(workflow).user_tasks + self.complete_form(workflow, tasks[0], {"has_bananas": True}) + workflow = self.get_workflow_api(workflow) + self.assertEquals('Task_Num_Bananas', workflow.next_task['name']) + + # Trying to re-submit the initial task, and answer differently, should result in an error. + self.complete_form(workflow, tasks[0], {"has_bananas": False}, error_code="invalid_state") + + # Make the old task the current task. + rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, tasks[0].id), + headers=self.logged_in_headers(), + content_type="application/json") + self.assert_success(rv) + json_data = json.loads(rv.get_data(as_text=True)) + workflow = WorkflowApiSchema().load(json_data) + + # The next task should be a different value. + self.complete_form(workflow, tasks[0], {"has_bananas": False}) + workflow = self.get_workflow_api(workflow) + self.assertEquals('Task_Why_No_Bananas', workflow.next_task['name'])