diff --git a/process/README.txt b/process/README.txt index 7b13f2b..e60d2f6 100644 --- a/process/README.txt +++ b/process/README.txt @@ -5,50 +5,79 @@ Business Process Management We start with the definition of a simple process: - startNode --> n01 --> endNode + startActivity --> n01 --> endActivity - >>> from cybertools.process.definition import ProcessDefinition, Node, Transition - >>> process = ProcessDefinition() - >>> n01 = Node() - >>> process.startNode.addTransition(n01) - >>> n01.addTransition(process.endNode) + >>> from cybertools.process.definition import Process, Activity + >>> process = Process() + >>> n01 = Activity() + >>> process.startActivity.add(n01) + >>> n02 = Activity() + >>> n01.add(n02) Now let's execute the process: - >>> from cybertools.process.execution import ProcessInstance - >>> instance = ProcessInstance(process) - >>> execution = instance.execute() + >>> execution = process.execute() As there aren't any interactions with the outside world in our process we don't see anything. But we can check if the process instance has reached the -process' end node: +process' end activity: - >>> execution.currentNode is process.endNode + >>> execution.currentActivity is n02 True -So let's now associate an action handler with the process' nodes: +So let's now associate an action handler with the process' activitys: >>> from zope.component import provideAdapter, adapts >>> from zope.interface import implements - >>> from cybertools.process.interfaces import INode, IActionHandler + >>> from cybertools.process.interfaces import IActivity, IActionHandler >>> class DummyHandler(object): ... implements(IActionHandler) - ... adapts(INode) + ... adapts(IActivity) ... def __init__(self, context): pass ... def handle(self, execution): ... print 'working.' >>> provideAdapter(DummyHandler) - >>> execution = instance.execute() + >>> execution = process.execute() working. - >>> execution.currentNode is process.startNode + >>> execution.currentActivity is process.startActivity True >>> execution.trigger() working. - >>> execution.currentNode is n01 + >>> execution.currentActivity is n01 True >>> execution.trigger() working. - >>> execution.currentNode is process.endNode + >>> execution.currentActivity is n02 True + +Next we'll use a predefined action handler that creates a work item. As this +makes only sense if the action handler can give the outside world access +to the work item somehow, we have to subclass this generic, abstract class: + + >>> workItems = [] + >>> from cybertools.process.execution import WorkActionHandler + >>> class MyActionHandler(WorkActionHandler): + ... def handle(self, execution): + ... super(MyActionHandler, self).handle(execution) + ... workItems.append(self.workItem) + >>> provideAdapter(MyActionHandler) + + >>> execution = process.execute() + +Now the process is waiting for somebody to pick up the work item and +submit it: + + >>> execution.currentActivity is process.startActivity + True + >>> workItem = workItems[0] + >>> workItem.done + False + >>> workItem.submit() + >>> execution.currentActivity is n01 + True + >>> workItem.done + False + + diff --git a/process/definition.py b/process/definition.py index a4e5030..4953fc0 100644 --- a/process/definition.py +++ b/process/definition.py @@ -24,93 +24,52 @@ $Id$ from zope import component from zope.interface import implements -from cybertools.process.interfaces import INode, ITransition, IProcessDefinition +from cybertools.process.interfaces import IActivity, IProcess from cybertools.process.interfaces import IActionHandler +from cybertools.process.execution import Execution -class Node(object): +class Activity(object): - implements(INode) + implements(IActivity) - def __init__(self, process=None, name=u'', title=u'', handlerName=''): - self._process = process - self._incoming = set() - self._outgoing = set() + def __init__(self, name=u'', title=u'', handlerName=''): + self._successors = set() self._handlerName = handlerName self.__name__ = name self.title = title - def getProcess(self): return self._process - def setProcess(self, prc): self._process = prc - process = property(getProcess, setProcess) - @property - def outgoing(self): return self._outgoing - - @property - def incoming(self): return self._incoming + def successors(self): return self._successors def getHandlerName(self): return self._handlerName def setHandlerName(self, name): self._handlerName = name handlerName = property(getHandlerName, setHandlerName) - def addTransition(self, destination): - transition = Transition(destination) - self.outgoing.add(transition) - transition.source = self - if transition.destination.process is None: - transition.destination.process = self.process + def add(self, activity): + self.successors.add(activity) - def execute(self, execution): - execution.currentNode = self + def execute(self, execution=None): + if execution is None: + execution = Execution() + execution.currentActivity = self handler = component.queryAdapter(self, IActionHandler, name=self.handlerName) if handler is not None: handler.handle(execution) # creates work item; work item triggers execution else: execution.trigger() + return execution -class Transition(object): +class Process(object): - implements(ITransition) - - def __init__(self, destination, qualifier=u''): - self._source = None - self._destination = destination - self._qualifier = qualifier - - def getSource(self): return self._source - def setSource(self, node): self._source = node - source = property(getSource, setSource) - - def getDestination(self): return self._destination - def setDestination(self, node): self._destination = node - destination = property(getDestination, setDestination) - - def getQualifier(self): return self._qualifier - def setQualifier(self, node): self._qualifier = node - qualifier = property(getQualifier, setQualifier) - - def take(self, execution): - self.destination.execute(execution) - - -class ProcessDefinition(object): - - implements(IProcessDefinition) + implements(IProcess) def __init__(self): - self._instances = set() - self._startNode = Node(self) - self._endNode = Node(self) + self._startActivity = Activity() @property - def instances(self): return self._instances + def startActivity(self): return self._startActivity - def getStartNode(self): return self._startNode - def setStartNode(self, node): self._startNode = node - startNode = property(getStartNode, setStartNode) - - def getEndNode(self): return self._endNode - def setEndNode(self, node): self._endNode = node - endNode = property(getEndNode, setEndNode) + def execute(self): + return self.startActivity.execute() diff --git a/process/execution.py b/process/execution.py index ed610b0..7a19c74 100644 --- a/process/execution.py +++ b/process/execution.py @@ -23,52 +23,81 @@ $Id$ """ from zope.interface import implements -from cybertools.process.interfaces import IExecution, IProcessInstance +from zope.component import adapts +from cybertools.process.interfaces import IActivity, IExecution +from cybertools.process.interfaces import IWorkItem, IActionHandler class Execution(object): implements(IExecution) - def __init__(self, instance): - self._instance = instance - self._currentNode = None + def __init__(self, parent=None): + self._currentActivity = None self._workItem = None + self._parent = parent + self._children = set() - @property - def instance(self): return self._instance - - def getCurrentNode(self): return self._currentNode - def setCurrentNode(self, node): self._currentNode = node - currentNode = property(getCurrentNode, setCurrentNode) + def getCurrentActivity(self): return self._currentActivity + def setCurrentActivity(self, activity): self._currentActivity = activity + currentActivity = property(getCurrentActivity, setCurrentActivity) def getWorkItem(self): return self._workItem def setWorkItem(self, item): self._workItem = item workItem = property(getWorkItem, setWorkItem) - def trigger(self, transitionPattern=None): - outgoing = self.currentNode.outgoing - if outgoing: - transition = iter(outgoing).next() - transition.take(self) - - -class ProcessInstance(object): - - implements(IProcessInstance) - - def __init__(self, process): - self._process = process - self._executions = set() + @property + def parent(self): return self._parent @property - def process(self): return self._process + def children(self): return self._children + + def trigger(self, qualifiers=set()): + successors = [s for s in self.currentActivity.successors + if not qualifiers + or qualifiers.union(successor.qualifiers)] + for successor in successors: + if len(successors) == 1: + execution = self + else: + execution = Execution(self) + self.children.add(execution) + successor.execute(execution) + + +class WorkItem(object): + + implements(IWorkItem) + + def __init__(self, execution): + self._execution = execution + self._activity = execution.currentActivity @property - def executions(self): return self._executions + def execution(self): return self._execution + + @property + def activity(self): return self._activity + + _done = False + @property + def done(self): return self._done + + def submit(self, data={}): + _done = True + self.execution.trigger() + + +class WorkActionHandler(object): + """ A simple action handler that creates a work item. + """ + + implements(IActionHandler) + adapts(IActivity) + + def __init__(self, context): + self.context = context + + def handle(self, execution): + self.workItem = WorkItem(execution) - def execute(self): - execution = Execution(self) - self.executions.add(execution) - self.process.startNode.execute(execution) - return execution diff --git a/process/interfaces.py b/process/interfaces.py index 5bb5df0..3c86881 100644 --- a/process/interfaces.py +++ b/process/interfaces.py @@ -31,64 +31,53 @@ _ = MessageFactory('zope') # process/workflow definitions -class INode(Interface): +class IActivity(Interface): """ A step of a process - typically a state that lets the process wait for a user interaction or an action that is executed automatically. """ - process = Attribute('The process this node belongs to') - incoming = Attribute('Transitions that lead to this node') - outgoing = Attribute('Transitions that lead to the next nodes') + successors = Attribute('A set of activities following this activity') handlerName = Attribute('Name of an adapter that may handle the ' - 'execution of this node') + 'execution of this activity') + qualifier = Attribute('A collection of strings giving additional ' + 'information about an activity. ' + 'May be used by an execution context ' + 'for deciding which activity it will execute next') - def addTransition(destination): - """ Append a transition to the destination node given - to the collection of outgoing transitions. + def add(successor): + """ Append an activity to the collection of following activities. """ - def execute(execution): - """ Execute a node in an execution context of a process instance; - if this node signifies a wait state this will create a work item. + def execute(execution=None): + """ Execute a activity in an execution context of a process instance; + if this activity signifies a wait state this will create a work item. + If the execution argument is None a new execution context will + be created. The execution context is returned. """ -class ITransition(Interface): - """ A transition leading from one node (activity, state, action) to - the next. - """ - source = Attribute('The node that triggered this transition') - destination = Attribute('The destination node of this transition') - qualifier = Attribute('A string giving a hint for the meaning of the ' - 'transition. May be used by an execution context ' - 'for deciding which transition it will transfer ' - 'control to') - - def take(execution): - """ Pass over the execution context from the source node to the - destination node. - """ - - -class IProcessDefinition(Interface): +class IProcess(Interface): """ The definition of a process or workflow. """ - instances = Attribute('A collection of process instances created from ' - 'this process definition') - startNode = Attribute('The start node of this process') - endNode = Attribute('The end node of this process') + startActivity = Attribute('The start activity of this process') + + def execute(): + """ Start the process (typically by executing its start activity). + Return the execution context. + """ class IActionHandler(Interface): """ Will be called for handling process executions. Is typically - implemented as an adapter for INode. + implemented as an adapter for IActivity. """ def handle(execution): - """ Handles the execution of a node in the execution context given. + """ Handles the execution of a activity in the execution context given. """ + # process execution @@ -97,41 +86,36 @@ class IExecution(Interface): current states) of a process instance. """ - instance = Attribute('The process instance this execution context belongs to') - currentNode = Attribute('The node the process instance is currently in') + currentActivity = Attribute('The activity this execution is currently in') workItem = Attribute('The work item the process instance is currently ' - 'waiting for; None if the current node is not in a ' + 'waiting for; None if the current activity is not in a ' 'waiting state') + parent = Attribute('The execution context that has created this one ' + 'e.g. because of a forking operation') + children = Attribute('A collection of execution contexts that have been ' + 'created by this one') - def trigger(transitionQualifiers=None): + def trigger(qualifiers=None): """ A callback (handler) that will may be called by an action handler. This will typically lead to moving on the execution context - to an outgoing transition of the current node. The execution - context will use the transitionQualifiers to decide which outgoing - transition(s) to transfer control to. - """ - - -class IProcessInstance(Interface): - """ An executing process, i.e. an execution context that keeps track of - the currently active node(s) of the process. - """ - - process = Attribute('The process definition this instance is created from') - executions = Attribute('A collection of currently active execution contexts') - - def execute(): - """ Start the execution of the process with its start node; - return the execution context. + to a successor activity of the current activity. The execution + context will use the qualifiers argument to decide which + activities to transfer control to. """ class IWorkItem(Interface): - """ An instance of an activity from a process definition. + """ A work item tells some external entity - typically a user - to + do something in order to let the process proceed. """ - node = Attribute('The node this work item has been created from') - execution = Attribute('The execution context (and thus the process ' - 'instance) that has create this work item') - + activity = Attribute('The activity this work item has been created from') + execution = Attribute('The execution context that has created this work item') + done = Attribute('A Boolean attribute that is true if the work ' + 'item has been submitted') + def submit(data={}): + """ Provide the work item with some data (optional) and have it + continue the process by triggering the execution context. + This should also set the `done` attribute to True. + """ diff --git a/process/tests.py b/process/tests.py index 63fd6d9..3bd5ea8 100755 --- a/process/tests.py +++ b/process/tests.py @@ -8,13 +8,13 @@ $Id$ import unittest, doctest from zope.testing.doctestunit import DocFileSuite -from cybertools.process.definition import ProcessDefinition +from cybertools.process.definition import Process class TestProcess(unittest.TestCase): "Basic tests for the process package." def testBasicStuff(self): - p = ProcessDefinition() + p = Process() def test_suite(): diff --git a/process/work.py b/process/work.py deleted file mode 100644 index 5e380e1..0000000 --- a/process/work.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Copyright (c) 2006 Helmut Merz helmutm@cy55.de -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -""" -Work items (tasks) and similar stuff. - -$Id$ -""" - -from zope.interface import implements -from cybertools.process.interfaces import IWorkItem - - -class WorkItem(object): - - implements(IWorkItem) -