[Checkins] SVN: hurry.workflow/ Initial import (moved from codespeak).

Martijn Faassen faassen at startifact.com
Wed Jun 17 08:40:16 EDT 2009


Log message for revision 101107:
  Initial import (moved from codespeak).
  

Changed:
  A   hurry.workflow/
  A   hurry.workflow/trunk/
  A   hurry.workflow/trunk/CHANGES.txt
  A   hurry.workflow/trunk/CREDITS.txt
  A   hurry.workflow/trunk/INSTALL.txt
  A   hurry.workflow/trunk/README.txt
  A   hurry.workflow/trunk/buildout.cfg
  A   hurry.workflow/trunk/setup.py
  A   hurry.workflow/trunk/src/
  A   hurry.workflow/trunk/src/hurry/
  A   hurry.workflow/trunk/src/hurry/__init__.py
  A   hurry.workflow/trunk/src/hurry/workflow/
  A   hurry.workflow/trunk/src/hurry/workflow/__init__.py
  A   hurry.workflow/trunk/src/hurry/workflow/configure.zcml
  A   hurry.workflow/trunk/src/hurry/workflow/interfaces.py
  A   hurry.workflow/trunk/src/hurry/workflow/tests.py
  A   hurry.workflow/trunk/src/hurry/workflow/workflow.py
  A   hurry.workflow/trunk/src/hurry/workflow/workflow.txt

-=-
Added: hurry.workflow/trunk/CHANGES.txt
===================================================================
--- hurry.workflow/trunk/CHANGES.txt	                        (rev 0)
+++ hurry.workflow/trunk/CHANGES.txt	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,54 @@
+hurry.workflow changes
+**********************
+
+0.10 (unreleased)
+=================
+
+* ...
+
+0.9.2.1 (2007-08-15)
+====================
+
+Bug fixes
+---------
+
+* Oops, the patches in 0.9.2 were not actually applied. Fixed them
+  now.
+
+0.9.2 (2007-08-15)
+==================
+
+Bug fixes
+---------
+
+* zope.security changes broke imports in hurry.workflow. 
+
+* localUtility directive is now deprecated, so don't use it anymore.
+
+0.9.1 (2006-09-22)
+==================
+
+Feature changes
+---------------
+
+* first cheesehop release.
+
+0.9 (2006-06-15)
+================
+
+Feature changes
+---------------
+
+* separate out from hurry package into hurry.workflow
+
+* eggification work
+
+* Zope 3.3 compatibility work
+
+0.8 (2006-05-01)
+================
+
+Feature changes
+---------------
+
+Initial public release.

Added: hurry.workflow/trunk/CREDITS.txt
===================================================================
--- hurry.workflow/trunk/CREDITS.txt	                        (rev 0)
+++ hurry.workflow/trunk/CREDITS.txt	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,11 @@
+Credits
+-------
+
+* Martijn Faassen - initial and main developer
+
+* Jan-Wijbrand Kolman - suggestions and feedback
+
+* Tobias Rodäbel - compatibility fixes
+
+The hurry.workflow library for the Zope Toolkit was originally
+developed at Infrae (http://www.infrae.com).

Added: hurry.workflow/trunk/INSTALL.txt
===================================================================
--- hurry.workflow/trunk/INSTALL.txt	                        (rev 0)
+++ hurry.workflow/trunk/INSTALL.txt	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,9 @@
+Installation
+------------
+
+
+hurry.workflow needs Zope 3.3.
+
+Make sure hurry.workflow's src directory is on the Python path
+somehow, and then copy hurry.workflow-configure.zcml into your Zope's
+`etc/package-includes` directory.

Added: hurry.workflow/trunk/README.txt
===================================================================
--- hurry.workflow/trunk/README.txt	                        (rev 0)
+++ hurry.workflow/trunk/README.txt	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,5 @@
+**************
+hurry.workflow
+**************
+
+A simple but quite nifty workflow system for Zope 3.

Added: hurry.workflow/trunk/buildout.cfg
===================================================================
--- hurry.workflow/trunk/buildout.cfg	                        (rev 0)
+++ hurry.workflow/trunk/buildout.cfg	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,9 @@
+[buildout]
+develop = . 
+parts = test
+newest = false
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = hurry.workflow [test]
+defaults = ['--tests-pattern', '^f?tests$', '-v']

Added: hurry.workflow/trunk/setup.py
===================================================================
--- hurry.workflow/trunk/setup.py	                        (rev 0)
+++ hurry.workflow/trunk/setup.py	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,59 @@
+import os
+from setuptools import setup, find_packages
+
+def read(*rnames):
+    return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+long_description = (
+    read('README.txt')
+    + '\n' +
+    read('CHANGES.txt')
+    + '\n' +
+    'Detailed Documentation\n'
+    '**********************\n'
+    + '\n' +
+    read('src', 'hurry', 'workflow', 'workflow.txt')
+    + '\n' +
+    'Download\n'
+    '********\n'
+    )
+
+setup(
+    name="hurry.workflow",
+    version="0.10dev",
+    packages=find_packages('src'),
+    
+    package_dir= {'':'src'},
+    
+    namespace_packages=['hurry'],
+    package_data = {
+    '': ['*.txt', '*.zcml'],
+    },
+
+    zip_safe=False,
+    author='Martijn Faassen (at Infrae)',
+    author_email='faassen at startifact.com',
+    description="""\
+hurry.workflow is a simple workflow system. It can be used to
+implement stateful multi-version workflows for Zope Toolkit applications.
+""",
+    long_description=long_description,
+    license='ZPL 2.1',
+    keywords="zope zope3",
+    classifiers = ['Framework :: Zope3'],
+    extras_require = dict(
+        test = ['zope.testing'],
+        ),
+    install_requires=[
+        'setuptools',
+        'zope.interface',
+        'zope.component',
+        # this is an indirect dependency through zope.annotation, but
+        # this at the time of working didn't yet declare this
+        # dependency itself
+        'ZODB3',
+        'zope.event',
+        'zope.security',
+        'zope.annotation',
+        'zope.lifecycleevent'],
+    )

Added: hurry.workflow/trunk/src/hurry/__init__.py
===================================================================
--- hurry.workflow/trunk/src/hurry/__init__.py	                        (rev 0)
+++ hurry.workflow/trunk/src/hurry/__init__.py	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,7 @@
+# this is a namespace package
+try:
+    import pkg_resources
+    pkg_resources.declare_namespace(__name__)
+except ImportError:
+    import pkgutil
+    __path__ = pkgutil.extend_path(__path__, __name__)

Added: hurry.workflow/trunk/src/hurry/workflow/__init__.py
===================================================================
--- hurry.workflow/trunk/src/hurry/workflow/__init__.py	                        (rev 0)
+++ hurry.workflow/trunk/src/hurry/workflow/__init__.py	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1 @@
+# this is a package

Added: hurry.workflow/trunk/src/hurry/workflow/configure.zcml
===================================================================
--- hurry.workflow/trunk/src/hurry/workflow/configure.zcml	                        (rev 0)
+++ hurry.workflow/trunk/src/hurry/workflow/configure.zcml	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,12 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    >
+
+  <class class=".workflow.Workflow">
+    <require
+        interface=".interfaces.IWorkflow"
+        permission="zope.Public"
+        />
+  </class>
+
+</configure>

Added: hurry.workflow/trunk/src/hurry/workflow/interfaces.py
===================================================================
--- hurry.workflow/trunk/src/hurry/workflow/interfaces.py	                        (rev 0)
+++ hurry.workflow/trunk/src/hurry/workflow/interfaces.py	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,196 @@
+from zope.interface import Interface, Attribute
+from zope.component.interfaces import IObjectEvent
+
+MANUAL = 0
+AUTOMATIC = 1
+SYSTEM = 2
+
+class InvalidTransitionError(Exception):
+    pass
+
+class NoTransitionAvailableError(InvalidTransitionError):
+    pass
+
+class AmbiguousTransitionError(InvalidTransitionError):
+    pass
+
+class ConditionFailedError(Exception):
+    pass
+
+class IWorkflow(Interface):
+    """Defines workflow in the form of transition objects.
+
+    Defined as a utility.
+    """
+
+    def refresh(transitions):
+        """Refresh workflow completely with new transitions.
+        """
+        
+    def getTransitions(source):
+        """Get all transitions from source.
+        """
+
+    def getTransition(source, transition_id):
+        """Get transition with transition_id given source state.
+
+        If the transition is invalid from this source state,
+        an InvalidTransitionError is raised.
+        """
+
+    def getTransitionById(transition_id):
+        """Get transition with transition_id.
+        """
+
+class IWorkflowState(Interface):
+    """Store state on workflowed objects.
+
+    Defined as an adapter.
+    """
+    
+    def setState(state):
+        """Set workflow state for this object.
+        """
+        
+    def setId(id):
+        """Set workflow version id for this object.
+
+        This is used to mark all versions of an object with the
+        same id.
+        """
+        
+    def getState():
+        """Return workflow state of this object.
+        """
+
+    def getId():
+        """Get workflow version id for this object.
+
+        This is used to mark all versions of an object with the same id.
+        """
+        
+class IWorkflowInfo(Interface):
+    """Get workflow info about workflowed object, and drive workflow.
+
+    Defined as an adapter.
+    """
+
+    def setInitialState(state, comment=None):
+        """Set initial state for the context object.
+
+        Will also set a unique id for this new workflow.
+        
+        Fires a transition event.
+        """
+        
+    def fireTransition(transition_id, comment=None, side_effect=None,
+                       check_security=True):
+        """Fire a transition for the context object.
+
+        There's an optional comment parameter that contains some
+        opaque object that offers a comment about the transition.
+        This is useful for manual transitions where users can motivate
+        their actions.
+
+        There's also an optional side effect parameter which should
+        be a callable which receives the object undergoing the transition
+        as the parameter. This could do an editing action of the newly
+        transitioned workflow object before an actual transition event is
+        fired.
+
+        If check_security is set to False, security is not checked
+        and an application can fire a transition no matter what the
+        user's permission is.
+        """
+
+    def fireTransitionToward(state, comment=None, side_effect=None,
+                             check_security=True):
+        """Fire transition toward state.
+
+        Looks up a manual transition that will get to the indicated
+        state.
+
+        If no such transition is possible, NoTransitionAvailableError will
+        be raised.
+
+        If more than one manual transitions are possible,
+        AmbiguousTransitionError will be raised.
+        """
+        
+    def fireTransitionForVersions(state, transition_id):
+        """Fire a transition for all versions in a state.
+        """
+
+    def fireAutomatic():
+        """Fire automatic transitions if possible by condition.
+        """
+        
+    def hasVersion(state):
+        """Return true if a version exists in state.
+        """
+        
+    def getManualTransitionIds():
+        """Returns list of valid manual transitions.
+
+        These transitions have to have a condition that's True.
+        """
+
+    def getManualTransitionIdsToward(state):
+        """Returns list of manual transitions towards state.
+        """
+        
+    def getAutomaticTransitionIds():
+        """Returns list of possible automatic transitions.
+
+        Condition is not checked.
+        """
+        
+    def hasAutomaticTransitions():
+        """Return true if there are possible automatic outgoing transitions.
+
+        Condition is not checked.
+        """
+
+class IReadWorkflowVersions(Interface):
+    
+    def getVersions(state, id):
+        """Get all versions of object known for this id and state.
+        """
+
+    def getVersionsWithAutomaticTransitions():
+        """Get all versions that have outgoing transitions that are automatic.
+        """
+
+    def createVersionId():
+        """Return new unique version id.
+        """
+        
+    def hasVersion(id, state):
+        """Return true if a version exists with the specific workflow state.
+        """
+
+    def hasVersionId(id):
+        """Return True if version id is already in use.
+        """
+
+class IWriteWorkflowVersions(Interface):
+    def fireAutomatic():
+        """Fire all automatic transitions in the workflow (for all versions).
+        """
+    
+class IWorkflowVersions(IReadWorkflowVersions, IWriteWorkflowVersions):
+    """Interface to get information about versions of content in workflow.
+
+    This can be implemented on top of the Zope catalog, for instance.
+
+    Defined as a utility
+    """
+
+class IWorkflowTransitionEvent(IObjectEvent):
+    source = Attribute('Original state or None if initial state')
+    destination = Attribute('New state') 
+    transition = Attribute('Transition that was fired or None if initial state')
+    comment = Attribute('Comment that went with state transition')
+
+class IWorkflowVersionTransitionEvent(IWorkflowTransitionEvent):
+    old_object = Attribute('Old version of object')

Added: hurry.workflow/trunk/src/hurry/workflow/tests.py
===================================================================
--- hurry.workflow/trunk/src/hurry/workflow/tests.py	                        (rev 0)
+++ hurry.workflow/trunk/src/hurry/workflow/tests.py	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,78 @@
+import unittest
+
+from zope.testing import doctest
+from zope import component
+from zope.component import testing
+from zope.annotation import interfaces as annotation_interfaces
+from zope.annotation import attribute
+from hurry.workflow import interfaces, workflow
+
+class WorkflowVersions(workflow.WorkflowVersions):
+    """Simplistic implementation that keeps track of versions.
+
+    A real implementation would use something like the catalog.
+    """
+    def __init__(self):
+        self.versions = []
+        
+    def addVersion(self, obj):
+        self.versions.append(obj)
+        
+    def getVersions(self, state, id):
+        result = []
+        for version in self.versions:
+            state_adapter = interfaces.IWorkflowState(version)
+            if state_adapter.getId() == id and state_adapter.getState() == state:
+                result.append(version)
+        return result
+
+    def getVersionsWithAutomaticTransitions(self):
+        result = []
+        for version in self.versions:
+            if interfaces.IWorkflowInfo(version).hasAutomaticTransitions():
+                result.append(version)
+        return result
+    
+    def hasVersion(self, state, id):
+        return bool(self.getVersions(state, id))
+
+    def hasVersionId(self, id):
+        result = []
+        for version in self.versions:
+            state_adapter = interfaces.IWorkflowState(version)
+            if state_adapter.getId() == id:
+                return True
+        return False
+    
+    def clear(self):
+        self.versions = []
+        
+def workflowSetUp(doctest):
+    testing.setUp(doctest)
+    component.provideAdapter(
+        workflow.WorkflowState,
+        (annotation_interfaces.IAnnotatable,),
+        interfaces.IWorkflowState)
+    component.provideAdapter(
+        workflow.WorkflowInfo,
+        (annotation_interfaces.IAnnotatable,),
+        interfaces.IWorkflowInfo)
+    component.provideAdapter(
+        attribute.AttributeAnnotations,
+        (annotation_interfaces.IAttributeAnnotatable,),
+        annotation_interfaces.IAnnotations)
+    component.provideUtility(
+        WorkflowVersions(),
+        interfaces.IWorkflowVersions)
+    
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite(
+            'workflow.txt',
+            setUp=workflowSetUp, tearDown=testing.tearDown,
+            ),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+

Added: hurry.workflow/trunk/src/hurry/workflow/workflow.py
===================================================================
--- hurry.workflow/trunk/src/hurry/workflow/workflow.py	                        (rev 0)
+++ hurry.workflow/trunk/src/hurry/workflow/workflow.py	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,317 @@
+import random, sys
+
+from zope.interface import implements
+from zope.event import notify
+from zope.security.checker import CheckerPublic
+from zope.security.interfaces import NoInteraction, Unauthorized
+from zope.security.management import getInteraction
+from zope import component
+
+from zope.annotation.interfaces import IAnnotations
+from zope.lifecycleevent import ObjectModifiedEvent
+from zope.component.interfaces import ObjectEvent
+
+from hurry.workflow import interfaces
+from hurry.workflow.interfaces import MANUAL, AUTOMATIC, SYSTEM
+from hurry.workflow.interfaces import\
+     IWorkflow, IWorkflowState, IWorkflowInfo, IWorkflowVersions
+from hurry.workflow.interfaces import\
+     InvalidTransitionError, ConditionFailedError
+
+def NullCondition(wf, context):
+    return True
+
+def NullAction(wf, context):
+    pass
+
+# XXX this is needed to make the tests pass in the absence of
+# interactions..
+def nullCheckPermission(permission, principal_id):
+    return True
+
+class Transition(object):
+
+    def __init__(self, transition_id, title, source, destination,
+                 condition=NullCondition,
+                 action=NullAction,
+                 trigger=MANUAL,
+                 permission=CheckerPublic,
+                 order=0,
+                 **user_data):
+        self.transition_id = transition_id
+        self.title = title
+        self.source = source
+        self.destination = destination
+        self.condition = condition
+        self.action = action
+        self.trigger = trigger
+        self.permission = permission
+        self.order = order
+        self.user_data = user_data
+        
+    def __cmp__(self, other):
+        return cmp(self.order, other.order)
+
+# in the past this subclassed from zope.container.Contained and
+# persistent.Persistent.
+# to reduce dependencies these base classes have been removed.
+# You can choose to create a subclass in your own code that
+# mixes these in if you need persistent workflow
+class Workflow(object):
+    implements(IWorkflow)
+    
+    def __init__(self, transitions):
+        self.refresh(transitions)
+
+    def _register(self, transition):
+        transitions = self._sources.setdefault(transition.source, {})
+        transitions[transition.transition_id] = transition
+        self._id_transitions[transition.transition_id] = transition
+
+    def refresh(self, transitions):
+        self._sources = {}
+        self._id_transitions = {}
+        for transition in transitions:
+            self._register(transition)
+        self._p_changed = True
+        
+    def getTransitions(self, source):
+        try:
+            return self._sources[source].values()
+        except KeyError:
+            return []
+        
+    def getTransition(self, source, transition_id):
+        transition = self._id_transitions[transition_id]
+        if transition.source != source:
+            raise InvalidTransitionError
+        return transition
+
+    def getTransitionById(self, transition_id):
+        return self._id_transitions[transition_id]
+
+class WorkflowState(object):
+    implements(IWorkflowState)
+
+    def __init__(self, context):
+        # XXX okay, I'm tired of it not being able to set annotations, so
+        # we'll do this. Ugh.
+        from zope.security.proxy import removeSecurityProxy
+        self.context = removeSecurityProxy(context)
+
+    def initialize(self):
+        wf_versions = component.getUtility(IWorkflowVersions)
+        self.setId(wf_versions.createVersionId())
+        
+    def setState(self, state):
+        if state != self.getState():
+            IAnnotations(self.context)[
+                'hurry.workflow.state'] = state
+            
+    def setId(self, id):
+        # XXX catalog should be informed (or should it?)
+        IAnnotations(self.context)['hurry.workflow.id'] = id
+        
+    def getState(self):
+        try:
+            return IAnnotations(self.context)['hurry.workflow.state']
+        except KeyError:
+            return None
+
+    def getId(self):
+        try:
+            return IAnnotations(self.context)['hurry.workflow.id']
+        except KeyError:
+            return None
+            
+class WorkflowInfo(object):
+    implements(IWorkflowInfo)
+    
+    def __init__(self, context):
+        self.context = context
+                
+    def fireTransition(self, transition_id, comment=None, side_effect=None,
+                       check_security=True):
+        state = IWorkflowState(self.context)
+        wf = component.getUtility(IWorkflow)
+        # this raises InvalidTransitionError if id is invalid for current state
+        transition = wf.getTransition(state.getState(), transition_id)
+        # check whether we may execute this workflow transition
+        try:
+            interaction = getInteraction()
+        except NoInteraction:
+            checkPermission = nullCheckPermission
+        else:
+            if check_security:
+                checkPermission = interaction.checkPermission
+            else:
+                checkPermission = nullCheckPermission
+        if not checkPermission(
+            transition.permission, self.context):
+            raise Unauthorized(self.context,
+                               'transition: %s' % transition_id, 
+                               transition.permission)
+        # now make sure transition can still work in this context
+        if not transition.condition(self, self.context):
+            raise ConditionFailedError
+        # perform action, return any result as new version
+        result = transition.action(self, self.context)
+        if result is not None:
+            if transition.source is None:
+                IWorkflowState(result).initialize()
+            # stamp it with version
+            state = IWorkflowState(result)
+            state.setId(IWorkflowState(self.context).getId())
+            # execute any side effect:
+            if side_effect is not None:
+                side_effect(result)
+            event = WorkflowVersionTransitionEvent(result, self.context,
+                                                   transition.source,
+                                                   transition.destination,
+                                                   transition, comment)
+        else:
+            if transition.source is None:
+                IWorkflowState(self.context).initialize()
+            # execute any side effect
+            if side_effect is not None:
+                side_effect(self.context)
+            event = WorkflowTransitionEvent(self.context,
+                                            transition.source,
+                                            transition.destination,
+                                            transition, comment)
+        # change state of context or new object
+        state.setState(transition.destination)
+        notify(event)
+        # send modified event for original or new object
+        if result is None:
+            notify(ObjectModifiedEvent(self.context))
+        else:
+            notify(ObjectModifiedEvent(result))
+        return result
+
+    def fireTransitionToward(self, state, comment=None, side_effect=None,
+                             check_security=True):
+        transition_ids = self.getFireableTransitionIdsToward(state)
+        if not transition_ids:
+            raise interfaces.NoTransitionAvailableError
+        if len(transition_ids) != 1:
+            raise interfaces.AmbiguousTransitionError
+        return self.fireTransition(transition_ids[0],
+                                   comment, side_effect, check_security)
+        
+    def fireTransitionForVersions(self, state, transition_id):
+        id = IWorkflowState(self.context).getId()
+        wf_versions = component.getUtility(IWorkflowVersions)
+        for version in wf_versions.getVersions(state, id):
+            if version is self.context:
+                continue
+            IWorkflowInfo(version).fireTransition(transition_id)
+
+    def fireAutomatic(self):
+        for transition_id in self.getAutomaticTransitionIds():
+            try:
+                self.fireTransition(transition_id)
+            except ConditionFailedError:
+                # if condition failed, that's fine, then we weren't
+                # ready to fire yet
+                pass
+            else:
+                # if we actually managed to fire a transition,
+                # we're done with this one now.
+                return
+            
+    def hasVersion(self, state):
+        wf_versions = component.getUtility(IWorkflowVersions)
+        id = IWorkflowState(self.context).getId()
+        return wf_versions.hasVersion(state, id)
+    
+    def getManualTransitionIds(self):
+        try:
+            checkPermission = getInteraction().checkPermission
+        except NoInteraction:
+            checkPermission = nullCheckPermission
+        return [transition.transition_id for transition in
+                sorted(self._getTransitions(MANUAL)) if
+                transition.condition(self, self.context) and
+                checkPermission(transition.permission, self.context)]
+
+    def getSystemTransitionIds(self):
+        # ignore permission checks
+        return [transition.transition_id for transition in
+                sorted(self._getTransitions(SYSTEM)) if
+                transition.condition(self, self.context)]
+
+    def getFireableTransitionIds(self):
+        return self.getManualTransitionIds() + self.getSystemTransitionIds()
+    
+    def getFireableTransitionIdsToward(self, state):
+        wf = component.getUtility(IWorkflow)
+        result = []
+        for transition_id in self.getFireableTransitionIds():
+            transition = wf.getTransitionById(transition_id)
+            if transition.destination == state:
+                result.append(transition_id)
+        return result
+    
+    def getAutomaticTransitionIds(self):
+        return [transition.transition_id for transition in
+                self._getTransitions(AUTOMATIC)]
+
+    def hasAutomaticTransitions(self):
+        # XXX could be faster
+        return bool(self.getAutomaticTransitionIds())
+
+    def _getTransitions(self, trigger):
+        # retrieve all possible transitions from workflow utility
+        wf = component.getUtility(IWorkflow)
+        transitions = wf.getTransitions(
+            IWorkflowState(self.context).getState())
+        # now filter these transitions to retrieve all possible
+        # transitions in this context, and return their ids
+        return [transition for transition in transitions if
+                transition.trigger == trigger]
+            
+class WorkflowVersions(object):
+    implements(IWorkflowVersions)
+
+    def getVersions(self, state, id):
+        raise NotImplementedError
+
+    def getVersionsWithAutomaticTransitions(self):
+        raise NotImplementedError
+
+    def createVersionId(self):
+        while True:
+            id = random.randrange(sys.maxint)
+            if not self.hasVersionId(id):
+                return id
+        assert False, "Shouldn't ever reach here"
+
+    def hasVersion(self, state, id):
+        raise NotImplementedError
+
+    def hasVersionId(self, id):
+        raise NotImplementedError
+
+    def fireAutomatic(self):
+        for version in self.getVersionsWithAutomaticTransitions():
+            IWorkflowInfo(version).fireAutomatic()
+
+class WorkflowTransitionEvent(ObjectEvent):
+    implements(interfaces.IWorkflowTransitionEvent)
+
+    def __init__(self, object, source, destination, transition, comment):
+        super(WorkflowTransitionEvent, self).__init__(object)
+        self.source = source
+        self.destination = destination
+        self.transition = transition
+        self.comment = comment
+
+class WorkflowVersionTransitionEvent(WorkflowTransitionEvent):
+    implements(interfaces.IWorkflowVersionTransitionEvent)
+
+    def __init__(self, object, old_object, source, destination,
+                 transition, comment):
+        super(WorkflowVersionTransitionEvent, self).__init__(
+            object, source, destination, transition, comment)
+        self.old_object = old_object

Added: hurry.workflow/trunk/src/hurry/workflow/workflow.txt
===================================================================
--- hurry.workflow/trunk/src/hurry/workflow/workflow.txt	                        (rev 0)
+++ hurry.workflow/trunk/src/hurry/workflow/workflow.txt	2009-06-17 12:40:16 UTC (rev 101107)
@@ -0,0 +1,783 @@
+Hurry Workflow
+==============
+
+The hurry workflow system is a "roll my own because I'm in a hurry"
+framework.
+
+Basic workflow
+--------------
+
+Let's first make a content object that can go into a workflow::
+
+   >>> from zope.interface import implements, Attribute
+ 
+   >>> from zope.annotation.interfaces import IAttributeAnnotatable
+   >>> class IDocument(IAttributeAnnotatable):
+   ...    title = Attribute('Title')
+   >>> class Document(object):
+   ...    implements(IDocument) 
+   ...    def __init__(self, title):
+   ...        self.title = title
+
+As you can see, such a content object must provide IAnnotatable, as
+this is used to store the workflow state. The system uses the
+IWorkflowState adapter to get and set an object's workflow state::
+
+    >>> from hurry.workflow import interfaces
+    >>> document = Document('Foo')
+    >>> state = interfaces.IWorkflowState(document)
+    >>> print state.getState()
+    None
+
+The state can be set directly for an object using the IWorkflowState
+adapter as well::
+
+    >>> state.setState('foo')
+    >>> state.getState()
+    'foo'
+
+But let's set it back to None again, so we can start again in a
+pristine state for this document::
+ 
+    >>> state.setState(None)
+ 
+It's not recommended use setState() do this ourselves, though: usually
+we'll let the workflow system take care of state transitions and the
+setting of the initial state.
+
+Now let's define a simple workflow transition from 'a' to 'b'. It
+needs a condition which must return True before the transition is
+allowed to occur::
+
+    >>> def NullCondition(wf, context):
+    ...    return True
+
+and an action that takes place when the transition is taken::
+
+    >>> def NullAction(wf, context):
+    ...    pass
+
+Now let's construct a transition::
+
+    >>> from hurry.workflow import workflow 
+    >>> transition = workflow.Transition( 
+    ...     transition_id='a_to_b', 
+    ...     title='A to B', 
+    ...     source='a', 
+    ...     destination='b', 
+    ...     condition=NullCondition, 
+    ...     action=NullAction, 
+    ...     trigger=interfaces.MANUAL)
+
+The transition trigger is either MANUAL, AUTOMATIC or SYSTEM. MANUAL
+indicates user action is needed to fire the transition. AUTOMATIC
+transitions fire automatically. SYSTEM is a workflow transition
+directly fired by the system, and not directly by the user.
+
+We also will introduce an initial transition, that moves an object
+into the workflow (for instance just after it is created)::
+
+    >>> init_transition = workflow.Transition(
+    ...     transition_id='to_a',
+    ...     title='Create A',
+    ...     source=None,
+    ...     destination='a')
+
+And a final transition, when the object moves out of the workflow again
+(for instance just before it is deleted)::
+
+    >>> final_transition = workflow.Transition(
+    ...     transition_id='finalize',
+    ...     title='Delete',
+    ...     source='b',
+    ...     destination=None)
+ 
+Now let's put the transitions in an workflow utility::
+
+    >>> wf = workflow.Workflow([transition, init_transition, final_transition])
+    >>> from zope import component
+    >>> component.provideUtility(wf, interfaces.IWorkflow)
+
+Workflow transitions cause events to be fired; we will put in a simple
+handler so we can check whether things were successfully fired::
+
+    >>> events = []
+    >>> def transition_handler(event):
+    ...     events.append(event)
+    >>> component.provideHandler(
+    ...     transition_handler,
+    ...     [interfaces.IWorkflowTransitionEvent])
+
+To get what transitions to other states are possible from an object,
+as well as to fire transitions and set initial state, we use the
+IWorkflowInfo adapter::
+
+    >>> info = interfaces.IWorkflowInfo(document)
+
+We'll initialize the workflow by firing the 'to_a' transition::
+
+    >>> info.fireTransition('to_a')
+
+This should've fired an event::
+
+    >>> events[-1].transition.transition_id
+    'to_a'
+    >>> events[-1].source is None
+    True
+    >>> events[-1].destination
+    'a'
+
+There's only a single transition defined to workflow state 'b'::
+
+    >>> info.getManualTransitionIds()
+    ['a_to_b']
+
+We can also get this by asking which manual (or system) transition
+exists that brings us to the desired workflow state::
+
+    >>> info.getFireableTransitionIdsToward('b')
+    ['a_to_b']
+
+Since this is a manually triggered transition, we can fire this
+transition::
+
+    >>> info.fireTransition('a_to_b')
+
+The workflow state should now be 'b'::
+
+    >>> state.getState()
+    'b'
+
+We check that the event indeed got fired::
+
+    >>> events[-1].transition.transition_id
+    'a_to_b'
+    >>> events[-1].source
+    'a'
+    >>> events[-1].destination
+    'b'
+
+We will also try fireTransitionToward here, so we sneak back the
+workflow to state 'a' again and try that::
+
+    >>> state.setState('a')
+
+Try going through a transition we cannot reach first::
+
+    >>> info.fireTransitionToward('c')
+    Traceback (most recent call last):
+    ...
+    NoTransitionAvailableError
+
+Now go to 'b' again::
+
+    >>> info.fireTransitionToward('b')
+    >>> state.getState()
+    'b'
+
+Finally, before forgetting about our document, we finalize the workflow::
+
+    >>> info.fireTransition('finalize')
+    >>> state.getState() is None
+    True
+
+And we have another event that was fired::
+
+    >>> events[-1].transition.transition_id
+    'finalize'
+    >>> events[-1].source
+    'b'
+    >>> events[-1].destination is None
+    True
+   
+Multi-version workflow
+----------------------
+
+Now let's go for a more complicated scenario where have multiple
+versions of a document. At any one time a document can have an
+UNPUBLISHED version and a PUBLISHED version. There can also be a
+CLOSED version and any number of ARCHIVED versions::
+
+   >>> UNPUBLISHED = 'unpublished'
+   >>> PUBLISHED = 'published'
+   >>> CLOSED = 'closed'
+   >>> ARCHIVED = 'archived'
+
+Let's start with a simple initial transition::
+
+   >>> init_transition = workflow.Transition(
+   ...    transition_id='init',
+   ...    title='Initialize',
+   ...    source=None,
+   ...    destination=UNPUBLISHED)
+
+When the unpublished version is published, any previously published
+version is made to be the CLOSED version. To accomplish this secondary
+state transition, we'll use the system's built-in versioning ability
+with the 'fireTransitionsForVersions' method, which can be used to
+fire transitions of other versions of the document::
+
+   >>> def PublishAction(wf, context):
+   ...    wf.fireTransitionForVersions(PUBLISHED, 'close')
+
+Now let's build the transition::
+
+   >>> publish_transition = workflow.Transition(
+   ...    transition_id='publish',
+   ...    title='Publish',
+   ...    source=UNPUBLISHED,
+   ...    destination=PUBLISHED,
+   ...    condition=NullCondition,
+   ...    action=PublishAction,
+   ...    trigger=interfaces.MANUAL,
+   ...    order=1)
+
+Next, we'll define a transition from PUBLISHED to CLOSED, which means
+we want to archive whatever was closed before::
+
+   >>> def CloseAction(wf, context):
+   ...    wf.fireTransitionForVersions(CLOSED, 'archive')
+   >>> close_transition = workflow.Transition(
+   ...    transition_id='close',
+   ...    title='Close',
+   ...    source=PUBLISHED,
+   ...    destination=CLOSED,
+   ...    condition=NullCondition,
+   ...    action=CloseAction,
+   ...    trigger=interfaces.MANUAL,
+   ...    order=2)
+
+Note that CloseAction will also be executed automatically whenever
+state is transitioned from PUBLISHED to CLOSED using
+fireTransitionsForVersions. This means that publishing a document
+results in the previously closed document being archived.
+
+If there is a PUBLISHED but no UNPUBLISHED version, we can make a new
+copy of the PUBLISHED version and make that the UNPUBLISHED version::
+
+   >>> def CanCopyCondition(wf, context):
+   ...     return not wf.hasVersion(UNPUBLISHED)
+
+Since we are actually creating a new content object, the action should
+return the newly created object with the new state::
+
+   >>> def CopyAction(wf, context):
+   ...     return Document('copy of %s' % context.title)
+
+   >>> copy_transition = workflow.Transition(
+   ...     transition_id='copy',
+   ...     title='Copy',
+   ...     source=PUBLISHED,
+   ...     destination=UNPUBLISHED,
+   ...     condition=CanCopyCondition,
+   ...     action=CopyAction,
+   ...     trigger=interfaces.MANUAL,
+   ...     order=3)
+
+A very similar transition applies to the closed version. If we have
+no UNPUBLISHED version and no PUBLISHED version, we can make a new copy
+from the CLOSED version::
+
+   >>> def CanCopyCondition(wf, context):
+   ...     return (not wf.hasVersion(UNPUBLISHED) and 
+   ...         not wf.hasVersion(PUBLISHED))
+ 
+   >>> copy_closed_transition = workflow.Transition(
+   ...     transition_id='copy_closed',
+   ...     title='Copy',
+   ...     source=CLOSED,
+   ...     destination=UNPUBLISHED,
+   ...     condition=CanCopyCondition,
+   ...     action=CopyAction,
+   ...     trigger=interfaces.MANUAL,
+   ...     order=4)
+
+Finally let's build the archiving transition::
+
+   >>> archive_transition = workflow.Transition(
+   ...     transition_id='archive',
+   ...     title='Archive',
+   ...     source=CLOSED,
+   ...     destination=ARCHIVED,
+   ...     condition=NullCondition,
+   ...     action=NullAction,
+   ...     trigger=interfaces.MANUAL,
+   ...     order=5)
+
+Now let's build and provide the workflow utility::
+
+   >>> wf = workflow.Workflow([init_transition, 
+   ...                         publish_transition, close_transition,
+   ...                         copy_transition, copy_closed_transition,
+   ...                         archive_transition])
+
+   >>> component.provideUtility(wf, interfaces.IWorkflow)
+
+Let's get the workflow_versions utility which we can use to track
+versions and come up with a new unique id::
+
+   >>> workflow_versions = component.getUtility(interfaces.IWorkflowVersions)
+
+And let's start with a document::
+
+   >>> document = Document('bar')
+   >>> info = interfaces.IWorkflowInfo(document)
+   >>> info.fireTransition('init')
+
+We need the document id to compare later when we create a new version::
+
+   >>> state = interfaces.IWorkflowState(document)
+   >>> document_id = state.getId()
+
+Let's add it to the workflow versions container so we can find it. Note
+that we're using a private API here; this could be implemented as adding
+it to a folder or any other way, as long as getVersions() works later::
+
+   >>> workflow_versions.addVersion(document) # private API
+
+Also clear out previously recorded events::
+
+   >>> del events[:]
+
+We can publish it::
+
+   >>> info.getManualTransitionIds()
+   ['publish']
+
+So let's do that::
+
+   >>> info.fireTransition('publish')
+   >>> state.getState()
+   'published'
+
+The last event should be the 'publish' transition::
+
+   >>> events[-1].transition.transition_id
+   'publish'
+
+And now we can either close or create a new copy of it. Note that the
+names are sorted using the order of the transitions::
+
+   >>> info.getManualTransitionIds()
+   ['close', 'copy']
+
+Let's close it::
+
+  >>> info.fireTransition('close')
+  >>> state.getState()
+  'closed'
+
+We're going to create a new copy for editing now::
+
+  >>> info.getManualTransitionIds()
+  ['copy_closed', 'archive']
+  >>> document2 = info.fireTransition('copy_closed')
+  >>> workflow_versions.addVersion(document2) # private API to track it
+  >>> document2.title
+  'copy of bar'
+  >>> state = interfaces.IWorkflowState(document2)
+  >>> state.getState()
+  'unpublished'
+  >>> state.getId() == document_id
+  True
+
+The original version is still there in its original state::
+
+  >>> interfaces.IWorkflowState(document).getState()
+  'closed'
+
+Let's also check the last event in some detail::
+
+  >>> event = events[-1]
+  >>> event.transition.transition_id
+  'copy_closed'
+  >>> event.old_object == document
+  True
+  >>> event.object == document2
+  True
+
+Now we are going to publish the new version::
+
+  >>> info = interfaces.IWorkflowInfo(document2)
+  >>> info.getManualTransitionIds()
+  ['publish']
+  >>> info.fireTransition('publish')
+  >>> interfaces.IWorkflowState(document2).getState()
+  'published'
+
+The original is still closed::
+
+  >>> interfaces.IWorkflowState(document).getState()
+  'closed'
+
+Now let's publish another copy after this::
+
+  >>> document3 = info.fireTransition('copy')
+  >>> workflow_versions.addVersion(document3)
+  >>> interfaces.IWorkflowInfo(document3).fireTransition('publish')
+
+This copy is now published::
+
+  >>> interfaces.IWorkflowState(document3).getState()
+  'published'
+ 
+And the previously published version is now closed::
+  
+  >>> interfaces.IWorkflowState(document2).getState()
+  'closed'
+
+Note that due to the condition, it's not possible to copy from the
+closed version, as there is a published version still remaining::
+
+  >>> interfaces.IWorkflowInfo(document2).getManualTransitionIds()
+  ['archive']
+
+Meanwhile, the original version, previously closed, is now archived::
+
+  >>> interfaces.IWorkflowState(document).getState()
+  'archived'
+
+Automatic transitions
+---------------------
+
+Now let's try a workflow transition that is automatic and time-based.
+We'll set up a very simple workflow between 'unpublished' and
+'published', and have the 'published' transition be time-based.
+
+To simulate time, we have moments::
+
+   >>> time_moment = 0
+
+We will only publish if time_moment is greater than 3::
+
+   >>> def TimeCondition(wf, context):
+   ...    return time_moment > 3
+
+Set up the transition using this condition; note that this one is
+automatic, i.e. it doesn't have to be triggered by humans::
+
+   >>> publish_transition = workflow.Transition(
+   ...    transition_id='publish',
+   ...    title='Publish',
+   ...    source=UNPUBLISHED,
+   ...    destination=PUBLISHED,
+   ...    condition=TimeCondition,
+   ...    action=NullAction,
+   ...    trigger=interfaces.AUTOMATIC)
+
+Set up the workflow using this transition, and reusing the
+init transition we defined before::
+
+   >>> wf = workflow.Workflow([init_transition, publish_transition])
+   >>> component.provideUtility(wf, interfaces.IWorkflow)
+
+Clear out all versions; this is an private API we just use for
+demonstration purposes::
+
+   >>> workflow_versions.clear()
+
+Now create a document::
+
+   >>> document = Document('bar')
+   >>> info = interfaces.IWorkflowInfo(document)
+   >>> info.fireTransition('init')
+
+Private again; do this with the catalog or any way you prefer in your
+own code::
+
+   >>> workflow_versions.addVersion(document)
+
+Since this transition is automatic, we should see it like this::
+
+   >>> interfaces.IWorkflowInfo(document).getAutomaticTransitionIds()
+   ['publish']
+
+Now fire let's any automatic transitions::
+
+   >>> workflow_versions.fireAutomatic()
+
+Nothing should have happened as we are still at time moment 0::
+  
+   >>> state = interfaces.IWorkflowState(document)
+   >>> state.getState()
+   'unpublished'
+
+We change the time moment past 3::
+
+   >>> time_moment = 4
+
+Now fire any automatic transitions again::
+
+   >>> workflow_versions.fireAutomatic()
+
+The transition has fired, so the state will be 'published'::
+ 
+   >>> state.getState()
+   'published'
+
+System transitions
+------------------
+
+Let's try system transitions now. This transition shouldn't show up
+as manual nor as automatic::
+
+   >>> publish_transition = workflow.Transition(
+   ...    transition_id='publish',
+   ...    title='Publish',
+   ...    source=UNPUBLISHED,
+   ...    destination=PUBLISHED,
+   ...    trigger=interfaces.SYSTEM)
+
+Set up the workflow using this transition, and reusing the
+init transition we defined before::
+
+   >>> wf = workflow.Workflow([init_transition, publish_transition])
+   >>> component.provideUtility(wf, interfaces.IWorkflow)
+
+Clear out all versions; this is an private API we just use for
+demonstration purposes::
+
+   >>> workflow_versions.clear()
+
+Now create a document::
+
+   >>> document = Document('bar')
+   >>> info = interfaces.IWorkflowInfo(document)
+   >>> info.fireTransition('init')
+
+Private again; do this with the catalog or any way you prefer in your
+own code::
+
+   >>> workflow_versions.addVersion(document)
+
+We should see it as a system transition::
+
+   >>> info.getSystemTransitionIds()
+   ['publish']
+
+but not as automatic nor manual::
+
+   >>> info.getAutomaticTransitionIds()
+   []
+   >>> info.getManualTransitionIds()
+   []
+
+This transition can be fired::
+
+   >>> info.fireTransition('publish')
+   >>> interfaces.IWorkflowState(document).getState()
+   'published'
+
+Multiple transitions
+--------------------
+
+It's possible to have multiple transitions from the source state to
+the target state, for instance an automatic and a manual one.
+
+Let's set up a workflow with two manual transitions and a single
+automatic transitions between two states::
+
+   >>> publish_1_transition = workflow.Transition(
+   ...    transition_id='publish 1',
+   ...    title='Publish 1',
+   ...    source=UNPUBLISHED,
+   ...    destination=PUBLISHED,
+   ...    condition=NullCondition,
+   ...    action=NullAction,
+   ...    trigger=interfaces.MANUAL)
+
+   >>> publish_2_transition = workflow.Transition(
+   ...    transition_id='publish 2',
+   ...    title='Publish 2',
+   ...    source=UNPUBLISHED,
+   ...    destination=PUBLISHED,
+   ...    condition=NullCondition,
+   ...    action=NullAction,
+   ...    trigger=interfaces.MANUAL)
+
+   >>> publish_auto_transition = workflow.Transition(
+   ...    transition_id='publish auto',
+   ...    title='Publish Auto',
+   ...    source=UNPUBLISHED,
+   ...    destination=PUBLISHED,
+   ...    condition=TimeCondition,
+   ...    action=NullAction,
+   ...    trigger=interfaces.AUTOMATIC)
+
+Clear out all versions; this is an private API we just use for
+demonstration purposes::
+
+   >>> workflow_versions.clear()
+
+Since we're using the time condition again, let's make sure
+time is at 0 again so that the publish_auto_transition doesn't fire::
+
+   >>> time_moment = 0
+
+Now set up the workflow using these transitions, plus our
+init_transition::
+
+   >>> wf = workflow.Workflow([init_transition, 
+   ...     publish_1_transition, publish_2_transition, 
+   ...     publish_auto_transition])
+   >>> component.provideUtility(wf, interfaces.IWorkflow)
+
+Now create a document::
+
+   >>> document = Document('bar')
+   >>> info = interfaces.IWorkflowInfo(document)
+   >>> info.fireTransition('init')
+
+We should have two manual transitions::
+
+   >>> sorted(interfaces.IWorkflowInfo(document).getManualTransitionIds())
+   ['publish 1', 'publish 2']
+
+And a single automatic transition::
+
+   >>> interfaces.IWorkflowInfo(document).getAutomaticTransitionIds()
+   ['publish auto']
+
+Protecting transitions with permissions
+---------------------------------------
+
+Permissions can be (and should be) protected with a permission, so
+that not everybody can execute them.
+
+Let's set up a workflow with a permission that has a permission::
+
+   >>> publish_transition = workflow.Transition(
+   ...    transition_id='publish',
+   ...    title='Publish',
+   ...    source=UNPUBLISHED,
+   ...    destination=PUBLISHED,
+   ...    condition=NullCondition,
+   ...    action=NullAction,
+   ...    trigger=interfaces.MANUAL,
+   ...    permission="zope.ManageContent")
+
+Quickly set up the workflow state again for a document::
+
+   >>> workflow_versions.clear()
+   >>> wf = workflow.Workflow([init_transition, publish_transition])
+   >>> component.provideUtility(wf, interfaces.IWorkflow)
+   >>> document = Document('bar')
+   >>> info = interfaces.IWorkflowInfo(document)
+   >>> info.fireTransition('init')
+
+Let's set up the security context::
+
+   >>> from zope.security.interfaces import Unauthorized 
+   >>> from zope.security.management import newInteraction, endInteraction
+   >>> class Principal:
+   ...    def __init__(self, id):
+   ...        self.id = id
+   ...        self.groups = [] 
+   >>> class Participation:
+   ...   interaction = None
+   ...   def __init__(self, principal):
+   ...      self.principal = principal
+   >>> endInteraction() # XXX argh, apparently one is already active?
+   >>> newInteraction(Participation(Principal('bob')))
+
+We shouldn't see this permission appear in our list of possible transitions,
+as we do not have access::
+
+   >>> info.getManualTransitionIds()
+   []
+
+Now let's try firing the transition. It should fail with Unauthorized::
+
+   >>> try:
+   ...     info.fireTransition('publish')
+   ... except Unauthorized:
+   ...     print "Got unauthorized"
+   Got unauthorized
+
+The system user is however allowed to do it::
+
+   >>> from zope.security.management import system_user   
+   >>> endInteraction() 
+   >>> newInteraction(Participation(system_user))
+   >>> info.fireTransition('publish')
+
+And this goes off without a problem.
+
+There is also a special way to make it happen by passing check_security is
+False to fireTransition::
+
+   >>> endInteraction()
+   >>> newInteraction(Participation(Principal('bob')))
+   >>> interfaces.IWorkflowState(document).setState(UNPUBLISHED)
+   >>> info.fireTransition('publish', check_security=False)
+
+Side effects during transitions
+-------------------------------
+
+Sometimes we would like something to get executed *before* the
+WorkflowTransitionEvent is fired, but after a (potential) new version
+of the object has been created.  If an object is edited during the
+same request as a workflow transition, the editing should take place
+after a potential new version has been created, otherwise the old, not
+the new, version will be edited.
+
+If something like a history logger hooks into IWorkflowTransitionEvent
+however, it would get information about the new copy *before* the
+editing took place. To allow an editing to take place between the
+creation of the new copy and the firing of the event, a side effect
+function can be passed along when a transition is fired. 
+
+The sequence of execution then is:
+
+* firing of transition itself, creating a new version
+
+* executing the side effect function on the new version
+
+* firing the IWorkflowTransitionEvent
+
+Let's set up a very simple workflow:
+
+   >>> foo_transition = workflow.Transition(
+   ...    transition_id='foo',
+   ...    title='Foo',
+   ...    source=UNPUBLISHED,
+   ...    destination=PUBLISHED,
+   ...    condition=NullCondition,
+   ...    action=CopyAction,
+   ...    trigger=interfaces.MANUAL)
+
+Quickly set up the workflow state again for a document::
+
+   >>> workflow_versions.clear()
+   >>> wf = workflow.Workflow([init_transition, foo_transition])
+   >>> component.provideUtility(wf, interfaces.IWorkflow)
+   >>> document = Document('bar')
+   >>> events = []
+   >>> info = interfaces.IWorkflowInfo(document)
+   >>> info.fireTransition('init')
+
+Now let's set up a side effect::
+
+   >>> def side_effect(context):
+   ...    context.title = context.title + '!'
+
+Now fire the transition, with a side effect::
+
+   >>> new_version = info.fireTransition('foo', side_effect=side_effect)
+   
+The title of the new version should now have a ! at the end::
+  
+   >>> new_version.title[-1] == '!'
+   True
+
+But the old version doesn't::
+
+   >>> document.title[-1] == '!'
+   False
+
+The events list we set up before should contain two events::
+
+   >>> len(events)
+   2
+   >>> events[1].object.title[-1] == '!'
+   True



More information about the Checkins mailing list