merged in main and resolved conflicts w/ burnettk

This commit is contained in:
jasquat 2023-04-07 14:59:49 -04:00
commit e2b8f17a7e
No known key found for this signature in database
376 changed files with 14374 additions and 9052 deletions

View File

@ -16,10 +16,7 @@ jobs:
fail-fast: false
matrix:
include:
# FIXME: https://github.com/mysql/mysql-connector-python/pull/86
# put back when poetry update protobuf mysql-connector-python updates protobuf
# right now mysql is forcing protobuf to version 3
# - { python: "3.11", os: "ubuntu-latest", session: "safety" }
- { python: "3.11", os: "ubuntu-latest", session: "safety" }
- { python: "3.11", os: "ubuntu-latest", session: "mypy" }
- { python: "3.10", os: "ubuntu-latest", session: "mypy" }
- { python: "3.9", os: "ubuntu-latest", session: "mypy" }
@ -176,6 +173,19 @@ jobs:
name: logs-${{matrix.python}}-${{matrix.os}}-${{matrix.database}}
path: "./log/*.log"
# burnettk created an account at https://app.snyk.io/org/kevin-jfx
# and added his SNYK_TOKEN secret under the spiff-arena repo.
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/python@master
with:
args: spiffworkflow-backend
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run_pre_commit_checks:
runs-on: ubuntu-latest
defaults:
@ -184,9 +194,6 @@ jobs:
steps:
- name: Check out the repository
uses: actions/checkout@v3.3.0
with:
# Disabling shallow clone is recommended for improving relevancy of reporting in sonarcloud
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4.2.0
with:
@ -205,9 +212,6 @@ jobs:
steps:
- name: Check out the repository
uses: actions/checkout@v3.3.0
with:
# Disabling shallow clone is recommended for improving relevancy of reporting in sonarcloud
fetch-depth: 0
- name: Checkout Samples
uses: actions/checkout@v3
with:
@ -281,7 +285,7 @@ jobs:
# so just skip everything but main
if: github.ref_name == 'main'
with:
projectBaseDir: spiffworkflow-frontend
projectBaseDir: spiffworkflow-backend
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
pyrightconfig.json
.idea/
t
*~
.dccache
*~

55
Jenkinsfile vendored
View File

@ -32,6 +32,11 @@ pipeline {
description: 'ID of Jenkins credential for Docker registry.',
defaultValue: params.DOCKER_CRED_ID ?: 'MISSING'
)
string(
name: 'DISCORD_WEBHOOK_CRED',
description: 'Name of cretential with Discord webhook',
defaultValue: params.DISCORD_WEBHOOK_CRED ?: "",
)
booleanParam(
name: 'PUBLISH',
description: 'Publish built Docker images.',
@ -61,6 +66,16 @@ pipeline {
image.push(env.DOCKER_TAG)
}
} }
post {
success { script {
if (params.DISCORD_WEBHOOK_CRED) {
discordNotify(
header: 'SpiffWorkflow Docker image published!',
cred: params.DISCORD_WEBHOOK_CRED,
)
}
} }
}
}
} // stages
post {
@ -68,3 +83,43 @@ pipeline {
cleanup { cleanWs() }
} // post
} // pipeline
def discordNotify(Map args=[:]) {
def opts = [
header: args.header ?: 'Deployment successful!',
title: args.title ?: "${env.JOB_NAME}#${env.BUILD_NUMBER}",
cred: args.cred ?: null,
]
def repo = [
url: GIT_URL.minus('.git'),
branch: GIT_BRANCH.minus('origin/'),
commit: GIT_COMMIT.take(8),
prev: (
env.GIT_PREVIOUS_SUCCESSFUL_COMMIT ?: env.GIT_PREVIOUS_COMMIT ?: 'master'
).take(8),
]
wrap([$class: 'BuildUser']) {
BUILD_USER_ID = env.BUILD_USER_ID
}
withCredentials([
string(
credentialsId: opts.cred,
variable: 'DISCORD_WEBHOOK',
),
]) {
discordSend(
link: env.BUILD_URL,
result: currentBuild.currentResult,
webhookURL: env.DISCORD_WEBHOOK,
title: opts.title,
description: """
${opts.header}
Image: [`${params.DOCKER_NAME}:${params.DOCKER_TAG}`](https://hub.docker.com/r/${params.DOCKER_NAME}/tags?name=${params.DOCKER_TAG})
Branch: [`${repo.branch}`](${repo.url}/commits/${repo.branch})
Commit: [`${repo.commit}`](${repo.url}/commit/${repo.commit})
Diff: [`${repo.prev}...${repo.commit}`](${repo.url}/compare/${repo.prev}...${repo.commit})
By: [`${BUILD_USER_ID}`](${repo.url}/commits?author=${BUILD_USER_ID})
""",
)
}
}

View File

@ -1 +0,0 @@
"""__init.py__"""

View File

@ -72,7 +72,7 @@ def convert_timer_expressions(dct):
def add_default_condition_to_cond_task_specs(dct):
for spec in [ts for ts in dct['spec']['task_specs'].values() if ts['typename'] == 'ExclusiveGateway']:
if (None, spec['default_task_spec']) not in spec['cond_task_specs']:
if spec['default_task_spec'] is not None and (None, spec['default_task_spec']) not in spec['cond_task_specs']:
spec['cond_task_specs'].append({'condition': None, 'task_spec': spec['default_task_spec']})
def create_data_objects_and_io_specs(dct):
@ -111,3 +111,14 @@ def check_multiinstance(dct):
specs = [ spec for spec in dct['spec']['task_specs'].values() if 'prevtaskclass' in spec ]
if len(specs) > 0:
raise VersionMigrationError("This workflow cannot be migrated because it contains MultiInstance Tasks")
def remove_loop_reset(dct):
task_specs = [spec for spec in dct['spec']['task_specs'].values() if spec['typename'] == 'LoopResetTask']
for spec in task_specs:
if spec['typename'] == 'LoopResetTask':
tasks = [t for t in dct['tasks'].values() if t['task_spec'] == spec['name']]
for task in tasks:
dct['tasks'].pop(task['id'])
parent = dct['tasks'].get(task['parent'])
parent['children'] = [c for c in parent['children'] if c != task['id']]
dct['spec']['task_specs'].pop(spec['name'])

View File

@ -6,6 +6,7 @@ from .version_1_2 import (
add_default_condition_to_cond_task_specs,
create_data_objects_and_io_specs,
check_multiinstance,
remove_loop_reset,
)
def from_version_1_1(old):
@ -23,12 +24,18 @@ def from_version_1_1(old):
Data inputs and outputs on process specs were moved inside a BPMNIOSpecification, and
are now TaskDataReferences; BpmnDataSpecifications that referred to Data Objects are
now DataObjects.
Multiinstance tasks were completely refactored, in a way that is simply too difficult to
migrate.
Loop reset tasks were removed.
"""
new = deepcopy(old)
convert_timer_expressions(new)
add_default_condition_to_cond_task_specs(new)
create_data_objects_and_io_specs(new)
check_multiinstance(new)
remove_loop_reset(new)
new['VERSION'] = "1.2"
return new

View File

@ -1,5 +1,4 @@
from ..specs.BpmnProcessSpec import BpmnProcessSpec
from ..specs.MultiInstanceTask import MultiInstanceTask
from ..specs.events.IntermediateEvent import _BoundaryEventParent
from .helpers.spec import WorkflowSpecConverter

View File

@ -2,7 +2,6 @@ from .helpers.spec import TaskSpecConverter
from ...specs.StartTask import StartTask
from ...specs.Simple import Simple
from ...specs.LoopResetTask import LoopResetTask
from ..specs.BpmnProcessSpec import _EndJoin
from ..specs.BpmnSpecMixin import _BpmnCondition
@ -27,8 +26,6 @@ from ..specs.events.IntermediateEvent import (
ReceiveTask,
)
from ..workflow import BpmnWorkflow
class DefaultTaskSpecConverter(TaskSpecConverter):
@ -50,23 +47,6 @@ class StartTaskConverter(DefaultTaskSpecConverter):
super().__init__(StartTask, registry)
class LoopResetTaskConverter(DefaultTaskSpecConverter):
def __init__(self, registry):
super().__init__(LoopResetTask, registry)
def to_dict(self, spec):
dct = super().to_dict(spec)
dct['destination_id'] = str(spec.destination_id)
dct['destination_spec_name'] = spec.destination_spec_name
return dct
def from_dict(self, dct):
spec = self.task_spec_from_dict(dct)
spec.destination_id = self.registry.convert(spec.destination_id)
return spec
class EndJoinConverter(DefaultTaskSpecConverter):
def __init__(self, registry):
super().__init__(_EndJoin, registry)
@ -317,7 +297,6 @@ DEFAULT_TASK_SPEC_CONVERTER_CLASSES = [
SimpleTaskConverter,
StartTaskConverter,
EndJoinConverter,
LoopResetTaskConverter,
NoneTaskConverter,
UserTaskConverter,
ManualTaskConverter,

View File

@ -246,7 +246,7 @@ class BpmnWorkflowSerializer:
if isinstance(task_spec, SubWorkflowTask) and task_id in top_dct.get('subprocesses', {}):
subprocess_spec = top.subprocess_specs[task_spec.spec]
subprocess = self.wf_class(subprocess_spec, {}, name=task_spec.name, parent=process)
subprocess = self.wf_class(subprocess_spec, {}, name=task_spec.name, parent=process, deserializing=True)
subprocess_dct = top_dct['subprocesses'].get(task_id, {})
subprocess.data = self.data_converter.restore(subprocess_dct.pop('data'))
subprocess.success = subprocess_dct.pop('success')
@ -254,8 +254,12 @@ class BpmnWorkflowSerializer:
subprocess.completed_event.connect(task_spec._on_subworkflow_completed, task)
top_level_workflow.subprocesses[task.id] = subprocess
for child in [ process_dct['tasks'][c] for c in task_dict['children'] ]:
self.task_tree_from_dict(process_dct, child['id'], task, process, top, top_dct)
for child_task_id in task_dict['children']:
if child_task_id in process_dct['tasks']:
child = process_dct['tasks'][child_task_id]
self.task_tree_from_dict(process_dct, child_task_id, task, process, top, top_dct)
else:
raise ValueError(f"Task {task_id} ({task_spec.name}) has child {child_task_id}, but no such task exists")
return task

View File

@ -47,9 +47,10 @@ class _EndJoin(UnstructuredJoin):
return force or len(waiting_tasks) == 0, waiting_tasks
def _on_complete_hook(self, my_task):
super(_EndJoin, self)._on_complete_hook(my_task)
def _run_hook(self, my_task):
result = super(_EndJoin, self)._run_hook(my_task)
my_task.workflow.data.update(my_task.data)
return result
class BpmnProcessSpec(WorkflowSpec):

View File

@ -110,11 +110,12 @@ class InclusiveGateway(MultiChoice, UnstructuredJoin):
return complete, waiting_tasks
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
outputs = self._get_matching_outputs(my_task)
if len(outputs) == 0:
raise WorkflowTaskException(f'No conditions satisfied on gateway', task=my_task)
my_task._sync_children(outputs, TaskState.FUTURE)
return True
@property
def spec_type(self):

View File

@ -29,14 +29,14 @@ class ScriptEngineTask(Simple, BpmnSpecMixin):
"""Please override for specific Implementations, see ScriptTask below for an example"""
pass
def _on_complete_hook(self, task):
def _run_hook(self, task):
try:
self._execute(task)
super(ScriptEngineTask, self)._on_complete_hook(task)
super(ScriptEngineTask, self)._run_hook(task)
except Exception as exc:
task._set_state(TaskState.WAITING)
raise exc
return True
class ScriptTask(ScriptEngineTask):

View File

@ -25,9 +25,6 @@ class SubWorkflowTask(BpmnSpecMixin):
def spec_type(self):
return 'Subprocess'
def _on_ready_hook(self, my_task):
super()._on_ready_hook(my_task)
def _on_subworkflow_completed(self, subworkflow, my_task):
self.update_data(my_task, subworkflow)
my_task._set_state(TaskState.READY)

View File

@ -54,14 +54,14 @@ class UnstructuredJoin(Join, BpmnSpecMixin):
last_changed = None
thread_tasks = []
for task in split_task._find_any(self):
# Ignore tasks from other threads.
if task.thread_id != my_task.thread_id:
# Ignore tasks from other threads. (Do we need this condition?)
continue
# Ignore my outgoing branches.
if self.split_task and task._is_descendant_of(my_task):
continue
# For an inclusive join, this can happen - it's a future join
if not task.parent._is_finished():
# For an inclusive join, this can happen - it's a future join
continue
if my_task._is_descendant_of(task):
# Skip ancestors (otherwise the branch this task is on will get dropped)
continue
# We have found a matching instance.
thread_tasks.append(task)
@ -77,20 +77,13 @@ class UnstructuredJoin(Join, BpmnSpecMixin):
for task in thread_tasks:
collected_data.update(task.data)
# Mark the identified task instances as COMPLETED. The exception
# is the most recently changed task, for which we assume READY.
# By setting the state to READY only, we allow for calling
# :class:`Task.complete()`, which leads to the task tree being
# (re)built underneath the node.
for task in thread_tasks:
if task == last_changed:
task.data.update(collected_data)
self.entered_event.emit(my_task.workflow, my_task)
task._ready()
else:
task._set_state(TaskState.COMPLETED)
if task != last_changed:
task._set_state(TaskState.CANCELLED)
task._drop_children()
else:
task.data.update(collected_data)
def task_should_set_children_future(self, my_task):
return True

View File

@ -18,7 +18,6 @@
# 02110-1301 USA
from .event_types import ThrowingEvent, CatchingEvent
from .event_definitions import CycleTimerEventDefinition
from ..BpmnSpecMixin import BpmnSpecMixin
from ....specs.Simple import Simple
from ....task import TaskState
@ -67,13 +66,15 @@ class _BoundaryEventParent(Simple, BpmnSpecMixin):
def spec_type(self):
return 'Boundary Event Parent'
def _on_ready_hook(self, my_task):
def _run_hook(self, my_task):
# Clear any events that our children might have received and
# wait for new events
for child in my_task.children:
if isinstance(child.task_spec, BoundaryEvent):
child.task_spec.event_definition.reset(child)
child._set_state(TaskState.WAITING)
return True
def _child_complete_hook(self, child_task):
@ -123,7 +124,7 @@ class BoundaryEvent(CatchingEvent):
super(BoundaryEvent, self).catch(my_task, event_definition)
# Would love to get rid of this statement and manage in the workflow
# However, it is not really compatible with how boundary events work.
my_task.complete()
my_task.run()
class EventBasedGateway(CatchingEvent):
@ -135,8 +136,8 @@ class EventBasedGateway(CatchingEvent):
def _predict_hook(self, my_task):
my_task._sync_children(self.outputs, state=TaskState.MAYBE)
def _on_complete_hook(self, my_task):
def _on_ready_hook(self, my_task):
seen_events = my_task.internal_data.get('seen_events', [])
for child in my_task.children:
if not child.task_spec.event_definition.has_fired(child):
if child.task_spec.event_definition not in seen_events:
child.cancel()
return super()._on_complete_hook(my_task)

View File

@ -20,13 +20,14 @@
import re
from datetime import datetime, timedelta, timezone
from calendar import monthrange
from time import timezone as tzoffset
from time import timezone as tzoffset, altzone as dstoffset, daylight as isdst
from copy import deepcopy
from SpiffWorkflow.exceptions import SpiffWorkflowException, WorkflowException
from SpiffWorkflow.exceptions import WorkflowException
from SpiffWorkflow.task import TaskState
LOCALTZ = timezone(timedelta(seconds=-1 * tzoffset))
seconds_from_utc = dstoffset if isdst else tzoffset
LOCALTZ = timezone(timedelta(seconds=-1 * seconds_from_utc))
class EventDefinition(object):
@ -452,42 +453,38 @@ class CycleTimerEventDefinition(TimerEventDefinition):
def event_type(self):
return 'Cycle Timer'
def has_fired(self, my_task):
def cycle_complete(self, my_task):
if not my_task._get_internal_data('event_fired'):
# Only check for the next cycle when the event has not fired to prevent cycles from being skipped.
event_value = my_task._get_internal_data('event_value')
if event_value is None:
expression = my_task.workflow.script_engine.evaluate(my_task, self.expression)
cycles, start, duration = TimerEventDefinition.parse_iso_recurring_interval(expression)
event_value = {'cycles': cycles, 'next': start.isoformat(), 'duration': duration.total_seconds()}
event_value = my_task._get_internal_data('event_value')
if event_value is None:
# Don't necessarily like this, but it's a lot more staightforward than trying to only create
# a child task on loop iterations after the first
my_task._drop_children()
expression = my_task.workflow.script_engine.evaluate(my_task, self.expression)
cycles, start, duration = TimerEventDefinition.parse_iso_recurring_interval(expression)
event_value = {'cycles': cycles, 'next': start.isoformat(), 'duration': duration.total_seconds()}
if event_value['cycles'] > 0:
next_event = datetime.fromisoformat(event_value['next'])
if next_event < datetime.now(timezone.utc):
my_task._set_internal_data(event_fired=True)
event_value['next'] = (next_event + timedelta(seconds=event_value['duration'])).isoformat()
# When the next timer event passes, return True to allow the parent task to generate another child
# Use event fired to indicate that this timer has completed all cycles and the task can be completed
ready = False
if event_value['cycles'] != 0:
next_event = datetime.fromisoformat(event_value['next'])
if next_event < datetime.now(timezone.utc):
event_value['next'] = (next_event + timedelta(seconds=event_value['duration'])).isoformat()
event_value['cycles'] -= 1
ready = True
else:
my_task.internal_data.pop('event_value', None)
my_task.internal_data['event_fired'] = True
my_task._set_internal_data(event_value=event_value)
return my_task._get_internal_data('event_fired', False)
my_task._set_internal_data(event_value=event_value)
return ready
def timer_value(self, my_task):
event_value = my_task._get_internal_data('event_value')
if event_value is not None and event_value['cycles'] > 0:
if event_value is not None and event_value['cycles'] != 0:
return event_value['next']
def complete(self, my_task):
event_value = my_task._get_internal_data('event_value')
if event_value is not None and event_value['cycles'] == 0:
my_task.internal_data.pop('event_value')
return True
def complete_cycle(self, my_task):
# Only increment when the task completes
if my_task._get_internal_data('event_value') is not None:
my_task.internal_data['event_value']['cycles'] -= 1
class MultipleEventDefinition(EventDefinition):
@ -504,11 +501,10 @@ class MultipleEventDefinition(EventDefinition):
seen_events = my_task.internal_data.get('seen_events', [])
for event in self.event_definitions:
if isinstance(event, (TimerEventDefinition, CycleTimerEventDefinition)):
if isinstance(event, TimerEventDefinition):
child = [c for c in my_task.children if c.task_spec.event_definition == event]
child[0].task_spec._update_hook(child[0])
child[0]._set_state(TaskState.MAYBE)
if event.has_fired(my_task):
if event.has_fired(child[0]):
seen_events.append(event)
if self.parallel:

View File

@ -57,21 +57,22 @@ class CatchingEvent(Simple, BpmnSpecMixin):
if self.event_definition.has_fired(my_task):
return True
else:
elif isinstance(self.event_definition, CycleTimerEventDefinition):
if self.event_definition.cycle_complete(my_task):
for output in self.outputs:
child = my_task._add_child(output, TaskState.READY)
child.task_spec._predict(child, mask=TaskState.READY|TaskState.PREDICTED_MASK)
if my_task.state != TaskState.WAITING:
my_task._set_state(TaskState.WAITING)
elif my_task.state != TaskState.WAITING:
my_task._set_state(TaskState.WAITING)
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
if isinstance(self.event_definition, MessageEventDefinition):
self.event_definition.update_task_data(my_task)
elif isinstance(self.event_definition, CycleTimerEventDefinition):
self.event_definition.complete_cycle(my_task)
if not self.event_definition.complete(my_task):
for output in self.outputs:
my_task._add_child(output)
my_task._set_state(TaskState.WAITING)
self.event_definition.reset(my_task)
super(CatchingEvent, self)._on_complete_hook(my_task)
return super(CatchingEvent, self)._run_hook(my_task)
# This fixes the problem of boundary events remaining cancelled if the task is reused.
# It pains me to add these methods, but unless we can get rid of the loop reset task we're stuck
@ -95,6 +96,7 @@ class ThrowingEvent(Simple, BpmnSpecMixin):
super(ThrowingEvent, self).__init__(wf_spec, name, **kwargs)
self.event_definition = event_definition
def _on_complete_hook(self, my_task):
super(ThrowingEvent, self)._on_complete_hook(my_task)
def _run_hook(self, my_task):
super(ThrowingEvent, self)._run_hook(my_task)
self.event_definition.throw(my_task)
return True

View File

@ -29,7 +29,7 @@ from .specs.events.StartEvent import StartEvent
from .specs.SubWorkflowTask import CallActivity
from ..task import TaskState, Task
from ..workflow import Workflow
from ..exceptions import WorkflowException, WorkflowTaskException
from ..exceptions import TaskNotFoundException, WorkflowException, WorkflowTaskException
class BpmnMessage:
@ -251,7 +251,7 @@ class BpmnWorkflow(Workflow):
for task in engine_steps:
if will_complete_task is not None:
will_complete_task(task)
task.complete()
task.run()
if did_complete_task is not None:
did_complete_task(task)
if task.task_spec.name == exit_at:
@ -271,7 +271,10 @@ class BpmnWorkflow(Workflow):
for my_task in self.get_tasks(TaskState.WAITING):
if will_refresh_task is not None:
will_refresh_task(my_task)
my_task.task_spec._update(my_task)
# This seems redundant, but the state could have been updated by another waiting task and no longer be waiting.
# Someday, I would like to get rid of this method, and also do_engine_steps
if my_task.state == TaskState.WAITING:
my_task.task_spec._update(my_task)
if did_refresh_task is not None:
did_refresh_task(my_task)
@ -279,10 +282,15 @@ class BpmnWorkflow(Workflow):
return [t for t in self.get_tasks(workflow=workflow) if t.task_spec.name == name]
def get_tasks(self, state=TaskState.ANY_MASK, workflow=None):
# Now that I've revisited and had to ask myself what the hell was I doing, I realize I should comment this
tasks = []
top = self._get_outermost_workflow()
wf = workflow or top
for task in Workflow.get_tasks(wf):
# I think it makes more sense to start with the current workflow, which is probably going to be the top
# most of the time anyway
wf = workflow or self
# We can't filter the iterator on the state because we have to subprocesses, and the subprocess task will
# almost surely be in a different state than the tasks we want
for task in Workflow.get_tasks_iterator(wf):
subprocess = top.subprocesses.get(task.id)
if subprocess is not None:
tasks.extend(subprocess.get_tasks(state, subprocess))
@ -290,42 +298,28 @@ class BpmnWorkflow(Workflow):
tasks.append(task)
return tasks
def _find_task(self, task_id):
if task_id is None:
raise WorkflowException('task_id is None', task_spec=self.spec)
for task in self.get_tasks():
def get_task_from_id(self, task_id, workflow=None):
for task in self.get_tasks(workflow=workflow):
if task.id == task_id:
return task
raise WorkflowException(f'A task with the given task_id ({task_id}) was not found', task_spec=self.spec)
raise TaskNotFoundException(f'A task with the given task_id ({task_id}) was not found', task_spec=self.spec)
def complete_task_from_id(self, task_id):
# I don't even know why we use this stupid function instead of calling task.complete,
# since all it does is search the task tree and call the method
task = self._find_task(task_id)
return task.complete()
def reset_task_from_id(self, task_id):
task = self._find_task(task_id)
if task.workflow.last_task and task.workflow.last_task.data:
data = task.workflow.last_task.data
return task.reset_token(data)
def get_ready_user_tasks(self,lane=None):
def get_ready_user_tasks(self, lane=None, workflow=None):
"""Returns a list of User Tasks that are READY for user action"""
if lane is not None:
return [t for t in self.get_tasks(TaskState.READY)
return [t for t in self.get_tasks(TaskState.READY, workflow)
if (not self._is_engine_task(t.task_spec))
and (t.task_spec.lane == lane)]
else:
return [t for t in self.get_tasks(TaskState.READY)
return [t for t in self.get_tasks(TaskState.READY, workflow)
if not self._is_engine_task(t.task_spec)]
def get_waiting_tasks(self):
def get_waiting_tasks(self, workflow=None):
"""Returns a list of all WAITING tasks"""
return self.get_tasks(TaskState.WAITING)
return self.get_tasks(TaskState.WAITING, workflow)
def get_catching_tasks(self):
return [ task for task in self.get_tasks() if isinstance(task.task_spec, CatchingEvent) ]
def get_catching_tasks(self, workflow=None):
return [task for task in self.get_tasks(workflow=workflow) if isinstance(task.task_spec, CatchingEvent)]
def _is_engine_task(self, task_spec):
return (not hasattr(task_spec, 'is_engine_task') or task_spec.is_engine_task())

View File

@ -5,10 +5,7 @@ from ..specs.model import DecisionTable, Rule, HitPolicy
from ..specs.model import Input, InputEntry, Output, OutputEntry
from ..engine.DMNEngine import DMNEngine
class BusinessRuleTaskConverter(TaskSpecConverter):
def __init__(self, registry):
super().__init__(BusinessRuleTask, registry)
class BaseBusinessRuleTaskConverter(TaskSpecConverter):
def to_dict(self, spec):
dct = self.get_default_attributes(spec)
@ -98,3 +95,8 @@ class BusinessRuleTaskConverter(TaskSpecConverter):
rule.outputEntries = [self.output_entry_from_dict(entry, outputs)
for entry in dct['output_entries']]
return rule
class BusinessRuleTaskConverter(BaseBusinessRuleTaskConverter):
def __init__(self, registry):
super().__init__(BusinessRuleTask, registry)

View File

@ -1,5 +1,4 @@
from SpiffWorkflow.exceptions import WorkflowTaskException, WorkflowException, \
SpiffWorkflowException
from SpiffWorkflow.exceptions import WorkflowTaskException, SpiffWorkflowException
from ...specs.Simple import Simple
@ -17,7 +16,6 @@ class BusinessRuleTask(Simple, BpmnSpecMixin):
def __init__(self, wf_spec, name, dmnEngine, **kwargs):
super().__init__(wf_spec, name, **kwargs)
self.dmnEngine = dmnEngine
self.resDict = None
@ -25,11 +23,10 @@ class BusinessRuleTask(Simple, BpmnSpecMixin):
def spec_class(self):
return 'Business Rule Task'
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
try:
my_task.data = DeepMerge.merge(my_task.data,
self.dmnEngine.result(my_task))
super(BusinessRuleTask, self)._on_complete_hook(my_task)
my_task.data = DeepMerge.merge(my_task.data, self.dmnEngine.result(my_task))
super(BusinessRuleTask, self)._run_hook(my_task)
except SpiffWorkflowException as we:
we.add_note(f"Business Rule Task '{my_task.task_spec.description}'.")
raise we
@ -37,4 +34,4 @@ class BusinessRuleTask(Simple, BpmnSpecMixin):
error = WorkflowTaskException(str(e), task=my_task)
error.add_note(f"Business Rule Task '{my_task.task_spec.description}'.")
raise error
return True

View File

@ -129,3 +129,7 @@ class WorkflowTaskException(WorkflowException):
class StorageException(SpiffWorkflowException):
pass
class TaskNotFoundException(WorkflowException):
pass

View File

@ -16,9 +16,7 @@ from builtins import object
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import re
from .. import operators
from .. import specs
from ..specs.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel
from ..specs.CancelTask import CancelTask
@ -41,7 +39,6 @@ from ..specs.ThreadSplit import ThreadSplit
from ..specs.Transform import Transform
from ..specs.Trigger import Trigger
from ..specs.WorkflowSpec import WorkflowSpec
from ..specs.LoopResetTask import LoopResetTask
# Create a list of tag names out of the spec names.
def spec_map():
@ -68,7 +65,6 @@ def spec_map():
'transform': Transform,
'trigger': Trigger,
'workflow-spec': WorkflowSpec,
'loop-reset-task': LoopResetTask,
'task': Simple,
}

View File

@ -22,8 +22,7 @@ from base64 import b64encode, b64decode
from ..workflow import Workflow
from ..util.impl import get_class
from ..task import Task
from ..operators import (Attrib, PathAttrib, Equal, NotEqual,
Operator, GreaterThan, LessThan, Match)
from ..operators import (Attrib, PathAttrib, Equal, NotEqual, Operator, GreaterThan, LessThan, Match)
from ..specs.base import TaskSpec
from ..specs.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel
@ -44,10 +43,8 @@ from ..specs.SubWorkflow import SubWorkflow
from ..specs.ThreadStart import ThreadStart
from ..specs.ThreadMerge import ThreadMerge
from ..specs.ThreadSplit import ThreadSplit
from ..specs.Transform import Transform
from ..specs.Trigger import Trigger
from ..specs.WorkflowSpec import WorkflowSpec
from ..specs.LoopResetTask import LoopResetTask
from .base import Serializer
from .exceptions import TaskNotSupportedError, MissingSpecError
import warnings
@ -169,7 +166,6 @@ class DictionarySerializer(Serializer):
s_state['defines'] = self.serialize_dict(spec.defines)
s_state['pre_assign'] = self.serialize_list(spec.pre_assign)
s_state['post_assign'] = self.serialize_list(spec.post_assign)
s_state['locks'] = spec.locks[:]
# Note: Events are not serialized; this is documented in
# the TaskSpec API docs.
@ -190,7 +186,6 @@ class DictionarySerializer(Serializer):
spec.pre_assign = self.deserialize_list(s_state.get('pre_assign', []))
spec.post_assign = self.deserialize_list(
s_state.get('post_assign', []))
spec.locks = s_state.get('locks', [])[:]
# We can't restore inputs and outputs yet because they may not be
# deserialized yet. So keep the names, and resolve them in the end.
spec.inputs = s_state.get('inputs', [])[:]
@ -302,18 +297,6 @@ class DictionarySerializer(Serializer):
self.deserialize_task_spec(wf_spec, s_state, spec=spec)
return spec
def serialize_loop_reset_task(self, spec):
s_state = self.serialize_task_spec(spec)
s_state['destination_id'] = spec.destination_id
s_state['destination_spec_name'] = spec.destination_spec_name
return s_state
def deserialize_loop_reset_task(self, wf_spec, s_state):
spec = LoopResetTask(wf_spec, s_state['name'], s_state['destination_id'],
s_state['destination_spec_name'])
self.deserialize_task_spec(wf_spec, s_state, spec=spec)
return spec
def serialize_join(self, spec):
s_state = self.serialize_task_spec(spec)
s_state['split_task'] = spec.split_task
@ -555,8 +538,7 @@ class DictionarySerializer(Serializer):
del spec.task_specs['Start']
start_task_spec_state = s_state['task_specs']['Start']
start_task_spec = StartTask.deserialize(
self, spec, start_task_spec_state)
start_task_spec = StartTask.deserialize(self, spec, start_task_spec_state)
spec.start = start_task_spec
spec.task_specs['Start'] = start_task_spec
for name, task_spec_state in list(s_state['task_specs'].items()):
@ -602,20 +584,34 @@ class DictionarySerializer(Serializer):
s_state['wf_spec']"""
if wf_spec is None:
# The json serializer serializes the spec as a string and then serializes it again, hence this check
# I'm not confident that this is going to actually work, but this serializer is so fundamentally flawed
# that I'm not going to put the effort in to be sure this works.
if isinstance(s_state['wf_spec'], str):
spec_dct = json.loads(s_state['wf_spec'])
else:
spec_dct = s_state['wf_spec']
reset_specs = [spec['name'] for spec in spec_dct['task_specs'].values() if spec['class'].endswith('LoopResetTask')]
for name in reset_specs:
s_state['wf_spec']['task_specs'].pop(name)
wf_spec = self.deserialize_workflow_spec(s_state['wf_spec'], **kwargs)
else:
reset_specs = []
workflow = wf_class(wf_spec)
workflow.data = self.deserialize_dict(s_state['data'])
workflow.success = s_state['success']
workflow.spec = wf_spec
workflow.task_tree = self.deserialize_task(
workflow, s_state['task_tree'])
workflow.task_tree = self.deserialize_task(workflow, s_state['task_tree'], reset_specs)
# Re-connect parents
tasklist = list(workflow.get_tasks())
tasklist = workflow.get_tasks()
for task in tasklist:
task.parent = workflow.get_task(task.parent,tasklist)
if task.parent is not None:
task.parent = workflow.get_task_from_id(task.parent, tasklist)
workflow.last_task = workflow.get_task(s_state['last_task'],tasklist)
if workflow.last_task is not None:
workflow.last_task = workflow.get_task_from_id(s_state['last_task'],tasklist)
workflow.update_task_mapping()
return workflow
@ -636,81 +632,48 @@ class DictionarySerializer(Serializer):
" internal_data to store the subworkflow).")
s_state = dict()
# id
s_state['id'] = task.id
# workflow
s_state['workflow_name'] = task.workflow.name
# parent
s_state['parent'] = task.parent.id if task.parent is not None else None
# children
if not skip_children:
s_state['children'] = [
self.serialize_task(child) for child in task.children]
# state
s_state['state'] = task.state
s_state['triggered'] = task.triggered
# task_spec
s_state['task_spec'] = task.task_spec.name
# last_state_change
s_state['last_state_change'] = task.last_state_change
# data
s_state['data'] = self.serialize_dict(task.data)
# internal_data
s_state['internal_data'] = task.internal_data
return s_state
def deserialize_task(self, workflow, s_state):
def deserialize_task(self, workflow, s_state, ignored_specs=None):
assert isinstance(workflow, Workflow)
splits = s_state['task_spec'].split('_')
oldtaskname = s_state['task_spec']
task_spec = workflow.get_task_spec_from_name(oldtaskname)
old_spec_name = s_state['task_spec']
if old_spec_name in ignored_specs:
return None
task_spec = workflow.get_task_spec_from_name(old_spec_name)
if task_spec is None:
raise MissingSpecError("Unknown task spec: " + oldtaskname)
raise MissingSpecError("Unknown task spec: " + old_spec_name)
task = Task(workflow, task_spec)
if getattr(task_spec,'isSequential',False) and \
s_state['internal_data'].get('splits') is not None:
if getattr(task_spec,'isSequential',False) and s_state['internal_data'].get('splits') is not None:
task.task_spec.expanded = s_state['internal_data']['splits']
# id
task.id = s_state['id']
# parent
# as the task_tree might not be complete yet
# keep the ids so they can be processed at the end
task.parent = s_state['parent']
# children
task.children = self._deserialize_task_children(task, s_state)
# state
task.children = self._deserialize_task_children(task, s_state, ignored_specs)
task._state = s_state['state']
task.triggered = s_state['triggered']
# last_state_change
task.last_state_change = s_state['last_state_change']
# data
task.data = self.deserialize_dict(s_state['data'])
# internal_data
task.internal_data = s_state['internal_data']
return task
def _deserialize_task_children(self, task, s_state):
def _deserialize_task_children(self, task, s_state, ignored_specs):
"""This may need to be overridden if you need to support
deserialization of sub-workflows"""
return [self.deserialize_task(task.workflow, c)
for c in s_state['children']]
children = [self.deserialize_task(task.workflow, c, ignored_specs) for c in s_state['children']]
return [c for c in children if c is not None]

View File

@ -22,24 +22,20 @@ from .dict import DictionarySerializer
class JSONSerializer(DictionarySerializer):
def serialize_workflow_spec(self, wf_spec, **kwargs):
thedict = super(JSONSerializer, self).serialize_workflow_spec(
wf_spec, **kwargs)
thedict = super(JSONSerializer, self).serialize_workflow_spec(wf_spec, **kwargs)
return self._dumps(thedict)
def deserialize_workflow_spec(self, s_state, **kwargs):
thedict = self._loads(s_state)
return super(JSONSerializer, self).deserialize_workflow_spec(
thedict, **kwargs)
return super(JSONSerializer, self).deserialize_workflow_spec(thedict, **kwargs)
def serialize_workflow(self, workflow, **kwargs):
thedict = super(JSONSerializer, self).serialize_workflow(
workflow, **kwargs)
thedict = super(JSONSerializer, self).serialize_workflow(workflow, **kwargs)
return self._dumps(thedict)
def deserialize_workflow(self, s_state, **kwargs):
thedict = self._loads(s_state)
return super(JSONSerializer, self).deserialize_workflow(
thedict, **kwargs)
return super(JSONSerializer, self).deserialize_workflow(thedict, **kwargs)
def _object_hook(self, dct):
if '__uuid__' in dct:

View File

@ -176,8 +176,7 @@ class XmlSerializer(Serializer):
threshold_field = start_node.attrib.get('threshold-field', '').lower()
file_name = start_node.attrib.get('file', '').lower()
file_field = start_node.attrib.get('file-field', '').lower()
kwargs = {'lock': [],
'data': {},
kwargs = {'data': {},
'defines': {},
'pre_assign': [],
'post_assign': []}
@ -253,10 +252,6 @@ class XmlSerializer(Serializer):
elif not isinstance(context, list):
context = [context]
context.append(node.text)
elif node.tag == 'lock':
if not node.text:
self.raise_parser_exception('Empty %s tag' % node.tag)
kwargs['lock'].append(node.text)
elif node.tag == 'pick':
if not node.text:
self.raise_parser_exception('Empty %s tag' % node.tag)

View File

@ -15,15 +15,12 @@ from builtins import str
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import re
import warnings
from lxml import etree
from lxml.etree import SubElement
from ..workflow import Workflow
from .. import specs, operators
from ..task import Task, TaskStateNames
from ..operators import (Attrib, Assign, PathAttrib, Equal, NotEqual,
GreaterThan, LessThan, Match)
from ..operators import (Attrib, Assign, PathAttrib, Equal, NotEqual, GreaterThan, LessThan, Match)
from ..specs.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel
from ..specs.CancelTask import CancelTask
@ -43,10 +40,8 @@ from ..specs.SubWorkflow import SubWorkflow
from ..specs.ThreadStart import ThreadStart
from ..specs.ThreadMerge import ThreadMerge
from ..specs.ThreadSplit import ThreadSplit
from ..specs.Transform import Transform
from ..specs.Trigger import Trigger
from ..specs.WorkflowSpec import WorkflowSpec
from ..specs.LoopResetTask import LoopResetTask
from .base import Serializer, spec_map, op_map
from .exceptions import TaskNotSupportedError
@ -726,31 +721,17 @@ class XmlSerializer(Serializer):
workflow.task_tree = self.deserialize_task(workflow, task_tree_elem[0])
# Re-connect parents
for task in workflow.get_tasks():
task.parent = workflow.get_task(task.parent)
for task in workflow.get_tasks_iterator():
if task.parent is not None:
task.parent = workflow.get_task_from_id(task.parent)
# last_task
last_task = elem.findtext('last-task')
if last_task is not None:
workflow.last_task = workflow.get_task(last_task)
workflow.last_task = workflow.get_task_from_id(last_task)
return workflow
def serialize_loop_reset_task(self, spec):
elem = etree.Element('loop-reset-task')
SubElement(elem, 'destination_id').text = str(spec.destination_id)
SubElement(elem, 'destination_spec_name').text = str(spec.destination_spec_name)
return self.serialize_task_spec(spec, elem)
def deserialize_loop_reset_task(self, wf_spec, elem, cls=LoopResetTask, **kwargs):
destination_id = elem.findtext('destination_id')
destination_spec_name = elem.findtext('destination_spec_name')
task = self.deserialize_task_spec(wf_spec, elem, cls,
destination_id=destination_id,
destination_spec_name=destination_spec_name)
return task
def serialize_task(self, task, skip_children=False):
assert isinstance(task, Task)

View File

@ -55,9 +55,9 @@ class Cancel(TaskSpec):
if len(self.outputs) > 0:
raise WorkflowException('Cancel with an output.', task_spec=self)
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
my_task.workflow.cancel(self.cancel_successfully)
TaskSpec._on_complete_hook(self, my_task)
return True
def serialize(self, serializer):
return serializer.serialize_cancel(self)

View File

@ -16,7 +16,6 @@
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from .base import TaskSpec
from .Trigger import Trigger
@ -30,12 +29,12 @@ class CancelTask(Trigger):
parallel split.
"""
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
for task_name in self.context:
cancel_tasks = my_task.workflow.get_task_spec_from_name(task_name)
for cancel_task in my_task._get_root()._find_any(cancel_tasks):
cancel_task.cancel()
TaskSpec._on_complete_hook(self, my_task)
return True
def serialize(self, serializer):
return serializer.serialize_cancel_task(self)

View File

@ -117,7 +117,7 @@ class Celery(TaskSpec):
self.call = call or []
self.args = call_args or {}
self.merge_results = merge_results
skip = 'data', 'defines', 'pre_assign', 'post_assign', 'lock'
skip = 'data', 'defines', 'pre_assign', 'post_assign'
self.kwargs = dict(i for i in list(kwargs.items()) if i[0] not in skip)
self.result_key = result_key

View File

@ -55,7 +55,7 @@ class Choose(Trigger):
self.context = context
self.choice = choice is not None and choice or []
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
context = my_task.workflow.get_task_spec_from_name(self.context)
triggered = []
for task in my_task.workflow.task_tree:
@ -66,7 +66,7 @@ class Choose(Trigger):
triggered.append(task)
for task in triggered:
context._predict(task)
TaskSpec._on_complete_hook(self, my_task)
return True
def serialize(self, serializer):
return serializer.serialize_choose(self)

View File

@ -61,16 +61,7 @@ class ExclusiveChoice(MultiChoice):
if self.default_task_spec is None:
raise WorkflowException('A default output is required.', task_spec=self)
def _predict_hook(self, my_task):
# If the task's status is not predicted, we default to MAYBE
# for all it's outputs except the default choice, which is
# LIKELY.
# Otherwise, copy my own state to the children.
my_task._sync_children(self.outputs)
spec = self._wf_spec.get_task_spec_from_name(self.default_task_spec)
my_task._set_likely_task(spec)
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
output = self._wf_spec.get_task_spec_from_name(self.default_task_spec)
for condition, spec_name in self.cond_task_specs:
@ -82,6 +73,10 @@ class ExclusiveChoice(MultiChoice):
raise WorkflowException(f'No conditions satisfied for {my_task.task_spec.name}', task_spec=self)
my_task._sync_children([output], TaskState.FUTURE)
for child in my_task.children:
child.task_spec._predict(child, mask=TaskState.FUTURE|TaskState.PREDICTED_MASK)
return True
def serialize(self, serializer):
return serializer.serialize_exclusive_choice(self)

View File

@ -120,8 +120,7 @@ class Join(TaskSpec):
# If the task is predicted with less outputs than he has
# children, that means the prediction may be incomplete (for
# example, because a prediction is not yet possible at this time).
if not child._is_definite() \
and len(child.task_spec.outputs) > len(child.children):
if child._is_predicted() and len(child.task_spec.outputs) > len(child.children):
return True
return False
@ -153,8 +152,8 @@ class Join(TaskSpec):
# Look at the tree to find all places where this task is used.
tasks = []
for input in self.inputs:
tasks += my_task.workflow.task_mapping[my_task.thread_id][input]
for spec in self.inputs:
tasks.extend([ t for t in my_task.workflow.task_tree._find_any(spec) if t.thread_id == my_task.thread_id ])
# Look up which tasks have already completed.
waiting_tasks = []
@ -162,7 +161,7 @@ class Join(TaskSpec):
for task in tasks:
if task.parent is None or task._has_state(TaskState.COMPLETED):
completed += 1
else:
elif not task._is_finished():
waiting_tasks.append(task)
# If the threshold was reached, get ready to fire.
@ -186,8 +185,6 @@ class Join(TaskSpec):
waiting_tasks = []
completed = 0
for task in tasks:
# Refresh path prediction.
task.task_spec._predict(task)
if not self._branch_may_merge_at(task):
completed += 1
elif self._branch_is_complete(task):
@ -204,16 +201,16 @@ class Join(TaskSpec):
Returns True if the threshold was reached, False otherwise.
Also returns the list of tasks that yet need to be completed.
"""
# If the threshold was already reached, there is nothing else to do.
if my_task._has_state(TaskState.COMPLETED):
return True, None
if my_task._is_finished():
return False, None
if my_task._has_state(TaskState.READY):
return True, None
# Check whether we may fire.
if self.split_task is None:
return self._check_threshold_unstructured(my_task, force)
return self._check_threshold_structured(my_task, force)
else:
return self._check_threshold_structured(my_task, force)
def _update_hook(self, my_task):
# Check whether enough incoming branches have completed.
@ -224,22 +221,16 @@ class Join(TaskSpec):
if self.cancel_remaining:
for task in waiting_tasks:
task.cancel()
# Update the state of our child objects.
self._do_join(my_task)
else:
return True
elif not my_task._is_finished():
my_task._set_state(TaskState.WAITING)
def _do_join(self, my_task):
def _find_tasks(self, my_task):
split_task = self._get_split_task(my_task)
# Identify all corresponding task instances within the thread.
# Also remember which of those instances was most recently changed,
# because we are making this one the instance that will
# continue the thread of control. In other words, we will continue
# to build the task tree underneath the most recently changed task.
last_changed = None
thread_tasks = []
for task in split_task._find_any(self):
# Ignore tasks from other threads.
@ -248,27 +239,16 @@ class Join(TaskSpec):
# Ignore my outgoing branches.
if self.split_task and task._is_descendant_of(my_task):
continue
# We have found a matching instance.
thread_tasks.append(task)
return thread_tasks
# Check whether the state of the instance was recently
# changed.
changed = task.parent.last_state_change
if last_changed is None or changed > last_changed.parent.last_state_change:
last_changed = task
def _do_join(self, my_task):
# Mark the identified task instances as COMPLETED. The exception
# is the most recently changed task, for which we assume READY.
# By setting the state to READY only, we allow for calling
# :class:`Task.complete()`, which leads to the task tree being
# (re)built underneath the node.
for task in thread_tasks:
if task == last_changed:
self.entered_event.emit(my_task.workflow, my_task)
task._ready()
else:
task._set_state(TaskState.COMPLETED)
# Execution will continue from this task; mark others as cancelled
for task in self._find_tasks(my_task):
if task != my_task:
task._set_state(TaskState.CANCELLED)
task._drop_children()
def _on_trigger(self, my_task):
@ -276,10 +256,11 @@ class Join(TaskSpec):
May be called to fire the Join before the incoming branches are
completed.
"""
for task in my_task.workflow.task_tree._find_any(self):
if task.thread_id != my_task.thread_id:
continue
self._do_join(task)
tasks = sorted(self._find_tasks(my_task), key=lambda t: t.last_state_change)
for task in tasks[:-1]:
task._set_state(TaskState.CANCELLED)
task._drop_children()
tasks[-1]._ready()
def serialize(self, serializer):
return serializer.serialize_join(self)

View File

@ -1,73 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2021 Sartography
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from .base import TaskSpec
from ..task import TaskState
from SpiffWorkflow.exceptions import WorkflowTaskException
class LoopResetTask(TaskSpec):
"""
This task is used as a placeholder when we are going to loopback
to a previous point in the workflow. When this task is completed,
it will reset the workflow back to a previous point.
"""
def __init__(self, wf_spec, name, destination_id, destination_spec_name, **kwargs):
"""
Constructor.
:param script: the script that must be executed by the script engine.
"""
super(LoopResetTask, self).__init__(wf_spec, name, **kwargs)
self.destination_id = destination_id
self.destination_spec_name = destination_spec_name
def _on_complete_hook(self, task):
try:
# Prefer the exact task id, but if not available, use the
# last instance of the task_spec.
destination = task.workflow.get_task(self.destination_id)
if not destination:
destination = task.workflow.get_tasks_from_spec_name(
self.destination_spec_name)[-1]
destination.reset_token(task.data, reset_data=False)
except Exception as e:
# set state to WAITING (because it is definitely not COMPLETED)
# and raise WorkflowException pointing to this task because
# maybe upstream someone will be able to handle this situation
task._set_state(TaskState.WAITING)
if isinstance(e, WorkflowTaskException):
e.add_note('Error occurred during a loop back to a previous step.')
raise e
else:
raise WorkflowTaskException(
'Error during loop back:' + str(e), task=task, exception=e)
super(LoopResetTask, self)._on_complete_hook(task)
def serialize(self, serializer):
return serializer.serialize_loop_reset_task(self)
@classmethod
def deserialize(cls, serializer, wf_spec, s_state):
return serializer.deserialize_loop_reset_task(wf_spec, s_state)

View File

@ -89,32 +89,18 @@ class MultiChoice(TaskSpec):
# The caller needs to make sure that predict() is called.
def _predict_hook(self, my_task):
if self.choice:
outputs = [self._wf_spec.get_task_spec_from_name(o)
for o in self.choice]
else:
outputs = self.outputs
# Default to MAYBE for all conditional outputs, default to LIKELY
# for unconditional ones. We can not default to FUTURE, because
# a call to trigger() may override the unconditional paths.
my_task._sync_children(outputs)
if not my_task._is_definite():
best_state = my_task.state
else:
best_state = TaskState.LIKELY
# Collect a list of all unconditional outputs.
outputs = []
conditional, unconditional = [], []
for condition, output in self.cond_task_specs:
if condition is None:
outputs.append(self._wf_spec.get_task_spec_from_name(output))
for child in my_task.children:
if child._is_definite():
if self.choice is not None and output not in self.choice:
continue
if child.task_spec in outputs:
child._set_state(best_state)
if condition is None:
unconditional.append(self._wf_spec.get_task_spec_from_name(output))
else:
conditional.append(self._wf_spec.get_task_spec_from_name(output))
state = TaskState.MAYBE if my_task.state == TaskState.MAYBE else TaskState.LIKELY
my_task._sync_children(unconditional, state)
for spec in conditional:
my_task._add_child(spec, TaskState.MAYBE)
def _get_matching_outputs(self, my_task):
outputs = []
@ -125,12 +111,12 @@ class MultiChoice(TaskSpec):
outputs.append(self._wf_spec.get_task_spec_from_name(output))
return outputs
def _on_complete_hook(self, my_task):
"""
Runs the task. Should not be called directly.
Returns True if completed, False otherwise.
"""
def _run_hook(self, my_task):
"""Runs the task. Should not be called directly."""
my_task._sync_children(self._get_matching_outputs(my_task), TaskState.FUTURE)
for child in my_task.children:
child.task_spec._predict(child, mask=TaskState.FUTURE|TaskState.PREDICTED_MASK)
return True
def serialize(self, serializer):
return serializer.serialize_multi_choice(self)

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from builtins import range
# Copyright (C) 2007 Samuel Abels
#
# This library is free software; you can redistribute it and/or
@ -75,33 +74,24 @@ class MultiInstance(TaskSpec):
for output in self.outputs:
new_task = my_task._add_child(output, state)
new_task.triggered = True
output._predict(new_task)
output._predict(new_task, mask=TaskState.FUTURE|TaskState.READY|TaskState.PREDICTED_MASK)
def _get_predicted_outputs(self, my_task):
split_n = int(valueof(my_task, self.times, 1))
# Predict the outputs.
outputs = []
for i in range(split_n):
outputs += self.outputs
return outputs
return self.outputs * split_n
def _predict_hook(self, my_task):
split_n = int(valueof(my_task, self.times, 1))
my_task._set_internal_data(splits=split_n)
# Create the outgoing tasks.
outputs = []
for i in range(split_n):
outputs += self.outputs
outputs = self._get_predicted_outputs(my_task)
if my_task._is_definite():
my_task._sync_children(outputs, TaskState.FUTURE)
else:
my_task._sync_children(outputs, TaskState.LIKELY)
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
outputs = self._get_predicted_outputs(my_task)
my_task._sync_children(outputs, TaskState.FUTURE)
self._predict(my_task, mask=TaskState.FUTURE|TaskState.PREDICTED_MASK)
return True
def serialize(self, serializer):
return serializer.serialize_multi_instance(self)

View File

@ -47,10 +47,10 @@ class ReleaseMutex(TaskSpec):
TaskSpec.__init__(self, wf_spec, name, **kwargs)
self.mutex = mutex
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
mutex = my_task.workflow._get_mutex(self.mutex)
mutex.unlock()
TaskSpec._on_complete_hook(self, my_task)
return True
def serialize(self, serializer):
return serializer.serialize_release_mutex(self)

View File

@ -98,41 +98,31 @@ class SubWorkflow(TaskSpec):
xml = etree.parse(fp).getroot()
wf_spec = WorkflowSpec.deserialize(serializer, xml, filename=file_name)
outer_workflow = my_task.workflow.outer_workflow
return Workflow(wf_spec, parent=outer_workflow)
def _on_ready_before_hook(self, my_task):
subworkflow = self._create_subworkflow(my_task)
subworkflow.completed_event.connect(
self._on_subworkflow_completed, my_task)
self._integrate_subworkflow_tree(my_task, subworkflow)
my_task._set_internal_data(subworkflow=subworkflow)
def _integrate_subworkflow_tree(self, my_task, subworkflow):
# Integrate the tree of the subworkflow into the tree of this workflow.
my_task._sync_children(self.outputs, TaskState.LIKELY)
subworkflow = Workflow(wf_spec, parent=outer_workflow)
my_task._sync_children(self.outputs, TaskState.FUTURE)
for child in subworkflow.task_tree.children:
my_task.children.insert(0, child)
child.parent = my_task
child.state = TaskState.READY
subworkflow.completed_event.connect(self._on_subworkflow_completed, my_task)
my_task._set_internal_data(subworkflow=subworkflow)
my_task._set_state(TaskState.WAITING)
def _on_ready_hook(self, my_task):
def _run_hook(self, my_task):
# Assign variables, if so requested.
subworkflow = my_task._get_internal_data('subworkflow')
for child in subworkflow.task_tree.children:
for assignment in self.in_assign:
assignment.assign(my_task, child)
child.task_spec._update(child)
# Instead of completing immediately, we'll wait for the subworkflow to complete
my_task._set_state(TaskState.WAITING)
return True
def _update_hook(self, my_task):
super()._update_hook(my_task)
subworkflow = my_task._get_internal_data('subworkflow')
if subworkflow is None:
# On the first update, we have to create the subworkflow
return True
self._create_subworkflow(my_task)
elif subworkflow.is_completed():
# Then wait until it finishes to complete
my_task.complete()
def _on_subworkflow_completed(self, subworkflow, my_task):

View File

@ -62,8 +62,7 @@ class ThreadMerge(Join):
# task that did the conditional parallel split.
split_task = my_task._find_ancestor_from_name(self.split_task)
if split_task is None:
msg = 'Join with %s, which was not reached' % self.split_task
raise WorkflowException(msg, task_spec=self)
raise WorkflowException(f'Join with %s, which was not reached {self.split_task}', task_spec=self)
tasks = split_task.task_spec._get_activated_threads(split_task)
# The default threshold is the number of threads that were started.
@ -105,8 +104,7 @@ class ThreadMerge(Join):
my_task._set_state(TaskState.WAITING)
return
split_task_spec = my_task.workflow.get_task_spec_from_name(
self.split_task)
split_task_spec = my_task.workflow.get_task_spec_from_name(self.split_task)
split_task = my_task._find_ancestor(split_task_spec)
# Find the inbound task that was completed last.

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from builtins import range
# Copyright (C) 2007 Samuel Abels
#
# This library is free software; you can redistribute it and/or
@ -108,31 +107,25 @@ class ThreadSplit(TaskSpec):
new_task = my_task.add_child(output, TaskState.READY)
new_task.triggered = True
def _predict_hook(self, my_task):
def _get_predicted_outputs(self, my_task):
split_n = int(valueof(my_task, self.times))
return [self.thread_starter] * split_n
def _predict_hook(self, my_task):
# if we were created with thread_starter suppressed, connect it now.
if self.thread_starter is None:
self.thread_starter = self.outputs[0]
# Predict the outputs.
outputs = []
for i in range(split_n):
outputs.append(self.thread_starter)
outputs = self._get_predicted_outputs(my_task)
if my_task._is_definite():
my_task._sync_children(outputs, TaskState.FUTURE)
else:
my_task._sync_children(outputs, TaskState.LIKELY)
def _on_complete_hook(self, my_task):
# Split, and remember the number of splits in the context data.
split_n = int(valueof(my_task, self.times))
# Create the outgoing tasks.
outputs = []
for i in range(split_n):
outputs.append(self.thread_starter)
def _run_hook(self, my_task):
outputs = self._get_predicted_outputs(my_task)
my_task._sync_children(outputs, TaskState.FUTURE)
return True
def serialize(self, serializer):
return serializer.serialize_thread_split(self)

View File

@ -17,6 +17,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from .base import TaskSpec
from SpiffWorkflow.task import TaskState
class ThreadStart(TaskSpec):
@ -42,9 +43,10 @@ class ThreadStart(TaskSpec):
TaskSpec.__init__(self, wf_spec, name, **kwargs)
self.internal = True
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
my_task._assign_new_thread_id()
TaskSpec._on_complete_hook(self, my_task)
my_task._sync_children(self.outputs, TaskState.READY)
return True
def serialize(self, serializer):
return serializer.serialize_thread_start(self)

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from builtins import range
# Copyright (C) 2007 Samuel Abels
#
# This library is free software; you can redistribute it and/or
@ -65,15 +64,14 @@ class Trigger(TaskSpec):
self.queued += 1
# All tasks that have already completed need to be put back to
# READY.
for thetask in my_task.workflow.task_tree:
if thetask.thread_id != my_task.thread_id:
for task in my_task.workflow.task_tree:
if task.thread_id != my_task.thread_id:
continue
if (thetask.task_spec == self and
thetask._has_state(TaskState.COMPLETED)):
thetask._set_state(TaskState.FUTURE)
thetask._ready()
if task.task_spec == self and task._has_state(TaskState.COMPLETED):
task._set_state(TaskState.FUTURE)
task._ready()
def _on_complete_hook(self, my_task):
def _run_hook(self, my_task):
"""
A hook into _on_complete() that does the task specific work.
@ -85,10 +83,10 @@ class Trigger(TaskSpec):
times = int(valueof(my_task, self.times, 1)) + self.queued
for i in range(times):
for task_name in self.context:
task = my_task.workflow.get_task_spec_from_name(task_name)
task._on_trigger(my_task)
task_spec = my_task.workflow.get_task_spec_from_name(task_name)
task_spec._on_trigger(my_task)
self.queued = 0
TaskSpec._on_complete_hook(self, my_task)
return True
def serialize(self, serializer):
return serializer.serialize_trigger(self)

View File

@ -74,10 +74,6 @@ class TaskSpec(object):
:param wf_spec: A reference to the workflow specification that owns it.
:type name: string
:param name: A name for the task.
:type lock: list(str)
:param lock: A list of mutex names. The mutex is acquired
on entry of execute() and released on leave of
execute().
:type manual: bool
:param manual: Whether this task requires a manual action to complete.
:type data: dict((str, object))
@ -107,7 +103,6 @@ class TaskSpec(object):
self.defines = kwargs.get('defines', {})
self.pre_assign = kwargs.get('pre_assign',[])
self.post_assign = kwargs.get('post_assign', [])
self.locks = kwargs.get('lock', [])
self.lookahead = 2 # Maximum number of MAYBE predictions.
# Events.
@ -213,7 +208,7 @@ class TaskSpec(object):
if len(self.inputs) < 1:
raise WorkflowException(self, 'No input task connected.')
def _predict(self, my_task, seen=None, looked_ahead=0):
def _predict(self, my_task, seen=None, looked_ahead=0, mask=TaskState.PREDICTED_MASK):
"""
Updates the branch such that all possible future routes are added.
@ -229,26 +224,25 @@ class TaskSpec(object):
if seen is None:
seen = []
self._predict_hook(my_task)
if not my_task._is_definite():
if my_task._has_state(mask):
self._predict_hook(my_task)
if my_task._is_predicted():
seen.append(self)
look_ahead = my_task._is_definite() or looked_ahead + 1 < self.lookahead
for child in my_task.children:
if not child._is_finished() and child not in seen and look_ahead:
child.task_spec._predict(child, seen[:], looked_ahead + 1)
if child._has_state(mask) and child not in seen and look_ahead:
child.task_spec._predict(child, seen[:], looked_ahead + 1, mask)
def _predict_hook(self, my_task):
# If the task's status is not predicted, we default to FUTURE for all it's outputs.
# If the task's status is definite, we default to FUTURE for all it's outputs.
# Otherwise, copy my own state to the children.
if my_task._is_definite():
if my_task._is_definite():
best_state = TaskState.FUTURE
else:
best_state = my_task.state
my_task._sync_children(self.outputs, best_state)
for child in my_task.children:
if not child._is_definite():
child._set_state(best_state)
def _update(self, my_task):
"""
@ -281,42 +275,13 @@ class TaskSpec(object):
assert my_task is not None
self.test()
# Acquire locks, if any.
for lock in self.locks:
mutex = my_task.workflow._get_mutex(lock)
if not mutex.testandset():
return
# Assign variables, if so requested.
for assignment in self.pre_assign:
assignment.assign(my_task, my_task)
# Run task-specific code.
self._on_ready_before_hook(my_task)
self.reached_event.emit(my_task.workflow, my_task)
self._on_ready_hook(my_task)
# Run user code, if any.
if self.ready_event.emit(my_task.workflow, my_task):
# Assign variables, if so requested.
for assignment in self.post_assign:
assignment.assign(my_task, my_task)
# Release locks, if any.
for lock in self.locks:
mutex = my_task.workflow._get_mutex(lock)
mutex.unlock()
self.finished_event.emit(my_task.workflow, my_task)
def _on_ready_before_hook(self, my_task):
"""
A hook into _on_ready() that does the task specific work.
:type my_task: Task
:param my_task: The associated task in the task tree.
"""
pass
self.reached_event.emit(my_task.workflow, my_task)
def _on_ready_hook(self, my_task):
"""
@ -327,6 +292,35 @@ class TaskSpec(object):
"""
pass
def _run(self, my_task):
"""
Run the task.
:type my_task: Task
:param my_task: The associated task in the task tree.
:rtype: boolean or None
:returns: the value returned by the task spec's run method.
"""
result = self._run_hook(my_task)
# Run user code, if any.
if self.ready_event.emit(my_task.workflow, my_task):
# Assign variables, if so requested.
for assignment in self.post_assign:
assignment.assign(my_task, my_task)
self.finished_event.emit(my_task.workflow, my_task)
return result
def _run_hook(self, my_task):
"""
A hook into _run() that does the task specific work.
:type my_task: Task
:param my_task: The associated task in the task tree.
"""
return True
def _on_cancel(self, my_task):
"""
May be called by another task to cancel the operation before it was
@ -359,20 +353,12 @@ class TaskSpec(object):
:rtype: boolean
:returns: True on success, False otherwise.
"""
assert my_task is not None
# We have to set the last task here, because the on_complete_hook
# of a loopback task may overwrite what the last_task will be.
my_task.workflow.last_task = my_task
self._on_complete_hook(my_task)
for child in my_task.children:
# Don't like this, but this is the most expedient way of preventing cancelled tasks from reactivation
if child.state != TaskState.CANCELLED:
if not child._is_finished():
child.task_spec._update(child)
my_task.workflow._task_completed_notify(my_task)
self.completed_event.emit(my_task.workflow, my_task)
return True
def _on_complete_hook(self, my_task):
"""
@ -419,7 +405,6 @@ class TaskSpec(object):
'defines':self.defines,
'pre_assign':self.pre_assign,
'post_assign':self.post_assign,
'locks':self.locks,
'lookahead':self.lookahead,
}
@ -457,7 +442,6 @@ class TaskSpec(object):
out.defines = s_state.get('defines')
out.pre_assign = s_state.get('pre_assign')
out.post_assign = s_state.get('post_assign')
out.locks = s_state.get('locks')
out.lookahead = s_state.get('lookahead')
return out

View File

@ -6,6 +6,7 @@ from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnValidator, full_tag
from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent
from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent
from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import IntermediateThrowEvent, BoundaryEvent, IntermediateCatchEvent
from SpiffWorkflow.spiff.specs.none_task import NoneTask
from SpiffWorkflow.spiff.specs.manual_task import ManualTask
from SpiffWorkflow.spiff.specs.user_task import UserTask
@ -13,10 +14,18 @@ from SpiffWorkflow.spiff.specs.script_task import ScriptTask
from SpiffWorkflow.spiff.specs.subworkflow_task import SubWorkflowTask, TransactionSubprocess, CallActivity
from SpiffWorkflow.spiff.specs.service_task import ServiceTask
from SpiffWorkflow.spiff.specs.events.event_types import SendTask, ReceiveTask
from SpiffWorkflow.spiff.parser.task_spec import SpiffTaskParser, SubWorkflowParser, CallActivityParser, ServiceTaskParser, ScriptTaskParser
from SpiffWorkflow.spiff.specs.business_rule_task import BusinessRuleTask
from SpiffWorkflow.spiff.parser.task_spec import (
SpiffTaskParser,
SubWorkflowParser,
CallActivityParser,
ServiceTaskParser,
ScriptTaskParser,
BusinessRuleTaskParser
)
from SpiffWorkflow.spiff.parser.event_parsers import (SpiffStartEventParser, SpiffEndEventParser, SpiffBoundaryEventParser,
SpiffIntermediateCatchEventParser, SpiffIntermediateThrowEventParser, SpiffSendTaskParser, SpiffReceiveTaskParser)
from SpiffWorkflow.dmn.specs import BusinessRuleTask
from SpiffWorkflow.spiff.parser.task_spec import BusinessRuleTaskParser
@ -44,4 +53,3 @@ class SpiffBpmnParser(BpmnDmnParser):
full_tag('receiveTask'): (SpiffReceiveTaskParser, ReceiveTask),
full_tag('businessRuleTask'): (BusinessRuleTaskParser, BusinessRuleTask)
}

View File

@ -1,11 +1,11 @@
from lxml import etree
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.bpmn.parser.TaskParser import TaskParser
from SpiffWorkflow.bpmn.parser.task_parsers import SubprocessParser
from SpiffWorkflow.bpmn.parser.util import xpath_eval
from SpiffWorkflow.spiff.specs.multiinstance_task import StandardLoopTask, ParallelMultiInstanceTask, SequentialMultiInstanceTask
from SpiffWorkflow.spiff.specs.business_rule_task import BusinessRuleTask
SPIFFWORKFLOW_MODEL_NS = 'http://spiffworkflow.org/bpmn/schema/1.0/core'
SPIFFWORKFLOW_MODEL_PREFIX = 'spiffworkflow'
@ -169,13 +169,19 @@ class BusinessRuleTaskParser(SpiffTaskParser):
def create_task(self):
decision_ref = self.get_decision_ref(self.node)
return BusinessRuleTask(self.spec,
self.get_task_spec_name(),
dmnEngine=self.process_parser.parser.get_engine(decision_ref, self.node),
lane=self.lane,
position=self.position,
description=self.node.get('name', None)
)
extensions = self.parse_extensions()
prescript = extensions.get('preScript')
postscript = extensions.get('postScript')
return BusinessRuleTask(
self.spec,
self.get_task_spec_name(),
dmnEngine=self.process_parser.parser.get_engine(decision_ref, self.node),
lane=self.lane,
position=self.position,
description=self.node.get('name', None),
prescript=prescript,
postscript=postscript,
)
@staticmethod
def get_decision_ref(node):

View File

@ -5,7 +5,6 @@ from SpiffWorkflow.bpmn.serializer.task_spec import (
SimpleTaskConverter,
StartTaskConverter,
EndJoinConverter,
LoopResetTaskConverter,
StartEventConverter,
EndEventConverter,
IntermediateCatchEventConverter,
@ -32,6 +31,7 @@ from .task_spec import (
CallActivityTaskConverter,
ParallelMultiInstanceTaskConverter,
SequentialMultiInstanceTaskConverter,
BusinessRuleTaskConverter,
)
from SpiffWorkflow.bpmn.serializer.event_definition import MessageEventDefinitionConverter as DefaultMessageEventDefinitionConverter
@ -42,7 +42,6 @@ SPIFF_SPEC_CONFIG['task_specs'] = [
SimpleTaskConverter,
StartTaskConverter,
EndJoinConverter,
LoopResetTaskConverter,
StartEventConverter,
EndEventConverter,
IntermediateCatchEventConverter,
@ -66,6 +65,7 @@ SPIFF_SPEC_CONFIG['task_specs'] = [
StandardLoopTaskConverter,
ParallelMultiInstanceTaskConverter,
SequentialMultiInstanceTaskConverter,
BusinessRuleTaskConverter
]
SPIFF_SPEC_CONFIG['event_definitions'].remove(DefaultMessageEventDefinitionConverter)
SPIFF_SPEC_CONFIG['event_definitions'].append(MessageEventDefinitionConverter)

View File

@ -1,5 +1,6 @@
from SpiffWorkflow.bpmn.serializer.helpers.spec import TaskSpecConverter
from SpiffWorkflow.bpmn.serializer.task_spec import MultiInstanceTaskConverter
from SpiffWorkflow.dmn.serializer.task_spec import BaseBusinessRuleTaskConverter
from SpiffWorkflow.spiff.specs.none_task import NoneTask
from SpiffWorkflow.spiff.specs.manual_task import ManualTask
@ -9,6 +10,7 @@ from SpiffWorkflow.spiff.specs.service_task import ServiceTask
from SpiffWorkflow.spiff.specs.subworkflow_task import SubWorkflowTask, TransactionSubprocess, CallActivity
from SpiffWorkflow.spiff.specs.events.event_types import SendTask, ReceiveTask
from SpiffWorkflow.spiff.specs.multiinstance_task import StandardLoopTask, ParallelMultiInstanceTask, SequentialMultiInstanceTask
from SpiffWorkflow.spiff.specs.business_rule_task import BusinessRuleTask
class SpiffBpmnTaskConverter(TaskSpecConverter):
@ -39,6 +41,16 @@ class UserTaskConverter(SpiffBpmnTaskConverter):
super().__init__(UserTask, registry)
class BusinessRuleTaskConverter(BaseBusinessRuleTaskConverter, SpiffBpmnTaskConverter):
def __init__(self, registry):
super().__init__(BusinessRuleTask, registry)
def to_dict(self, spec):
dct = BaseBusinessRuleTaskConverter.to_dict(self, spec)
dct.update(SpiffBpmnTaskConverter.to_dict(self, spec))
return dct
class SendTaskConverter(SpiffBpmnTaskConverter):
def __init__(self, registry, typename=None):

View File

@ -0,0 +1,5 @@
from SpiffWorkflow.spiff.specs.spiff_task import SpiffBpmnTask
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask as DefaultBusinessRuleTask
class BusinessRuleTask(DefaultBusinessRuleTask, SpiffBpmnTask):
pass

View File

@ -77,9 +77,9 @@ class TaskState:
CANCELLED = 64
FINISHED_MASK = CANCELLED | COMPLETED
DEFINITE_MASK = FUTURE | WAITING | READY | FINISHED_MASK
PREDICTED_MASK = FUTURE | LIKELY | MAYBE
NOT_FINISHED_MASK = PREDICTED_MASK | WAITING | READY
DEFINITE_MASK = FUTURE | WAITING | READY
PREDICTED_MASK = LIKELY | MAYBE
NOT_FINISHED_MASK = PREDICTED_MASK | DEFINITE_MASK
ANY_MASK = FINISHED_MASK | NOT_FINISHED_MASK
@ -292,43 +292,6 @@ class Task(object, metaclass=DeprecatedMetaTask):
self.data = DeepMerge.merge(self.data, data)
data_log.info('Data update', extra=self.log_info())
def set_children_future(self):
"""
for a parallel gateway, we need to set up our
children so that the gateway figures out that it needs to join up
the inputs - otherwise our child process never gets marked as
'READY'
"""
if not self.task_spec.task_should_set_children_future(self):
return
self.task_spec.task_will_set_children_future(self)
# now we set this one to execute
self._set_state(TaskState.MAYBE)
self._sync_children(self.task_spec.outputs)
for child in self.children:
child.set_children_future()
def reset_token(self, data, reset_data=False):
"""
Resets the token to this task. This should allow a trip 'back in time'
as it were to items that have already been completed.
:type reset_data: bool
:param reset_data: Do we want to have the data be where we left of in
this task or not
"""
self.internal_data = {}
if not reset_data and self.workflow.last_task and self.workflow.last_task.data:
# This is a little sly, the data that will get inherited should
# be from the last completed task, but we don't want to alter
# the tree, so we just set the parent's data to the given data.
self.parent.data = copy.deepcopy(data)
self.workflow.last_task = self.parent
self.set_children_future() # this method actually fixes the problem
self._set_state(TaskState.FUTURE)
self.task_spec._update(self)
def __iter__(self):
return Task.Iterator(self)
@ -366,9 +329,7 @@ class Task(object, metaclass=DeprecatedMetaTask):
self.children.remove(task)
def _has_state(self, state):
"""
Returns True if the Task has the given state flag set.
"""
"""Returns True if the Task has the given state flag set."""
return (self.state & state) != 0
def _is_finished(self):
@ -380,6 +341,43 @@ class Task(object, metaclass=DeprecatedMetaTask):
def _is_definite(self):
return self._has_state(TaskState.DEFINITE_MASK)
def set_children_future(self):
"""
for a parallel gateway, we need to set up our
children so that the gateway figures out that it needs to join up
the inputs - otherwise our child process never gets marked as
'READY'
"""
if not self.task_spec.task_should_set_children_future(self):
return
self.task_spec.task_will_set_children_future(self)
# now we set this one to execute
self._set_state(TaskState.MAYBE)
self._sync_children(self.task_spec.outputs)
for child in self.children:
child.set_children_future()
def reset_token(self, data, reset_data=False):
"""
Resets the token to this task. This should allow a trip 'back in time'
as it were to items that have already been completed.
:type reset_data: bool
:param reset_data: Do we want to have the data be where we left of in
this task or not
"""
self.internal_data = {}
if not reset_data and self.workflow.last_task and self.workflow.last_task.data:
# This is a little sly, the data that will get inherited should
# be from the last completed task, but we don't want to alter
# the tree, so we just set the parent's data to the given data.
self.parent.data = copy.deepcopy(data)
self.workflow.last_task = self.parent
self.set_children_future() # this method actually fixes the problem
self._set_state(TaskState.FUTURE)
self.task_spec._update(self)
def _add_child(self, task_spec, state=TaskState.MAYBE):
"""
Adds a new child and assigns the given TaskSpec to it.
@ -391,17 +389,60 @@ class Task(object, metaclass=DeprecatedMetaTask):
:rtype: Task
:returns: The new child task.
"""
if task_spec is None:
raise ValueError(self, '_add_child() requires a TaskSpec')
if self._is_predicted() and state & TaskState.PREDICTED_MASK == 0:
msg = 'Attempt to add non-predicted child to predicted task'
raise WorkflowException(msg, task_spec=self.task_spec)
raise WorkflowException('Attempt to add non-predicted child to predicted task', task_spec=self.task_spec)
task = Task(self.workflow, task_spec, self, state=state)
task.thread_id = self.thread_id
if state == TaskState.READY:
task._ready()
return task
def _sync_children(self, task_specs, state=TaskState.MAYBE):
"""
This method syncs up the task's children with the given list of task
specs. In other words::
- Add one child for each given TaskSpec, unless that child already
exists.
- Remove all children for which there is no spec in the given list,
unless it is a "triggered" task.
.. note::
It is an error if the task has a non-predicted child that is
not given in the TaskSpecs.
:type task_specs: list(TaskSpec)
:param task_specs: The list of task specs that may become children.
:type state: integer
:param state: The bitmask of states for the new children.
"""
if task_specs is None:
raise ValueError('"task_specs" argument is None')
new_children = task_specs[:]
# Create a list of all children that are no longer needed.
unneeded_children = []
for child in self.children:
if child.triggered:
# Triggered tasks are never removed.
pass
elif child.task_spec in new_children:
# If the task already exists, remove it from to-be-added and update its state
new_children.remove(child.task_spec)
if not child._is_finished():
child._set_state(state)
else:
if child._is_definite():
# Definite tasks must not be removed, so they HAVE to be in the given task spec list.
raise WorkflowException(f'removal of non-predicted child {child}', task_spec=self.task_spec)
unneeded_children.append(child)
# Update children accordingly
for child in unneeded_children:
self.children.remove(child)
for task_spec in new_children:
self._add_child(task_spec, state)
def _assign_new_thread_id(self, recursive=True):
"""
Assigns a new thread id to the task.
@ -419,78 +460,6 @@ class Task(object, metaclass=DeprecatedMetaTask):
child.thread_id = self.thread_id
return self.thread_id
def _sync_children(self, task_specs, state=TaskState.MAYBE):
"""
This method syncs up the task's children with the given list of task
specs. In other words::
- Add one child for each given TaskSpec, unless that child already
exists.
- Remove all children for which there is no spec in the given list,
unless it is a "triggered" task.
- Handle looping back to previous tasks, so we don't end up with
an infinitely large tree.
.. note::
It is an error if the task has a non-predicted child that is
not given in the TaskSpecs.
:type task_specs: list(TaskSpec)
:param task_specs: The list of task specs that may become children.
:type state: integer
:param state: The bitmask of states for the new children.
"""
if task_specs is None:
raise ValueError('"task_specs" argument is None')
new_children = task_specs[:]
# If a child task_spec is also an ancestor, we are looping back,
# replace those specs with a loopReset task.
root_task = self._get_root()
for index, task_spec in enumerate(new_children):
ancestor_task = self._find_ancestor(task_spec)
if ancestor_task and ancestor_task != root_task:
destination = ancestor_task
new_spec = self.workflow.get_reset_task_spec(destination)
new_spec.outputs = []
new_spec.inputs = task_spec.inputs
new_children[index] = new_spec
# Create a list of all children that are no longer needed.
unneeded_children = []
for child in self.children:
# Triggered tasks are never removed.
if child.triggered:
continue
# If the task already exists, remove it from to-be-added
if child.task_spec in new_children:
new_children.remove(child.task_spec)
# We should set the state here but that breaks everything
continue
# Definite tasks must not be removed, so they HAVE to be in the given task spec list.
if child._is_definite():
raise WorkflowException(f'removal of non-predicted child {child}', task_spec=self.task_spec)
unneeded_children.append(child)
# Remove and add the children accordingly.
for child in unneeded_children:
self.children.remove(child)
for task_spec in new_children:
self._add_child(task_spec, state)
def _set_likely_task(self, task_specs):
if not isinstance(task_specs, list):
task_specs = [task_specs]
for task_spec in task_specs:
for child in self.children:
if child.task_spec != task_spec:
continue
if child._is_definite():
continue
child._set_state(TaskState.LIKELY)
def _is_descendant_of(self, parent):
"""
Returns True if parent is in the list of ancestors, returns False
@ -574,15 +543,6 @@ class Task(object, metaclass=DeprecatedMetaTask):
return self.parent
return self.parent._find_ancestor_from_name(name)
def _ready(self):
"""
Marks the task as ready for execution.
"""
if self._has_state(TaskState.COMPLETED) or self._has_state(TaskState.CANCELLED):
return
self._set_state(TaskState.READY)
self.task_spec._on_ready(self)
def get_name(self):
return str(self.task_spec.name)
@ -590,14 +550,10 @@ class Task(object, metaclass=DeprecatedMetaTask):
return str(self.task_spec.description)
def get_state_name(self):
"""
Returns a textual representation of this Task's state.
"""
state_name = []
"""Returns a textual representation of this Task's state."""
for state, name in list(TaskStateNames.items()):
if self._has_state(state):
state_name.append(name)
return '|'.join(state_name)
return name
def get_spec_data(self, name=None, default=None):
"""
@ -648,37 +604,54 @@ class Task(object, metaclass=DeprecatedMetaTask):
"""
return self.data.get(name, default)
def cancel(self):
"""
Cancels the item if it was not yet completed, and removes
any children that are LIKELY.
"""
if self._is_finished():
for child in self.children:
child.cancel()
def _ready(self):
"""Marks the task as ready for execution."""
if self._has_state(TaskState.COMPLETED) or self._has_state(TaskState.CANCELLED):
return
self._set_state(TaskState.CANCELLED)
self._drop_children()
self.task_spec._on_cancel(self)
self._set_state(TaskState.READY)
self.task_spec._on_ready(self)
def complete(self):
def run(self):
"""
Called by the associated task to let us know that its state
has changed (e.g. from FUTURE to COMPLETED.)
Execute the task.
If the return value of task_spec._run is None, assume the task is not finished,
and move the task to WAITING.
:rtype: boolean or None
:returns: the value returned by the task spec's run method
"""
self._set_state(TaskState.COMPLETED)
# I am taking back my previous comment about running the task after it's completed being "CRAZY"
# Turns out that tasks are in fact supposed to be complete at this point and I've been wrong all along
# about when tasks should actually be executed
start = time.time()
retval = self.task_spec._on_complete(self)
retval = self.task_spec._run(self)
extra = self.log_info({
'action': 'Complete',
'elapsed': time.time() - start
})
metrics.debug('', extra=extra)
if retval is None:
self._set_state(TaskState.WAITING)
else:
# If we add an error state, the we can move the task to COMPLETE or ERROR
# according to the return value.
self.complete()
return retval
def cancel(self):
"""Cancels the item if it was not yet completed, and removes any children that are LIKELY."""
if self._is_finished():
for child in self.children:
child.cancel()
else:
self._set_state(TaskState.CANCELLED)
self._drop_children()
self.task_spec._on_cancel(self)
def complete(self):
"""Marks this task complete."""
self._set_state(TaskState.COMPLETED)
self.task_spec._on_complete(self)
self.workflow.last_task = self
def trigger(self, *args):
"""
If recursive is True, the state is applied to the tree recursively.

View File

@ -1,30 +1,22 @@
from builtins import object
from threading import Lock
try:
# python 2
from mutex import mutex
class mutex(object):
except ImportError:
# python 3
from threading import Lock
def __init__(self):
self.lock = Lock()
class mutex(object):
def lock(self):
raise NotImplementedError
def __init__(self):
self.lock = Lock()
def lock(self):
raise NotImplementedError
def test(self):
has = self.lock.acquire(blocking=False)
if has:
self.lock.release()
return has
def testandset(self):
return self.lock.acquire(blocking=False)
def unlock(self):
def test(self):
has = self.lock.acquire(blocking=False)
if has:
self.lock.release()
return has
def testandset(self):
return self.lock.acquire(blocking=False)
def unlock(self):
self.lock.release()

View File

@ -20,14 +20,14 @@
import logging
from .specs.Simple import Simple
from .specs.LoopResetTask import LoopResetTask
from .task import Task, TaskState
from .util.compat import mutex
from .util.event import Event
from .exceptions import WorkflowException
from .exceptions import TaskNotFoundException, WorkflowException
logger = logging.getLogger('spiff')
class Workflow(object):
"""
@ -54,29 +54,25 @@ class Workflow(object):
self.outer_workflow = kwargs.get('parent', self)
self.locks = {}
self.last_task = None
if deserializing:
assert 'Root' in workflow_spec.task_specs
root = workflow_spec.task_specs['Root'] # Probably deserialized
if 'Root' in workflow_spec.task_specs:
root = workflow_spec.task_specs['Root']
else:
if 'Root' in workflow_spec.task_specs:
root = workflow_spec.task_specs['Root']
else:
root = Simple(workflow_spec, 'Root')
logger.info('Initialize', extra=self.log_info())
root = Simple(workflow_spec, 'Root')
# Setting TaskState.COMPLETED prevents the root task from being executed.
self.task_tree = Task(self, root, state=TaskState.COMPLETED)
start = self.task_tree._add_child(self.spec.start, state=TaskState.FUTURE)
self.success = True
self.debug = False
# Events.
self.completed_event = Event()
start = self.task_tree._add_child(self.spec.start, state=TaskState.FUTURE)
self.spec.start._predict(start)
if 'parent' not in kwargs:
start.task_spec._update(start)
if not deserializing:
self._predict()
if 'parent' not in kwargs:
start.task_spec._update(start)
logger.info('Initialize', extra=self.log_info())
self.task_mapping = self._get_task_mapping()
@ -108,6 +104,10 @@ class Workflow(object):
return True
return False
def _predict(self, mask=TaskState.NOT_FINISHED_MASK):
for task in Workflow.get_tasks(self,TaskState.NOT_FINISHED_MASK):
task.task_spec._predict(task, mask=mask)
def _get_waiting_tasks(self):
waiting = Task.Iterator(self.task_tree, TaskState.WAITING)
return [w for w in waiting]
@ -195,24 +195,6 @@ class Workflow(object):
"""
return self.spec.get_task_spec_from_name(name)
def get_task(self, id,tasklist=None):
"""
Returns the task with the given id.
:type id:integer
:param id: The id of a task.
:param tasklist: Optional cache of get_tasks for operations
where we are calling multiple times as when we
are deserializing the workflow
:rtype: Task
:returns: The task with the given id.
"""
if tasklist:
tasks = [task for task in tasklist if task.id == id]
else:
tasks = [task for task in self.get_tasks() if task.id == id]
return tasks[0] if len(tasks) == 1 else None
def get_tasks_from_spec_name(self, name):
"""
Returns all tasks whose spec has the given name.
@ -222,15 +204,7 @@ class Workflow(object):
:rtype: list[Task]
:returns: A list of tasks that relate to the spec with the given name.
"""
return [task for task in self.get_tasks_iterator()
if task.task_spec.name == name]
def empty(self,str):
if str == None:
return True
if str == '':
return True
return False
return [task for task in self.get_tasks_iterator() if task.task_spec.name == name]
def get_tasks(self, state=TaskState.ANY_MASK):
"""
@ -243,38 +217,6 @@ class Workflow(object):
"""
return [t for t in Task.Iterator(self.task_tree, state)]
def reset_task_from_id(self, task_id):
"""
Runs the task with the given id.
:type task_id: integer
:param task_id: The id of the Task object.
"""
if task_id is None:
raise WorkflowException('task_id is None', task_spec=self.spec)
data = {}
if self.last_task and self.last_task.data:
data = self.last_task.data
for task in self.task_tree:
if task.id == task_id:
return task.reset_token(data)
msg = 'A task with the given task_id (%s) was not found' % task_id
raise WorkflowException(msg, task_spec=self.spec)
def get_reset_task_spec(self, destination):
"""
Returns a task, that once complete, will reset the workflow back
to a previously completed task.
:param destination: Task to reset to, on complete.
:return: TaskSpec
"""
name = "return_to_" + destination.task_spec.name
spec = self.get_task_spec_from_name(name)
if not spec:
spec = LoopResetTask(self.spec, name, destination.id,
destination.task_spec.name)
return spec
def get_tasks_iterator(self, state=TaskState.ANY_MASK):
"""
Returns a iterator of Task objects with the given state.
@ -286,22 +228,54 @@ class Workflow(object):
"""
return Task.Iterator(self.task_tree, state)
def complete_task_from_id(self, task_id):
def get_task_from_id(self, task_id, tasklist=None):
"""
Returns the task with the given id.
:type id:integer
:param id: The id of a task.
:param tasklist: Optional cache of get_tasks for operations
where we are calling multiple times as when we
are deserializing the workflow
:rtype: Task
:returns: The task with the given id.
"""
if task_id is None:
raise WorkflowException('task_id is None', task_spec=self.spec)
tasklist = tasklist or self.task_tree
for task in self.task_tree:
if task.id == task_id:
return task
msg = 'A task with the given task_id (%s) was not found' % task_id
raise TaskNotFoundException(msg, task_spec=self.spec)
def run_task_from_id(self, task_id):
"""
Runs the task with the given id.
:type task_id: integer
:param task_id: The id of the Task object.
"""
if task_id is None:
raise WorkflowException('task_id is None', task_spec=self.spec)
for task in self.task_tree:
if task.id == task_id:
return task.complete()
msg = 'A task with the given task_id (%s) was not found' % task_id
raise WorkflowException(msg, task_spec=self.spec)
task = self.get_task_from_id(task_id)
return task.run()
def complete_next(self, pick_up=True, halt_on_manual=True):
def reset_task_from_id(self, task_id):
"""
Runs the task with the given id.
:type task_id: integer
:param task_id: The id of the Task object.
"""
# Given that this is a BPMN thing it's questionable whether this belongs here at all
# However, since it calls a BPMN thing on `task`, I guess I'll leave it here
# At least it's not in both places any more
data = {}
if self.last_task and self.last_task.data:
data = self.last_task.data
task = self.get_task_from_id(task_id)
return task.reset_token(data)
def run_next(self, pick_up=True, halt_on_manual=True):
"""
Runs the next task.
Returns True if completed, False otherwise.
@ -329,7 +303,7 @@ class Workflow(object):
self.last_task = None
if task is not None:
if not (halt_on_manual and task.task_spec.manual):
if task.complete():
if task.run():
self.last_task = task
return True
blacklist.append(task)
@ -340,7 +314,7 @@ class Workflow(object):
if task._is_descendant_of(blacklisted_task):
continue
if not (halt_on_manual and task.task_spec.manual):
if task.complete():
if task.run():
self.last_task = task
return True
blacklist.append(task)
@ -353,7 +327,7 @@ class Workflow(object):
return True
return False
def complete_all(self, pick_up=True, halt_on_manual=True):
def run_all(self, pick_up=True, halt_on_manual=True):
"""
Runs all branches until completion. This is a convenience wrapper
around :meth:`complete_next`, and the pick_up argument is passed
@ -366,7 +340,7 @@ class Workflow(object):
complete any tasks that have manual=True.
See :meth:`SpiffWorkflow.specs.TaskSpec.__init__`
"""
while self.complete_next(pick_up, halt_on_manual):
while self.run_next(pick_up, halt_on_manual):
pass
def get_dump(self):

View File

@ -19,7 +19,7 @@ workflow = Workflow(spec)
# Execute until all tasks are done or require manual intervention.
# For the sake of this tutorial, we ignore the "manual" flag on the
# tasks. In practice, you probably don't want to do that.
workflow.complete_all(halt_on_manual=False)
workflow.run_all(halt_on_manual=False)
# Alternatively, this is what a UI would do for a manual task.
#workflow.complete_task_from_id(...)

View File

@ -21,7 +21,10 @@ setup(name='SpiffWorkflow',
author_email='dan@sartography.com',
license='lGPLv2',
packages=find_packages(exclude=['tests', 'tests.*']),
package_data={'SpiffWorkflow.bpmn.parser.schema': ['*.xsd']},
package_data={
'SpiffWorkflow.bpmn.parser': ['schema/*.xsd'],
'SpiffWorkflow.dmn.parser': ['schema/*.xsd'],
},
install_requires=['configparser', 'lxml', 'celery',
# required for python 3.7 - https://stackoverflow.com/a/73932581
'importlib-metadata<5.0; python_version <= "3.7"'],

View File

@ -1,103 +0,0 @@
# -*- coding: utf-8 -*-
from builtins import object
import sys
import unittest
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from lxml import etree
from SpiffWorkflow.specs.WorkflowSpec import WorkflowSpec
from SpiffWorkflow.task import Task
from SpiffWorkflow.serializer.prettyxml import XmlSerializer
from tests.SpiffWorkflow.util import run_workflow
class WorkflowTestData(object):
def __init__(self, filename, spec, path, data):
self.filename = filename
self.spec = spec
self.path = path
self.data = data
class PatternTest(unittest.TestCase):
maxDiff = None
def setUp(self):
Task.id_pool = 0
Task.thread_id_pool = 0
self.xml_path = ['data/spiff/control-flow',
'data/spiff/data',
'data/spiff/resource',
'data/spiff']
self.workflows = []
for basedir in self.xml_path:
dirname = os.path.join(os.path.dirname(__file__), basedir)
for filename in os.listdir(dirname):
if not filename.endswith(('.xml', '.py')):
continue
if filename.endswith('__.py'):
continue
filename = os.path.join(dirname, filename)
self.load_workflow_spec(filename)
def load_workflow_spec(self, filename):
# Load the .path file.
path_file = os.path.splitext(filename)[0] + '.path'
if os.path.exists(path_file):
with open(path_file) as fp:
expected_path = fp.read()
else:
expected_path = None
# Load the .data file.
data_file = os.path.splitext(filename)[0] + '.data'
if os.path.exists(data_file):
with open(data_file) as fp:
expected_data = fp.read()
else:
expected_data = None
# Test patterns that are defined in XML format.
if filename.endswith('.xml'):
with open(filename) as fp:
xml = etree.parse(fp).getroot()
serializer = XmlSerializer()
wf_spec = WorkflowSpec.deserialize(
serializer, xml, filename=filename)
# Test patterns that are defined in Python.
elif filename.endswith('.py'):
with open(filename) as fp:
code = compile(fp.read(), filename, 'exec')
thedict = {}
result = eval(code, thedict)
wf_spec = thedict['TestWorkflowSpec']()
else:
raise Exception('unsuported specification format', filename)
test_data = WorkflowTestData(
filename, wf_spec, expected_path, expected_data)
self.workflows.append(test_data)
def testWorkflowSpec(self):
for test in self.workflows:
print(test.filename)
run_workflow(self, test.spec, test.path, test.data)
def suite():
return unittest.TestLoader().loadTestsFromTestCase(PatternTest)
if __name__ == '__main__':
if len(sys.argv) == 2:
test = PatternTest('run_pattern')
test.setUp()
test.run_pattern(sys.argv[1])
sys.exit(0)
unittest.TextTestRunner(verbosity=2).run(suite())

View File

@ -2,7 +2,7 @@
import unittest
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
from .BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'matth'

View File

@ -1,10 +1,7 @@
# -*- coding: utf-8 -*-
from builtins import range
import unittest
import logging
from SpiffWorkflow.task import TaskState
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'matth'
@ -28,13 +25,9 @@ class BaseParallelTestCase(BpmnWorkflowTestCase):
"Doing step '%s' (with choice='%s')", s, choice)
else:
logging.info("Doing step '%s'", s)
# logging.debug(self.workflow.get_dump())
self.do_next_named_step(
s, choice=choice, only_one_instance=only_one_instance)
self.do_next_named_step(s, choice=choice, only_one_instance=only_one_instance)
self.workflow.do_engine_steps()
if save_restore:
# logging.debug("Before SaveRestore: \n%s" %
# self.workflow.get_dump())
self.save_restore()
self.workflow.do_engine_steps()

View File

@ -34,7 +34,7 @@ class TestUserTask(UserTask):
def do_choice(self, task, choice):
task.set_data(choice=choice)
task.complete()
task.run()
class TestExclusiveGatewayParser(ConditionalGatewayParser):

View File

@ -64,7 +64,7 @@ class BpmnWorkflowTestCase(unittest.TestCase):
def switch_workflow(p):
for task_id, sp in p.workflow._get_outermost_workflow().subprocesses.items():
if p in sp.get_tasks(workflow=sp):
return p.workflow.get_task(task_id)
return p.workflow.get_task_from_id(task_id)
def is_match(t):
if not (t.task_spec.name == step_name_path[-1] or t.task_spec.description == step_name_path[-1]):
@ -116,7 +116,7 @@ class BpmnWorkflowTestCase(unittest.TestCase):
if set_attribs:
tasks[0].set_data(**set_attribs)
tasks[0].complete()
tasks[0].run()
def save_restore(self):

View File

@ -6,7 +6,7 @@ from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.exceptions import WorkflowTaskException
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
from .BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'kellym'

View File

@ -82,7 +82,7 @@ class CollaborationTest(BpmnWorkflowTestCase):
workflow.do_engine_steps()
for idx, task in enumerate(workflow.get_ready_user_tasks()):
task.data['task_num'] = idx
task.complete()
task.run()
workflow.do_engine_steps()
ready_tasks = workflow.get_ready_user_tasks()
waiting = workflow.get_tasks_from_spec_name('get_response')
@ -94,7 +94,7 @@ class CollaborationTest(BpmnWorkflowTestCase):
# Now copy the task_num that was sent into a new variable
for task in ready_tasks:
task.data.update(init_id=task.data['task_num'])
task.complete()
task.run()
workflow.do_engine_steps()
# If the messages were routed properly, the id should match
for task in workflow.get_tasks_from_spec_name('subprocess_end'):
@ -108,7 +108,7 @@ class CollaborationTest(BpmnWorkflowTestCase):
workflow.do_engine_steps()
for idx, task in enumerate(workflow.get_ready_user_tasks()):
task.data['task_num'] = idx
task.complete()
task.run()
workflow.do_engine_steps()
# Two processes should have been started and two corresponding catch events should be waiting
@ -121,12 +121,12 @@ class CollaborationTest(BpmnWorkflowTestCase):
# Now copy the task_num that was sent into a new variable
for task in ready_tasks:
task.data.update(init_id=task.data['task_num'])
task.complete()
task.run()
workflow.do_engine_steps()
# Complete dummy tasks
for task in workflow.get_ready_user_tasks():
task.complete()
task.run()
workflow.do_engine_steps()
# Repeat for the other process, using a different mapped name
@ -136,7 +136,7 @@ class CollaborationTest(BpmnWorkflowTestCase):
self.assertEqual(len(waiting), 2)
for task in ready_tasks:
task.data.update(subprocess=task.data['task_num'])
task.complete()
task.run()
workflow.do_engine_steps()
# If the messages were routed properly, the id should match

View File

@ -22,13 +22,13 @@ class DataObjectReferenceTest(BpmnWorkflowTestCase):
# Add the data so that we can advance the workflow
ready_tasks = self.workflow.get_ready_user_tasks()
ready_tasks[0].data = { 'obj_1': 'hello' }
ready_tasks[0].complete()
ready_tasks[0].run()
# Remove the data before advancing
ready_tasks = self.workflow.get_ready_user_tasks()
self.workflow.data.pop('obj_1')
with self.assertRaises(WorkflowDataException) as exc:
ready_tasks[0].complete()
ready_tasks[0].run()
self.assertEqual(exc.data_output.name, 'obj_1')
def testMissingDataOutput(self):
@ -37,7 +37,7 @@ class DataObjectReferenceTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_ready_user_tasks()
with self.assertRaises(WorkflowDataException) as exc:
ready_tasks[0].complete()
ready_tasks[0].run()
self.assertEqual(exc.data_output.name, 'obj_1')
def actual_test(self, save_restore):
@ -48,7 +48,7 @@ class DataObjectReferenceTest(BpmnWorkflowTestCase):
# Set up the data
ready_tasks = self.workflow.get_ready_user_tasks()
ready_tasks[0].data = { 'obj_1': 'hello' }
ready_tasks[0].complete()
ready_tasks[0].run()
# After task completion, obj_1 should be copied out of the task into the workflow
self.assertNotIn('obj_1', ready_tasks[0].data)
self.assertIn('obj_1', self.workflow.data)
@ -59,14 +59,14 @@ class DataObjectReferenceTest(BpmnWorkflowTestCase):
# Set a value for obj_1 in the task data again
ready_tasks = self.workflow.get_ready_user_tasks()
ready_tasks[0].data = { 'obj_1': 'hello again' }
ready_tasks[0].complete()
ready_tasks[0].run()
# Check to make sure we use the workflow value instead of the value we set
ready_tasks = self.workflow.get_ready_user_tasks()
self.assertEqual(ready_tasks[0].data['obj_1'], 'hello')
# Modify the value in the task
ready_tasks[0].data = { 'obj_1': 'hello again' }
ready_tasks[0].complete()
ready_tasks[0].run()
# We did not set an output data reference so obj_1 should remain unchanged in the workflow data
# and be removed from the task data
self.assertNotIn('obj_1', ready_tasks[0].data)
@ -77,7 +77,7 @@ class DataObjectReferenceTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_ready_user_tasks()
self.assertEqual(ready_tasks[0].data['obj_1'], 'hello')
ready_tasks[0].data['obj_1'] = 'hello again'
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
sp = self.workflow.get_tasks_from_spec_name('subprocess')[0]
# It was copied out

View File

@ -76,7 +76,7 @@ class CallActivityDataTest(BpmnWorkflowTestCase):
waiting = self.workflow.get_tasks(TaskState.WAITING)
while len(waiting) == 0:
next_task = self.workflow.get_tasks(TaskState.READY)[0]
next_task.complete()
next_task.run()
waiting = self.workflow.get_tasks(TaskState.WAITING)
def complete_subprocess(self):
@ -84,7 +84,7 @@ class CallActivityDataTest(BpmnWorkflowTestCase):
waiting = self.workflow.get_tasks(TaskState.WAITING)
while len(waiting) > 0:
next_task = self.workflow.get_tasks(TaskState.READY)[0]
next_task.complete()
next_task.run()
waiting = self.workflow.get_tasks(TaskState.WAITING)
@ -113,7 +113,7 @@ class IOSpecOnTaskTest(BpmnWorkflowTestCase):
task = self.workflow.get_tasks_from_spec_name('any_task')[0]
task.data.update({'out_1': 1})
with self.assertRaises(WorkflowDataException) as exc:
task.complete()
task.run()
self.assertEqual(exc.exception.data_output.name, 'out_2')
def actual_test(self, save_restore=False):
@ -124,6 +124,6 @@ class IOSpecOnTaskTest(BpmnWorkflowTestCase):
task = self.workflow.get_tasks_from_spec_name('any_task')[0]
self.assertDictEqual(task.data, {'in_1': 1, 'in_2': 'hello world'})
task.data.update({'out_1': 1, 'out_2': 'bye', 'extra': True})
task.complete()
task.run()
self.workflow.do_engine_steps()
self.assertDictEqual(self.workflow.last_task.data, {'out_1': 1, 'out_2': 'bye'})

View File

@ -36,4 +36,4 @@ class InclusiveGatewayTest(BpmnWorkflowTestCase):
def set_data(self, value):
task = self.workflow.get_ready_user_tasks()[0]
task.data = value
task.complete()
task.run()

View File

@ -26,7 +26,7 @@ class BaseTestCase(BpmnWorkflowTestCase):
self.assertEqual(task.task_spec.name, 'any_task [child]')
self.assertIn('input_item', task.data)
task.data['output_item'] = task.data['input_item'] * 2
task.complete()
task.run()
if save_restore:
self.save_restore()
ready_tasks = self.workflow.get_ready_user_tasks()
@ -47,7 +47,7 @@ class BaseTestCase(BpmnWorkflowTestCase):
self.assertEqual(len(ready_tasks), 3)
task = [t for t in ready_tasks if t.data['input_item'] == 2][0]
task.data['output_item'] = task.data['input_item'] * 2
task.complete()
task.run()
self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks()

View File

@ -20,7 +20,7 @@ class ParallelOrderTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps()
self.assertFalse(self.workflow.is_completed())
self.assertEquals(4, len(self.workflow.get_ready_user_tasks()))
self.assertEqual(4, len(self.workflow.get_ready_user_tasks()))
tasks = self.workflow.get_ready_user_tasks()
self.assertEquals("Task 1", tasks[0].get_description())
self.assertEquals("Task 2", tasks[1].get_description())

View File

@ -51,7 +51,7 @@ class ParallelThroughSameTaskTest(BpmnWorkflowTestCase):
self.assertEqual(2, len(ready_tasks))
self.assertEqual(
'Repeated Task', ready_tasks[0].task_spec.description)
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
# The inclusive gateway allows us through here, because there is no route for the other thread
# that doesn't use the same sequence flow
@ -82,7 +82,7 @@ class ParallelThroughSameTaskTest(BpmnWorkflowTestCase):
self.assertEqual(2, len(ready_tasks))
self.assertEqual(
'Repeated Task', ready_tasks[0].task_spec.description)
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
self.save_restore()
# The inclusive gateway allows us through here, because there is no route for the other thread

View File

@ -35,7 +35,7 @@ class ResetSubProcessTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps()
top_level_task = self.workflow.get_ready_user_tasks()[0]
self.workflow.complete_task_from_id(top_level_task.id)
self.workflow.run_task_from_id(top_level_task.id)
self.workflow.do_engine_steps()
task = self.workflow.get_ready_user_tasks()[0]
self.save_restore()
@ -50,11 +50,11 @@ class ResetSubProcessTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps()
self.assertEqual(1, len(self.workflow.get_ready_user_tasks()))
task = self.workflow.get_ready_user_tasks()[0]
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(task.get_name(),'SubTask2')
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
task = self.workflow.get_tasks_from_spec_name('Task1')[0]
task.reset_token(self.workflow.last_task.data)
@ -62,19 +62,19 @@ class ResetSubProcessTest(BpmnWorkflowTestCase):
self.reload_save_restore()
task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(task.get_name(),'Task1')
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(task.get_name(),'Subtask2')
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(task.get_name(),'Subtask2A')
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(task.get_name(),'Task2')
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed())

View File

@ -1,28 +0,0 @@
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.task import TaskState
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
class ResetTimerTest(BpmnWorkflowTestCase):
def test_timer(self):
spec, subprocess = self.load_workflow_spec('reset_timer.bpmn', 'main')
self.workflow = BpmnWorkflow(spec, subprocess)
self.workflow.do_engine_steps()
task_1 = self.workflow.get_tasks_from_spec_name('task_1')[0]
timer = self.workflow.get_tasks_from_spec_name('timer')[0]
original_timer = timer.internal_data.get('event_value')
# This returns us to the task
task_1.data['modify'] = True
task_1.complete()
self.workflow.do_engine_steps()
# The timer should be waiting and the time should have been updated
self.assertEqual(task_1.state, TaskState.READY)
self.assertEqual(timer.state, TaskState.WAITING)
self.assertGreater(timer.internal_data.get('event_value'), original_timer)
task_1.data['modify'] = False
task_1.complete()
self.workflow.do_engine_steps()
self.assertEqual(timer.state, TaskState.CANCELLED)
self.assertTrue(self.workflow.is_completed())

View File

@ -27,7 +27,7 @@ class BaseTestCase(BpmnWorkflowTestCase):
self.assertEqual(task.task_spec.name, 'any_task [child]')
self.assertIn('input_item', task.data)
task.data['output_item'] = task.data['input_item'] * 2
task.complete()
task.run()
if save_restore:
self.save_restore()
ready_tasks = self.workflow.get_ready_user_tasks()
@ -54,7 +54,7 @@ class BaseTestCase(BpmnWorkflowTestCase):
self.assertEqual(ready.task_spec.name, 'any_task [child]')
self.assertIn('input_item', ready.data)
ready.data['output_item'] = ready.data['input_item'] * 2
ready.complete()
ready.run()
self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks()
ready_tasks = self.workflow.get_ready_user_tasks()

View File

@ -21,7 +21,7 @@ class StandardLoopTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_ready_user_tasks()
self.assertEqual(len(ready_tasks), 1)
ready_tasks[0].data[str(idx)] = True
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed())
@ -36,7 +36,7 @@ class StandardLoopTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_ready_user_tasks()
self.assertEqual(len(ready_tasks), 1)
ready_tasks[0].data['done'] = True
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed())

View File

@ -35,7 +35,7 @@ class SwimLaneTest(BpmnWorkflowTestCase):
self.assertEqual(0, len(btasks))
task = atasks[0]
self.assertEqual('Activity_A1', task.task_spec.name)
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
atasks = self.workflow.get_ready_user_tasks(lane="A")
btasks = self.workflow.get_ready_user_tasks(lane="B")
@ -44,10 +44,10 @@ class SwimLaneTest(BpmnWorkflowTestCase):
# Complete the gateway and the two tasks in B Lane
btasks[0].data = {'NeedClarification': False}
self.workflow.complete_task_from_id(btasks[0].id)
self.workflow.run_task_from_id(btasks[0].id)
self.workflow.do_engine_steps()
btasks = self.workflow.get_ready_user_tasks(lane="B")
self.workflow.complete_task_from_id(btasks[0].id)
self.workflow.run_task_from_id(btasks[0].id)
self.workflow.do_engine_steps()
# Assert we are in lane C
@ -56,7 +56,7 @@ class SwimLaneTest(BpmnWorkflowTestCase):
self.assertEqual(tasks[0].task_spec.lane, "C")
# Step into the sub-process, assure that is also in lane C
self.workflow.complete_task_from_id(tasks[0].id)
self.workflow.run_task_from_id(tasks[0].id)
self.workflow.do_engine_steps()
tasks = self.workflow.get_ready_user_tasks()
self.assertEqual("SubProcessTask", tasks[0].task_spec.description)

View File

@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
import datetime
import unittest
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from SpiffWorkflow.bpmn.PythonScriptEngineEnvironment import TaskDataEnvironment
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'sartography'
class CustomScriptEngine(PythonScriptEngine):
"""This is a custom script processor that can be easily injected into Spiff Workflow.
It will execute python code read in from the bpmn. It will also make any scripts in the
scripts directory available for execution. """
def __init__(self):
environment = TaskDataEnvironment({
'timedelta': datetime.timedelta,
})
super().__init__(environment=environment)
class TooManyLoopsTest(BpmnWorkflowTestCase):
"""Looping back around many times would cause the tree of tasks to grow
for each loop, doing this a 100 or 1000 times would cause the system to
run fail in various ways. This assures that is no longer the case."""
def testRunThroughHappy(self):
self.actual_test(save_restore=False)
def testThroughSaveRestore(self):
self.actual_test(save_restore=True)
def actual_test(self,save_restore = False):
spec, subprocesses = self.load_workflow_spec('too_many_loops*.bpmn', 'loops')
self.workflow = BpmnWorkflow(spec, subprocesses, script_engine=CustomScriptEngine())
counter = 0
data = {}
while not self.workflow.is_completed():
self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks()
if (self.workflow.last_task.data != data):
data = self.workflow.last_task.data
counter += 1 # There is a 10 millisecond wait task.
if save_restore:
self.save_restore()
self.workflow.script_engine = CustomScriptEngine()
self.assertEqual(20, self.workflow.last_task.data['counter'])
def test_with_sub_process(self):
# Found an issue where looping back would fail when it happens
# right after a sub-process. So assuring this is fixed.
counter = 0
spec, subprocesses = self.load_workflow_spec('too_many_loops_sub_process.bpmn', 'loops_sub')
self.workflow = BpmnWorkflow(spec, subprocesses, script_engine=CustomScriptEngine())
data = {}
while not self.workflow.is_completed():
self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks()
if (self.workflow.last_task.data != data):
data = self.workflow.last_task.data
counter += 1 # There is a 10 millisecond wait task.
# self.save_restore()
self.assertEqual(20, self.workflow.last_task.data['counter'])
# One less, because we don't go back through once the first counter
# hits 20.
self.assertEqual(19, self.workflow.last_task.data['counter2'])
def test_with_two_call_activities(self):
spec, subprocess = self.load_workflow_spec('sub_in_loop*.bpmn', 'main')
self.workflow = BpmnWorkflow(spec, subprocess, script_engine=CustomScriptEngine())
self.workflow.do_engine_steps()
for loop in range(3):
ready = self.workflow.get_ready_user_tasks()
ready[0].data = { 'done': True if loop == 3 else False }
ready[0].complete()
self.workflow.refresh_waiting_tasks()
self.workflow.do_engine_steps()
self.save_restore()
self.workflow.script_engine = CustomScriptEngine()
def suite():
return unittest.TestLoader().loadTestsFromTestCase(TooManyLoopsTest)
if __name__ == '__main__':
unittest.TextTestRunner(verbosity=2).run(suite())

View File

@ -1,291 +1,303 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" xmlns:signavio="http://www.signavio.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" exporter="Signavio Process Editor, http://www.signavio.com" exporterVersion="" expressionLanguage="http://www.w3.org/1999/XPath" id="sid-fafc406d-50f2-41f2-b75b-ad652445d52e" targetNamespace="http://www.signavio.com/bpmn20" typeLanguage="http://www.w3.org/2001/XMLSchema" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd">
<collaboration id="sid-5801c79c-823f-4040-b680-417ef5bcb3a2">
<participant id="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00" name="Parallel Looping After Join" processRef="sid-41eb2b6c-08bc-4a61-b38b-5f32052139c5">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/>
</extensionElements>
</participant>
</collaboration>
<process id="sid-41eb2b6c-08bc-4a61-b38b-5f32052139c5" isClosed="false" isExecutable="false" name="Parallel Looping After Join" processType="None">
<laneSet id="sid-127ca06b-aba8-45aa-bd8a-7be70ec94b2c">
<lane id="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142" name="Tester">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue=""/>
</extensionElements>
<flowNodeRef>sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE</flowNodeRef>
<flowNodeRef>sid-349F8C0C-45EA-489C-84DD-1D944F48D778</flowNodeRef>
<flowNodeRef>sid-57463471-693A-42A2-9EC6-6460BEDECA86</flowNodeRef>
<flowNodeRef>sid-CA089240-802A-4C32-9130-FB1A33DDCCC3</flowNodeRef>
<flowNodeRef>sid-E976FBC2-266E-420F-8D4D-C8FBC6199090</flowNodeRef>
<flowNodeRef>sid-F3A979E3-F586-4807-8223-1FAB5A5647B0</flowNodeRef>
<flowNodeRef>sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB</flowNodeRef>
<flowNodeRef>sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897</flowNodeRef>
<flowNodeRef>sid-ABD788A3-CD57-4280-A22A-260B3AEEE138</flowNodeRef>
<flowNodeRef>sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7</flowNodeRef>
<flowNodeRef>sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58</flowNodeRef>
<flowNodeRef>sid-1946C635-7886-4687-844F-C644FA6222B8</flowNodeRef>
<flowNodeRef>sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1</flowNodeRef>
<flowNodeRef>sid-55C018B8-C073-4292-9ED0-79BDE50E7498</flowNodeRef>
<flowNodeRef>sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9</flowNodeRef>
</lane>
</laneSet>
<startEvent id="sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE" name="">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/>
</extensionElements>
<outgoing>sid-F3994F51-FE54-4910-A1F4-E5895AA1A612</outgoing>
</startEvent>
<parallelGateway gatewayDirection="Diverging" id="sid-349F8C0C-45EA-489C-84DD-1D944F48D778" name="First Split">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/>
</extensionElements>
<incoming>sid-3E0EBE59-75C8-465C-90CC-197CE808A96E</incoming>
<outgoing>sid-7E15C71B-DE9E-4788-B140-A647C99FDC94</outgoing>
<outgoing>sid-B6E22A74-A691-453A-A789-B9F8AF787D7C</outgoing>
</parallelGateway>
<task completionQuantity="1" id="sid-57463471-693A-42A2-9EC6-6460BEDECA86" isForCompensation="false" name="1" startQuantity="1">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/>
</extensionElements>
<incoming>sid-7E15C71B-DE9E-4788-B140-A647C99FDC94</incoming>
<outgoing>sid-607CB05E-8762-41B6-AD43-C3970661A99D</outgoing>
</task>
<task completionQuantity="1" id="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3" isForCompensation="false" name="2" startQuantity="1">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/>
</extensionElements>
<incoming>sid-B6E22A74-A691-453A-A789-B9F8AF787D7C</incoming>
<outgoing>sid-CAEAD081-6E73-4C98-8656-C67DA18F5140</outgoing>
</task>
<parallelGateway gatewayDirection="Converging" id="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090" name="Join of First Split">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/>
</extensionElements>
<incoming>sid-231F8A51-752F-4CB3-8FD1-23D153238344</incoming>
<incoming>sid-607CB05E-8762-41B6-AD43-C3970661A99D</incoming>
<outgoing>sid-0895E09C-077C-4D12-8C11-31F28CBC7740</outgoing>
</parallelGateway>
<task completionQuantity="1" id="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0" isForCompensation="false" name="Retry?" startQuantity="1">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/>
</extensionElements>
<incoming>sid-0895E09C-077C-4D12-8C11-31F28CBC7740</incoming>
<outgoing>sid-2668AC98-39E4-4B12-9052-930528086CAC</outgoing>
</task>
<endEvent id="sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB" name="">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/>
</extensionElements>
<incoming>sid-F6160C0E-216C-4D72-98D1-CC5549327D55</incoming>
</endEvent>
<parallelGateway gatewayDirection="Diverging" id="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897" name="Second Split">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/>
</extensionElements>
<incoming>sid-CAEAD081-6E73-4C98-8656-C67DA18F5140</incoming>
<outgoing>sid-918C653D-0960-4223-9C28-78114F238BCC</outgoing>
<outgoing>sid-FD82C2A6-7C54-4890-901E-A7E864F7605C</outgoing>
</parallelGateway>
<task completionQuantity="1" id="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138" isForCompensation="false" name="2A" startQuantity="1">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/>
</extensionElements>
<incoming>sid-918C653D-0960-4223-9C28-78114F238BCC</incoming>
<outgoing>sid-961AF51C-9935-410E-AAA4-105B19186F5E</outgoing>
</task>
<task completionQuantity="1" id="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7" isForCompensation="false" name="2B" startQuantity="1">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/>
</extensionElements>
<incoming>sid-FD82C2A6-7C54-4890-901E-A7E864F7605C</incoming>
<outgoing>sid-47947925-21CD-46FF-8D3F-294B235AA4CF</outgoing>
</task>
<parallelGateway gatewayDirection="Converging" id="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58" name="Join of Second Split">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/>
</extensionElements>
<incoming>sid-961AF51C-9935-410E-AAA4-105B19186F5E</incoming>
<incoming>sid-47947925-21CD-46FF-8D3F-294B235AA4CF</incoming>
<outgoing>sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0</outgoing>
</parallelGateway>
<task completionQuantity="1" id="sid-1946C635-7886-4687-844F-C644FA6222B8" isForCompensation="false" name="2 Done" startQuantity="1">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/>
</extensionElements>
<incoming>sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0</incoming>
<outgoing>sid-231F8A51-752F-4CB3-8FD1-23D153238344</outgoing>
</task>
<exclusiveGateway gatewayDirection="Diverging" id="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1" name="">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/>
</extensionElements>
<incoming>sid-2668AC98-39E4-4B12-9052-930528086CAC</incoming>
<outgoing>sid-08D6385B-C6BB-45FC-A6BD-2369F392868D</outgoing>
<outgoing>sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459</outgoing>
</exclusiveGateway>
<task completionQuantity="1" id="sid-55C018B8-C073-4292-9ED0-79BDE50E7498" isForCompensation="false" name="Done" startQuantity="1">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/>
</extensionElements>
<incoming>sid-08D6385B-C6BB-45FC-A6BD-2369F392868D</incoming>
<outgoing>sid-F6160C0E-216C-4D72-98D1-CC5549327D55</outgoing>
</task>
<task completionQuantity="1" id="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9" isForCompensation="false" name="Go" startQuantity="1">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/>
</extensionElements>
<incoming>sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459</incoming>
<incoming>sid-F3994F51-FE54-4910-A1F4-E5895AA1A612</incoming>
<outgoing>sid-3E0EBE59-75C8-465C-90CC-197CE808A96E</outgoing>
</task>
<sequenceFlow id="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94" name="" sourceRef="sid-349F8C0C-45EA-489C-84DD-1D944F48D778" targetRef="sid-57463471-693A-42A2-9EC6-6460BEDECA86"/>
<sequenceFlow id="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C" name="" sourceRef="sid-349F8C0C-45EA-489C-84DD-1D944F48D778" targetRef="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3"/>
<sequenceFlow id="sid-0895E09C-077C-4D12-8C11-31F28CBC7740" name="" sourceRef="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090" targetRef="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0"/>
<sequenceFlow id="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140" name="" sourceRef="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3" targetRef="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897"/>
<sequenceFlow id="sid-918C653D-0960-4223-9C28-78114F238BCC" name="" sourceRef="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897" targetRef="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138"/>
<sequenceFlow id="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C" name="" sourceRef="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897" targetRef="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7"/>
<sequenceFlow id="sid-961AF51C-9935-410E-AAA4-105B19186F5E" name="" sourceRef="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138" targetRef="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58"/>
<sequenceFlow id="sid-47947925-21CD-46FF-8D3F-294B235AA4CF" name="" sourceRef="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7" targetRef="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58"/>
<sequenceFlow id="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0" name="" sourceRef="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58" targetRef="sid-1946C635-7886-4687-844F-C644FA6222B8"/>
<sequenceFlow id="sid-231F8A51-752F-4CB3-8FD1-23D153238344" name="" sourceRef="sid-1946C635-7886-4687-844F-C644FA6222B8" targetRef="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090"/>
<sequenceFlow id="sid-607CB05E-8762-41B6-AD43-C3970661A99D" name="" sourceRef="sid-57463471-693A-42A2-9EC6-6460BEDECA86" targetRef="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090"/>
<sequenceFlow id="sid-2668AC98-39E4-4B12-9052-930528086CAC" name="" sourceRef="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0" targetRef="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1"/>
<sequenceFlow id="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D" name="No" sourceRef="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1" targetRef="sid-55C018B8-C073-4292-9ED0-79BDE50E7498"/>
<sequenceFlow id="sid-F6160C0E-216C-4D72-98D1-CC5549327D55" name="" sourceRef="sid-55C018B8-C073-4292-9ED0-79BDE50E7498" targetRef="sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB"/>
<sequenceFlow id="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459" name="Yes" sourceRef="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1" targetRef="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9"/>
<sequenceFlow id="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E" name="" sourceRef="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9" targetRef="sid-349F8C0C-45EA-489C-84DD-1D944F48D778"/>
<sequenceFlow id="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612" name="" sourceRef="sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE" targetRef="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9"/>
</process>
<bpmndi:BPMNDiagram id="sid-162a2324-4820-489a-9df8-04591b6b429a">
<bpmndi:BPMNPlane bpmnElement="sid-5801c79c-823f-4040-b680-417ef5bcb3a2" id="sid-4c9e28b7-9050-4a64-8a52-634f8f2febc3">
<bpmndi:BPMNShape bpmnElement="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00" id="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00_gui" isHorizontal="true">
<omgdc:Bounds height="619.0" width="794.0" x="120.0" y="90.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142" id="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142_gui" isHorizontal="true">
<omgdc:Bounds height="619.0" width="764.0" x="150.0" y="90.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE" id="sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE_gui">
<omgdc:Bounds height="30.0" width="30.0" x="190.0" y="122.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-349F8C0C-45EA-489C-84DD-1D944F48D778" id="sid-349F8C0C-45EA-489C-84DD-1D944F48D778_gui">
<omgdc:Bounds height="40.0" width="40.0" x="374.0" y="180.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-57463471-693A-42A2-9EC6-6460BEDECA86" id="sid-57463471-693A-42A2-9EC6-6460BEDECA86_gui">
<omgdc:Bounds height="80.0" width="100.0" x="510.0" y="214.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3" id="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3_gui">
<omgdc:Bounds height="80.0" width="100.0" x="231.0" y="214.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090" id="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090_gui">
<omgdc:Bounds height="40.0" width="40.0" x="540.0" y="405.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0" id="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0_gui">
<omgdc:Bounds height="80.0" width="100.0" x="615.0" y="385.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB" id="sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB_gui">
<omgdc:Bounds height="28.0" width="28.0" x="780.0" y="630.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897" id="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897_gui">
<omgdc:Bounds height="40.0" width="40.0" x="261.0" y="338.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138" id="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138_gui">
<omgdc:Bounds height="80.0" width="100.0" x="177.0" y="417.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7" id="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7_gui">
<omgdc:Bounds height="80.0" width="100.0" x="344.0" y="408.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58" id="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58_gui">
<omgdc:Bounds height="40.0" width="40.0" x="285.0" y="555.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-1946C635-7886-4687-844F-C644FA6222B8" id="sid-1946C635-7886-4687-844F-C644FA6222B8_gui">
<omgdc:Bounds height="80.0" width="100.0" x="420.0" y="535.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1" id="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1_gui" isMarkerVisible="true">
<omgdc:Bounds height="40.0" width="40.0" x="774.0" y="405.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-55C018B8-C073-4292-9ED0-79BDE50E7498" id="sid-55C018B8-C073-4292-9ED0-79BDE50E7498_gui">
<omgdc:Bounds height="80.0" width="100.0" x="744.0" y="497.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9" id="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9_gui">
<omgdc:Bounds height="80.0" width="100.0" x="467.0" y="105.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94" id="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94_gui">
<omgdi:waypoint x="414.0" y="206.0"/>
<omgdi:waypoint x="510.0" y="238.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D" id="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D_gui">
<omgdi:waypoint x="794.0" y="445.0"/>
<omgdi:waypoint x="794.0" y="497.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612" id="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612_gui">
<omgdi:waypoint x="220.0" y="137.0"/>
<omgdi:waypoint x="394.0" y="137.0"/>
<omgdi:waypoint x="467.0" y="142.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-231F8A51-752F-4CB3-8FD1-23D153238344" id="sid-231F8A51-752F-4CB3-8FD1-23D153238344_gui">
<omgdi:waypoint x="475.0" y="535.0"/>
<omgdi:waypoint x="491.0" y="425.0"/>
<omgdi:waypoint x="540.0" y="425.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-F6160C0E-216C-4D72-98D1-CC5549327D55" id="sid-F6160C0E-216C-4D72-98D1-CC5549327D55_gui">
<omgdi:waypoint x="794.0" y="577.0"/>
<omgdi:waypoint x="794.0" y="630.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C" id="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C_gui">
<omgdi:waypoint x="394.0" y="220.0"/>
<omgdi:waypoint x="394.5" y="254.0"/>
<omgdi:waypoint x="331.0" y="254.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-2668AC98-39E4-4B12-9052-930528086CAC" id="sid-2668AC98-39E4-4B12-9052-930528086CAC_gui">
<omgdi:waypoint x="715.0" y="425.0"/>
<omgdi:waypoint x="774.0" y="425.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140" id="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140_gui">
<omgdi:waypoint x="281.0" y="294.0"/>
<omgdi:waypoint x="281.0" y="338.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-918C653D-0960-4223-9C28-78114F238BCC" id="sid-918C653D-0960-4223-9C28-78114F238BCC_gui">
<omgdi:waypoint x="261.0" y="358.0"/>
<omgdi:waypoint x="227.0" y="358.5"/>
<omgdi:waypoint x="227.0" y="417.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C" id="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C_gui">
<omgdi:waypoint x="301.0" y="358.0"/>
<omgdi:waypoint x="394.0" y="358.5"/>
<omgdi:waypoint x="394.0" y="408.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459" id="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459_gui">
<omgdi:waypoint x="794.0" y="405.0"/>
<omgdi:waypoint x="794.5" y="145.0"/>
<omgdi:waypoint x="567.0" y="145.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-607CB05E-8762-41B6-AD43-C3970661A99D" id="sid-607CB05E-8762-41B6-AD43-C3970661A99D_gui">
<omgdi:waypoint x="560.0" y="294.0"/>
<omgdi:waypoint x="560.0" y="405.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0" id="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0_gui">
<omgdi:waypoint x="325.0" y="575.0"/>
<omgdi:waypoint x="420.0" y="575.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-961AF51C-9935-410E-AAA4-105B19186F5E" id="sid-961AF51C-9935-410E-AAA4-105B19186F5E_gui">
<omgdi:waypoint x="277.0" y="457.0"/>
<omgdi:waypoint x="305.0" y="457.0"/>
<omgdi:waypoint x="305.0" y="555.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E" id="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E_gui">
<omgdi:waypoint x="467.0" y="167.0"/>
<omgdi:waypoint x="414.0" y="191.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-0895E09C-077C-4D12-8C11-31F28CBC7740" id="sid-0895E09C-077C-4D12-8C11-31F28CBC7740_gui">
<omgdi:waypoint x="580.0" y="425.0"/>
<omgdi:waypoint x="615.0" y="425.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-47947925-21CD-46FF-8D3F-294B235AA4CF" id="sid-47947925-21CD-46FF-8D3F-294B235AA4CF_gui">
<omgdi:waypoint x="344.0" y="448.0"/>
<omgdi:waypoint x="305.0" y="448.0"/>
<omgdi:waypoint x="305.0" y="555.0"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:signavio="http://www.signavio.com" id="sid-fafc406d-50f2-41f2-b75b-ad652445d52e" targetNamespace="http://www.signavio.com/bpmn20" exporter="Camunda Modeler" exporterVersion="4.11.1" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd">
<collaboration id="sid-5801c79c-823f-4040-b680-417ef5bcb3a2">
<participant id="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00" name="Parallel Looping After Join" processRef="sid-41eb2b6c-08bc-4a61-b38b-5f32052139c5">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements>
</participant>
</collaboration>
<process id="sid-41eb2b6c-08bc-4a61-b38b-5f32052139c5" name="Parallel Looping After Join" processType="None" isClosed="false" isExecutable="false">
<laneSet id="sid-127ca06b-aba8-45aa-bd8a-7be70ec94b2c">
<lane id="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142" name="Tester">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="" />
</extensionElements>
<flowNodeRef>start</flowNodeRef>
<flowNodeRef>first_split</flowNodeRef>
<flowNodeRef>one</flowNodeRef>
<flowNodeRef>two</flowNodeRef>
<flowNodeRef>join_of_first</flowNodeRef>
<flowNodeRef>retry</flowNodeRef>
<flowNodeRef>end</flowNodeRef>
<flowNodeRef>second_split</flowNodeRef>
<flowNodeRef>two_a</flowNodeRef>
<flowNodeRef>two_b</flowNodeRef>
<flowNodeRef>join_of_second_split</flowNodeRef>
<flowNodeRef>two_done</flowNodeRef>
<flowNodeRef>exclusive</flowNodeRef>
<flowNodeRef>done</flowNodeRef>
<flowNodeRef>go</flowNodeRef>
</lane>
</laneSet>
<startEvent id="start" name="">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements>
<outgoing>sid-F3994F51-FE54-4910-A1F4-E5895AA1A612</outgoing>
</startEvent>
<parallelGateway id="first_split" name="First Split" gatewayDirection="Diverging">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements>
<incoming>sid-3E0EBE59-75C8-465C-90CC-197CE808A96E</incoming>
<outgoing>sid-7E15C71B-DE9E-4788-B140-A647C99FDC94</outgoing>
<outgoing>sid-B6E22A74-A691-453A-A789-B9F8AF787D7C</outgoing>
</parallelGateway>
<task id="one" name="1">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements>
<incoming>sid-7E15C71B-DE9E-4788-B140-A647C99FDC94</incoming>
<outgoing>join_of_first_split</outgoing>
</task>
<task id="two" name="2">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements>
<incoming>sid-B6E22A74-A691-453A-A789-B9F8AF787D7C</incoming>
<outgoing>sid-CAEAD081-6E73-4C98-8656-C67DA18F5140</outgoing>
</task>
<parallelGateway id="join_of_first" name="Join of First Split" gatewayDirection="Converging">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements>
<incoming>sid-231F8A51-752F-4CB3-8FD1-23D153238344</incoming>
<incoming>join_of_first_split</incoming>
<outgoing>sid-0895E09C-077C-4D12-8C11-31F28CBC7740</outgoing>
</parallelGateway>
<task id="retry" name="Retry?">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements>
<incoming>sid-0895E09C-077C-4D12-8C11-31F28CBC7740</incoming>
<outgoing>sid-2668AC98-39E4-4B12-9052-930528086CAC</outgoing>
</task>
<endEvent id="end" name="">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements>
<incoming>sid-F6160C0E-216C-4D72-98D1-CC5549327D55</incoming>
</endEvent>
<parallelGateway id="second_split" name="Second Split" gatewayDirection="Diverging">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements>
<incoming>sid-CAEAD081-6E73-4C98-8656-C67DA18F5140</incoming>
<outgoing>sid-918C653D-0960-4223-9C28-78114F238BCC</outgoing>
<outgoing>sid-FD82C2A6-7C54-4890-901E-A7E864F7605C</outgoing>
</parallelGateway>
<task id="two_a" name="2A">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements>
<incoming>sid-918C653D-0960-4223-9C28-78114F238BCC</incoming>
<outgoing>sid-961AF51C-9935-410E-AAA4-105B19186F5E</outgoing>
</task>
<task id="two_b" name="2B">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements>
<incoming>sid-FD82C2A6-7C54-4890-901E-A7E864F7605C</incoming>
<outgoing>sid-47947925-21CD-46FF-8D3F-294B235AA4CF</outgoing>
</task>
<parallelGateway id="join_of_second_split" name="Join of Second Split" gatewayDirection="Converging">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements>
<incoming>sid-961AF51C-9935-410E-AAA4-105B19186F5E</incoming>
<incoming>sid-47947925-21CD-46FF-8D3F-294B235AA4CF</incoming>
<outgoing>sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0</outgoing>
</parallelGateway>
<task id="two_done" name="2 Done">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements>
<incoming>sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0</incoming>
<outgoing>sid-231F8A51-752F-4CB3-8FD1-23D153238344</outgoing>
</task>
<exclusiveGateway id="exclusive" name="" gatewayDirection="Diverging">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements>
<incoming>sid-2668AC98-39E4-4B12-9052-930528086CAC</incoming>
<outgoing>sid-08D6385B-C6BB-45FC-A6BD-2369F392868D</outgoing>
<outgoing>sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459</outgoing>
</exclusiveGateway>
<task id="done" name="Done">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements>
<incoming>sid-08D6385B-C6BB-45FC-A6BD-2369F392868D</incoming>
<outgoing>sid-F6160C0E-216C-4D72-98D1-CC5549327D55</outgoing>
</task>
<task id="go" name="Go">
<extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements>
<incoming>sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459</incoming>
<incoming>sid-F3994F51-FE54-4910-A1F4-E5895AA1A612</incoming>
<outgoing>sid-3E0EBE59-75C8-465C-90CC-197CE808A96E</outgoing>
</task>
<sequenceFlow id="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94" name="" sourceRef="first_split" targetRef="one" />
<sequenceFlow id="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C" name="" sourceRef="first_split" targetRef="two" />
<sequenceFlow id="sid-0895E09C-077C-4D12-8C11-31F28CBC7740" name="" sourceRef="join_of_first" targetRef="retry" />
<sequenceFlow id="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140" name="" sourceRef="two" targetRef="second_split" />
<sequenceFlow id="sid-918C653D-0960-4223-9C28-78114F238BCC" name="" sourceRef="second_split" targetRef="two_a" />
<sequenceFlow id="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C" name="" sourceRef="second_split" targetRef="two_b" />
<sequenceFlow id="sid-961AF51C-9935-410E-AAA4-105B19186F5E" name="" sourceRef="two_a" targetRef="join_of_second_split" />
<sequenceFlow id="sid-47947925-21CD-46FF-8D3F-294B235AA4CF" name="" sourceRef="two_b" targetRef="join_of_second_split" />
<sequenceFlow id="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0" name="" sourceRef="join_of_second_split" targetRef="two_done" />
<sequenceFlow id="sid-231F8A51-752F-4CB3-8FD1-23D153238344" name="" sourceRef="two_done" targetRef="join_of_first" />
<sequenceFlow id="join_of_first_split" name="" sourceRef="one" targetRef="join_of_first" />
<sequenceFlow id="sid-2668AC98-39E4-4B12-9052-930528086CAC" name="" sourceRef="retry" targetRef="exclusive" />
<sequenceFlow id="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D" name="No" sourceRef="exclusive" targetRef="done" />
<sequenceFlow id="sid-F6160C0E-216C-4D72-98D1-CC5549327D55" name="" sourceRef="done" targetRef="end" />
<sequenceFlow id="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459" name="Yes" sourceRef="exclusive" targetRef="go" />
<sequenceFlow id="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E" name="" sourceRef="go" targetRef="first_split" />
<sequenceFlow id="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612" name="" sourceRef="start" targetRef="go" />
</process>
<bpmndi:BPMNDiagram id="sid-162a2324-4820-489a-9df8-04591b6b429a">
<bpmndi:BPMNPlane id="sid-4c9e28b7-9050-4a64-8a52-634f8f2febc3" bpmnElement="sid-5801c79c-823f-4040-b680-417ef5bcb3a2">
<bpmndi:BPMNShape id="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00_gui" bpmnElement="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00" isHorizontal="true">
<omgdc:Bounds x="120" y="90" width="794" height="619" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142_gui" bpmnElement="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142" isHorizontal="true">
<omgdc:Bounds x="150" y="90" width="764" height="619" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612_gui" bpmnElement="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612">
<omgdi:waypoint x="220" y="137" />
<omgdi:waypoint x="394" y="137" />
<omgdi:waypoint x="467" y="142" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E_gui" bpmnElement="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E">
<omgdi:waypoint x="467" y="167" />
<omgdi:waypoint x="414" y="191" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459_gui" bpmnElement="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459">
<omgdi:waypoint x="794" y="405" />
<omgdi:waypoint x="794.5" y="145" />
<omgdi:waypoint x="567" y="145" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-F6160C0E-216C-4D72-98D1-CC5549327D55_gui" bpmnElement="sid-F6160C0E-216C-4D72-98D1-CC5549327D55">
<omgdi:waypoint x="794" y="577" />
<omgdi:waypoint x="794" y="630" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D_gui" bpmnElement="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D">
<omgdi:waypoint x="794" y="445" />
<omgdi:waypoint x="794" y="497" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-2668AC98-39E4-4B12-9052-930528086CAC_gui" bpmnElement="sid-2668AC98-39E4-4B12-9052-930528086CAC">
<omgdi:waypoint x="715" y="425" />
<omgdi:waypoint x="774" y="425" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-607CB05E-8762-41B6-AD43-C3970661A99D_gui" bpmnElement="join_of_first_split">
<omgdi:waypoint x="560" y="294" />
<omgdi:waypoint x="560" y="405" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-231F8A51-752F-4CB3-8FD1-23D153238344_gui" bpmnElement="sid-231F8A51-752F-4CB3-8FD1-23D153238344">
<omgdi:waypoint x="475" y="535" />
<omgdi:waypoint x="491" y="425" />
<omgdi:waypoint x="540" y="425" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0_gui" bpmnElement="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0">
<omgdi:waypoint x="325" y="575" />
<omgdi:waypoint x="420" y="575" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-47947925-21CD-46FF-8D3F-294B235AA4CF_gui" bpmnElement="sid-47947925-21CD-46FF-8D3F-294B235AA4CF">
<omgdi:waypoint x="344" y="448" />
<omgdi:waypoint x="305" y="448" />
<omgdi:waypoint x="305" y="555" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-961AF51C-9935-410E-AAA4-105B19186F5E_gui" bpmnElement="sid-961AF51C-9935-410E-AAA4-105B19186F5E">
<omgdi:waypoint x="277" y="457" />
<omgdi:waypoint x="305" y="457" />
<omgdi:waypoint x="305" y="555" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C_gui" bpmnElement="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C">
<omgdi:waypoint x="301" y="358" />
<omgdi:waypoint x="394" y="358.5" />
<omgdi:waypoint x="394" y="408" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-918C653D-0960-4223-9C28-78114F238BCC_gui" bpmnElement="sid-918C653D-0960-4223-9C28-78114F238BCC">
<omgdi:waypoint x="261" y="358" />
<omgdi:waypoint x="227" y="358.5" />
<omgdi:waypoint x="227" y="417" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140_gui" bpmnElement="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140">
<omgdi:waypoint x="281" y="294" />
<omgdi:waypoint x="281" y="338" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-0895E09C-077C-4D12-8C11-31F28CBC7740_gui" bpmnElement="sid-0895E09C-077C-4D12-8C11-31F28CBC7740">
<omgdi:waypoint x="580" y="425" />
<omgdi:waypoint x="615" y="425" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C_gui" bpmnElement="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C">
<omgdi:waypoint x="394" y="220" />
<omgdi:waypoint x="394.5" y="254" />
<omgdi:waypoint x="331" y="254" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94_gui" bpmnElement="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94">
<omgdi:waypoint x="414" y="206" />
<omgdi:waypoint x="510" y="238" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE_gui" bpmnElement="start">
<omgdc:Bounds x="190" y="122" width="30" height="30" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-349F8C0C-45EA-489C-84DD-1D944F48D778_gui" bpmnElement="first_split">
<omgdc:Bounds x="374" y="180" width="40" height="40" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="371" y="220" width="46" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-57463471-693A-42A2-9EC6-6460BEDECA86_gui" bpmnElement="one">
<omgdc:Bounds x="510" y="214" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3_gui" bpmnElement="two">
<omgdc:Bounds x="231" y="214" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090_gui" bpmnElement="join_of_first">
<omgdc:Bounds x="540" y="405" width="40" height="40" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="519" y="445" width="82" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0_gui" bpmnElement="retry">
<omgdc:Bounds x="615" y="385" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB_gui" bpmnElement="end">
<omgdc:Bounds x="780" y="630" width="28" height="28" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897_gui" bpmnElement="second_split">
<omgdc:Bounds x="261" y="338" width="40" height="40" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="250" y="378" width="62" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138_gui" bpmnElement="two_a">
<omgdc:Bounds x="177" y="417" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7_gui" bpmnElement="two_b">
<omgdc:Bounds x="344" y="408" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58_gui" bpmnElement="join_of_second_split">
<omgdc:Bounds x="285" y="555" width="40" height="40" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="269" y="595" width="73" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-1946C635-7886-4687-844F-C644FA6222B8_gui" bpmnElement="two_done">
<omgdc:Bounds x="420" y="535" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1_gui" bpmnElement="exclusive" isMarkerVisible="true">
<omgdc:Bounds x="774" y="405" width="40" height="40" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-55C018B8-C073-4292-9ED0-79BDE50E7498_gui" bpmnElement="done">
<omgdc:Bounds x="744" y="497" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9_gui" bpmnElement="go">
<omgdc:Bounds x="467" y="105" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>

View File

@ -1,104 +0,0 @@
<?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:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1svhxil" 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="main" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0j648np</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:exclusiveGateway id="Gateway_1hq5zma" default="Flow_13cp5nc">
<bpmn:incoming>Flow_0j648np</bpmn:incoming>
<bpmn:incoming>modify</bpmn:incoming>
<bpmn:outgoing>Flow_13cp5nc</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_0j648np" sourceRef="StartEvent_1" targetRef="Gateway_1hq5zma" />
<bpmn:task id="task_1" name="Task 1">
<bpmn:incoming>Flow_13cp5nc</bpmn:incoming>
<bpmn:outgoing>Flow_1r81vou</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_13cp5nc" sourceRef="Gateway_1hq5zma" targetRef="task_1" />
<bpmn:task id="task_2" name="Task 2">
<bpmn:incoming>Flow_0m5s7t9</bpmn:incoming>
<bpmn:outgoing>Flow_0p7c88x</bpmn:outgoing>
</bpmn:task>
<bpmn:endEvent id="Event_07pdq0w">
<bpmn:incoming>Flow_1gm7381</bpmn:incoming>
<bpmn:incoming>Flow_0p7c88x</bpmn:incoming>
</bpmn:endEvent>
<bpmn:boundaryEvent id="timer" attachedToRef="task_1">
<bpmn:outgoing>Flow_0m5s7t9</bpmn:outgoing>
<bpmn:timerEventDefinition id="TimerEventDefinition_0hu2ovu">
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">"PT60S"</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<bpmn:sequenceFlow id="Flow_0m5s7t9" sourceRef="timer" targetRef="task_2" />
<bpmn:exclusiveGateway id="Gateway_123uzx5" default="Flow_1gm7381">
<bpmn:incoming>Flow_1r81vou</bpmn:incoming>
<bpmn:outgoing>modify</bpmn:outgoing>
<bpmn:outgoing>Flow_1gm7381</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_1r81vou" sourceRef="task_1" targetRef="Gateway_123uzx5" />
<bpmn:sequenceFlow id="modify" name="Modify&#10;" sourceRef="Gateway_123uzx5" targetRef="Gateway_1hq5zma">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">modify</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1gm7381" sourceRef="Gateway_123uzx5" targetRef="Event_07pdq0w" />
<bpmn:sequenceFlow id="Flow_0p7c88x" sourceRef="task_2" targetRef="Event_07pdq0w" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="main">
<bpmndi:BPMNEdge id="Flow_0j648np_di" bpmnElement="Flow_0j648np">
<di:waypoint x="215" y="197" />
<di:waypoint x="265" y="197" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_13cp5nc_di" bpmnElement="Flow_13cp5nc">
<di:waypoint x="315" y="197" />
<di:waypoint x="370" y="197" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0m5s7t9_di" bpmnElement="Flow_0m5s7t9">
<di:waypoint x="420" y="255" />
<di:waypoint x="420" y="300" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1r81vou_di" bpmnElement="Flow_1r81vou">
<di:waypoint x="470" y="197" />
<di:waypoint x="525" y="197" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1l30w6o_di" bpmnElement="modify">
<di:waypoint x="550" y="172" />
<di:waypoint x="550" y="100" />
<di:waypoint x="290" y="100" />
<di:waypoint x="290" y="172" />
<bpmndi:BPMNLabel>
<dc:Bounds x="404" y="82" width="33" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gm7381_di" bpmnElement="Flow_1gm7381">
<di:waypoint x="575" y="197" />
<di:waypoint x="632" y="197" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0p7c88x_di" bpmnElement="Flow_0p7c88x">
<di:waypoint x="470" y="340" />
<di:waypoint x="650" y="340" />
<di:waypoint x="650" y="215" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="179" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1hq5zma_di" bpmnElement="Gateway_1hq5zma" isMarkerVisible="true">
<dc:Bounds x="265" y="172" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1f3jg2c_di" bpmnElement="task_1">
<dc:Bounds x="370" y="157" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1r0ra56_di" bpmnElement="task_2">
<dc:Bounds x="370" y="300" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_123uzx5_di" bpmnElement="Gateway_123uzx5" isMarkerVisible="true">
<dc:Bounds x="525" y="172" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_07pdq0w_di" bpmnElement="Event_07pdq0w">
<dc:Bounds x="632" y="179" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1g1bbcs_di" bpmnElement="timer">
<dc:Bounds x="402" y="219" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,600 @@
{
"serializer_version": "1.1",
"data": {},
"last_task": "65ef57f0-3fbf-4851-b7c8-a03de9a9062d",
"success": true,
"tasks": {
"fcccd5d5-8e9c-4dba-91c3-5b2ed44bb332": {
"id": "fcccd5d5-8e9c-4dba-91c3-5b2ed44bb332",
"parent": null,
"children": [
"e9523ea2-0474-4c36-a7c2-24ff56633ed7"
],
"last_state_change": 1678818080.7799659,
"state": 32,
"task_spec": "Root",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"e9523ea2-0474-4c36-a7c2-24ff56633ed7": {
"id": "e9523ea2-0474-4c36-a7c2-24ff56633ed7",
"parent": "fcccd5d5-8e9c-4dba-91c3-5b2ed44bb332",
"children": [
"af4a1c32-12a7-46a7-b985-284fef1cb993"
],
"last_state_change": 1678818080.7888825,
"state": 32,
"task_spec": "Start",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"af4a1c32-12a7-46a7-b985-284fef1cb993": {
"id": "af4a1c32-12a7-46a7-b985-284fef1cb993",
"parent": "e9523ea2-0474-4c36-a7c2-24ff56633ed7",
"children": [
"65ef57f0-3fbf-4851-b7c8-a03de9a9062d"
],
"last_state_change": 1678818080.7926495,
"state": 32,
"task_spec": "StartEvent_1",
"triggered": false,
"workflow_name": "loops",
"internal_data": {
"event_fired": true
},
"data": {}
},
"65ef57f0-3fbf-4851-b7c8-a03de9a9062d": {
"id": "65ef57f0-3fbf-4851-b7c8-a03de9a9062d",
"parent": "af4a1c32-12a7-46a7-b985-284fef1cb993",
"children": [
"b5200074-8196-40d3-8f83-204cf132856c"
],
"last_state_change": 1678818080.7970355,
"state": 32,
"task_spec": "initialize",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {
"counter": 0
}
},
"b5200074-8196-40d3-8f83-204cf132856c": {
"id": "b5200074-8196-40d3-8f83-204cf132856c",
"parent": "65ef57f0-3fbf-4851-b7c8-a03de9a9062d",
"children": [
"2ab8ec7f-a9ee-4891-a7a9-20250e2ab816"
],
"last_state_change": 1678818091.420421,
"state": 16,
"task_spec": "TIMER_EVENT",
"triggered": false,
"workflow_name": "loops",
"internal_data": {
"event_value": "2023-03-14T18:21:20.809141+00:00",
"event_fired": true
},
"data": {
"counter": 0
}
},
"2ab8ec7f-a9ee-4891-a7a9-20250e2ab816": {
"id": "2ab8ec7f-a9ee-4891-a7a9-20250e2ab816",
"parent": "b5200074-8196-40d3-8f83-204cf132856c",
"children": [
"259de884-e162-4e0b-8c86-d4827870e2ca"
],
"last_state_change": 1678818080.7815213,
"state": 4,
"task_spec": "increment_counter",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"259de884-e162-4e0b-8c86-d4827870e2ca": {
"id": "259de884-e162-4e0b-8c86-d4827870e2ca",
"parent": "2ab8ec7f-a9ee-4891-a7a9-20250e2ab816",
"children": [
"bd7c4868-142b-487f-9ed3-6a384762dd55"
],
"last_state_change": 1678818080.7818034,
"state": 4,
"task_spec": "Activity_0w5u4k4",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"bd7c4868-142b-487f-9ed3-6a384762dd55": {
"id": "bd7c4868-142b-487f-9ed3-6a384762dd55",
"parent": "259de884-e162-4e0b-8c86-d4827870e2ca",
"children": [
"2fc6691e-f99b-46f2-bebc-7ba729880d13",
"3a14ea66-76b3-4442-8cb3-71a89b9b92fd"
],
"last_state_change": 1678818080.782314,
"state": 4,
"task_spec": "Gateway_over_20",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"2fc6691e-f99b-46f2-bebc-7ba729880d13": {
"id": "2fc6691e-f99b-46f2-bebc-7ba729880d13",
"parent": "bd7c4868-142b-487f-9ed3-6a384762dd55",
"children": [
"655b5f82-14dd-4edd-b88d-94e8cce02e78"
],
"last_state_change": 1678818080.783402,
"state": 1,
"task_spec": "end_event5",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"655b5f82-14dd-4edd-b88d-94e8cce02e78": {
"id": "655b5f82-14dd-4edd-b88d-94e8cce02e78",
"parent": "2fc6691e-f99b-46f2-bebc-7ba729880d13",
"children": [],
"last_state_change": 1678818080.7840528,
"state": 1,
"task_spec": "loops.EndJoin",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"3a14ea66-76b3-4442-8cb3-71a89b9b92fd": {
"id": "3a14ea66-76b3-4442-8cb3-71a89b9b92fd",
"parent": "bd7c4868-142b-487f-9ed3-6a384762dd55",
"children": [],
"last_state_change": 1678818080.7835343,
"state": 1,
"task_spec": "return_to_TIMER_EVENT",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
}
},
"root": "fcccd5d5-8e9c-4dba-91c3-5b2ed44bb332",
"spec": {
"name": "loops",
"description": "loops",
"file": "/home/essweine/work/sartography/code/SpiffWorkflow/tests/SpiffWorkflow/bpmn/data/too_many_loops.bpmn",
"task_specs": {
"Start": {
"id": "loops_1",
"name": "Start",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [],
"outputs": [
"StartEvent_1"
],
"typename": "StartTask"
},
"loops.EndJoin": {
"id": "loops_2",
"name": "loops.EndJoin",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"end_event5"
],
"outputs": [
"End"
],
"typename": "_EndJoin"
},
"End": {
"id": "loops_3",
"name": "End",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"loops.EndJoin"
],
"outputs": [],
"typename": "Simple"
},
"StartEvent_1": {
"id": "loops_4",
"name": "StartEvent_1",
"description": null,
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Start"
],
"outputs": [
"initialize"
],
"lane": null,
"documentation": null,
"position": {
"x": 152.0,
"y": 159.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"event_definition": {
"internal": false,
"external": false,
"typename": "NoneEventDefinition"
},
"typename": "StartEvent",
"extensions": {}
},
"initialize": {
"id": "loops_5",
"name": "initialize",
"description": "initialize",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"StartEvent_1"
],
"outputs": [
"TIMER_EVENT"
],
"lane": null,
"documentation": null,
"position": {
"x": 250.0,
"y": 137.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"script": "counter = 0",
"typename": "ScriptTask",
"extensions": {}
},
"TIMER_EVENT": {
"id": "loops_6",
"name": "TIMER_EVENT",
"description": "Wait",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Gateway_over_20",
"initialize"
],
"outputs": [
"increment_counter"
],
"lane": null,
"documentation": null,
"position": {
"x": 412.0,
"y": 159.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"event_definition": {
"internal": true,
"external": true,
"name": "Wait",
"expression": "\"PT.01S\"",
"typename": "DurationTimerEventDefinition"
},
"typename": "IntermediateCatchEvent",
"extensions": {}
},
"increment_counter": {
"id": "loops_7",
"name": "increment_counter",
"description": "increment counter",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"TIMER_EVENT"
],
"outputs": [
"Activity_0w5u4k4"
],
"lane": null,
"documentation": null,
"position": {
"x": 480.0,
"y": 137.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"script": "counter = counter + 1",
"typename": "ScriptTask",
"extensions": {}
},
"Activity_0w5u4k4": {
"id": "loops_8",
"name": "Activity_0w5u4k4",
"description": "call something",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"increment_counter"
],
"outputs": [
"Gateway_over_20"
],
"lane": null,
"documentation": null,
"position": {
"x": 620.0,
"y": 137.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"spec": "loops_ca",
"typename": "CallActivity",
"extensions": {}
},
"Gateway_over_20": {
"id": "loops_9",
"name": "Gateway_over_20",
"description": "is > 20",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Activity_0w5u4k4"
],
"outputs": [
"end_event5",
"TIMER_EVENT"
],
"lane": null,
"documentation": null,
"position": {
"x": 755.0,
"y": 152.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"cond_task_specs": [
{
"condition": "counter >= 20",
"task_spec": "end_event5"
},
{
"condition": "counter < 20",
"task_spec": "TIMER_EVENT"
}
],
"choice": null,
"default_task_spec": null,
"typename": "ExclusiveGateway",
"extensions": {}
},
"end_event5": {
"id": "loops_10",
"name": "end_event5",
"description": null,
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Gateway_over_20"
],
"outputs": [
"loops.EndJoin"
],
"lane": null,
"documentation": "### Results\nSubmission for Pre-Review was sent to the HSR-IRB on {{ sent_local_date_str }} at {{ sent_local_time_str }}.\n\nThe HSR-IRB started the Pre-Review process on {{ end_local_date_str }} at {{ end_local_time_str }} and assigned {{ irb_info.IRB_ADMINISTRATIVE_REVIEWER }} as the reviewer.\n\n### Metrics\n\n\nDays elapsed: {{days_delta }}",
"position": {
"x": 932.0,
"y": 159.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"event_definition": {
"internal": false,
"external": false,
"typename": "NoneEventDefinition"
},
"typename": "EndEvent",
"extensions": {}
},
"Root": {
"id": "loops_11",
"name": "Root",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [],
"outputs": [],
"typename": "Simple"
},
"return_to_TIMER_EVENT": {
"id": "loops_12",
"name": "return_to_TIMER_EVENT",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Gateway_over_20",
"initialize"
],
"outputs": [],
"destination_id": "b5200074-8196-40d3-8f83-204cf132856c",
"destination_spec_name": "TIMER_EVENT",
"typename": "LoopResetTask"
}
},
"io_specification": null,
"data_objects": {},
"correlation_keys": {},
"typename": "BpmnProcessSpec"
},
"subprocess_specs": {
"loops_ca": {
"name": "loops_ca",
"description": "loops_ca",
"file": "/home/essweine/work/sartography/code/SpiffWorkflow/tests/SpiffWorkflow/bpmn/data/too_many_loops_call_activity.bpmn",
"task_specs": {
"Start": {
"id": "loops_ca_1",
"name": "Start",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [],
"outputs": [
"StartEvent_1"
],
"typename": "StartTask"
},
"loops_ca.EndJoin": {
"id": "loops_ca_2",
"name": "loops_ca.EndJoin",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"end_event5"
],
"outputs": [
"End"
],
"typename": "_EndJoin"
},
"End": {
"id": "loops_ca_3",
"name": "End",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"loops_ca.EndJoin"
],
"outputs": [],
"typename": "Simple"
},
"StartEvent_1": {
"id": "loops_ca_4",
"name": "StartEvent_1",
"description": null,
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Start"
],
"outputs": [
"increment_counter"
],
"lane": null,
"documentation": null,
"position": {
"x": 152.0,
"y": 109.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"event_definition": {
"internal": false,
"external": false,
"typename": "NoneEventDefinition"
},
"typename": "StartEvent",
"extensions": {}
},
"increment_counter": {
"id": "loops_ca_5",
"name": "increment_counter",
"description": "increment counter",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"StartEvent_1"
],
"outputs": [
"end_event5"
],
"lane": null,
"documentation": null,
"position": {
"x": 220.0,
"y": 87.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"script": "counter2 = 1000",
"typename": "ScriptTask",
"extensions": {}
},
"end_event5": {
"id": "loops_ca_6",
"name": "end_event5",
"description": null,
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"increment_counter"
],
"outputs": [
"loops_ca.EndJoin"
],
"lane": null,
"documentation": "### Results\nSubmission for Pre-Review was sent to the HSR-IRB on {{ sent_local_date_str }} at {{ sent_local_time_str }}.\n\nThe HSR-IRB started the Pre-Review process on {{ end_local_date_str }} at {{ end_local_time_str }} and assigned {{ irb_info.IRB_ADMINISTRATIVE_REVIEWER }} as the reviewer.\n\n### Metrics\n\n\nDays elapsed: {{days_delta }}",
"position": {
"x": 362.0,
"y": 109.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"event_definition": {
"internal": false,
"external": false,
"typename": "NoneEventDefinition"
},
"typename": "EndEvent",
"extensions": {}
}
},
"io_specification": null,
"data_objects": {},
"correlation_keys": {},
"typename": "BpmnProcessSpec"
}
},
"subprocesses": {},
"bpmn_messages": [],
"correlations": {}
}

View File

@ -1,88 +0,0 @@
<?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:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0hx6iqe" 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="main" name="Main" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0nlj5lh</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="initialize" name="Initialize">
<bpmn:incoming>Flow_0nlj5lh</bpmn:incoming>
<bpmn:incoming>Flow_16vai1a</bpmn:incoming>
<bpmn:outgoing>Flow_1lkecht</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_1lkecht" sourceRef="initialize" targetRef="subprocess_in_loop" />
<bpmn:callActivity id="subprocess_in_loop" name="Subprocess in Loop" calledElement="subprocess">
<bpmn:incoming>Flow_1lkecht</bpmn:incoming>
<bpmn:outgoing>Flow_1vci114</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:sequenceFlow id="Flow_1vci114" sourceRef="subprocess_in_loop" targetRef="call_2" />
<bpmn:exclusiveGateway id="Gateway_158gdvg" default="Flow_16vai1a">
<bpmn:incoming>Flow_0iui938</bpmn:incoming>
<bpmn:outgoing>Flow_0ew7zdi</bpmn:outgoing>
<bpmn:outgoing>Flow_16vai1a</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="Event_0l6q7ei">
<bpmn:incoming>Flow_0ew7zdi</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0ew7zdi" sourceRef="Gateway_158gdvg" targetRef="Event_0l6q7ei">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">done</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_0iui938" sourceRef="call_2" targetRef="Gateway_158gdvg" />
<bpmn:callActivity id="call_2" name="Call Subprocess again" calledElement="subprocess">
<bpmn:incoming>Flow_1vci114</bpmn:incoming>
<bpmn:outgoing>Flow_0iui938</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:sequenceFlow id="Flow_0nlj5lh" sourceRef="StartEvent_1" targetRef="initialize" />
<bpmn:sequenceFlow id="Flow_16vai1a" sourceRef="Gateway_158gdvg" targetRef="initialize" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="main">
<bpmndi:BPMNEdge id="Flow_1lkecht_di" bpmnElement="Flow_1lkecht">
<di:waypoint x="350" y="117" />
<di:waypoint x="420" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1vci114_di" bpmnElement="Flow_1vci114">
<di:waypoint x="520" y="117" />
<di:waypoint x="580" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0ew7zdi_di" bpmnElement="Flow_0ew7zdi">
<di:waypoint x="785" y="117" />
<di:waypoint x="862" y="117" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1104" y="159" width="60" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0iui938_di" bpmnElement="Flow_0iui938">
<di:waypoint x="680" y="117" />
<di:waypoint x="735" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0nlj5lh_di" bpmnElement="Flow_0nlj5lh">
<di:waypoint x="188" y="117" />
<di:waypoint x="250" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_16vai1a_di" bpmnElement="Flow_16vai1a">
<di:waypoint x="760" y="142" />
<di:waypoint x="760" y="240" />
<di:waypoint x="300" y="240" />
<di:waypoint x="300" y="157" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_0721i5u_di" bpmnElement="initialize">
<dc:Bounds x="250" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1lcptwy_di" bpmnElement="call_2">
<dc:Bounds x="580" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1stre5m_di" bpmnElement="subprocess_in_loop">
<dc:Bounds x="420" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_158gdvg_di" bpmnElement="Gateway_158gdvg" isMarkerVisible="true">
<dc:Bounds x="735" y="92" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0l6q7ei_di" bpmnElement="Event_0l6q7ei">
<dc:Bounds x="862" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0vwjlip" 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="subprocess" name="Subprocess" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1dbtwxp</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1dbtwxp" sourceRef="StartEvent_1" targetRef="Activity_1g5e8v7" />
<bpmn:endEvent id="Event_1ukgnws">
<bpmn:incoming>Flow_1t99mly</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1t99mly" sourceRef="Activity_1g5e8v7" targetRef="Event_1ukgnws" />
<bpmn:task id="Activity_1g5e8v7" name="Subprocess Task">
<bpmn:incoming>Flow_1dbtwxp</bpmn:incoming>
<bpmn:outgoing>Flow_1t99mly</bpmn:outgoing>
</bpmn:task>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="subprocess">
<bpmndi:BPMNEdge id="Flow_1dbtwxp_di" bpmnElement="Flow_1dbtwxp">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1t99mly_di" bpmnElement="Flow_1t99mly">
<di:waypoint x="370" y="117" />
<di:waypoint x="432" y="117" />
</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="Event_1ukgnws_di" bpmnElement="Event_1ukgnws">
<dc:Bounds x="432" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0hql87y_di" bpmnElement="Activity_1g5e8v7">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,125 +0,0 @@
<?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" id="Definitions_d4f3442" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1">
<bpmn:process id="loops" isExecutable="true">
<bpmn:scriptTask id="increment_counter" name="increment counter">
<bpmn:incoming>Flow_1gb8wca</bpmn:incoming>
<bpmn:outgoing>Flow_1d2usdq</bpmn:outgoing>
<bpmn:script>counter = counter + 1</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1d2usdq" sourceRef="increment_counter" targetRef="Activity_0w5u4k4" />
<bpmn:endEvent id="end_event5">
<bpmn:documentation>### Results
Submission for Pre-Review was sent to the HSR-IRB on {{ sent_local_date_str }} at {{ sent_local_time_str }}.
The HSR-IRB started the Pre-Review process on {{ end_local_date_str }} at {{ end_local_time_str }} and assigned {{ irb_info.IRB_ADMINISTRATIVE_REVIEWER }} as the reviewer.
### Metrics
Days elapsed: {{days_delta }}</bpmn:documentation>
<bpmn:incoming>Flow_1tj9oz1</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1gb8wca" sourceRef="TIMER_EVENT" targetRef="increment_counter" />
<bpmn:intermediateCatchEvent id="TIMER_EVENT" name="Wait">
<bpmn:incoming>Flow_15jw6a4</bpmn:incoming>
<bpmn:incoming>Flow_0op1a19</bpmn:incoming>
<bpmn:outgoing>Flow_1gb8wca</bpmn:outgoing>
<bpmn:timerEventDefinition id="TimerEventDefinition_0x6divu">
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">"PT.01S"</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<bpmn:exclusiveGateway id="Gateway_over_20" name="is &#62; 20">
<bpmn:incoming>Flow_0mxlkif</bpmn:incoming>
<bpmn:outgoing>Flow_1tj9oz1</bpmn:outgoing>
<bpmn:outgoing>Flow_0op1a19</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_1tj9oz1" name="Yes" sourceRef="Gateway_over_20" targetRef="end_event5">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">counter &gt;= 20</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_0op1a19" name="No" sourceRef="Gateway_over_20" targetRef="TIMER_EVENT">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">counter &lt; 20</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_15jw6a4" sourceRef="initialize" targetRef="TIMER_EVENT" />
<bpmn:scriptTask id="initialize" name="initialize">
<bpmn:incoming>Flow_0q7fkb7</bpmn:incoming>
<bpmn:outgoing>Flow_15jw6a4</bpmn:outgoing>
<bpmn:script>counter = 0</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0mxlkif" sourceRef="Activity_0w5u4k4" targetRef="Gateway_over_20" />
<bpmn:callActivity id="Activity_0w5u4k4" name="call something" calledElement="loops_ca">
<bpmn:incoming>Flow_1d2usdq</bpmn:incoming>
<bpmn:outgoing>Flow_0mxlkif</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0q7fkb7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0q7fkb7" sourceRef="StartEvent_1" targetRef="initialize" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="loops">
<bpmndi:BPMNEdge id="Flow_0q7fkb7_di" bpmnElement="Flow_0q7fkb7">
<di:waypoint x="188" y="177" />
<di:waypoint x="250" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0mxlkif_di" bpmnElement="Flow_0mxlkif">
<di:waypoint x="720" y="177" />
<di:waypoint x="755" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_15jw6a4_di" bpmnElement="Flow_15jw6a4">
<di:waypoint x="350" y="177" />
<di:waypoint x="412" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0op1a19_di" bpmnElement="Flow_0op1a19">
<di:waypoint x="780" y="152" />
<di:waypoint x="780" y="80" />
<di:waypoint x="430" y="80" />
<di:waypoint x="430" y="159" />
<bpmndi:BPMNLabel>
<dc:Bounds x="598" y="62" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1tj9oz1_di" bpmnElement="Flow_1tj9oz1">
<di:waypoint x="805" y="177" />
<di:waypoint x="932" y="177" />
<bpmndi:BPMNLabel>
<dc:Bounds x="818" y="159" width="19" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gb8wca_di" bpmnElement="Flow_1gb8wca">
<di:waypoint x="448" y="177" />
<di:waypoint x="480" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1d2usdq_di" bpmnElement="Flow_1d2usdq">
<di:waypoint x="580" y="177" />
<di:waypoint x="620" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_1fgfg5d_di" bpmnElement="increment_counter">
<dc:Bounds x="480" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1tseamj_di" bpmnElement="end_event5">
<dc:Bounds x="932" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0whindt_di" bpmnElement="TIMER_EVENT">
<dc:Bounds x="412" y="159" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="419" y="202" width="22" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0gr4bqq_di" bpmnElement="Gateway_over_20" isMarkerVisible="true">
<dc:Bounds x="755" y="152" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="768" y="209" width="33" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0whalo9_di" bpmnElement="initialize">
<dc:Bounds x="250" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0mmgsiw_di" bpmnElement="Activity_0w5u4k4">
<dc:Bounds x="620" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,48 +0,0 @@
<?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" id="Definitions_d4f3442" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.10.0">
<bpmn:process id="loops_ca" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_175n91v</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:scriptTask id="increment_counter" name="increment counter">
<bpmn:incoming>Flow_175n91v</bpmn:incoming>
<bpmn:outgoing>Flow_1d2usdq</bpmn:outgoing>
<bpmn:script>counter2 = 1000</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1d2usdq" sourceRef="increment_counter" targetRef="end_event5" />
<bpmn:endEvent id="end_event5">
<bpmn:documentation>### Results
Submission for Pre-Review was sent to the HSR-IRB on {{ sent_local_date_str }} at {{ sent_local_time_str }}.
The HSR-IRB started the Pre-Review process on {{ end_local_date_str }} at {{ end_local_time_str }} and assigned {{ irb_info.IRB_ADMINISTRATIVE_REVIEWER }} as the reviewer.
### Metrics
Days elapsed: {{days_delta }}</bpmn:documentation>
<bpmn:incoming>Flow_1d2usdq</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_175n91v" sourceRef="StartEvent_1" targetRef="increment_counter" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="loops_ca">
<bpmndi:BPMNEdge id="Flow_175n91v_di" bpmnElement="Flow_175n91v">
<di:waypoint x="188" y="127" />
<di:waypoint x="220" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1d2usdq_di" bpmnElement="Flow_1d2usdq">
<di:waypoint x="320" y="127" />
<di:waypoint x="362" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="109" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1fgfg5d_di" bpmnElement="increment_counter">
<dc:Bounds x="220" y="87" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1tseamj_di" bpmnElement="end_event5">
<dc:Bounds x="362" y="109" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,157 +0,0 @@
<?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" id="Definitions_d4f3442" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1">
<bpmn:process id="loops_sub" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0q7fkb7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0q7fkb7" sourceRef="StartEvent_1" targetRef="initialize" />
<bpmn:scriptTask id="increment_counter" name="increment counter">
<bpmn:incoming>Flow_1gb8wca</bpmn:incoming>
<bpmn:outgoing>Flow_1d2usdq</bpmn:outgoing>
<bpmn:script>counter = counter + 1</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1d2usdq" sourceRef="increment_counter" targetRef="Gateway_over_20" />
<bpmn:endEvent id="end_event5">
<bpmn:documentation>### Results
Submission for Pre-Review was sent to the HSR-IRB on {{ sent_local_date_str }} at {{ sent_local_time_str }}.
The HSR-IRB started the Pre-Review process on {{ end_local_date_str }} at {{ end_local_time_str }} and assigned {{ irb_info.IRB_ADMINISTRATIVE_REVIEWER }} as the reviewer.
### Metrics
Days elapsed: {{days_delta }}</bpmn:documentation>
<bpmn:incoming>Flow_1tj9oz1</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1gb8wca" sourceRef="TIMER_EVENT" targetRef="increment_counter" />
<bpmn:intermediateCatchEvent id="TIMER_EVENT" name="Wait">
<bpmn:incoming>Flow_15jw6a4</bpmn:incoming>
<bpmn:incoming>Flow_1ivr6d7</bpmn:incoming>
<bpmn:outgoing>Flow_1gb8wca</bpmn:outgoing>
<bpmn:timerEventDefinition id="TimerEventDefinition_0x6divu">
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">"PT0.01S"</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<bpmn:exclusiveGateway id="Gateway_over_20" name="is &#62; 20">
<bpmn:incoming>Flow_1d2usdq</bpmn:incoming>
<bpmn:outgoing>Flow_1tj9oz1</bpmn:outgoing>
<bpmn:outgoing>Flow_0op1a19</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_1tj9oz1" name="Yes" sourceRef="Gateway_over_20" targetRef="end_event5">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">counter &gt;= 20</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_0op1a19" name="No" sourceRef="Gateway_over_20" targetRef="my_sub_process">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">counter &lt; 20</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_15jw6a4" sourceRef="initialize" targetRef="TIMER_EVENT" />
<bpmn:scriptTask id="initialize" name="initialize">
<bpmn:incoming>Flow_0q7fkb7</bpmn:incoming>
<bpmn:outgoing>Flow_15jw6a4</bpmn:outgoing>
<bpmn:script>counter = 0
counter2 = 0
counter3 = 0</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1ivr6d7" sourceRef="my_sub_process" targetRef="TIMER_EVENT" />
<bpmn:subProcess id="my_sub_process" name="Sub Process">
<bpmn:incoming>Flow_0op1a19</bpmn:incoming>
<bpmn:outgoing>Flow_1ivr6d7</bpmn:outgoing>
<bpmn:startEvent id="Event_0ubr6g5">
<bpmn:outgoing>Flow_1fcanuu</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1fcanuu" sourceRef="Event_0ubr6g5" targetRef="Activity_05x1bpx" />
<bpmn:scriptTask id="Activity_05x1bpx" name="Increment Counter #2">
<bpmn:incoming>Flow_1fcanuu</bpmn:incoming>
<bpmn:outgoing>Flow_04le6u5</bpmn:outgoing>
<bpmn:script>counter2 += 1</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_0umaw1x">
<bpmn:incoming>Flow_04le6u5</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_04le6u5" sourceRef="Activity_05x1bpx" targetRef="Event_0umaw1x" />
</bpmn:subProcess>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="loops_sub">
<bpmndi:BPMNEdge id="Flow_1ivr6d7_di" bpmnElement="Flow_1ivr6d7">
<di:waypoint x="490" y="310" />
<di:waypoint x="430" y="310" />
<di:waypoint x="430" y="145" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_15jw6a4_di" bpmnElement="Flow_15jw6a4">
<di:waypoint x="350" y="127" />
<di:waypoint x="412" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0op1a19_di" bpmnElement="Flow_0op1a19">
<di:waypoint x="920" y="152" />
<di:waypoint x="920" y="310" />
<di:waypoint x="840" y="310" />
<bpmndi:BPMNLabel>
<dc:Bounds x="863" y="292" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1tj9oz1_di" bpmnElement="Flow_1tj9oz1">
<di:waypoint x="945" y="127" />
<di:waypoint x="1072" y="127" />
<bpmndi:BPMNLabel>
<dc:Bounds x="958" y="109" width="19" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gb8wca_di" bpmnElement="Flow_1gb8wca">
<di:waypoint x="448" y="127" />
<di:waypoint x="625" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1d2usdq_di" bpmnElement="Flow_1d2usdq">
<di:waypoint x="725" y="127" />
<di:waypoint x="895" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0q7fkb7_di" bpmnElement="Flow_0q7fkb7">
<di:waypoint x="188" y="127" />
<di:waypoint x="250" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="109" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1fgfg5d_di" bpmnElement="increment_counter">
<dc:Bounds x="625" y="87" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1tseamj_di" bpmnElement="end_event5">
<dc:Bounds x="1072" y="109" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0whindt_di" bpmnElement="TIMER_EVENT">
<dc:Bounds x="412" y="109" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="419" y="85" width="22" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0gr4bqq_di" bpmnElement="Gateway_over_20" isMarkerVisible="true">
<dc:Bounds x="895" y="102" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="908" y="159" width="33" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0whalo9_di" bpmnElement="initialize">
<dc:Bounds x="250" y="87" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0v3bik3_di" bpmnElement="my_sub_process" isExpanded="true">
<dc:Bounds x="490" y="210" width="350" height="200" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_04le6u5_di" bpmnElement="Flow_04le6u5">
<di:waypoint x="720" y="310" />
<di:waypoint x="782" y="310" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1fcanuu_di" bpmnElement="Flow_1fcanuu">
<di:waypoint x="566" y="310" />
<di:waypoint x="620" y="310" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Event_0ubr6g5_di" bpmnElement="Event_0ubr6g5">
<dc:Bounds x="530" y="292" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_08o251p_di" bpmnElement="Activity_05x1bpx">
<dc:Bounds x="620" y="270" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0umaw1x_di" bpmnElement="Event_0umaw1x">
<dc:Bounds x="782" y="292" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -51,7 +51,7 @@ class CallActivityEscalationTest(BpmnWorkflowTestCase):
task.set_data(should_escalate=True)
self.workflow.do_engine_steps()
self.save_restore()
self.workflow.complete_all()
self.workflow.run_all()
self.assertEqual(True, self.workflow.is_completed())
self.assertEqual(True, 'EndEvent_specific1_noninterrupting_normal' in completed_set)
@ -81,7 +81,7 @@ class CallActivityEscalationTest(BpmnWorkflowTestCase):
task.set_data(should_escalate=False)
self.workflow.do_engine_steps()
self.save_restore()
self.workflow.complete_all()
self.workflow.run_all()
self.assertEqual(True, self.workflow.is_completed())
self.assertEqual(True, 'EndEvent_specific1_noninterrupting_normal' in completed_set)
@ -109,7 +109,7 @@ class CallActivityEscalationTest(BpmnWorkflowTestCase):
track_workflow(self.spec, completed_set)
self.workflow.do_engine_steps()
self.save_restore()
self.workflow.complete_all()
self.workflow.run_all()
self.assertEqual(True, self.workflow.is_completed())
self.assertEqual(True, 'EndEvent_specific1_noninterrupting_normal' in completed_set)

View File

@ -28,7 +28,7 @@ class EventBasedGatewayTest(BpmnWorkflowTestCase):
if save_restore:
self.save_restore()
self.workflow.script_engine = self.script_engine
self.assertEqual(len(waiting_tasks), 1)
self.assertEqual(len(waiting_tasks), 2)
self.workflow.catch(MessageEventDefinition('message_1'))
self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks()
@ -41,7 +41,7 @@ class EventBasedGatewayTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps()
waiting_tasks = self.workflow.get_waiting_tasks()
self.assertEqual(len(waiting_tasks), 1)
self.assertEqual(len(waiting_tasks), 2)
timer_event = waiting_tasks[0].task_spec.event_definition.event_definitions[-1]
self.workflow.catch(timer_event)
self.workflow.refresh_waiting_tasks()

View File

@ -33,7 +33,7 @@ class MultipleEventsTest(BpmnWorkflowTestCase):
task = self.workflow.get_tasks(TaskState.READY)[0]
# Move to User Task 1
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
task = self.workflow.get_tasks(TaskState.READY)[0]
self.assertEqual('UserTaskOne', task.get_name())
@ -52,10 +52,10 @@ class MultipleEventsTest(BpmnWorkflowTestCase):
task = self.workflow.get_tasks(TaskState.READY)[0]
# Move to User Task 2
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
task = self.workflow.get_tasks(TaskState.READY)[0]
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
task = self.workflow.get_tasks(TaskState.READY)[0]
self.assertEqual('UserTaskTwo', task.get_name())

View File

@ -42,6 +42,6 @@ class MultipleThrowEventStartsEventTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_ready_user_tasks()
self.assertEqual(len(ready_tasks), 1)
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
self.assertEqual(self.workflow.is_completed(), True)

View File

@ -62,7 +62,7 @@ class NITimerDurationTest(BpmnWorkflowTestCase):
task.data['delay_reason'] = 'Just Because'
elif task.task_spec.name == 'Activity_Work':
task.data['work_done'] = 'Yes'
task.complete()
task.run()
self.workflow.refresh_waiting_tasks()
self.workflow.do_engine_steps()
self.assertEqual(self.workflow.is_completed(),True)

View File

@ -55,11 +55,11 @@ class TimerCycleTest(BpmnWorkflowTestCase):
time.sleep(0.05)
self.workflow.refresh_waiting_tasks()
events = self.workflow.waiting_events()
if loopcount == 0:
# Wait time is 0.1s, so the first time through, there should still be a waiting event
if loopcount < 2:
# Wait time is 0.1s, two child tasks are created
self.assertEqual(len(events), 1)
else:
# By the second iteration, both should be complete
# By the third iteration, the event should no longer be waiting
self.assertEqual(len(events), 0)
# Get coffee still ready

View File

@ -39,7 +39,7 @@ class TimerDurationTest(BpmnWorkflowTestCase):
# Make sure the task can still be called.
task = self.workflow.get_ready_user_tasks()[0]
task.complete()
task.run()
self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed())

View File

@ -24,7 +24,7 @@ class TimerDurationTest(BpmnWorkflowTestCase):
def actual_test(self,save_restore = False):
self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_tasks(TaskState.READY)
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
loopcount = 0
@ -43,7 +43,7 @@ class TimerDurationTest(BpmnWorkflowTestCase):
self.assertEqual(subworkflow.state, TaskState.CANCELLED)
ready_tasks = self.workflow.get_ready_user_tasks()
while len(ready_tasks) > 0:
ready_tasks[0].complete()
ready_tasks[0].run()
ready_tasks = self.workflow.get_ready_user_tasks()
self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed())

View File

@ -19,11 +19,11 @@ class TransactionSubprocessTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_tasks(TaskState.READY)
ready_tasks[0].update_data({'value': 'asdf'})
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_tasks(TaskState.READY)
ready_tasks[0].update_data({'quantity': 2})
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
self.assertIn('value', self.workflow.last_task.data)
@ -48,7 +48,7 @@ class TransactionSubprocessTest(BpmnWorkflowTestCase):
# If value == '', we cancel
ready_tasks[0].update_data({'value': ''})
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
# If the subprocess gets cancelled, verify that data set there does not persist
@ -72,13 +72,13 @@ class TransactionSubprocessTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_tasks(TaskState.READY)
ready_tasks[0].update_data({'value': 'asdf'})
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_tasks(TaskState.READY)
# If quantity == 0, we throw an error with no error code
ready_tasks[0].update_data({'quantity': 0})
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
# We formerly checked that subprocess data does not persist, but I think it should persist
@ -103,13 +103,13 @@ class TransactionSubprocessTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_tasks(TaskState.READY)
ready_tasks[0].update_data({'value': 'asdf'})
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_tasks(TaskState.READY)
# If quantity < 0, we throw 'Error 1'
ready_tasks[0].update_data({'quantity': -1})
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
# The cancel boundary event should be cancelled

View File

@ -142,7 +142,7 @@ class BpmnWorkflowSerializerTest(BaseTestCase):
def test_serialize_workflow_where_script_task_includes_function(self):
self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_ready_user_tasks()
ready_tasks[0].complete()
ready_tasks[0].run()
self.workflow.do_engine_steps()
results = self.serializer.serialize_json(self.workflow)
assert self.workflow.is_completed()
@ -161,7 +161,7 @@ class BpmnWorkflowSerializerTest(BaseTestCase):
self.assertEqual(w1.data, w2.data)
self.assertEqual(w1.name, w2.name)
for task in w1.get_ready_user_tasks():
w2_task = w2.get_task(task.id)
w2_task = w2.get_task_from_id(task.id)
self.assertIsNotNone(w2_task)
self.assertEqual(task.data, w2_task.data)

View File

@ -16,11 +16,12 @@ class Version_1_0_Test(BaseTestCase):
def test_convert_subprocess(self):
# The serialization used here comes from NestedSubprocessTest saved at line 25 with version 1.0
fn = os.path.join(self.DATA_DIR, 'serialization', 'v1.0.json')
wf = self.serializer.deserialize_json(open(fn).read())
with open(fn) as fh:
wf = self.serializer.deserialize_json(fh.read())
# We should be able to finish the workflow from this point
ready_tasks = wf.get_tasks(TaskState.READY)
self.assertEqual('Action3', ready_tasks[0].task_spec.description)
ready_tasks[0].complete()
ready_tasks[0].run()
wf.do_engine_steps()
self.assertEqual(True, wf.is_completed())
@ -49,7 +50,7 @@ class Version_1_1_Test(BaseTestCase):
self.assertEqual(len(task.task_spec.cond_task_specs), 2)
ready_task = wf.get_ready_user_tasks()[0]
ready_task.data['NeedClarification'] = 'Yes'
ready_task.complete()
ready_task.run()
wf.do_engine_steps()
ready_task = wf.get_ready_user_tasks()[0]
self.assertEqual(ready_task.task_spec.name, 'Activity_A2')
@ -58,4 +59,15 @@ class Version_1_1_Test(BaseTestCase):
fn = os.path.join(self.DATA_DIR, 'serialization', 'v1.1-multi.json')
with self.assertRaises(VersionMigrationError) as ctx:
wf = self.serializer.deserialize_json(open(fn).read())
self.assertEqual(ctx.exception.message, "This workflow cannot be migrated because it contains MultiInstance Tasks")
self.assertEqual(ctx.exception.message, "This workflow cannot be migrated because it contains MultiInstance Tasks")
def test_remove_loop_reset(self):
fn = os.path.join(self.DATA_DIR, 'serialization', 'v1.1-loop-reset.json')
wf = self.serializer.deserialize_json(open(fn).read())
# Allow 3 seconds max to allow this test to complete (there are 20 loops with a 0.1s timer)
end = time.time() + 3
while not wf.is_completed() and time.time() < end:
wf.do_engine_steps()
wf.refresh_waiting_tasks()
self.assertTrue(wf.is_completed())
self.assertEqual(wf.last_task.data['counter'], 20)

View File

@ -39,7 +39,7 @@ class CallActivityMessageTest(BaseTestCase):
current_task = ready_tasks[0]
self.assertEqual(current_task.task_spec.name,step[0])
current_task.update_data(step[1])
current_task.complete()
current_task.run()
self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks()
if save_restore: self.save_restore()

View File

@ -52,7 +52,7 @@ class ClashingNameTest(BaseTestCase):
firsttaskid = task.id
self.assertEqual(step['taskname'], task.task_spec.name)
task.update_data({step['formvar']: step['answer']})
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
if save_restore: self.save_restore()
@ -68,7 +68,7 @@ class ClashingNameTest(BaseTestCase):
task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(step['taskname'], task.task_spec.name)
task.update_data({step['formvar']: step['answer']})
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
if save_restore: self.save_restore()

View File

@ -29,7 +29,7 @@ class DMNCustomScriptTest(BaseTestCase):
def complete_manual_task(self):
manual_task = self.workflow.get_tasks_from_spec_name('manual_task')[0]
self.workflow.complete_task_from_id(manual_task.id)
self.workflow.run_task_from_id(manual_task.id)
self.workflow.do_engine_steps()
def testDmnHappy(self):

View File

@ -16,7 +16,7 @@ class DMNDictTest(BaseTestCase):
self.workflow = BpmnWorkflow(self.spec)
self.workflow.do_engine_steps()
x = self.workflow.get_ready_user_tasks()
self.workflow.complete_task_from_id(x[0].id)
self.workflow.run_task_from_id(x[0].id)
self.workflow.do_engine_steps()
self.assertDictEqual(self.workflow.last_task.data, self.expectedResult)
@ -25,7 +25,7 @@ class DMNDictTest(BaseTestCase):
self.workflow.do_engine_steps()
self.save_restore()
x = self.workflow.get_ready_user_tasks()
self.workflow.complete_task_from_id(x[0].id)
self.workflow.run_task_from_id(x[0].id)
self.workflow.do_engine_steps()
self.save_restore()
self.assertDictEqual(self.workflow.last_task.data, self.expectedResult)

View File

@ -41,7 +41,7 @@ class ExternalMessageBoundaryTest(BaseTestCase):
self.assertEqual(True, ready_tasks[1].data['caughtinterrupt'])
self.assertEqual('Meaningless User Task',ready_tasks[0].task_spec.description)
self.assertEqual(False, ready_tasks[0].data['caughtinterrupt'])
ready_tasks[1].complete()
ready_tasks[1].run()
self.workflow.do_engine_steps()
# what I think is going on here is that when we hit the reset, it is updating the
# last_task and appending the data to whatever happened there, so it would make sense that
@ -52,7 +52,7 @@ class ExternalMessageBoundaryTest(BaseTestCase):
# The user activity was cancelled and we should continue from the boundary event
self.assertEqual(1, len(ready_tasks),'Expected to have two ready tasks')
event = self.workflow.get_tasks_from_spec_name('Event_19detfv')[0]
event.complete()
event.run()
self.assertEqual('SomethingDrastic', event.data['reset_var'])
self.assertEqual(False, event.data['caughtinterrupt'])

View File

@ -31,7 +31,7 @@ class BusinessRuleTaskParserTest(BaseTestCase):
self.assertTrue(True, "An error was raised..")
self.assertEqual("InvalidDecisionTaskId", we.task_spec.name)
self.maxDiff = 1000
self.assertEquals("Error evaluating expression 'spam= 1'. Rule failed on row 1. Business Rule Task 'Invalid Decision'.", str(we))
self.assertEqual("Error evaluating expression 'spam= 1'. Rule failed on row 1. Business Rule Task 'Invalid Decision'.", str(we))
def suite():
return unittest.TestLoader().loadTestsFromTestCase(BusinessRuleTaskParserTest)

View File

@ -41,7 +41,7 @@ class MessageBoundaryTest(BaseTestCase):
if task.task_spec.name == step[0]:
task.update_data(step[1])
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
time.sleep(.01)
self.workflow.refresh_waiting_tasks()

View File

@ -23,10 +23,10 @@ class MultiInstanceDMNTest(BaseTestCase):
self.save_restore()
self.workflow.do_engine_steps()
self.workflow.complete_next()
self.workflow.run_next()
self.save_restore()
self.workflow.do_engine_steps()
self.workflow.complete_next()
self.workflow.run_next()
self.save_restore()
self.workflow.do_engine_steps()
self.save_restore()

View File

@ -29,7 +29,7 @@ class NIMessageBoundaryTest(BaseTestCase):
ready_tasks = self.workflow.get_tasks(TaskState.READY)
self.assertEqual(1, len(ready_tasks))
self.workflow.complete_task_from_id(ready_tasks[0].id)
self.workflow.run_task_from_id(ready_tasks[0].id)
self.workflow.do_engine_steps()
# first we run through a couple of steps where we answer No to each
@ -45,7 +45,7 @@ class NIMessageBoundaryTest(BaseTestCase):
'We got a ready task that we did not expect - %s'%(
task.task_spec.name))
task.data[response[0]] = response[1]
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
# if we have a list of tasks - that list becomes invalid
# after we do a save restore, so I'm completing the list
@ -66,7 +66,7 @@ class NIMessageBoundaryTest(BaseTestCase):
'We got a ready task that we did not expect - %s'%(
task.task_spec.name))
task.data[response[0]] = response[1]
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
if save_restore: self.save_restore()
@ -75,14 +75,14 @@ class NIMessageBoundaryTest(BaseTestCase):
task = ready_tasks[0]
self.assertEqual(task.task_spec.name,'Activity_DoWork')
task.data['work_done'] = 'Yes'
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_tasks(TaskState.READY)
self.assertEqual(len(ready_tasks), 1)
task = ready_tasks[0]
self.assertEqual(task.task_spec.name, 'Activity_WorkCompleted')
task.data['work_completed'] = 'Lots of Stuff'
self.workflow.complete_task_from_id(task.id)
self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps()
self.assertEqual(self.workflow.is_completed(),True)
self.assertEqual(self.workflow.last_task.data,{'Event_InterruptBoundary_Response': 'Youre late!',

View File

@ -32,7 +32,7 @@ class ParseMultiInstanceTest(BaseTestCase):
self.assertEqual(len(ready_tasks), 3)
for task in ready_tasks:
task.data['output_item'] = task.data['output_item'] * 2
task.complete()
task.run()
self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed())
@ -58,7 +58,7 @@ class ParseMultiInstanceTest(BaseTestCase):
self.assertEqual(len(ready_tasks), 3)
for task in ready_tasks:
task.data['output_item'] = task.data['output_item'] * 2
task.complete()
task.run()
self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed())
@ -84,7 +84,7 @@ class ParseMultiInstanceTest(BaseTestCase):
self.assertEqual(len(ready_tasks), 3)
for task in ready_tasks:
task.data['input_item'] = task.data['input_item'] * 2
task.complete()
task.run()
self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed())

Some files were not shown because too many files have changed in this diff Show More