diff --git a/README.txt b/README.txt
index 8f99c3e..c99e26a 100644
--- a/README.txt
+++ b/README.txt
@@ -121,7 +121,7 @@ take a data object and return a boolean value. The data object is
called "workflow-relevant data". A process instance has a data object
containing this data. In the example, above, the condition simply
returned the value of the `publish` attribute. How does this attribute
-get set? It needs to be set by the review activity. Do to that, we
+get set? It needs to be set by the review activity. To do that, we
need to arrange for the activity to set the data. This brings us to
applications.
@@ -312,6 +312,160 @@ be published:
ActivityFinished(Activity('sample.reject'))
ProcessFinished(Process('sample'))
+Ordering output transitions
+---------------------------
+
+Normally, outgoing transitions are ordered in the order of transition
+definition and all transitions from a given activity are used.
+
+If transitions are defined in an inconvenient order, then the workflow
+might not work as expected. For example, let's modify the above
+process by switching the order of definition of some of the
+transitions:
+
+ >>> pd = process.ProcessDefinition('sample')
+ >>> zope.component.provideUtility(pd, name=pd.id)
+ >>> pd.defineActivities(
+ ... author = process.ActivityDefinition(),
+ ... review = process.ActivityDefinition(),
+ ... publish = process.ActivityDefinition(),
+ ... reject = process.ActivityDefinition(),
+ ... )
+ >>> pd.defineTransitions(
+ ... process.TransitionDefinition('author', 'review'),
+ ... process.TransitionDefinition('review', 'reject'),
+ ... process.TransitionDefinition(
+ ... 'review', 'publish', condition=lambda data: data.publish),
+ ... )
+
+ >>> pd.defineApplications(
+ ... author = process.Application(),
+ ... review = process.Application(
+ ... process.OutputParameter('publish')),
+ ... publish = process.Application(),
+ ... reject = process.Application(),
+ ... )
+
+ >>> pd.activities['author'].addApplication('author')
+ >>> pd.activities['review'].addApplication('review', ['publish'])
+ >>> pd.activities['publish'].addApplication('publish')
+ >>> pd.activities['reject'].addApplication('reject')
+
+ >>> pd.defineParticipants(
+ ... author = process.Participant(),
+ ... reviewer = process.Participant(),
+ ... )
+
+ >>> pd.activities['author'].definePerformer('author')
+ >>> pd.activities['review'].definePerformer('reviewer')
+
+and run our process:
+
+ >>> proc = pd()
+ >>> proc.start()
+ ... # doctest: +NORMALIZE_WHITESPACE
+ ProcessStarted(Process('sample'))
+ Transition(None, Activity('sample.author'))
+ ActivityStarted(Activity('sample.author'))
+
+ >>> work_list.pop().finish()
+ WorkItemFinished('author')
+ ActivityFinished(Activity('sample.author'))
+ Transition(Activity('sample.author'), Activity('sample.review'))
+ ActivityStarted(Activity('sample.review'))
+
+This time, we'll say that we should publish:
+
+ >>> work_list.pop().finish(True)
+ WorkItemFinished('review')
+ ActivityFinished(Activity('sample.review'))
+ Transition(Activity('sample.review'), Activity('sample.reject'))
+ ActivityStarted(Activity('sample.reject'))
+ Rejected
+ WorkItemFinished('reject')
+ ActivityFinished(Activity('sample.reject'))
+ ProcessFinished(Process('sample'))
+
+But we went to the reject activity anyway. Why? Because transitions
+are tested in order. Because the transition to the reject activity was
+tested first and had no condition, we followed it without checking the
+condition for the transition to the publish activity. We can fix this
+by specifying outgoing transitions on the reviewer activity directly.
+To do this, we'll also need to specify ids in our transitions. Let's
+redefine the process:
+
+
+ >>> pd = process.ProcessDefinition('sample')
+ >>> zope.component.provideUtility(pd, name=pd.id)
+ >>> pd.defineActivities(
+ ... author = process.ActivityDefinition(),
+ ... review = process.ActivityDefinition(),
+ ... publish = process.ActivityDefinition(),
+ ... reject = process.ActivityDefinition(),
+ ... )
+ >>> pd.defineTransitions(
+ ... process.TransitionDefinition('author', 'review'),
+ ... process.TransitionDefinition('review', 'reject', id='reject'),
+ ... process.TransitionDefinition(
+ ... 'review', 'publish', id='publish',
+ ... condition=lambda data: data.publish),
+ ... )
+
+ >>> pd.defineApplications(
+ ... author = process.Application(),
+ ... review = process.Application(
+ ... process.OutputParameter('publish')),
+ ... publish = process.Application(),
+ ... reject = process.Application(),
+ ... )
+
+ >>> pd.activities['author'].addApplication('author')
+ >>> pd.activities['review'].addApplication('review', ['publish'])
+ >>> pd.activities['publish'].addApplication('publish')
+ >>> pd.activities['reject'].addApplication('reject')
+
+ >>> pd.defineParticipants(
+ ... author = process.Participant(),
+ ... reviewer = process.Participant(),
+ ... )
+
+ >>> pd.activities['author'].definePerformer('author')
+ >>> pd.activities['review'].definePerformer('reviewer')
+
+ >>> pd.activities['review'].addOutgoing('publish')
+ >>> pd.activities['review'].addOutgoing('reject')
+
+Now, when we run the process, we'll go to the publish activity as
+expected:
+
+
+ >>> proc = pd()
+ >>> proc.start()
+ ... # doctest: +NORMALIZE_WHITESPACE
+ ProcessStarted(Process('sample'))
+ Transition(None, Activity('sample.author'))
+ ActivityStarted(Activity('sample.author'))
+
+ >>> work_list.pop().finish()
+ WorkItemFinished('author')
+ ActivityFinished(Activity('sample.author'))
+ Transition(Activity('sample.author'), Activity('sample.review'))
+ ActivityStarted(Activity('sample.review'))
+
+ >>> work_list.pop().finish(True)
+ WorkItemFinished('review')
+ ActivityFinished(Activity('sample.review'))
+ Transition(Activity('sample.review'), Activity('sample.publish'))
+ ActivityStarted(Activity('sample.publish'))
+ Published
+ WorkItemFinished('publish')
+ ActivityFinished(Activity('sample.publish'))
+ ProcessFinished(Process('sample'))
+
+
+Complex Flows
+-------------
+
Lets look at a more complex example. In this example, we'll extend
the process to work with multiple reviewers. We'll also make the
work-list handling a bit more sophisticated. We'll also introduce
diff --git a/interfaces.py b/interfaces.py
index b4d5b1a..a467f6e 100644
--- a/interfaces.py
+++ b/interfaces.py
@@ -31,7 +31,14 @@ class IProcessDefinition(interface.Interface):
participants = interface.Attribute(
"""Process participants
- This is a mapping from participant id to participant definitions
+ This is a mapping from participant id to participant definition
+ """
+ )
+
+ activities = interface.Attribute(
+ """Process activities
+
+ This is a mapping from activity id to activity definition
"""
)
diff --git a/process.py b/process.py
index 35620e3..d9b4b0c 100644
--- a/process.py
+++ b/process.py
@@ -56,6 +56,12 @@ def defineTransitions(self, *transitions):
self._dirty()
self.transitions.extend(transitions)
+ # Compute activity transitions based on transition data:
+ activities = self.activities
+ for transition in transitions:
+ activities[transition.from_].transitionOutgoing(transition)
+ activities[transition.to].incoming += (transition, )
+
def defineApplications(self, **applications):
for id, application in applications.items():
application.id = id
@@ -70,20 +76,10 @@ def defineParameters(self, *parameters):
self.parameters += parameters
def _start(self):
- # Compute activity incoming and outgoing transitions
# Return an initial transition
activities = self.activities
- # Reinitialize activity transitions
- for activity in activities.values():
- activity.incoming = activity.outgoing = ()
-
- # Recompute activity transitions based on transition data:
- for transition in self.transitions:
- activities[transition.from_].outgoing += (transition, )
- activities[transition.to].incoming += (transition, )
-
# Find the start, making sure that there is one and that there
# aren't any activities w no transitions:
start = ()
@@ -128,6 +124,7 @@ class ActivityDefinition:
def __init__(self, __name__=None):
self.__name__ = __name__
self.incoming = self.outgoing = ()
+ self.transition_outgoing = self.explicit_outgoing = ()
self.applications = ()
self.andJoinSetting = self.andSplitSetting = False
@@ -147,6 +144,25 @@ def addApplication(self, application, actual=()):
def definePerformer(self, performer):
self.performer = performer
+ def addOutgoing(self, transition_id):
+ self.explicit_outgoing += (transition_id,)
+ self.computeOutgoing()
+
+ def transitionOutgoing(self, transition):
+ self.transition_outgoing += (transition,)
+ self.computeOutgoing()
+
+ def computeOutgoing(self):
+ if self.explicit_outgoing:
+ transitions = dict([(t.id, t) for t in self.transition_outgoing])
+ self.outgoing = ()
+ for tid in self.explicit_outgoing:
+ transition = transitions.get(tid)
+ if transition is not None:
+ self.outgoing += (transition,)
+ else:
+ self.outgoing = self.transition_outgoing
+
def always_true(data):
return True
@@ -154,7 +170,8 @@ class TransitionDefinition:
interface.implements(interfaces.ITransitionDefinition)
- def __init__(self, from_, to, condition=always_true):
+ def __init__(self, from_, to, condition=always_true, id=None):
+ self.id = id
self.from_ = from_
self.to = to
self.condition = condition
diff --git a/publication.xpdl b/publication.xpdl
index 93d09f2..6846a16 100644
--- a/publication.xpdl
+++ b/publication.xpdl
@@ -179,8 +179,8 @@
-
+
@@ -262,9 +262,9 @@
-
+
diff --git a/xpdl.py b/xpdl.py
index ae894f3..fbc7b59 100644
--- a/xpdl.py
+++ b/xpdl.py
@@ -82,6 +82,10 @@ def startElementNS(self, name, qname, attrs):
else:
result = None
+ if result is None:
+ # Just dup the top of the stack
+ result = self.stack[-1]
+
self.stack.append(result)
self.text = u''
@@ -104,22 +108,11 @@ def characters(self, text):
def setDocumentLocator(self, locator):
self.locator = locator
- def dup(self, attrs):
- # Just duplicate whatever is on the top of the stack
- return self.stack[-1]
-
######################################################################
# Application handlers
# Pointless container elements that we want to "ignore" by having them
# dup their containers:
- for tag in (
- 'FormalParameters', 'Participants', 'Applications', 'Activities',
- 'Implementation', 'ActualParameters', 'Transitions',
- 'TransitionRestrictions', 'TransitionRestriction',
- ):
- start_handlers[(xpdlns, tag)] = dup
-
def Package(self, attrs):
package = self.package
package.id = attrs[(None, 'Id')]
@@ -222,6 +215,11 @@ def Split(self, attrs):
if Type == u'AND':
self.stack[-1].andSplit(True)
start_handlers[(xpdlns, 'Split')] = Split
+
+ def TransitionRef(self, attrs):
+ Id = attrs.get((None, 'Id'))
+ self.stack[-1].addOutgoing(Id)
+ start_handlers[(xpdlns, 'TransitionRef')] = TransitionRef
# Activity definitions