[Erp5-report] r15810 - /erp5/trunk/products/ERP5Type/Base.py

nobody at svn.erp5.org nobody at svn.erp5.org
Mon Aug 27 13:59:42 CEST 2007


Author: jp
Date: Mon Aug 27 13:59:42 2007
New Revision: 15810

URL: http://svn.erp5.org?rev=15810&view=rev
Log:
Remove objectCount method. Improved layout selection in Web mode. New WorkflowMethod implementation. This new implementation solves most issues which existed previously and related to multiple workflows for a single workflow method ID. It should also be faster but will require some more work so that changes in workflow definition are reflected automatically in workflow methods registration.

Modified:
    erp5/trunk/products/ERP5Type/Base.py

Modified: erp5/trunk/products/ERP5Type/Base.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5Type/Base.py?rev=15810&r1=15809&r2=15810&view=diff
==============================================================================
--- erp5/trunk/products/ERP5Type/Base.py (original)
+++ erp5/trunk/products/ERP5Type/Base.py Mon Aug 27 13:59:42 2007
@@ -40,6 +40,7 @@
 from Products.CMFCore.PortalContent import PortalContent
 from Products.CMFCore.Expression import Expression
 from Products.CMFCore.utils import getToolByName, _getViewFor
+from Products.CMFCore.WorkflowCore import ObjectDeleted, ObjectMoved
 
 from Products.DCWorkflow.Transitions import TRIGGER_WORKFLOW_METHOD, TRIGGER_USER_ACTION
 
@@ -49,11 +50,11 @@
 from Products.ERP5Type.Utils import UpperCase
 from Products.ERP5Type.Utils import convertToUpperCase, convertToMixedCase
 from Products.ERP5Type.Utils import createExpressionContext
+from Products.ERP5Type.Accessor.Accessor import Accessor
 from Products.ERP5Type.Accessor.TypeDefinition import list_types
 from Products.ERP5Type.Accessor import Base as BaseAccessor
 from Products.ERP5Type.XMLExportImport import Base_asXML
 from Products.ERP5Type.Cache import CachingMethod, clearCache, getReadOnlyTransactionCache
-from Products.CMFCore.WorkflowCore import ObjectDeleted
 from Accessor import WorkflowState
 from Products.ERP5Type.Log import log as unrestrictedLog
 from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
@@ -67,7 +68,7 @@
 from Products.ERP5Type.Accessor.TypeDefinition import asDate
 
 from string import join
-import sys
+import sys, re
 from Products.ERP5Type.PsycoWrapper import psyco
 
 from cStringIO import StringIO
@@ -82,13 +83,29 @@
 
 _MARKER=[]
 
+wildcard_interaction_method_id_matcher = re.compile(".*[\+\*].*")
+
 class WorkflowMethod(Method):
 
-  def __init__(self, method, id=None, reindex=1):
+  def __init__(self, method, id=None, reindex=1, interaction_id=None):
     self._m = method
     if id is None:
       id = method.__name__
     self._id = id
+    self._interaction_id = interaction_id
+    # Only publishable methods can be published as interactions
+    # A pure private method (ex. _doNothing) can not be published
+    # This is intentional to prevent methods such as submit, share to 
+    # be called from a URL. If someone can show that this way
+    # is wrong (ex. for remote operation of a site), let us know.
+    if not method.__name__.startswith('_'):
+      self.__name__ = id
+      for func_id in ['func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']:
+        setattr(self, func_id, getattr(method, func_id, None))
+    self._invoke_once = {}
+    self._invoke_always = {} # Store in a dict all workflow IDs which require to
+                                # invoke wrapWorkflowMethod at every call
+                                # during the same transaction
 
   def _setId(self, id) :
     self._id = id
@@ -97,20 +114,78 @@
     """
       Invoke the wrapped method, and deal with the results.
     """
+    if self._id in ('getPhysicalPath', 'getId'):
+      # To prevent infinite recursion, 2 methods must have special treatment
+      # this is clearly not the best way to implement this but it is 
+      # already better than what we had. I (JPS) would prefer to use
+      # critical sections in this part of the code and a
+      # thread variable which tells in which semantic context the code
+      # should ne executed. - XXX
+      return apply(self._m, (instance,) + args, kw) 
+
+    call_method = 0 # By default, this method was never called
+    if self._invoke_once:
+      # Check if this method has already been called in this transaction
+      # (only check this if we use once only workflow methods)
+      call_method_key = ('Products.ERP5Type.Base.WorkflowMethod.__call__', self._id, instance.getPhysicalPath())
+      transactional_variable = getTransactionalVariable(call_method_key)
+      try:
+        call_method = transactional_variable['call_method_key']
+      except KeyError:
+        transactional_variable['call_method_dict'] = 1
+
+    if call_method and not self._invoke_always:
+      # Try to return immediately if there are no invoke always workflow methods
+      return apply(self.__dict__['_m'], (instance,) + args, kw)
+
+    # Invoke transitions on appropriate workflow
+    if call_method:
+      candidate_transition_item_list = self._invoke_always.items()
+    else:
+      candidate_transition_item_list = self._invoke_always.items() + self._invoke_once.items()
+
+    # New implementation does not use any longer wrapWorkflowMethod
     wf = getToolByName(instance, 'portal_workflow', None)
-    if wf is None or not hasattr(wf, 'wrapWorkflowMethod'):
-      # No workflow tool found.
+
+    # Call whatever must be called before changing states
+    after_invoke_once = {}
+    for wf_id, transition_list in candidate_transition_item_list:
+      after_invoke_once[wf_id] = wf[wf_id].notifyBefore(instance, self._id,
+                          args=args, kw=kw, transition_list=transition_list)
+
+    # Compute expected result
+    result = apply(self.__dict__['_m'], (instance,) + args, kw)
+
+    # Change the state of statefull workflows
+    for wf_id, transition_list in candidate_transition_item_list:
       try:
-        res = apply(self._m, (instance,) + args, kw)
-      except ObjectDeleted, ex:
-        res = ex.getResult()
-      else:
-        if getattr(aq_base(instance), 'reindexObject', None) is not None:
-          instance.reindexObject()
-    else:
-      res = wf.wrapWorkflowMethod(instance, self._id, self.__dict__['_m'],
-                                  (instance,) + args, kw)
-    return res
+        wf[wf_id].notifyWorkflowMethod(instance, self._id, args=args, kw=kw, transition_list=transition_list)
+      except ObjectDeleted:
+        # Re-raise with a different result.
+        raise ObjectDeleted(result)
+      except ObjectMoved, ex:
+        # Re-raise with a different result.
+        raise ObjectMoved(ex.getNewObject(), result)
+
+    # Call whatever must be called after changing states
+    for wf_id, transition_list in candidate_transition_item_list:
+      wf[wf_id].notifySuccess(instance, self._id, result, args=args, kw=kw, transition_list=transition_list)
+
+    # Return result finally
+    return result
+
+  def registerTransitionAlways(self, workflow_id, transition_id):
+    """
+      Transitions registered as always will be invoked always
+    """
+    self._invoke_always.setdefault(workflow_id, []).append(transition_id)
+
+  def registerTransitionOncePerTransaction(self, workflow_id, transition_id):
+    """
+      Transitions registered as one per transactions will be invoked 
+      only once per transaction
+    """
+    self._invoke_once.setdefault(workflow_id, []).append(transition_id)
 
 class ActionMethod(Method):
 
@@ -137,8 +212,94 @@
 
 class PropertyHolder:
   isRADContent = 1
+
   def __init__(self):
     self.__name__ = 'PropertyHolder'
+
+  def _getItemList(self):
+    return self.__dict__.items()
+
+  def getAccessorMethodItemList(self):
+    """
+    Return a list of tuple (id, method) for every accessor
+    """
+    return filter(lambda x: isinstance(x[1], Accessor), self._getItemList())
+
+  def getAccessorMethodIdList(self):
+    """
+    Return the list of accessor IDs
+    """
+    return map(lambda x: x[0], self.getAccessorMethodItemList())
+
+  def getWorkflowMethodItemList(self):
+    """
+    Return a list of tuple (id, method) for every workflow method
+    """
+    return filter(lambda x: isinstance(x[1], WorkflowMethod), self._getItemList())
+
+  def getWorkflowMethodIdList(self):
+    """
+    Return the list of workflow method IDs
+    """
+    return map(lambda x: x[0], self.getWorkflowMethodItemList())
+
+  def getActionMethodItemList(self):
+    """
+    Return a list of tuple (id, method) for every workflow action method
+    """
+    return filter(lambda x: isinstance(x[1], ActionMethod), self._getItemList())
+
+  def getActionMethodIdList(self):
+    """
+    Return the list of workflow action method IDs
+    """
+    return map(lambda x: x[0], self.getActionMethodItemList())
+
+  def _getClassDict(self, klass, inherited=1, local=1):
+    """
+    Return a dict for every property of a class
+    """
+    result = {}
+    if inherited:
+      base_list = list(klass.__bases__)
+      base_list.reverse()
+      for klass in base_list:
+        result.update(self._getClassDict(klass, inherited=1, local=1))
+    if local:
+      result.update(klass.__dict__)
+    return result
+
+  def _getClassItemList(self, klass, inherited=1, local=1):
+    """
+    Return a list of tuple (id, method) for every property of a class
+    """
+    return self._getClassDict(klass, inherited=inherited, local=local).items()
+
+  def getClassMethodItemList(self, klass, inherited=1, local=1):
+    """
+    Return a list of tuple (id, method) for every class method
+    """
+    return filter(lambda x: callable(x[1]) and not isinstance(x[1], Method), 
+                  self._getClassItemList(klass, inherited=inherited, local=local))
+
+  def getClassMethodIdList(self, klass, inherited=1, local=1):
+    """
+    Return the list of class method IDs
+    """
+    return map(lambda x: x[0], self.getClassMethodItemList(klass, inherited=inherited, local=local))
+
+  def getClassPropertyItemList(self, klass, inherited=1, local=1):
+    """
+    Return a list of tuple (id, method) for every class method
+    """
+    return filter(lambda x: not callable(x[1]), 
+                  self._getClassItemList(klass, inherited=inherited, local=local))
+
+  def getClassPropertyIdList(self, klass, inherited=1, local=1):
+    """
+    Return the list of class method IDs
+    """
+    return map(lambda x: repr(x[0]), self.getClassPropertyItemList(klass, inherited=inherited, local=local))
 
 def getClassPropertyList(klass):
   ps_list = getattr(klass, 'property_sheets', ())
@@ -291,8 +452,8 @@
             else:
               work_method_holder = klass
             LOG('initializePortalTypeDynamicWorkflowMethods', 100,
-                  'WARNING! Can not initialize %s on %s' % \
-                    (method_id, str(work_method_holder)))
+                  'WARNING! Can not initialize %s on %s due to existing %s' % \
+                    (method_id, str(work_method_holder), method))
         elif tdef.trigger_type == TRIGGER_WORKFLOW_METHOD:
           method_id = convertToMixedCase(tr_id)
           # We have to make a difference between a method which is on
@@ -301,6 +462,7 @@
           if (not hasattr(prop_holder, method_id)) and \
              (not hasattr(klass, method_id)):
             method = WorkflowMethod(klass._doNothing, tr_id)
+            method.registerTransitionAlways(wf_id, tr_id)
             # Attach to portal_type
             setattr(prop_holder, method_id, method)
             prop_holder.security.declareProtected(
@@ -318,10 +480,11 @@
             # Wrap method
             if callable(method):
               if not isinstance(method, WorkflowMethod):
-                setattr(work_method_holder, method_id,
-                        WorkflowMethod(method, method_id))
+                method = WorkflowMethod(method, method_id)
+                method.registerTransitionAlways(wf_id, tr_id)
+                setattr(work_method_holder, method_id, method)
               else :
-                # some methods (eg. set_ready) doesn't have the same name
+                # some methods (eg. set_ready) don't have the same name
                 # (setReady) as the workflow transition (set_ready).
                 # If they are associated with both an InteractionWorkflow
                 # and a DCWorkflow, and the WorkflowMethod is created for
@@ -329,42 +492,79 @@
                 # a wrong transition name (setReady).
                 # Here we force it's id to be the transition name (set_ready).
                 method._setId(tr_id)
+                method.registerTransitionAlways(wf_id, tr_id)
             else:
               LOG('initializePortalTypeDynamicWorkflowMethods', 100,
                   'WARNING! Can not initialize %s on %s' % \
                     (method_id, str(work_method_holder)))
-    # XXX This part is (more or less...) a copy and paste
-    elif wf.__class__.__name__ in ('InteractionWorkflowDefinition', ):
+  # XXX This part is (more or less...) a copy and paste
+  # We need to run this part twice in order to handle interactions of interactions
+  for wf in portal_workflow.getWorkflowsFor(self) * 2:
+    wf_id = wf.id
+    if wf.__class__.__name__ in ('InteractionWorkflowDefinition', ):
       for tr_id in wf.interactions.objectIds():
         tdef = wf.interactions.get(tr_id, None)
         if tdef.trigger_type == TRIGGER_WORKFLOW_METHOD:
+          # XXX Prefiltering per portal type would be more efficient
           for imethod_id in tdef.method_id:
-            method_id = imethod_id
-            if (not hasattr(prop_holder, method_id)) and \
-               (not hasattr(klass,method_id)):
-              method = WorkflowMethod(klass._doNothing, imethod_id)
-              # Attach to portal_type
-              setattr(prop_holder, method_id, method)
-              prop_holder.security.declareProtected(
-                                      Permissions.AccessContentsInformation,
-                                      method_id)
+            if bool(wildcard_interaction_method_id_matcher.match(imethod_id)):
+              # Interactions workflows can use regexp based wildcard methods
+              method_id_matcher = re.compile(imethod_id) # XXX What happens if exception ?
+              method_id_list = prop_holder.getAccessorMethodIdList() + prop_holder.getWorkflowMethodIdList()\
+                             + prop_holder.getClassMethodIdList(klass)
+                # XXX - class stuff is missing here
+              method_id_list = filter(lambda x: method_id_matcher.match(x), method_id_list)
             else:
-              # Wrap method into WorkflowMethod is needed
-              if getattr(klass, method_id, None) is not None:
-                method = getattr(klass, method_id)
-                if callable(method):
-                  if not isinstance(method, WorkflowMethod):
-                    method = WorkflowMethod(method, method_id)
-                    # We must set the method on the klass
-                    # because klass has priority in lookup over
-                    # _ac_dynamic
-                    setattr(klass, method_id, method)
+              # Single method
+              method_id_list = [imethod_id]
+            for method_id in method_id_list:
+              if (not hasattr(prop_holder, method_id)) and \
+                (not hasattr(klass,method_id)):
+                method = WorkflowMethod(klass._doNothing, imethod_id)
+                if not tdef.once_per_transaction:
+                  method.registerTransitionAlways(wf_id, tr_id)
+                else:
+                  method.registerTransitionOncePerTransaction(wf_id, tr_id)
+                # Attach to portal_type
+                setattr(prop_holder, method_id, method)
+                prop_holder.security.declareProtected(
+                                        Permissions.AccessContentsInformation,
+                                        method_id)
               else:
-                method = getattr(prop_holder, method_id)
-                if callable(method):
-                  if not isinstance(method, WorkflowMethod):
-                    method = WorkflowMethod(method, method_id)
-                    setattr(prop_holder, method_id, method)
+                # Wrap method into WorkflowMethod is needed
+                if getattr(klass, method_id, None) is not None:
+                  method = getattr(klass, method_id)
+                  if callable(method):
+                    if not isinstance(method, WorkflowMethod):
+                      method = WorkflowMethod(method, method_id)
+                      if not tdef.once_per_transaction:
+                        method.registerTransitionAlways(wf_id, tr_id)
+                      else:
+                        method.registerTransitionOncePerTransaction(wf_id, tr_id)
+                      # We must set the method on the klass
+                      # because klass has priority in lookup over
+                      # _ac_dynamic
+                      setattr(klass, method_id, method)
+                    else:
+                      if not tdef.once_per_transaction:
+                        method.registerTransitionAlways(wf_id, tr_id)
+                      else:
+                        method.registerTransitionOncePerTransaction(wf_id, tr_id)
+                else:
+                  method = getattr(prop_holder, method_id)
+                  if callable(method):
+                    if not isinstance(method, WorkflowMethod):
+                      method = WorkflowMethod(method, method_id)
+                      if not tdef.once_per_transaction:
+                        method.registerTransitionAlways(wf_id, tr_id)
+                      else:
+                        method.registerTransitionOncePerTransaction(wf_id, tr_id)
+                      setattr(prop_holder, method_id, method)
+                    else:
+                      if not tdef.once_per_transaction:
+                        method.registerTransitionAlways(wf_id, tr_id)
+                      else:
+                        method.registerTransitionOncePerTransaction(wf_id, tr_id)
 
 class Base( CopyContainer,
             PortalContent,
@@ -470,6 +670,9 @@
               pformat(rev2.__dict__)))
 
   def _aq_dynamic(self, id):
+    # _aq_dynamic has been created so that callable objects
+    # and default properties can be associated per portal type
+    # and per class. Other uses are possible (ex. WebSection).
     ptype = self.portal_type
 
     #LOG('_aq_dynamic', 0, 'self = %r, id = %r, ptype = %r' % (self, id, ptype))
@@ -2115,7 +2318,7 @@
         context = klass(self.getId())
       except TypeError:
         # If __init__ does not take the id argument, the class is probably
-        # a tool, and the id is fixed.
+        # a tool, and the id is predifined and immutable.
         context = klass()
       context.__dict__.update(self.__dict__)
       # Copy REQUEST properties to self
@@ -2131,7 +2334,7 @@
       # Make it a temp content
       temp_object = TempBase(self.getId())
       for k in ('isIndexable', 'reindexObject', 'recursiveReindexObject', 'activate', 'setUid', ):
-        setattr(context, k, getattr(temp_object,k))
+        setattr(context, k, getattr(temp_object, k))
       # Return result
       return context.__of__(self.aq_parent)
     else:
@@ -2164,12 +2367,6 @@
 
   security.declareProtected(Permissions.AccessContentsInformation,
                             'objectCount')
-  def objectCount(self):
-    """
-      Returns number of objects
-    """
-    return len(self.objectIds())
-
   # Hide Acquisition to prevent loops (ex. in cells)
   # Another approach is to use XMLObject everywhere
   # DIRTY TRICK XXX
@@ -2184,7 +2381,6 @@
 #
 #   def contentIds(self, *args, **kw):
 #     return []
-
 
   security.declarePublic('immediateReindexObject')
   def immediateReindexObject(self, *args, **kw):
@@ -2573,14 +2769,20 @@
   security.declareProtected(Permissions.AccessContentsInformation, 'getApplicableLayout')
   def getApplicableLayout(self):
     """
-      The application layout of a standard document in the content layout.
+      The applicable layout of a standard document in the content layout.
 
       However, if we are displaying a Web Section as its default document,
       we should use the container layout.
     """
     try:
+      # Default documents should be displayed in the layout of the container
       if self.REQUEST.get('is_web_section_default_document', None):
         return self.REQUEST.get('current_web_section').getContainerLayout()
+      # ERP5 Modules should be displayed as containers
+      # XXX - this shows that what is probably needed is a more sophisticated
+      # mapping system between contents and layouts.
+      if self.getParentValue().meta_type == 'ERP5 Site':
+        return self.getContainerLayout()
       return self.getContentLayout() or self.getContainerLayout()
     except AttributeError:
       return None
@@ -2745,6 +2947,10 @@
          PortalType, and are browsed a second time to be able to group them
          by property or category.
     """
+    # New implementation of DocumentationHelper (ongoing work)
+    #from DocumentationHelper import PortalTypeInstanceDocumentationHelper
+    #return PortalTypeInstanceDocumentationHelper(self.getRelativeUrl()).__of__(self)
+
     if item_id is None:
       documented_item = self
       item_id = documented_item.getTitle()




More information about the Erp5-report mailing list