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