Expose a flag on the workflow model in the api to shown if it is using the latest spec. Added a soft_reset and hard_reset onto the workflow endpoint that will allow you to cause a hard or soft reset.

This commit is contained in:
Dan Funk 2020-03-05 16:45:44 -05:00
parent 7b21b78987
commit 906bacff6a
6 changed files with 67 additions and 26 deletions

View File

@ -533,6 +533,19 @@ paths:
get:
operationId: crc.api.workflow.get_workflow
summary: Detailed information for a specific workflow instance
parameters:
- name: soft_reset
in: query
required: false
description: Set this to true to use the latest workflow specification to load minor modifications to the spec.
schema:
type: boolean
- name: hard_reset
in: query
required: false
description: Set this to true to reset the workflow
schema:
type: boolean
tags:
- Workflows and Tasks
responses:

View File

@ -75,17 +75,18 @@ def __get_workflow_api_model(processor: WorkflowProcessor):
next_task=None,
user_tasks=user_tasks,
workflow_spec_id=processor.workflow_spec_id,
spec_version=processor.get_spec_version()
spec_version=processor.get_spec_version(),
is_latest_spec=processor.get_spec_version() == processor.get_latest_version_string(processor.workflow_spec_id)
)
if processor.next_task():
workflow_api.next_task = Task.from_spiff(processor.next_task())
return workflow_api
def get_workflow(workflow_id):
def get_workflow(workflow_id, soft_reset=False, hard_reset=False):
schema = WorkflowApiSchema()
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
processor = WorkflowProcessor(workflow_model)
processor = WorkflowProcessor(workflow_model, soft_reset=soft_reset, hard_reset=hard_reset)
return schema.dump(__get_workflow_api_model(processor))

View File

@ -95,7 +95,7 @@ class TaskSchema(ma.Schema):
class WorkflowApi(object):
def __init__(self, id, status, user_tasks, last_task, next_task, workflow_spec_id, spec_version):
def __init__(self, id, status, user_tasks, last_task, next_task, workflow_spec_id, spec_version, is_latest_spec):
self.id = id
self.status = status
self.user_tasks = user_tasks
@ -103,11 +103,15 @@ class WorkflowApi(object):
self.next_task = next_task
self.workflow_spec_id = workflow_spec_id
self.spec_version = spec_version
self.is_latest_spec = is_latest_spec
class WorkflowApiSchema(ma.Schema):
class Meta:
model = WorkflowApi
fields = ["id", "status", "user_tasks", "last_task", "next_task", "workflow_spec_id", "spec_version"]
fields = ["id", "status",
"user_tasks", "last_task", "next_task",
"workflow_spec_id",
"spec_version", "is_latest_spec"]
unknown = INCLUDE
status = EnumField(WorkflowStatus)

View File

@ -92,7 +92,7 @@ class WorkflowProcessor(object):
STUDY_ID_KEY = "study_id"
def __init__(self, workflow_model: WorkflowModel, soft_reset=False, hard_reset=False):
"""Create a Worflow Processor based on the serialized information available in the workflow model.
"""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.
If hard_reset is set to true, it will create a new Workflow, but embed the data from the last
completed task in the previous workflow.
@ -118,14 +118,36 @@ class WorkflowProcessor(object):
return parser
@staticmethod
def __get_file_models_for_version(workflow_spec_id, version):
"""Version is in the format v[VERSION] [FILE_ID_LIST]
def get_latest_version_string(workflow_spec_id):
"""Version is in the format v[VERSION] (FILE_ID_LIST)
For example, a single bpmn file with only one version would be
v1 [12] Where 12 is the id of the file that is used to create the
v1 (12) Where 12 is the id of the file data model that is used to create the
specification. If multiple files exist, they are added on in
dot notation to both the version number and the file list. So
a Spec that includes a BPMN, DMN, an a Word file all on the first
version would be v1.1.1 [12,45,21]"""
version would be v1.1.1 (12.45.21)"""
# this could potentially become expensive to load all the data in the data models.
# in which case we might consider using a deferred loader for the actual data, but
# trying not to pre-optimize.
file_data_models = WorkflowProcessor.__get_latest_file_models(workflow_spec_id)
major_version = 0 # The version of the primary file.
minor_version = [] # The versions of the minor files if any.
file_ids = []
for file_data in file_data_models:
file_ids.append(file_data.id)
if file_data.file_model.primary:
major_version = file_data.version
else:
minor_version.append(file_data.version)
minor_version.insert(0, major_version) # Add major version to beginning.
version = ".".join(str(x) for x in minor_version)
files = ".".join(str(x) for x in file_ids)
full_version = "v%s (%s)" % (version, files)
return full_version
@staticmethod
def __get_file_models_for_version(workflow_spec_id, version):
file_id_strings = re.findall('\((.*)\)', version)[0].split(".")
file_ids = [int(i) for i in file_id_strings]
files = session.query(FileDataModel)\
@ -148,21 +170,19 @@ class WorkflowProcessor(object):
.order_by(FileModel.id)\
.all()
@staticmethod
def get_spec(workflow_spec_id, version=None):
"""Returns the requested version of the specification,
or the lastest version if none is specified."""
parser = WorkflowProcessor.get_parser()
major_version = 0 # The version of the primary file.
minor_version = [] # The versions of the minor files if any.
file_ids = []
process_id = None
if version is None:
file_data_models = WorkflowProcessor.__get_latest_file_models(workflow_spec_id)
version = WorkflowProcessor.get_latest_version_string(workflow_spec_id)
else:
file_data_models = WorkflowProcessor.__get_file_models_for_version(workflow_spec_id, version)
for file_data in file_data_models:
file_ids.append(file_data.id)
if file_data.file_model.type == FileType.bpmn:
bpmn: ElementTree.Element = ElementTree.fromstring(file_data.data)
if file_data.file_model.primary:
@ -171,17 +191,10 @@ class WorkflowProcessor(object):
elif file_data.file_model.type == FileType.dmn:
dmn: ElementTree.Element = ElementTree.fromstring(file_data.data)
parser.add_dmn_xml(dmn, filename=file_data.file_model.name)
if file_data.file_model.primary:
major_version = file_data.version
else:
minor_version.append(file_data.version)
if process_id is None:
raise(Exception("There is no primary BPMN model defined for workflow %s" % workflow_spec_id))
minor_version.insert(0, major_version) # Add major version to beginning.
spec = parser.get_spec(process_id)
version = ".".join(str(x) for x in minor_version)
files = ".".join(str(x) for x in file_ids)
spec.description = "v%s (%s)" % (version, files)
spec.description = version
return spec
@staticmethod

View File

@ -25,8 +25,10 @@ class TestTasksApi(BaseTest):
workflow = session.query(WorkflowModel).filter_by(study_id=study.id, workflow_spec_id=workflow_name).first()
return workflow
def get_workflow_api(self, workflow):
rv = self.app.get('/v1.0/workflow/%i' % workflow.id, content_type="application/json")
def get_workflow_api(self, workflow, soft_reset=False, hard_reset=False):
rv = self.app.get('/v1.0/workflow/%i?soft_reset=%s&hard_reset=%s' %
(workflow.id, str(soft_reset), str(hard_reset)),
content_type="application/json")
json_data = json.loads(rv.get_data(as_text=True))
workflow_api = WorkflowApiSchema().load(json_data)
self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id)
@ -210,6 +212,7 @@ class TestTasksApi(BaseTest):
workflow = self.create_workflow('two_forms')
workflow_api = self.get_workflow_api(workflow)
self.complete_form(workflow, workflow_api.user_tasks[0], {"color": "blue"})
self.assertTrue(workflow_api.is_latest_spec)
# Modify the specification, with a major change that alters the flow and can't be deserialized
# effectively, if it uses the latest spec files.
@ -218,4 +221,8 @@ class TestTasksApi(BaseTest):
workflow_api = self.get_workflow_api(workflow)
self.assertTrue(workflow_api.spec_version.startswith("v1 "))
self.assertTrue(workflow_api.latest_spec_version)
self.assertFalse(workflow_api.is_latest_spec)
workflow_api = self.get_workflow_api(workflow, hard_reset=True)
self.assertTrue(workflow_api.spec_version.startswith("v2 "))
self.assertTrue(workflow_api.is_latest_spec)

View File

@ -319,4 +319,7 @@ class TestWorkflowProcessor(BaseTest):
self.assertEquals("New Step", processor3.next_task().task_spec.description)
self.assertEquals({"color": "blue"}, processor3.next_task().data)
def test_get_latest_spec_version(self):
workflow_spec_model = self.load_test_spec("two_forms")
version = WorkflowProcessor.get_latest_version_string("two_forms")
self.assertTrue(version.startswith("v1 "))