Allow the workflow to be requested without making changes to the workflow - requires that you specify a read_only flag of true, otherwise it assumes that you want a fully prepared workflow with the next ready task set to run.
This commit is contained in:
parent
300026cbc8
commit
f15626033d
|
@ -622,6 +622,12 @@ paths:
|
|||
description: Set this to true to reset the workflow
|
||||
schema:
|
||||
type: boolean
|
||||
- name: read_only
|
||||
in: query
|
||||
required: false
|
||||
description: Does not run any automatic or script tasks and should not be used for updates.
|
||||
schema:
|
||||
type: boolean
|
||||
tags:
|
||||
- Workflows and Tasks
|
||||
responses:
|
||||
|
|
|
@ -24,6 +24,11 @@ class ApiError(Exception):
|
|||
instance.task_id = task.task_spec.name or ""
|
||||
instance.task_name = task.task_spec.description or ""
|
||||
instance.file_name = task.workflow.spec.file or ""
|
||||
|
||||
# Fixme: spiffworkflow is doing something weird where task ends up referenced in the data in some cases.
|
||||
if "task" in task.data:
|
||||
task.data.pop("task")
|
||||
|
||||
instance.task_data = task.data
|
||||
app.logger.error(message, exc_info=True)
|
||||
return instance
|
||||
|
|
|
@ -95,11 +95,18 @@ def delete_workflow_specification(spec_id):
|
|||
session.commit()
|
||||
|
||||
|
||||
def get_workflow(workflow_id, soft_reset=False, hard_reset=False):
|
||||
def get_workflow(workflow_id, soft_reset=False, hard_reset=False, read_only=False):
|
||||
"""Soft reset will attempt to update to the latest spec without starting over,
|
||||
Hard reset will update to the latest spec and start from the beginning.
|
||||
Read Only will return the workflow in a read only state, without running any
|
||||
engine tasks or logging any events. """
|
||||
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first()
|
||||
processor = WorkflowProcessor(workflow_model, soft_reset=soft_reset, hard_reset=hard_reset)
|
||||
workflow_api_model = WorkflowService.processor_to_workflow_api(processor)
|
||||
WorkflowService.update_task_assignments(processor)
|
||||
if not read_only:
|
||||
processor.do_engine_steps()
|
||||
processor.save()
|
||||
WorkflowService.update_task_assignments(processor)
|
||||
workflow_api_model = WorkflowService.processor_to_workflow_api(processor, read_only=read_only)
|
||||
return WorkflowApiSchema().dump(workflow_api_model)
|
||||
|
||||
|
||||
|
|
|
@ -143,7 +143,8 @@ class NavigationItemSchema(ma.Schema):
|
|||
|
||||
class WorkflowApi(object):
|
||||
def __init__(self, id, status, next_task, navigation,
|
||||
spec_version, is_latest_spec, workflow_spec_id, total_tasks, completed_tasks, last_updated, title):
|
||||
spec_version, is_latest_spec, workflow_spec_id, total_tasks, completed_tasks,
|
||||
last_updated, title, read_only):
|
||||
self.id = id
|
||||
self.status = status
|
||||
self.next_task = next_task # The next task that requires user input.
|
||||
|
@ -155,13 +156,14 @@ class WorkflowApi(object):
|
|||
self.completed_tasks = completed_tasks
|
||||
self.last_updated = last_updated
|
||||
self.title = title
|
||||
self.read_only = read_only
|
||||
|
||||
class WorkflowApiSchema(ma.Schema):
|
||||
class Meta:
|
||||
model = WorkflowApi
|
||||
fields = ["id", "status", "next_task", "navigation",
|
||||
"workflow_spec_id", "spec_version", "is_latest_spec", "total_tasks", "completed_tasks",
|
||||
"last_updated", "title"]
|
||||
"last_updated", "title", "read_only"]
|
||||
unknown = INCLUDE
|
||||
|
||||
status = EnumField(WorkflowStatus)
|
||||
|
@ -172,7 +174,7 @@ class WorkflowApiSchema(ma.Schema):
|
|||
def make_workflow(self, data, **kwargs):
|
||||
keys = ['id', 'status', 'next_task', 'navigation',
|
||||
'workflow_spec_id', 'spec_version', 'is_latest_spec', "total_tasks", "completed_tasks",
|
||||
"last_updated", "title"]
|
||||
"last_updated", "title", "read_only"]
|
||||
filtered_fields = {key: data[key] for key in keys}
|
||||
filtered_fields['next_task'] = TaskSchema().make_task(data['next_task'])
|
||||
return WorkflowApi(**filtered_fields)
|
||||
|
|
|
@ -117,7 +117,8 @@ class WorkflowProcessor(object):
|
|||
STUDY_ID_KEY = "study_id"
|
||||
VALIDATION_PROCESS_KEY = "validate_only"
|
||||
|
||||
def __init__(self, workflow_model: WorkflowModel, soft_reset=False, hard_reset=False, validate_only=False):
|
||||
def __init__(self, workflow_model: WorkflowModel,
|
||||
soft_reset=False, hard_reset=False, validate_only=False):
|
||||
"""Create a Workflow Processor based on the serialized information available in the workflow model.
|
||||
If soft_reset is set to true, it will try to use the latest version of the workflow specification
|
||||
without resetting to the beginning of the workflow. This will work for some minor changes to the spec.
|
||||
|
@ -180,10 +181,10 @@ class WorkflowProcessor(object):
|
|||
bpmn_workflow = BpmnWorkflow(spec, script_engine=self._script_engine)
|
||||
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = workflow_model.study_id
|
||||
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = validate_only
|
||||
try:
|
||||
bpmn_workflow.do_engine_steps()
|
||||
except WorkflowException as we:
|
||||
raise ApiError.from_task_spec("error_loading_workflow", str(we), we.sender)
|
||||
# try:
|
||||
# bpmn_workflow.do_engine_steps()
|
||||
# except WorkflowException as we:
|
||||
# raise ApiError.from_task_spec("error_loading_workflow", str(we), we.sender)
|
||||
return bpmn_workflow
|
||||
|
||||
def save(self):
|
||||
|
|
|
@ -216,7 +216,7 @@ class WorkflowService(object):
|
|||
return ''.join(random.choice(letters) for i in range(string_length))
|
||||
|
||||
@staticmethod
|
||||
def processor_to_workflow_api(processor: WorkflowProcessor, next_task=None):
|
||||
def processor_to_workflow_api(processor: WorkflowProcessor, next_task=None, read_only=False):
|
||||
"""Returns an API model representing the state of the current workflow, if requested, and
|
||||
possible, next_task is set to the current_task."""
|
||||
|
||||
|
@ -260,7 +260,8 @@ class WorkflowService(object):
|
|||
total_tasks=len(navigation),
|
||||
completed_tasks=processor.workflow_model.completed_tasks,
|
||||
last_updated=processor.workflow_model.last_updated,
|
||||
title=spec.display_name
|
||||
title=spec.display_name,
|
||||
read_only=read_only
|
||||
)
|
||||
if not next_task: # The Next Task can be requested to be a certain task, useful for parallel tasks.
|
||||
# This may or may not work, sometimes there is no next task to complete.
|
||||
|
|
|
@ -308,12 +308,13 @@ class BaseTest(unittest.TestCase):
|
|||
db.session.commit()
|
||||
return approval
|
||||
|
||||
def get_workflow_api(self, workflow, soft_reset=False, hard_reset=False, user_uid="dhf8r"):
|
||||
def get_workflow_api(self, workflow, soft_reset=False, hard_reset=False, read_only=False, user_uid="dhf8r"):
|
||||
user = session.query(UserModel).filter_by(uid=user_uid).first()
|
||||
self.assertIsNotNone(user)
|
||||
|
||||
rv = self.app.get('/v1.0/workflow/%i?soft_reset=%s&hard_reset=%s' %
|
||||
(workflow.id, str(soft_reset), str(hard_reset)),
|
||||
rv = self.app.get(f'/v1.0/workflow/{workflow.id}'
|
||||
f'?soft_reset={str(soft_reset)}'
|
||||
f'&hard_reset={str(hard_reset)}'
|
||||
f'&read_only={str(read_only)}',
|
||||
headers=self.logged_in_headers(user),
|
||||
content_type="application/json")
|
||||
self.assert_success(rv)
|
||||
|
|
|
@ -72,10 +72,10 @@ class TestFilesApi(BaseTest):
|
|||
self.assertEqual(file, file2)
|
||||
|
||||
def test_add_file_from_task_and_form_errors_on_invalid_form_field_name(self):
|
||||
self.load_example_data()
|
||||
self.create_reference_document()
|
||||
workflow = self.create_workflow('file_upload_form')
|
||||
processor = WorkflowProcessor(workflow)
|
||||
processor.do_engine_steps()
|
||||
task = processor.next_task()
|
||||
data = {'file': (io.BytesIO(b"abcdef"), 'random_fact.svg')}
|
||||
correct_name = task.task_spec.form.fields[0].id
|
||||
|
@ -96,6 +96,7 @@ class TestFilesApi(BaseTest):
|
|||
self.create_reference_document()
|
||||
workflow = self.create_workflow('file_upload_form')
|
||||
processor = WorkflowProcessor(workflow)
|
||||
processor.do_engine_steps()
|
||||
task = processor.next_task()
|
||||
data = {'file': (io.BytesIO(b"abcdef"), 'random_fact.svg')}
|
||||
correct_name = task.task_spec.form.fields[0].id
|
||||
|
|
|
@ -79,6 +79,7 @@ class TestStudyService(BaseTest):
|
|||
# Initialize the Workflow with the workflow processor.
|
||||
workflow_model = db.session.query(WorkflowModel).filter(WorkflowModel.id == workflow.id).first()
|
||||
processor = WorkflowProcessor(workflow_model)
|
||||
processor.do_engine_steps()
|
||||
|
||||
# Assure the workflow is now started, and knows the total and completed tasks.
|
||||
studies = StudyService.get_studies_for_user(user)
|
||||
|
|
|
@ -9,6 +9,7 @@ from crc import session, app
|
|||
from crc.models.api_models import WorkflowApiSchema, MultiInstanceType, TaskSchema
|
||||
from crc.models.file import FileModelSchema
|
||||
from crc.models.workflow import WorkflowStatus
|
||||
from crc.models.task_event import TaskEventModel
|
||||
|
||||
|
||||
class TestTasksApi(BaseTest):
|
||||
|
@ -42,6 +43,24 @@ class TestTasksApi(BaseTest):
|
|||
"""
|
||||
self.assertTrue(str.startswith(task.documentation, expected_docs))
|
||||
|
||||
def test_get_read_only_workflow(self):
|
||||
# Set up a new workflow
|
||||
workflow = self.create_workflow('two_forms')
|
||||
# get the first form in the two form workflow.
|
||||
workflow_api = self.get_workflow_api(workflow, read_only=True)
|
||||
|
||||
# There should be no task event logs related to the workflow at this point.
|
||||
task_events = session.query(TaskEventModel).filter(TaskEventModel.workflow_id == workflow.id).all()
|
||||
self.assertEqual(0, len(task_events))
|
||||
|
||||
# Since the workflow was not started, the call to read-only should not execute any engine steps the
|
||||
# current task should be the start event.
|
||||
self.assertEqual("Start", workflow_api.next_task.name)
|
||||
|
||||
# the workflow_api should have a read_only attribute set to true
|
||||
self.assertEquals(True, workflow_api.read_only)
|
||||
|
||||
|
||||
def test_two_forms_task(self):
|
||||
# Set up a new workflow
|
||||
self.load_example_data()
|
||||
|
@ -457,3 +476,5 @@ class TestTasksApi(BaseTest):
|
|||
workflow = self.get_workflow_api(workflow)
|
||||
self.assertEqual(WorkflowStatus.complete, workflow.status)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ class TestWorkflowProcessor(BaseTest):
|
|||
workflow_spec_model = self.load_test_spec("random_fact")
|
||||
study = session.query(StudyModel).first()
|
||||
processor = self.get_processor(study, workflow_spec_model)
|
||||
processor.do_engine_steps()
|
||||
self.assertEqual(study.id, processor.bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY])
|
||||
self.assertIsNotNone(processor)
|
||||
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
|
||||
|
@ -62,6 +63,7 @@ class TestWorkflowProcessor(BaseTest):
|
|||
files = session.query(FileModel).filter_by(workflow_spec_id='decision_table').all()
|
||||
self.assertEqual(2, len(files))
|
||||
processor = self.get_processor(study, workflow_spec_model)
|
||||
processor.do_engine_steps()
|
||||
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
|
||||
next_user_tasks = processor.next_user_tasks()
|
||||
self.assertEqual(1, len(next_user_tasks))
|
||||
|
@ -86,6 +88,7 @@ class TestWorkflowProcessor(BaseTest):
|
|||
workflow_spec_model = self.load_test_spec("parallel_tasks")
|
||||
study = session.query(StudyModel).first()
|
||||
processor = self.get_processor(study, workflow_spec_model)
|
||||
processor.do_engine_steps()
|
||||
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
|
||||
|
||||
# Complete the first steps of the 4 parallel tasks
|
||||
|
@ -127,6 +130,7 @@ class TestWorkflowProcessor(BaseTest):
|
|||
study = session.query(StudyModel).first()
|
||||
workflow_spec_model = self.load_test_spec("parallel_tasks")
|
||||
processor = self.get_processor(study, workflow_spec_model)
|
||||
processor.do_engine_steps()
|
||||
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
|
||||
next_user_tasks = processor.next_user_tasks()
|
||||
self.assertEqual(4, len(next_user_tasks))
|
||||
|
@ -215,6 +219,7 @@ class TestWorkflowProcessor(BaseTest):
|
|||
self.assertEqual(2, len(files))
|
||||
workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="docx").first()
|
||||
processor = self.get_processor(study, workflow_spec_model)
|
||||
processor.do_engine_steps()
|
||||
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
|
||||
next_user_tasks = processor.next_user_tasks()
|
||||
self.assertEqual(1, len(next_user_tasks))
|
||||
|
@ -278,6 +283,7 @@ class TestWorkflowProcessor(BaseTest):
|
|||
study = session.query(StudyModel).first()
|
||||
workflow_spec_model = self.load_test_spec("two_forms")
|
||||
processor = self.get_processor(study, workflow_spec_model)
|
||||
processor.do_engine_steps()
|
||||
self.assertEqual(processor.workflow_model.workflow_spec_id, workflow_spec_model.id)
|
||||
task = processor.next_task()
|
||||
task.data = {"color": "blue"}
|
||||
|
|
|
@ -47,6 +47,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
|
|||
workflow_spec_model = self.load_test_spec("multi_instance")
|
||||
study = session.query(StudyModel).first()
|
||||
processor = self.get_processor(study, workflow_spec_model)
|
||||
processor.bpmn_workflow.do_engine_steps()
|
||||
self.assertEqual(study.id, processor.bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY])
|
||||
self.assertIsNotNone(processor)
|
||||
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
|
||||
|
|
|
@ -89,7 +89,7 @@ class TestWorkflowSpecValidation(BaseTest):
|
|||
self.load_example_data()
|
||||
errors = self.validate_workflow("invalid_script")
|
||||
self.assertEqual(2, len(errors))
|
||||
self.assertEqual("error_loading_workflow", errors[0]['code'])
|
||||
self.assertEqual("workflow_validation_exception", errors[0]['code'])
|
||||
self.assertTrue("NoSuchScript" in errors[0]['message'])
|
||||
self.assertEqual("Invalid_Script_Task", errors[0]['task_id'])
|
||||
self.assertEqual("An Invalid Script Reference", errors[0]['task_name'])
|
||||
|
@ -99,7 +99,7 @@ class TestWorkflowSpecValidation(BaseTest):
|
|||
self.load_example_data()
|
||||
errors = self.validate_workflow("invalid_script2")
|
||||
self.assertEqual(2, len(errors))
|
||||
self.assertEqual("error_loading_workflow", errors[0]['code'])
|
||||
self.assertEqual("workflow_validation_exception", errors[0]['code'])
|
||||
self.assertEqual("Invalid_Script_Task", errors[0]['task_id'])
|
||||
self.assertEqual("An Invalid Script Reference", errors[0]['task_name'])
|
||||
self.assertEqual("invalid_script2.bpmn", errors[0]['file_name'])
|
||||
|
|
Loading…
Reference in New Issue