Merge pull request #27 from sartography/improvement/better-interactive-workflow-runner
Improvement/better interactive workflow runner
This commit is contained in:
commit
fdcdf1eade
|
@ -6,3 +6,4 @@ docs/build
|
|||
docs/_build
|
||||
__pycache__
|
||||
*.log
|
||||
*.db
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
FROM python:3.10.4-slim-bullseye
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
RUN apt-get update && apt-get install -y git sqlite3
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt ./
|
||||
|
@ -6,4 +8,4 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||
|
||||
ADD . .
|
||||
|
||||
ENTRYPOINT [ "python", "./spiff-bpmn-runner.py" ]
|
||||
ENTRYPOINT [ "./runner.py -e spiff_example.spiff.file" ]
|
||||
|
|
34
README.rst
34
README.rst
|
@ -43,30 +43,34 @@ the models and application can be found there.
|
|||
Models
|
||||
^^^^^^
|
||||
|
||||
Example BPMN and DMN files can be found in the :code:`bpmn` directory of this repository.
|
||||
Example BPMN and DMN files can be found in the `bpmn` directory of this repository.
|
||||
There are several versions of a product ordering process of variying complexity located in the
|
||||
`bpmn/tutorial` directory of the repo which contain most of the elements that SpiffWorkflow supports. These
|
||||
diagrams can be viewed in any BPMN editor, but many of them have custom extensions created with
|
||||
`bpmn-js-spiffworflow <https://github.com/sartography/bpmn-js-spiffworkflow>`_.
|
||||
|
||||
Running a Workflow
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
Loading Workflows
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
To execute the complete workflow:
|
||||
To add a workflow via the command line and store serialized specs in JSON files:
|
||||
|
||||
.. code:: bash
|
||||
.. code-block:: console
|
||||
|
||||
./spiff-bpmn-runner.py -p order_product \
|
||||
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
|
||||
-b bpmn/tutorial/top_level_multi.bpmn bpmn/tutorial/call_activity_multi.bpmn
|
||||
./runner.py -e spiff_example.spiff.file add \
|
||||
-p order_product \
|
||||
-b bpmn/tutorial/{top_level,call_activity}.bpmn \
|
||||
-d bpmn/tutorial/{product_prices,shipping_costs}.dmn
|
||||
|
||||
To restore a saved workflow:
|
||||
Running Workflows
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. code:: bash
|
||||
To run the curses application using serialized JSON files:
|
||||
|
||||
./spiff-bpmn-runner.py -r <saved_workflow_file>
|
||||
.. code-block:: console
|
||||
|
||||
To see all program options:
|
||||
./runner.py -e spiff_example.spiff.file
|
||||
|
||||
.. code:: bash
|
||||
|
||||
./spiff-bpmn-runner.py --help
|
||||
Select the 'Start Workflow' screen and start the process.
|
||||
|
||||
Run in docker
|
||||
^^^^^^^^^^^^^
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
<?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="product_price" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="4.11.1">
|
||||
<decision id="product_prices" name="Product Prices">
|
||||
<decisionTable id="DecisionTable_0irsx4u">
|
||||
<input id="Input_1">
|
||||
<inputExpression id="InputExpression_1" typeRef="string" expressionLanguage="python">
|
||||
<text>product_name</text>
|
||||
</inputExpression>
|
||||
</input>
|
||||
<output id="Output_1" label="product_price" typeRef="long" />
|
||||
<rule id="DecisionRule_0yzfvwg">
|
||||
<description>Product A</description>
|
||||
<inputEntry id="UnaryTests_09p3f1m">
|
||||
<text>"product_a"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_04z7pbc">
|
||||
<text>15.00</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_06r9cs6">
|
||||
<description>Product B</description>
|
||||
<inputEntry id="UnaryTests_081dqoi">
|
||||
<text>"product_b"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_09pcew9">
|
||||
<text>15.00</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_1p0fmx5">
|
||||
<description>Product C: color</description>
|
||||
<inputEntry id="UnaryTests_0iajjkd">
|
||||
<text>"product_c"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_0xey55a">
|
||||
<text>25.00</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_1yvfyok">
|
||||
<description>Product D: color, size</description>
|
||||
<inputEntry id="UnaryTests_012k20z">
|
||||
<text>"product_d"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_0l1h1st">
|
||||
<text>20.00</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_034qyb9">
|
||||
<description>Product E: color, size, style</description>
|
||||
<inputEntry id="UnaryTests_1751ikg">
|
||||
<text>"product_e"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1yaavlq">
|
||||
<text>25.00</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_1m9eyai">
|
||||
<description>Product F: color, size, style</description>
|
||||
<inputEntry id="UnaryTests_0w5l948">
|
||||
<text>"product_f"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_0lic6qk">
|
||||
<text>30.00</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_08r1zzy">
|
||||
<description>Product G: style</description>
|
||||
<inputEntry id="UnaryTests_1wn1qis">
|
||||
<text>"product_g"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1vi74nu">
|
||||
<text>25.00</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
</decisionTable>
|
||||
</decision>
|
||||
<dmndi:DMNDI>
|
||||
<dmndi:DMNDiagram>
|
||||
<dmndi:DMNShape dmnElementRef="product_prices">
|
||||
<dc:Bounds height="80" width="180" x="160" y="100" />
|
||||
</dmndi:DMNShape>
|
||||
</dmndi:DMNDiagram>
|
||||
</dmndi:DMNDI>
|
||||
</definitions>
|
|
@ -0,0 +1,38 @@
|
|||
<?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_1f7g629" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="4.11.1">
|
||||
<decision id="shipping_costs" name="Shipping Costs">
|
||||
<decisionTable id="DecisionTable_1ywyzrl">
|
||||
<input id="Input_1">
|
||||
<inputExpression id="InputExpression_1" typeRef="string" expressionLanguage="python">
|
||||
<text>shipping_method</text>
|
||||
</inputExpression>
|
||||
</input>
|
||||
<output id="Output_1" name="shipping_cost" typeRef="long" />
|
||||
<rule id="DecisionRule_1hgbw82">
|
||||
<description>Ground</description>
|
||||
<inputEntry id="UnaryTests_0nlmjvp">
|
||||
<text>"standard"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1a2wcms">
|
||||
<text>5.00</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_03844nt">
|
||||
<description>Express</description>
|
||||
<inputEntry id="UnaryTests_03npf6a">
|
||||
<text>"overnight"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1nm5aox">
|
||||
<text>25.00</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
</decisionTable>
|
||||
</decision>
|
||||
<dmndi:DMNDI>
|
||||
<dmndi:DMNDiagram>
|
||||
<dmndi:DMNShape dmnElementRef="shipping_costs">
|
||||
<dc:Bounds height="80" width="180" x="160" y="100" />
|
||||
</dmndi:DMNShape>
|
||||
</dmndi:DMNDiagram>
|
||||
</dmndi:DMNDI>
|
||||
</definitions>
|
|
@ -1,6 +1,6 @@
|
|||
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="customize_product" isExecutable="false">
|
||||
<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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="customize_product" isExecutable="true">
|
||||
<bpmn:exclusiveGateway id="Gateway_0ocn7fn" name="Is Color Customizable?" default="Flow_1h8w6f7">
|
||||
<bpmn:incoming>Flow_104dmrv</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0ikn93z</bpmn:outgoing>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="customize_product" isExecutable="false">
|
||||
<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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="customize_product" isExecutable="true">
|
||||
<bpmn:exclusiveGateway id="Gateway_0ocn7fn" name="Is Color Customizable?" default="Flow_1h8w6f7">
|
||||
<bpmn:incoming>Flow_104dmrv</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0ikn93z</bpmn:outgoing>
|
||||
|
@ -115,6 +115,82 @@ product_price = None</spiffworkflow:preScript>
|
|||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="customize_product">
|
||||
<bpmndi:BPMNEdge id="Flow_0nzk0dv_di" bpmnElement="Flow_0nzk0dv">
|
||||
<di:waypoint x="1340" y="207" />
|
||||
<di:waypoint x="1412" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0uy2bcm_di" bpmnElement="Flow_0uy2bcm">
|
||||
<di:waypoint x="1020" y="290" />
|
||||
<di:waypoint x="1120" y="290" />
|
||||
<di:waypoint x="1120" y="247" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_043j5w0_di" bpmnElement="Flow_043j5w0">
|
||||
<di:waypoint x="870" y="232" />
|
||||
<di:waypoint x="870" y="290" />
|
||||
<di:waypoint x="920" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="876" y="258" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1gj4orb_di" bpmnElement="Flow_1gj4orb">
|
||||
<di:waypoint x="1170" y="207" />
|
||||
<di:waypoint x="1240" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1y8t5or_di" bpmnElement="Flow_1y8t5or">
|
||||
<di:waypoint x="810" y="120" />
|
||||
<di:waypoint x="870" y="120" />
|
||||
<di:waypoint x="870" y="182" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0apn5fw_di" bpmnElement="Flow_0apn5fw">
|
||||
<di:waypoint x="640" y="182" />
|
||||
<di:waypoint x="640" y="120" />
|
||||
<di:waypoint x="710" y="120" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="646" y="148" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1r5bppm_di" bpmnElement="Flow_1r5bppm">
|
||||
<di:waypoint x="895" y="207" />
|
||||
<di:waypoint x="1070" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1012" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_16qjxga_di" bpmnElement="Flow_16qjxga">
|
||||
<di:waypoint x="590" y="290" />
|
||||
<di:waypoint x="640" y="290" />
|
||||
<di:waypoint x="640" y="232" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0b4pvj2_di" bpmnElement="Flow_0b4pvj2">
|
||||
<di:waypoint x="665" y="207" />
|
||||
<di:waypoint x="845" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="748" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06gb1zr_di" bpmnElement="Flow_06gb1zr">
|
||||
<di:waypoint x="205" y="207" />
|
||||
<di:waypoint x="260" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1h8w6f7_di" bpmnElement="Flow_1h8w6f7">
|
||||
<di:waypoint x="465" y="207" />
|
||||
<di:waypoint x="615" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="532" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_104dmrv_di" bpmnElement="Flow_104dmrv">
|
||||
<di:waypoint x="360" y="207" />
|
||||
<di:waypoint x="415" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0ikn93z_di" bpmnElement="Flow_0ikn93z">
|
||||
<di:waypoint x="440" y="232" />
|
||||
<di:waypoint x="440" y="290" />
|
||||
<di:waypoint x="490" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="446" y="257" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="Gateway_0ocn7fn_di" bpmnElement="Gateway_0ocn7fn" isMarkerVisible="true">
|
||||
<dc:Bounds x="415" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
|
@ -151,94 +227,18 @@ product_price = None</spiffworkflow:preScript>
|
|||
<bpmndi:BPMNShape id="Activity_0aeqvs6_di" bpmnElement="Activity_1x0wxtq">
|
||||
<dc:Bounds x="710" y="80" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0clnuqi_di" bpmnElement="Activity_1mkqpod">
|
||||
<dc:Bounds x="920" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_10j4iim_di" bpmnElement="Event_10j4iim">
|
||||
<dc:Bounds x="1412" y="189" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1397" y="232" width="70" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0clnuqi_di" bpmnElement="Activity_1mkqpod">
|
||||
<dc:Bounds x="920" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_01p1fli_di" bpmnElement="Activity_1mvycv2">
|
||||
<dc:Bounds x="1240" y="167" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_0ikn93z_di" bpmnElement="Flow_0ikn93z">
|
||||
<di:waypoint x="440" y="232" />
|
||||
<di:waypoint x="440" y="290" />
|
||||
<di:waypoint x="490" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="446" y="257" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_104dmrv_di" bpmnElement="Flow_104dmrv">
|
||||
<di:waypoint x="360" y="207" />
|
||||
<di:waypoint x="415" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1h8w6f7_di" bpmnElement="Flow_1h8w6f7">
|
||||
<di:waypoint x="465" y="207" />
|
||||
<di:waypoint x="615" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="532" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06gb1zr_di" bpmnElement="Flow_06gb1zr">
|
||||
<di:waypoint x="205" y="207" />
|
||||
<di:waypoint x="260" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0b4pvj2_di" bpmnElement="Flow_0b4pvj2">
|
||||
<di:waypoint x="665" y="207" />
|
||||
<di:waypoint x="845" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="748" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_16qjxga_di" bpmnElement="Flow_16qjxga">
|
||||
<di:waypoint x="590" y="290" />
|
||||
<di:waypoint x="640" y="290" />
|
||||
<di:waypoint x="640" y="232" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1r5bppm_di" bpmnElement="Flow_1r5bppm">
|
||||
<di:waypoint x="895" y="207" />
|
||||
<di:waypoint x="1070" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1012" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0apn5fw_di" bpmnElement="Flow_0apn5fw">
|
||||
<di:waypoint x="640" y="182" />
|
||||
<di:waypoint x="640" y="120" />
|
||||
<di:waypoint x="710" y="120" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="646" y="148" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1y8t5or_di" bpmnElement="Flow_1y8t5or">
|
||||
<di:waypoint x="810" y="120" />
|
||||
<di:waypoint x="870" y="120" />
|
||||
<di:waypoint x="870" y="182" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1gj4orb_di" bpmnElement="Flow_1gj4orb">
|
||||
<di:waypoint x="1170" y="207" />
|
||||
<di:waypoint x="1240" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_043j5w0_di" bpmnElement="Flow_043j5w0">
|
||||
<di:waypoint x="870" y="232" />
|
||||
<di:waypoint x="870" y="290" />
|
||||
<di:waypoint x="920" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="876" y="258" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0uy2bcm_di" bpmnElement="Flow_0uy2bcm">
|
||||
<di:waypoint x="1020" y="290" />
|
||||
<di:waypoint x="1120" y="290" />
|
||||
<di:waypoint x="1120" y="247" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0nzk0dv_di" bpmnElement="Flow_0nzk0dv">
|
||||
<di:waypoint x="1340" y="207" />
|
||||
<di:waypoint x="1412" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="customize_product" isExecutable="false">
|
||||
<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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="customize_product" isExecutable="true">
|
||||
<bpmn:exclusiveGateway id="Gateway_0ocn7fn" name="Is Color Customizable?" default="Flow_1h8w6f7">
|
||||
<bpmn:incoming>Flow_06k811b</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0ikn93z</bpmn:outgoing>
|
||||
|
@ -92,134 +92,134 @@
|
|||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="customize_product">
|
||||
<bpmndi:BPMNShape id="Gateway_0ocn7fn_di" bpmnElement="Gateway_0ocn7fn" isMarkerVisible="true">
|
||||
<dc:Bounds x="415" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="404" y="152" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_0y2l88d_di" bpmnElement="Gateway_0y2l88d" isMarkerVisible="true">
|
||||
<dc:Bounds x="615" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="653" y="216" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0263vxi_di" bpmnElement="Activity_0263vxi">
|
||||
<dc:Bounds x="490" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_1iupgqu_di" bpmnElement="Gateway_1iupgqu" isMarkerVisible="true">
|
||||
<dc:Bounds x="845" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="893" y="166" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0aeqvs6_di" bpmnElement="Activity_1x0wxtq">
|
||||
<dc:Bounds x="710" y="80" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_1qku44o_di" bpmnElement="Gateway_1qku44o" isMarkerVisible="true">
|
||||
<dc:Bounds x="1045" y="182" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_10j4iim_di" bpmnElement="Event_10j4iim">
|
||||
<dc:Bounds x="1142" y="189" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1127" y="232" width="70" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_09a7t9p_di" bpmnElement="Event_09a7t9p">
|
||||
<dc:Bounds x="-8" y="189" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="-25" y="232" width="70" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1uazifo_di" bpmnElement="Activity_1uazifo">
|
||||
<dc:Bounds x="80" y="167" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1j06imw_di" bpmnElement="Activity_1ln3xkw">
|
||||
<dc:Bounds x="240" y="167" width="100" height="80" />
|
||||
<bpmndi:BPMNLabel />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0clnuqi_di" bpmnElement="Activity_1mkqpod">
|
||||
<dc:Bounds x="930" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_0ikn93z_di" bpmnElement="Flow_0ikn93z">
|
||||
<di:waypoint x="440" y="232" />
|
||||
<di:waypoint x="440" y="290" />
|
||||
<di:waypoint x="490" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="446" y="257" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
<bpmndi:BPMNEdge id="Flow_06k811b_di" bpmnElement="Flow_06k811b">
|
||||
<di:waypoint x="520" y="207" />
|
||||
<di:waypoint x="595" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_104dmrv_di" bpmnElement="Flow_104dmrv">
|
||||
<di:waypoint x="180" y="207" />
|
||||
<di:waypoint x="240" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1h8w6f7_di" bpmnElement="Flow_1h8w6f7">
|
||||
<di:waypoint x="465" y="207" />
|
||||
<di:waypoint x="615" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="532" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06gb1zr_di" bpmnElement="Flow_06gb1zr">
|
||||
<di:waypoint x="28" y="207" />
|
||||
<di:waypoint x="80" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0b4pvj2_di" bpmnElement="Flow_0b4pvj2">
|
||||
<di:waypoint x="665" y="207" />
|
||||
<di:waypoint x="845" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="748" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_16qjxga_di" bpmnElement="Flow_16qjxga">
|
||||
<di:waypoint x="590" y="290" />
|
||||
<di:waypoint x="640" y="290" />
|
||||
<di:waypoint x="640" y="232" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0apn5fw_di" bpmnElement="Flow_0apn5fw">
|
||||
<di:waypoint x="640" y="182" />
|
||||
<di:waypoint x="640" y="120" />
|
||||
<di:waypoint x="710" y="120" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="646" y="148" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1y8t5or_di" bpmnElement="Flow_1y8t5or">
|
||||
<di:waypoint x="810" y="120" />
|
||||
<di:waypoint x="870" y="120" />
|
||||
<di:waypoint x="870" y="182" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_043j5w0_di" bpmnElement="Flow_043j5w0">
|
||||
<di:waypoint x="870" y="232" />
|
||||
<di:waypoint x="870" y="290" />
|
||||
<di:waypoint x="930" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="877" y="258" width="18" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_076jkq7_di" bpmnElement="Flow_076jkq7">
|
||||
<di:waypoint x="895" y="207" />
|
||||
<di:waypoint x="1045" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1002" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
<bpmndi:BPMNEdge id="Flow_0ndmg19_di" bpmnElement="Flow_0ndmg19">
|
||||
<di:waypoint x="1275" y="207" />
|
||||
<di:waypoint x="1322" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0wedkbj_di" bpmnElement="Flow_0wedkbj">
|
||||
<di:waypoint x="1030" y="290" />
|
||||
<di:waypoint x="1070" y="290" />
|
||||
<di:waypoint x="1070" y="232" />
|
||||
<di:waypoint x="1210" y="290" />
|
||||
<di:waypoint x="1250" y="290" />
|
||||
<di:waypoint x="1250" y="232" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1036" y="272" width="18" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0ndmg19_di" bpmnElement="Flow_0ndmg19">
|
||||
<di:waypoint x="1095" y="207" />
|
||||
<di:waypoint x="1142" y="207" />
|
||||
<bpmndi:BPMNEdge id="Flow_076jkq7_di" bpmnElement="Flow_076jkq7">
|
||||
<di:waypoint x="1075" y="207" />
|
||||
<di:waypoint x="1225" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1182" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06k811b_di" bpmnElement="Flow_06k811b">
|
||||
<di:waypoint x="340" y="207" />
|
||||
<di:waypoint x="415" y="207" />
|
||||
<bpmndi:BPMNEdge id="Flow_043j5w0_di" bpmnElement="Flow_043j5w0">
|
||||
<di:waypoint x="1050" y="232" />
|
||||
<di:waypoint x="1050" y="290" />
|
||||
<di:waypoint x="1110" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1057" y="258" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1y8t5or_di" bpmnElement="Flow_1y8t5or">
|
||||
<di:waypoint x="990" y="120" />
|
||||
<di:waypoint x="1050" y="120" />
|
||||
<di:waypoint x="1050" y="182" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0apn5fw_di" bpmnElement="Flow_0apn5fw">
|
||||
<di:waypoint x="820" y="182" />
|
||||
<di:waypoint x="820" y="120" />
|
||||
<di:waypoint x="890" y="120" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="826" y="148" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_16qjxga_di" bpmnElement="Flow_16qjxga">
|
||||
<di:waypoint x="770" y="290" />
|
||||
<di:waypoint x="820" y="290" />
|
||||
<di:waypoint x="820" y="232" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0b4pvj2_di" bpmnElement="Flow_0b4pvj2">
|
||||
<di:waypoint x="845" y="207" />
|
||||
<di:waypoint x="1025" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="928" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06gb1zr_di" bpmnElement="Flow_06gb1zr">
|
||||
<di:waypoint x="208" y="207" />
|
||||
<di:waypoint x="260" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1h8w6f7_di" bpmnElement="Flow_1h8w6f7">
|
||||
<di:waypoint x="645" y="207" />
|
||||
<di:waypoint x="795" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="712" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_104dmrv_di" bpmnElement="Flow_104dmrv">
|
||||
<di:waypoint x="360" y="207" />
|
||||
<di:waypoint x="420" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0ikn93z_di" bpmnElement="Flow_0ikn93z">
|
||||
<di:waypoint x="620" y="232" />
|
||||
<di:waypoint x="620" y="290" />
|
||||
<di:waypoint x="670" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="626" y="257" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="Gateway_0ocn7fn_di" bpmnElement="Gateway_0ocn7fn" isMarkerVisible="true">
|
||||
<dc:Bounds x="595" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="584" y="152" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1uazifo_di" bpmnElement="Activity_1uazifo">
|
||||
<dc:Bounds x="260" y="167" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_09a7t9p_di" bpmnElement="Event_09a7t9p">
|
||||
<dc:Bounds x="172" y="189" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="155" y="232" width="70" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_0y2l88d_di" bpmnElement="Gateway_0y2l88d" isMarkerVisible="true">
|
||||
<dc:Bounds x="795" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="833" y="216" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0263vxi_di" bpmnElement="Activity_0263vxi">
|
||||
<dc:Bounds x="670" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_1iupgqu_di" bpmnElement="Gateway_1iupgqu" isMarkerVisible="true">
|
||||
<dc:Bounds x="1025" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1073" y="166" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0aeqvs6_di" bpmnElement="Activity_1x0wxtq">
|
||||
<dc:Bounds x="890" y="80" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_10j4iim_di" bpmnElement="Event_10j4iim">
|
||||
<dc:Bounds x="1322" y="189" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1307" y="232" width="70" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0clnuqi_di" bpmnElement="Activity_1mkqpod">
|
||||
<dc:Bounds x="1110" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_1qku44o_di" bpmnElement="Gateway_1qku44o" isMarkerVisible="true">
|
||||
<dc:Bounds x="1225" y="182" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1j06imw_di" bpmnElement="Activity_1ln3xkw">
|
||||
<dc:Bounds x="420" y="167" width="100" height="80" />
|
||||
<bpmndi:BPMNLabel />
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="customize_product" isExecutable="false">
|
||||
<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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="customize_product" isExecutable="true">
|
||||
<bpmn:exclusiveGateway id="Gateway_0ocn7fn" name="Is Color Customizable?" default="Flow_1h8w6f7">
|
||||
<bpmn:incoming>Flow_06k811b</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0ikn93z</bpmn:outgoing>
|
||||
|
@ -88,7 +88,7 @@
|
|||
<bpmn:extensionElements>
|
||||
<spiffworkflow:serviceTaskOperator id="lookup_product_info" resultVariable="product_info">
|
||||
<spiffworkflow:parameters>
|
||||
<spiffworkflow:parameter id="product_name" type="str" value="product_name"/>
|
||||
<spiffworkflow:parameter id="product_name" type="str" value="product_name" />
|
||||
</spiffworkflow:parameters>
|
||||
</spiffworkflow:serviceTaskOperator>
|
||||
<spiffworkflow:postScript>product_info = product_info_from_dict(product_info)</spiffworkflow:postScript>
|
||||
|
@ -99,134 +99,134 @@
|
|||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="customize_product">
|
||||
<bpmndi:BPMNShape id="Gateway_0ocn7fn_di" bpmnElement="Gateway_0ocn7fn" isMarkerVisible="true">
|
||||
<dc:Bounds x="415" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="404" y="152" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_0y2l88d_di" bpmnElement="Gateway_0y2l88d" isMarkerVisible="true">
|
||||
<dc:Bounds x="615" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="653" y="216" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0263vxi_di" bpmnElement="Activity_0263vxi">
|
||||
<dc:Bounds x="490" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_1iupgqu_di" bpmnElement="Gateway_1iupgqu" isMarkerVisible="true">
|
||||
<dc:Bounds x="845" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="893" y="166" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0aeqvs6_di" bpmnElement="Activity_1x0wxtq">
|
||||
<dc:Bounds x="710" y="80" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_1qku44o_di" bpmnElement="Gateway_1qku44o" isMarkerVisible="true">
|
||||
<dc:Bounds x="1045" y="182" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_10j4iim_di" bpmnElement="Event_10j4iim">
|
||||
<dc:Bounds x="1142" y="189" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1127" y="232" width="70" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_09a7t9p_di" bpmnElement="Event_09a7t9p">
|
||||
<dc:Bounds x="-8" y="189" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="-25" y="232" width="70" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1uazifo_di" bpmnElement="Activity_1uazifo">
|
||||
<dc:Bounds x="80" y="167" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1j06imw_di" bpmnElement="Activity_1ln3xkw">
|
||||
<dc:Bounds x="240" y="167" width="100" height="80" />
|
||||
<bpmndi:BPMNLabel />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0clnuqi_di" bpmnElement="Activity_1mkqpod">
|
||||
<dc:Bounds x="930" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_0ikn93z_di" bpmnElement="Flow_0ikn93z">
|
||||
<di:waypoint x="440" y="232" />
|
||||
<di:waypoint x="440" y="290" />
|
||||
<di:waypoint x="490" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="446" y="257" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
<bpmndi:BPMNEdge id="Flow_06k811b_di" bpmnElement="Flow_06k811b">
|
||||
<di:waypoint x="520" y="207" />
|
||||
<di:waypoint x="595" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_104dmrv_di" bpmnElement="Flow_104dmrv">
|
||||
<di:waypoint x="180" y="207" />
|
||||
<di:waypoint x="240" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1h8w6f7_di" bpmnElement="Flow_1h8w6f7">
|
||||
<di:waypoint x="465" y="207" />
|
||||
<di:waypoint x="615" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="532" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06gb1zr_di" bpmnElement="Flow_06gb1zr">
|
||||
<di:waypoint x="28" y="207" />
|
||||
<di:waypoint x="80" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0b4pvj2_di" bpmnElement="Flow_0b4pvj2">
|
||||
<di:waypoint x="665" y="207" />
|
||||
<di:waypoint x="845" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="748" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_16qjxga_di" bpmnElement="Flow_16qjxga">
|
||||
<di:waypoint x="590" y="290" />
|
||||
<di:waypoint x="640" y="290" />
|
||||
<di:waypoint x="640" y="232" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0apn5fw_di" bpmnElement="Flow_0apn5fw">
|
||||
<di:waypoint x="640" y="182" />
|
||||
<di:waypoint x="640" y="120" />
|
||||
<di:waypoint x="710" y="120" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="646" y="148" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1y8t5or_di" bpmnElement="Flow_1y8t5or">
|
||||
<di:waypoint x="810" y="120" />
|
||||
<di:waypoint x="870" y="120" />
|
||||
<di:waypoint x="870" y="182" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_043j5w0_di" bpmnElement="Flow_043j5w0">
|
||||
<di:waypoint x="870" y="232" />
|
||||
<di:waypoint x="870" y="290" />
|
||||
<di:waypoint x="930" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="877" y="258" width="18" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_076jkq7_di" bpmnElement="Flow_076jkq7">
|
||||
<di:waypoint x="895" y="207" />
|
||||
<di:waypoint x="1045" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1002" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
<bpmndi:BPMNEdge id="Flow_0ndmg19_di" bpmnElement="Flow_0ndmg19">
|
||||
<di:waypoint x="1275" y="207" />
|
||||
<di:waypoint x="1322" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0wedkbj_di" bpmnElement="Flow_0wedkbj">
|
||||
<di:waypoint x="1030" y="290" />
|
||||
<di:waypoint x="1070" y="290" />
|
||||
<di:waypoint x="1070" y="232" />
|
||||
<di:waypoint x="1210" y="290" />
|
||||
<di:waypoint x="1250" y="290" />
|
||||
<di:waypoint x="1250" y="232" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1036" y="272" width="18" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0ndmg19_di" bpmnElement="Flow_0ndmg19">
|
||||
<di:waypoint x="1095" y="207" />
|
||||
<di:waypoint x="1142" y="207" />
|
||||
<bpmndi:BPMNEdge id="Flow_076jkq7_di" bpmnElement="Flow_076jkq7">
|
||||
<di:waypoint x="1075" y="207" />
|
||||
<di:waypoint x="1225" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1182" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06k811b_di" bpmnElement="Flow_06k811b">
|
||||
<di:waypoint x="340" y="207" />
|
||||
<di:waypoint x="415" y="207" />
|
||||
<bpmndi:BPMNEdge id="Flow_043j5w0_di" bpmnElement="Flow_043j5w0">
|
||||
<di:waypoint x="1050" y="232" />
|
||||
<di:waypoint x="1050" y="290" />
|
||||
<di:waypoint x="1110" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1057" y="258" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1y8t5or_di" bpmnElement="Flow_1y8t5or">
|
||||
<di:waypoint x="990" y="120" />
|
||||
<di:waypoint x="1050" y="120" />
|
||||
<di:waypoint x="1050" y="182" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0apn5fw_di" bpmnElement="Flow_0apn5fw">
|
||||
<di:waypoint x="820" y="182" />
|
||||
<di:waypoint x="820" y="120" />
|
||||
<di:waypoint x="890" y="120" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="826" y="148" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_16qjxga_di" bpmnElement="Flow_16qjxga">
|
||||
<di:waypoint x="770" y="290" />
|
||||
<di:waypoint x="820" y="290" />
|
||||
<di:waypoint x="820" y="232" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0b4pvj2_di" bpmnElement="Flow_0b4pvj2">
|
||||
<di:waypoint x="845" y="207" />
|
||||
<di:waypoint x="1025" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="928" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06gb1zr_di" bpmnElement="Flow_06gb1zr">
|
||||
<di:waypoint x="208" y="207" />
|
||||
<di:waypoint x="260" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1h8w6f7_di" bpmnElement="Flow_1h8w6f7">
|
||||
<di:waypoint x="645" y="207" />
|
||||
<di:waypoint x="795" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="712" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_104dmrv_di" bpmnElement="Flow_104dmrv">
|
||||
<di:waypoint x="360" y="207" />
|
||||
<di:waypoint x="420" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0ikn93z_di" bpmnElement="Flow_0ikn93z">
|
||||
<di:waypoint x="620" y="232" />
|
||||
<di:waypoint x="620" y="290" />
|
||||
<di:waypoint x="670" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="626" y="257" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="Gateway_0ocn7fn_di" bpmnElement="Gateway_0ocn7fn" isMarkerVisible="true">
|
||||
<dc:Bounds x="595" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="584" y="152" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1uazifo_di" bpmnElement="Activity_1uazifo">
|
||||
<dc:Bounds x="260" y="167" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_09a7t9p_di" bpmnElement="Event_09a7t9p">
|
||||
<dc:Bounds x="172" y="189" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="155" y="232" width="70" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_0y2l88d_di" bpmnElement="Gateway_0y2l88d" isMarkerVisible="true">
|
||||
<dc:Bounds x="795" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="833" y="216" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0263vxi_di" bpmnElement="Activity_0263vxi">
|
||||
<dc:Bounds x="670" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_1iupgqu_di" bpmnElement="Gateway_1iupgqu" isMarkerVisible="true">
|
||||
<dc:Bounds x="1025" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1073" y="166" width="73" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0aeqvs6_di" bpmnElement="Activity_1x0wxtq">
|
||||
<dc:Bounds x="890" y="80" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_10j4iim_di" bpmnElement="Event_10j4iim">
|
||||
<dc:Bounds x="1322" y="189" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1307" y="232" width="70" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0clnuqi_di" bpmnElement="Activity_1mkqpod">
|
||||
<dc:Bounds x="1110" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_1qku44o_di" bpmnElement="Gateway_1qku44o" isMarkerVisible="true">
|
||||
<dc:Bounds x="1225" y="182" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1j06imw_di" bpmnElement="Activity_1ln3xkw">
|
||||
<dc:Bounds x="420" y="167" width="100" height="80" />
|
||||
<bpmndi:BPMNLabel />
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="end_it_all" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_0m91lb4</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0m91lb4" sourceRef="StartEvent_1" targetRef="collect_info" />
|
||||
<bpmn:scriptTask id="collect_info" name="Collect Info">
|
||||
<bpmn:incoming>Flow_0m91lb4</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_14oh26j</bpmn:outgoing>
|
||||
<bpmn:script>from os import getpid
|
||||
pid = getpid()</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
<bpmn:sequenceFlow id="Flow_14oh26j" sourceRef="collect_info" targetRef="confirm" />
|
||||
<bpmn:userTask id="confirm" name="Confirm">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:properties>
|
||||
<spiffworkflow:property name="formJsonSchemaFilename" value="dangerous.json" />
|
||||
</spiffworkflow:properties>
|
||||
<spiffworkflow:instructionsForEndUser>Process ID: {{ pid }}</spiffworkflow:instructionsForEndUser>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_14oh26j</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_030s9hw</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:exclusiveGateway id="Gateway_0dq17uq" default="Flow_1mp6zm4">
|
||||
<bpmn:incoming>Flow_030s9hw</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1mp6zm4</bpmn:outgoing>
|
||||
<bpmn:outgoing>Flow_0jsq853</bpmn:outgoing>
|
||||
</bpmn:exclusiveGateway>
|
||||
<bpmn:sequenceFlow id="Flow_030s9hw" sourceRef="confirm" targetRef="Gateway_0dq17uq" />
|
||||
<bpmn:endEvent id="Event_1gxr06r">
|
||||
<bpmn:incoming>Flow_1mp6zm4</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1mp6zm4" sourceRef="Gateway_0dq17uq" targetRef="Event_1gxr06r" />
|
||||
<bpmn:sequenceFlow id="Flow_0jsq853" sourceRef="Gateway_0dq17uq" targetRef="end_it">
|
||||
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">kill_it == 'Y'</bpmn:conditionExpression>
|
||||
</bpmn:sequenceFlow>
|
||||
<bpmn:endEvent id="Event_1f51fmu">
|
||||
<bpmn:incoming>Flow_0uef7p4</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0uef7p4" sourceRef="end_it" targetRef="Event_1f51fmu" />
|
||||
<bpmn:scriptTask id="end_it" name="End It">
|
||||
<bpmn:incoming>Flow_0jsq853</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0uef7p4</bpmn:outgoing>
|
||||
<bpmn:script>from os import kill
|
||||
from signal import SIGKILL
|
||||
kill(pid, SIGKILL)</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="end_it_all">
|
||||
<bpmndi:BPMNEdge id="Flow_14oh26j_di" bpmnElement="Flow_14oh26j">
|
||||
<di:waypoint x="370" y="117" />
|
||||
<di:waypoint x="420" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0m91lb4_di" bpmnElement="Flow_0m91lb4">
|
||||
<di:waypoint x="215" y="117" />
|
||||
<di:waypoint x="270" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_030s9hw_di" bpmnElement="Flow_030s9hw">
|
||||
<di:waypoint x="520" y="117" />
|
||||
<di:waypoint x="575" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1mp6zm4_di" bpmnElement="Flow_1mp6zm4">
|
||||
<di:waypoint x="625" y="117" />
|
||||
<di:waypoint x="862" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0jsq853_di" bpmnElement="Flow_0jsq853">
|
||||
<di:waypoint x="600" y="142" />
|
||||
<di:waypoint x="600" y="230" />
|
||||
<di:waypoint x="690" y="230" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0uef7p4_di" bpmnElement="Flow_0uef7p4">
|
||||
<di:waypoint x="790" y="230" />
|
||||
<di:waypoint x="862" y="230" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_19ubvxe_di" bpmnElement="collect_info">
|
||||
<dc:Bounds x="270" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0epjwdj_di" bpmnElement="confirm">
|
||||
<dc:Bounds x="420" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_0dq17uq_di" bpmnElement="Gateway_0dq17uq" isMarkerVisible="true">
|
||||
<dc:Bounds x="575" y="92" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_1f51fmu_di" bpmnElement="Event_1f51fmu">
|
||||
<dc:Bounds x="862" y="212" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_1gxr06r_di" bpmnElement="Event_1gxr06r">
|
||||
<dc:Bounds x="862" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0fakvwk_di" bpmnElement="end_it">
|
||||
<dc:Bounds x="690" y="190" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -1,6 +1,6 @@
|
|||
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="customize_product" isExecutable="false">
|
||||
<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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ibnyhd" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
|
||||
<bpmn:process id="customize_product" isExecutable="true">
|
||||
<bpmn:ioSpecification>
|
||||
<bpmn:dataOutput id="product_name" name="Product Name" />
|
||||
<bpmn:dataOutput id="product_quantity" name="Product Quantity" />
|
||||
|
@ -98,18 +98,78 @@
|
|||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="customize_product">
|
||||
<bpmndi:BPMNShape id="DataOutput-791976010-1DI" bpmnElement="product_quantity">
|
||||
<dc:Bounds x="1212" y="45" width="36" height="50" />
|
||||
<bpmndi:BPMNEdge id="Flow_0uy2bcm_di" bpmnElement="Flow_0uy2bcm">
|
||||
<di:waypoint x="1020" y="290" />
|
||||
<di:waypoint x="1120" y="290" />
|
||||
<di:waypoint x="1120" y="247" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_043j5w0_di" bpmnElement="Flow_043j5w0">
|
||||
<di:waypoint x="870" y="232" />
|
||||
<di:waypoint x="870" y="290" />
|
||||
<di:waypoint x="920" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1189" y="102" width="82" height="14" />
|
||||
<dc:Bounds x="876" y="258" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="DataOutput-278882889-1DI" bpmnElement="product_name">
|
||||
<dc:Bounds x="1102" y="45" width="36" height="50" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1gj4orb_di" bpmnElement="Flow_1gj4orb">
|
||||
<di:waypoint x="1170" y="207" />
|
||||
<di:waypoint x="1232" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1y8t5or_di" bpmnElement="Flow_1y8t5or">
|
||||
<di:waypoint x="810" y="120" />
|
||||
<di:waypoint x="870" y="120" />
|
||||
<di:waypoint x="870" y="182" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0apn5fw_di" bpmnElement="Flow_0apn5fw">
|
||||
<di:waypoint x="640" y="182" />
|
||||
<di:waypoint x="640" y="120" />
|
||||
<di:waypoint x="710" y="120" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1085" y="102" width="71" height="14" />
|
||||
<dc:Bounds x="646" y="148" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1r5bppm_di" bpmnElement="Flow_1r5bppm">
|
||||
<di:waypoint x="895" y="207" />
|
||||
<di:waypoint x="1070" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1012" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_16qjxga_di" bpmnElement="Flow_16qjxga">
|
||||
<di:waypoint x="590" y="290" />
|
||||
<di:waypoint x="640" y="290" />
|
||||
<di:waypoint x="640" y="232" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0b4pvj2_di" bpmnElement="Flow_0b4pvj2">
|
||||
<di:waypoint x="665" y="207" />
|
||||
<di:waypoint x="845" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="748" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06gb1zr_di" bpmnElement="Flow_06gb1zr">
|
||||
<di:waypoint x="205" y="207" />
|
||||
<di:waypoint x="260" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1h8w6f7_di" bpmnElement="Flow_1h8w6f7">
|
||||
<di:waypoint x="465" y="207" />
|
||||
<di:waypoint x="615" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="532" y="183" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_104dmrv_di" bpmnElement="Flow_104dmrv">
|
||||
<di:waypoint x="360" y="207" />
|
||||
<di:waypoint x="415" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0ikn93z_di" bpmnElement="Flow_0ikn93z">
|
||||
<di:waypoint x="440" y="232" />
|
||||
<di:waypoint x="440" y="290" />
|
||||
<di:waypoint x="490" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="446" y="257" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="Gateway_0ocn7fn_di" bpmnElement="Gateway_0ocn7fn" isMarkerVisible="true">
|
||||
<dc:Bounds x="415" y="182" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
|
@ -155,78 +215,18 @@
|
|||
<bpmndi:BPMNShape id="Activity_0clnuqi_di" bpmnElement="Activity_1mkqpod">
|
||||
<dc:Bounds x="920" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_0ikn93z_di" bpmnElement="Flow_0ikn93z">
|
||||
<di:waypoint x="440" y="232" />
|
||||
<di:waypoint x="440" y="290" />
|
||||
<di:waypoint x="490" y="290" />
|
||||
<bpmndi:BPMNShape id="DataOutput-278882889-1DI" bpmnElement="product_name">
|
||||
<dc:Bounds x="1102" y="45" width="36" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="446" y="257" width="19" height="14" />
|
||||
<dc:Bounds x="1085" y="102" width="71" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_104dmrv_di" bpmnElement="Flow_104dmrv">
|
||||
<di:waypoint x="360" y="207" />
|
||||
<di:waypoint x="415" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1h8w6f7_di" bpmnElement="Flow_1h8w6f7">
|
||||
<di:waypoint x="465" y="207" />
|
||||
<di:waypoint x="615" y="207" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="DataOutput-791976010-1DI" bpmnElement="product_quantity">
|
||||
<dc:Bounds x="1212" y="45" width="36" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="532" y="183" width="15" height="14" />
|
||||
<dc:Bounds x="1189" y="102" width="82" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06gb1zr_di" bpmnElement="Flow_06gb1zr">
|
||||
<di:waypoint x="205" y="207" />
|
||||
<di:waypoint x="260" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0b4pvj2_di" bpmnElement="Flow_0b4pvj2">
|
||||
<di:waypoint x="665" y="207" />
|
||||
<di:waypoint x="845" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="748" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_16qjxga_di" bpmnElement="Flow_16qjxga">
|
||||
<di:waypoint x="590" y="290" />
|
||||
<di:waypoint x="640" y="290" />
|
||||
<di:waypoint x="640" y="232" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1r5bppm_di" bpmnElement="Flow_1r5bppm">
|
||||
<di:waypoint x="895" y="207" />
|
||||
<di:waypoint x="1070" y="207" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1012" y="189" width="15" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0apn5fw_di" bpmnElement="Flow_0apn5fw">
|
||||
<di:waypoint x="640" y="182" />
|
||||
<di:waypoint x="640" y="120" />
|
||||
<di:waypoint x="710" y="120" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="646" y="148" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1y8t5or_di" bpmnElement="Flow_1y8t5or">
|
||||
<di:waypoint x="810" y="120" />
|
||||
<di:waypoint x="870" y="120" />
|
||||
<di:waypoint x="870" y="182" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1gj4orb_di" bpmnElement="Flow_1gj4orb">
|
||||
<di:waypoint x="1170" y="207" />
|
||||
<di:waypoint x="1232" y="207" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_043j5w0_di" bpmnElement="Flow_043j5w0">
|
||||
<di:waypoint x="870" y="232" />
|
||||
<di:waypoint x="870" y="290" />
|
||||
<di:waypoint x="920" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="876" y="258" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0uy2bcm_di" bpmnElement="Flow_0uy2bcm">
|
||||
<di:waypoint x="1020" y="290" />
|
||||
<di:waypoint x="1120" y="290" />
|
||||
<di:waypoint x="1120" y="247" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "Kill This Process?",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kill_it"
|
||||
],
|
||||
"properties": {
|
||||
"kill_it": {
|
||||
"title": "Kill it?",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import sys, traceback
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer
|
||||
from SpiffWorkflow.bpmn.specs.defaults import ManualTask, NoneTask
|
||||
from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser
|
||||
from SpiffWorkflow.camunda.specs.user_task import UserTask, EnumFormField
|
||||
from SpiffWorkflow.camunda.serializer.config import CAMUNDA_SPEC_CONFIG
|
||||
|
||||
from SpiffWorkflow.util.deep_merge import DeepMerge
|
||||
|
||||
from runner.runner import SimpleBpmnRunner
|
||||
from runner.shared import create_arg_parser
|
||||
from runner.product_info import registry
|
||||
from runner.script_engine import custom_script_engine
|
||||
|
||||
parser = CamundaParser()
|
||||
|
||||
wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter(CAMUNDA_SPEC_CONFIG)
|
||||
serializer = BpmnWorkflowSerializer(wf_spec_converter, registry)
|
||||
|
||||
def update_data(dct, name, value):
|
||||
path = name.split('.')
|
||||
current = dct
|
||||
for component in path[:-1]:
|
||||
if component not in current:
|
||||
current[component] = {}
|
||||
current = current[component]
|
||||
current[path[-1]] = value
|
||||
|
||||
def display_task(task):
|
||||
print(f'\n{task.task_spec.bpmn_name}')
|
||||
if task.task_spec.documentation is not None:
|
||||
template = Template(task.task_spec.documentation)
|
||||
print(template.render(task.data))
|
||||
|
||||
def complete_user_task(task):
|
||||
display_task(task)
|
||||
dct = {}
|
||||
for field in task.task_spec.form.fields:
|
||||
if isinstance(field, EnumFormField):
|
||||
option_map = dict([ (opt.name, opt.id) for opt in field.options ])
|
||||
options = "(" + ', '.join(option_map) + ")"
|
||||
prompt = f"{field.label} {options} "
|
||||
option = input(prompt)
|
||||
while option not in option_map:
|
||||
print(f'Invalid selection!')
|
||||
option = input(prompt)
|
||||
response = option_map[option]
|
||||
else:
|
||||
response = input(f"{field.label} ")
|
||||
if field.type == "long":
|
||||
response = int(response)
|
||||
update_data(dct, field.id, response)
|
||||
DeepMerge.merge(task.data, dct)
|
||||
|
||||
def complete_manual_task(task):
|
||||
display_task(task)
|
||||
input("Press any key to complete task.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
arg_parser = create_arg_parser()
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
handlers = {
|
||||
UserTask: complete_user_task,
|
||||
ManualTask: complete_manual_task,
|
||||
NoneTask: complete_manual_task,
|
||||
}
|
||||
|
||||
try:
|
||||
runner = SimpleBpmnRunner(parser, serializer, script_engine=custom_script_engine, handlers=handlers)
|
||||
if args.restore is not None:
|
||||
runner.restore(args.restore)
|
||||
else:
|
||||
runner.parse(args.process or args.collaboration, args.bpmn, args.dmn, args.collaboration is not None)
|
||||
runner.run_workflow(args.step)
|
||||
except Exception as exc:
|
||||
sys.stderr.write(traceback.format_exc())
|
||||
sys.exit(1)
|
|
@ -1,3 +1,3 @@
|
|||
SpiffWorkflow==2.0
|
||||
SpiffWorkflow @ git+https://github.com/sartography/SpiffWorkflow@main
|
||||
Jinja2==3.1.2
|
||||
RestrictedPython==6.0
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import curses
|
||||
import importlib
|
||||
import sys, traceback
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from spiff_example.curses_ui import CursesUI
|
||||
from spiff_example.cli import add_subparsers, configure_logging
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = ArgumentParser('Simple BPMN App')
|
||||
parser.add_argument('-e', '--engine', dest='engine', required=True, metavar='MODULE', help='load engine from %(metavar)s')
|
||||
subparsers = parser.add_subparsers(dest='subcommand')
|
||||
add_subparsers(subparsers)
|
||||
|
||||
args = parser.parse_args()
|
||||
config = importlib.import_module(args.engine)
|
||||
|
||||
try:
|
||||
if args.subcommand is None:
|
||||
curses.wrapper(CursesUI, config.engine)
|
||||
else:
|
||||
configure_logging()
|
||||
args.func(config.engine, args)
|
||||
except Exception as exc:
|
||||
sys.stderr.write(traceback.format_exc())
|
||||
sys.exit(1)
|
150
runner/runner.py
150
runner/runner.py
|
@ -1,150 +0,0 @@
|
|||
import json
|
||||
|
||||
from SpiffWorkflow.task import TaskState
|
||||
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
|
||||
|
||||
|
||||
class SimpleBpmnRunner:
|
||||
|
||||
def __init__(self, parser, serializer, script_engine=None, handlers=None):
|
||||
|
||||
self.parser = parser
|
||||
self.serializer = serializer
|
||||
self.script_engine = script_engine
|
||||
self.handlers = handlers or {}
|
||||
self.workflow = None
|
||||
|
||||
def parse(self, name, bpmn_files, dmn_files=None, collaboration=False):
|
||||
|
||||
self.parser.add_bpmn_files(bpmn_files)
|
||||
if dmn_files:
|
||||
self.parser.add_dmn_files(dmn_files)
|
||||
|
||||
if collaboration:
|
||||
top_level, subprocesses = self.parser.get_collaboration(name)
|
||||
else:
|
||||
top_level = self.parser.get_spec(name)
|
||||
subprocesses = self.parser.get_subprocess_specs(name)
|
||||
self.workflow = BpmnWorkflow(top_level, subprocesses, script_engine=self.script_engine)
|
||||
|
||||
def restore(self, filename):
|
||||
with open(filename) as fh:
|
||||
self.workflow = self.serializer.deserialize_json(fh.read())
|
||||
if self.script_engine is not None:
|
||||
self.workflow.script_engine = self.script_engine
|
||||
|
||||
def dump(self):
|
||||
filename = input('Enter filename: ')
|
||||
with open(filename, 'w') as fh:
|
||||
dct = self.serializer.workflow_to_dict(self.workflow)
|
||||
dct[self.serializer.VERSION_KEY] = self.serializer.VERSION
|
||||
fh.write(json.dumps(dct, indent=2, separators=[', ', ': ']))
|
||||
|
||||
def get_task_description(self, task, include_state=True):
|
||||
|
||||
task_spec = task.task_spec
|
||||
lane = f'{task_spec.lane}' if task_spec.lane is not None else '-'
|
||||
name = task_spec.bpmn_name if task_spec.bpmn_name is not None else '-'
|
||||
description = task_spec.description if task_spec.description is not None else 'Task'
|
||||
state = f'{task.get_state_name()}' if include_state else ''
|
||||
return f'[{lane}] {name} ({description}: {task_spec.bpmn_id}) {state}'
|
||||
|
||||
def get_task_details(self, task):
|
||||
print(self.get_task_description(task))
|
||||
print(json.dumps(self.serializer.data_converter.convert(task.data), indent=2, separators=[', ', ': ']))
|
||||
|
||||
def list_tasks(self, tasks, heading, include_state=True):
|
||||
print(f'\n{heading}\n')
|
||||
for task in tasks:
|
||||
print(f'{self.get_task_description(task, include_state)}')
|
||||
|
||||
def show_prompt(self, prompt, options):
|
||||
|
||||
print()
|
||||
for action, description in options.items():
|
||||
print(f'\t<{action}> {description}')
|
||||
action = input(prompt)
|
||||
while action not in options:
|
||||
print("Invalid selection")
|
||||
action = input(prompt)
|
||||
return action
|
||||
|
||||
def show_workflow_options(self, ready_tasks):
|
||||
|
||||
options = {}
|
||||
if len(ready_tasks):
|
||||
options['r'] = 'Run a task'
|
||||
options['p'] = 'List past tasks'
|
||||
options['f'] = 'List future tasks'
|
||||
options['a'] = 'List all tasks'
|
||||
options['d'] = 'Dump workflow state'
|
||||
options['w'] = 'Wait'
|
||||
return self.show_prompt('\nSelect action: ', options)
|
||||
|
||||
def select_task(self, tasks, heading=None):
|
||||
if heading is not None:
|
||||
print(f'\n{heading}')
|
||||
options = {}
|
||||
for idx, task in enumerate(tasks):
|
||||
options[str(idx + 1)] = self.get_task_description(task, False)
|
||||
value = self.show_prompt('\nSelect task: ', options)
|
||||
return tasks[int(value) - 1]
|
||||
|
||||
def advance(self):
|
||||
engine_tasks = [t for t in self.workflow.get_tasks(TaskState.READY) if not t.task_spec.manual]
|
||||
while len(engine_tasks) > 0:
|
||||
for task in engine_tasks:
|
||||
task.run()
|
||||
self.workflow.refresh_waiting_tasks()
|
||||
engine_tasks = [t for t in self.workflow.get_tasks(TaskState.READY) if not t.task_spec.manual]
|
||||
|
||||
def run_workflow(self, step=False):
|
||||
|
||||
while not self.workflow.is_completed():
|
||||
|
||||
if not step:
|
||||
self.advance()
|
||||
|
||||
tasks = self.workflow.get_tasks(TaskState.READY|TaskState.WAITING)
|
||||
runnable = [t for t in tasks if t.state == TaskState.READY]
|
||||
human_tasks = [t for t in runnable if t.task_spec.manual]
|
||||
current_tasks = human_tasks if not step else runnable
|
||||
|
||||
self.list_tasks(tasks, 'Ready and Waiting Tasks')
|
||||
if len(current_tasks) > 0:
|
||||
action = self.show_workflow_options(current_tasks)
|
||||
else:
|
||||
action = None
|
||||
if len(tasks) > 0:
|
||||
input("\nPress any key to update task list")
|
||||
|
||||
if action == 'r':
|
||||
task = self.select_task(current_tasks)
|
||||
handler = self.handlers.get(type(task.task_spec))
|
||||
if handler is not None:
|
||||
handler(task)
|
||||
task.run()
|
||||
elif action == 'p':
|
||||
finished = [t for t in self.workflow.get_tasks(TaskState.FINISHED_MASK) if t.task_spec.bpmn_id is not None]
|
||||
task = self.select_task(finished, 'View Task Details')
|
||||
self.get_task_details(task)
|
||||
elif action == 'a':
|
||||
self.list_tasks([t for t in self.workflow.get_tasks() if t.task_spec.bpmn_id is not None], 'All Tasks')
|
||||
elif action == 'f':
|
||||
self.list_tasks([t for t in self.workflow.get_tasks(TaskState.FUTURE) if t.task_spec.bpmn_id is not None], 'Future Tasks')
|
||||
elif action == 'd':
|
||||
self.dump()
|
||||
|
||||
self.workflow.refresh_waiting_tasks()
|
||||
|
||||
while action != 'q':
|
||||
action = self.show_prompt('\nSelect action: ', {
|
||||
'a': 'List all tasks',
|
||||
'v': 'View workflow data',
|
||||
'q': 'Quit',
|
||||
})
|
||||
if action == 'a':
|
||||
self.list_tasks([t for t in self.workflow.get_tasks() if t.task_spec.bpmn_id is not None], "All Tasks")
|
||||
elif action == 'v':
|
||||
dct = self.serializer.data_converter.convert(self.workflow.data)
|
||||
print('\n' + json.dumps(dct, indent=2, separators=[', ', ': ']))
|
|
@ -1,83 +0,0 @@
|
|||
import subprocess, os, json
|
||||
|
||||
import datetime
|
||||
|
||||
from RestrictedPython import safe_globals
|
||||
|
||||
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
|
||||
from SpiffWorkflow.bpmn.PythonScriptEngineEnvironment import BasePythonScriptEngineEnvironment, TaskDataEnvironment
|
||||
from SpiffWorkflow.util.deep_merge import DeepMerge
|
||||
|
||||
from runner.product_info import (
|
||||
lookup_product_info,
|
||||
lookup_shipping_cost,
|
||||
loads,
|
||||
dumps,
|
||||
product_info_to_dict,
|
||||
product_info_from_dict
|
||||
)
|
||||
|
||||
env_globals = {
|
||||
'lookup_product_info': lookup_product_info,
|
||||
'lookup_shipping_cost': lookup_shipping_cost,
|
||||
'datetime': datetime,
|
||||
}
|
||||
custom_env = TaskDataEnvironment(env_globals)
|
||||
custom_script_engine = PythonScriptEngine(environment=custom_env)
|
||||
|
||||
|
||||
restricted_env = TaskDataEnvironment(safe_globals)
|
||||
restricted_script_engine = PythonScriptEngine(environment=restricted_env)
|
||||
|
||||
|
||||
class SubprocessScriptingEnvironment(BasePythonScriptEngineEnvironment):
|
||||
|
||||
def __init__(self, executable, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.executable = executable
|
||||
|
||||
def evaluate(self, expression, context, external_methods=None):
|
||||
output = self.run(['eval', '-e', expression], context, external_methods)
|
||||
return self.parse_output(output)
|
||||
|
||||
def execute(self, script, context, external_methods=None):
|
||||
output = self.run(['exec', '-s', script], context, external_methods)
|
||||
DeepMerge.merge(context, self.parse_output(output))
|
||||
return True
|
||||
|
||||
def run(self, args, context, external_methods):
|
||||
cmd = [self.executable] + args + ['-l', dumps(context)]
|
||||
if external_methods is not None:
|
||||
cmd.extend(['-g', dumps(external_methods)])
|
||||
return subprocess.run(cmd, capture_output=True)
|
||||
|
||||
def parse_output(self, output):
|
||||
if output.stderr:
|
||||
raise Exception(output.stderr)
|
||||
return loads(output.stdout)
|
||||
|
||||
executable = os.path.join(os.path.dirname(__file__), 'subprocess.py')
|
||||
subprocess_script_engine = PythonScriptEngine(environment=SubprocessScriptingEnvironment(executable))
|
||||
|
||||
|
||||
service_task_env = TaskDataEnvironment({
|
||||
'product_info_from_dict': product_info_from_dict,
|
||||
'datetime': datetime,
|
||||
})
|
||||
|
||||
class ServiceTaskEngine(PythonScriptEngine):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(environment=service_task_env)
|
||||
|
||||
def call_service(self, operation_name, operation_params, task_data):
|
||||
if operation_name == 'lookup_product_info':
|
||||
product_info = lookup_product_info(operation_params['product_name']['value'])
|
||||
result = product_info_to_dict(product_info)
|
||||
elif operation_name == 'lookup_shipping_cost':
|
||||
result = lookup_shipping_cost(operation_params['shipping_method']['value'])
|
||||
else:
|
||||
raise Exception("Unknown Service!")
|
||||
return json.dumps(result)
|
||||
|
||||
service_task_engine = ServiceTaskEngine()
|
|
@ -1,46 +0,0 @@
|
|||
import argparse
|
||||
import json
|
||||
import logging
|
||||
|
||||
def create_arg_parser():
|
||||
|
||||
parser = argparse.ArgumentParser('Simple BPMN runner')
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('-p', '--process', dest='process', help='The top-level BPMN Process ID')
|
||||
group.add_argument('-c', '--collabortion', dest='collaboration', help='The ID of the collaboration')
|
||||
parser.add_argument('-b', '--bpmn', dest='bpmn', nargs='+', help='BPMN files to load')
|
||||
parser.add_argument('-d', '--dmn', dest='dmn', nargs='*', help='DMN files to load')
|
||||
parser.add_argument('-r', '--restore', dest='restore', metavar='FILE', help='Restore state from %(metavar)s')
|
||||
parser.add_argument('-s', '--step', dest='step', action='store_true', help='Display prompt at every step')
|
||||
parser.add_argument('-l', '--log-level', dest='log_level', metavar='LEVEL', help='Use log level %(metavar)s', default='WARN')
|
||||
return parser
|
||||
|
||||
def configure_logging(log_level, filename):
|
||||
|
||||
logging.addLevelName(15, 'DATA')
|
||||
|
||||
def get_logger(name, fmt):
|
||||
logger = logging.getLogger(name)
|
||||
formatter = logging.Formatter(fmt)
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
return logger
|
||||
|
||||
spiff_log = get_logger('spiff', '%(asctime)s [%(name)s:%(levelname)s] (%(workflow_name)s:%(task_spec)s) %(message)s')
|
||||
metrics_log = get_logger('spiff.metrics', '%(asctime)s [%(name)s:%(levelname)s] (%(task_type)s:%(action)s) %(elapsed)2.4f')
|
||||
metrics_log.propagate = False
|
||||
|
||||
def log_updates(rec):
|
||||
with open(filename, 'a') as fh:
|
||||
fh.write(json.dumps({
|
||||
'task_id': str(rec.task_id),
|
||||
'timestamp': rec.created,
|
||||
'data': rec.data,
|
||||
}))
|
||||
fh.write('\n')
|
||||
return 0
|
||||
|
||||
data_log = logging.getLogger('spiff.data')
|
||||
data_log.addFilter(log_updates)
|
||||
spiff_log.setLevel(log_level)
|
|
@ -1,33 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
|
||||
from product_info import lookup_product_info, lookup_shipping_cost, dumps, loads
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parent = argparse.ArgumentParser()
|
||||
subparsers = parent.add_subparsers(dest='method')
|
||||
|
||||
shared = argparse.ArgumentParser('Context', add_help=False)
|
||||
shared.add_argument('-g', '--globals', dest='globals')
|
||||
shared.add_argument('-l', '--locals', dest='locals', required=True)
|
||||
|
||||
eval_args = subparsers.add_parser('eval', parents=[shared])
|
||||
eval_args.add_argument('-e', '--expr', dest='expr', type=str, required=True)
|
||||
|
||||
exec_args = subparsers.add_parser('exec', parents=[shared])
|
||||
exec_args.add_argument('-s', '--script', dest='script', type=str, required=True)
|
||||
|
||||
args = parent.parse_args()
|
||||
global_ctx = globals()
|
||||
if args.globals is not None:
|
||||
global_ctx.update(loads(args.globals))
|
||||
local_ctx = loads(args.locals)
|
||||
if args.method == 'eval':
|
||||
result = eval(args.expr, global_ctx, local_ctx)
|
||||
elif args.method == 'exec':
|
||||
exec(args.script, global_ctx, local_ctx)
|
||||
result = local_ctx
|
||||
print(dumps(result))
|
|
@ -1,83 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import sys, traceback
|
||||
import json, os
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer
|
||||
|
||||
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser, BpmnValidator
|
||||
from SpiffWorkflow.spiff.serializer.config import SPIFF_SPEC_CONFIG
|
||||
|
||||
from SpiffWorkflow.spiff.specs.defaults import UserTask, ManualTask, NoneTask
|
||||
|
||||
from runner.runner import SimpleBpmnRunner
|
||||
from runner.shared import create_arg_parser, configure_logging
|
||||
from runner.product_info import registry
|
||||
from runner.script_engine import custom_script_engine
|
||||
|
||||
parser = SpiffBpmnParser(validator=BpmnValidator())
|
||||
|
||||
wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter(SPIFF_SPEC_CONFIG)
|
||||
serializer = BpmnWorkflowSerializer(wf_spec_converter, registry)
|
||||
|
||||
forms_dir = 'bpmn/tutorial/forms'
|
||||
|
||||
def display_instructions(task):
|
||||
text = task.task_spec.extensions.get('instructionsForEndUser')
|
||||
print(f'\n{task.task_spec.bpmn_name}')
|
||||
if text is not None:
|
||||
template = Template(text)
|
||||
print(template.render(task.data))
|
||||
|
||||
def complete_user_task(task):
|
||||
display_instructions(task)
|
||||
filename = task.task_spec.extensions['properties']['formJsonSchemaFilename']
|
||||
schema = json.load(open(os.path.join(forms_dir, filename)))
|
||||
data = {}
|
||||
for field, config in schema['properties'].items():
|
||||
if 'oneOf' in config:
|
||||
option_map = dict([ (v['title'], v['const']) for v in config['oneOf'] ])
|
||||
options = "(" + ', '.join(option_map) + ")"
|
||||
prompt = f"{field} {options} "
|
||||
option = input(prompt)
|
||||
while option not in option_map:
|
||||
print(f'Invalid selection!')
|
||||
option = input(prompt)
|
||||
response = option_map[option]
|
||||
else:
|
||||
response = input(f"{config['title']} ")
|
||||
if config['type'] == 'integer':
|
||||
response = int(response)
|
||||
data[field] = response
|
||||
task.update_data(data)
|
||||
|
||||
def complete_manual_task(task):
|
||||
display_instructions(task)
|
||||
input("Press any key to complete task")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
arg_parser = create_arg_parser()
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
configure_logging(args.log_level, 'data.log')
|
||||
|
||||
handlers = {
|
||||
UserTask: complete_user_task,
|
||||
ManualTask: complete_manual_task,
|
||||
NoneTask: complete_manual_task,
|
||||
}
|
||||
|
||||
try:
|
||||
runner = SimpleBpmnRunner(parser, serializer, script_engine=custom_script_engine, handlers=handlers)
|
||||
if args.restore is not None:
|
||||
runner.restore(args.restore)
|
||||
else:
|
||||
runner.parse(args.process or args.collaboration, args.bpmn, args.dmn, args.collaboration is not None)
|
||||
runner.run_workflow(args.step)
|
||||
except Exception as exc:
|
||||
sys.stderr.write(traceback.format_exc())
|
||||
sys.exit(1)
|
|
@ -0,0 +1,69 @@
|
|||
from jinja2 import Template
|
||||
|
||||
from SpiffWorkflow.util.deep_merge import DeepMerge
|
||||
from SpiffWorkflow.camunda.specs.user_task import EnumFormField
|
||||
|
||||
from ..curses_ui.user_input import Field
|
||||
|
||||
class TaskHandler:
|
||||
|
||||
def __init__(self, task):
|
||||
self.task = task
|
||||
|
||||
def get_documentation(self):
|
||||
text = f'{self.task.task_spec.bpmn_name}'
|
||||
if self.task.task_spec.documentation is not None:
|
||||
template = Template(self.task.task_spec.documentation)
|
||||
text += template.render(self.task.data)
|
||||
text += '\n\n'
|
||||
return text
|
||||
|
||||
def update_data(self, dct, name, value):
|
||||
path = name.split('.')
|
||||
current = dct
|
||||
for component in path[:-1]:
|
||||
if component not in current:
|
||||
current[component] = {}
|
||||
current = current[component]
|
||||
current[path[-1]] = value
|
||||
|
||||
def on_complete(self, results):
|
||||
self.task.run()
|
||||
|
||||
|
||||
class ManualTaskHandler(TaskHandler):
|
||||
|
||||
def get_configuration(self):
|
||||
return self.get_documentation(), []
|
||||
|
||||
|
||||
class UserTaskHandler(TaskHandler):
|
||||
|
||||
def get_configuration(self):
|
||||
return self.get_documentation(), self.get_fields()
|
||||
|
||||
def create_field(self, field):
|
||||
if isinstance(field, EnumFormField):
|
||||
option_map = dict((opt.name, opt.id) for opt in field.options)
|
||||
label = field.label + ' (' + ', '.join(option_map) + ')'
|
||||
def validate(value):
|
||||
if value not in option_map:
|
||||
raise Exception(f'Invalid option: {value}')
|
||||
else:
|
||||
return option_map[value]
|
||||
field = Field(field.id, label, lambda v: v, validate, '')
|
||||
elif field.type == 'long':
|
||||
field = Field(field.id, field.label, lambda v: str(v) if v is not None else '', int, None)
|
||||
else:
|
||||
field = Field(field.id, field.label, str, str, '')
|
||||
return field
|
||||
|
||||
def get_fields(self):
|
||||
return [self.create_field(f) for f in self.task.task_spec.form.fields]
|
||||
|
||||
def on_complete(self, results):
|
||||
dct = {}
|
||||
for name, value in results.items():
|
||||
self.update_data(dct, name, value)
|
||||
DeepMerge.merge(self.task.data, dct)
|
||||
super().on_complete(results)
|
|
@ -0,0 +1,44 @@
|
|||
import sqlite3
|
||||
import logging
|
||||
|
||||
from SpiffWorkflow.camunda.serializer import DEFAULT_CONFIG
|
||||
from SpiffWorkflow.camunda.parser import CamundaParser
|
||||
from SpiffWorkflow.camunda.specs import UserTask
|
||||
from SpiffWorkflow.bpmn.specs.defaults import ManualTask, NoneTask
|
||||
from SpiffWorkflow.bpmn import BpmnWorkflow
|
||||
from SpiffWorkflow.bpmn.util.subworkflow import BpmnSubWorkflow
|
||||
from SpiffWorkflow.bpmn.specs import BpmnProcessSpec
|
||||
|
||||
from ..serializer.sqlite import (
|
||||
SqliteSerializer,
|
||||
WorkflowConverter,
|
||||
SubworkflowConverter,
|
||||
WorkflowSpecConverter
|
||||
)
|
||||
from ..engine import BpmnEngine
|
||||
from .curses_handlers import UserTaskHandler, ManualTaskHandler
|
||||
|
||||
logger = logging.getLogger('spiff_engine')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
DEFAULT_CONFIG[BpmnWorkflow] = WorkflowConverter
|
||||
DEFAULT_CONFIG[BpmnSubWorkflow] = SubworkflowConverter
|
||||
DEFAULT_CONFIG[BpmnProcessSpec] = WorkflowSpecConverter
|
||||
|
||||
dbname = 'camunda.db'
|
||||
|
||||
with sqlite3.connect(dbname) as db:
|
||||
SqliteSerializer.initialize(db)
|
||||
|
||||
registry = SqliteSerializer.configure(DEFAULT_CONFIG)
|
||||
serializer = SqliteSerializer(dbname, registry=registry)
|
||||
|
||||
parser = CamundaParser()
|
||||
|
||||
handlers = {
|
||||
UserTask: UserTaskHandler,
|
||||
ManualTask: ManualTaskHandler,
|
||||
NoneTask: ManualTaskHandler,
|
||||
}
|
||||
|
||||
engine = BpmnEngine(parser, serializer, handlers)
|
|
@ -0,0 +1 @@
|
|||
from .subcommands import add_subparsers, configure_logging
|
|
@ -0,0 +1,55 @@
|
|||
import sys
|
||||
import json
|
||||
import logging
|
||||
|
||||
def configure_logging():
|
||||
spiff_logger = logging.getLogger('spiff')
|
||||
spiff_handler = logging.StreamHandler()
|
||||
spiff_handler.setFormatter('%(asctime)s [%(name)s:%(levelname)s] (%(workflow_spec)s:%(task_spec)s) %(message)s')
|
||||
spiff_logger.addHandler(spiff_handler)
|
||||
|
||||
metrics_logger = logging.getLogger('spiff.metrics')
|
||||
metrics_handler = logging.StreamHandler()
|
||||
metrics_handler.setFormatter('%(asctime)s [%(name)s:%(levelname)s] (%(workflow_spec)s:%(task_spec)s) %(elasped)s')
|
||||
metrics_logger.addHandler(metrics_handler)
|
||||
|
||||
def add(engine, args):
|
||||
if args.process is not None:
|
||||
engine.add_spec(args.process, args.bpmn, args.dmn)
|
||||
else:
|
||||
engine.add_collaboration(args.collaboration, args.bpmn, args.dmn)
|
||||
|
||||
def show_library(engine, args):
|
||||
for spec_id, name, filename in engine.list_specs():
|
||||
sys.stdout.write(f'{spec_id} {name:<20s} {filename}\n')
|
||||
|
||||
def run(engine, args):
|
||||
wf_id = engine.start_workflow(args.spec_id)
|
||||
wf = engine.get_workflow(wf_id)
|
||||
engine.run_until_user_input_required(wf)
|
||||
engine.update_workflow(wf, wf_id)
|
||||
if not args.active and not wf.is_completed():
|
||||
raise Exception('Expected the workflow to complete')
|
||||
sys.stdout.write(json.dumps(wf.data, indent=2, separators=[', ', ': ']))
|
||||
sys.stdout.write('\n')
|
||||
|
||||
|
||||
def add_subparsers(subparsers):
|
||||
|
||||
add_spec = subparsers.add_parser('add', help='Add a worfklow spec')
|
||||
group = add_spec.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-p', '--process', dest='process', metavar='BPMN ID', help='The top-level BPMN Process ID')
|
||||
group.add_argument('-c', '--collabortion', dest='collaboration', metavar='BPMN ID', help='The ID of the collaboration')
|
||||
add_spec.add_argument('-b', '--bpmn', dest='bpmn', nargs='+', metavar='FILE', help='BPMN files to load')
|
||||
add_spec.add_argument('-d', '--dmn', dest='dmn', nargs='*', metavar='FILE', help='DMN files to load')
|
||||
add_spec.set_defaults(func=add)
|
||||
|
||||
list_specs = subparsers.add_parser('list', help='List available specs')
|
||||
list_specs.set_defaults(func=show_library)
|
||||
|
||||
run_wf = subparsers.add_parser('run', help='Run a workflow')
|
||||
run_wf.add_argument('-s', '--spec-id', dest='spec_id', metavar='SPEC ID', help='The ID of the spec to run')
|
||||
run_wf.add_argument('-a', '--active-ok', dest='active', action='store_true', help='Suppress exception if the workflow does not complete')
|
||||
run_wf.set_defaults(func=run)
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
from .ui import CursesUI
|
|
@ -0,0 +1,75 @@
|
|||
import curses, curses.ascii
|
||||
|
||||
class Region:
|
||||
|
||||
def __init__(self):
|
||||
self.top = 0
|
||||
self.left = 0
|
||||
self.height = 1
|
||||
self.width = 1
|
||||
|
||||
@property
|
||||
def bottom(self):
|
||||
return self.top + self.height - 1
|
||||
|
||||
@property
|
||||
def right(self):
|
||||
return self.left + self.width - 1
|
||||
|
||||
@property
|
||||
def box(self):
|
||||
return self.top, self.left, self.bottom, self.right
|
||||
|
||||
def resize(self, top, left, height, width):
|
||||
self.top, self.left, self.height, self.width = top, left, height, width
|
||||
|
||||
|
||||
class Content:
|
||||
|
||||
def __init__(self, region):
|
||||
|
||||
self.region = region
|
||||
|
||||
self.screen = curses.newpad(self.region.height, self.region.width)
|
||||
self.screen.keypad(True)
|
||||
self.screen.scrollok(True)
|
||||
self.screen.idlok(True)
|
||||
self.screen.attron(curses.COLOR_WHITE)
|
||||
|
||||
self.content_height = 1
|
||||
self.first_visible = 0
|
||||
|
||||
self.menu = ['[ESC] return to main menu']
|
||||
|
||||
@property
|
||||
def last_visible(self):
|
||||
return self.first_visible + self.region.height - 1
|
||||
|
||||
def scroll_up(self, y):
|
||||
if self.first_visible > 0 and y == self.first_visible:
|
||||
self.first_visible -= 1
|
||||
self.screen.move(max(0, y - 1), 0)
|
||||
|
||||
def scroll_down(self, y):
|
||||
if self.last_visible < self.content_height - 1 and y == self.last_visible - 1:
|
||||
self.first_visible += 1
|
||||
self.screen.move(min(self.content_height - 1, y + 1), 0)
|
||||
|
||||
def page_up(self, y):
|
||||
self.first_visible = max(0, y - self.region.height)
|
||||
self.screen.move(self.first_visible, 0)
|
||||
|
||||
def page_down(self, y):
|
||||
self.first_visible = min(self.content_height - self.region.height, y + self.region.height)
|
||||
self.screen.move(self.first_visible, 0)
|
||||
|
||||
def draw(self):
|
||||
pass
|
||||
|
||||
def handle_key(self, ch, y, x):
|
||||
pass
|
||||
|
||||
def resize(self):
|
||||
self.screen.resize(max(self.region.height, self.content_height), self.region.width)
|
||||
self.screen.noutrefresh(self.first_visible, 0, *self.region.box)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import curses, curses.ascii
|
||||
from .content import Content
|
||||
|
||||
|
||||
class ListView(Content):
|
||||
|
||||
def __init__(self, region, header, select_action, delete_action):
|
||||
|
||||
super().__init__(region)
|
||||
self.header = header
|
||||
self.select_action = select_action
|
||||
self.delete_action = delete_action
|
||||
|
||||
self._items = []
|
||||
self.item_ids = []
|
||||
self.selected = 0
|
||||
self.selected_attr = curses.color_pair(6) | curses.A_BOLD
|
||||
|
||||
self.menu = [
|
||||
'[ENTER] run',
|
||||
'[d] delete'
|
||||
] + self.menu
|
||||
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
return self._items
|
||||
|
||||
@items.setter
|
||||
def items(self, items):
|
||||
if len(items) > 0:
|
||||
item_ids, items = zip(*[(item[0], item[1:]) for item in items])
|
||||
self.item_ids = list(item_ids)
|
||||
self._items = [ [str(v) if v is not None else '' for v in item] for item in items ]
|
||||
else:
|
||||
self._items, self.item_ids = [], []
|
||||
|
||||
def draw(self):
|
||||
|
||||
self.screen.erase()
|
||||
self.screen.move(0, 0)
|
||||
col_widths = [2] + [max([len(val) for val in item]) for item in zip(*[self.header] + list(self.items))]
|
||||
fmt = ' '.join([ f'{{{idx}:{w}s}}' for idx, w in enumerate(col_widths) ])
|
||||
self.screen.addstr(fmt.format('', *self.header) + '\n', curses.A_BOLD)
|
||||
|
||||
for idx, item in enumerate(self.items):
|
||||
attr = self.selected_attr if idx == self.selected else 0
|
||||
self.screen.addstr('\n' + fmt.format('*', *item), attr)
|
||||
|
||||
self.screen.move(self.selected + 2, 0)
|
||||
self.screen.noutrefresh(self.first_visible, 0, *self.region.box)
|
||||
|
||||
def handle_key(self, ch, y, x):
|
||||
if ch == curses.ascii.NL:
|
||||
item_id = self.item_ids[self.selected]
|
||||
self.select_action(item_id)
|
||||
elif ch == curses.KEY_DOWN and self.selected < len(self.items) - 1:
|
||||
self.selected += 1
|
||||
self.draw()
|
||||
elif ch == curses.KEY_UP and self.selected > 0:
|
||||
self.selected -= 1
|
||||
self.draw()
|
||||
elif chr(ch).lower() == 'd':
|
||||
item_id = self.item_ids[self.selected]
|
||||
self.delete_action(item_id)
|
||||
self._items.pop(self.selected)
|
||||
self.item_ids.pop(self.selected)
|
||||
if self.selected == len(self.item_ids):
|
||||
self.selected = max(0, self.selected - 1)
|
||||
self.draw()
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import curses, curses.ascii
|
||||
import traceback, logging
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from .content import Content
|
||||
|
||||
logger = logging.getLogger('root')
|
||||
|
||||
|
||||
class LogHandler(logging.Handler):
|
||||
|
||||
def __init__(self, write):
|
||||
super().__init__()
|
||||
self.write = write
|
||||
|
||||
def emit(self, record):
|
||||
self.write(record)
|
||||
|
||||
|
||||
class LogView(Content):
|
||||
|
||||
def __init__(self, region):
|
||||
|
||||
super().__init__(region)
|
||||
|
||||
logger.addHandler(LogHandler(self.write))
|
||||
self.styles = {
|
||||
'ERROR': curses.color_pair(9),
|
||||
'WARNING': curses.color_pair(11),
|
||||
}
|
||||
self.menu = ['[ESC] return to previous screen']
|
||||
|
||||
self.previous_state = None
|
||||
|
||||
def write(self, record):
|
||||
|
||||
y, x = curses.getsyx()
|
||||
|
||||
if record.exc_info is not None and record.levelno == 40:
|
||||
trace = traceback.format_exception(*record.exc_info)
|
||||
else:
|
||||
trace = []
|
||||
|
||||
self.content_height += len(trace) + 1
|
||||
self.first_visible = max(0, self.content_height - self.region.height)
|
||||
self.resize()
|
||||
|
||||
dt = datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S.%f')
|
||||
if record.name == 'spiff':
|
||||
message = f'{dt} [{record.name}:{record.levelname}] ({record.workflow_spec}:{record.task_spec}) {record.msg}'
|
||||
else:
|
||||
message = f'{dt} [{record.name}:{record.levelname}] {record.msg}'
|
||||
self.screen.addstr(f'\n{message}', self.styles.get(record.levelname, 0))
|
||||
for line in trace:
|
||||
self.screen.addstr(f'\n{line}')
|
||||
|
||||
self.screen.clrtoeol()
|
||||
self.screen.refresh(self.first_visible, 0, *self.region.box)
|
||||
|
||||
curses.setsyx(y, x)
|
||||
|
||||
def draw(self):
|
||||
curses.curs_set(1)
|
||||
|
||||
def handle_key(self, ch, y, x):
|
||||
if ch == curses.KEY_UP:
|
||||
self.scroll_up(y)
|
||||
elif ch == curses.KEY_DOWN:
|
||||
self.scroll_down(y)
|
||||
elif ch == curses.KEY_PPAGE:
|
||||
self.page_up(y)
|
||||
elif ch == curses.KEY_NPAGE:
|
||||
self.page_down(y)
|
||||
self.screen.refresh(self.first_visible, 0, *self.region.box)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import curses, curses.ascii
|
||||
from .content import Content
|
||||
|
||||
|
||||
class Menu(Content):
|
||||
|
||||
def __init__(self, region, menu_items):
|
||||
|
||||
super().__init__(region)
|
||||
|
||||
self.current_option = 0
|
||||
self.options, self.handlers = zip(*menu_items)
|
||||
self.menu = None
|
||||
|
||||
def draw(self):
|
||||
|
||||
curses.curs_set(0)
|
||||
self.screen.erase()
|
||||
mid_x = self.region.width // 2
|
||||
mid_y = self.region.height // 2
|
||||
self.screen.move(1, mid_x)
|
||||
for idx, option in enumerate(self.options):
|
||||
attr = curses.A_BOLD if idx == self.current_option else 0
|
||||
self.screen.addstr(mid_y + idx, mid_x - len(option) // 2, f'{option}\n', attr)
|
||||
self.screen.noutrefresh(self.first_visible, 0, *self.region.box)
|
||||
|
||||
def handle_key(self, ch, y, x):
|
||||
|
||||
if ch == curses.KEY_DOWN and self.current_option < len(self.options) - 1:
|
||||
self.current_option += 1
|
||||
self.draw()
|
||||
self.screen.noutrefresh(self.first_visible, 0, *self.region.box)
|
||||
elif ch == curses.KEY_UP and self.current_option > 0:
|
||||
self.current_option -= 1
|
||||
self.draw()
|
||||
self.screen.noutrefresh(self.first_visible, 0, *self.region.box)
|
||||
elif ch == curses.ascii.NL:
|
||||
self.handlers[self.current_option]()
|
||||
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import curses, curses.ascii
|
||||
from .content import Content
|
||||
|
||||
|
||||
class SpecView:
|
||||
|
||||
def __init__(self, left, right, add_spec):
|
||||
|
||||
self.left = Content(left)
|
||||
self.right = Content(right)
|
||||
self.add_spec = add_spec
|
||||
|
||||
self.bpmn_id = None
|
||||
self.bpmn_files = []
|
||||
self.dmn_files = []
|
||||
|
||||
self.bpmn_id_line = 2
|
||||
self.bpmn_line = 4
|
||||
self.dmn_line = 6
|
||||
self.add_line = 8
|
||||
|
||||
self.screen = self.right.screen
|
||||
self.menu = self.right.menu
|
||||
|
||||
def can_edit(self, lineno):
|
||||
return lineno in [self.bpmn_id_line, self.bpmn_line, self.dmn_line]
|
||||
|
||||
def bpmn_filename(self, lineno):
|
||||
return lineno > self.bpmn_line and lineno <= self.bpmn_line + len(self.bpmn_files)
|
||||
|
||||
def dmn_filename(self, lineno):
|
||||
return lineno > self.dmn_line and lineno <= self.dmn_line + len(self.dmn_files)
|
||||
|
||||
def draw(self, lineno=None, clear=False):
|
||||
|
||||
self.bpmn_line = 2 + self.bpmn_id_line
|
||||
self.dmn_line = 2 + self.bpmn_line + len(self.bpmn_files)
|
||||
self.add_line = 2 + self.dmn_line + len(self.dmn_files)
|
||||
|
||||
self.left.screen.erase()
|
||||
self.right.screen.erase()
|
||||
|
||||
self.left.screen.addstr(self.bpmn_id_line, self.left.region.width - 13, 'Process ID: ')
|
||||
self.left.screen.addstr(self.bpmn_line, self.left.region.width - 13, 'BPMN files: ')
|
||||
self.left.screen.addstr(self.dmn_line, self.left.region.width - 12, 'DMN files: ')
|
||||
|
||||
if self.bpmn_id is not None:
|
||||
self.right.screen.addstr(self.bpmn_id_line, 0, self.bpmn_id)
|
||||
for offset, filename in enumerate(self.bpmn_files):
|
||||
self.right.screen.addstr(self.bpmn_line + offset + 1, 0, f'[X] {filename}')
|
||||
for offset, filename in enumerate(self.dmn_files):
|
||||
self.right.screen.addstr(self.dmn_line + offset + 1, 0, f'[X] {filename}')
|
||||
|
||||
self.right.screen.addstr(self.add_line, 0, '[Add]', curses.A_BOLD)
|
||||
self.right.screen.addstr(' (Press ESC to cancel)')
|
||||
|
||||
self.right.screen.move(lineno or self.bpmn_id_line, 0)
|
||||
if clear:
|
||||
self.right.screen.clrtoeol()
|
||||
|
||||
self.left.screen.noutrefresh(self.left.first_visible, 0, *self.left.region.box)
|
||||
self.right.screen.noutrefresh(self.right.first_visible, 0, *self.right.region.box)
|
||||
|
||||
curses.curs_set(1)
|
||||
curses.ungetch(curses.KEY_LEFT)
|
||||
|
||||
def handle_key(self, ch, y, x):
|
||||
|
||||
if ch == curses.KEY_BACKSPACE and self.can_edit(y):
|
||||
self.right.screen.move(y, max(0, x - 1))
|
||||
self.right.screen.delch(y, max(0, x - 1))
|
||||
elif ch == curses.KEY_LEFT and self.can_edit(y):
|
||||
self.right.screen.move(y, max(0, x - 1))
|
||||
elif ch == curses.KEY_RIGHT and self.can_edit(y):
|
||||
line = self.right.screen.instr(y, 0, self.right.region.width).decode('utf-8').rstrip()
|
||||
self.right.screen.move(y, min(len(line), x + 1))
|
||||
elif ch == curses.KEY_DOWN:
|
||||
if self.bpmn_filename(y + 1) or self.dmn_filename(y + 1):
|
||||
self.right.screen.move(y + 1, 1)
|
||||
elif ch == curses.KEY_UP:
|
||||
if y - 1 == self.bpmn_line or y - 1 == self.dmn_line:
|
||||
self.right.screen.move(y - 1, 0)
|
||||
elif self.bpmn_filename(y - 1) or self.dmn_filename(y - 1):
|
||||
self.right.screen.move(y - 1, 1)
|
||||
elif ch == curses.ascii.TAB:
|
||||
if y == self.bpmn_id_line:
|
||||
self.right.screen.move(self.bpmn_line, 0)
|
||||
elif y == self.bpmn_line or self.bpmn_filename(y):
|
||||
self.right.screen.move(self.dmn_line, 0)
|
||||
elif y == self.dmn_line or self.dmn_filename(y):
|
||||
self.right.screen.move(self.add_line, 1)
|
||||
elif y == self.add_line:
|
||||
self.right.screen.move(self.bpmn_id_line, 0)
|
||||
elif ch == curses.ascii.NL:
|
||||
text = self.right.screen.instr(y, 0, x).decode('utf-8').strip()
|
||||
if y == self.bpmn_id_line and text != '':
|
||||
self.bpmn_id = text
|
||||
self.right.screen.addstr(y, 0, text, curses.A_ITALIC)
|
||||
self.right.screen.move(self.bpmn_line, 0)
|
||||
elif y == self.bpmn_line and text != '':
|
||||
self.bpmn_files.append(text)
|
||||
self.draw(self.bpmn_line, True)
|
||||
elif y == self.dmn_line and text != '':
|
||||
self.dmn_files.append(text)
|
||||
self.draw(self.dmn_line, True)
|
||||
elif self.bpmn_filename(y):
|
||||
self.bpmn_files.pop(y - self.bpmn_line - 1)
|
||||
self.draw(self.bpmn_line)
|
||||
elif self.dmn_filename(y):
|
||||
self.dmn_files.pop(y - self.dmn_line - 1)
|
||||
self.draw(self.dmn_line)
|
||||
elif y == self.add_line:
|
||||
spec_id = self.add_spec(self.bpmn_id, self.bpmn_files, self.dmn_files or None)
|
||||
self.bpmn_id = None
|
||||
self.bpmn_files = []
|
||||
self.dmn_files = []
|
||||
self.draw()
|
||||
elif curses.ascii.isprint(ch):
|
||||
self.right.screen.echochar(ch)
|
||||
self.left.screen.noutrefresh(0, 0, *self.left.region.box)
|
||||
self.right.screen.noutrefresh(0, 0, *self.right.region.box)
|
||||
|
||||
def resize(self):
|
||||
self.left.resize()
|
||||
self.right.resize()
|
|
@ -0,0 +1,194 @@
|
|||
import curses, curses.ascii
|
||||
import sys
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from .content import Region, Content
|
||||
|
||||
from .menu import Menu
|
||||
from .log_view import LogView
|
||||
from .list_view import ListView
|
||||
from .workflow_view import WorkflowView, default_view, run_view
|
||||
from .spec_view import SpecView
|
||||
from .user_input import UserInput, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CursesUI:
|
||||
|
||||
def __init__(self, window, engine):
|
||||
|
||||
for i in range(1, int(curses.COLOR_PAIRS / 256)):
|
||||
curses.init_pair(i, i, 0)
|
||||
|
||||
self.engine = engine
|
||||
|
||||
self.window = window
|
||||
self.window.attron(curses.COLOR_WHITE)
|
||||
self.window.nodelay(True)
|
||||
|
||||
self.left = Region()
|
||||
self.right = Region()
|
||||
self.top = Region()
|
||||
self.menu = Region()
|
||||
self.bottom = Region()
|
||||
|
||||
self.menu_content = Content(self.menu)
|
||||
|
||||
self._states = {
|
||||
'main_menu': Menu(self.top, [
|
||||
('Add spec', lambda: self.set_state('add_spec')),
|
||||
('Start Workflow', lambda: self.set_state('start_workflow')),
|
||||
('Resume workflow', lambda: self.set_state('resume_workflow')),
|
||||
('List workflows', lambda: self.set_state('list_workflows')),
|
||||
('Quit', self.quit),
|
||||
]),
|
||||
'add_spec': SpecView(self.left, self.right, self.engine.add_spec),
|
||||
'log_view': LogView(self.bottom),
|
||||
'start_workflow': ListView(
|
||||
self.top,
|
||||
['Name', 'Filename'],
|
||||
self.start_workflow,
|
||||
self.engine.delete_workflow_spec,
|
||||
),
|
||||
'resume_workflow': ListView(
|
||||
self.top,
|
||||
['Spec', 'Active tasks', 'Started', 'Updated'],
|
||||
self.run_workflow,
|
||||
self.engine.delete_workflow
|
||||
),
|
||||
'list_workflows': ListView(
|
||||
self.top,
|
||||
['Spec', 'Active tasks', 'Started', 'Updated', 'Ended'],
|
||||
self.view_workflow,
|
||||
self.engine.delete_workflow,
|
||||
),
|
||||
'view_workflow': WorkflowView(self),
|
||||
'user_input': UserInput(self.left, self.right),
|
||||
}
|
||||
self.resize()
|
||||
self._state = None
|
||||
self.state = 'main_menu'
|
||||
self.run()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._states[self._state]
|
||||
|
||||
@state.setter
|
||||
def state(self, state):
|
||||
self._state = state
|
||||
self.menu_content.screen.erase()
|
||||
if self.state.menu is not None:
|
||||
for action in self.state.menu:
|
||||
self.menu_content.screen.addstr(f'{action} ')
|
||||
self.menu_content.screen.noutrefresh(0, 0, *self.menu.box)
|
||||
self.state.draw()
|
||||
curses.doupdate()
|
||||
|
||||
def set_state(self, state): # For callbacks on different screens
|
||||
if state == 'start_workflow':
|
||||
self._switch_to_list('start_workflow', self.engine.list_specs())
|
||||
elif state == 'resume_workflow':
|
||||
self._switch_to_list('resume_workflow', self.engine.list_workflows())
|
||||
elif state == 'list_workflows':
|
||||
self._switch_to_list('list_workflows', self.engine.list_workflows(True))
|
||||
self.state = state
|
||||
|
||||
def run(self):
|
||||
|
||||
while True:
|
||||
y, x = self.state.screen.getyx()
|
||||
ch = self.state.screen.getch()
|
||||
if ch == curses.KEY_RESIZE:
|
||||
self.resize()
|
||||
self.state.draw()
|
||||
elif ch == curses.ascii.ESC:
|
||||
if self._state in ['log_view', 'view_workflow']:
|
||||
self.set_state(self.state._previous_state)
|
||||
elif self._state == 'user_input':
|
||||
self.set_state('view_workflow')
|
||||
else:
|
||||
self.state = 'main_menu'
|
||||
elif chr(ch) == ';':
|
||||
self._states['log_view']._previous_state = self._state
|
||||
self.state = 'log_view'
|
||||
else:
|
||||
try:
|
||||
self.state.handle_key(ch, y, x)
|
||||
except Exception as exc:
|
||||
logger.error(str(exc), exc_info=True)
|
||||
curses.doupdate()
|
||||
|
||||
def start_workflow(self, spec_id):
|
||||
wf_id = self.engine.start_workflow(spec_id)
|
||||
self.set_workflow(wf_id, False, run_view)
|
||||
|
||||
def _switch_to_list(self, state, items):
|
||||
self._states[state].items = items
|
||||
self.state = state
|
||||
|
||||
def run_workflow(self, wf_id):
|
||||
self.set_workflow(wf_id, False, run_view)
|
||||
|
||||
def view_workflow(self, wf_id):
|
||||
self.set_workflow(wf_id, True, default_view)
|
||||
|
||||
def set_workflow(self, wf_id, step, filters):
|
||||
workflow = self.engine.get_workflow(wf_id)
|
||||
self._states['view_workflow'].set_workflow(workflow, wf_id)
|
||||
self._states['view_workflow'].step = step
|
||||
self._states['view_workflow']._previous_state = 'list_workflows' if step else 'resume_workflow'
|
||||
self._states['view_workflow'].current_filter = filters.copy()
|
||||
self._run_workflow()
|
||||
|
||||
def _run_workflow(self):
|
||||
if not self._states['view_workflow'].step:
|
||||
self.engine.run_until_user_input_required(self._states['view_workflow'].workflow)
|
||||
self.state = 'view_workflow'
|
||||
|
||||
def show_filters(self, fields):
|
||||
|
||||
def on_complete(results):
|
||||
self._states['view_workflow'].current_filter.update(results)
|
||||
self.state = 'view_workflow'
|
||||
|
||||
self._states['user_input'].configure('', fields, on_complete)
|
||||
self.state = 'user_input'
|
||||
|
||||
def complete_task(self, task):
|
||||
|
||||
handler = self.engine.handler(task)
|
||||
if handler is not None:
|
||||
instructions, fields = handler.get_configuration()
|
||||
def on_complete(results):
|
||||
handler.on_complete(results)
|
||||
self._run_workflow()
|
||||
self._states['user_input'].configure(instructions, fields, on_complete)
|
||||
self.state = 'user_input'
|
||||
else:
|
||||
task.run()
|
||||
self._run_workflow()
|
||||
|
||||
def quit(self):
|
||||
sys.exit(0)
|
||||
|
||||
def resize(self):
|
||||
|
||||
y, x = self.window.getmaxyx()
|
||||
div_y, div_x = y // 4, x // 3
|
||||
|
||||
self.top.resize(1, 1, y - div_y - 3, x - 2)
|
||||
self.left.resize(1, 1, y - div_y - 3, div_x - 1)
|
||||
self.right.resize(1, div_x, y - div_y - 3, x - div_x - 1)
|
||||
self.menu.resize(y - div_y - 1, 1, 1, x - 2)
|
||||
self.bottom.resize(y - div_y + 1, 0, div_y - 1, x)
|
||||
|
||||
for state in self._states.values():
|
||||
state.resize()
|
||||
self.menu_content.resize()
|
||||
|
||||
self.window.hline(y - div_y, 0, curses.ACS_HLINE, x)
|
||||
self.window.refresh()
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import curses, curses.ascii
|
||||
from .content import Content
|
||||
|
||||
|
||||
class Field:
|
||||
|
||||
def __init__(self, name, label, to_str, from_str, value):
|
||||
self.name = name
|
||||
self.label = label
|
||||
self.to_str = to_str
|
||||
self.from_str = from_str
|
||||
self.value = value
|
||||
|
||||
class UserInput:
|
||||
|
||||
def __init__(self, left, right):
|
||||
|
||||
self.left = Content(left)
|
||||
self.right = Content(right)
|
||||
self.screen = self.right.screen
|
||||
|
||||
self.instructions = ''
|
||||
|
||||
self.fields = []
|
||||
self.on_complete = None
|
||||
|
||||
self.current_field = 0
|
||||
self.offsets = []
|
||||
|
||||
self.menu = ['[ESC] to cancel']
|
||||
|
||||
def configure(self, instructions, fields, on_complete):
|
||||
|
||||
self.instructions = instructions
|
||||
self.fields = fields
|
||||
self.on_complete = on_complete
|
||||
self.offsets = []
|
||||
self.current_field = 0
|
||||
self.results = {}
|
||||
|
||||
def draw(self, itemno=None):
|
||||
|
||||
self.left.screen.erase()
|
||||
self.right.screen.erase()
|
||||
self.left.screen.move(0, 0)
|
||||
self.right.screen.move(0, 0)
|
||||
|
||||
if self.instructions != '':
|
||||
self.right.screen.addstr(2, 0, self.instructions)
|
||||
|
||||
y, x = self.right.screen.getyx()
|
||||
for idx, field in enumerate(self.fields):
|
||||
offset = y + idx * 2
|
||||
self.offsets.append(offset)
|
||||
text = f'{field.label}: '
|
||||
self.left.screen.addstr(offset, self.left.region.width - len(text), text, curses.A_BOLD)
|
||||
self.right.screen.addstr(offset, 0, field.to_str(field.value))
|
||||
|
||||
y, x = self.right.screen.getyx()
|
||||
offset = y + 2
|
||||
self.right.screen.addstr(offset, 0, '[Done]', curses.A_BOLD)
|
||||
self.offsets.append(offset)
|
||||
|
||||
self.right.screen.move(self.offsets[itemno or 0], 0)
|
||||
|
||||
self.left.screen.noutrefresh(0, 0, *self.left.region.box)
|
||||
self.right.screen.noutrefresh(0, 0, *self.right.region.box)
|
||||
|
||||
curses.curs_set(1)
|
||||
curses.ungetch(curses.KEY_LEFT)
|
||||
|
||||
def handle_key(self, ch, y, x):
|
||||
|
||||
if ch == curses.KEY_BACKSPACE:
|
||||
self.right.screen.move(y, max(0, x - 1))
|
||||
self.right.screen.delch(y, max(0, x - 1))
|
||||
elif ch == curses.ascii.STX:
|
||||
self.right.screen.move(y, 0)
|
||||
elif ch == curses.ascii.ETX:
|
||||
line = self.right.screen.instr(y, 0, self.right.region.width).decode('utf-8').rstrip()
|
||||
self.right.screen.move(y, len(line) - 1)
|
||||
elif ch == curses.KEY_LEFT:
|
||||
self.right.screen.move(y, max(0, x - 1))
|
||||
elif ch == curses.KEY_RIGHT:
|
||||
line = self.right.screen.instr(y, 0, self.right.region.width).decode('utf-8').rstrip()
|
||||
self.right.screen.move(y, min(len(line), x + 1))
|
||||
elif ch == curses.ascii.TAB:
|
||||
self.current_field = 0 if self.current_field == len(self.offsets) - 1 else self.current_field + 1
|
||||
self.right.screen.move(self.offsets[self.current_field], 0)
|
||||
elif curses.ascii.isprint(ch):
|
||||
self.right.screen.echochar(ch)
|
||||
|
||||
self.left.screen.noutrefresh(self.left.first_visible, 0, *self.left.region.box)
|
||||
self.right.screen.noutrefresh(self.left.first_visible, 0, *self.right.region.box)
|
||||
|
||||
if ch == curses.ascii.NL:
|
||||
if self.current_field < len(self.offsets) - 1:
|
||||
field = self.fields[self.current_field]
|
||||
line = self.right.screen.instr(y, 0, self.right.region.width).decode('utf-8').rstrip()
|
||||
self.right.screen.addstr(self.offsets[self.current_field], 0, line, curses.A_ITALIC)
|
||||
field.value = field.from_str(line)
|
||||
curses.ungetch(curses.ascii.TAB)
|
||||
self.left.screen.noutrefresh(self.left.first_visible, 0, *self.left.region.box)
|
||||
self.right.screen.noutrefresh(self.left.first_visible, 0, *self.right.region.box)
|
||||
else:
|
||||
self.on_complete(dict((f.name, f.value) for f in self.fields))
|
||||
|
||||
def resize(self):
|
||||
self.left.resize()
|
||||
self.right.resize()
|
|
@ -0,0 +1,228 @@
|
|||
import curses, curses.ascii
|
||||
import json
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from SpiffWorkflow.util.task import TaskState
|
||||
|
||||
from .content import Content
|
||||
from .user_input import Field
|
||||
|
||||
default_view = {
|
||||
'state': TaskState.ANY_MASK,
|
||||
'spec_name': None,
|
||||
'updated_ts': 0,
|
||||
}
|
||||
|
||||
run_view = {
|
||||
'state': TaskState.READY|TaskState.WAITING,
|
||||
'spec_name': None,
|
||||
'updated_ts': 0,
|
||||
}
|
||||
|
||||
|
||||
class WorkflowView:
|
||||
|
||||
def __init__(self, app):
|
||||
|
||||
self.left = Content(app.left)
|
||||
self.right = Content(app.right)
|
||||
|
||||
self.engine = app.engine
|
||||
self.complete_task = app.complete_task
|
||||
self._show_filters = app.show_filters
|
||||
|
||||
|
||||
self.workflow = None
|
||||
self.workflow_id = None
|
||||
self.current_filter = default_view
|
||||
self.step = True
|
||||
self.task_view = 'list'
|
||||
self.info_view = 'task'
|
||||
self.scroll = 'left'
|
||||
self.tasks = []
|
||||
self.selected = 0
|
||||
self._previous_state = None
|
||||
|
||||
self.screen = self.left.screen
|
||||
self.menu = [
|
||||
'[l]ist/tree view',
|
||||
'[w]orkflow/task data view',
|
||||
'[f]ilter tasks',
|
||||
'[r]efresh tasks',
|
||||
'[s]ave workflow state',
|
||||
]
|
||||
|
||||
self.styles = {
|
||||
TaskState.MAYBE: curses.color_pair(4),
|
||||
TaskState.LIKELY: curses.color_pair(4),
|
||||
TaskState.FUTURE: curses.color_pair(6),
|
||||
TaskState.WAITING: curses.color_pair(3),
|
||||
TaskState.READY: curses.color_pair(2),
|
||||
TaskState.STARTED: curses.color_pair(6),
|
||||
TaskState.ERROR: curses.color_pair(1),
|
||||
TaskState.CANCELLED: curses.color_pair(5),
|
||||
}
|
||||
|
||||
def set_workflow(self, workflow, wf_id):
|
||||
self.workflow = workflow
|
||||
self.workflow_id = wf_id
|
||||
|
||||
def draw(self):
|
||||
self.update_task_tree()
|
||||
self.update_info()
|
||||
|
||||
def update_task_tree(self):
|
||||
|
||||
self.tasks = [t for t in self.workflow.get_tasks(**self.current_filter)]
|
||||
if self.selected > len(self.tasks) - 1:
|
||||
self.selected = 0
|
||||
self.left.screen.erase()
|
||||
if len(self.tasks) > 0:
|
||||
self.left.content_height = len(self.tasks)
|
||||
self.left.resize()
|
||||
for idx, task in enumerate(self.tasks):
|
||||
indent = 2 * task.depth
|
||||
color = self.styles.get(task.state, 0)
|
||||
attr = color | curses.A_BOLD if idx == self.selected else color
|
||||
name = task.task_spec.bpmn_name or task.task_spec.name
|
||||
lane = f'({task.task_spec.lane}) ' if task.task_spec.lane is not None else ''
|
||||
state = TaskState.get_name(task.state)
|
||||
task_info = f'{lane}{name} [{state}]'
|
||||
if self.task_view == 'list':
|
||||
self.left.screen.addstr(idx, 0, task_info, attr)
|
||||
else:
|
||||
self.left.screen.addstr(idx, 0, ' ' * indent + task_info, attr)
|
||||
if self.info_view == 'task':
|
||||
self.show_task()
|
||||
self.left.screen.move(self.selected, 0)
|
||||
else:
|
||||
self.info_view = 'workflow'
|
||||
self.left.content_height = self.left.region.height - 1
|
||||
self.left.resize()
|
||||
self.left.screen.addstr(0, 0, 'No tasks available')
|
||||
self.left.screen.noutrefresh(self.left.first_visible, 0, *self.left.region.box)
|
||||
|
||||
def update_info(self):
|
||||
if self.info_view == 'task' and len(self.tasks) > 0:
|
||||
self.show_task()
|
||||
else:
|
||||
self.show_workflow()
|
||||
|
||||
def show_task(self):
|
||||
task = self.tasks[self.selected]
|
||||
info = {
|
||||
'Name': task.task_spec.name,
|
||||
'Bpmn ID': task.task_spec.bpmn_id or '',
|
||||
'Bpmn name': task.task_spec.bpmn_name or '',
|
||||
'Description': task.task_spec.description,
|
||||
'Last state change': datetime.fromtimestamp(task.last_state_change),
|
||||
}
|
||||
self._show_details(info, task.data)
|
||||
|
||||
def show_workflow(self):
|
||||
info = {
|
||||
'Spec': self.workflow.spec.name,
|
||||
'Ready tasks': len(self.workflow.get_tasks(state=TaskState.READY)),
|
||||
'Waiting tasks': len(self.workflow.get_tasks(state=TaskState.WAITING)),
|
||||
'Finished tasks': len(self.workflow.get_tasks(state=TaskState.FINISHED_MASK)),
|
||||
'Total tasks': len(self.workflow.get_tasks()),
|
||||
'Waiting subprocesses': len([sp for sp in self.workflow.subprocesses.values() if not sp.is_completed()]),
|
||||
'Total subprocesses': len(self.workflow.subprocesses)
|
||||
}
|
||||
self._show_details(info, self.workflow.data)
|
||||
|
||||
def _show_details(self, info, data=None):
|
||||
|
||||
self.right.screen.erase()
|
||||
|
||||
lines = len(info)
|
||||
if data is not None:
|
||||
lines += 2
|
||||
serialized = {}
|
||||
for key, value in data.items():
|
||||
serialized[key] = json.dumps(value, indent=2)
|
||||
lines += len(serialized[key].split('\n'))
|
||||
self.right.content_height = lines + 1
|
||||
|
||||
for name, value in info.items():
|
||||
self.right.screen.addstr(f'{name}: ', curses.A_BOLD)
|
||||
self.right.screen.addstr(f'{value}\n')
|
||||
|
||||
if data is not None:
|
||||
self.right.screen.addstr('\nData\n', curses.A_BOLD)
|
||||
for name, value in serialized.items():
|
||||
self.right.screen.addstr(f'{name}: ', curses.A_ITALIC)
|
||||
self.right.screen.addstr(f'{value}\n')
|
||||
|
||||
self.right.screen.noutrefresh(self.right.first_visible, 0, *self.right.region.box)
|
||||
|
||||
def show_filters(self):
|
||||
values = self.current_filter
|
||||
fields = [
|
||||
Field('state', 'State', TaskState.get_name, TaskState.get_value, values['state']),
|
||||
Field('spec_name', 'Task spec', lambda v: v or '', lambda v: None if v == '' else v, values['spec_name']),
|
||||
Field(
|
||||
'updated_ts',
|
||||
'Updated on or after',
|
||||
lambda v: datetime.fromtimestamp(v).isoformat(),
|
||||
lambda v: datetime.fromisoformat(v).timestamp(),
|
||||
values['updated_ts'],
|
||||
),
|
||||
]
|
||||
self._show_filters(fields)
|
||||
|
||||
def handle_key(self, ch, y, x):
|
||||
|
||||
if chr(ch).lower() == 'l':
|
||||
self.task_view = 'tree' if self.task_view == 'list' else 'list'
|
||||
self.update_task_tree()
|
||||
elif chr(ch).lower() == 'w':
|
||||
self.info_view = 'workflow' if self.info_view == 'task' else 'task'
|
||||
self.update_info()
|
||||
elif chr(ch).lower() == 'f':
|
||||
self.show_filters()
|
||||
elif chr(ch).lower() == 'r':
|
||||
if self.step is False:
|
||||
self.engine.run_ready_events(self.workflow)
|
||||
else:
|
||||
self.workflow.refresh_waiting_tasks()
|
||||
self.update_task_tree()
|
||||
elif chr(ch).lower() == 's':
|
||||
self.engine.update_workflow(self.workflow, self.workflow_id)
|
||||
elif ch == curses.ascii.TAB:
|
||||
if self.scroll == 'right':
|
||||
self.scroll = 'left'
|
||||
self.screen = self.left.screen
|
||||
curses.curs_set(0)
|
||||
else:
|
||||
self.scroll = 'right'
|
||||
self.screen = self.right.screen
|
||||
self.right.screen.move(0, 0)
|
||||
curses.curs_set(1)
|
||||
elif ch == curses.KEY_DOWN:
|
||||
if self.scroll == 'left' and self.selected < len(self.tasks) - 1:
|
||||
self.selected += 1
|
||||
self.left.scroll_down(y)
|
||||
self.update_task_tree()
|
||||
else:
|
||||
self.right.scroll_down(y)
|
||||
self.right.screen.noutrefresh(self.right.first_visible, 0, *self.right.region.box)
|
||||
elif ch == curses.KEY_UP:
|
||||
if self.scroll == 'left' and self.selected > 0:
|
||||
self.selected -= 1
|
||||
self.left.scroll_up(y)
|
||||
self.update_task_tree()
|
||||
elif self.scroll == 'right':
|
||||
self.right.scroll_up(y)
|
||||
self.right.screen.noutrefresh(self.right.first_visible, 0, *self.right.region.box)
|
||||
elif ch == curses.ascii.NL:
|
||||
if self.scroll == 'left' and len(self.tasks) > 0:
|
||||
task = self.tasks[self.selected]
|
||||
if task.state == TaskState.READY:
|
||||
self.complete_task(task)
|
||||
self.draw()
|
||||
|
||||
def resize(self):
|
||||
self.left.resize()
|
||||
self.right.resize()
|
|
@ -0,0 +1 @@
|
|||
from .engine import BpmnEngine
|
|
@ -0,0 +1,100 @@
|
|||
import curses
|
||||
import logging
|
||||
|
||||
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
|
||||
from SpiffWorkflow.bpmn.specs.mixins.events.event_types import CatchingEvent
|
||||
from SpiffWorkflow.bpmn import BpmnWorkflow
|
||||
from SpiffWorkflow.bpmn.script_engine import PythonScriptEngine
|
||||
from SpiffWorkflow import TaskState
|
||||
|
||||
|
||||
logger = logging.getLogger('spiff_engine')
|
||||
|
||||
class BpmnEngine:
|
||||
|
||||
def __init__(self, parser, serializer, handlers=None, script_engine=None):
|
||||
|
||||
self.parser = parser
|
||||
self.serializer = serializer
|
||||
self._script_engine = script_engine or PythonScriptEngine()
|
||||
self._handlers = handlers or {}
|
||||
|
||||
def handler(self, task):
|
||||
handler = self._handlers.get(task.task_spec.__class__)
|
||||
if handler is not None:
|
||||
return handler(task)
|
||||
|
||||
def add_spec(self, process_id, bpmn_files, dmn_files):
|
||||
self.add_files(bpmn_files, dmn_files)
|
||||
try:
|
||||
spec = self.parser.get_spec(process_id)
|
||||
dependencies = self.parser.get_subprocess_specs(process_id)
|
||||
except ValidationException as exc:
|
||||
# Clear the process parsers so the files can be re-added
|
||||
# There's probably plenty of other stuff that should be here
|
||||
# However, our parser makes me mad so not investigating further at this time
|
||||
self.parser.process_parsers = {}
|
||||
raise exc
|
||||
spec_id = self.serializer.create_workflow_spec(spec, dependencies)
|
||||
logger.info(f'Added {process_id} with id {spec_id}')
|
||||
return spec_id
|
||||
|
||||
def add_collaboration(self, collaboration_id, bpmn_files, dmn_files=None):
|
||||
self.add_files(bpmn_files, dmn_files)
|
||||
try:
|
||||
spec, dependencies = self.parser.get_collaboration(collaboration_id)
|
||||
except ValidationException as exc:
|
||||
self.parser.process_parsers = {}
|
||||
raise exc
|
||||
spec_id = self.serializer.create_workflow_spec(spec, dependencies)
|
||||
logger.info(f'Added {collaboration_id} with id {spec_id}')
|
||||
return spec_id
|
||||
|
||||
def add_files(self, bpmn_files, dmn_files):
|
||||
self.parser.add_bpmn_files(bpmn_files)
|
||||
if dmn_files is not None:
|
||||
self.parser.add_dmn_files(dmn_files)
|
||||
|
||||
def list_specs(self):
|
||||
return self.serializer.list_specs()
|
||||
|
||||
def delete_workflow_spec(self, spec_id):
|
||||
self.serializer.delete_workflow_spec(spec_id)
|
||||
logger.info(f'Deleted workflow spec with id {spec_id}')
|
||||
|
||||
def start_workflow(self, spec_id):
|
||||
spec, sp_specs = self.serializer.get_workflow_spec(spec_id)
|
||||
wf = BpmnWorkflow(spec, sp_specs, script_engine=self._script_engine)
|
||||
wf_id = self.serializer.create_workflow(wf, spec_id)
|
||||
logger.info(f'Created workflow with id {wf_id}')
|
||||
return wf_id
|
||||
|
||||
def get_workflow(self, wf_id):
|
||||
wf = self.serializer.get_workflow(wf_id)
|
||||
wf.script_engine = self._script_engine
|
||||
return wf
|
||||
|
||||
def update_workflow(self, workflow, wf_id):
|
||||
logger.info(f'Saved workflow {wf_id}')
|
||||
self.serializer.update_workflow(workflow, wf_id)
|
||||
|
||||
def list_workflows(self, include_completed=False):
|
||||
return self.serializer.list_workflows(include_completed)
|
||||
|
||||
def delete_workflow(self, wf_id):
|
||||
self.serializer.delete_workflow(wf_id)
|
||||
logger.info(f'Deleted workflow with id {wf_id}')
|
||||
|
||||
def run_until_user_input_required(self, workflow):
|
||||
task = workflow.get_next_task(state=TaskState.READY, manual=False)
|
||||
while task is not None:
|
||||
task.run()
|
||||
self.run_ready_events(workflow)
|
||||
task = workflow.get_next_task(state=TaskState.READY, manual=False)
|
||||
|
||||
def run_ready_events(self, workflow):
|
||||
workflow.refresh_waiting_tasks()
|
||||
task = workflow.get_next_task(state=TaskState.READY, spec_class=CatchingEvent)
|
||||
while task is not None:
|
||||
task.run()
|
||||
task = workflow.get_next_task(state=TaskState.READY, spec_class=CatchingEvent)
|
|
@ -0,0 +1 @@
|
|||
from .serializer import FileSerializer
|
|
@ -0,0 +1,117 @@
|
|||
import json, re
|
||||
import os
|
||||
import logging
|
||||
from uuid import uuid4, UUID
|
||||
from datetime import datetime
|
||||
|
||||
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer
|
||||
from SpiffWorkflow.bpmn.serializer.default.workflow import BpmnWorkflowConverter, BpmnSubWorkflowConverter
|
||||
from SpiffWorkflow.bpmn.serializer.default.process_spec import BpmnProcessSpecConverter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FileSerializer(BpmnWorkflowSerializer):
|
||||
|
||||
@staticmethod
|
||||
def initialize(dirname):
|
||||
try:
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
os.mkdir(os.path.join(dirname, 'spec'))
|
||||
os.mkdir(os.path.join(dirname, 'instance'))
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
def __init__(self, dirname, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.dirname = dirname
|
||||
self.fmt = {'indent': 2, 'separators': [', ', ': ']}
|
||||
|
||||
def create_workflow_spec(self, spec, dependencies):
|
||||
|
||||
spec_dir = os.path.join(self.dirname, 'spec')
|
||||
if spec.file is not None:
|
||||
dirname = os.path.join(spec_dir, os.path.dirname(spec.file), spec.name)
|
||||
else:
|
||||
dirname = os.path.join(spec_dir, spec.name)
|
||||
filename = os.path.join(dirname, f'{spec.name}.json')
|
||||
try:
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
with open(filename, 'x') as fh:
|
||||
fh.write(json.dumps(self.to_dict(spec), **self.fmt))
|
||||
if len(dependencies) > 0:
|
||||
os.mkdir(os.path.join(dirname, 'dependencies'))
|
||||
for name, sp in dependencies.items():
|
||||
with open(os.path.join(dirname, 'dependencies', f'{name}.json'), 'w') as fh:
|
||||
fh.write(json.dumps(self.to_dict(sp), **self.fmt))
|
||||
except FileExistsError:
|
||||
pass
|
||||
return filename
|
||||
|
||||
def delete_workflow_spec(self, filename):
|
||||
try:
|
||||
os.remove(filename)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def get_workflow_spec(self, filename, **kwargs):
|
||||
dirname = os.path.dirname(filename)
|
||||
with open(filename) as fh:
|
||||
spec = self.from_dict(json.loads(fh.read()))
|
||||
subprocess_specs = {}
|
||||
depdir = os.path.join(os.path.dirname(filename), 'dependencies')
|
||||
if os.path.exists(depdir):
|
||||
for f in os.listdir(depdir):
|
||||
name = re.sub('\.json$', '', os.path.basename(f))
|
||||
with open(os.path.join(depdir, f)) as fh:
|
||||
subprocess_specs[name] = self.from_dict(json.loads(fh.read()))
|
||||
return spec, subprocess_specs
|
||||
|
||||
def list_specs(self):
|
||||
library = []
|
||||
for root, dirs, files in os.walk(os.path.join(self.dirname, 'spec')):
|
||||
if 'dependencies' not in root:
|
||||
for f in files:
|
||||
filename = os.path.join(root, f)
|
||||
name = re.sub('\.json$', '', os.path.basename(f))
|
||||
path = re.sub(os.path.join(self.dirname, 'spec'), '', filename).lstrip('/')
|
||||
library.append((filename, name, path))
|
||||
return library
|
||||
|
||||
def create_workflow(self, workflow, spec_id):
|
||||
name = re.sub('\.json$', '', os.path.basename(spec_id))
|
||||
dirname = os.path.join(self.dirname, 'instance', name)
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
wf_id = uuid4()
|
||||
with open(os.path.join(dirname, f'{wf_id}.json'), 'w') as fh:
|
||||
fh.write(json.dumps(self.to_dict(workflow), **self.fmt))
|
||||
return os.path.join(dirname, f'{wf_id}.json')
|
||||
|
||||
def get_workflow(self, filename, **kwargs):
|
||||
with open(filename) as fh:
|
||||
return self.from_dict(json.loads(fh.read()))
|
||||
|
||||
def update_workflow(self, workflow, filename):
|
||||
with open(filename, 'w') as fh:
|
||||
fh.write(json.dumps(self.to_dict(workflow), **self.fmt))
|
||||
|
||||
def delete_workflow(self, filename):
|
||||
try:
|
||||
os.remove(filename)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def list_workflows(self, include_completed):
|
||||
instances = []
|
||||
for root, dirs, files in os.walk(os.path.join(self.dirname, 'instance')):
|
||||
for f in files:
|
||||
filename = os.path.join(root, f)
|
||||
name = os.path.split(os.path.dirname(filename))[-1]
|
||||
stat = os.lstat(filename)
|
||||
created = datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%^m-%d %H:%M:%S')
|
||||
updated = datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%^m-%d %H:%M:%S')
|
||||
# '?' is active tasks -- we can't know this unless we reydrate the workflow
|
||||
# We also have to lose the ability to filter out completed workflows
|
||||
instances.append((filename, name, '-', created, updated, '-'))
|
||||
return instances
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from .serializer import (
|
||||
SqliteSerializer,
|
||||
WorkflowConverter,
|
||||
SubworkflowConverter,
|
||||
WorkflowSpecConverter,
|
||||
)
|
|
@ -0,0 +1,221 @@
|
|||
create table if not exists _workflow_spec (
|
||||
id uuid primary key,
|
||||
serialization json
|
||||
);
|
||||
|
||||
create table if not exists _task_spec (
|
||||
workflow_spec_id uuid references _workflow_spec (id) on delete cascade,
|
||||
name text generated always as (serialization->>'name') stored,
|
||||
serialization json
|
||||
);
|
||||
create index if not exists task_spec_workflow on _task_spec (workflow_spec_id);
|
||||
create index if not exists task_spec_name on _task_spec (name);
|
||||
|
||||
create view if not exists workflow_spec as
|
||||
with
|
||||
ts as (select workflow_spec_id, json_group_object(name, json(serialization)) task_specs from _task_spec group by workflow_spec_id)
|
||||
select
|
||||
id, json_insert(serialization, '$.task_specs', json(task_specs)) serialization
|
||||
from (
|
||||
select _workflow_spec.id id, serialization, task_specs from _workflow_spec join ts on _workflow_spec.id=ts.workflow_spec_id
|
||||
);
|
||||
|
||||
create trigger if not exists insert_workflow_spec instead of insert on workflow_spec
|
||||
begin
|
||||
insert into _workflow_spec (id, serialization) values (new.id, json_remove(new.serialization, '$.task_specs'));
|
||||
insert into _task_spec (workflow_spec_id, serialization) select new.id, value from json_each(new.serialization->>'task_specs');
|
||||
end;
|
||||
|
||||
create trigger if not exists delete_workflow_spec instead of delete on workflow_spec
|
||||
begin
|
||||
delete from _workflow_spec where id=old.id;
|
||||
delete from _task_spec where workflow_spec_id=old.id;
|
||||
end;
|
||||
|
||||
create table if not exists _spec_dependency (
|
||||
parent_id uuid references _workflow_spec (id) on delete cascade,
|
||||
child_id uuid references _workflow_spec (id) on delete cascade
|
||||
);
|
||||
create index if not exists spec_parent on _spec_dependency (parent_id);
|
||||
create index if not exists spec_child on _spec_dependency (child_id);
|
||||
|
||||
create view if not exists spec_dependency as
|
||||
with recursive
|
||||
dependency(root, descendant, depth) as (
|
||||
select parent_id, child_id, 0 from _spec_dependency
|
||||
union
|
||||
select root, child_id, depth + 1 from _spec_dependency, dependency where parent_id=dependency.descendant
|
||||
)
|
||||
select root, descendant, depth, serialization from dependency join workflow_spec on dependency.descendant=workflow_spec.id;
|
||||
|
||||
create table if not exists _workflow (
|
||||
id uuid primary key,
|
||||
workflow_spec_id uuid references _workflow_spec (id),
|
||||
serialization json
|
||||
);
|
||||
|
||||
create table if not exists _task (
|
||||
workflow_id references _workflow (id) on delete cascade,
|
||||
id uuid generated always as (serialization->>'id') stored unique,
|
||||
serialization json
|
||||
);
|
||||
create index if not exists task_id on _task (id);
|
||||
create index if not exists task_workflow_id on _task (workflow_id);
|
||||
|
||||
create table if not exists _task_data (
|
||||
workflow_id uuid references _workflow (id) on delete cascade,
|
||||
task_id uuid,
|
||||
name text,
|
||||
value json,
|
||||
last_updated timestamp default current_timestamp,
|
||||
unique (task_id, name)
|
||||
);
|
||||
create index if not exists task_data_id on _task_data (task_id);
|
||||
create index if not exists task_data_name on _task_data (name);
|
||||
|
||||
create view if not exists task as
|
||||
with data as (
|
||||
select task_id, json_group_object(name, iif(json_valid(value), json(value), value)) task_data
|
||||
from _task_data group by task_id
|
||||
)
|
||||
select _task.workflow_id, _task.id, json_insert(serialization, '$.data', json(ifnull(task_data, '{}'))) serialization
|
||||
from _task left join data on _task.id=data.task_id;
|
||||
|
||||
create trigger if not exists insert_task instead of insert on task
|
||||
begin
|
||||
insert into _task (workflow_id, serialization) values (new.workflow_id, json_remove(new.serialization, '$.data'));
|
||||
insert into _task_data (workflow_id, task_id, name, value)
|
||||
select new.workflow_id, new.serialization->>'id', key, value from json_each(new.serialization->>'data') where true
|
||||
on conflict (task_id, name) do nothing;
|
||||
end;
|
||||
|
||||
create trigger if not exists update_task instead of update on task
|
||||
begin
|
||||
update _task set serialization=json_remove(new.serialization, '$.data') where _task.id=new.serialization->>'id';
|
||||
delete from _task_data where task_id=new.id and name not in (select key from json_each(new.serialization->>'data'));
|
||||
insert into _task_data (workflow_id, task_id, name, value)
|
||||
select new.workflow_id, new.serialization->>'id', key, value from json_each(new.serialization->>'data') where true
|
||||
on conflict (task_id, name) do update set value=excluded.value;
|
||||
end;
|
||||
|
||||
create trigger if not exists delete_task instead of delete on task
|
||||
begin
|
||||
delete from _task where id=old.id;
|
||||
delete from _task_data where task_id=old.id;
|
||||
end;
|
||||
|
||||
create table if not exists _workflow_data (
|
||||
workflow_id uuid references _workflow (id) on delete cascade,
|
||||
name text,
|
||||
value json,
|
||||
last_updated timestamp default current_timestamp,
|
||||
unique (workflow_id, name)
|
||||
);
|
||||
create index if not exists workflow_data_id on _workflow_data (workflow_id);
|
||||
create index if not exists wokflow_data_name on _workflow_data (name);
|
||||
|
||||
create view if not exists workflow as
|
||||
with
|
||||
tasks as (select workflow_id, json_group_object(id, json(serialization)) tasks from task group by workflow_id),
|
||||
data as (select workflow_id, json_group_object(name, iif(json_valid(value), json(value), value)) data from _workflow_data group by workflow_id)
|
||||
select
|
||||
_workflow.id,
|
||||
_workflow.workflow_spec_id,
|
||||
json_insert(
|
||||
json_insert(
|
||||
json_insert(
|
||||
_workflow.serialization,
|
||||
'$.data',
|
||||
json(ifnull(data, '{}'))
|
||||
),
|
||||
'$.tasks',
|
||||
json(tasks)
|
||||
),
|
||||
'$.spec',
|
||||
json(workflow_spec.serialization)
|
||||
) serialization
|
||||
from _workflow
|
||||
left join data on _workflow.id=data.workflow_id
|
||||
join tasks on _workflow.id=tasks.workflow_id
|
||||
join workflow_spec on _workflow.workflow_spec_id=workflow_spec.id;
|
||||
|
||||
create trigger if not exists insert_workflow instead of insert on workflow
|
||||
begin
|
||||
insert into _workflow (id, workflow_spec_id, serialization)
|
||||
values (
|
||||
new.id,
|
||||
new.workflow_spec_id,
|
||||
json_remove(json_remove(new.serialization, '$.tasks'), '$.data')
|
||||
);
|
||||
insert into task (workflow_id, serialization) select new.id, value from json_each(new.serialization->>'tasks');
|
||||
insert into _workflow_data (workflow_id, name, value) select new.id, key, value from json_each(new.serialization->>'data');
|
||||
end;
|
||||
|
||||
create trigger if not exists update_workflow instead of update on workflow
|
||||
begin
|
||||
update _workflow set serialization=json_remove(json_remove(new.serialization, '$.tasks'), '$.data') where _workflow.id=new.id;
|
||||
delete from task where workflow_id=new.id and id not in (select value->>'id' from json_each(new.serialization->>'tasks'));
|
||||
update task set serialization=value from (
|
||||
select value, serialization from json_each(new.serialization->>'tasks') t join _task on value->>'id'=_task.id
|
||||
) t
|
||||
where value->>'id'=task.id and value->>'last_state_change' > t.serialization->>'last_state_change';
|
||||
insert into task (workflow_id, serialization) select new.id, value from json_each(new.serialization->>'tasks')
|
||||
where value->>'id' not in (select id from _task where workflow_id=new.id);
|
||||
delete from _workflow_data where workflow_id=new.id and name not in (select key from json_each(new.serialization->>'data'));
|
||||
insert into _workflow_data (workflow_id, name, value) select new.id, key, value from json_each(new.serialization->>'$.data') where true
|
||||
on conflict (workflow_id, name) do update set value=excluded.value;
|
||||
end;
|
||||
|
||||
create trigger if not exists delete_workflow instead of delete on workflow
|
||||
begin
|
||||
delete from _workflow where id=old.id;
|
||||
delete from _task where workflow_id=old.id;
|
||||
delete from _workflow_data where workflow_id=old.id;
|
||||
end;
|
||||
|
||||
create view if not exists workflow_dependency as
|
||||
with recursive
|
||||
subworkflow as (select workflow_id, id from _task where id in (select id from _workflow)),
|
||||
dependency(root, descendant, depth) as (
|
||||
select workflow_id, id, 1 from subworkflow
|
||||
union
|
||||
select workflow_id, dependency.descendant, depth + 1 from subworkflow, dependency where subworkflow.id=dependency.root
|
||||
)
|
||||
select root, descendant, depth, serialization from dependency join workflow on dependency.descendant=workflow.id;
|
||||
|
||||
create view if not exists spec_library as
|
||||
select id, serialization->>'name' name, serialization->>'file' filename from workflow_spec
|
||||
where id not in (select distinct child_id from _spec_dependency);
|
||||
|
||||
create table if not exists instance (
|
||||
id uuid primary key references _workflow (id) on delete cascade,
|
||||
bullshit text,
|
||||
spec_name text,
|
||||
active_tasks int,
|
||||
started timestamp,
|
||||
updated timestamp,
|
||||
ended timestamp
|
||||
);
|
||||
|
||||
create trigger if not exists create_instance instead of insert on workflow
|
||||
begin
|
||||
insert into instance (id, spec_name, active_tasks, started) select * from (
|
||||
select new.id, name, count(value), current_timestamp
|
||||
from (select name from spec_library where id=new.workflow_spec_id), json_each(new.serialization->>'tasks')
|
||||
where value->>'state' between 8 and 32
|
||||
) where new.serialization->>'typename'='BpmnWorkflow';
|
||||
end;
|
||||
|
||||
create trigger if not exists update_instance instead of update on workflow
|
||||
begin
|
||||
update instance set updated=current_timestamp, ended=t.ended from (
|
||||
select iif(count(value)=0, current_timestamp, null) ended
|
||||
from json_each(new.serialization->>'tasks') where value->>'state' < 64
|
||||
) t where id=new.id;
|
||||
end;
|
||||
|
||||
create trigger if not exists delete_instance instead of delete on workflow
|
||||
begin
|
||||
delete from instance where id=old.id;
|
||||
end;
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
import sqlite3, json
|
||||
import os
|
||||
import logging
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer
|
||||
from SpiffWorkflow.bpmn.serializer.default.workflow import BpmnWorkflowConverter, BpmnSubWorkflowConverter
|
||||
from SpiffWorkflow.bpmn.serializer.default.process_spec import BpmnProcessSpecConverter
|
||||
from SpiffWorkflow.bpmn.specs.mixins.subworkflow_task import SubWorkflowTask
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WorkflowConverter(BpmnWorkflowConverter):
|
||||
|
||||
def to_dict(self, workflow):
|
||||
dct = super(BpmnWorkflowConverter, self).to_dict(workflow)
|
||||
dct['bpmn_events'] = self.registry.convert(workflow.bpmn_events)
|
||||
dct['subprocesses'] = {}
|
||||
dct['tasks'] = list(dct['tasks'].values())
|
||||
return dct
|
||||
|
||||
class SubworkflowConverter(BpmnSubWorkflowConverter):
|
||||
|
||||
def to_dict(self, workflow):
|
||||
dct = super().to_dict(workflow)
|
||||
dct['tasks'] = list(dct['tasks'].values())
|
||||
return dct
|
||||
|
||||
class WorkflowSpecConverter(BpmnProcessSpecConverter):
|
||||
|
||||
def to_dict(self, spec):
|
||||
dct = super().to_dict(spec)
|
||||
dct['task_specs'] = list(dct['task_specs'].values())
|
||||
return dct
|
||||
|
||||
|
||||
class SqliteSerializer(BpmnWorkflowSerializer):
|
||||
|
||||
@staticmethod
|
||||
def initialize(db):
|
||||
with open(os.path.join(os.path.dirname(__file__), 'schema-sqlite.sql')) as fh:
|
||||
db.executescript(fh.read())
|
||||
db.commit()
|
||||
|
||||
def __init__(self, dbname, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.dbname = dbname
|
||||
|
||||
def create_workflow_spec(self, spec, dependencies):
|
||||
spec_id, new = self.execute(self._create_workflow_spec, spec)
|
||||
if new and len(dependencies) > 0:
|
||||
pairs = self.get_spec_dependencies(spec_id, spec, dependencies)
|
||||
# This handles the case where the participant requires an event to be kicked off
|
||||
added = list(map(lambda p: p[1], pairs))
|
||||
for name, child in dependencies.items():
|
||||
child_id, new_child = self.execute(self._create_workflow_spec, child)
|
||||
if new_child:
|
||||
pairs |= self.get_spec_dependencies(child_id, child, dependencies)
|
||||
pairs.add((spec_id, child_id))
|
||||
self.execute(self._set_spec_dependencies, pairs)
|
||||
return spec_id
|
||||
|
||||
def get_spec_dependencies(self, parent_id, parent, dependencies):
|
||||
# There ought to be an option in the parser to do this
|
||||
pairs = set()
|
||||
for task_spec in filter(lambda ts: isinstance(ts, SubWorkflowTask), parent.task_specs.values()):
|
||||
child = dependencies.get(task_spec.spec)
|
||||
child_id, new = self.execute(self._create_workflow_spec, child)
|
||||
pairs.add((parent_id, child_id))
|
||||
if new:
|
||||
pairs |= self.get_spec_dependencies(child_id, child, dependencies)
|
||||
return pairs
|
||||
|
||||
def get_workflow_spec(self, spec_id, include_dependencies=True):
|
||||
return self.execute(self._get_workflow_spec, spec_id, include_dependencies)
|
||||
|
||||
def list_specs(self):
|
||||
return self.execute(self._list_specs)
|
||||
|
||||
def delete_workflow_spec(self, spec_id):
|
||||
return self.execute(self._delete_workflow_spec, spec_id)
|
||||
|
||||
def create_workflow(self, workflow, spec_id):
|
||||
return self.execute(self._create_workflow, workflow, spec_id)
|
||||
|
||||
def get_workflow(self, wf_id, include_dependencies=True):
|
||||
return self.execute(self._get_workflow, wf_id, include_dependencies)
|
||||
|
||||
def update_workflow(self, workflow, wf_id):
|
||||
return self.execute(self._update_workflow, workflow, wf_id)
|
||||
|
||||
def list_workflows(self, include_completed=False):
|
||||
return self.execute(self._list_workflows, include_completed)
|
||||
|
||||
def delete_workflow(self, wf_id):
|
||||
return self.execute(self._delete_workflow, wf_id)
|
||||
|
||||
def _create_workflow_spec(self, cursor, spec):
|
||||
cursor.execute(
|
||||
"select id, false from workflow_spec where serialization->>'file'=? and serialization->>'name'=?",
|
||||
(spec.file, spec.name)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
dct = self.to_dict(spec)
|
||||
spec_id = uuid4()
|
||||
cursor.execute("insert into workflow_spec (id, serialization) values (?, ?)", (spec_id, dct))
|
||||
return spec_id, True
|
||||
else:
|
||||
return row
|
||||
|
||||
def _set_spec_dependencies(self, cursor, values):
|
||||
cursor.executemany("insert into _spec_dependency (parent_id, child_id) values (?, ?)", values)
|
||||
|
||||
def _get_workflow_spec(self, cursor, spec_id, include_dependencies):
|
||||
cursor.execute("select serialization as 'serialization [json]' from workflow_spec where id=?", (spec_id, ))
|
||||
spec = self.from_dict(cursor.fetchone()[0])
|
||||
subprocess_specs = {}
|
||||
if include_dependencies:
|
||||
subprocess_specs = self._get_subprocess_specs(cursor, spec_id)
|
||||
return spec, subprocess_specs
|
||||
|
||||
def _get_subprocess_specs(self, cursor, spec_id):
|
||||
subprocess_specs = {}
|
||||
cursor.execute(
|
||||
"select serialization->>'name', serialization as 'serialization [json]' from spec_dependency where root=?",
|
||||
(spec_id, )
|
||||
)
|
||||
for name, serialization in cursor:
|
||||
subprocess_specs[name] = self.from_dict(serialization)
|
||||
return subprocess_specs
|
||||
|
||||
def _list_specs(self, cursor):
|
||||
cursor.execute("select id, name, filename from spec_library")
|
||||
return cursor.fetchall()
|
||||
|
||||
def _delete_workflow_spec(self, cursor, spec_id):
|
||||
try:
|
||||
cursor.execute("delete from workflow_spec where id=?", (spec_id, ))
|
||||
except sqlite3.IntegrityError:
|
||||
logger.warning(f'Unable to delete spec {spec_id} because it is used by existing workflows')
|
||||
|
||||
def _create_workflow(self, cursor, workflow, spec_id):
|
||||
dct = super().to_dict(workflow)
|
||||
wf_id = uuid4()
|
||||
stmt = "insert into workflow (id, workflow_spec_id, serialization) values (?, ?, ?)"
|
||||
cursor.execute(stmt, (wf_id, spec_id, dct))
|
||||
if len(workflow.subprocesses) > 0:
|
||||
cursor.execute("select serialization->>'name', descendant from spec_dependency where root=?", (spec_id, ))
|
||||
dependencies = dict((name, id) for name, id in cursor)
|
||||
for sp_id, sp in workflow.subprocesses.items():
|
||||
cursor.execute(stmt, (sp_id, dependencies[sp.spec.name], self.to_dict(sp)))
|
||||
return wf_id
|
||||
|
||||
def _get_workflow(self, cursor, wf_id, include_dependencies):
|
||||
cursor.execute("select workflow_spec_id, serialization as 'serialization [json]' from workflow where id=?", (wf_id, ))
|
||||
row = cursor.fetchone()
|
||||
spec_id, workflow = row[0], self.from_dict(row[1])
|
||||
if include_dependencies:
|
||||
workflow.subprocess_specs = self._get_subprocess_specs(cursor, spec_id)
|
||||
cursor.execute(
|
||||
"select descendant as 'id [uuid]', serialization as 'serialization [json]' from workflow_dependency where root=? order by depth",
|
||||
(wf_id, )
|
||||
)
|
||||
for sp_id, sp in cursor:
|
||||
task = workflow.get_task_from_id(sp_id)
|
||||
workflow.subprocesses[sp_id] = self.from_dict(sp, task=task, top_workflow=workflow)
|
||||
return workflow
|
||||
|
||||
def _update_workflow(self, cursor, workflow, wf_id):
|
||||
dct = self.to_dict(workflow)
|
||||
cursor.execute("select descendant as 'id [uuid]' from workflow_dependency where root=?", (wf_id, ))
|
||||
dependencies = [row[0] for row in cursor]
|
||||
cursor.execute(
|
||||
"select serialization->>'name', descendant as 'id [uuid]' from spec_dependency where root=(select workflow_spec_id from _workflow where id=?)",
|
||||
(wf_id, )
|
||||
)
|
||||
spec_dependencies = dict((name, spec_id) for name, spec_id in cursor)
|
||||
stmt = "update workflow set serialization=? where id=?"
|
||||
cursor.execute(stmt, (dct, wf_id))
|
||||
for sp_id, sp in workflow.subprocesses.items():
|
||||
sp_dct = self.to_dict(sp)
|
||||
if sp_id in dependencies:
|
||||
cursor.execute(stmt, (sp_dct, sp_id))
|
||||
else:
|
||||
cursor.execute(
|
||||
"insert into workflow (id, workflow_spec_id, serialization) values (?, ?, ?)",
|
||||
(sp_id, spec_dependencies[sp.spec.name], sp_dct)
|
||||
)
|
||||
|
||||
def _list_workflows(self, cursor, include_completed):
|
||||
if include_completed:
|
||||
query = "select id, spec_name, active_tasks, started, updated, ended from instance"
|
||||
else:
|
||||
query = "select id, spec_name, active_tasks, started, updated, ended from instance where ended is null"
|
||||
cursor.execute(query)
|
||||
return cursor.fetchall()
|
||||
|
||||
def _delete_workflow(self, cursor, wf_id):
|
||||
cursor.execute("delete from workflow where id=?", (wf_id, ))
|
||||
|
||||
def execute(self, func, *args, **kwargs):
|
||||
|
||||
conn = sqlite3.connect(self.dbname, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
|
||||
conn.execute("pragma foreign_keys=on")
|
||||
sqlite3.register_adapter(UUID, lambda v: str(v))
|
||||
sqlite3.register_converter("uuid", lambda s: UUID(s.decode('utf-8')))
|
||||
sqlite3.register_adapter(dict, lambda v: json.dumps(v))
|
||||
sqlite3.register_converter("json", lambda s: json.loads(s))
|
||||
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
rv = func(cursor, *args, **kwargs)
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
logger.error(str(exc), exc_info=True)
|
||||
conn.rollback()
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return rv
|
|
@ -0,0 +1,63 @@
|
|||
import os, json
|
||||
import logging
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
from ..curses_ui.user_input import Field
|
||||
|
||||
forms_dir = 'bpmn/tutorial/forms'
|
||||
|
||||
class TaskHandler:
|
||||
|
||||
def __init__(self, task):
|
||||
self.task = task
|
||||
|
||||
def get_instructions(self):
|
||||
instructions = f'{self.task.task_spec.bpmn_name}\n\n'
|
||||
text = self.task.task_spec.extensions.get('instructionsForEndUser')
|
||||
if text is not None:
|
||||
template = Template(text)
|
||||
instructions += template.render(self.task.data)
|
||||
instructions += '\n\n'
|
||||
return instructions
|
||||
|
||||
def on_complete(self, results):
|
||||
self.task.run()
|
||||
|
||||
|
||||
class ManualTaskHandler(TaskHandler):
|
||||
|
||||
def get_configuration(self):
|
||||
return self.get_instructions(), []
|
||||
|
||||
|
||||
class UserTaskHandler(TaskHandler):
|
||||
|
||||
def get_configuration(self):
|
||||
return self.get_instructions(), self.get_fields()
|
||||
|
||||
def create_field(self, name, config):
|
||||
if 'oneOf' in config:
|
||||
option_map = dict([ (v['title'], v['const']) for v in config['oneOf'] ])
|
||||
label = f'{config["title"]} ' + '(' + ', '.join(option_map) + ')'
|
||||
def validate(value):
|
||||
if value not in option_map:
|
||||
raise Exception(f'Invalid option: {value}')
|
||||
else:
|
||||
return option_map[value]
|
||||
field = Field(name, label, lambda v: v, validate, '')
|
||||
elif config['type'] == 'integer':
|
||||
field = Field(name, config['title'], lambda v: str(v) if v is not None else '', int, None)
|
||||
else:
|
||||
field = Field(name, config['title'], str, str, '')
|
||||
return field
|
||||
|
||||
def get_fields(self):
|
||||
filename = self.task.task_spec.extensions['properties']['formJsonSchemaFilename']
|
||||
schema = json.load(open(os.path.join(forms_dir, filename)))
|
||||
return [self.create_field(name, config) for name, config in schema['properties'].items()]
|
||||
|
||||
def on_complete(self, results):
|
||||
self.task.set_data(**results)
|
||||
super().on_complete(results)
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import logging
|
||||
import datetime
|
||||
|
||||
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser
|
||||
from SpiffWorkflow.spiff.specs.defaults import UserTask, ManualTask
|
||||
from SpiffWorkflow.spiff.serializer.config import SPIFF_CONFIG
|
||||
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow, BpmnSubWorkflow
|
||||
from SpiffWorkflow.bpmn.specs.bpmn_process_spec import BpmnProcessSpec
|
||||
from SpiffWorkflow.bpmn.specs.mixins.none_task import NoneTask
|
||||
from SpiffWorkflow.bpmn.script_engine import PythonScriptEngine, TaskDataEnvironment
|
||||
|
||||
from ..serializer.file import FileSerializer
|
||||
from ..engine import BpmnEngine
|
||||
from .curses_handlers import UserTaskHandler, ManualTaskHandler
|
||||
|
||||
from .product_info import (
|
||||
ProductInfo,
|
||||
product_info_to_dict,
|
||||
product_info_from_dict,
|
||||
lookup_product_info,
|
||||
lookup_shipping_cost,
|
||||
)
|
||||
|
||||
logger = logging.getLogger('spiff_engine')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
spiff_logger = logging.getLogger('spiff')
|
||||
spiff_logger.setLevel(logging.INFO)
|
||||
|
||||
dirname = 'wfdata'
|
||||
FileSerializer.initialize(dirname)
|
||||
|
||||
registry = FileSerializer.configure(SPIFF_CONFIG)
|
||||
registry.register(ProductInfo, product_info_to_dict, product_info_from_dict)
|
||||
|
||||
serializer = FileSerializer(dirname, registry=registry)
|
||||
|
||||
parser = SpiffBpmnParser()
|
||||
|
||||
handlers = {
|
||||
UserTask: UserTaskHandler,
|
||||
ManualTask: ManualTaskHandler,
|
||||
NoneTask: ManualTaskHandler,
|
||||
}
|
||||
|
||||
script_env = TaskDataEnvironment({
|
||||
'datetime': datetime,
|
||||
'lookup_product_info': lookup_product_info,
|
||||
'lookup_shipping_cost': lookup_shipping_cost,
|
||||
})
|
||||
script_engine = PythonScriptEngine(script_env)
|
||||
|
||||
engine = BpmnEngine(parser, serializer, handlers, script_engine)
|
|
@ -1,17 +1,18 @@
|
|||
from SpiffWorkflow.bpmn.specs.event_definitions import TimerEventDefinition, NoneEventDefinition
|
||||
from SpiffWorkflow.bpmn.specs.mixins.events.start_event import StartEvent
|
||||
from SpiffWorkflow.spiff.specs.spiff_task import SpiffBpmnTask
|
||||
from SpiffWorkflow.bpmn.specs.event_definitions import NoneEventDefinition
|
||||
from SpiffWorkflow.bpmn.specs.event_definitions.timer import TimerEventDefinition
|
||||
from SpiffWorkflow.bpmn.specs.mixins import StartEventMixin
|
||||
from SpiffWorkflow.spiff.specs import SpiffBpmnTask
|
||||
|
||||
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer
|
||||
from SpiffWorkflow.bpmn.serializer.task_spec import StartEventConverter
|
||||
from SpiffWorkflow.bpmn.serializer import BpmnWorkflowSerializer
|
||||
from SpiffWorkflow.bpmn.serializer.default import EventConverter
|
||||
from SpiffWorkflow.spiff.serializer.task_spec import SpiffBpmnTaskConverter
|
||||
from SpiffWorkflow.spiff.serializer.config import SPIFF_SPEC_CONFIG
|
||||
from SpiffWorkflow.spiff.serializer import DEFAULT_CONFIG
|
||||
|
||||
from SpiffWorkflow.spiff.parser import SpiffBpmnParser
|
||||
from SpiffWorkflow.spiff.parser.event_parsers import StartEventParser
|
||||
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser
|
||||
from SpiffWorkflow.bpmn.parser.util import full_tag
|
||||
|
||||
class CustomStartEvent(StartEvent, SpiffBpmnTask):
|
||||
class CustomStartEvent(StartEventMixin, SpiffBpmnTask):
|
||||
|
||||
def __init__(self, wf_spec, bpmn_id, event_definition, **kwargs):
|
||||
|
||||
|
@ -22,7 +23,6 @@ class CustomStartEvent(StartEvent, SpiffBpmnTask):
|
|||
super().__init__(wf_spec, bpmn_id, event_definition, **kwargs)
|
||||
self.timer_event = None
|
||||
|
||||
|
||||
class CustomStartEventConverter(SpiffBpmnTaskConverter):
|
||||
|
||||
def __init__(self, registry):
|
||||
|
@ -36,14 +36,10 @@ class CustomStartEventConverter(SpiffBpmnTaskConverter):
|
|||
dct['event_definition'] = self.registry.convert(spec.event_definition)
|
||||
return dct
|
||||
|
||||
|
||||
SPIFF_SPEC_CONFIG['task_specs'].remove(StartEventConverter)
|
||||
SPIFF_SPEC_CONFIG['task_specs'].append(CustomStartEventConverter)
|
||||
|
||||
wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter(SPIFF_SPEC_CONFIG)
|
||||
serializer = BpmnWorkflowSerializer(wf_spec_converter)
|
||||
|
||||
DEFAULT_CONFIG['task_specs'].remove(StartEventConverter)
|
||||
DEFAULT_CONFIG['task_specs'].append(CustomStartEventConverter)
|
||||
registry = BpmnWorkflowSerializer.configure(DEFAULT_CONFIG)
|
||||
serializer = BpmnWorkflowSerializer(registry)
|
||||
|
||||
parser = SpiffBpmnParser()
|
||||
parser.OVERRIDE_PARSER_CLASSES[full_tag('startEvent')] = (StartEventParser, CustomStartEvent)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import logging
|
||||
import datetime
|
||||
|
||||
from SpiffWorkflow.spiff.parser import SpiffBpmnParser
|
||||
from SpiffWorkflow.spiff.specs.defaults import UserTask, ManualTask
|
||||
from SpiffWorkflow.spiff.serializer.config import SPIFF_CONFIG
|
||||
from SpiffWorkflow.bpmn.specs.bpmn_process_spec import BpmnProcessSpec
|
||||
from SpiffWorkflow.bpmn.specs.mixins.none_task import NoneTask
|
||||
from SpiffWorkflow.bpmn.script_engine import PythonScriptEngine, TaskDataEnvironment
|
||||
|
||||
from ..serializer.file import FileSerializer
|
||||
from ..engine import BpmnEngine
|
||||
from .curses_handlers import UserTaskHandler, ManualTaskHandler
|
||||
|
||||
logger = logging.getLogger('spiff_engine')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
spiff_logger = logging.getLogger('spiff')
|
||||
spiff_logger.setLevel(logging.INFO)
|
||||
|
||||
dirname = 'wfdata'
|
||||
FileSerializer.initialize(dirname)
|
||||
|
||||
registry = FileSerializer.configure(SPIFF_CONFIG)
|
||||
serializer = FileSerializer(dirname, registry=registry)
|
||||
|
||||
parser = SpiffBpmnParser()
|
||||
|
||||
handlers = {
|
||||
UserTask: UserTaskHandler,
|
||||
ManualTask: ManualTaskHandler,
|
||||
NoneTask: ManualTaskHandler,
|
||||
}
|
||||
|
||||
script_env = TaskDataEnvironment({'datetime': datetime })
|
||||
|
||||
script_engine = PythonScriptEngine(script_env)
|
||||
|
||||
engine = BpmnEngine(parser, serializer, handlers, script_engine)
|
|
@ -1,10 +1,6 @@
|
|||
import json
|
||||
from collections import namedtuple
|
||||
|
||||
from SpiffWorkflow.bpmn.serializer.helpers.dictionary import DictionaryConverter
|
||||
|
||||
ProductInfo = namedtuple('ProductInfo', ['color', 'size', 'style', 'price'])
|
||||
|
||||
INVENTORY = {
|
||||
'product_a': ProductInfo(False, False, False, 15.00),
|
||||
'product_b': ProductInfo(False, False, False, 15.00),
|
||||
|
@ -32,13 +28,4 @@ def product_info_to_dict(obj):
|
|||
def product_info_from_dict(dct):
|
||||
return ProductInfo(**dct)
|
||||
|
||||
registry = DictionaryConverter()
|
||||
registry.register(ProductInfo, product_info_to_dict, product_info_from_dict)
|
||||
|
||||
def dumps(obj):
|
||||
dct = registry.convert(obj)
|
||||
return json.dumps(dct)
|
||||
|
||||
def loads(s):
|
||||
dct = json.loads(s)
|
||||
return registry.restore(dct)
|
|
@ -0,0 +1,40 @@
|
|||
import logging
|
||||
import datetime
|
||||
|
||||
from RestrictedPython import safe_globals
|
||||
|
||||
from SpiffWorkflow.spiff.parser import SpiffBpmnParser
|
||||
from SpiffWorkflow.spiff.specs.defaults import UserTask, ManualTask
|
||||
from SpiffWorkflow.spiff.serializer.config import SPIFF_CONFIG
|
||||
from SpiffWorkflow.bpmn.specs.bpmn_process_spec import BpmnProcessSpec
|
||||
from SpiffWorkflow.bpmn.specs.mixins.none_task import NoneTask
|
||||
from SpiffWorkflow.bpmn.script_engine import PythonScriptEngine, TaskDataEnvironment
|
||||
|
||||
from ..serializer.file import FileSerializer
|
||||
from ..engine import BpmnEngine
|
||||
from .curses_handlers import UserTaskHandler, ManualTaskHandler
|
||||
|
||||
logger = logging.getLogger('spiff_engine')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
spiff_logger = logging.getLogger('spiff')
|
||||
spiff_logger.setLevel(logging.INFO)
|
||||
|
||||
dirname = 'wfdata'
|
||||
FileSerializer.initialize(dirname)
|
||||
|
||||
registry = FileSerializer.configure(SPIFF_CONFIG)
|
||||
serializer = FileSerializer(dirname, registry=registry)
|
||||
|
||||
parser = SpiffBpmnParser()
|
||||
|
||||
handlers = {
|
||||
UserTask: UserTaskHandler,
|
||||
ManualTask: ManualTaskHandler,
|
||||
NoneTask: ManualTaskHandler,
|
||||
}
|
||||
|
||||
script_env = TaskDataEnvironment(safe_globals)
|
||||
script_engine = PythonScriptEngine(script_env)
|
||||
|
||||
engine = BpmnEngine(parser, serializer, handlers, script_engine)
|
|
@ -0,0 +1,66 @@
|
|||
import json
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from SpiffWorkflow.spiff.parser import SpiffBpmnParser
|
||||
from SpiffWorkflow.spiff.specs.defaults import UserTask, ManualTask
|
||||
from SpiffWorkflow.spiff.serializer.config import SPIFF_CONFIG
|
||||
from SpiffWorkflow.bpmn.specs.bpmn_process_spec import BpmnProcessSpec
|
||||
from SpiffWorkflow.bpmn.specs.mixins.none_task import NoneTask
|
||||
from SpiffWorkflow.bpmn.script_engine import PythonScriptEngine, TaskDataEnvironment
|
||||
|
||||
from ..serializer.file import FileSerializer
|
||||
from ..engine import BpmnEngine
|
||||
from .curses_handlers import UserTaskHandler, ManualTaskHandler
|
||||
|
||||
from .product_info import (
|
||||
ProductInfo,
|
||||
product_info_to_dict,
|
||||
product_info_from_dict,
|
||||
lookup_product_info,
|
||||
lookup_shipping_cost,
|
||||
)
|
||||
logger = logging.getLogger('spiff_engine')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
spiff_logger = logging.getLogger('spiff_engine')
|
||||
spiff_logger.setLevel(logging.INFO)
|
||||
|
||||
dirname = 'wfdata'
|
||||
FileSerializer.initialize(dirname)
|
||||
|
||||
registry = FileSerializer.configure(SPIFF_CONFIG)
|
||||
registry.register(ProductInfo, product_info_to_dict, product_info_from_dict)
|
||||
serializer = FileSerializer(dirname, registry=registry)
|
||||
|
||||
parser = SpiffBpmnParser()
|
||||
|
||||
handlers = {
|
||||
UserTask: UserTaskHandler,
|
||||
ManualTask: ManualTaskHandler,
|
||||
NoneTask: ManualTaskHandler,
|
||||
}
|
||||
|
||||
service_task_env = TaskDataEnvironment({
|
||||
'product_info_from_dict': product_info_from_dict,
|
||||
'datetime': datetime,
|
||||
})
|
||||
|
||||
class ServiceTaskEngine(PythonScriptEngine):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(environment=service_task_env)
|
||||
|
||||
def call_service(self, operation_name, operation_params, task_data):
|
||||
if operation_name == 'lookup_product_info':
|
||||
product_info = lookup_product_info(operation_params['product_name']['value'])
|
||||
result = product_info_to_dict(product_info)
|
||||
elif operation_name == 'lookup_shipping_cost':
|
||||
result = lookup_shipping_cost(operation_params['shipping_method']['value'])
|
||||
else:
|
||||
raise Exception("Unknown Service!")
|
||||
return json.dumps(result)
|
||||
|
||||
script_engine = ServiceTaskEngine()
|
||||
|
||||
engine = BpmnEngine(parser, serializer, handlers, script_engine)
|
|
@ -0,0 +1,52 @@
|
|||
import sqlite3
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from SpiffWorkflow.spiff.parser import SpiffBpmnParser
|
||||
from SpiffWorkflow.spiff.specs.defaults import UserTask, ManualTask
|
||||
from SpiffWorkflow.spiff.serializer import DEFAULT_CONFIG
|
||||
from SpiffWorkflow.bpmn import BpmnWorkflow
|
||||
from SpiffWorkflow.bpmn.util.subworkflow import BpmnSubWorkflow
|
||||
from SpiffWorkflow.bpmn.specs import BpmnProcessSpec
|
||||
from SpiffWorkflow.bpmn.specs.mixins import NoneTaskMixin as NoneTask
|
||||
from SpiffWorkflow.bpmn.script_engine import TaskDataEnvironment, PythonScriptEngine
|
||||
|
||||
from ..serializer.sqlite import (
|
||||
SqliteSerializer,
|
||||
WorkflowConverter,
|
||||
SubworkflowConverter,
|
||||
WorkflowSpecConverter
|
||||
)
|
||||
from ..engine import BpmnEngine
|
||||
from .curses_handlers import UserTaskHandler, ManualTaskHandler
|
||||
|
||||
logger = logging.getLogger('spiff_engine')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
spiff_logger = logging.getLogger('spiff')
|
||||
spiff_logger.setLevel(logging.INFO)
|
||||
|
||||
DEFAULT_CONFIG[BpmnWorkflow] = WorkflowConverter
|
||||
DEFAULT_CONFIG[BpmnSubWorkflow] = SubworkflowConverter
|
||||
DEFAULT_CONFIG[BpmnProcessSpec] = WorkflowSpecConverter
|
||||
|
||||
dbname = 'spiff.db'
|
||||
|
||||
with sqlite3.connect(dbname) as db:
|
||||
SqliteSerializer.initialize(db)
|
||||
|
||||
registry = SqliteSerializer.configure(DEFAULT_CONFIG)
|
||||
serializer = SqliteSerializer(dbname, registry=registry)
|
||||
|
||||
parser = SpiffBpmnParser()
|
||||
|
||||
handlers = {
|
||||
UserTask: UserTaskHandler,
|
||||
ManualTask: ManualTaskHandler,
|
||||
NoneTask: ManualTaskHandler,
|
||||
}
|
||||
|
||||
script_env = TaskDataEnvironment({'datetime': datetime })
|
||||
script_engine = PythonScriptEngine(script_env)
|
||||
|
||||
engine = BpmnEngine(parser, serializer, handlers, script_engine)
|
Loading…
Reference in New Issue