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: get:
operationId: crc.api.workflow.get_workflow operationId: crc.api.workflow.get_workflow
summary: Detailed information for a specific workflow instance 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: tags:
- Workflows and Tasks - Workflows and Tasks
responses: responses:

View File

@ -75,17 +75,18 @@ def __get_workflow_api_model(processor: WorkflowProcessor):
next_task=None, next_task=None,
user_tasks=user_tasks, user_tasks=user_tasks,
workflow_spec_id=processor.workflow_spec_id, 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(): if processor.next_task():
workflow_api.next_task = Task.from_spiff(processor.next_task()) workflow_api.next_task = Task.from_spiff(processor.next_task())
return workflow_api return workflow_api
def get_workflow(workflow_id): def get_workflow(workflow_id, soft_reset=False, hard_reset=False):
schema = WorkflowApiSchema() schema = WorkflowApiSchema()
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, soft_reset=soft_reset, hard_reset=hard_reset)
return schema.dump(__get_workflow_api_model(processor)) return schema.dump(__get_workflow_api_model(processor))

View File

@ -95,7 +95,7 @@ class TaskSchema(ma.Schema):
class WorkflowApi(object): 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.id = id
self.status = status self.status = status
self.user_tasks = user_tasks self.user_tasks = user_tasks
@ -103,11 +103,15 @@ class WorkflowApi(object):
self.next_task = next_task self.next_task = next_task
self.workflow_spec_id = workflow_spec_id self.workflow_spec_id = workflow_spec_id
self.spec_version = spec_version self.spec_version = spec_version
self.is_latest_spec = is_latest_spec
class WorkflowApiSchema(ma.Schema): class WorkflowApiSchema(ma.Schema):
class Meta: class Meta:
model = WorkflowApi 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 unknown = INCLUDE
status = EnumField(WorkflowStatus) status = EnumField(WorkflowStatus)

View File

@ -92,7 +92,7 @@ class WorkflowProcessor(object):
STUDY_ID_KEY = "study_id" STUDY_ID_KEY = "study_id"
def __init__(self, workflow_model: WorkflowModel, soft_reset=False, hard_reset=False): 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 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 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. completed task in the previous workflow.
@ -118,14 +118,36 @@ class WorkflowProcessor(object):
return parser return parser
@staticmethod @staticmethod
def __get_file_models_for_version(workflow_spec_id, version): def get_latest_version_string(workflow_spec_id):
"""Version is in the format v[VERSION] [FILE_ID_LIST] """Version is in the format v[VERSION] (FILE_ID_LIST)
For example, a single bpmn file with only one version would be 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 specification. If multiple files exist, they are added on in
dot notation to both the version number and the file list. So 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 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_id_strings = re.findall('\((.*)\)', version)[0].split(".")
file_ids = [int(i) for i in file_id_strings] file_ids = [int(i) for i in file_id_strings]
files = session.query(FileDataModel)\ files = session.query(FileDataModel)\
@ -148,21 +170,19 @@ class WorkflowProcessor(object):
.order_by(FileModel.id)\ .order_by(FileModel.id)\
.all() .all()
@staticmethod @staticmethod
def get_spec(workflow_spec_id, version=None): def get_spec(workflow_spec_id, version=None):
"""Returns the requested version of the specification, """Returns the requested version of the specification,
or the lastest version if none is specified.""" or the lastest version if none is specified."""
parser = WorkflowProcessor.get_parser() 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 process_id = None
if version is None: if version is None:
file_data_models = WorkflowProcessor.__get_latest_file_models(workflow_spec_id) file_data_models = WorkflowProcessor.__get_latest_file_models(workflow_spec_id)
version = WorkflowProcessor.get_latest_version_string(workflow_spec_id)
else: else:
file_data_models = WorkflowProcessor.__get_file_models_for_version(workflow_spec_id, version) file_data_models = WorkflowProcessor.__get_file_models_for_version(workflow_spec_id, version)
for file_data in file_data_models: for file_data in file_data_models:
file_ids.append(file_data.id)
if file_data.file_model.type == FileType.bpmn: if file_data.file_model.type == FileType.bpmn:
bpmn: ElementTree.Element = ElementTree.fromstring(file_data.data) bpmn: ElementTree.Element = ElementTree.fromstring(file_data.data)
if file_data.file_model.primary: if file_data.file_model.primary:
@ -171,17 +191,10 @@ class WorkflowProcessor(object):
elif file_data.file_model.type == FileType.dmn: elif file_data.file_model.type == FileType.dmn:
dmn: ElementTree.Element = ElementTree.fromstring(file_data.data) dmn: ElementTree.Element = ElementTree.fromstring(file_data.data)
parser.add_dmn_xml(dmn, filename=file_data.file_model.name) 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: if process_id is None:
raise(Exception("There is no primary BPMN model defined for workflow %s" % workflow_spec_id)) 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) spec = parser.get_spec(process_id)
version = ".".join(str(x) for x in minor_version) spec.description = version
files = ".".join(str(x) for x in file_ids)
spec.description = "v%s (%s)" % (version, files)
return spec return spec
@staticmethod @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() workflow = session.query(WorkflowModel).filter_by(study_id=study.id, workflow_spec_id=workflow_name).first()
return workflow return workflow
def get_workflow_api(self, workflow): def get_workflow_api(self, workflow, soft_reset=False, hard_reset=False):
rv = self.app.get('/v1.0/workflow/%i' % workflow.id, content_type="application/json") 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)) json_data = json.loads(rv.get_data(as_text=True))
workflow_api = WorkflowApiSchema().load(json_data) workflow_api = WorkflowApiSchema().load(json_data)
self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id) 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 = self.create_workflow('two_forms')
workflow_api = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
self.complete_form(workflow, workflow_api.user_tasks[0], {"color": "blue"}) 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 # Modify the specification, with a major change that alters the flow and can't be deserialized
# effectively, if it uses the latest spec files. # effectively, if it uses the latest spec files.
@ -218,4 +221,8 @@ class TestTasksApi(BaseTest):
workflow_api = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
self.assertTrue(workflow_api.spec_version.startswith("v1 ")) 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("New Step", processor3.next_task().task_spec.description)
self.assertEquals({"color": "blue"}, processor3.next_task().data) 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 "))