diff --git a/Pipfile.lock b/Pipfile.lock index 97a58a73..58285c07 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -783,31 +783,40 @@ "spiffworkflow": { "editable": true, "git": "https://github.com/sartography/SpiffWorkflow.git", - "ref": "635afdec60dcb46dcef8c47cd4ba455cb0877f8b" + "ref": "661902387021f7130ae27fa35434eb3b2c138610" }, "sqlalchemy": { "hashes": [ - "sha256:083e383a1dca8384d0ea6378bd182d83c600ed4ff4ec8247d3b2442cf70db1ad", - "sha256:0a690a6486658d03cc6a73536d46e796b6570ac1f8a7ec133f9e28c448b69828", - "sha256:114b6ace30001f056e944cebd46daef38fdb41ebb98f5e5940241a03ed6cad43", - "sha256:128f6179325f7597a46403dde0bf148478f868df44841348dfc8d158e00db1f9", - "sha256:13d48cd8b925b6893a4e59b2dfb3e59a5204fd8c98289aad353af78bd214db49", - "sha256:211a1ce7e825f7142121144bac76f53ac28b12172716a710f4bf3eab477e730b", - "sha256:2dc57ee80b76813759cccd1a7affedf9c4dbe5b065a91fb6092c9d8151d66078", - "sha256:3e625e283eecc15aee5b1ef77203bfb542563fa4a9aa622c7643c7b55438ff49", - "sha256:43078c7ec0457387c79b8d52fff90a7ad352ca4c7aa841c366238c3e2cf52fdf", - "sha256:5b1bf3c2c2dca738235ce08079783ef04f1a7fc5b21cf24adaae77f2da4e73c3", - "sha256:6056b671aeda3fc451382e52ab8a753c0d5f66ef2a5ccc8fa5ba7abd20988b4d", - "sha256:68d78cf4a9dfade2e6cf57c4be19f7b82ed66e67dacf93b32bb390c9bed12749", - "sha256:7025c639ce7e170db845e94006cf5f404e243e6fc00d6c86fa19e8ad8d411880", - "sha256:7224e126c00b8178dfd227bc337ba5e754b197a3867d33b9f30dc0208f773d70", - "sha256:7d98e0785c4cd7ae30b4a451416db71f5724a1839025544b4edbd92e00b91f0f", - "sha256:8d8c21e9d4efef01351bf28513648ceb988031be4159745a7ad1b3e28c8ff68a", - "sha256:bbb545da054e6297242a1bb1ba88e7a8ffb679f518258d66798ec712b82e4e07", - "sha256:d00b393f05dbd4ecd65c989b7f5a81110eae4baea7a6a4cdd94c20a908d1456e", - "sha256:e18752cecaef61031252ca72031d4d6247b3212ebb84748fc5d1a0d2029c23ea" + "sha256:128bc917ed20d78143a45024455ff0aed7d3b96772eba13d5dbaf9cc57e5c41b", + "sha256:156a27548ba4e1fed944ff9fcdc150633e61d350d673ae7baaf6c25c04ac1f71", + "sha256:27e2efc8f77661c9af2681755974205e7462f1ae126f498f4fe12a8b24761d15", + "sha256:2a12f8be25b9ea3d1d5b165202181f2b7da4b3395289000284e5bb86154ce87c", + "sha256:31c043d5211aa0e0773821fcc318eb5cbe2ec916dfbc4c6eea0c5188971988eb", + "sha256:65eb3b03229f684af0cf0ad3bcc771970c1260a82a791a8d07bffb63d8c95bcc", + "sha256:6cd157ce74a911325e164441ff2d9b4e244659a25b3146310518d83202f15f7a", + "sha256:703c002277f0fbc3c04d0ae4989a174753a7554b2963c584ce2ec0cddcf2bc53", + "sha256:869bbb637de58ab0a912b7f20e9192132f9fbc47fc6b5111cd1e0f6cdf5cf9b0", + "sha256:8a0e0cd21da047ea10267c37caf12add400a92f0620c8bc09e4a6531a765d6d7", + "sha256:8d01e949a5d22e5c4800d59b50617c56125fc187fbeb8fa423e99858546de616", + "sha256:925b4fe5e7c03ed76912b75a9a41dfd682d59c0be43bce88d3b27f7f5ba028fb", + "sha256:9cb1819008f0225a7c066cac8bb0cf90847b2c4a6eb9ebb7431dbd00c56c06c5", + "sha256:a87d496884f40c94c85a647c385f4fd5887941d2609f71043e2b73f2436d9c65", + "sha256:a9030cd30caf848a13a192c5e45367e3c6f363726569a56e75dc1151ee26d859", + "sha256:a9e75e49a0f1583eee0ce93270232b8e7bb4b1edc89cc70b07600d525aef4f43", + "sha256:b50f45d0e82b4562f59f0e0ca511f65e412f2a97d790eea5f60e34e5f1aabc9a", + "sha256:b7878e59ec31f12d54b3797689402ee3b5cfcb5598f2ebf26491732758751908", + "sha256:ce1ddaadee913543ff0154021d31b134551f63428065168e756d90bdc4c686f5", + "sha256:ce2646e4c0807f3461be0653502bb48c6e91a5171d6e450367082c79e12868bf", + "sha256:ce6c3d18b2a8ce364013d47b9cad71db815df31d55918403f8db7d890c9d07ae", + "sha256:e4e2664232005bd306f878b0f167a31f944a07c4de0152c444f8c61bbe3cfb38", + "sha256:e8aa395482728de8bdcca9cc0faf3765ab483e81e01923aaa736b42f0294f570", + "sha256:eb4fcf7105bf071c71068c6eee47499ab8d4b8f5a11fc35147c934f0faa60f23", + "sha256:ed375a79f06cad285166e5be74745df1ed6845c5624aafadec4b7a29c25866ef", + "sha256:f35248f7e0d63b234a109dd72fbfb4b5cb6cb6840b221d0df0ecbf54ab087654", + "sha256:f502ef245c492b391e0e23e94cba030ab91722dcc56963c85bfd7f3441ea2bbe", + "sha256:fe01bac7226499aedf472c62fa3b85b2c619365f3f14dd222ffe4f3aa91e5f98" ], - "version": "==1.3.16" + "version": "==1.3.17" }, "swagger-ui-bundle": { "hashes": [ diff --git a/crc/api.yml b/crc/api.yml index 2e323598..c9542885 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -943,12 +943,6 @@ components: type: array items: $ref: "#/components/schemas/NavigationItem" - user_tasks: - type: array - items: - $ref: "#/components/schemas/Task" - last_task: - $ref: "#/components/schemas/Task" next_task: $ref: "#/components/schemas/Task" workflow_spec_id: @@ -994,6 +988,19 @@ components: $ref: "#/components/schemas/Form" documentation: type: string + data: + type: object + multiInstanceType: + type: enum + enum: ['none', 'looping', 'parallel', 'sequential'] + multiInstanceCount: + type: number + multiInstanceIndex: + type: number + processName: + type: string + properties: + type: object example: id: study_identification name: Study Identification @@ -1170,6 +1177,10 @@ components: type: number format: integer example: 5 + task_id: + type: string + format: uuid + example: "1234123uuid1234" name: type: string example: "Task_Has_bananas" @@ -1177,7 +1188,7 @@ components: type: string example: "Has Bananas?" backtracks: - type: booean + type: boolean example: false level: type: integer @@ -1185,14 +1196,19 @@ components: indent: type: integer example: 2 - childcount: + childCount: type: integer example: 4 state: type: enum enum: ['FUTURE', 'WAITING', 'READY', 'CANCELLED', 'COMPLETED','LIKELY','MAYBE'] readOnly: true - + isDecision: + type: boolean + example: False + readOnly: true + task: + $ref: "#/components/schemas/Task" diff --git a/crc/api/workflow.py b/crc/api/workflow.py index d1ad6d72..b8597da9 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -2,7 +2,7 @@ import uuid from crc import session from crc.api.common import ApiError, ApiErrorSchema -from crc.models.api_models import WorkflowApi, WorkflowApiSchema +from crc.models.api_models import WorkflowApi, WorkflowApiSchema, NavigationItem, NavigationItemSchema from crc.models.file import FileModel, LookupDataSchema from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, WorkflowSpecCategoryModel, \ WorkflowSpecCategoryModelSchema @@ -83,17 +83,36 @@ def delete_workflow_specification(spec_id): session.commit() -def __get_workflow_api_model(processor: WorkflowProcessor): - spiff_tasks = processor.get_ready_user_tasks() - user_tasks = [WorkflowService.spiff_task_to_api_task(t, add_docs_and_forms=False) for t in spiff_tasks] +def __get_workflow_api_model(processor: WorkflowProcessor, next_task = None): + """Returns an API model representing the state of the current workflow, if requested, and + possible, next_task is set to the current_task.""" + + nav_dict = processor.bpmn_workflow.get_nav_list() + navigation = [] + for nav_item in nav_dict: + spiff_task = processor.bpmn_workflow.get_task(nav_item['task_id']) + if 'description' in nav_item: + nav_item['title'] = nav_item.pop('description') + else: + nav_item['title'] = "" + if spiff_task: + nav_item['task'] = WorkflowService.spiff_task_to_api_task(spiff_task, add_docs_and_forms=False) + nav_item['title'] = nav_item['task'].title # Prefer the task title. + else: + nav_item['task'] = None + nav_item['childCount'] = nav_item.pop('child_count') + if 'is_decision' in nav_item: + nav_item['isDecision'] = nav_item.pop('is_decision') + else: + nav_item['isDecision'] = False + + navigation.append(NavigationItem(**nav_item)) + NavigationItemSchema().dump(nav_item) workflow_api = WorkflowApi( id=processor.get_workflow_id(), status=processor.get_status(), - navigation=processor.bpmn_workflow.get_nav_list(), - last_task=WorkflowService.spiff_task_to_api_task(processor.bpmn_workflow.last_task), next_task=None, - previous_task=processor.previous_task(), - user_tasks=user_tasks, + navigation=navigation, workflow_spec_id=processor.workflow_spec_id, spec_version=processor.get_spec_version(), is_latest_spec=processor.get_spec_version() == processor.get_latest_version_string(processor.workflow_spec_id), @@ -101,7 +120,9 @@ def __get_workflow_api_model(processor: WorkflowProcessor): completed_tasks=processor.workflow_model.completed_tasks, last_updated=processor.workflow_model.last_updated ) - next_task = processor.next_task() + 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. + next_task = processor.next_task() if next_task: workflow_api.next_task = WorkflowService.spiff_task_to_api_task(next_task, add_docs_and_forms=True) @@ -118,19 +139,20 @@ def get_workflow(workflow_id, soft_reset=False, hard_reset=False): def delete_workflow(workflow_id): StudyService.delete_workflow(workflow_id) + 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) - if task.state != task.COMPLETED: + if task.state != task.COMPLETED and task.state != task.READY: raise ApiError("invalid_state", "You may not move the token to a task who's state is not " - "currently set to COMPLETE.") + "currently set to COMPLETE or READY.") task.reset_token(reset_data=False) # we could optionally clear the previous data. processor.save() WorkflowService.log_task_action(processor, task, WorkflowService.TASK_ACTION_TOKEN_RESET) - workflow_api_model = __get_workflow_api_model(processor) + workflow_api_model = __get_workflow_api_model(processor, task) return WorkflowApiSchema().dump(workflow_api_model) diff --git a/crc/models/api_models.py b/crc/models/api_models.py index f8704492..2222bc8d 100644 --- a/crc/models/api_models.py +++ b/crc/models/api_models.py @@ -15,6 +15,20 @@ class MultiInstanceType(enum.Enum): sequential = "sequential" +class NavigationItem(object): + def __init__(self, id, task_id, name, title, backtracks, level, indent, childCount, state, isDecision, task=None): + self.id = id + self.task_id = task_id + self.name = name, + self.title = title + self.backtracks = backtracks + self.level = level + self.indent = indent + self.childCount = childCount + self.state = state + self.isDecision = isDecision + self.task = task + class Task(object): ENUM_OPTIONS_FILE_PROP = "enum.options.file" @@ -24,7 +38,7 @@ class Task(object): def __init__(self, id, name, title, type, state, form, documentation, data, - mi_type, mi_count, mi_index, process_name, properties): + multiInstanceType, multiInstanceCount, multiInstanceIndex, processName, properties): self.id = id self.name = name self.title = title @@ -33,10 +47,10 @@ class Task(object): self.form = form self.documentation = documentation self.data = data - self.mi_type = mi_type # Some tasks have a repeat behavior. - self.mi_count = mi_count # This is the number of times the task could repeat. - self.mi_index = mi_index # And the index of the currently repeating task. - self.process_name = process_name + self.multiInstanceType = multiInstanceType # Some tasks have a repeat behavior. + self.multiInstanceCount = multiInstanceCount # This is the number of times the task could repeat. + self.multiInstanceIndex = multiInstanceIndex # And the index of the currently repeating task. + self.processName = processName self.properties = properties # Arbitrary extension properties from BPMN editor. @@ -55,7 +69,6 @@ class FormFieldPropertySchema(ma.Schema): "id", "value" ] - class FormFieldSchema(ma.Schema): class Meta: fields = [ @@ -75,31 +88,38 @@ class FormSchema(ma.Schema): class TaskSchema(ma.Schema): class Meta: - fields = ["id", "name", "title", "type", "state", "form", "documentation", "data", "mi_type", - "mi_count", "mi_index", "process_name", "properties"] + fields = ["id", "name", "title", "type", "state", "form", "documentation", "data", "multiInstanceType", + "multiInstanceCount", "multiInstanceIndex", "processName", "properties"] - mi_type = EnumField(MultiInstanceType) + multiInstanceType = EnumField(MultiInstanceType) documentation = marshmallow.fields.String(required=False, allow_none=True) form = marshmallow.fields.Nested(FormSchema, required=False, allow_none=True) title = marshmallow.fields.String(required=False, allow_none=True) - process_name = marshmallow.fields.String(required=False, allow_none=True) + processName = marshmallow.fields.String(required=False, allow_none=True) @marshmallow.post_load def make_task(self, data, **kwargs): return Task(**data) +class NavigationItemSchema(ma.Schema): + class Meta: + fields = ["id", "task_id", "name", "title", "backtracks", "level", "indent", "childCount", "state", + "isDecision", "task"] + unknown = INCLUDE + task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False, allow_none=True) + backtracks = marshmallow.fields.String(required=False, allow_none=True) + title = marshmallow.fields.String(required=False, allow_none=True) + task_id = marshmallow.fields.String(required=False, allow_none=True) + + class WorkflowApi(object): - def __init__(self, id, status, navigation, user_tasks, last_task, next_task, previous_task, + def __init__(self, id, status, next_task, navigation, spec_version, is_latest_spec, workflow_spec_id, total_tasks, completed_tasks, last_updated): self.id = id self.status = status - self.navigation = navigation - self.user_tasks = user_tasks - self.last_task = last_task # The last task that was completed, may be different than previous. self.next_task = next_task # The next task that requires user input. - self.previous_task = previous_task # The opposite of next task. - + self.navigation = navigation self.workflow_spec_id = workflow_spec_id self.spec_version = spec_version self.is_latest_spec = is_latest_spec @@ -110,21 +130,20 @@ class WorkflowApi(object): class WorkflowApiSchema(ma.Schema): class Meta: model = WorkflowApi - fields = ["id", "status", "navigation", "user_tasks", "last_task", "next_task", "previous_task", + fields = ["id", "status", "next_task", "navigation", "workflow_spec_id", "spec_version", "is_latest_spec", "total_tasks", "completed_tasks", "last_updated"] unknown = INCLUDE status = EnumField(WorkflowStatus) - user_tasks = marshmallow.fields.List(marshmallow.fields.Nested(TaskSchema, dump_only=True)) - last_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False) next_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False) - previous_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False) + navigation = marshmallow.fields.List(marshmallow.fields.Nested(NavigationItemSchema, dump_only=True)) @marshmallow.post_load def make_workflow(self, data, **kwargs): - keys = ['id', 'status', 'navigation', 'user_tasks', 'last_task', 'next_task', 'previous_task', + keys = ['id', 'status', 'next_task', 'navigation', 'workflow_spec_id', 'spec_version', 'is_latest_spec', "total_tasks", "completed_tasks", "last_updated"] filtered_fields = {key: data[key] for key in keys} + filtered_fields['next_task'] = TaskSchema().make_task(data['next_task']) return WorkflowApi(**filtered_fields) diff --git a/crc/services/workflow_processor.py b/crc/services/workflow_processor.py index ce85b43d..7f06c47b 100644 --- a/crc/services/workflow_processor.py +++ b/crc/services/workflow_processor.py @@ -392,6 +392,17 @@ class WorkflowProcessor(object): def get_ready_user_tasks(self): return self.bpmn_workflow.get_ready_user_tasks() + def get_current_user_tasks(self): + """Return a list of all user tasks that are READY or + COMPLETE and are parallel to the READY Task.""" + ready_tasks = self.bpmn_workflow.get_ready_user_tasks() + additional_tasks = [] + if len(ready_tasks) > 0: + for child in ready_tasks[0].parent.children: + if child.state == SpiffTask.COMPLETED: + additional_tasks.append(child) + return ready_tasks + additional_tasks + def get_all_user_tasks(self): all_tasks = self.bpmn_workflow.get_tasks(SpiffTask.ANY_MASK) return [t for t in all_tasks if not self.bpmn_workflow._is_engine_task(t.task_spec)] @@ -425,5 +436,8 @@ class WorkflowProcessor(object): return process_elements[0].attrib['id'] - + def get_nav_item(self, task): + for nav_item in self.bpmn_workflow.get_nav_list(): + if nav_item['task_id'] == task.id: + return nav_item diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py index f5d39ca9..750654f9 100644 --- a/crc/services/workflow_service.py +++ b/crc/services/workflow_service.py @@ -24,7 +24,6 @@ from SpiffWorkflow import Task as SpiffTask, WorkflowException class WorkflowService(object): - TASK_ACTION_COMPLETE = "Complete" TASK_ACTION_TOKEN_RESET = "Backwards Move" TASK_ACTION_HARD_RESET = "Restart (Hard)" @@ -54,7 +53,8 @@ class WorkflowService(object): tasks = bpmn_workflow.get_tasks(SpiffTask.READY) for task in tasks: task_api = WorkflowService.spiff_task_to_api_task( - task, add_docs_and_forms=True) # Assure we try to process the documenation, and raise those errors. + task, + add_docs_and_forms=True) # Assure we try to process the documenation, and raise those errors. WorkflowProcessor.populate_form_with_random_data(task, task_api) task.complete() except WorkflowException as we: @@ -102,17 +102,19 @@ class WorkflowService(object): spiff_task.get_state_name(), None, "", - spiff_task.data, + {}, mi_type, info["mi_count"], info["mi_index"], - process_name=spiff_task.task_spec._wf_spec.description, - properties=props) + processName=spiff_task.task_spec._wf_spec.description, + properties=props + ) # Only process the form and documentation if requested. # The task should be in a completed or a ready state, and should # not be a previously completed MI Task. if add_docs_and_forms: + task.data = spiff_task.data if hasattr(spiff_task.task_spec, "form"): task.form = spiff_task.task_spec.form for field in task.form.fields: @@ -123,20 +125,28 @@ class WorkflowService(object): # some tasks, particularly multi-instance tasks that all have the same spec # but need different labels. if spiff_task.state == SpiffTask.READY: - task.props = WorkflowService._process_properties(spiff_task, props) + task.properties = WorkflowService._process_properties(spiff_task, props) + # Replace the title with the display name if it is set in the task properties, + # otherwise strip off the first word of the task, as that should be following + # a BPMN standard, and should not be included in the display. + if task.properties and "display_name" in task.properties: + task.title = task.properties['display_name'] + elif task.title and ' ' in task.title: + task.title = task.title.partition(' ')[2] return task @staticmethod def _process_properties(spiff_task, props): """Runs all the property values through the Jinja2 processor to inject data.""" - for k,v in props.items(): + for k, v in props.items(): try: template = Template(v) props[k] = template.render(**spiff_task.data) except jinja2.exceptions.TemplateError as ue: app.logger.error("Failed to process task property %s " % str(ue)) + return props @staticmethod def _process_documentation(spiff_task): @@ -161,7 +171,7 @@ class WorkflowService(object): return template.render(**spiff_task.data) except jinja2.exceptions.TemplateError as ue: -# return "Error processing template. %s" % ue.message + # return "Error processing template. %s" % ue.message raise ApiError(code="template_error", message="Error processing template for task %s: %s" % (spiff_task.task_spec.name, str(ue)), status_code=500) # TODO: Catch additional errors and report back. @@ -278,14 +288,11 @@ class WorkflowService(object): task_title=task.title, task_type=str(task.type), task_state=task.state, - mi_type=task.mi_type.value, # Some tasks have a repeat behavior. - mi_count=task.mi_count, # This is the number of times the task could repeat. - mi_index=task.mi_index, # And the index of the currently repeating task. - process_name=task.process_name, + mi_type=task.multiInstanceType.value, # Some tasks have a repeat behavior. + mi_count=task.multiInstanceCount, # This is the number of times the task could repeat. + mi_index=task.multiInstanceIndex, # And the index of the currently repeating task. + process_name=task.processName, date=datetime.now(), ) db.session.add(task_event) db.session.commit() - - - diff --git a/tests/data/exclusive_gateway/exclusive_gateway.bpmn b/tests/data/exclusive_gateway/exclusive_gateway.bpmn index 26ba0e7b..1c7e55fe 100644 --- a/tests/data/exclusive_gateway/exclusive_gateway.bpmn +++ b/tests/data/exclusive_gateway/exclusive_gateway.bpmn @@ -1,11 +1,11 @@ - + SequenceFlow_1pnq3kg - + @@ -15,7 +15,7 @@ SequenceFlow_1lmkn99 - + SequenceFlow_1lmkn99 SequenceFlow_Yes_Bananas SequenceFlow_No_Bananas @@ -55,29 +55,13 @@ - - - - - - + + + - - - - - - - - - - - - - - - - + + + @@ -87,6 +71,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -96,17 +107,9 @@ - - - - - - - - diff --git a/tests/test_tasks_api.py b/tests/test_tasks_api.py index 53b48bd0..f0ddd623 100644 --- a/tests/test_tasks_api.py +++ b/tests/test_tasks_api.py @@ -66,10 +66,15 @@ class TestTasksApi(BaseTest): self.assertEquals(task_in.title, event.task_title) self.assertEquals(task_in.type, event.task_type) self.assertEquals("COMPLETED", event.task_state) - self.assertEquals(task_in.mi_type.value, event.mi_type) - self.assertEquals(task_in.mi_count, event.mi_count) - self.assertEquals(task_in.mi_index, event.mi_index) - self.assertEquals(task_in.process_name, event.process_name) + # Not sure what vodoo is happening inside of marshmallow to get me in this state. + if isinstance(task_in.multiInstanceType,MultiInstanceType): + self.assertEquals(task_in.multiInstanceType.value, event.mi_type) + else: + self.assertEquals(task_in.multiInstanceType, event.mi_type) + + self.assertEquals(task_in.multiInstanceCount, event.mi_count) + self.assertEquals(task_in.multiInstanceIndex, event.mi_index) + self.assertEquals(task_in.processName, event.process_name) self.assertIsNotNone(event.date) @@ -82,9 +87,9 @@ class TestTasksApi(BaseTest): workflow = self.create_workflow('random_fact') workflow = self.get_workflow_api(workflow) task = workflow.next_task - self.assertEqual("Task_User_Select_Type", task['name']) - self.assertEqual(3, len(task['form']["fields"][0]["options"])) - self.assertIsNotNone(task['documentation']) + self.assertEqual("Task_User_Select_Type", task.name) + self.assertEqual(3, len(task.form["fields"][0]["options"])) + self.assertIsNotNone(task.documentation) expected_docs = """# h1 Heading 8-) ## h2 Heading ### h3 Heading @@ -92,7 +97,7 @@ class TestTasksApi(BaseTest): ##### h5 Heading ###### h6 Heading """ - self.assertTrue(str.startswith(task['documentation'], expected_docs)) + self.assertTrue(str.startswith(task.documentation, expected_docs)) def test_two_forms_task(self): # Set up a new workflow @@ -101,44 +106,44 @@ class TestTasksApi(BaseTest): # get the first form in the two form workflow. workflow_api = self.get_workflow_api(workflow) self.assertEqual('two_forms', workflow_api.workflow_spec_id) - self.assertEqual(1, len(workflow_api.user_tasks)) - self.assertIsNotNone(workflow_api.next_task['form']) - self.assertEqual("UserTask", workflow_api.next_task['type']) - self.assertEqual("StepOne", workflow_api.next_task['name']) - self.assertEqual(1, len(workflow_api.next_task['form']['fields'])) + self.assertEqual(2, len(workflow_api.navigation)) + self.assertIsNotNone(workflow_api.next_task.form) + self.assertEqual("UserTask", workflow_api.next_task.type) + self.assertEqual("StepOne", workflow_api.next_task.name) + self.assertEqual(1, len(workflow_api.next_task.form['fields'])) # Complete the form for Step one and post it. - self.complete_form(workflow, workflow_api.user_tasks[0], {"color": "blue"}) + self.complete_form(workflow, workflow_api.next_task, {"color": "blue"}) # Get the next Task workflow_api = self.get_workflow_api(workflow) - self.assertEqual("StepTwo", workflow_api.next_task['name']) + self.assertEqual("StepTwo", workflow_api.next_task.name) # Get all user Tasks and check that the data have been saved - for task in workflow_api.user_tasks: - self.assertIsNotNone(task.data) - for val in task.data.values(): - self.assertIsNotNone(val) + task = workflow_api.next_task + self.assertIsNotNone(task.data) + for val in task.data.values(): + self.assertIsNotNone(val) def test_error_message_on_bad_gateway_expression(self): self.load_example_data() workflow = self.create_workflow('exclusive_gateway') # get the first form in the two form workflow. - tasks = self.get_workflow_api(workflow).user_tasks - self.complete_form(workflow, tasks[0], {"has_bananas": True}) + task = self.get_workflow_api(workflow).next_task + self.complete_form(workflow, task, {"has_bananas": True}) def test_workflow_with_parallel_forms(self): self.load_example_data() workflow = self.create_workflow('exclusive_gateway') # get the first form in the two form workflow. - tasks = self.get_workflow_api(workflow).user_tasks - self.complete_form(workflow, tasks[0], {"has_bananas": True}) + task = self.get_workflow_api(workflow).next_task + self.complete_form(workflow, task, {"has_bananas": True}) # Get the next Task workflow_api = self.get_workflow_api(workflow) - self.assertEqual("Task_Num_Bananas", workflow_api.next_task['name']) + self.assertEqual("Task_Num_Bananas", workflow_api.next_task.name) def test_navigation_with_parallel_forms(self): self.load_example_data() @@ -148,30 +153,14 @@ class TestTasksApi(BaseTest): workflow_api = self.get_workflow_api(workflow) self.assertIsNotNone(workflow_api.navigation) - - - def test_get_workflow_contains_details_about_last_task_data(self): - self.load_example_data() - workflow = self.create_workflow('exclusive_gateway') - - # get the first form in the two form workflow. - tasks = self.get_workflow_api(workflow).user_tasks - workflow_api = self.complete_form(workflow, tasks[0], {"has_bananas": True}) - - self.assertIsNotNone(workflow_api.last_task) - self.assertEqual({"has_bananas": True}, workflow_api.last_task['data']) - - def test_get_workflow_contains_reference_to_last_task_and_next_task(self): - self.load_example_data() - workflow = self.create_workflow('exclusive_gateway') - - # get the first form in the two form workflow. - tasks = self.get_workflow_api(workflow).user_tasks - self.complete_form(workflow, tasks[0], {"has_bananas": True}) - - workflow_api = self.get_workflow_api(workflow) - self.assertIsNotNone(workflow_api.last_task) - self.assertIsNotNone(workflow_api.next_task) + nav = workflow_api.navigation + self.assertEquals(5, len(nav)) + self.assertEquals("Do You Have Bananas", nav[0]['title']) + self.assertEquals("READY", nav[0]['state']) + self.assertEquals("Bananas?", nav[1]['title']) + self.assertEquals("FUTURE", nav[1]['state']) + self.assertEquals("yes", nav[2]['title']) + self.assertEquals("NOOP", nav[2]['state']) def test_document_added_to_workflow_shows_up_in_file_list(self): @@ -179,7 +168,7 @@ class TestTasksApi(BaseTest): self.create_reference_document() workflow = self.create_workflow('docx') # get the first form in the two form workflow. - tasks = self.get_workflow_api(workflow).user_tasks + task = self.get_workflow_api(workflow).next_task data = { "full_name": "Buck of the Wild", "date": "5/1/2020", @@ -187,9 +176,9 @@ class TestTasksApi(BaseTest): "company": "In the company of wolves", "last_name": "Mr. Wolf" } - workflow_api = self.complete_form(workflow, tasks[0], data) + workflow_api = self.complete_form(workflow, task, data) self.assertIsNotNone(workflow_api.next_task) - self.assertEqual("EndEvent_0evb22x", workflow_api.next_task['name']) + self.assertEqual("EndEvent_0evb22x", workflow_api.next_task.name) self.assertTrue(workflow_api.status == WorkflowStatus.complete) rv = self.app.get('/v1.0/file?workflow_id=%i' % workflow.id, headers=self.logged_in_headers()) self.assert_success(rv) @@ -207,14 +196,14 @@ class TestTasksApi(BaseTest): workflow = self.create_workflow('random_fact') workflow_api = self.get_workflow_api(workflow) task = workflow_api.next_task - self.assertEqual("Task_User_Select_Type", task['name']) - self.assertEqual(3, len(task['form']["fields"][0]["options"])) - self.assertIsNotNone(task['documentation']) - self.complete_form(workflow, workflow_api.user_tasks[0], {"type": "norris"}) + self.assertEqual("Task_User_Select_Type", task.name) + self.assertEqual(3, len(task.form["fields"][0]["options"])) + self.assertIsNotNone(task.documentation) + self.complete_form(workflow, workflow_api.next_task, {"type": "norris"}) workflow_api = self.get_workflow_api(workflow) - self.assertEqual("EndEvent_0u1cgrf", workflow_api.next_task['name']) - self.assertIsNotNone(workflow_api.next_task['documentation']) - self.assertTrue("norris" in workflow_api.next_task['documentation']) + self.assertEqual("EndEvent_0u1cgrf", workflow_api.next_task.name) + self.assertIsNotNone(workflow_api.next_task.documentation) + self.assertTrue("norris" in workflow_api.next_task.documentation) def test_load_workflow_from_outdated_spec(self): @@ -222,7 +211,7 @@ class TestTasksApi(BaseTest): self.load_example_data() 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.complete_form(workflow, workflow_api.next_task, {"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 @@ -249,7 +238,7 @@ class TestTasksApi(BaseTest): self.load_example_data() 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.complete_form(workflow, workflow_api.next_task, {"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 @@ -275,22 +264,22 @@ class TestTasksApi(BaseTest): workflow = self.create_workflow('manual_task_with_external_documentation') # get the first form in the two form workflow. - tasks = self.get_workflow_api(workflow).user_tasks - workflow_api = self.complete_form(workflow, tasks[0], {"name": "Dan"}) + task = self.get_workflow_api(workflow).next_task + workflow_api = self.complete_form(workflow, task, {"name": "Dan"}) workflow = self.get_workflow_api(workflow) - self.assertEquals('Task_Manual_One', workflow.next_task['name']) - self.assertEquals('ManualTask', workflow_api.next_task['type']) - self.assertTrue('Markdown' in workflow_api.next_task['documentation']) - self.assertTrue('Dan' in workflow_api.next_task['documentation']) + self.assertEquals('Task_Manual_One', workflow.next_task.name) + self.assertEquals('ManualTask', workflow_api.next_task.type) + self.assertTrue('Markdown' in workflow_api.next_task.documentation) + self.assertTrue('Dan' in workflow_api.next_task.documentation) def test_bpmn_extension_properties_are_populated(self): self.load_example_data() workflow = self.create_workflow('manual_task_with_external_documentation') # get the first form in the two form workflow. - tasks = self.get_workflow_api(workflow).user_tasks - self.assertEquals("JustAValue", tasks[0].properties['JustAKey']) + task = self.get_workflow_api(workflow).next_task + self.assertEquals("JustAValue", task.properties['JustAKey']) @patch('crc.services.protocol_builder.requests.get') @@ -303,14 +292,15 @@ class TestTasksApi(BaseTest): workflow = self.create_workflow('multi_instance') # get the first form in the two form workflow. - tasks = self.get_workflow_api(workflow).user_tasks - self.assertEquals(1, len(tasks)) - self.assertEquals("UserTask", tasks[0].type) - self.assertEquals(MultiInstanceType.sequential, tasks[0].mi_type) - self.assertEquals(9, tasks[0].mi_count) + workflow = self.get_workflow_api(workflow) + navigation = self.get_workflow_api(workflow).navigation + self.assertEquals(4, len(navigation)) # Start task, form_task, multi_task, end task + self.assertEquals("UserTask", workflow.next_task.type) + self.assertEquals(MultiInstanceType.sequential.value, workflow.next_task.multiInstanceType) + self.assertEquals(9, workflow.next_task.multiInstanceCount) # Assure that the names for each task are properly updated, so they aren't all the same. - self.assertEquals("Primary Investigator", tasks[0].properties['display_name']) + self.assertEquals("Primary Investigator", workflow.next_task.properties['display_name']) def test_lookup_endpoint_for_task_field_enumerations(self): @@ -319,9 +309,9 @@ class TestTasksApi(BaseTest): # get the first form in the two form workflow. workflow = self.get_workflow_api(workflow) task = workflow.next_task - field_id = task['form']['fields'][0]['id'] + field_id = task.form['fields'][0]['id'] rv = self.app.get('/v1.0/workflow/%i/task/%s/lookup/%s?query=%s&limit=5' % - (workflow.id, task['id'], field_id, 'c'), # All records with a word that starts with 'c' + (workflow.id, task.id, field_id, 'c'), # All records with a word that starts with 'c' headers=self.logged_in_headers(), content_type="application/json") self.assert_success(rv) @@ -333,17 +323,20 @@ class TestTasksApi(BaseTest): self.load_example_data() workflow = self.create_workflow('subprocess') - tasks = self.get_workflow_api(workflow).user_tasks - self.assertEquals(1, len(tasks)) - self.assertEquals("UserTask", tasks[0].type) - self.assertEquals("Activity_A", tasks[0].name) - self.assertEquals("My Sub Process", tasks[0].process_name) - workflow_api = self.complete_form(workflow, tasks[0], {"name": "Dan"}) - task = TaskSchema().load(workflow_api.next_task) + workflow_api = self.get_workflow_api(workflow) + navigation = workflow_api.navigation + task = workflow_api.next_task + + self.assertEquals(2, len(navigation)) + self.assertEquals("UserTask", task.type) + self.assertEquals("Activity_A", task.name) + self.assertEquals("My Sub Process", task.processName) + workflow_api = self.complete_form(workflow, task, {"name": "Dan"}) + task = workflow_api.next_task self.assertIsNotNone(task) self.assertEquals("Activity_B", task.name) - self.assertEquals("Sub Workflow Example", task.process_name) + self.assertEquals("Sub Workflow Example", task.processName) workflow_api = self.complete_form(workflow, task, {"name": "Dan"}) self.assertEquals(WorkflowStatus.complete, workflow_api.status) @@ -352,46 +345,42 @@ class TestTasksApi(BaseTest): 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}) + first_task = self.get_workflow_api(workflow).next_task + self.complete_form(workflow, first_task, {"has_bananas": True}) workflow = self.get_workflow_api(workflow) - self.assertEquals('Task_Num_Bananas', workflow.next_task['name']) + 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") + self.complete_form(workflow, first_task, {"has_bananas": False}, error_code="invalid_state") # Go ahead and set the number of bananas. workflow = self.get_workflow_api(workflow) - task = TaskSchema().load(workflow.next_task) + task = workflow.next_task self.complete_form(workflow, task, {"num_bananas": 4}) # We are now at the end of the workflow. # Make the old task the current task. - rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, tasks[0].id), + rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, first_task.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) - # Assure the last task is the task we were on before the reset, - # and the Next Task is the one we just reset the token to be on. - self.assertEquals("Task_Has_Bananas", workflow.next_task['name']) - self.assertEquals("End", workflow.last_task['name']) + # Assure the Next Task is the one we just reset the token to be on. + self.assertEquals("Task_Has_Bananas", workflow.next_task.name) # Go ahead and get that workflow one more time, it should still be right. workflow = self.get_workflow_api(workflow) - # Assure the last task is the task we were on before the reset, - # and the Next Task is the one we just reset the token to be on. - self.assertEquals("Task_Has_Bananas", workflow.next_task['name']) - self.assertEquals("End", workflow.last_task['name']) + # Assure the Next Task is the one we just reset the token to be on. + self.assertEquals("Task_Has_Bananas", workflow.next_task.name) # The next task should be a different value. - self.complete_form(workflow, tasks[0], {"has_bananas": False}) + self.complete_form(workflow, workflow.next_task, {"has_bananas": False}) workflow = self.get_workflow_api(workflow) - self.assertEquals('Task_Why_No_Bananas', workflow.next_task['name']) + self.assertEquals('Task_Why_No_Bananas', workflow.next_task.name) @patch('crc.services.protocol_builder.requests.get') def test_parallel_multi_instance(self, mock_get): @@ -404,14 +393,18 @@ class TestTasksApi(BaseTest): self.load_example_data() workflow = self.create_workflow('multi_instance_parallel') - tasks = self.get_workflow_api(workflow).user_tasks - self.assertEquals(9, len(tasks)) - self.assertEquals("UserTask", tasks[0].type) - self.assertEquals("MutiInstanceTask", tasks[0].name) - self.assertEquals("Gather more information", tasks[0].title) + workflow_api = self.get_workflow_api(workflow) + self.assertEquals(12, len(workflow_api.navigation)) + ready_items = [nav for nav in workflow_api.navigation if nav['state'] == "READY"] + self.assertEquals(9, len(ready_items)) + + self.assertEquals("UserTask", workflow_api.next_task.type) + self.assertEquals("MutiInstanceTask",workflow_api.next_task.name) + self.assertEquals("more information", workflow_api.next_task.title) for i in random.sample(range(9), 9): - self.complete_form(workflow, tasks[i], {"investigator":{"email": "dhf8r@virginia.edu"}}) + task = TaskSchema().load(ready_items[i]['task']) + self.complete_form(workflow, task, {"investigator":{"email": "dhf8r@virginia.edu"}}) #tasks = self.get_workflow_api(workflow).user_tasks workflow = self.get_workflow_api(workflow) diff --git a/tests/test_workflow_processor_multi_instance.py b/tests/test_workflow_processor_multi_instance.py index f2252129..cad925fa 100644 --- a/tests/test_workflow_processor_multi_instance.py +++ b/tests/test_workflow_processor_multi_instance.py @@ -61,9 +61,9 @@ class TestWorkflowProcessorMultiInstance(BaseTest): self.assertEqual("MutiInstanceTask", task.get_name()) api_task = WorkflowService.spiff_task_to_api_task(task) - self.assertEquals(MultiInstanceType.sequential, api_task.mi_type) - self.assertEquals(3, api_task.mi_count) - self.assertEquals(1, api_task.mi_index) + self.assertEquals(MultiInstanceType.sequential, api_task.multiInstanceType) + self.assertEquals(3, api_task.multiInstanceCount) + self.assertEquals(1, api_task.multiInstanceIndex) task.update_data({"investigator":{"email":"asd3v@virginia.edu"}}) processor.complete_task(task) processor.do_engine_steps() @@ -72,8 +72,8 @@ class TestWorkflowProcessorMultiInstance(BaseTest): api_task = WorkflowService.spiff_task_to_api_task(task) self.assertEqual("MutiInstanceTask", api_task.name) task.update_data({"investigator":{"email":"asdf32@virginia.edu"}}) - self.assertEquals(3, api_task.mi_count) - self.assertEquals(2, api_task.mi_index) + self.assertEquals(3, api_task.multiInstanceCount) + self.assertEquals(2, api_task.multiInstanceIndex) processor.complete_task(task) processor.do_engine_steps() @@ -81,8 +81,8 @@ class TestWorkflowProcessorMultiInstance(BaseTest): api_task = WorkflowService.spiff_task_to_api_task(task) self.assertEqual("MutiInstanceTask", task.get_name()) task.update_data({"investigator":{"email":"dhf8r@virginia.edu"}}) - self.assertEquals(3, api_task.mi_count) - self.assertEquals(3, api_task.mi_index) + self.assertEquals(3, api_task.multiInstanceCount) + self.assertEquals(3, api_task.multiInstanceIndex) processor.complete_task(task) processor.do_engine_steps() task = processor.bpmn_workflow.last_task @@ -120,7 +120,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest): self.assertEquals("asd3v", task.data["investigator"]["user_id"]) # The last of the tasks api_task = WorkflowService.spiff_task_to_api_task(task) - self.assertEquals(MultiInstanceType.parallel, api_task.mi_type) + self.assertEquals(MultiInstanceType.parallel, api_task.multiInstanceType) task.update_data({"investigator":{"email":"dhf8r@virginia.edu"}}) processor.complete_task(task) processor.do_engine_steps()