From c9662b1e10d011bb23f0c25906a47d69961a37c8 Mon Sep 17 00:00:00 2001
From: Alexandre Boeglin <alex@nexedi.com>
Date: Thu, 22 Nov 2007 17:47:28 +0000
Subject: [PATCH] * _isTreeDelivered method moved from Rule to AppliedRule and 
  SimulationMovement * added transactional variable based cache to
 _isTreeDelivered * activate and flush _isTreeDelivered cache in
 SimulationMovement.expand and   AppliedRule.expand (so that it's only active
 when calling expand, and   flushed afterwards, to prevent issues if we call
 expand in the same   transaction as a builder) * modified
 SimulationMovement.expand behaviour, so that it removes applied   rules that
 no longer test(), and add new ones, if no movment in the applied   rule sub
 tree is linked to a delivery

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@17756 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5/Document/AppliedRule.py        |  49 ++++++++
 product/ERP5/Document/DeliveryRule.py       |   4 +-
 product/ERP5/Document/OrderRule.py          |   2 +-
 product/ERP5/Document/Rule.py               |  17 +--
 product/ERP5/Document/SimulationMovement.py | 119 +++++++++++++++-----
 5 files changed, 142 insertions(+), 49 deletions(-)

diff --git a/product/ERP5/Document/AppliedRule.py b/product/ERP5/Document/AppliedRule.py
index 202d04e8db..6cf7ee7bfc 100644
--- a/product/ERP5/Document/AppliedRule.py
+++ b/product/ERP5/Document/AppliedRule.py
@@ -31,9 +31,13 @@ from Products.CMFCore.utils import getToolByName
 from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
 from Products.ERP5Type.XMLObject import XMLObject
 from Products.ERP5Type.PsycoWrapper import psyco
+from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
 
 from zLOG import LOG
 
+TREE_DELIVERED_CACHE_KEY = 'AppliedRule._isTreeDelivered_cache'
+TREE_DELIVERED_CACHE_ENABLED = 'TREE_DELIVERED_CACHE_ENABLED'
+
 class AppliedRule(XMLObject):
     """
       An applied rule holds a list of simulation movements.
@@ -89,6 +93,14 @@ class AppliedRule(XMLObject):
         An applied rule can be expanded only if its parent movement
         is expanded.
       """
+      tv = getTransactionalVariable(self)
+      cache = tv.setdefault(TREE_DELIVERED_CACHE_KEY, {})
+      cache_enabled = cache.get(TREE_DELIVERED_CACHE_ENABLED, 0)
+
+      # enable cache
+      if not cache_enabled:
+        cache[TREE_DELIVERED_CACHE_ENABLED] = 1
+
       rule = self.getSpecialiseValue()
       if rule is not None:
         if self.isRootAppliedRule():
@@ -102,6 +114,13 @@ class AppliedRule(XMLObject):
 #                                 "recursiveImmediateReindexObject"]).\
 #                                   notifySimulationChange(rule._v_notify_dict)
 
+      # disable and clear cache
+      if not cache_enabled:
+        try:
+          del tv[TREE_DELIVERED_CACHE_KEY]
+        except KeyError:
+          pass
+
     security.declareProtected(Permissions.ModifyPortalContent, 'solve')
     def solve(self, solution_list):
       """
@@ -193,3 +212,33 @@ class AppliedRule(XMLObject):
                delivery_url)
         else:
           delivery_value.notifySimulationChange()
+
+    def _isTreeDelivered(self):
+      """
+      Checks if submovements of this applied rule (going down the complete
+      simulation tree) have a delivery relation.
+      Returns True if at least one is delivered, False if none of them are.
+
+      see SimulationMovement._isTreeDelivered
+      """
+      tv = getTransactionalVariable(self)
+      cache = tv.setdefault(TREE_DELIVERED_CACHE_KEY, {})
+      cache_enabled = cache.get(TREE_DELIVERED_CACHE_ENABLED, 0)
+
+      def getTreeDelivered(applied_rule):
+        for movement in applied_rule.objectValues():
+          if movement._isTreeDelivered():
+            return True
+        return False
+
+      rule_key = self.getRelativeUrl()
+      if cache_enabled:
+        try:
+          return cache[rule_key]
+        except:
+          result = getTreeDelivered(self)
+          cache[rule_key] = result
+          return result
+      else:
+        return getTreeDelivered(self)
+
diff --git a/product/ERP5/Document/DeliveryRule.py b/product/ERP5/Document/DeliveryRule.py
index dc47a06f22..156c2bdb13 100644
--- a/product/ERP5/Document/DeliveryRule.py
+++ b/product/ERP5/Document/DeliveryRule.py
@@ -85,7 +85,7 @@ class DeliveryRule(Rule):
             (delivery.getPortalDraftOrderStateList() + \
             delivery.getPortalPlannedOrderStateList()):
           movement_delivery = movement.getDeliveryValue()
-          if not self._isTreeDelivered([movement], ignore_first=1) and \
+          if not movement._isTreeDelivered(ignore_first=1) and \
               movement_delivery not in delivery_movement_list:
             applied_rule._delObject(movement.getId())
           else:
@@ -103,7 +103,7 @@ class DeliveryRule(Rule):
             # We are on a line
             new_id = deliv_mvt.getId()
           else:
-            # Weare on a cell
+            # We are on a cell
             new_id = "%s_%s" % (deliv_mvt.getParentId(), deliv_mvt.getId())
           # Generate the simulation deliv_mvt
           # XXX Hardcoded value
diff --git a/product/ERP5/Document/OrderRule.py b/product/ERP5/Document/OrderRule.py
index 0700000439..fefa9e0ad2 100644
--- a/product/ERP5/Document/OrderRule.py
+++ b/product/ERP5/Document/OrderRule.py
@@ -86,7 +86,7 @@ class OrderRule(DeliveryRule):
             order.getPortalReservedInventoryStateList() and
             not movement.getLastExpandSimulationState() in
             order.getPortalCurrentInventoryStateList()) and \
-            not self._isTreeDelivered([movement]):
+            not movement._isTreeDelivered():
 
           movement_order = movement.getOrderValue()
           if movement_order in order_movement_list:
diff --git a/product/ERP5/Document/Rule.py b/product/ERP5/Document/Rule.py
index 6a21ab0e45..dffabc5561 100644
--- a/product/ERP5/Document/Rule.py
+++ b/product/ERP5/Document/Rule.py
@@ -220,21 +220,6 @@ class Rule(Predicate, XMLObject):
     return 1
 
 #### Helpers
-  def _isTreeDelivered(self, movement_list, ignore_first=0):
-    """
-    returns 1 if the movement or any of its child is linked to a delivery
-    """
-    child_movement_list = []
-    for movement in movement_list:
-      if not ignore_first and len(movement.getDeliveryList()) > 0:
-        return 1
-      else:
-        for applied_rule in movement.objectValues():
-          child_movement_list = applied_rule.objectValues()
-    if len(child_movement_list) == 0:
-      return 0
-    return self._isTreeDelivered(child_movement_list)
-
   def _getCurrentMovementList(self, applied_rule, **kw):
     """
     Returns the list of current children of the applied rule, sorted in 3
@@ -263,7 +248,7 @@ class Rule(Predicate, XMLObject):
       if movement.isFrozen():
         immutable_movement_list.append(movement)
       else:
-        if self._isTreeDelivered([movement]):
+        if movement._isTreeDelivered():
           mutable_movement_list.append(movement)
         else:
           deletable_movement_list.append(movement)
diff --git a/product/ERP5/Document/SimulationMovement.py b/product/ERP5/Document/SimulationMovement.py
index a851c8647c..2c1c9ccd65 100644
--- a/product/ERP5/Document/SimulationMovement.py
+++ b/product/ERP5/Document/SimulationMovement.py
@@ -31,6 +31,7 @@ from AccessControl import ClassSecurityInfo
 from Products.CMFCore.utils import getToolByName
 
 from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
+from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
 
 from Products.ERP5.Document.Movement import Movement
 
@@ -38,6 +39,8 @@ from zLOG import LOG
 
 from Acquisition import aq_base
 
+from AppliedRule import TREE_DELIVERED_CACHE_KEY, TREE_DELIVERED_CACHE_ENABLED
+
 # XXX Do we need to create groups ? (ie. confirm group include confirmed, getting_ready and ready
 
 parent_to_movement_simulation_state = {
@@ -209,36 +212,57 @@ class SimulationMovement(Movement):
   security.declareProtected(Permissions.ModifyPortalContent, 'expand')
   def expand(self, force=0, **kw):
     """
-      Parses all existing applied rules and make sure they apply.
-      Checks other possible rules and starts expansion process
-      (instanciates rule and calls expand on rule)
-
-      Only movements which applied rule parent is expanded can
-      be expanded.
-    """
-    # XXX Default behaviour is not to expand if it has already been
-    # expanded, but some rules are configuration rules and need to be
-    # reexpanded  each time, because the rule apply only if predicates
-    # are true, then this kind of rule must always be tested. Currently,
-    # we know that invoicing rule acts like this, and that it comes after
-    # invoice or invoicing_rule, so we if we come from invoince rule or
-    # invoicing rule, we always expand regardless of the causality state.
-    if ((self.getParentValue().getSpecialiseReference() not in
-         ('default_invoicing_rule', 'default_invoice_rule')
-         and self.getCausalityState() == 'expanded' ) or \
-         len(self.objectIds()) != 0):
-      # Reexpand
-      for my_applied_rule in self.objectValues():
-        my_applied_rule.expand(force=force,**kw)
-    else:
-      portal_rules = getToolByName(self, 'portal_rules')
-      # Parse each rule and test if it applies
-      for rule in portal_rules.searchRuleList(self):
-        rule.constructNewAppliedRule(self, **kw)
-      for my_applied_rule in self.objectValues() :
-        my_applied_rule.expand(force=force,**kw)
-      # Set to expanded
-      self.setCausalityState('expanded')
+    Checks all existing applied rules and make sure they still apply.
+    Checks for other possible rules and starts expansion process (instanciates
+    applied rules and calls expand on them).
+
+    First get all applicable rules,
+    then, delete all applied rules that no longer match and are not linked to
+    a delivery,
+    finally, apply new rules if no rule with the same type is already applied.
+    """
+    portal_rules = getToolByName(self, 'portal_rules')
+
+    tv = getTransactionalVariable(self)
+    cache = tv.setdefault(TREE_DELIVERED_CACHE_KEY, {})
+    cache_enabled = cache.get(TREE_DELIVERED_CACHE_ENABLED, 0)
+
+    # enable cache
+    if not cache_enabled:
+      cache[TREE_DELIVERED_CACHE_ENABLED] = 1
+
+    applied_rule_dict = {}
+    applicable_rule_dict = {}
+    for rule in portal_rules.searchRuleList(self, sort_on='version',
+        sort_order='descending'):
+      ref = rule.getReference()
+      if ref and ref not in applicable_rule_dict.iterkeys():
+        applicable_rule_dict[ref] = rule
+
+    for applied_rule in self.objectValues():
+      rule = applied_rule.getSpecialiseValue()
+      if not applied_rule._isTreeDelivered() and not rule.test(self):
+        self._delObject(applied_rule.getId())
+      else:
+        applied_rule_dict[rule.getPortalType()] = applied_rule
+
+    for rule in applicable_rule_dict.itervalues():
+      rule_type = rule.getPortalType()
+      if rule_type not in applied_rule_dict.iterkeys():
+        applied_rule = rule.constructNewAppliedRule(self, **kw)
+        applied_rule_dict[rule_type] = applied_rule
+
+    self.setCausalityState('expanded')
+    # expand
+    for applied_rule in applied_rule_dict.itervalues():
+      applied_rule.expand(force=force, **kw)
+
+    # disable and clear cache
+    if not cache_enabled:
+      try:
+        del tv[TREE_DELIVERED_CACHE_KEY]
+      except KeyError:
+        pass
 
   security.declareProtected(Permissions.ModifyPortalContent, 'diverge')
   def diverge(self):
@@ -480,3 +504,38 @@ class SimulationMovement(Movement):
   #                                         'recursiveImmediateReindexObject']))
   #    activity.edit()
 
+  def _isTreeDelivered(self, ignore_first=0):
+    """
+    checks if subapplied rules  of this movement (going down the complete
+    simulation tree) have a child with a delivery relation.
+    Returns True if at least one is delivered, False if none of them are.
+
+    see AppliedRule._isTreeDelivered
+    """
+    tv = getTransactionalVariable(self)
+    cache = tv.setdefault(TREE_DELIVERED_CACHE_KEY, {})
+    cache_enabled = cache.get(TREE_DELIVERED_CACHE_ENABLED, 0)
+
+    def getTreeDelivered(movement, ignore_first=0):
+      if ignore_first:
+        if len(movement.getDeliveryList()) > 0:
+          return True
+      for applied_rule in movement.objectValues():
+        if applied_rule._isTreeDelivered():
+          return True
+      return False
+
+    if ignore_first:
+      rule_key = (self.getRelativeUrl(), 1)
+    else:
+      rule_key = self.getRelativeUrl()
+    if cache_enabled:
+      try:
+        return cache[rule_key]
+      except:
+        result = getTreeDelivered(self, ignore_first=ignore_first)
+        cache[rule_key] = result
+        return result
+    else:
+      return getTreeDelivered(self, ignore_first=ignore_first)
+
-- 
2.30.9