diff --git a/Pipfile.lock b/Pipfile.lock index 08d82b80..52a03637 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -25,10 +25,10 @@ }, "alembic": { "hashes": [ - "sha256:d412982920653db6e5a44bfd13b1d0db5685cbaaccaf226195749c706e1e862a" + "sha256:2df2519a5b002f881517693b95626905a39c5faf4b5a1f94de4f1441095d1d26" ], "index": "pypi", - "version": "==1.3.3" + "version": "==1.4.0" }, "amqp": { "hashes": [ @@ -293,11 +293,11 @@ }, "flask-restful": { "hashes": [ - "sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8", - "sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9" + "sha256:5ea9a5991abf2cb69b4aac19793faac6c032300505b325687d7c305ffaa76915", + "sha256:d891118b951921f1cec80cabb4db98ea6058a35e6404788f9e70d5b243813ec2" ], "index": "pypi", - "version": "==0.3.7" + "version": "==0.3.8" }, "flask-sqlalchemy": { "hashes": [ @@ -723,7 +723,7 @@ "spiffworkflow": { "editable": true, "git": "https://github.com/sartography/SpiffWorkflow.git", - "ref": "7640c6e32d3894b13f8a078849922cf7cb6884a5" + "ref": "6091825791a5e9f11c12639ac07688377bad697e" }, "sqlalchemy": { "hashes": [ @@ -778,10 +778,10 @@ }, "werkzeug": { "hashes": [ - "sha256:1e0dedc2acb1f46827daa2e399c1485c8fa17c0d8e70b6b875b4e7f54bf408d2", - "sha256:b353856d37dec59d6511359f97f6a4b2468442e454bd1c98298ddce53cac1f04" + "sha256:169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096", + "sha256:6dc65cf9091cf750012f56f2cad759fa9e879f511b5ff8685e456b4e3bf90d16" ], - "version": "==0.16.1" + "version": "==1.0.0" }, "xlsxwriter": { "hashes": [ diff --git a/crc/api.yml b/crc/api.yml index 5e95de29..67802c28 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -487,64 +487,6 @@ paths: responses: '204': description: The workflow was removed - /workflow/{workflow_id}/all_tasks: - get: - operationId: crc.api.workflow.get_all_tasks - summary: Return a list of all tasks for this workflow - tags: - - Workflows and Tasks - parameters: - - name: workflow_id - in: path - required: true - description: The id of the workflow - schema: - type: integer - format: int32 - responses: - '200': - description: Expected response to a valid request - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/Task" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /workflow/{workflow_id}/tasks: - get: - operationId: crc.api.workflow.get_ready_user_tasks - summary: Returns the list of ready user tasks for this workflow - tags: - - Workflows and Tasks - parameters: - - name: workflow_id - in: path - required: true - description: The id of the workflow - schema: - type: integer - format: int32 - responses: - '200': - description: Expected response to a valid request - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/Task" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" # /v1.0/workflow/0/task/0 /workflow/{workflow_id}/task/{task_id}: parameters: @@ -693,21 +635,16 @@ components: format: int64 status: type: enum - enum: ['user_input_required','waiting','complete'] - study_id: - readOnly: true + enum: ['new','user_input_required','waiting','complete'] + last_task_id: type: integer - workflow_spec_id: - readOnly: true - type: String - current_task_ids: + next_task_id: + type: integer + user_tasks: type: array items: - type: String - messages: - type: array - items: - type: String + $ref: "#/components/schemas/Task" + example: id: 291234 status: 'user_input_required' diff --git a/crc/api/study.py b/crc/api/study.py index 44cefb6f..3737eda0 100644 --- a/crc/api/study.py +++ b/crc/api/study.py @@ -2,9 +2,10 @@ from connexion import NoContent from crc import session from crc.api.common import ApiError, ApiErrorSchema +from crc.api.workflow import __get_workflow_api_model from crc.models.study import StudyModelSchema, StudyModel -from crc.models.workflow import WorkflowModel, WorkflowModelSchema, WorkflowSpecModel -from crc.workflow_processor import WorkflowProcessor +from crc.models.workflow import WorkflowModel, WorkflowApiSchema, WorkflowSpecModel +from crc.workflow_processor import Workflow, WorkflowProcessor def all_studies(): @@ -19,7 +20,11 @@ def add_study(body): session.commit() # FIXME: We need to ask the protocol builder what workflows to add to the study, not just add them all. for spec in session.query(WorkflowSpecModel).all(): - workflow = __get_workflow_instance(study.id, spec) + processor = WorkflowProcessor.create(spec.id) + workflow = WorkflowModel(bpmn_workflow_json=processor.serialize(), + status=processor.get_status(), + study_id=study.id, + workflow_spec_id=spec.id) session.add(workflow) session.commit() return StudyModelSchema().dump(study) @@ -56,9 +61,14 @@ def post_update_study_from_protocol_builder(study_id): def get_study_workflows(study_id): - workflows = session.query(WorkflowModel).filter_by(study_id=study_id).all() - schema = WorkflowModelSchema(many=True) - return schema.dump(workflows) + workflow_models = session.query(WorkflowModel).filter_by(study_id=study_id).all() + api_models = [] + for workflow_model in workflow_models: + processor = WorkflowProcessor(workflow_model.workflow_spec_id, + workflow_model.bpmn_workflow_json) + api_models.append( __get_workflow_api_model(workflow_model, processor)) + schema = WorkflowApiSchema(many=True) + return schema.dump(api_models) def add_workflow_to_study(study_id, body): @@ -66,15 +76,12 @@ def add_workflow_to_study(study_id, body): if workflow_spec_model is None: error = ApiError('unknown_spec', 'The specification "' + body['id'] + '" is not recognized.') return ApiErrorSchema.dump(error), 404 - workflow = __get_workflow_instance(study_id, workflow_spec_model) - session.add(workflow) - session.commit() - return WorkflowModelSchema().dump(workflow) - -def __get_workflow_instance(study_id, workflow_spec_model): processor = WorkflowProcessor.create(workflow_spec_model.id) workflow = WorkflowModel(bpmn_workflow_json=processor.serialize(), status=processor.get_status(), study_id=study_id, workflow_spec_id=workflow_spec_model.id) - return workflow \ No newline at end of file + session.add(workflow) + session.commit() + return WorkflowApiSchema().dump(__get_workflow_api_model(workflow, processor)) + diff --git a/crc/api/workflow.py b/crc/api/workflow.py index 4040a065..62829a3c 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -1,10 +1,12 @@ import uuid +from flask import json + from crc.api.file import delete_file from crc import session from crc.api.common import ApiError, ApiErrorSchema -from crc.models.workflow import WorkflowModel, WorkflowModelSchema, WorkflowSpecModelSchema, WorkflowSpecModel, \ - Task, TaskSchema +from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, \ + Task, TaskSchema, WorkflowApiSchema, WorkflowApi from crc.workflow_processor import WorkflowProcessor from crc.models.file import FileModel @@ -65,37 +67,26 @@ def delete_workflow_specification(spec_id): session.commit() +def __get_workflow_api_model(workflow_model: WorkflowModel, processor: WorkflowProcessor): + spiff_tasks = processor.get_all_user_tasks() + user_tasks = map(Task.from_spiff, spiff_tasks) + return WorkflowApi(id=workflow_model.id, status=workflow_model.status, + last_task=Task.from_spiff(processor.bpmn_workflow.last_task), + next_task=Task.from_spiff(processor.next_task()), + user_tasks=user_tasks) + + def get_workflow(workflow_id): - schema = WorkflowModelSchema() - workflow = session.query(WorkflowModel).filter_by(id=workflow_id).first() - return schema.dump(workflow) + schema = WorkflowApiSchema() + workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first() + processor = WorkflowProcessor(workflow_model.workflow_spec_id, + workflow_model.bpmn_workflow_json) + return schema.dump(__get_workflow_api_model(workflow_model, processor)) def delete(workflow_id): session.query(WorkflowModel).filter_by(id=workflow_id).delete() session.commit() -3 - - -def get_all_tasks(workflow_id): - workflow = session.query(WorkflowModel).filter_by(id=workflow_id).first() - processor = WorkflowProcessor(workflow.workflow_spec_id, workflow.bpmn_workflow_json) - spiff_tasks = processor.get_all_user_tasks() - tasks = [] - for st in spiff_tasks: - tasks.append(Task.from_spiff(st)) - return TaskSchema(many=True).dump(tasks) - - -def get_ready_user_tasks(workflow_id): - workflow = session.query(WorkflowModel).filter_by(id=workflow_id).first() - processor = WorkflowProcessor(workflow.workflow_spec_id, workflow.bpmn_workflow_json) - spiff_tasks = processor.get_ready_user_tasks() - tasks = [] - for st in spiff_tasks: - tasks.append(Task.from_spiff(st)) - return TaskSchema(many=True).dump(tasks) - def get_task(workflow_id, task_id): workflow = session.query(WorkflowModel).filter_by(id=workflow_id).first() @@ -103,14 +94,16 @@ def get_task(workflow_id, task_id): def update_task(workflow_id, task_id, body): - workflow = session.query(WorkflowModel).filter_by(id=workflow_id).first() - processor = WorkflowProcessor(workflow.workflow_spec_id, workflow.bpmn_workflow_json) + workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first() + processor = WorkflowProcessor(workflow_model.workflow_spec_id, workflow_model.bpmn_workflow_json) task_id = uuid.UUID(task_id) task = processor.bpmn_workflow.get_task(task_id) task.data = body processor.complete_task(task) processor.do_engine_steps() - workflow.bpmn_workflow_json = processor.serialize() - session.add(workflow) + workflow_model.last_completed_task_id = task.id + workflow_model.bpmn_workflow_json = processor.serialize() + session.add(workflow_model) session.commit() - return WorkflowModelSchema().dump(workflow) + return WorkflowApiSchema().dump(__get_workflow_api_model(workflow_model, processor) + ) diff --git a/crc/models/workflow.py b/crc/models/workflow.py index 2849f2e8..148d8a3a 100644 --- a/crc/models/workflow.py +++ b/crc/models/workflow.py @@ -1,6 +1,7 @@ import enum import marshmallow +from marshmallow import post_dump, pre_dump, EXCLUDE, INCLUDE from marshmallow_enum import EnumField from marshmallow_sqlalchemy import ModelSchema @@ -15,7 +16,6 @@ class WorkflowSpecModel(db.Model): description = db.Column(db.Text) primary_process_id = db.Column(db.String) - class WorkflowSpecModelSchema(ModelSchema): class Meta: model = WorkflowSpecModel @@ -35,17 +35,10 @@ class WorkflowModel(db.Model): status = db.Column(db.Enum(WorkflowStatus)) study_id = db.Column(db.Integer, db.ForeignKey('study.id')) workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id')) + last_completed_task_id = db.Column(db.String) -class WorkflowModelSchema(ModelSchema): - class Meta: - model = WorkflowModel - include_fk = True # Includes foreign keys - - status = EnumField(WorkflowStatus) - - -class Task: +class Task(object): def __init__(self, id, name, title, type, state, form, documentation, data): self.id = id self.name = name @@ -61,13 +54,12 @@ class Task: instance = cls(spiff_task.id, spiff_task.task_spec.name, spiff_task.task_spec.description, - "task", + spiff_task.task_spec.__class__.__name__, spiff_task.get_state_name(), - {}, + None, spiff_task.task_spec.documentation, spiff_task.data) if hasattr(spiff_task.task_spec, "form"): - instance.type = "form" instance.form = spiff_task.task_spec.form return instance @@ -108,8 +100,32 @@ class TaskSchema(ma.Schema): fields = ["id", "name", "title", "type", "state", "form", "documentation", "data"] documentation = marshmallow.fields.String(required=False, allow_none=True) - form = marshmallow.fields.Nested(FormSchema) + form = marshmallow.fields.Nested(FormSchema, required=False, allow_none=True) + title = marshmallow.fields.String(required=False, allow_none=True) @marshmallow.post_load def make_task(self, data, **kwargs): return Task(**data) + + +class WorkflowApi(object): + def __init__(self, id, status, user_tasks, last_task, next_task): + self.id = id + self.status = status + self.user_tasks = user_tasks + self.last_task = last_task + self.next_task = next_task + +class WorkflowApiSchema(ma.Schema): + class Meta: + model = WorkflowApi + fields = ["id", "status", "user_tasks", "last_task", "next_task"] + 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) + next_task = marshmallow.fields.Nested(TaskSchema, dump_only=True) + + @marshmallow.post_load + def make_workflow(self, data, **kwargs): + return WorkflowApi(**data) \ No newline at end of file diff --git a/crc/workflow_processor.py b/crc/workflow_processor.py index 15a42aa5..67a06f3a 100644 --- a/crc/workflow_processor.py +++ b/crc/workflow_processor.py @@ -1,9 +1,7 @@ import xml.etree.ElementTree as ElementTree -from SpiffWorkflow import Task as SpiffTask +from SpiffWorkflow import Task as SpiffTask, Workflow from SpiffWorkflow.bpmn.BpmnScriptEngine import BpmnScriptEngine -from SpiffWorkflow.bpmn.parser.task_parsers import UserTaskParser -from SpiffWorkflow.bpmn.parser.util import full_tag from SpiffWorkflow.bpmn.serializer.BpmnSerializer import BpmnSerializer from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser @@ -54,6 +52,7 @@ class CustomBpmnScriptEngine(BpmnScriptEngine): details=str(ne)) + class MyCustomParser(BpmnDmnParser): """ A BPMN and DMN parser that can also parse Camunda forms. @@ -61,8 +60,7 @@ class MyCustomParser(BpmnDmnParser): OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES OVERRIDE_PARSER_CLASSES.update(CamundaParser.OVERRIDE_PARSER_CLASSES) - -class WorkflowProcessor: +class WorkflowProcessor(object): _script_engine = CustomBpmnScriptEngine() _serializer = BpmnSerializer() @@ -96,6 +94,8 @@ class WorkflowProcessor: raise(Exception("There is no primary BPMN model defined for workflow %s" % workflow_spec_id)) return parser.get_spec(process_id) + + @classmethod def create(cls, workflow_spec_id): spec = WorkflowProcessor.get_spec(workflow_spec_id) @@ -123,6 +123,21 @@ class WorkflowProcessor: def next_user_tasks(self): return self.bpmn_workflow.get_ready_user_tasks() + def next_task(self): + """Returns the next user task that should be completed + even if there are parallel tasks and mulitple options are + available.""" + ready_tasks = self.bpmn_workflow.get_ready_user_tasks() + if len(ready_tasks) == 0: + return None + elif len(ready_tasks) == 1: + return ready_tasks[0] + else: + for task in ready_tasks: + if task.parent == self.bpmn_workflow.last_task: + return task; + return ready_tasks[0] + def complete_task(self, task): self.bpmn_workflow.complete_task_from_id(task.id) diff --git a/tests/test_api.py b/tests/test_api.py index 7feeab99..eb75ba43 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -5,7 +5,7 @@ from crc import session from crc.models.file import FileModel from crc.models.study import StudyModel, StudyModelSchema, ProtocolBuilderStatus from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \ - WorkflowModelSchema, TaskSchema + WorkflowApiSchema from tests.base_test import BaseTest @@ -162,15 +162,15 @@ class TestStudy(BaseTest): data=json.dumps(WorkflowSpecModelSchema().dump(spec))) self.assert_success(rv) self.assertEqual(1, session.query(WorkflowModel).count()) - workflow = session.query(WorkflowModel).first() - self.assertEqual(study.id, workflow.study_id) - self.assertEqual(WorkflowStatus.user_input_required, workflow.status) - self.assertIsNotNone(workflow.bpmn_workflow_json) - self.assertEqual(spec.id, workflow.workflow_spec_id) + workflow_model = session.query(WorkflowModel).first() + self.assertEqual(study.id, workflow_model.study_id) + self.assertEqual(WorkflowStatus.user_input_required, workflow_model.status) + self.assertIsNotNone(workflow_model.bpmn_workflow_json) + self.assertEqual(spec.id, workflow_model.workflow_spec_id) json_data = json.loads(rv.get_data(as_text=True)) - workflow2 = WorkflowModelSchema().load(json_data, session=session) - self.assertEqual(workflow.id, workflow2.id) + workflow2 = WorkflowApiSchema().load(json_data) + self.assertEqual(workflow_model.id, workflow2.id) def test_delete_workflow(self): self.load_example_data() @@ -180,8 +180,7 @@ class TestStudy(BaseTest): data=json.dumps(WorkflowSpecModelSchema().dump(spec))) self.assertEqual(1, session.query(WorkflowModel).count()) json_data = json.loads(rv.get_data(as_text=True)) - workflow = WorkflowModelSchema().load(json_data, session=session) + workflow = WorkflowApiSchema().load(json_data) rv = self.app.delete('/v1.0/workflow/%i' % workflow.id) self.assert_success(rv) self.assertEqual(0, session.query(WorkflowModel).count()) - diff --git a/tests/test_tasks_api.py b/tests/test_tasks_api.py index de29c8a1..1ab9c01e 100644 --- a/tests/test_tasks_api.py +++ b/tests/test_tasks_api.py @@ -4,8 +4,8 @@ from datetime import datetime from crc import session from crc.models.file import FileModel from crc.models.study import StudyModel, StudyModelSchema, ProtocolBuilderStatus -from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \ - WorkflowModelSchema, TaskSchema +from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, \ + WorkflowStatus, TaskSchema, WorkflowApiSchema from tests.base_test import BaseTest @@ -19,18 +19,11 @@ class TestTasksApi(BaseTest): workflow = session.query(WorkflowModel).filter_by(study_id = study.id, workflow_spec_id=workflow_name).first() return workflow - def get_tasks(self, workflow): - rv = self.app.get('/v1.0/workflow/%i/tasks' % workflow.id, content_type="application/json") + def get_workflow_api(self, workflow): + rv = self.app.get('/v1.0/workflow/%i' % workflow.id, content_type="application/json") json_data = json.loads(rv.get_data(as_text=True)) - tasks = TaskSchema(many=True).load(json_data) - return tasks - - def get_all_tasks(self, workflow): - rv = self.app.get('/v1.0/workflow/%i/all_tasks' % workflow.id, content_type="application/json") - self.assert_success(rv) - json_data = json.loads(rv.get_data(as_text=True)) - all_tasks = TaskSchema(many=True).load(json_data) - return all_tasks + workflow_api = WorkflowApiSchema().load(json_data) + return workflow_api def complete_form(self, workflow, task, dict_data): rv = self.app.put('/v1.0/workflow/%i/task/%s/data' % (workflow.id, task.id), @@ -38,13 +31,13 @@ class TestTasksApi(BaseTest): data=json.dumps(dict_data)) self.assert_success(rv) json_data = json.loads(rv.get_data(as_text=True)) - workflow = WorkflowModelSchema().load(json_data, session=session) + workflow = WorkflowApiSchema().load(json_data) return workflow def test_get_current_user_tasks(self): self.load_example_data() workflow = self.create_workflow('random_fact') - tasks = self.get_tasks(workflow) + tasks = self.get_workflow_api(workflow).user_tasks self.assertEqual("Task_User_Select_Type", tasks[0].name) self.assertEqual(3, len(tasks[0].form["fields"][0]["options"])) @@ -53,22 +46,22 @@ class TestTasksApi(BaseTest): self.load_example_data() workflow = self.create_workflow('two_forms') # get the first form in the two form workflow. - tasks = self.get_tasks(workflow) - self.assertEqual(1, len(tasks)) - self.assertIsNotNone(tasks[0].form) - self.assertEqual("StepOne", tasks[0].name) - self.assertEqual(1, len(tasks[0].form['fields'])) + workflow_api = self.get_workflow_api(workflow) + self.assertEqual(2, len(workflow_api.user_tasks)) + self.assertIsNotNone(workflow_api.user_tasks[0].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, tasks[0], {"color": "blue"}) + self.complete_form(workflow, workflow_api.user_tasks[0], {"color": "blue"}) # Get the next Task - tasks = self.get_tasks(workflow) - self.assertEqual("StepTwo", tasks[0].name) + workflow_api = self.get_workflow_api(workflow) + self.assertEqual("StepTwo", workflow_api.next_task['name']) # Get all user Tasks and check that the data have been saved - all_tasks = self.get_all_tasks(workflow) - for task in all_tasks: + for task in workflow_api.user_tasks: self.assertIsNotNone(task.data) for val in task.data.values(): self.assertIsNotNone(val) @@ -78,18 +71,40 @@ class TestTasksApi(BaseTest): workflow = self.create_workflow('exclusive_gateway') # get the first form in the two form workflow. - tasks = self.get_tasks(workflow) + tasks = self.get_workflow_api(workflow).user_tasks self.complete_form(workflow, tasks[0], {"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_tasks(workflow) + tasks = self.get_workflow_api(workflow).user_tasks self.complete_form(workflow, tasks[0], {"has_bananas": True}) # Get the next Task - tasks = self.get_tasks(workflow) - self.assertEqual("Task_Num_Bananas", tasks[0].name) + workflow_api = self.get_workflow_api(workflow) + self.assertEqual("Task_Num_Bananas", workflow_api.next_task['name']) + + 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.assertEquals({"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) diff --git a/tests/test_workflow_processor.py b/tests/test_workflow_processor.py index bfa02630..0ccb8f2f 100644 --- a/tests/test_workflow_processor.py +++ b/tests/test_workflow_processor.py @@ -6,7 +6,7 @@ from crc.api.rest_exception import RestException from crc.models.file import FileModel from crc.models.workflow import WorkflowSpecModel, WorkflowStatus from tests.base_test import BaseTest -from crc.workflow_processor import WorkflowProcessor +from crc.workflow_processor import Workflow, WorkflowProcessor class TestWorkflowProcessor(BaseTest): @@ -17,7 +17,7 @@ class TestWorkflowProcessor(BaseTest): letters = string.ascii_lowercase return ''.join(random.choice(letters) for i in range(stringLength)) - def _complete_form_with_random_data(self, task): + def _populate_form_with_random_data(self, task): form_data = {} for field in task.task_spec.form.fields: form_data[field.id] = self._randomString() @@ -79,10 +79,10 @@ class TestWorkflowProcessor(BaseTest): self.assertEqual(WorkflowStatus.user_input_required, processor.get_status()) next_user_tasks = processor.next_user_tasks() self.assertEqual(4, len(next_user_tasks)) - self._complete_form_with_random_data(next_user_tasks[0]) - self._complete_form_with_random_data(next_user_tasks[1]) - self._complete_form_with_random_data(next_user_tasks[2]) - self._complete_form_with_random_data(next_user_tasks[3]) + self._populate_form_with_random_data(next_user_tasks[0]) + self._populate_form_with_random_data(next_user_tasks[1]) + self._populate_form_with_random_data(next_user_tasks[2]) + self._populate_form_with_random_data(next_user_tasks[3]) processor.complete_task(next_user_tasks[0]) processor.complete_task(next_user_tasks[1]) processor.complete_task(next_user_tasks[2]) @@ -90,10 +90,10 @@ class TestWorkflowProcessor(BaseTest): # There are another 4 tasks to complete (each task, had a follow up task in the parallel list) next_user_tasks = processor.next_user_tasks() self.assertEqual(4, len(next_user_tasks)) - self._complete_form_with_random_data(next_user_tasks[0]) - self._complete_form_with_random_data(next_user_tasks[1]) - self._complete_form_with_random_data(next_user_tasks[2]) - self._complete_form_with_random_data(next_user_tasks[3]) + self._populate_form_with_random_data(next_user_tasks[0]) + self._populate_form_with_random_data(next_user_tasks[1]) + self._populate_form_with_random_data(next_user_tasks[2]) + self._populate_form_with_random_data(next_user_tasks[3]) processor.complete_task(next_user_tasks[0]) processor.complete_task(next_user_tasks[1]) processor.complete_task(next_user_tasks[2]) @@ -101,13 +101,34 @@ class TestWorkflowProcessor(BaseTest): processor.do_engine_steps() self.assertTrue(processor.bpmn_workflow.is_completed()) + def test_workflow_processor_knows_the_text_task_even_when_parallel(self): + self.load_example_data() + workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="parallel_tasks").first() + processor = WorkflowProcessor.create(workflow_spec_model.id) + self.assertEqual(WorkflowStatus.user_input_required, processor.get_status()) + next_user_tasks = processor.next_user_tasks() + self.assertEqual(4, len(next_user_tasks)) + self.assertEqual(next_user_tasks[0], processor.next_task(), "First task in list of 4") + + # Complete the third open task, so do things out of order + # this should cause the system to recommend the first ready task that is a + # child of the last completed task. + task = next_user_tasks[2] + self._populate_form_with_random_data(task) + processor.complete_task(task) + next_user_tasks = processor.next_user_tasks() + self.assertEqual(processor.bpmn_workflow.last_task, task) + self.assertEqual(4, len(next_user_tasks)) + self.assertEqual(task.children[0], processor.next_task()) + + def test_workflow_with_bad_expression_raises_sensible_error(self): workflow_spec_model = self.load_test_spec("invalid_expression") processor = WorkflowProcessor.create(workflow_spec_model.id) processor.do_engine_steps() next_user_tasks = processor.next_user_tasks() self.assertEqual(1, len(next_user_tasks)) - self._complete_form_with_random_data(next_user_tasks[0]) + self._populate_form_with_random_data(next_user_tasks[0]) processor.complete_task(next_user_tasks[0]) with self.assertRaises(RestException) as context: processor.do_engine_steps()