From 5081c2b575be59e1c64a4203e6006569bd8b7862 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Wed, 4 May 2005 14:12:06 +0000 Subject: [PATCH] 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. --- README.txt | 156 ++++++++++++++++++++++++++++++++++++++++++++++- interfaces.py | 9 ++- process.py | 39 ++++++++---- publication.xpdl | 4 +- xpdl.py | 20 +++--- 5 files changed, 202 insertions(+), 26 deletions(-) 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