Skip to content

Commit

Permalink
add support for conditional events
Browse files Browse the repository at this point in the history
  • Loading branch information
essweine committed Sep 29, 2023
1 parent 92a7fdc commit d4bd493
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 12 deletions.
30 changes: 21 additions & 9 deletions SpiffWorkflow/bpmn/parser/event_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@
CorrelationProperty
)
from SpiffWorkflow.bpmn.specs.event_definitions.multiple import MultipleEventDefinition

from SpiffWorkflow.bpmn.specs.event_definitions.conditional import ConditionalEventDefinition

CANCEL_EVENT_XPATH = './/bpmn:cancelEventDefinition'
CONDITIONAL_EVENT_XPATH = './/bpmn:conditionalEventDefinition'
ERROR_EVENT_XPATH = './/bpmn:errorEventDefinition'
ESCALATION_EVENT_XPATH = './/bpmn:escalationEventDefinition'
TERMINATION_EVENT_XPATH = './/bpmn:terminateEventDefinition'
Expand Down Expand Up @@ -78,6 +79,12 @@ def get_event_description(self, event):
def parse_cancel_event(self, event):
return CancelEventDefinition(description=self.get_event_description(event))

def parse_conditional_event(self, event):
expression = self.xpath('.//bpmn:condition')
if len(expression) == 0:
raise ValidationException('Conditional event definition with missing condition', node=self.node, file_name=self.filename)
return ConditionalEventDefinition(expression[0].text, description=self.get_event_description(event))

def parse_error_event(self, error_event):
"""Parse the errorEventDefinition node and return an instance of ErrorEventDefinition."""
error_ref = error_event.get('errorRef')
Expand Down Expand Up @@ -202,6 +209,8 @@ def get_event_definition(self, xpaths):
event_definitions.append(self.parse_escalation_event(event))
elif path == TERMINATION_EVENT_XPATH:
event_definitions.append(self.parse_terminate_event(event))
elif path == CONDITIONAL_EVENT_XPATH:
event_definitions.append(self.parse_conditional_event(event))

parallel = self.node.get('parallelMultiple') == 'true'

Expand All @@ -218,7 +227,8 @@ class StartEventParser(EventDefinitionParser):
Support Message, Signal, and Timer events."""

def create_task(self):
event_definition = self.get_event_definition([MESSAGE_EVENT_XPATH, SIGNAL_EVENT_XPATH, TIMER_EVENT_XPATH])
event_definition = self.get_event_definition(
[MESSAGE_EVENT_XPATH, SIGNAL_EVENT_XPATH, TIMER_EVENT_XPATH, CONDITIONAL_EVENT_XPATH])
task = self._create_task(event_definition)
self.spec.start.connect(task)
return task
Expand All @@ -231,8 +241,8 @@ class EndEventParser(EventDefinitionParser):
"""Parses an End Event. Handles Termination, Escalation, Cancel, and Error End Events."""

def create_task(self):
event_definition = self.get_event_definition([MESSAGE_EVENT_XPATH, CANCEL_EVENT_XPATH, ERROR_EVENT_XPATH,
ESCALATION_EVENT_XPATH, TERMINATION_EVENT_XPATH])
event_definition = self.get_event_definition(
[MESSAGE_EVENT_XPATH, CANCEL_EVENT_XPATH, ERROR_EVENT_XPATH, ESCALATION_EVENT_XPATH, TERMINATION_EVENT_XPATH])
task = self._create_task(event_definition)
task.connect(self.spec.end)
return task
Expand All @@ -242,16 +252,17 @@ class IntermediateCatchEventParser(EventDefinitionParser):
"""Parses an Intermediate Catch Event. Currently supports Message, Signal, and Timer definitions."""

def create_task(self):
event_definition = self.get_event_definition([MESSAGE_EVENT_XPATH, SIGNAL_EVENT_XPATH, TIMER_EVENT_XPATH])
event_definition = self.get_event_definition(
[MESSAGE_EVENT_XPATH, SIGNAL_EVENT_XPATH, TIMER_EVENT_XPATH, CONDITIONAL_EVENT_XPATH])
return super()._create_task(event_definition)


class IntermediateThrowEventParser(EventDefinitionParser):
"""Parses an Intermediate Catch Event. Currently supports Message, Signal and Timer event definitions."""

def create_task(self):
event_definition = self.get_event_definition([ESCALATION_EVENT_XPATH, MESSAGE_EVENT_XPATH,
SIGNAL_EVENT_XPATH, TIMER_EVENT_XPATH])
event_definition = self.get_event_definition(
[ESCALATION_EVENT_XPATH, MESSAGE_EVENT_XPATH, SIGNAL_EVENT_XPATH, TIMER_EVENT_XPATH])
return self._create_task(event_definition)


Expand Down Expand Up @@ -284,8 +295,9 @@ class BoundaryEventParser(EventDefinitionParser):

def create_task(self):
cancel_activity = self.node.get('cancelActivity', default='true').lower() == 'true'
event_definition = self.get_event_definition([CANCEL_EVENT_XPATH, ERROR_EVENT_XPATH, ESCALATION_EVENT_XPATH,
MESSAGE_EVENT_XPATH, SIGNAL_EVENT_XPATH, TIMER_EVENT_XPATH])
event_definition = self.get_event_definition(
[CANCEL_EVENT_XPATH, ERROR_EVENT_XPATH, ESCALATION_EVENT_XPATH,
MESSAGE_EVENT_XPATH, SIGNAL_EVENT_XPATH, TIMER_EVENT_XPATH, CONDITIONAL_EVENT_XPATH])
if isinstance(event_definition, NoneEventDefinition):
raise NotImplementedError('Unsupported Catch Event: %r', etree.tostring(self.node))
return self._create_task(event_definition, cancel_activity)
Expand Down
1 change: 1 addition & 0 deletions SpiffWorkflow/bpmn/parser/spec_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@
full_tag('messageEventDefinition'): 'Message',
full_tag('signalEventDefinition'): 'Signal',
full_tag('timerEventDefinition'): 'Timer',
full_tag('conditionalEventDefinition'): 'Conditional',
}
4 changes: 1 addition & 3 deletions SpiffWorkflow/bpmn/specs/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,10 @@ def _predict_hook(self, my_task):
if not isinstance(child.task_spec, BoundaryEvent):
child._set_state(state)

def _update_hook(self, my_task):
super()._update_hook(my_task)
def _run_hook(self, my_task):
for task in my_task.children:
if isinstance(task.task_spec, BoundaryEvent) and task.has_state(TaskState.PREDICTED_MASK):
task._set_state(TaskState.WAITING)
task.task_spec._predict(task)
return True


Expand Down
14 changes: 14 additions & 0 deletions SpiffWorkflow/bpmn/specs/event_definitions/conditional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from .base import EventDefinition

class ConditionalEventDefinition(EventDefinition):
"""Conditional events can be used to trigger flows based on the state of the workflow"""

def __init__(self, expression, **kwargs):
super().__init__(**kwargs)
self.expression = expression

def has_fired(self, my_task):
my_task._set_internal_data(
has_fired=my_task.workflow.script_engine.evaluate(my_task, self.expression, external_methods=my_task.workflow.data)
)
return my_task._get_internal_data('has_fired', False)
246 changes: 246 additions & 0 deletions tests/SpiffWorkflow/bpmn/data/conditional_event.bpmn
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
<?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:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1qnx3d3" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.17.0">
<bpmn:collaboration id="Collaboration_1ud0u7p">
<bpmn:participant id="Participant_0uwwo9w" processRef="intermediate" />
<bpmn:participant id="Participant_0gbaaol" processRef="boundary" />
</bpmn:collaboration>
<bpmn:process id="intermediate" isExecutable="true">
<bpmn:startEvent id="start_1">
<bpmn:outgoing>Flow_1mljxxc</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1mljxxc" sourceRef="start_1" targetRef="gateway_a" />
<bpmn:parallelGateway id="gateway_a">
<bpmn:incoming>Flow_1mljxxc</bpmn:incoming>
<bpmn:outgoing>Flow_1h0v0hk</bpmn:outgoing>
<bpmn:outgoing>Flow_0mhqfql</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:task id="task_a" name="A">
<bpmn:incoming>Flow_1h0v0hk</bpmn:incoming>
<bpmn:outgoing>Flow_17j4s3n</bpmn:outgoing>
<bpmn:dataOutputAssociation id="DataOutputAssociation_0iw335n">
<bpmn:targetRef>task_a_state</bpmn:targetRef>
</bpmn:dataOutputAssociation>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_1h0v0hk" sourceRef="gateway_a" targetRef="task_a" />
<bpmn:dataObjectReference id="task_a_state" name="Task A State" dataObjectRef="task_a_done" />
<bpmn:dataObject id="task_a_done" />
<bpmn:task id="task_b" name="B">
<bpmn:incoming>Flow_0mhqfql</bpmn:incoming>
<bpmn:outgoing>Flow_0ko13c7</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_0mhqfql" sourceRef="gateway_a" targetRef="task_b" />
<bpmn:sequenceFlow id="Flow_0ko13c7" sourceRef="task_b" targetRef="event_1" />
<bpmn:intermediateCatchEvent id="event_1">
<bpmn:incoming>Flow_0ko13c7</bpmn:incoming>
<bpmn:outgoing>Flow_0sopv1l</bpmn:outgoing>
<bpmn:conditionalEventDefinition id="ConditionalEventDefinition_1hjay1u">
<bpmn:condition>task_a_done</bpmn:condition>
</bpmn:conditionalEventDefinition>
</bpmn:intermediateCatchEvent>
<bpmn:sequenceFlow id="Flow_17j4s3n" sourceRef="task_a" targetRef="gateway_b" />
<bpmn:parallelGateway id="gateway_b">
<bpmn:incoming>Flow_17j4s3n</bpmn:incoming>
<bpmn:incoming>Flow_0sopv1l</bpmn:incoming>
<bpmn:outgoing>Flow_1h2qxht</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:sequenceFlow id="Flow_0sopv1l" sourceRef="event_1" targetRef="gateway_b" />
<bpmn:sequenceFlow id="Flow_1h2qxht" sourceRef="gateway_b" targetRef="end_1" />
<bpmn:endEvent id="end_1">
<bpmn:incoming>Flow_1h2qxht</bpmn:incoming>
</bpmn:endEvent>
</bpmn:process>
<bpmn:process id="boundary">
<bpmn:startEvent id="start_2">
<bpmn:outgoing>Flow_1831ias</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1831ias" sourceRef="start_2" targetRef="gateway_c" />
<bpmn:parallelGateway id="gateway_c">
<bpmn:incoming>Flow_1831ias</bpmn:incoming>
<bpmn:outgoing>Flow_02lwya2</bpmn:outgoing>
<bpmn:outgoing>Flow_1tr8d48</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:sequenceFlow id="Flow_02lwya2" sourceRef="gateway_c" targetRef="task_c" />
<bpmn:endEvent id="end_2">
<bpmn:incoming>Flow_1jup2i8</bpmn:incoming>
</bpmn:endEvent>
<bpmn:dataObjectReference id="task_c_state" name="Task C State" dataObjectRef="task_c_done" />
<bpmn:dataObject id="task_c_done" />
<bpmn:sequenceFlow id="Flow_1tr8d48" sourceRef="gateway_c" targetRef="task_d" />
<bpmn:sequenceFlow id="Flow_1jup2i8" sourceRef="gateway_d" targetRef="end_2" />
<bpmn:sequenceFlow id="Flow_0e9kim0" sourceRef="task_c" targetRef="gateway_d" />
<bpmn:task id="task_c" name="C">
<bpmn:incoming>Flow_02lwya2</bpmn:incoming>
<bpmn:outgoing>Flow_0e9kim0</bpmn:outgoing>
<bpmn:dataOutputAssociation id="DataOutputAssociation_0o9iwa9">
<bpmn:targetRef>task_c_state</bpmn:targetRef>
</bpmn:dataOutputAssociation>
</bpmn:task>
<bpmn:task id="task_d" name="D">
<bpmn:incoming>Flow_1tr8d48</bpmn:incoming>
<bpmn:outgoing>Flow_0qwg2no</bpmn:outgoing>
</bpmn:task>
<bpmn:boundaryEvent id="event_2" attachedToRef="task_d">
<bpmn:outgoing>Flow_073y2vz</bpmn:outgoing>
<bpmn:conditionalEventDefinition id="ConditionalEventDefinition_0pky92w">
<bpmn:condition>task_c_done</bpmn:condition>
</bpmn:conditionalEventDefinition>
</bpmn:boundaryEvent>
<bpmn:parallelGateway id="gateway_d">
<bpmn:incoming>Flow_0e9kim0</bpmn:incoming>
<bpmn:incoming>Flow_053v4qk</bpmn:incoming>
<bpmn:outgoing>Flow_1jup2i8</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:exclusiveGateway id="exclusive" default="Flow_053v4qk">
<bpmn:incoming>Flow_0qwg2no</bpmn:incoming>
<bpmn:incoming>Flow_073y2vz</bpmn:incoming>
<bpmn:outgoing>Flow_053v4qk</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_0qwg2no" sourceRef="task_d" targetRef="exclusive" />
<bpmn:sequenceFlow id="Flow_053v4qk" sourceRef="exclusive" targetRef="gateway_d" />
<bpmn:sequenceFlow id="Flow_073y2vz" sourceRef="event_2" targetRef="exclusive" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_1ud0u7p">
<bpmndi:BPMNShape id="Participant_0uwwo9w_di" bpmnElement="Participant_0uwwo9w" isHorizontal="true">
<dc:Bounds x="120" y="40" width="720" height="430" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0b6igq0_di" bpmnElement="start_1">
<dc:Bounds x="182" y="242" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1vlm0pg_di" bpmnElement="gateway_a">
<dc:Bounds x="275" y="235" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0nef4mu_di" bpmnElement="task_a">
<dc:Bounds x="390" y="220" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_02rjzbp_di" bpmnElement="task_b">
<dc:Bounds x="390" y="330" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1ejo66d_di" bpmnElement="event_1">
<dc:Bounds x="562" y="352" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_05lg9g5_di" bpmnElement="gateway_b">
<dc:Bounds x="635" y="235" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0u9ktd6_di" bpmnElement="end_1">
<dc:Bounds x="752" y="242" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1mljxxc_di" bpmnElement="Flow_1mljxxc">
<di:waypoint x="218" y="260" />
<di:waypoint x="275" y="260" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1h0v0hk_di" bpmnElement="Flow_1h0v0hk">
<di:waypoint x="325" y="260" />
<di:waypoint x="390" y="260" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="DataObjectReference_1htt05t_di" bpmnElement="task_a_state">
<dc:Bounds x="422" y="105" width="36" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="411" y="75" width="62" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0mhqfql_di" bpmnElement="Flow_0mhqfql">
<di:waypoint x="300" y="285" />
<di:waypoint x="300" y="370" />
<di:waypoint x="390" y="370" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0ko13c7_di" bpmnElement="Flow_0ko13c7">
<di:waypoint x="490" y="370" />
<di:waypoint x="562" y="370" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_17j4s3n_di" bpmnElement="Flow_17j4s3n">
<di:waypoint x="490" y="260" />
<di:waypoint x="635" y="260" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0sopv1l_di" bpmnElement="Flow_0sopv1l">
<di:waypoint x="598" y="370" />
<di:waypoint x="660" y="370" />
<di:waypoint x="660" y="285" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1h2qxht_di" bpmnElement="Flow_1h2qxht">
<di:waypoint x="685" y="260" />
<di:waypoint x="752" y="260" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Participant_0gbaaol_di" bpmnElement="Participant_0gbaaol" isHorizontal="true">
<dc:Bounds x="120" y="510" width="720" height="410" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0j81tcf_di" bpmnElement="start_2">
<dc:Bounds x="182" y="672" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_13zx39j_di" bpmnElement="gateway_c">
<dc:Bounds x="275" y="665" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0le2z7i_di" bpmnElement="end_2">
<dc:Bounds x="752" y="672" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1wxbofg_di" bpmnElement="task_c">
<dc:Bounds x="420" y="650" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="DataObjectReference_1kr2c69_di" bpmnElement="task_c_state">
<dc:Bounds x="452" y="555" width="36" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="439" y="518" width="63" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0uodsbv_di" bpmnElement="task_d">
<dc:Bounds x="420" y="760" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0dccwe5_di" bpmnElement="gateway_d">
<dc:Bounds x="615" y="665" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1gi8vv5_di" bpmnElement="exclusive" isMarkerVisible="true">
<dc:Bounds x="615" y="775" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1xbhk0a_di" bpmnElement="event_2">
<dc:Bounds x="452" y="822" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1831ias_di" bpmnElement="Flow_1831ias">
<di:waypoint x="218" y="690" />
<di:waypoint x="275" y="690" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_02lwya2_di" bpmnElement="Flow_02lwya2">
<di:waypoint x="325" y="690" />
<di:waypoint x="420" y="690" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1tr8d48_di" bpmnElement="Flow_1tr8d48">
<di:waypoint x="300" y="715" />
<di:waypoint x="300" y="800" />
<di:waypoint x="420" y="800" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1jup2i8_di" bpmnElement="Flow_1jup2i8">
<di:waypoint x="665" y="690" />
<di:waypoint x="752" y="690" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0e9kim0_di" bpmnElement="Flow_0e9kim0">
<di:waypoint x="520" y="690" />
<di:waypoint x="615" y="690" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0qwg2no_di" bpmnElement="Flow_0qwg2no">
<di:waypoint x="520" y="800" />
<di:waypoint x="615" y="800" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_053v4qk_di" bpmnElement="Flow_053v4qk">
<di:waypoint x="640" y="775" />
<di:waypoint x="640" y="715" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_073y2vz_di" bpmnElement="Flow_073y2vz">
<di:waypoint x="470" y="858" />
<di:waypoint x="470" y="878" />
<di:waypoint x="640" y="878" />
<di:waypoint x="640" y="825" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="DataOutputAssociation_0iw335n_di" bpmnElement="DataOutputAssociation_0iw335n">
<di:waypoint x="439" y="220" />
<di:waypoint x="438" y="155" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="DataOutputAssociation_0o9iwa9_di" bpmnElement="DataOutputAssociation_0o9iwa9">
<di:waypoint x="469" y="650" />
<di:waypoint x="468" y="605" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
Loading

0 comments on commit d4bd493

Please sign in to comment.