From 95b75f864d6c269c714e3d056f498a675c55a3ee Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Thu, 23 Jan 2020 15:32:53 -0500 Subject: [PATCH] You can now add multiple files to a workflow spec, and if properly linked, you can associate a DMN file with a BPMN to process decision tables. --- Pipfile.lock | 2 +- .../bpmn/decision_table/decision_table.bpmn | 55 +++++++++++++++++++ .../bpmn/decision_table/message_to_ginger.dmn | 48 ++++++++++++++++ .../bpmn/{ => random_fact}/random_fact.bpmn | 0 .../bpmn/{ => two_forms}/two_forms.bpmn | 0 crc/workflow_processor.py | 37 ++++++++++--- example_data.py | 50 +++++++++++++---- tests/test_api.py | 1 + tests/test_workflow_processor.py | 18 +++++- 9 files changed, 189 insertions(+), 22 deletions(-) create mode 100644 crc/static/bpmn/decision_table/decision_table.bpmn create mode 100644 crc/static/bpmn/decision_table/message_to_ginger.dmn rename crc/static/bpmn/{ => random_fact}/random_fact.bpmn (100%) rename crc/static/bpmn/{ => two_forms}/two_forms.bpmn (100%) diff --git a/Pipfile.lock b/Pipfile.lock index a41e37b7..0b72321c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -667,7 +667,7 @@ "spiffworkflow": { "editable": true, "git": "https://github.com/sartography/SpiffWorkflow.git", - "ref": "7dd5bfb15fc227c2b5a36c052f987adf9c0df1a8" + "ref": "7640c6e32d3894b13f8a078849922cf7cb6884a5" }, "sqlalchemy": { "hashes": [ diff --git a/crc/static/bpmn/decision_table/decision_table.bpmn b/crc/static/bpmn/decision_table/decision_table.bpmn new file mode 100644 index 00000000..d79740a3 --- /dev/null +++ b/crc/static/bpmn/decision_table/decision_table.bpmn @@ -0,0 +1,55 @@ + + + + + SequenceFlow_1ma1wxb + + + + + + + + + SequenceFlow_1ma1wxb + SequenceFlow_1uxaqwp + + + + SequenceFlow_1uxaqwp + SequenceFlow_0grui6f + + + SequenceFlow_0grui6f + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crc/static/bpmn/decision_table/message_to_ginger.dmn b/crc/static/bpmn/decision_table/message_to_ginger.dmn new file mode 100644 index 00000000..a5e58c86 --- /dev/null +++ b/crc/static/bpmn/decision_table/message_to_ginger.dmn @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + 0 + + + "GREAT Dog! I love you." + + + + + 1 + + + "Oh, Ginger." + + + + + 2 + + + "Sheesh, you silly dog." + + + + + > 2 + + + "!@#$!@#$" + + + + + diff --git a/crc/static/bpmn/random_fact.bpmn b/crc/static/bpmn/random_fact/random_fact.bpmn similarity index 100% rename from crc/static/bpmn/random_fact.bpmn rename to crc/static/bpmn/random_fact/random_fact.bpmn diff --git a/crc/static/bpmn/two_forms.bpmn b/crc/static/bpmn/two_forms/two_forms.bpmn similarity index 100% rename from crc/static/bpmn/two_forms.bpmn rename to crc/static/bpmn/two_forms/two_forms.bpmn diff --git a/crc/workflow_processor.py b/crc/workflow_processor.py index 7ff4aea5..d1b06c69 100644 --- a/crc/workflow_processor.py +++ b/crc/workflow_processor.py @@ -1,13 +1,15 @@ import xml.etree.ElementTree as ElementTree 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 - +from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser from crc import session -from crc.models.file import FileDataModel, FileModel +from crc.models.file import FileDataModel, FileModel, FileType from crc.models.workflow import WorkflowStatus @@ -31,6 +33,14 @@ class CustomBpmnScriptEngine(BpmnScriptEngine): klass().do_task(task.data) +class MyCustomParser(BpmnDmnParser): + """ + A BPMN and DMN parser that can also parse Camunda forms. + """ + OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES + OVERRIDE_PARSER_CLASSES.update(CamundaParser.OVERRIDE_PARSER_CLASSES) + + class WorkflowProcessor: _script_engine = CustomBpmnScriptEngine() _serializer = BpmnSerializer() @@ -40,23 +50,34 @@ class WorkflowProcessor: self.bpmn_workflow = self._serializer.deserialize_workflow(bpmn_json, workflow_spec=wf_spec) self.bpmn_workflow.script_engine = self._script_engine + @staticmethod + def get_parser(): + parser = MyCustomParser() + return parser + @staticmethod def get_spec(workflow_spec_id): + parser = WorkflowProcessor.get_parser() + process_id = None file_data_models = session.query(FileDataModel) \ .join(FileModel) \ .filter(FileModel.workflow_spec_id == workflow_spec_id).all() - parser = CamundaParser() for file_data in file_data_models: - bpmn: ElementTree.Element = ElementTree.fromstring(file_data.data) - process_id = WorkflowProcessor.__get_process_id(bpmn) - parser.add_bpmn_xml(bpmn, filename=file_data.file_model.name) + if file_data.file_model.type == FileType.bpmn: + bpmn: ElementTree.Element = ElementTree.fromstring(file_data.data) + if file_data.file_model.primary: + process_id = WorkflowProcessor.__get_process_id(bpmn) + parser.add_bpmn_xml(bpmn, filename=file_data.file_model.name) + 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 process_id is None: + raise(Exception("There is no primary BPMN model defined for workflow " + workflow_spec_id)) return parser.get_spec(process_id) @classmethod def create(cls, workflow_spec_id): - parser = CamundaParser() spec = WorkflowProcessor.get_spec(workflow_spec_id) - bpmn_workflow = BpmnWorkflow(spec, script_engine=cls._script_engine) bpmn_workflow.do_engine_steps() json = cls._serializer.serialize_workflow(bpmn_workflow) diff --git a/example_data.py b/example_data.py index 947c3e55..6ddcae70 100644 --- a/example_data.py +++ b/example_data.py @@ -1,4 +1,5 @@ import datetime +import glob import os from crc import app, db, session @@ -36,26 +37,53 @@ class ExampleDataLoader: description='Displays a random fact about a topic of your choosing.') workflow_specifications += \ self.create_spec(id="two_forms", - display_name="Two dump questions on two seperate tasks", - description='Displays a random fact about a topic of your choosing.') + display_name="Two dump questions on two separate tasks", + description='the name says it all') + workflow_specifications += \ + self.create_spec(id="decision_table", + display_name="Form with Decision Table", + description='the name says it all') + all_data = studies + workflow_specifications return all_data def create_spec(self, id, display_name, description): - """Assumes that a file exists in static/bpmn with the same name as the given id. + """Assumes that a directory exists in static/bpmn with the same name as the given id. + further assumes that the [id].bpmn is the primary file for the workflow. returns an array of data models to be added to the database.""" + models = [] spec = WorkflowSpecModel(id=id, display_name=display_name, description=description) - file_model = FileModel(name=id + ".bpmn", type=FileType.bpmn, version="1", - last_updated=datetime.datetime.now(), primary=True, - workflow_spec_id=id) - filename = os.path.join(app.root_path, 'static', 'bpmn', id + ".bpmn") - file = open(filename, "rb") - workflow_data = FileDataModel(data=file.read(), file_model=file_model) - file.close() - return [spec, file_model, workflow_data] + models.append(spec) + + filepath = os.path.join(app.root_path, 'static', 'bpmn', id, "*") + files = glob.glob(filepath) + for file_path in files: + noise, file_extension = os.path.splitext(file_path) + filename = os.path.basename(file_path) + if file_extension.lower() == '.bpmn': + type=FileType.bpmn + elif file_extension.lower() == '.dmn': + type=FileType.dmn + elif file_extension.lower() == '.svg': + type = FileType.svg + else: + raise Exception("Unsupported file type:" + file_path) + continue + + is_primary = filename.lower() == id + ".bpmn" + file_model = FileModel(name=filename, type=type, version="1", + last_updated=datetime.datetime.now(), primary=is_primary, + workflow_spec_id=id) + models.append(file_model) + try: + file = open(file_path, "rb") + models.append(FileDataModel(data=file.read(), file_model=file_model)) + finally: + file.close() + return models @staticmethod def clean_db(): diff --git a/tests/test_api.py b/tests/test_api.py index 77390ec2..aede5fef 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -198,3 +198,4 @@ class TestStudy(BaseTest, unittest.TestCase): json_data = json.loads(rv.get_data(as_text=True)) tasks = TaskSchema(many=True).load(json_data) self.assertEqual("StepTwo", tasks[0].name) + diff --git a/tests/test_workflow_processor.py b/tests/test_workflow_processor.py index 26e3fdb9..94b1ba0b 100644 --- a/tests/test_workflow_processor.py +++ b/tests/test_workflow_processor.py @@ -1,6 +1,7 @@ import unittest from crc import session +from crc.models.file import FileModel from crc.models.workflow import WorkflowSpecModel, WorkflowStatus from crc.workflow_processor import WorkflowProcessor from tests.base_test import BaseTest @@ -32,11 +33,24 @@ class TestWorkflowProcessor(BaseTest, unittest.TestCase): self.assertIsNotNone(data) self.assertIn("details", data) - def test_two_forms(self): + def test_workflow_with_dmn(self): self.load_example_data() - workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="two_forms").first() + files = session.query(FileModel).filter_by(workflow_spec_id='decision_table').all() + self.assertEquals(2, len(files)) + workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="decision_table").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(1, len(next_user_tasks)) task = next_user_tasks[0] + self.assertEqual("get_num_presents", task.get_name()) + model = {"num_presents": 1} + if task.data is None: + task.data = {} + task.data.update(model) + processor.complete_task(task) + processor.do_engine_steps() data = processor.get_data() + self.assertIsNotNone(data) + self.assertIn("message", data) + self.assertEqual("Oh, Ginger.", data.get('message')) \ No newline at end of file