[Zope3-checkins] SVN: Zope3/trunk/src/zope/wfmc/ Created a first-cut wfmc workflow engine implementation.

Jim Fulton jim at zope.com
Wed Dec 22 15:47:06 EST 2004


Log message for revision 28689:
  Created a first-cut wfmc workflow engine implementation.
  
  

Changed:
  A   Zope3/trunk/src/zope/wfmc/
  A   Zope3/trunk/src/zope/wfmc/README.txt
  A   Zope3/trunk/src/zope/wfmc/__init__.py
  A   Zope3/trunk/src/zope/wfmc/interfaces.py
  A   Zope3/trunk/src/zope/wfmc/process.py
  A   Zope3/trunk/src/zope/wfmc/tests.py

-=-
Added: Zope3/trunk/src/zope/wfmc/README.txt
===================================================================
--- Zope3/trunk/src/zope/wfmc/README.txt	2004-12-22 18:12:54 UTC (rev 28688)
+++ Zope3/trunk/src/zope/wfmc/README.txt	2004-12-22 20:47:04 UTC (rev 28689)
@@ -0,0 +1,876 @@
+Workflow-Management Coalition Workflow Engine
+=============================================
+
+This package provides an implementation of a Workflow-Management
+Coalition (WFMC) workflow engine.  The engine is provided as a
+collection of workflow process components.  Workflow processes can be
+defined in Python or via the XML Process-Definition Language, XPDL.
+
+In this document, we'll look at Python-defined process definitions:
+
+    >>> from zope.wfmc import process
+    >>> pd = process.ProcessDefinition('sample')
+
+The argument to the process is a process id.
+
+A process has a number of parts.  Let's look at a sample review
+process::
+
+                              -----------
+                           -->| Publish |
+  ----------   ---------- /   -----------
+  | Author |-->| Review |-    ----------
+  ----------   ---------- \-->| Reject |
+                              ----------  
+
+Here we have a single start activity and 2 end activities.  We could
+have modeled this with a single end activity, but that is not
+required.  A single start activity *is* required. A process definition
+has a set of activities, with transitions between them.  Let's define
+the activities for our process definition:
+
+    >>> pd.defineActivities(
+    ...     author  = process.ActivityDefinition(),
+    ...     review  = process.ActivityDefinition(),
+    ...     publish = process.ActivityDefinition(),
+    ...     reject  = process.ActivityDefinition(),
+    ...     )
+
+We supply activities as keyword arguments. The argument names provide
+activity ids that we'll use when defining transitions:
+
+    >>> pd.defineTransitions(
+    ...     process.TransitionDefinition('author', 'review'),
+    ...     process.TransitionDefinition('review', 'publish'),
+    ...     process.TransitionDefinition('review', 'reject'),
+    ...     )
+
+Each transition is constructed with an identifier for a starting
+activity, and an identifier for an ending activity.
+
+Before we can use a workflow definition, we have to register it as a
+utility. This is necessary so that process instances can find their
+definitions.  In addition, the utility name must match the process id:
+
+    >>> import zope.component
+    >>> zope.component.provideUtility(pd, name=pd.id)
+
+Now, with this definition, we can execute our workflow.  We haven't
+defined any work yet, but we can see the workflow execute.  We'll see
+the workflow executing by registering a subscriber that logs workflow
+events:
+
+    >>> def log_workflow(event):
+    ...     print event
+
+    >>> import zope.event
+    >>> zope.event.subscribers.append(log_workflow)
+
+To use the workflow definition, we need to create an instance:
+
+    >>> proc = pd()
+
+Now, if we start the workflow:
+
+    >>> proc.start()
+    ProcessStarted(Process('sample'))
+    Transition(None, Activity('sample.author'))
+    ActivityStarted(Activity('sample.author'))
+    ActivityFinished(Activity('sample.author'))
+    Transition(Activity('sample.author'), Activity('sample.review'))
+    ActivityStarted(Activity('sample.review'))
+    ActivityFinished(Activity('sample.review'))
+    Transition(Activity('sample.review'), Activity('sample.publish'))
+    ActivityStarted(Activity('sample.publish'))
+    ActivityFinished(Activity('sample.publish'))
+    ProcessFinished(Process('sample'))
+
+we see that we transition immediately into the author activity, then
+into review and publish.  Normally, we'd need to do some work in each
+activity, and transitions would continue only after work had been
+done, however, in this case, we didn't define any work, so each
+activity completed immediately.  
+
+Note that we didn't transition into the rejected activity.  By
+default, when an activity is completed, the first transition for which
+it's condition evaluates to `True` is used.  By default, transitions
+have boolean conditions [1]_ that evaluate to `True`, so the transition
+to `publish` is used because it was defined before the transition to
+`reject`.  What we want is to transition to `publish` if a reviewer
+approves the content for publication, but to `reject` if the reviewer
+rejects the content for publication.  We can use a condition for this:
+
+    >>> 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', 'publish', condition=lambda data: data.publish),
+    ...     process.TransitionDefinition('review', 'reject'),
+    ...     )
+
+We redefined the workflow process, specifying a condition for the
+transition to `publish`.  Boolean conditions are just callable objects that
+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
+need to arrange for the activity to set the data.  This brings us to
+applications.
+
+Process definitions are meant to be used with different
+applications. For this reason, process definitions don't include
+application logic.  What they do include is a specifications of the
+applications to be invoked and the flow of work-flow-relevant data to
+and from the application.  Now, we can define our applications:
+
+    >>> pd.defineApplications(
+    ...     author = process.Application(),
+    ...     review = process.Application(
+    ...         process.OutputParameter('publish')),
+    ...     publish = process.Application(),
+    ...     reject = process.Application(),
+    ...     )
+
+We used the same names for the applications that we used for our
+activities. This isn't required, but is a common practice.  Note that
+the `review` application includes a specification of an output
+parameter.  Now that we've defined our applications, we need to modify
+our activites to use them:
+
+    >>> pd.activities['author'].addApplication('author')
+    >>> pd.activities['review'].addApplication('review',
+    ...     process.OutputParameter('publish'))
+    >>> pd.activities['publish'].addApplication('publish')
+    >>> pd.activities['reject'].addApplication('reject')
+
+An activity can use many applications, so we call `addApplication`.  In
+the application definition for the 'review' application, we again
+specified an output arguments definition. Why do we specify the output
+parameter for both the application definition and the application
+assignment?  The parameters in the application definition are "formal
+paramerters".  The paramerters in the application assignment are
+"actual parameters".  Parameters are posistional. The names (but not
+the direction) are allowed to differ.  The names of the actual
+parameters are used to access or update workflow-relevant data.  In
+this example, the output parameter, will be used to add a `publish`
+attribute to the workflow relevant data.
+
+We've declared some applications, and we've wired them up to
+activitis, but we still haven't specified any application code. Before
+we can specify application code, we need to consider who will be
+performing the application.  Workflow applications are normally
+executed by people, or other external actors.  As with applications,
+process definitions allow participants in the workflow to be declared
+and identified with activities.  We declare participants much as we
+declare applications, except without parameters:
+
+    >>> pd.defineParticipants(
+    ...     author   = process.Participant(),
+    ...     reviewer = process.Participant(),
+    ...     )
+
+In this case, we happened to reuse an activity name for one, but
+not both of the participants.  Having defined these participants, we
+can associate them with activities:
+
+    >>> pd.activities['author'].definePerformer('author')
+    >>> pd.activities['review'].definePerformer('reviewer')
+
+For each of the participants provided, we need to provide a named
+adapter from an activity instance
+(`zope.wfmc.interfaces.IActivity`) to
+`zope.wfmc.interfaces.IParticipant`.  When a process needs to get a
+participant, it adapts the activity instance to `IParticipant` using a
+qualified name, consisting of the process-definition identifier, a
+dot, and the performer's participant identifier. If an adapter can't
+be found with that name, it tries again using just a dot followed by
+the participant identifier.  This way, participant adapters can be
+shared accross multiple process definitions, or provided for a
+specific definition.  If an activity doesn't have a performer, then
+procedure above is used with an empty participant id.  We first look
+for an adapter with a name consisting of the process id followed by a
+dot, and then we look for an adapter with a single dot as it's name.
+
+Application implementations are provided as named adapters from
+participants to `zope.wfmc.interfaces.IWorkItem`.  As when looking up
+applications, we first look for an adapter with a name consisting of
+the process-definition id, a dot, and the application id.  If that
+fails, we look for an adapter consisting of a dot followed by the
+application id.  Workflow items provide a `start` method, which is
+used to start the work and pass input arguments.  It is the
+responsibility of the work item, at some later time, to call the
+`workitemFinished` method on the activity, to notify the activity that
+the work item was completed. Output parameters are passed to the
+`workitemFinished` method.
+
+Let's implement participants for our process. We'll start with a
+generic participant:
+
+    >>> import zope.interface
+    >>> from zope.wfmc import interfaces
+
+    >>> class Participant(object):
+    ...     zope.component.adapts(interfaces.IActivity)
+    ...     zope.interface.implements(interfaces.IParticipant)
+    ...
+    ...     def __init__(self, activity):
+    ...         self.activity = activity
+
+    >>> zope.component.provideAdapter(Participant, name=".author")
+    >>> zope.component.provideAdapter(Participant, name=".reviewer")
+    >>> zope.component.provideAdapter(Participant, name=".")
+
+And finally, we can define adapters that implement our application:
+
+    >>> work_list = []
+
+    >>> class ApplicationBase:
+    ...     zope.component.adapts(interfaces.IParticipant)
+    ...     zope.interface.implements(interfaces.IWorkItem)
+    ...
+    ...     def __init__(self, participant):
+    ...         self.participant = participant
+    ...         work_list.append(self)
+    ...
+    ...     def start(self):
+    ...         pass
+    ...
+    ...     def finish(self):
+    ...         self.participant.activity.workItemFinished(self)
+
+    >>> class Author(ApplicationBase):
+    ...     pass
+    
+    >>> zope.component.provideAdapter(Author, name=".author")
+
+    >>> class Review(ApplicationBase):
+    ...     def finish(self, publish):
+    ...         self.participant.activity.workItemFinished(self, publish)
+    
+    >>> zope.component.provideAdapter(Review, name=".review")
+
+    >>> class Publish(ApplicationBase):
+    ...     def start(self):
+    ...         print "Published"
+    ...         self.finish()
+    
+    >>> zope.component.provideAdapter(Publish, name=".publish")
+
+    >>> class Reject(ApplicationBase):
+    ...     def start(self):
+    ...         print "Rejected"
+    ...         self.finish()
+    
+    >>> zope.component.provideAdapter(Reject, name=".reject")
+
+Now, when we instantiate and start our workflow:
+  
+    >>> proc = pd()
+    >>> proc.start()
+    ... # doctest: +NORMALIZE_WHITESPACE
+    ProcessStarted(Process('sample'))
+    Transition(None, Activity('sample.author'))
+    ActivityStarted(Activity('sample.author'))
+
+We transition into the author activity and wait for work to get done.
+To move forward, we need to get at the authoring work item, so we can
+finish it.  Our work items add themselves to a work list, so we can
+get the item from the list.
+
+    >>> item = work_list.pop()
+
+Now we can finish the work item, by calling it's finish method:
+
+    >>> item.finish()
+    WorkItemFinished('author')
+    ActivityFinished(Activity('sample.author'))
+    Transition(Activity('sample.author'), Activity('sample.review'))
+    ActivityStarted(Activity('sample.review'))
+
+We see that we transitioned to the review activity.  Note that the
+`finish` method isn't a part of the workflow APIs.  It was defined by
+our sample classes. Other applications could use fifferent mechanisms.
+
+Now, we'll finish the review process by calling the review work item's
+`finish`. We'll pass `False`, indicating that the content should not
+be published:
+
+    >>> work_list.pop().finish(False)
+    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'))
+
+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
+some new concepts:
+
+- splits and joins
+
+- process arguments
+
+Consider the publication
+process shown below::
+
+
+  Author:      Tech          Tech          Editorial
+               Reviewer 1:   Reviewer 2:   Reviewer:
+  ===========  ===========   ===========   ==============
+                                                           ---------
+       ----------------------------------------------------| Start |
+      /                                                    ---------
+      |
+      V
+  -----------
+  | Prepare |<------------------------------\                
+  -----------                                \
+      |        ------------                   \
+      |        | Tech     |--------------- \   \   
+      |------->| Review 1 |                 V   |
+      |        ------------  ----------    -------------
+       \                     | Tech   |    | Editorial |   ----------
+         ------------------->| Review |--->| Review    |-->| Reject |
+                             | 2      |    -------------   ----------
+                             ----------      |      |
+  -----------                               /        \
+  | Prepare |                              /          \--------\
+  | Final   |<----------------------------/                    |
+  -----------                                                  |
+     ^   |                                 ----------          V
+     |    \------------------------------->| Review |      -----------
+      \                                    | Final  |----->| Publish |
+       ------------------------------------|        |      -----------
+                                           ----------
+
+Here we've arranged the process diagram into columns, with the
+activities for each participant. We have four participants, the
+author, twp technical reviewers, and an editorial reviewer.  The
+author prepares a draft.  The author sends the draft to *both*
+technical reviewers for review.  When the technical reviews have
+completed, the editorial review does an initial editorial
+review. Based on the technical reviews, the editor may choose to:
+
+- Reject the document
+
+- Publish the document as is
+
+- Request technical changes (based on the technical reviewers'
+  commonents), or
+
+- Request editorial changes.
+
+If technical changes are required, the work flows back to the
+"Prepare" activity.  If editorial changes are necessary, then work
+flows to the "Prepare Final" activity.  When the author has made the
+editorial changes, work flows to "Review Final".  The editor may
+request additional changes, in which case, work flows back to "Prepare
+Final", otherwise, the work flows to "Publish".
+
+This example illustrates different kinds of "joins" and "splits".  The
+term "join" refers to the way incoming transitions to an activity are
+handled. There are two kinds of joins: "and" and "xor".  With an "and"
+join, the activity waits for each of the incoming transitions.  In
+this example, the inputs to the "Editorial Review" activity form an
+"and" join.  Editorial review waits until each of the technical
+reviews are completed.  The rest of the joins in this example are
+"xor" joins.  The activity starts on any transition into the activity.
+
+The term "split" refers to way outgoing transitions from an activity
+are handled.  Normally, exactly one transition out of an activity is
+used. This is called an "xor" split.  With an "and" split, all
+transitions with boolean conditions that evaluate to `True` are used.
+In this example, the "Prepare" activity has an "and" split.  Work
+flows simultaneously to the two technical review activities.  The rest
+of the splits in this example are "xor" splits.
+
+Lets create our new workflow process:
+
+    >>> Publication = process.ProcessDefinition('Publication')
+    >>> zope.component.provideUtility(Publication, name=Publication.id)
+
+    >>> Publication.defineActivities(
+    ...     start   = process.ActivityDefinition("Start"),
+    ...     prepare = process.ActivityDefinition("Prepare"),
+    ...     tech1   = process.ActivityDefinition("Technical Review 1"),
+    ...     tech2   = process.ActivityDefinition("Technical Review 2"),
+    ...     review  = process.ActivityDefinition("Editorial Review"),
+    ...     final   = process.ActivityDefinition("Final Preparation"),
+    ...     rfinal  = process.ActivityDefinition("Review Final"),
+    ...     publish = process.ActivityDefinition("Publish"),
+    ...     reject  = process.ActivityDefinition("Reject"),
+    ...     )
+
+Here, we've passed strings to the activity definitions providing
+names. Names must be either unicode or ASCII strings.
+
+We define our transitions:
+
+    >>> Publication.defineTransitions(
+    ...     process.TransitionDefinition('start', 'prepare'),
+    ...     process.TransitionDefinition('prepare', 'tech1'),
+    ...     process.TransitionDefinition('prepare', 'tech2'),
+    ...     process.TransitionDefinition('tech1', 'review'),
+    ...     process.TransitionDefinition('tech2', 'review'),
+    ...
+    ...     process.TransitionDefinition(
+    ...         'review', 'reject', 
+    ...         condition=lambda data: not data.publish
+    ...         ),
+    ...     process.TransitionDefinition(
+    ...         'review', 'prepare', 
+    ...         condition=lambda data: data.tech_changes
+    ...         ),
+    ...     process.TransitionDefinition(
+    ...         'review', 'final', 
+    ...         condition=lambda data: data.ed_changes
+    ...         ),
+    ...     process.TransitionDefinition('review', 'publish'), 
+    ...
+    ...     process.TransitionDefinition('final', 'rfinal'),
+    ...     process.TransitionDefinition(
+    ...         'rfinal', 'final', 
+    ...         condition=lambda data: data.ed_changes
+    ...         ),
+    ...     process.TransitionDefinition('rfinal', 'publish'), 
+    ...     )
+
+We specify our "and" split and join:
+
+    >>> Publication.activities['prepare'].andSplit(True)
+    >>> Publication.activities['review'].andJoin(True)
+
+We define our participants and applications:
+
+    >>> Publication.defineParticipants(
+    ...     author   = process.Participant("Author"),
+    ...     tech1    = process.Participant("Technical Reviewer 1"),
+    ...     tech2    = process.Participant("Technical Reviewer 2"),
+    ...     reviewer = process.Participant("Editorial Reviewer"),
+    ...     )
+
+    >>> Publication.defineApplications(
+    ...     prepare = process.Application(),
+    ...     tech_review = process.Application(
+    ...         process.OutputParameter('publish'),
+    ...         process.OutputParameter('tech_changes'),
+    ...         ),
+    ...     ed_review = process.Application(
+    ...         process.InputParameter('publish1'),
+    ...         process.InputParameter('tech_changes1'),
+    ...         process.InputParameter('publish2'),
+    ...         process.InputParameter('tech_changes2'),
+    ...         process.OutputParameter('publish'),
+    ...         process.OutputParameter('tech_changes'),
+    ...         process.OutputParameter('ed_changes'),
+    ...         ),
+    ...     publish = process.Application(),
+    ...     reject = process.Application(),
+    ...     final = process.Application(),
+    ...     rfinal = process.Application(
+    ...         process.OutputParameter('ed_changes'),
+    ...         ),
+    ...     )
+
+    >>> Publication.activities['prepare'].definePerformer('author')
+    >>> Publication.activities['prepare'].addApplication('prepare')
+
+    >>> Publication.activities['tech1'].definePerformer('tech1')
+    >>> Publication.activities['tech1'].addApplication(
+    ...     'tech_review',
+    ...     process.OutputParameter('publish1'),
+    ...     process.OutputParameter('tech_changes1'),
+    ...     )
+
+    >>> Publication.activities['tech2'].definePerformer('tech2')
+    >>> Publication.activities['tech2'].addApplication(
+    ...     'tech_review',
+    ...     process.OutputParameter('publish2'),
+    ...     process.OutputParameter('tech_changes2'),
+    ...     )
+
+    >>> Publication.activities['review'].definePerformer('reviewer')
+    >>> Publication.activities['review'].addApplication(
+    ...     'ed_review',
+    ...     process.InputParameter('publish1'),
+    ...     process.InputParameter('tech_changes1'),
+    ...     process.InputParameter('publish2'),
+    ...     process.InputParameter('tech_changes2'),
+    ...     process.OutputParameter('publish'),
+    ...     process.OutputParameter('tech_changes'),
+    ...     process.OutputParameter('ed_changes'),
+    ...     )
+
+    >>> Publication.activities['final'].definePerformer('author')
+    >>> Publication.activities['final'].addApplication('final')
+
+    >>> Publication.activities['rfinal'].definePerformer('reviewer')
+    >>> Publication.activities['rfinal'].addApplication(
+    ...     'rfinal',
+    ...     process.OutputParameter('ed_changes'),
+    ...     )
+
+    >>> Publication.activities['publish'].addApplication('publish')
+    >>> Publication.activities['reject'].addApplication('reject')
+
+We want to be able to specify an author when we start the process.
+We'd also like to be told the final disposition of the process.  To
+accomplish this, we'll define parameters for our process:
+
+    >>> Publication.defineParameters(
+    ...     process.InputParameter('author'),
+    ...     process.OutputParameter('publish'),
+    ...     )
+
+Now that we've defined the process, we need to provide participant and
+application components.  Let's start with our participants.  Rather
+than sharing a single work list, we'll give each user their own
+work list.  We'll also create preexisting participants and return
+them. Finally, we'll create multiple authors and use the selected one:
+
+
+    >>> class User:
+    ...     def __init__(self):
+    ...         self.work_list = []
+
+    >>> authors = {'bob': User(), 'ted': User(), 'sally': User()}
+
+    >>> reviewer = User()
+    >>> tech1 = User()
+    >>> tech2 = User()
+
+    >>> class Author(Participant):
+    ...     def __init__(self, activity):
+    ...         Participant.__init__(self, activity)
+    ...         author_name = activity.process.workflowRelevantData.author
+    ...         self.user = authors[author_name]
+
+    >>> zope.component.provideAdapter(Author, name="Publication.author")
+
+When the process is created, the author name will be passed in and
+assigned to the workflow-relevent data.  Our author class uses this
+information to select the named user.
+
+    >>> class Reviewer(Participant):
+    ...     user = reviewer
+    >>> zope.component.provideAdapter(Reviewer, name="Publication.reviewer")
+
+    >>> class Tech1(Participant):
+    ...     user = tech1
+    >>> zope.component.provideAdapter(Tech1, name="Publication.tech1")
+
+    >>> class Tech2(Participant):
+    ...     user = tech2
+    >>> zope.component.provideAdapter(Tech2, name="Publication.tech2")
+
+Now we'll create our applications. Let's start with our author:
+
+    >>> class ApplicationBase(object):
+    ...     zope.component.adapts(interfaces.IParticipant)
+    ...     zope.interface.implements(interfaces.IWorkItem)
+    ...
+    ...     def __init__(self, participant):
+    ...         self.participant = participant
+    ...         self.activity = participant.activity
+    ...         participant.user.work_list.append(self)
+    ...
+    ...     def start(self):
+    ...         pass
+    ...
+    ...     def finish(self):
+    ...         self.participant.activity.workItemFinished(self)
+
+    >>> class Prepare(ApplicationBase):
+    ...
+    ...     def summary(self):
+    ...         process = self.activity.process
+    ...         doc = getattr(process.applicationRelevantData, 'doc', '')
+    ...         if doc:
+    ...             print 'Previous draft:'
+    ...             print doc
+    ...             print 'Changed we need to make:'
+    ...             for change in process.workflowRelevantData.tech_changes:
+    ...                 print change
+    ...         else:
+    ...             print 'Please write the initial draft'
+    ...
+    ...     def finish(self, doc):
+    ...         self.activity.process.applicationRelevantData.doc = doc
+    ...         super(Prepare, self).finish()
+
+    >>> zope.component.provideAdapter(Prepare, name="Publication.prepare")
+
+Since we used the prepare application for revisions as well as initial
+preparation, we provide a summary method to show us what we have to do.
+
+Here we get the document created by the author passed in as an
+argument to the finish method.  In a more realistic implementation,
+the author task would create the document at the start of the task and
+provide a user inyerface for the user to edit it.  We store the
+document as application-relevant data, since we'll want reviewers to 
+be able to access it, but we don't need it directly for workflow
+control.
+
+    >>> class TechReview(ApplicationBase):
+    ...
+    ...     def getDoc(self):
+    ...         return self.activity.process.applicationRelevantData.doc
+    ...
+    ...     def finish(self, decision, changes):
+    ...         self.activity.workItemFinished(self, decision, changes)
+
+    >>> zope.component.provideAdapter(TechReview, 
+    ...                               name="Publication.tech_review")
+
+Here, we provided a method to access the original document.
+
+    >>> class Review(TechReview):
+    ...
+    ...     def start(self, publish1, changes1, publish2, changes2):
+    ...         if not (publish1 and publish2):
+    ...             # Reject if either tech reviewer rejects
+    ...             self.activity.workItemFinished(
+    ...                 self, False, changes1 + changes2, ())
+    ...           
+    ...         if changes1 or changes2:
+    ...             # we won't do anyting if there are tech changes
+    ...             self.activity.workItemFinished(
+    ...                 self, True, changes1 + changes2, ())
+    ...
+    ...     def finish(self, ed_changes):
+    ...         self.activity.workItemFinished(self, True, (), ed_changes)
+
+    >>> zope.component.provideAdapter(Review, name="Publication.ed_review")
+
+In this implementation, we decided to reject outright if either
+technical editor recommended rejection and to send work back to
+preparation if there are any technical changes. We also subclassed
+`TechEdit` to get the `getDoc` method.
+
+We'll reuse the `publish` and `reject` application from the previous
+example.
+
+    >>> class Final(ApplicationBase):
+    ...
+    ...     def summary(self):
+    ...         process = self.activity.process
+    ...         doc = getattr(process.applicationRelevantData, 'doc', '')
+    ...         print 'Previous draft:'
+    ...         print self.activity.process.applicationRelevantData.doc
+    ...         print 'Changed we need to make:'
+    ...         for change in process.workflowRelevantData.ed_changes:
+    ...            print change
+    ...
+    ...     def finish(self, doc):
+    ...         self.activity.process.applicationRelevantData.doc = doc
+    ...         super(Final, self).finish()
+
+    >>> zope.component.provideAdapter(Final, name="Publication.final")
+
+In our this application, we simply update the document to reflect
+changes. 
+
+    >>> class ReviewFinal(TechReview):
+    ...
+    ...     def finish(self, ed_changes):
+    ...         self.activity.workItemFinished(self, ed_changes)
+
+    >>> zope.component.provideAdapter(ReviewFinal, name="Publication.rfinal")
+
+Our process now returns data.  When we create a process, we need to
+supply an object that it can call back to:
+
+    >>> class PublicationContext:
+    ...     zope.interface.implements(interfaces.IProcessContext)
+    ...
+    ...     def processFinished(self, process, decision):
+    ...         self.decision = decision
+
+Now, let's try out our process:
+
+    >>> context = PublicationContext()
+    >>> proc = Publication(context)
+    >>> proc.start('bob')
+    ProcessStarted(Process('Publication'))
+    Transition(None, Activity('Publication.start'))
+    ActivityStarted(Activity('Publication.start'))
+    ActivityFinished(Activity('Publication.start'))
+    Transition(Activity('Publication.start'), Activity('Publication.prepare'))
+    ActivityStarted(Activity('Publication.prepare'))
+
+We should have added an item to bob's work list. Let's get it and
+finish it, submitting a document:
+
+    >>> item = authors['bob'].work_list.pop()
+    >>> item.finish("I give my pledge, as an American\n"
+    ...             "to save, and faithfully to defend from waste\n"
+    ...             "the natural resources of my Country.")
+    WorkItemFinished('prepare')
+    ActivityFinished(Activity('Publication.prepare'))
+    Transition(Activity('Publication.prepare'), Activity('Publication.tech1'))
+    ActivityStarted(Activity('Publication.tech1'))
+    Transition(Activity('Publication.prepare'), Activity('Publication.tech2'))
+    ActivityStarted(Activity('Publication.tech2'))
+
+Notice that we transitioned to *two* activities, `tech1' and
+`tech2'.  This is because the prepare activity has an "and" split.
+Now we'll do a tech review.  Let's see what tech1 has:
+
+    >>> item = tech1.work_list.pop()
+    >>> print item.getDoc()   
+    I give my pledge, as an American
+    to save, and faithfully to defend from waste
+    the natural resources of my Country.
+
+Let's tell the author to change "dogs" to "men":
+
+    >>> item.finish(True, ['Change "American" to "human"'])
+    WorkItemFinished('tech_review')
+    ActivityFinished(Activity('Publication.tech1'))
+    Transition(Activity('Publication.tech1'), Activity('Publication.review'))
+
+Here we transitioned to the editorial review activity, but we didn't
+start it. This is because the editorial review activity has an "and"
+join, meaning that it won't start until both transitions have
+occurred.
+
+Now we'll do the other tectnical review:
+
+    >>> item = tech2.work_list.pop()
+    >>> item.finish(True, ['Change "Country" to "planet"'])
+    WorkItemFinished('tech_review')
+    ActivityFinished(Activity('Publication.tech2'))
+    Transition(Activity('Publication.tech2'), Activity('Publication.review'))
+    ActivityStarted(Activity('Publication.review'))
+    WorkItemFinished('ed_review')
+    ActivityFinished(Activity('Publication.review'))
+    Transition(Activity('Publication.review'), Activity('Publication.prepare'))
+    ActivityStarted(Activity('Publication.prepare'))
+
+Now when we transitioned to the editorial review activity, we started
+it, because each of the input transitions had happened.  Our editorial
+review application automatically sent the work back to preparation,
+because there were technical commments.  Let's address the comments:
+
+    >>> item = authors['bob'].work_list.pop()
+    >>> item.summary()
+    Previous draft:
+    I give my pledge, as an American
+    to save, and faithfully to defend from waste
+    the natural resources of my Country.
+    Changed we need to make:
+    Change "American" to "human"
+    Change "Country" to "planet"
+
+    >>> item.finish("I give my pledge, as an human\n"
+    ...             "to save, and faithfully to defend from waste\n"
+    ...             "the natural resources of my planet.")
+    WorkItemFinished('prepare')
+    ActivityFinished(Activity('Publication.prepare'))
+    Transition(Activity('Publication.prepare'), Activity('Publication.tech1'))
+    ActivityStarted(Activity('Publication.tech1'))
+    Transition(Activity('Publication.prepare'), Activity('Publication.tech2'))
+    ActivityStarted(Activity('Publication.tech2'))
+
+As before, after completing the initial edits, we start the technical
+review activities again.  We'll review it again. This time, we have no
+comments, because the author applied our requested changes:
+
+    >>> item = tech1.work_list.pop()
+    >>> item.finish(True, [])
+    WorkItemFinished('tech_review')
+    ActivityFinished(Activity('Publication.tech1'))
+    Transition(Activity('Publication.tech1'), Activity('Publication.review'))
+
+    >>> item = tech2.work_list.pop()
+    >>> item.finish(True, [])
+    WorkItemFinished('tech_review')
+    ActivityFinished(Activity('Publication.tech2'))
+    Transition(Activity('Publication.tech2'), Activity('Publication.review'))
+    ActivityStarted(Activity('Publication.review'))
+
+This time, we are left in the technical review activity because there
+weren't any technical changes. We're ready to do our editorial review.
+Whe'll request an editorial change:
+
+    >>> item = reviewer.work_list.pop()
+    >>> print item.getDoc()
+    I give my pledge, as an human
+    to save, and faithfully to defend from waste
+    the natural resources of my planet.
+
+    >>> item.finish(['change "an" to "a"'])
+    WorkItemFinished('ed_review')
+    ActivityFinished(Activity('Publication.review'))
+    Transition(Activity('Publication.review'), Activity('Publication.final'))
+    ActivityStarted(Activity('Publication.final'))
+
+Because we requested editorial changes, we transitioned to the final
+editing activity, so that the author can make the changes:
+
+    >>> item = authors['bob'].work_list.pop()
+    >>> item.summary()
+    Previous draft:
+    I give my pledge, as an human
+    to save, and faithfully to defend from waste
+    the natural resources of my planet.
+    Changed we need to make:
+    change "an" to "a"
+
+    >>> item.finish("I give my pledge, as a human\n"
+    ...             "to save, and faithfully to defend from waste\n"
+    ...             "the natural resources of my planet.")
+    WorkItemFinished('final')
+    ActivityFinished(Activity('Publication.final'))
+    Transition(Activity('Publication.final'), Activity('Publication.rfinal'))
+    ActivityStarted(Activity('Publication.rfinal'))
+
+We transition to the activity for reviewing the final edits.  We
+review the document and approve it for publication:
+
+    >>> item = reviewer.work_list.pop()
+    >>> print item.getDoc()
+    I give my pledge, as a human
+    to save, and faithfully to defend from waste
+    the natural resources of my planet.
+
+    >>> item.finish([])
+    WorkItemFinished('rfinal')
+    ActivityFinished(Activity('Publication.rfinal'))
+    Transition(Activity('Publication.rfinal'), Activity('Publication.publish'))
+    ActivityStarted(Activity('Publication.publish'))
+    Published
+    WorkItemFinished('publish')
+    ActivityFinished(Activity('Publication.publish'))
+    ProcessFinished(Process('Publication'))
+
+At this point, the rest of the process finished automatically.  In
+addition, the decision was recorded in the process context object:
+
+    >>> context.decision
+    True
+
+Comming Soon
+------------
+
+- XPDF support
+
+- Timeouts/exceptions
+
+- "otherwise" conditions
+
+
+.. [1] There are other kinds of conditions, namely "otherwise" and
+       "exception" conditions.


Property changes on: Zope3/trunk/src/zope/wfmc/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/zope/wfmc/__init__.py
===================================================================
--- Zope3/trunk/src/zope/wfmc/__init__.py	2004-12-22 18:12:54 UTC (rev 28688)
+++ Zope3/trunk/src/zope/wfmc/__init__.py	2004-12-22 20:47:04 UTC (rev 28689)
@@ -0,0 +1 @@
+#


Property changes on: Zope3/trunk/src/zope/wfmc/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/zope/wfmc/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/wfmc/interfaces.py	2004-12-22 18:12:54 UTC (rev 28688)
+++ Zope3/trunk/src/zope/wfmc/interfaces.py	2004-12-22 20:47:04 UTC (rev 28689)
@@ -0,0 +1,195 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Workflow-integration interfaces
+
+$Id$
+"""
+
+import zope.interface
+
+class IProcessDefinition(zope.interface.Interface):
+    """Process definition
+    """
+
+    id = zope.interface.Attribute("Process-definition identifier")
+
+    def defineActivities(**activities):
+        """Add activity definitions to the collection of defined activities
+
+        Activity definitions are supplied as keyword arguments.  The
+        keywords provide activity identifiers.  The values are
+        IActivityDefinition objects.
+        
+        """
+
+    def defineTransitions(*transitions):
+        """Add transition definitions
+
+        The transitions are ITransition objects.
+        """
+
+    def defineParticipants(**participants):
+        """Declare participants
+
+        The participants are provided as keyword arguments.
+        Participant identifiers are supplied as the keywords and the
+        definitions are supplied as values.  The definitions are
+        IParticipantDefinition objects.
+        """
+
+    def defineApplications(**applications):
+        """Declare applications
+
+        The applications are provided as keyword arguments.
+        Application identifiers are supplied as the keywords and the
+        definitions are supplied as values.  The definitions are
+        IApplicationDefinition objects.
+        """
+
+    def defineParameters(*parameters):
+        """Declate process parameters
+
+        Input parameters are set as workflow-relevent data.  Output
+        parameters are passed from workflow-relevant data to the
+        processFinished method of process-instances process contexts.
+        
+        """
+
+class IActivityDefinition(zope.interface.Interface):
+    """Activity definition
+    """
+
+    id = zope.interface.Attribute("Activity identifier")
+
+    def addApplication(id, *parameters):
+        """Declare that the activity uses the identified activity
+
+        The application identifier must match an application declared
+        for the process.  
+
+        Parameter definitions can be given as positional arguments.
+        The parameter definition directions must match those given in
+        the application definition.
+        """
+
+    def definePerformer(performer):
+        """Set the activity performer
+
+        The argument must be the identifier of a participant defined
+        for the enclosing process.
+        """
+
+    def setAndSplit(setting):
+        """Provide an and-split setting
+
+        If the setting is true, then the activity will use an "and" split.
+        """
+
+    def setAndJoin(setting):
+        """Provide an and-join setting
+
+        If the setting is true, then the activity will use an "and" join.
+        """
+
+class ITransitionDefinition(zope.interface.Interface):
+    """Activity definition
+    """
+
+class IProcess(zope.interface.Interface):
+    """Process instance
+    """
+
+    definition = zope.interface.Attribute("Process definition")
+
+    workflowRelevantData = zope.interface.Attribute(
+        """Workflow-relevant data
+
+        Object with attributes containing data used in conditions and
+        to pass data as parameters between applications
+        """
+        )
+
+    applicationRelevantData = zope.interface.Attribute(
+        """Application-relevant data
+
+        Object with attributes containing data used to pass data as
+        shared data for applications
+        
+        """
+        )
+
+class IProcessContext(zope.interface.Interface):
+    """Object that can recieve process results.
+    """
+
+    def processFinished(process, *results):
+        """Recieve notification of process completion, with results
+        """
+
+class IActivity(zope.interface.Interface):
+    """Activity instance
+    """
+
+    id = zope.interface.Attribute(
+        """Activity identifier
+
+        This identifier is set by the process instance
+
+        """)
+
+    definition = zope.interface.Attribute("Activity definition")
+
+    def workItemFinished(work_item, *results):
+        """Notify the activity that the work item has been completed.
+        """
+
+class IApplicationDefinition(zope.interface.Interface):
+    """Application definition
+    """
+
+    parameters = zope.interface.Attribute(
+        "A sequence of parameter definitions")
+
+class IParameterDefinition(zope.interface.Interface):
+    """Parameter definition
+    """
+
+    name = zope.interface.Attribute("Parameter name")
+
+    input = zope.interface.Attribute("Is this an input parameter?")
+
+    output = zope.interface.Attribute("Is this an output parameter?")
+
+class IParticipantDefinition(zope.interface.Interface):
+    """Participant definition
+    """
+
+class IParticipant(zope.interface.Interface):
+    """Workflow participant
+    """
+
+class IWorkItem(zope.interface.Interface):
+    """Work items
+    """
+
+    id = zope.interface.Attribute(
+        """Item identifier
+
+        This identifier is set by the activity instance
+
+        """)
+ 
+    def start(*arguments):
+        """Start the work
+        """


Property changes on: Zope3/trunk/src/zope/wfmc/interfaces.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/zope/wfmc/process.py
===================================================================
--- Zope3/trunk/src/zope/wfmc/process.py	2004-12-22 18:12:54 UTC (rev 28688)
+++ Zope3/trunk/src/zope/wfmc/process.py	2004-12-22 20:47:04 UTC (rev 28689)
@@ -0,0 +1,453 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Processes
+
+$Id$
+"""
+
+import sets
+
+import persistent
+import zope.cachedescriptors.property
+import zope.component
+import zope.event
+import zope.interface
+
+from zope.wfmc import interfaces
+
+class InvalidProcessDefinition(Exception):
+    """A process definition isn't valid in some way.
+    """
+
+class ProcessError(Exception):
+    """An error occured in execution of a process
+    """
+
+class ProcessDefinition:
+
+    zope.interface.implements(interfaces.IProcessDefinition)
+
+    def __init__(self, id):
+        self.id = id
+        self.activities = {}
+        self.transitions = []
+        self.applications = {}
+        self.participants = {}
+        self.parameters = ()
+
+    def defineActivities(self, **activities):
+        self._dirty()
+        for id, activity in activities.items():
+            activity.id = id
+            if activity.__name__ is None:
+                activity.__name__ = self.id + '.' + id
+            activity.process = self
+            self.activities[id] = activity
+
+    def defineTransitions(self, *transitions):
+        self._dirty()
+        self.transitions.extend(transitions)
+
+    def defineApplications(self, **applications):
+        for id, application in applications.items():
+            application.id = id
+            self.applications[id] = application
+
+    def defineParticipants(self, **participants):
+        for id, participant in participants.items():
+            participant.id = id
+            self.participants[id] = participant
+
+    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 = ()
+        for aid, activity in activities.items():
+            if not activity.incoming:
+                start += ((aid, activity), )
+                if not activity.outgoing:
+                    raise InvalidProcessDefinition(
+                        "Activity %s has no transitions",
+                        aid)
+
+        if len(start) != 1:
+            if start:
+                raise InvalidProcessDefinition("Multiple start activities",
+                                               [id for (id, a) in start])
+            else:
+                raise InvalidProcessDefinition("No start activities")
+                
+        return TransitionDefinition(None, start[0][0])
+        
+    _start = zope.cachedescriptors.property.Lazy(_start)
+
+    def __call__(self, context=None):
+        return Process(self, self._start, context)
+
+    def _dirty(self):
+        try:
+            del self._start
+        except AttributeError:
+            pass
+
+class ActivityDefinition:
+
+    zope.interface.implements(interfaces.IActivityDefinition)
+
+    performer = ''
+    process = None
+
+    def __init__(self, __name__=None):
+        self.__name__ = __name__
+        self.incoming = self.outgoing = ()
+        self.applications = ()
+        self.andJoinSetting = self.andSplitSetting = False
+
+    def andSplit(self, setting):
+        self.andSplitSetting = setting
+
+    def andJoin(self, setting):
+        self.andJoinSetting = setting
+
+    def addApplication(self, application, *parameters):
+        formal = self.process.applications[application].parameters
+        if len(formal) != len(parameters):
+            raise TypeError("Wrong number of parameters")
+
+        for formal, parameter in zip(formal, parameters):
+            if (formal.input != parameter.input
+                or formal.output != parameter.output
+                ):
+                raise TypeError("Parameter type missmatch")
+        
+        self.applications += ((application, parameters), )
+
+    def definePerformer(self, performer):
+        self.performer = performer
+        
+class TransitionDefinition:
+
+    zope.interface.implements(interfaces.ITransitionDefinition)
+
+    def __init__(self, from_, to, condition=lambda data: True):
+        self.from_ = from_
+        self.to = to
+        self.condition = condition
+
+        
+class Process(persistent.Persistent):
+
+    zope.interface.implements(interfaces.IProcess)
+
+    def __init__(self, definition, start, context=None):
+        self.process_definition_identifier = definition.id
+        self.startTransition = start
+        self.context = context
+        self.activities = {}
+        self.nextActivityId = 0
+        self.workflowRelevantData = WorkflowData()
+        self.applicationRelevantData = WorkflowData()
+
+    def definition(self):
+        return zope.component.getUtility(
+            interfaces.IProcessDefinition,
+            self.process_definition_identifier,
+            )
+    definition = property(definition)
+
+    def start(self, *arguments):
+        if self.activities:
+            raise TypeError("Already started")
+
+        definition = self.definition
+        data = self.workflowRelevantData
+        args = arguments
+        for parameter in definition.parameters:
+            if parameter.input:
+                arg, args = arguments[0], args[1:]
+                setattr(data, parameter.__name__, arg)
+        if args:
+            raise TypeError("Too many arguments. Expected %s. got %s",
+                            len(process_definition.parameter), len(arguments))
+
+        zope.event.notify(ProcessStarted(self))
+        self.transition(None, (self.startTransition, ))
+
+    def _finish(self):
+        if self.context is not None:
+            args = []
+            for parameter in self.definition.parameters:
+                if parameter.output:
+                    args.append(
+                        getattr(self.workflowRelevantData,
+                                parameter.__name__))
+            self.context.processFinished(self, *args)
+            
+        zope.event.notify(ProcessFinished(self))
+        
+        
+    def transition(self, activity, transitions):
+        if transitions:
+            definition = self.definition
+            
+            for transition in transitions:
+                activity_definition = definition.activities[transition.to]
+                next = None
+                if activity_definition.andJoinSetting:
+                    # If it's an and-join, we want only one.
+                    for i, a in self.activities.items():
+                        if a.activity_definition_identifier == transition.to:
+                            # we already have the activity -- use it
+                            next = a
+                            break
+
+                if next is None:
+                    next = Activity(self, activity_definition)
+                    self.nextActivityId += 1
+                    next.id = self.nextActivityId
+
+                zope.event.notify(Transition(activity, next))
+                self.activities[next.id] = next
+                next.start(transition)
+
+        if activity is not None:
+            del self.activities[activity.id]
+            if not self.activities:
+                self._finish()
+
+        self._p_changed = True
+
+    def __repr__(self):
+        return "Process(%r)" % self.process_definition_identifier
+
+class WorkflowData(persistent.Persistent):
+    """Container for workflow-relevant and application-relevant data
+    """
+
+class ProcessStarted:
+
+    def __init__(self, process):
+        self.process = process
+
+    def __repr__(self):
+        return "ProcessStarted(%r)" % self.process
+
+class ProcessFinished:
+
+    def __init__(self, process):
+        self.process = process
+
+    def __repr__(self):
+        return "ProcessFinished(%r)" % self.process
+        
+
+class Activity(persistent.Persistent):
+
+    zope.interface.implements(interfaces.IActivity)
+
+    def __init__(self, process, definition):
+        self.process = process
+        self.activity_definition_identifier = definition.id
+
+        workitems = {}
+        if definition.applications:
+
+            performer = zope.component.queryAdapter(
+                self, interfaces.IParticipant,
+                process.process_definition_identifier
+                + '.' + definition.performer)
+
+            if performer is None:
+                performer = zope.component.getAdapter(
+                    self, interfaces.IParticipant,
+                    '.' + definition.performer)
+
+            i = 0
+            for application, parameters in definition.applications:
+                workitem = zope.component.queryAdapter(
+                    performer, interfaces.IWorkItem,
+                    process.process_definition_identifier + '.' + application)
+                if workitem is None:
+                    workitem = zope.component.getAdapter(
+                        performer, interfaces.IWorkItem,
+                        '.' + application)
+                i += 1
+                workitem.id = i
+                workitems[i] = workitem, application, parameters
+        
+        self.workitems = workitems
+
+    def definition(self):
+        return self.process.definition.activities[
+            self.activity_definition_identifier]
+    definition = property(definition)
+
+    incoming = ()
+    def start(self, transition):
+        # Start the activity, if we've had enough incoming transitions
+
+        definition = self.definition
+
+        if definition.andJoinSetting:
+            if transition in self.incoming:
+                raise ProcessError(
+                    "Repeated incoming transition while waiting for and "
+                    "completion")
+            self.incoming += (transition, )
+
+            if len(self.incoming) < len(definition.incoming):
+                return # not enough incoming yet
+
+        zope.event.notify(ActivityStarted(self))
+        
+        if self.workitems:
+            for workitem, application, parameters in self.workitems.values():
+                args = []
+                for parameter in parameters:
+                    if parameter.input:
+                        args.append(
+                            getattr(self.process.workflowRelevantData,
+                                    parameter.__name__))
+                workitem.start(*args)
+        else:
+            # Since we don't have any work items, we're done
+            self.finish()
+
+    def workItemFinished(self, work_item, *results):
+        unused, application, parameters = self.workitems.pop(work_item.id)
+        self._p_changed = True
+        res = results
+        for parameter in parameters:
+            if parameter.output:
+                v = res[0]
+                res = res[1:]
+                setattr(self.process.workflowRelevantData,
+                        parameter.__name__, v)
+
+        if res:
+            raise TypeError("Too many results")
+
+        zope.event.notify(WorkItemFinished(
+            work_item, application, parameters, results))
+        
+        if not self.workitems:
+            self.finish()
+
+    def finish(self):
+        zope.event.notify(ActivityFinished(self))
+
+        definition = self.definition
+
+        transitions = []
+        for transition in definition.outgoing:
+            if transition.condition(self.process.workflowRelevantData):
+                transitions.append(transition)
+                if not definition.andSplitSetting:
+                    break # xor split, want first one
+
+        self.process.transition(self, transitions)
+
+    def __repr__(self):
+        return "Activity(%r)" % (
+            self.process.process_definition_identifier + '.' +
+            self.activity_definition_identifier
+            )
+
+class WorkItemFinished:
+
+    def __init__(self, workitem, application, parameters, results):
+        self.workitem =  workitem
+        self.application = application
+        self.parameters = parameters
+        self.results = results
+
+    def __repr__(self):
+        return "WorkItemFinished(%r)" % self.application
+
+class Transition:
+
+    def __init__(self, from_, to):
+        self.from_ = from_
+        self.to = to
+
+    def __repr__(self):
+        return "Transition(%r, %r)" % (self.from_, self.to)
+
+class ActivityFinished:
+
+    def __init__(self, activity):
+        self.activity = activity
+
+    def __repr__(self):
+        return "ActivityFinished(%r)" % self.activity
+
+class ActivityStarted:
+
+    def __init__(self, activity):
+        self.activity = activity
+
+    def __repr__(self):
+        return "ActivityStarted(%r)" % self.activity
+
+class Parameter:
+
+    zope.interface.implements(interfaces.IParameterDefinition)
+
+    input = output = False
+
+    def __init__(self, name):
+        self.__name__ = name
+
+class OutputParameter(Parameter):
+
+    output = True
+
+class InputParameter(Parameter):
+
+    input = True
+
+class InputOutputParameter(InputParameter, OutputParameter):
+
+    pass
+
+class Application:
+
+    zope.interface.implements(interfaces.IApplicationDefinition)
+    
+    def __init__(self, *parameters):
+        self.parameters = parameters
+
+class Participant:
+
+    zope.interface.implements(interfaces.IParticipantDefinition)
+
+    def __init__(self, name=None):
+        self.__name__ = name


Property changes on: Zope3/trunk/src/zope/wfmc/process.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/zope/wfmc/tests.py
===================================================================
--- Zope3/trunk/src/zope/wfmc/tests.py	2004-12-22 18:12:54 UTC (rev 28688)
+++ Zope3/trunk/src/zope/wfmc/tests.py	2004-12-22 20:47:04 UTC (rev 28689)
@@ -0,0 +1,36 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Test hookup
+
+$Id$
+"""
+import unittest
+import zope.event
+from zope.component.tests import placelesssetup
+
+def tearDown(test):
+    placelesssetup.tearDown(test)
+    zope.event.subscribers.pop()
+
+def test_suite():
+    from zope.testing import doctest
+    suite = unittest.TestSuite()
+    # suite.addTest(doctest.DocTestSuite())
+    suite.addTest(doctest.DocFileSuite('README.txt', tearDown=tearDown,
+                                       setUp=placelesssetup.setUp))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+


Property changes on: Zope3/trunk/src/zope/wfmc/tests.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native



More information about the Zope3-Checkins mailing list