Skip to content
This repository has been archived by the owner on Dec 21, 2020. It is now read-only.

Commit

Permalink
Fixed a bug/missfeature. Split transition-ordering specifications
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Jim Fulton committed May 4, 2005
1 parent 65b8fdf commit 5081c2b
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 26 deletions.
156 changes: 155 additions & 1 deletion README.txt
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion interfaces.py
Expand Up @@ -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
"""
)

Expand Down
39 changes: 28 additions & 11 deletions process.py
Expand Up @@ -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
Expand All @@ -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 = ()
Expand Down Expand Up @@ -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

Expand All @@ -147,14 +144,34 @@ 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

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
Expand Down
4 changes: 2 additions & 2 deletions publication.xpdl
Expand Up @@ -179,8 +179,8 @@
<Join Type="XOR"/>
<Split Type="AND">
<TransitionRefs>
<TransitionRef Id="Publication_Tra3"/>
<TransitionRef Id="Publication_Tra2"/>
<TransitionRef Id="Publication_Tra3"/>
</TransitionRefs>
</Split>
</TransitionRestriction>
Expand Down Expand Up @@ -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>
Expand Down
20 changes: 9 additions & 11 deletions xpdl.py
Expand Up @@ -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''

Expand All @@ -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')]
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 5081c2b

Please sign in to comment.