diff --git a/crc/api.yml b/crc/api.yml index 783fde2d..65fc5f70 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -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: diff --git a/crc/api/workflow.py b/crc/api/workflow.py index 26646307..e14023ea 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -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)) diff --git a/crc/models/api_models.py b/crc/models/api_models.py index 8aa748b1..c8575440 100644 --- a/crc/models/api_models.py +++ b/crc/models/api_models.py @@ -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) diff --git a/crc/services/workflow_processor.py b/crc/services/workflow_processor.py index 8b154cb1..34915236 100644 --- a/crc/services/workflow_processor.py +++ b/crc/services/workflow_processor.py @@ -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 diff --git a/tests/test_tasks_api.py b/tests/test_tasks_api.py index f326639e..fabe6d98 100644 --- a/tests/test_tasks_api.py +++ b/tests/test_tasks_api.py @@ -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) diff --git a/tests/test_workflow_processor.py b/tests/test_workflow_processor.py index 95369f96..258ab195 100644 --- a/tests/test_workflow_processor.py +++ b/tests/test_workflow_processor.py @@ -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 "))