Adding an API endpoint that allows setting the token on the workflow to a specific task.

Added error checking such that attempting to submit data for a task that is not in the "READY" state throws an error message.
For some reason I'm getting lots of errors in the tests as they try to hit API endpoints they were not hitting before, so adding a number of mocks to some of the study service tests.
This commit is contained in:
Dan Funk 2020-04-28 17:25:53 -04:00
parent 5e30e20b67
commit 13333fb306
5 changed files with 103 additions and 7 deletions

2
Pipfile.lock generated
View File

@ -783,7 +783,7 @@
"spiffworkflow": { "spiffworkflow": {
"editable": true, "editable": true,
"git": "https://github.com/sartography/SpiffWorkflow.git", "git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "f7df6cfdc1487251a798930069a8a11a80fa30af" "ref": "c3dc94deba2890a10d3b2b05d4a0dee54c83ed69"
}, },
"sqlalchemy": { "sqlalchemy": {
"hashes": [ "hashes": [

View File

@ -669,7 +669,35 @@ paths:
capital_assyria: Assur capital_assyria: Assur
responses: responses:
'201': '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: content:
application/json: application/json:
schema: schema:

View File

@ -121,12 +121,28 @@ def delete(workflow_id):
session.query(WorkflowModel).filter_by(id=workflow_id).delete() session.query(WorkflowModel).filter_by(id=workflow_id).delete()
session.commit() 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): def update_task(workflow_id, task_id, body):
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first() workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
processor = WorkflowProcessor(workflow_model) processor = WorkflowProcessor(workflow_model)
task_id = uuid.UUID(task_id) task_id = uuid.UUID(task_id)
task = processor.bpmn_workflow.get_task(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) task.update_data(body)
processor.complete_task(task) processor.complete_task(task)
processor.do_engine_steps() processor.do_engine_steps()

View File

@ -41,13 +41,23 @@ class TestStudyApi(BaseTest):
study = session.query(StudyModel).first() study = session.query(StudyModel).first()
self.assertIsNotNone(study) self.assertIsNotNone(study)
@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_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 """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.""" 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') docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response) 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 = self.add_test_study()
new_study = session.query(StudyModel).filter_by(id=new_study["id"]).first() 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.title, json_data['title'])
self.assertEqual(study.protocol_builder_status.name, json_data['protocol_builder_status']) 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_required_docs') # mock_docs
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details @patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies @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() self.load_example_data()
s = StudyModel( s = StudyModel(
id=54321, # This matches one of the ids from the study_details_json data. 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) mock_details.return_value = json.loads(details_response)
docs_response = self.protocol_builder_response('required_docs.json') docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response) 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 # 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") 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(len(json_data), num_db_studies_after)
self.assertEqual(num_open + num_active + num_incomplete + num_abandoned, 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 @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') docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response) 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() self.load_example_data()
study = session.query(StudyModel).first() study = session.query(StudyModel).first()

View File

@ -3,6 +3,7 @@ import os
from unittest.mock import patch from unittest.mock import patch
from crc import session, app from crc import session, app
from crc.api.common import ApiError
from crc.models.api_models import WorkflowApiSchema, MultiInstanceType from crc.models.api_models import WorkflowApiSchema, MultiInstanceType
from crc.models.file import FileModelSchema, LookupDataSchema from crc.models.file import FileModelSchema, LookupDataSchema
from crc.models.stats import WorkflowStatsModel, TaskEventModel 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) self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id)
return workflow_api 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): if isinstance(task, dict):
task_id = task["id"] task_id = task["id"]
else: else:
@ -32,6 +33,10 @@ class TestTasksApi(BaseTest):
headers=self.logged_in_headers(), headers=self.logged_in_headers(),
content_type="application/json", content_type="application/json",
data=json.dumps(dict_data)) data=json.dumps(dict_data))
if error_code:
self.assert_failure(rv, error_code=error_code)
return
self.assert_success(rv) self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True)) 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"}) workflow_api = self.complete_form(workflow, task, {"name": "Dan"})
self.assertEquals(WorkflowStatus.complete, workflow_api.status) 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'])