[Zope3-checkins] CVS: Zope3/src/zope/app/workflow/stateful - instance.py:1.8

Stephan Richter srichter@cosmos.phy.tufts.edu
Wed, 30 Jul 2003 11:24:44 -0400


Update of /cvs-repository/Zope3/src/zope/app/workflow/stateful
In directory cvs.zope.org:/tmp/cvs-serv7446/src/zope/app/workflow/stateful

Modified Files:
	instance.py 
Log Message:
This is a good checkpoint and I am going to check this stuff in...

  - Created Workflow events and enhanced some tests to test for them being
    published correctly.

  - Revived RelevantData and compacted the code. I threw out all of the 
    checker code for now. I have a feeling that we ain't gotta need it. 

The last steps are:

  - Being able to specify permissions (set and get) for each field of the 
    RelevantData Schema.

  - View/Modify the RelevantData in a Process Instance.

Once I am done with that, I think that the workflow code is in a good state
for the beta.



=== Zope3/src/zope/app/workflow/stateful/instance.py 1.7 => 1.8 ===
--- Zope3/src/zope/app/workflow/stateful/instance.py:1.7	Sat Jun  7 09:00:00 2003
+++ Zope3/src/zope/app/workflow/stateful/instance.py	Wed Jul 30 11:24:09 2003
@@ -17,69 +17,106 @@
 """
 __metaclass__ = type
 
-from types import StringTypes
 from persistence import Persistent
-from zope.schema import getFields
-from zope.interface import directlyProvides, implements
 
+from zope.app.context import ContextWrapper
+from zope.app.event import publish
+from zope.app.interfaces.workflow.stateful import AUTOMATIC
+from zope.app.interfaces.workflow.stateful import IStatefulProcessInstance
+from zope.app.interfaces.workflow.stateful import ITransitionEvent
+from zope.app.interfaces.workflow.stateful import \
+     IBeforeTransitionEvent, IAfterTransitionEvent
+from zope.app.interfaces.workflow.stateful import IRelevantDataChangeEvent
+from zope.app.interfaces.workflow.stateful import \
+     IBeforeRelevantDataChangeEvent, IAfterRelevantDataChangeEvent
+from zope.app.interfaces.workflow.stateful import AUTOMATIC
+from zope.app.traversing import getParent
+from zope.app.workflow.instance import ProcessInstance
+from zope.component import getService, getServiceManager
+from zope.context import ContextMethod, ContextProperty, getWrapperContainer
 from zope.exceptions import Unauthorized
-
-from zope.component import getService
-from zope.component import getServiceManager
-
+from zope.interface import directlyProvides, implements
 from zope.proxy import removeAllProxies
-from zope.context import ContextMethod
-from zope.app.context import ContextWrapper
-
+from zope.schema import getFields
 from zope.security.management import getSecurityManager
 from zope.security.checker import CheckerPublic
+from zope.tales.engine import Engine
 
-# XXX Needed for WfrData Permission checking
-# commented out for now
-#from zope.security.checker import CheckerPublic, selectChecker
-#from zope.security.checker import Checker
-#from zope.security.proxy import getChecker, Proxy
 
-from zope.tales.engine import Engine
+class TransitionEvent:
+    """A simple implementation of the transition event."""
+    implements(ITransitionEvent)
+
+    def __init__(self, object, process, transition):
+        self.object = object
+        self.process = process
+        self.transition = transition
+
+class BeforeTransitionEvent(TransitionEvent):
+    implements(IBeforeTransitionEvent)
+
+class AfterTransitionEvent(TransitionEvent):
+    implements(IAfterTransitionEvent)
 
-from zope.app.interfaces.workflow.stateful import IStatefulProcessInstance
-from zope.app.workflow.instance import ProcessInstance
 
+class RelevantDataChangeEvent:
+    """A simple implementation of the transition event."""
+    implements(IRelevantDataChangeEvent)
 
-class RelevantData:
-    pass
+    def __init__(self, process, schema, attributeName, oldValue, newValue):
+        self.process = process
+        self.schema = schema
+        self.attributeName = attributeName
+        self.oldValue = oldValue
+        self.newValue = newValue
 
-# XXX Example of how Changes to Workflow Relevant Data would send out Events
-# ToDo:
-# - Define Events:
-#    RelevantDataChangingWorkflowEvent
-#    RelevantDataChangedWorkflowEvent
-#
-# - all this is untested !!!!
-#
-#class RelevantData:
-#
-#    def __setattr__(self, key, value):
-#        is_schema_field = bool(key in getFields(self.__implements__).keys())
-#        if is_schema_field:
-#            # Send an Event bevor RelevantData changes
-#            oldvalue = getattr(self, key, None)
-#            print "send RelevantDataChangingWorkflowEvent(key:%s, old:%s, new:%s) here" \
-#                  % (key, oldvalue, value)
-#
-#        super(RelevantData, self).__setattr__(key, value)
-#
-#        if is_schema_field:
-#            # Send an Event after RelevantData has changed
-#            print "send RelevantDataChangedWorkflowEvent(key:%s, old:%s, new:%s) here" \
-#                  % (key, oldvalue, value)
+class BeforeRelevantDataChangeEvent(RelevantDataChangeEvent):
+    implements(IBeforeRelevantDataChangeEvent)
 
+class AfterRelevantDataChangeEvent(RelevantDataChangeEvent):
+    implements(IAfterRelevantDataChangeEvent)
 
 
+class RelevantData(Persistent):
+    """The relevant data object can store data that is important to the
+    workflow and fires events when this data is changed."""
+
+    def __init__(self, schema=None):
+        super(RelevantData, self).__init__()
+        self.__schema = None
+        # Add the new attributes, if there was a schema passed in
+        if schema is not None:
+            for name, field in getFields(schema).items():
+                setattr(self, name, field.default)
+            self.__schema = schema
+            directlyProvides(self, schema)
+
+    def __setattr__(self, key, value):
+        # The '__schema' attribute has a sepcial function
+        if key == '_RelevantData__schema':
+            return super(RelevantData, self).__setattr__(key, value)
+            
+        is_schema_field = self.__schema is not None and \
+                          key in getFields(self.__schema).keys()
+
+        if is_schema_field:
+            process = getWrapperContainer(self) 
+            # Send an Event before RelevantData changes
+            oldvalue = getattr(self, key, None)
+            publish(self, BeforeRelevantDataChangeEvent(
+                process, self.__schema, key, oldvalue, value))
+            
+        super(RelevantData, self).__setattr__(key, value)
+
+        if is_schema_field:
+            # Send an Event after RelevantData has changed
+            publish(self, AfterRelevantDataChangeEvent(
+                process, self.__schema, key, oldvalue, value))
+    __setattr__ = ContextMethod(__setattr__)
+
 
 class StateChangeInfo:
-    """Immutable StateChangeInfo.
-    """
+    """Immutable StateChangeInfo."""
 
     def __init__(self, transition):
         self.__old_state = transition.sourceState
@@ -90,59 +127,32 @@
     new_state = property(lambda self: self.__new_state)
 
 
-
 class StatefulProcessInstance(ProcessInstance, Persistent):
-    """Stateful Workflow ProcessInstance.
-    """
+    """Stateful Workflow ProcessInstance."""
 
     implements(IStatefulProcessInstance)
 
+    def getData(self):
+        return ContextWrapper(self._data, self, name="data")
 
-    ############################################################
-    # Implementation methods for interface
-    # zope.app.interfaces.workflow.IStatefulProcessInstance
-
-    data = property(lambda self: ContextWrapper(self._data, self))
-
-    # XXX this is not entirely tested nor finished
-    #def _getData(self):
-    #    """getter for Workflow Relevant Data."""
-    #    
-    #    data = self._data
-    #    if data is None:
-    #        return
-    #    
-    #    schema = data.__implements__
-    #
-    #    # XXX permissions need to be manageable TTW
-    #    # is this too much overhead ????
-    #    checker_getattr = {}
-    #    checker_setattr = {}
-    #    for name in schema.names(all=True):
-    #        
-    #        # XXX Just a dummy implementation for now
-    #        checker_getattr[name] = CheckerPublic
-    #        checker_setattr[name] = CheckerPublic
-    #        
-    #    checker = Checker(checker_getattr.get, checker_setattr.get)
-    #    return Proxy(data, checker)
-    #
-    #data = property(lambda self: ContextWrapper(self._getData(), self))
+    data = ContextProperty(getData) 
 
     def initialize(self):
-        pd = self._getProcessDefinition()
+        """See zope.app.interfaces.workflow.IStatefulProcessInstance""" 
+        pd = self.getProcessDefinition()
         clean_pd = removeAllProxies(pd)
         self._status = clean_pd.getInitialStateName()
 
-        # resolve schema class 
+        # resolve schema class
+        # This should really always return a schema
         schema = clean_pd.getRelevantDataSchema()
         if schema:
-            if type(schema) in StringTypes:
+            if isinstance(schema, (str, unicode)):
                 sm = getServiceManager(self)
-                schema =  sm.resolve(schema)
+                schema = sm.resolve(schema)
 
             # create relevant-data
-            self._data = self._buildRelevantData(schema)
+            self._data = RelevantData(schema)
         else:
             self._data = None
         # setup permission on data
@@ -152,46 +162,40 @@
     initialize = ContextMethod(initialize)
 
     def getOutgoingTransitions(self):
-        pd = self._getProcessDefinition()
+        """See zope.app.interfaces.workflow.IStatefulProcessInstance""" 
+        pd = self.getProcessDefinition()
         clean_pd = removeAllProxies(pd)
         return self._outgoingTransitions(clean_pd)
     getOutgoingTransitions = ContextMethod(getOutgoingTransitions)
 
     def fireTransition(self, id):
-        pd = self._getProcessDefinition()
+        """See zope.app.interfaces.workflow.IStatefulProcessInstance""" 
+        pd = self.getProcessDefinition()
         clean_pd = removeAllProxies(pd)
         if not id in self._outgoingTransitions(clean_pd):
             raise KeyError, 'Invalid Transition Id: %s' % id
-        trans = clean_pd.transitions[id]
-        # modify relevant-data if needed
+        transition = clean_pd.transitions[id]
+        # Get the object whose status is being changed.
+        obj = getParent(self)
 
-        # XXX Implement EventHandling in BaseClass as property ???
-        # send StatusChangingWorkflowEvent
-        #print "send StatusChangingWorkflowEvent(old:%s, new:%s) here" \
-        #      % (self._status, trans.destinationState)
+        # Send an event before the transition occurs.
+        publish(self, BeforeTransitionEvent(obj, self, transition))
 
         # change status
-        self._status = trans.destinationState
+        self._status = transition.destinationState
 
-        # send StatusChangedWorkflowEvent
-        #print "send StatusChangedWorkflowEvent(old:%s, new:%s) here" \
-        #      % (trans.sourceState, self._status)
+        # Send an event after the transition occured.
+        publish(self, AfterTransitionEvent(obj, self, transition))
 
-
-        # check for automatic transitions
+        # check for automatic transitions and fire them if necessary
         self._checkAndFireAuto(clean_pd)
     fireTransition = ContextMethod(fireTransition)
 
-    #
-    ############################################################
-
-    # XXX expose this method in the interface (without _) ???
-    def _getProcessDefinition(self):
-        """Get the ProcessDefinition object from WorkflowService.
-        """
+    def getProcessDefinition(self):
+        """Get the ProcessDefinition object from WorkflowService."""
         svc =  getService(self, "Workflows")
         return svc.getProcessDefinition(self.processDefinitionName)
-    _getProcessDefinition = ContextMethod(_getProcessDefinition)
+    getProcessDefinition = ContextMethod(getProcessDefinition)
 
     # XXX this is not entirely tested
     def _getContext(self):
@@ -207,7 +211,7 @@
 
         #content = getWrapperContainer(self)
 
-        # XXX How can i make shure that nobody modifies content
+        # XXX How can i make sure that nobody modifies content
         # while the condition scripts/conditions are evaluated ????
         # this hack only prevents from directly setting an attribute
         # using a setter-method directly is not protected :((
@@ -231,34 +235,23 @@
         return ctx
 
     def _evaluateCondition(self, transition, contexts):
-        """Evaluate a condition in context of relevant-data.
-        """
+        """Evaluate a condition in context of relevant-data."""
         if not transition.condition:
             return True
         expr = Engine.compile(transition.condition)
-        return expr(Engine.getContext( contexts=contexts ))
+        return expr(Engine.getContext(contexts=contexts))
 
     def _evaluateScript(self, transition, contexts):
+        """Evaluate a script in context of relevant-data."""
         script = transition.script
         if not script:
             return True
-        if type(script) in StringTypes:
+        if isinstance(script, (str, unicode)):
             sm = getServiceManager(self)
-            script =  sm.resolve(script)
+            script = sm.resolve(script)
         return script(contexts)
     _evaluateScript = ContextMethod(_evaluateScript)
 
-    def _buildRelevantData(self, schema):
-        """Create a new data object and initialize with Schema defaults.
-        """
-        data = RelevantData()
-        if schema is not None:
-            # set schema to RelevantData Instance
-            directlyProvides(data, schema)
-            for name, field in getFields(schema).items():
-                setattr(data, name, field.default)
-        return data
-
     def _outgoingTransitions(self, clean_pd):
         sm = getSecurityManager()
         ret = []
@@ -302,8 +295,7 @@
         outgoing_transitions = self.getOutgoingTransitions()
         for name in outgoing_transitions:
             trans = clean_pd.transitions[name]
-            # XXX Use Constants instead of strings
-            if trans.triggerMode == 'Automatic':
+            if trans.triggerMode == AUTOMATIC:
                 self.fireTransition(name)
                 return
     _checkAndFireAuto = ContextMethod(_checkAndFireAuto)