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.

This commit is contained in:
Dan Funk 2020-01-23 15:32:53 -05:00
parent 532c00fde5
commit 95b75f864d
9 changed files with 189 additions and 22 deletions

2
Pipfile.lock generated
View File

@ -667,7 +667,7 @@
"spiffworkflow": {
"editable": true,
"git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "7dd5bfb15fc227c2b5a36c052f987adf9c0df1a8"
"ref": "7640c6e32d3894b13f8a078849922cf7cb6884a5"
},
"sqlalchemy": {
"hashes": [

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1elv5t1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<bpmn:process id="Process_15vbyda" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1ma1wxb</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1ma1wxb" sourceRef="StartEvent_1" targetRef="get_num_presents" />
<bpmn:userTask id="get_num_presents" name="Get number of presents" camunda:formKey="present_question">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="num_presents" label="How many presents will my dog Ginger leave for me today?" type="long" defaultValue="0" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1ma1wxb</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1uxaqwp</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_1uxaqwp" sourceRef="get_num_presents" targetRef="Task_0sgafty" />
<bpmn:businessRuleTask id="Task_0sgafty" name="Message based on number of presents" camunda:decisionRef="presents_to_message">
<bpmn:incoming>SequenceFlow_1uxaqwp</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0grui6f</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:endEvent id="EndEvent_0tsqkyu">
<bpmn:incoming>SequenceFlow_0grui6f</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0grui6f" sourceRef="Task_0sgafty" targetRef="EndEvent_0tsqkyu" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_15vbyda">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1ma1wxb_di" bpmnElement="SequenceFlow_1ma1wxb">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_15w5gb3_di" bpmnElement="get_num_presents">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1uxaqwp_di" bpmnElement="SequenceFlow_1uxaqwp">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="BusinessRuleTask_10c5wgr_di" bpmnElement="Task_0sgafty">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0tsqkyu_di" bpmnElement="EndEvent_0tsqkyu">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0grui6f_di" bpmnElement="SequenceFlow_0grui6f">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0" id="Definitions_1hao5sb" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<decision id="presents_to_message" name="Decision 1">
<extensionElements>
<biodi:bounds x="150" y="150" width="180" height="80" />
</extensionElements>
<decisionTable id="decisionTable_1">
<input id="input_1" label="num_presents">
<inputExpression id="inputExpression_1" typeRef="long">
<text></text>
</inputExpression>
</input>
<output id="output_1" label="message" name="message" typeRef="string" />
<rule id="DecisionRule_0gl355z">
<inputEntry id="UnaryTests_06x22gk">
<text>0</text>
</inputEntry>
<outputEntry id="LiteralExpression_0yuxzxi">
<text>"GREAT Dog! I love you."</text>
</outputEntry>
</rule>
<rule id="DecisionRule_1s6l5b6">
<inputEntry id="UnaryTests_1oyo6k0">
<text>1</text>
</inputEntry>
<outputEntry id="LiteralExpression_09t5r62">
<text>"Oh, Ginger."</text>
</outputEntry>
</rule>
<rule id="DecisionRule_1dvd34d">
<inputEntry id="UnaryTests_1k557bj">
<text>2</text>
</inputEntry>
<outputEntry id="LiteralExpression_1n1eo23">
<text>"Sheesh, you silly dog."</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0tqqjg9">
<inputEntry id="UnaryTests_0dnd50d">
<text>&gt; 2</text>
</inputEntry>
<outputEntry id="LiteralExpression_0fk5uhh">
<text>"!@#$!@#$"</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -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)

View File

@ -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():

View File

@ -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)

View File

@ -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'))