[Zope3-checkins] SVN: Zope3/trunk/src/zope/wfmc/ Fixed a
bug/missfeature. Split transition-ordering specifications
Jim Fulton
jim at zope.com
Wed May 4 10:12:07 EDT 2005
Log message for revision 30239:
Fixed a bug/missfeature. Split transition-ordering specifications
were ignored. We were basing evaluation of transition conditions on
transition-definition order. We should, instead, base order of
evaluation of transition conditions on the order given in splits. The
spec is a bit vague in this area as it suggests that transition
restrictions are optional and that there can be multiple, which
doesn't make much sense.
Also changed the strategy for setting up transition information. We
now compute activity incoming and outgoing transitions as we build up
the model, rather than waiting until we compute the start node.
Added an attribute to teh piblic process-definition interface. This
was already used in the documentation.
Simplified the element-handling logic in the xpdl code.
Changed:
U Zope3/trunk/src/zope/wfmc/README.txt
U Zope3/trunk/src/zope/wfmc/interfaces.py
U Zope3/trunk/src/zope/wfmc/process.py
U Zope3/trunk/src/zope/wfmc/publication.xpdl
U Zope3/trunk/src/zope/wfmc/xpdl.py
-=-
Modified: Zope3/trunk/src/zope/wfmc/README.txt
===================================================================
--- Zope3/trunk/src/zope/wfmc/README.txt 2005-05-04 13:24:21 UTC (rev 30238)
+++ Zope3/trunk/src/zope/wfmc/README.txt 2005-05-04 14:12:06 UTC (rev 30239)
@@ -121,7 +121,7 @@
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 @@
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
Modified: Zope3/trunk/src/zope/wfmc/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/wfmc/interfaces.py 2005-05-04 13:24:21 UTC (rev 30238)
+++ Zope3/trunk/src/zope/wfmc/interfaces.py 2005-05-04 14:12:06 UTC (rev 30239)
@@ -31,10 +31,17 @@
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
+ """
+ )
+
applications = interface.Attribute(
"""Process applications
Modified: Zope3/trunk/src/zope/wfmc/process.py
===================================================================
--- Zope3/trunk/src/zope/wfmc/process.py 2005-05-04 13:24:21 UTC (rev 30238)
+++ Zope3/trunk/src/zope/wfmc/process.py 2005-05-04 14:12:06 UTC (rev 30239)
@@ -56,6 +56,12 @@
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 @@
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 @@
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 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 @@
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
Modified: Zope3/trunk/src/zope/wfmc/publication.xpdl
===================================================================
--- Zope3/trunk/src/zope/wfmc/publication.xpdl 2005-05-04 13:24:21 UTC (rev 30238)
+++ Zope3/trunk/src/zope/wfmc/publication.xpdl 2005-05-04 14:12:06 UTC (rev 30239)
@@ -179,8 +179,8 @@
<Join Type="XOR"/>
<Split Type="AND">
<TransitionRefs>
+ <TransitionRef Id="Publication_Tra2"/>
<TransitionRef Id="Publication_Tra3"/>
- <TransitionRef Id="Publication_Tra2"/>
</TransitionRefs>
</Split>
</TransitionRestriction>
@@ -262,9 +262,9 @@
<Split Type="XOR">
<TransitionRefs>
<TransitionRef Id="Publication_Tra9"/>
- <TransitionRef Id="Publication_Tra10"/>
<TransitionRef Id="Publication_Tra8"/>
<TransitionRef Id="Publication_Tra7"/>
+ <TransitionRef Id="Publication_Tra10"/>
</TransitionRefs>
</Split>
</TransitionRestriction>
Modified: Zope3/trunk/src/zope/wfmc/xpdl.py
===================================================================
--- Zope3/trunk/src/zope/wfmc/xpdl.py 2005-05-04 13:24:21 UTC (rev 30238)
+++ Zope3/trunk/src/zope/wfmc/xpdl.py 2005-05-04 14:12:06 UTC (rev 30239)
@@ -82,6 +82,10 @@
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 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 @@
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
More information about the Zope3-Checkins
mailing list