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:
parent
5e30e20b67
commit
13333fb306
|
@ -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": [
|
||||||
|
|
30
crc/api.yml
30
crc/api.yml
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
Loading…
Reference in New Issue