[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