mirror of
https://github.com/status-im/spiff-arena.git
synced 2025-02-10 16:56:31 +00:00
Squashed 'SpiffWorkflow/' changes from 46f410a28..46d3de27f
46d3de27f Merge pull request #267 from sartography/feature/dmn_collect_policy 2d5ca32d5 Support for the "COLLECT" hit policy. * DecisionTable constructor now expects a third argument (the HitPolicy) * DMNParser now checks for a hitPolicy attribute, but defaults ot "UNIQUE" as Camunda doesn't put another in there if Unique is selected. * DecisionTable deserializer will default to a hitPolicy of "UNIQUE" if not value is in the Json. git-subtree-dir: SpiffWorkflow git-subtree-split: 46d3de27ffbcaf60025f09d1cf04fcc7ee98658a
This commit is contained in:
parent
d2af3e7252
commit
cc1903387b
@ -177,7 +177,7 @@ class BPMNDictionarySerializer(DictionarySerializer):
|
|||||||
return s_state
|
return s_state
|
||||||
|
|
||||||
def deserialize_business_rule_task(self, wf_spec, s_state):
|
def deserialize_business_rule_task(self, wf_spec, s_state):
|
||||||
dt = DecisionTable(None,None)
|
dt = DecisionTable(None, None, None)
|
||||||
dt.deserialize(s_state['dmn'])
|
dt.deserialize(s_state['dmn'])
|
||||||
dmn_engine = DMNEngine(dt)
|
dmn_engine = DMNEngine(dt)
|
||||||
spec = BusinessRuleTask(wf_spec, s_state['name'], dmn_engine)
|
spec = BusinessRuleTask(wf_spec, s_state['name'], dmn_engine)
|
||||||
@ -224,7 +224,7 @@ class BPMNDictionarySerializer(DictionarySerializer):
|
|||||||
if s_state.get('expanded',None):
|
if s_state.get('expanded',None):
|
||||||
cls.expanded = self.deserialize_arg(s_state['expanded'])
|
cls.expanded = self.deserialize_arg(s_state['expanded'])
|
||||||
if isinstance(cls,BusinessRuleTask):
|
if isinstance(cls,BusinessRuleTask):
|
||||||
dt = DecisionTable(None,None)
|
dt = DecisionTable(None,None,None)
|
||||||
dt.deserialize(s_state['dmn'])
|
dt.deserialize(s_state['dmn'])
|
||||||
dmn_engine = DMNEngine(dt)
|
dmn_engine = DMNEngine(dt)
|
||||||
cls.dmnEngine=dmn_engine
|
cls.dmnEngine=dmn_engine
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from ..specs.model import HitPolicy
|
||||||
from ...util import levenshtein
|
from ...util import levenshtein
|
||||||
from ...workflow import WorkflowException
|
from ...workflow import WorkflowException
|
||||||
|
|
||||||
@ -16,9 +17,32 @@ class DMNEngine:
|
|||||||
self.decision_table = decision_table
|
self.decision_table = decision_table
|
||||||
|
|
||||||
def decide(self, task):
|
def decide(self, task):
|
||||||
|
rules = []
|
||||||
for rule in self.decision_table.rules:
|
for rule in self.decision_table.rules:
|
||||||
if self.__check_rule(rule, task):
|
if self.__check_rule(rule, task):
|
||||||
return rule
|
rules.append(rule)
|
||||||
|
if self.decision_table.hit_policy == HitPolicy.UNIQUE.value:
|
||||||
|
return rules
|
||||||
|
return rules
|
||||||
|
|
||||||
|
def result(self, task):
|
||||||
|
"""Returns the results of running this decision table against
|
||||||
|
a given task."""
|
||||||
|
result = {}
|
||||||
|
matched_rules = self.decide(task)
|
||||||
|
if len(matched_rules) == 1:
|
||||||
|
result = matched_rules[0].output_as_dict(task)
|
||||||
|
elif len(matched_rules) > 1: # This must be a multi-output
|
||||||
|
# each output will be an array of values, all outputs will
|
||||||
|
# be placed in a dict, which we will then merge.
|
||||||
|
for rule in matched_rules:
|
||||||
|
rule_output = rule.output_as_dict(task)
|
||||||
|
for key in rule_output.keys():
|
||||||
|
if not key in result:
|
||||||
|
result[key] = []
|
||||||
|
result[key].append(rule_output[key])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def __check_rule(self, rule, task):
|
def __check_rule(self, rule, task):
|
||||||
for input_entry in rule.inputEntries:
|
for input_entry in rule.inputEntries:
|
||||||
|
@ -93,9 +93,10 @@ class DMNParser(NodeParser):
|
|||||||
|
|
||||||
def _parse_decision_tables(self, decision, decisionElement):
|
def _parse_decision_tables(self, decision, decisionElement):
|
||||||
for decision_table_element in decisionElement.findall('{*}decisionTable'):
|
for decision_table_element in decisionElement.findall('{*}decisionTable'):
|
||||||
|
name = decision_table_element.attrib.get('name', '')
|
||||||
|
hitPolicy = decision_table_element.attrib.get('hitPolicy', 'UNIQUE').upper()
|
||||||
decision_table = DecisionTable(decision_table_element.attrib['id'],
|
decision_table = DecisionTable(decision_table_element.attrib['id'],
|
||||||
decision_table_element.attrib.get(
|
name, hitPolicy)
|
||||||
'name', ''))
|
|
||||||
decision.decisionTables.append(decision_table)
|
decision.decisionTables.append(decision_table)
|
||||||
|
|
||||||
# parse inputs
|
# parse inputs
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from ...bpmn.serializer.bpmn_converters import BpmnTaskSpecConverter
|
from ...bpmn.serializer.bpmn_converters import BpmnTaskSpecConverter
|
||||||
|
|
||||||
from ..specs.BusinessRuleTask import BusinessRuleTask
|
from ..specs.BusinessRuleTask import BusinessRuleTask
|
||||||
from ..specs.model import DecisionTable, Rule
|
from ..specs.model import DecisionTable, Rule, HitPolicy
|
||||||
from ..specs.model import Input, InputEntry, Output, OutputEntry
|
from ..specs.model import Input, InputEntry, Output, OutputEntry
|
||||||
from ..engine.DMNEngine import DMNEngine
|
from ..engine.DMNEngine import DMNEngine
|
||||||
|
|
||||||
@ -57,7 +57,8 @@ class BusinessRuleTaskConverter(BpmnTaskSpecConverter):
|
|||||||
return self.task_spec_from_dict(dct)
|
return self.task_spec_from_dict(dct)
|
||||||
|
|
||||||
def decision_table_from_dict(self, dct):
|
def decision_table_from_dict(self, dct):
|
||||||
table = DecisionTable(dct['id'], dct['name'])
|
hit_policy = dct.get('hit_policy', HitPolicy.UNIQUE.value)
|
||||||
|
table = DecisionTable(dct['id'], dct['name'], hit_policy)
|
||||||
table.inputs = [ Input(**val) for val in dct['inputs'] ]
|
table.inputs = [ Input(**val) for val in dct['inputs'] ]
|
||||||
table.outputs = [ Output(**val) for val in dct['outputs'] ]
|
table.outputs = [ Output(**val) for val in dct['outputs'] ]
|
||||||
table.rules = [ self.rule_from_dict(rule, table.inputs, table.outputs)
|
table.rules = [ self.rule_from_dict(rule, table.inputs, table.outputs)
|
||||||
|
@ -18,7 +18,6 @@ class BusinessRuleTask(Simple, BpmnSpecMixin):
|
|||||||
super().__init__(wf_spec, name, **kwargs)
|
super().__init__(wf_spec, name, **kwargs)
|
||||||
|
|
||||||
self.dmnEngine = dmnEngine
|
self.dmnEngine = dmnEngine
|
||||||
self.res = None
|
|
||||||
self.resDict = None
|
self.resDict = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -27,10 +26,8 @@ class BusinessRuleTask(Simple, BpmnSpecMixin):
|
|||||||
|
|
||||||
def _on_complete_hook(self, my_task):
|
def _on_complete_hook(self, my_task):
|
||||||
try:
|
try:
|
||||||
self.res = self.dmnEngine.decide(my_task)
|
my_task.data = DeepMerge.merge(my_task.data,
|
||||||
if self.res is not None: # it is conceivable that no rules fire.
|
self.dmnEngine.result(my_task))
|
||||||
self.resDict = self.res.output_as_dict(my_task)
|
|
||||||
my_task.data = DeepMerge.merge(my_task.data,self.resDict)
|
|
||||||
super(BusinessRuleTask, self)._on_complete_hook(my_task)
|
super(BusinessRuleTask, self)._on_complete_hook(my_task)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise WorkflowTaskExecException(my_task, str(e))
|
raise WorkflowTaskExecException(my_task, str(e))
|
||||||
|
@ -1,8 +1,24 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from ...util.deep_merge import DeepMerge
|
from ...util.deep_merge import DeepMerge
|
||||||
|
|
||||||
|
|
||||||
|
class HitPolicy(Enum):
|
||||||
|
UNIQUE = "UNIQUE"
|
||||||
|
COLLECT = "COLLECT"
|
||||||
|
# ANY = "ANY"
|
||||||
|
# PRIORITY = "PRIORITY"
|
||||||
|
# FIRST = "FIRST"
|
||||||
|
# OUTPUT_ORDER = "OUTPUT ORDER"
|
||||||
|
# RULE_ORDER = "RULE ORDER"
|
||||||
|
|
||||||
|
# class Aggregation(Enum):
|
||||||
|
# SUM = "SUM"
|
||||||
|
# COUNT = "COUNT"
|
||||||
|
# MIN = "MIN"
|
||||||
|
# MAX = "MAX"
|
||||||
|
|
||||||
class Decision:
|
class Decision:
|
||||||
def __init__(self, id, name):
|
def __init__(self, id, name):
|
||||||
self.id = id
|
self.id = id
|
||||||
@ -11,9 +27,10 @@ class Decision:
|
|||||||
self.decisionTables = []
|
self.decisionTables = []
|
||||||
|
|
||||||
class DecisionTable:
|
class DecisionTable:
|
||||||
def __init__(self, id, name):
|
def __init__(self, id, name, hit_policy):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.hit_policy = hit_policy
|
||||||
|
|
||||||
self.inputs = []
|
self.inputs = []
|
||||||
self.outputs = []
|
self.outputs = []
|
||||||
@ -23,6 +40,7 @@ class DecisionTable:
|
|||||||
out = {}
|
out = {}
|
||||||
out['id'] = self.id
|
out['id'] = self.id
|
||||||
out['name'] = self.name
|
out['name'] = self.name
|
||||||
|
out['hit_policy'] = self.hit_policy
|
||||||
out['inputs'] = [x.serialize() for x in self.inputs]
|
out['inputs'] = [x.serialize() for x in self.inputs]
|
||||||
out['outputs'] = [x.serialize() for x in self.outputs]
|
out['outputs'] = [x.serialize() for x in self.outputs]
|
||||||
out['rules'] = [x.serialize() for x in self.rules]
|
out['rules'] = [x.serialize() for x in self.rules]
|
||||||
@ -31,6 +49,10 @@ class DecisionTable:
|
|||||||
def deserialize(self,indict):
|
def deserialize(self,indict):
|
||||||
self.id = indict['id']
|
self.id = indict['id']
|
||||||
self.name = indict['name']
|
self.name = indict['name']
|
||||||
|
if 'hit_policy' in indict:
|
||||||
|
self.hit_policy = indict['hit_policy']
|
||||||
|
else:
|
||||||
|
self.hit_policy = HitPolicy.UNIQUE.value
|
||||||
self.inputs = [Input(**x) for x in indict['inputs']]
|
self.inputs = [Input(**x) for x in indict['inputs']]
|
||||||
list(map(lambda x, y: x.deserialize(y), self.inputs, indict['inputs']))
|
list(map(lambda x, y: x.deserialize(y), self.inputs, indict['inputs']))
|
||||||
self.outputs = [Output(**x) for x in indict['outputs']]
|
self.outputs = [Output(**x) for x in indict['outputs']]
|
||||||
|
@ -41,11 +41,17 @@ class DecisionRunner:
|
|||||||
'Exactly one decision table should exist! (%s)' \
|
'Exactly one decision table should exist! (%s)' \
|
||||||
% (len(decision.decisionTables))
|
% (len(decision.decisionTables))
|
||||||
|
|
||||||
self.dmnEngine = DMNEngine(decision.decisionTables[0])
|
self.decision_table = decision.decisionTables[0]
|
||||||
|
self.dmnEngine = DMNEngine(self.decision_table)
|
||||||
|
|
||||||
def decide(self, context):
|
def decide(self, context):
|
||||||
|
"""Makes the rather ugly assumption that there is only one
|
||||||
|
rule match for a decision - which was previously the case"""
|
||||||
if not isinstance(context, dict):
|
if not isinstance(context, dict):
|
||||||
context = {'input': context}
|
context = {'input': context}
|
||||||
task = Task(self.script_engine, context)
|
task = Task(self.script_engine, context)
|
||||||
return self.dmnEngine.decide(task)
|
return self.dmnEngine.decide(task)[0]
|
||||||
|
|
||||||
|
def result(self, context):
|
||||||
|
task = Task(self.script_engine, context)
|
||||||
|
return self.dmnEngine.result(task)
|
||||||
|
37
tests/SpiffWorkflow/dmn/HitPolicyTest.py
Normal file
37
tests/SpiffWorkflow/dmn/HitPolicyTest.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from SpiffWorkflow.dmn.engine.DMNEngine import DMNEngine
|
||||||
|
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser
|
||||||
|
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
|
||||||
|
from tests.SpiffWorkflow.dmn.DecisionRunner import DecisionRunner
|
||||||
|
from tests.SpiffWorkflow.dmn.python_engine.PythonDecisionRunner import \
|
||||||
|
PythonDecisionRunner
|
||||||
|
|
||||||
|
|
||||||
|
class HitPolicyTest(BpmnWorkflowTestCase):
|
||||||
|
PARSER_CLASS = BpmnDmnParser
|
||||||
|
|
||||||
|
def testHitPolicyUnique(self):
|
||||||
|
file_name = os.path.join(os.path.dirname(__file__), 'data', 'unique_hit.dmn')
|
||||||
|
runner = PythonDecisionRunner(file_name)
|
||||||
|
decision_table = runner.decision_table
|
||||||
|
self.assertEqual('UNIQUE', decision_table.hit_policy)
|
||||||
|
res = runner.result({'name': 'Larry'})
|
||||||
|
self.assertEqual(1, res['result'])
|
||||||
|
|
||||||
|
def testHitPolicyCollect(self):
|
||||||
|
file_name = os.path.join(os.path.dirname(__file__), 'data', 'collect_hit.dmn')
|
||||||
|
runner = PythonDecisionRunner(file_name)
|
||||||
|
decision_table = runner.decision_table
|
||||||
|
self.assertEqual('COLLECT', decision_table.hit_policy)
|
||||||
|
res = runner.result({'type': 'stooge'})
|
||||||
|
self.assertEqual(4, len(res['name']))
|
||||||
|
|
||||||
|
|
||||||
|
def suite():
|
||||||
|
return unittest.TestLoader().loadTestsFromTestCase(HitPolicyTest)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.TextTestRunner(verbosity=2).run(suite())
|
78
tests/SpiffWorkflow/dmn/data/collect_hit.dmn
Normal file
78
tests/SpiffWorkflow/dmn/data/collect_hit.dmn
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:dmndi="https://www.omg.org/spec/DMN/20191111/DMNDI/" xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/" id="Definitions_06veek1" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="5.0.0">
|
||||||
|
<decision id="Decision_ExclusiveAMCheck" name="UniqueHit">
|
||||||
|
<decisionTable id="decisionTable_1" hitPolicy="COLLECT">
|
||||||
|
<input id="InputClause_1ley07z" label="Input "type"">
|
||||||
|
<inputExpression id="LiteralExpression_0yhfcz3" typeRef="string" expressionLanguage="python">
|
||||||
|
<text>type</text>
|
||||||
|
</inputExpression>
|
||||||
|
</input>
|
||||||
|
<output id="output_1" label="Output "result"" name="result" typeRef="integer" />
|
||||||
|
<output id="OutputClause_1yrcvgl" label="Output "name"" name="name" typeRef="string" />
|
||||||
|
<rule id="DecisionRule_07162mr">
|
||||||
|
<description></description>
|
||||||
|
<inputEntry id="UnaryTests_079zee3">
|
||||||
|
<text>"stooge"</text>
|
||||||
|
</inputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_16l50ps">
|
||||||
|
<text>1</text>
|
||||||
|
</outputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_1lgpxjl">
|
||||||
|
<text>"Larry"</text>
|
||||||
|
</outputEntry>
|
||||||
|
</rule>
|
||||||
|
<rule id="DecisionRule_0ifa4wu">
|
||||||
|
<description></description>
|
||||||
|
<inputEntry id="UnaryTests_1fj8ed0">
|
||||||
|
<text>"stooge"</text>
|
||||||
|
</inputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_0td8sa6">
|
||||||
|
<text>2</text>
|
||||||
|
</outputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_13y7kcx">
|
||||||
|
<text>"Mo"</text>
|
||||||
|
</outputEntry>
|
||||||
|
</rule>
|
||||||
|
<rule id="DecisionRule_0x9z2jm">
|
||||||
|
<inputEntry id="UnaryTests_1nsph49">
|
||||||
|
<text>"stooge"</text>
|
||||||
|
</inputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_04v8i8e">
|
||||||
|
<text>3</text>
|
||||||
|
</outputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_1f70eo7">
|
||||||
|
<text>"Curly"</text>
|
||||||
|
</outputEntry>
|
||||||
|
</rule>
|
||||||
|
<rule id="DecisionRule_16ur1y7">
|
||||||
|
<inputEntry id="UnaryTests_0etcimx">
|
||||||
|
<text>"stooge"</text>
|
||||||
|
</inputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_0yc7vs6">
|
||||||
|
<text>4</text>
|
||||||
|
</outputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_1k2kclw">
|
||||||
|
<text>"Shemp"</text>
|
||||||
|
</outputEntry>
|
||||||
|
</rule>
|
||||||
|
<rule id="DecisionRule_0fzx40q">
|
||||||
|
<inputEntry id="UnaryTests_1gzu54o">
|
||||||
|
<text>"farmer"</text>
|
||||||
|
</inputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_11h0epz">
|
||||||
|
<text>5</text>
|
||||||
|
</outputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_031adn2">
|
||||||
|
<text>"Elmer Fudd"</text>
|
||||||
|
</outputEntry>
|
||||||
|
</rule>
|
||||||
|
</decisionTable>
|
||||||
|
</decision>
|
||||||
|
<dmndi:DMNDI>
|
||||||
|
<dmndi:DMNDiagram id="DMNDiagram_1vu8ul5">
|
||||||
|
<dmndi:DMNShape id="DMNShape_0sl8kie" dmnElementRef="Decision_ExclusiveAMCheck">
|
||||||
|
<dc:Bounds height="80" width="180" x="150" y="150" />
|
||||||
|
</dmndi:DMNShape>
|
||||||
|
</dmndi:DMNDiagram>
|
||||||
|
</dmndi:DMNDI>
|
||||||
|
</definitions>
|
54
tests/SpiffWorkflow/dmn/data/unique_hit.dmn
Normal file
54
tests/SpiffWorkflow/dmn/data/unique_hit.dmn
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/2.0" xmlns:dmndi="https://www.omg.org/spec/DMN/20191111/DMNDI/" xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/" id="Definitions_06veek1" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="5.0.0">
|
||||||
|
<decision id="Decision_ExclusiveAMCheck" name="UniqueHit">
|
||||||
|
<decisionTable id="decisionTable_1">
|
||||||
|
<input id="input_1" label="Input "name"" biodi:width="192">
|
||||||
|
<inputExpression id="inputExpression_1" typeRef="string" expressionLanguage="python">
|
||||||
|
<text>name</text>
|
||||||
|
</inputExpression>
|
||||||
|
</input>
|
||||||
|
<output id="output_1" label="Output "result"" name="result" typeRef="integer" />
|
||||||
|
<rule id="DecisionRule_07162mr">
|
||||||
|
<description></description>
|
||||||
|
<inputEntry id="UnaryTests_1jqxc3u">
|
||||||
|
<text>"Larry"</text>
|
||||||
|
</inputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_16l50ps">
|
||||||
|
<text>1</text>
|
||||||
|
</outputEntry>
|
||||||
|
</rule>
|
||||||
|
<rule id="DecisionRule_0ifa4wu">
|
||||||
|
<description></description>
|
||||||
|
<inputEntry id="UnaryTests_0szbwxc">
|
||||||
|
<text>"Mo"</text>
|
||||||
|
</inputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_0td8sa6">
|
||||||
|
<text>2</text>
|
||||||
|
</outputEntry>
|
||||||
|
</rule>
|
||||||
|
<rule id="DecisionRule_0x9z2jm">
|
||||||
|
<inputEntry id="UnaryTests_1ul57lx">
|
||||||
|
<text>"Curly"</text>
|
||||||
|
</inputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_04v8i8e">
|
||||||
|
<text>3</text>
|
||||||
|
</outputEntry>
|
||||||
|
</rule>
|
||||||
|
<rule id="DecisionRule_16ur1y7">
|
||||||
|
<inputEntry id="UnaryTests_09q92u9">
|
||||||
|
<text>"Shemp"</text>
|
||||||
|
</inputEntry>
|
||||||
|
<outputEntry id="LiteralExpression_0yc7vs6">
|
||||||
|
<text>4</text>
|
||||||
|
</outputEntry>
|
||||||
|
</rule>
|
||||||
|
</decisionTable>
|
||||||
|
</decision>
|
||||||
|
<dmndi:DMNDI>
|
||||||
|
<dmndi:DMNDiagram id="DMNDiagram_1vu8ul5">
|
||||||
|
<dmndi:DMNShape id="DMNShape_0sl8kie" dmnElementRef="Decision_ExclusiveAMCheck">
|
||||||
|
<dc:Bounds height="80" width="180" x="150" y="150" />
|
||||||
|
</dmndi:DMNShape>
|
||||||
|
</dmndi:DMNDiagram>
|
||||||
|
</dmndi:DMNDI>
|
||||||
|
</definitions>
|
Loading…
x
Reference in New Issue
Block a user