From c4b05340c1d51b90411d5df951d0571775b5fc1f Mon Sep 17 00:00:00 2001
From: Jean-Paul Smets <jp@nexedi.com>
Date: Mon, 8 Jan 2007 09:40:55 +0000
Subject: [PATCH] Refactored ERP5 Web classes with abstract API.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@11916 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5/Document/WebSection.py   | 277 ++++++++++++++++++++++++--
 product/ERP5/Document/WebSite.py      | 165 ++-------------
 product/ERP5/PropertySheet/WebSite.py |  14 ++
 3 files changed, 287 insertions(+), 169 deletions(-)

diff --git a/product/ERP5/Document/WebSection.py b/product/ERP5/Document/WebSection.py
index a6cbff46f5..9c3db85fb9 100644
--- a/product/ERP5/Document/WebSection.py
+++ b/product/ERP5/Document/WebSection.py
@@ -38,14 +38,64 @@ from Globals import get_request
 
 from zLOG import LOG
 
-from Products.ERP5.Document.WebSite import reserved_name_dict, reserved_name_dict_init
-from Products.ERP5.Document.WebSite import CACHE_KEY, WEBSITE_USER, WEBSECTION_KEY, DOCUMENT_NAME_KEY
+from Products.ERP5Type.Cache import getReadOnlyTransactionCache
 
+# Global keys used for URL generation
+WEBSECTION_KEY = 'web_section_value'
+WEBSITE_USER = 'web_site_user'
+
+Domain_getattr = Domain.inheritedAttribute('__getattr__')
+
+# We use a request key (CACHE_KEY) to store access attributes and prevent infinite recursion
+# We define a couple of reserved names for which we are not
+# going to try to do acquisition
+CACHE_KEY = 'web_site_aq_cache'
+DOCUMENT_NAME_KEY = 'web_section_document_name'
+reserved_name_dict = { 'getApplicableLayout' : 1,
+                       'getLayout' : 1,
+                       'Localizer' : 1,
+                       'field_render' : 1,
+                       'getListItemUrl' : 1,
+                       'getLocalPropertyManager' : 1,
+                       'getOrderedGlobalActionList' : 1,
+                       'allow_discussion' : 1,
+                       'im_func' : 1,
+                       'id' : 1,
+                       'method_id' : 1,
+                       'role_map' : 1,
+                       'func_defaults': 1,  }
+reserved_name_dict_init = 0
 
 class WebSection(Domain):
     """
       A Web Section is a Domain with an extended API intended to
-      support the creation of Web front ends to ERP5 contents.
+      support the creation of Web front ends to
+      server ERP5 contents through a pretty and configurable
+      user interface.
+
+      WebSection uses the following scripts for customisation:
+
+      - WebSection_getBreadcrumbItemList
+
+      - WebSection_getDocumentValueList
+
+      - WebSection_getPermanentURL
+
+      - WebSection_getDocumentValue
+
+      - WebSection_getDefaultDocumentValue
+
+      - WebSection_getSectionValue
+
+      - WebSection_getWebSiteValue
+
+      It defines the following REQUEST global variables:
+
+      - current_web_section
+
+      - current_web_document
+
+      - is_web_section_default_document
     """
     # CMF Type Definition
     meta_type = 'ERP5 Web Section'
@@ -66,31 +116,20 @@ class WebSection(Domain):
                       , PropertySheet.SortIndex
                       )
 
-    # Draft - this is being worked on
-    # Due to some issues in acquisition, the implementation  of getWebSectionValue
-    # through acquisition by containment does not work for URLs
-    # such as web_site_module/a/b/c/web_page_module/d
-    # getWebSectionValue will return web_site_module/a/b instead
-    # of web_site_module/a/b/c
-    #security.declareProtected(Permissions.AccessContentsInformation, 'getWebSectionValue')
-    #def getWebSectionValue(self):
-      #"""
-        #Returns the current web section (ie. self) though containment acquisition
-      #"""
-      #return self
+    web_section_key = WEBSECTION_KEY
 
     def _aq_dynamic(self, name):
       """
         Try to find a suitable document based on the
         web site local naming policies as defined by
-        the WebSite_getDocumentValue script
+        the getDocumentValue method
       """
       global reserved_name_dict_init
       global reserved_name_dict
       request = self.REQUEST
       # Register current web site physical path for later URL generation
-      if not request.has_key(WEBSECTION_KEY):
-        request[WEBSECTION_KEY] = self.getPhysicalPath()
+      if not request.has_key(self.web_section_key):
+        request[self.web_section_key] = self.getPhysicalPath()
         # Normalize web parameter in the request
         # Fix common user mistake and transform '1' string to boolean
         for web_param in ['ignore_layout', 'editable_mode']:
@@ -105,6 +144,7 @@ class WebSection(Domain):
         return dynamic
       # Do some optimisation here for names which can not be names of documents
       if  reserved_name_dict.has_key(name) \
+          or name.endswith('_getDocumentValue') \
           or name.startswith('_') or name.startswith('portal_')\
           or name.startswith('aq_') or name.startswith('selection_') \
           or name.startswith('sort-') or name.startswith('WebSite_') \
@@ -135,7 +175,8 @@ class WebSection(Domain):
         if user is not None:
           old_manager = getSecurityManager()
           newSecurityManager(get_request(), user)
-        document = self.WebSite_getDocumentValue(name=name, portal=portal)
+        LOG('Lookup', 0, str(name))
+        document = self.getDocumentValue(name=name, portal=portal)
         request[CACHE_KEY][name] = document
         if user is not None:
           setSecurityManager(old_manager)
@@ -152,6 +193,198 @@ class WebSection(Domain):
                                               editable_absolute_url=document.absolute_url()))
       return document
 
-    security.declareProtected(Permissions.AccessContentsInformation, 'getWebSiteValue')
-    def getWebSiteValue(self):
-      return self.getParentValue().getWebSiteValue()
+    security.declareProtected(Permissions.AccessContentsInformation, 'getWebSectionValue')
+    def getWebSectionValue(self):
+      """
+        Returns the current web section (ie. self) though containment acquisition.
+
+        To understand the misteries of acquisition and how the rule
+        containment vs. acquisition works, please look at
+        XXXX (Zope web site)
+      """
+      return self
+
+    # Default view display
+    security.declareProtected(Permissions.View, '__call__')
+    def __call__(self):
+      """
+        If a Web Section has a default document, we render
+        the default document instead of rendering the Web Section
+        itself.
+
+        The implementation is based on the presence of specific
+        variables in the REQUEST (besides editable_mode and
+        ignore_layout).
+
+        current_web_section -- defines the Web Section which is
+        used to display the current document.
+
+        current_web_document -- defines the Document (ex. Web Page)
+        which is being displayed within current_web_section.
+
+        is_web_section_default_document -- a boolean which is
+        set each time we display a default document as a section.
+
+        We use REQUEST parameters so that they are reset for every
+        Web transaction and can be accessed from widgets. 
+      """
+      self.REQUEST.set('current_web_section', self)
+      if not self.REQUEST.get('editable_mode') and not self.REQUEST.get('ignore_layout'):
+        document = self.getDefaultDocumentValue()
+        if document is not None:
+          self.REQUEST.set('current_web_document', document)
+          self.REQUEST.set('is_web_section_default_document', 1)
+          return document.__of__(self)()
+      return Domain.__call__(self)
+
+    # Layout Selection API
+    security.declareProtected(Permissions.AccessContentsInformation, 'getApplicableLayout')
+    def getApplicableLayout(self):
+      """
+        The applicable layout on a section is the container layout.
+      """
+      return self.getContainerLayout()
+
+    # WebSection API
+    security.declareProtected(Permissions.View, 'getDocumentValue')
+    def getDocumentValue(self, name=None, portal=None):
+      """
+        Return the default document with the given
+        name. The name parameter may represent anything
+        such as a document reference, an identifier,
+        etc.
+
+        If name is not provided, the method defaults
+        to returning the default document by calling
+        getDefaultDocumentValue.
+
+        This method must be implemented through a
+        portal type dependent script:
+          WebSection_getDocumentValue
+      """
+      if name is None:
+        return self.getDefaultDocumentValue()
+
+      cache = getReadOnlyTransactionCache(self)
+      method = None
+      if cache is not None:
+        key = ('getDocumentValue', self)
+        try:
+          method = cache[key]
+        except KeyError:
+          pass
+
+      if method is None: method = self._getTypeBasedMethod('getDocumentValue',
+                                        fallback_script_id='WebSection_getDocumentValue')
+
+      if cache is not None:
+        if not cache.has_key(key): cache[key] = method
+
+      return method(name, portal=portal)
+
+    security.declareProtected(Permissions.View, 'getDefaultDocumentValue')
+    def getDefaultDocumentValue(self):
+      """
+        Return the default document of the current
+        section.
+
+        This method must be implemented through a
+        portal type dependent script:
+          WebSection_getDefaultDocumentValue
+      """
+      cache = getReadOnlyTransactionCache(self)
+      if cache is not None:
+        key = ('getDefaultDocumentValue', self)
+        try:
+          return cache[key]
+        except KeyError:
+          pass
+
+      result = self._getTypeBasedMethod('getDefaultDocumentValue',
+                     fallback_script_id='WebSection_getDefaultDocumentValue')()
+
+      if cache is not None:
+        cache[key] = result
+
+      return result
+
+    security.declareProtected(Permissions.View, 'getDocumentValueList')
+    def getDocumentValueList(self, **kw):
+      """
+        Return the list of documents which belong to the
+        current section. The API is designed to
+        support additional parameters so that it is possible
+        to group documents by reference, version, language, etc.
+        or to implement filtering of documents.
+
+        This method must be implemented through a
+        portal type dependent script:
+          WebSection_getDocumentValueList
+      """
+      cache = getReadOnlyTransactionCache(self)
+      if cache is not None:
+        key = ('getDocumentValueList', self) + tuple(kw.items())
+        try:
+          return cache[key]
+        except KeyError:
+          pass
+
+      result = self._getTypeBasedMethod('getDocumentValueList',
+                     fallback_script_id='WebSection_getDocumentValueList')(**kw)
+
+      if cache is not None:
+        cache[key] = result
+
+      return result
+
+    security.declareProtected(Permissions.View, 'getPermanentURL')
+    def getPermanentURL(self, document):
+      """
+        Return a permanent URL of document in the context
+        of the current section.
+
+        This method must be implemented through a
+        portal type dependent script:
+          WebSection_getPermanentURL
+      """
+      cache = getReadOnlyTransactionCache(self)
+      if cache is not None:
+        key = ('getDocumentValueList', self, document.getPath())
+        try:
+          return cache[key]
+        except KeyError:
+          pass
+
+      result = self._getTypeBasedMethod('getPermanentURL',
+                     fallback_script_id='WebSection_getPermanentURL')(document)
+
+      if cache is not None:
+        cache[key] = result
+
+      return result
+
+    security.declareProtected(Permissions.View, 'getBreadcrumbItemList')
+    def getBreadcrumbItemList(self, document):
+      """
+        Return a section dependent breadcrumb in the form
+        of a list of (title, document) tuples.
+
+        This method must be implemented through a
+        portal type dependent script:
+          WebSection_getBreadcrumbItemList
+      """
+      cache = getReadOnlyTransactionCache(self)
+      if cache is not None:
+        key = ('getDocumentValueList', self, document.getPath())
+        try:
+          return cache[key]
+        except KeyError:
+          pass
+
+      result = self._getTypeBasedMethod('getBreadcrumbItemList',
+                     fallback_script_id='WebSection_getBreadcrumbItemList')(document)
+
+      if cache is not None:
+        cache[key] = result
+
+      return result
diff --git a/product/ERP5/Document/WebSite.py b/product/ERP5/Document/WebSite.py
index 8af5da08c7..28a94d0260 100644
--- a/product/ERP5/Document/WebSite.py
+++ b/product/ERP5/Document/WebSite.py
@@ -26,29 +26,18 @@
 ##############################################################################
 
 from Acquisition import ImplicitAcquisitionWrapper, aq_base, aq_inner
-
 from AccessControl import ClassSecurityInfo
-from AccessControl.User import emergency_user
-from AccessControl.SecurityManagement import getSecurityManager, newSecurityManager, setSecurityManager
 
-from Products.CMFCore.utils import getToolByName
-from Products.ERP5.Document.Domain import Domain
+from Products.ERP5.Document.WebSection import WebSection, WEBSECTION_KEY
 from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface, Cache
-from Products.ERP5Type.Base import TempBase
-
-from Products.CMFCore.utils import UniqueObject, _checkPermission, _getAuthenticatedUser
 
 from Globals import get_request
-
 from Persistence import Persistent
-
 from ZPublisher import BeforeTraverse
 
 from zLOG import LOG
 
 WEBSITE_KEY = 'web_site_value'
-WEBSECTION_KEY = 'web_section_value'
-WEBSITE_USER = 'web_site_user'
 
 class WebSiteTraversalHook(Persistent):
   """
@@ -111,62 +100,10 @@ class WebSiteTraversalHook(Persistent):
     self._v_request = request
     request.physicalPathToVirtualPath = self._physicalPathToVirtualPath
 
-
-
-Domain_getattr = Domain.inheritedAttribute('__getattr__')
-
-# Use a request key to store access attributes and prevent infinite recursion
-CACHE_KEY = 'web_site_aq_cache'
-DOCUMENT_NAME_KEY = 'web_site_document_name'
-reserved_name_dict = { 'getApplicableLayout' : 1,
-                       'getLayout' : 1,
-                       'Localizer' : 1,
-                       'field_render' : 1,
-                       'getListItemUrl' : 1,
-                       'getLocalPropertyManager' : 1,
-                       'getOrderedGlobalActionList' : 1,
-                       'allow_discussion' : 1,
-                       'im_func' : 1,
-                       'id' : 1,
-                       'method_id' : 1,
-                       'role_map' : 1,  }
-reserved_name_dict_init = 0
-
-class WebSite(Domain):
+class WebSite(WebSection):
     """
-      A Web Site root class. This class is used by ERP5 Commerce
-      to define the root of an eCommerce site.
-
-      The main idea of the WebSite class is to provide access to
-      documents by leveraging aq_dynamic with a user definable
-      script: WebSite_getDocumentValue
-
-      This script allows for implementing simple or
-      complex document lookup policies:
-
-      - access to documents by a unique reference (ex. as
-        in a Wiki)
-
-      - access to published documents only (ex.
-        publication_state == 'published')
-
-      - access to most relevant document (ex. latest
-        version, applicable language)
-
-      Changing this script allows for configuring completely
-      the behaviour of a web site and tweaking document
-      lookup policies to fit specific needs.
-
-      WARNING:
-        - Z Catalog Search permission must be set for Anonymous
-          (XXX is this still true ?)
-
-      TODO:
-        - accelerate document lookup by caching acceptable keys
-          (XXX is this still true ?)
-
-        - fix missing REQUEST information in aq_dynamic documents
-          (XXX is this still true ?)
+      The Web Site root class is specialises WebSection
+      by defining a global webmaster user.
     """
     # CMF Type Definition
     meta_type       = 'ERP5 Web Site'
@@ -186,79 +123,8 @@ class WebSite(Domain):
                       , PropertySheet.WebSite
                       )
 
-    def _aq_dynamic(self, name):
-      """
-        Try to find a suitable document based on the
-        web site local naming policies as defined by
-        the WebSite_getDocumentValue script
-      """
-      global reserved_name_dict_init
-      global reserved_name_dict
-      request = self.REQUEST
-      # Register current web site physical path for later URL generation
-      if not request.has_key(WEBSITE_KEY):
-        request[WEBSITE_KEY] = self.getPhysicalPath()
-        # Normalize web parameter in the request
-        # Fix common user mistake and transform '1' string to boolean
-        for web_param in ['ignore_layout', 'editable_mode']:
-          if hasattr(request, web_param):
-            if getattr(request, web_param, None) in ('1', 1, True):
-              request.set(web_param, True)
-            else:
-              request.set(web_param, False)
-      # First let us call the super method
-      dynamic = Domain._aq_dynamic(self, name)
-      if dynamic is not None:
-        return dynamic
-      # Do some optimisation here for names which can not be names of documents
-      if  reserved_name_dict.has_key(name) \
-          or name.startswith('_') or name.startswith('portal_')\
-          or name.startswith('aq_') or name.startswith('selection_') \
-          or name.startswith('sort-') or name.startswith('WebSite_') \
-          or name.startswith('WebSection_') or name.startswith('Base_'):
-        return None
-      if not reserved_name_dict_init:
-        # Feed reserved_name_dict_init with skin names
-        portal = self.getPortalObject()
-        for skin_folder in portal.portal_skins.objectValues():
-          for id in skin_folder.objectIds():
-            reserved_name_dict[id] = 1
-        for id in portal.objectIds():
-          reserved_name_dict[id] = 1
-        reserved_name_dict_init = 1
-      #LOG('aq_dynamic name',0, name)
-      if not request.has_key(CACHE_KEY):
-        request[CACHE_KEY] = {}
-      elif request[CACHE_KEY].has_key(name):
-        return request[CACHE_KEY][name]
-      try:
-        portal = self.getPortalObject()
-        # Use the webmaster identity to find documents
-        if request[CACHE_KEY].has_key(WEBSITE_USER):
-          user = request[CACHE_KEY][WEBSITE_USER] # Retrieve user from request cache
-        else:
-          user = portal.acl_users.getUserById(self.getWebmaster())
-          request[CACHE_KEY][WEBSITE_USER] = user # Cache user per request
-        if user is not None:
-          old_manager = getSecurityManager()
-          newSecurityManager(get_request(), user)
-        document = self.WebSite_getDocumentValue(name=name, portal=portal)
-        request[CACHE_KEY][name] = document
-        if user is not None:
-          setSecurityManager(old_manager)
-      except:
-        # Cleanup non recursion dict in case of exception
-        if request[CACHE_KEY].has_key(name):
-          del request[CACHE_KEY][name]
-        raise
-      if document is not None:
-        document = aq_base(document.asContext(id=name, # Hide some properties to permit location the original
-                                              original_container=document.getParentValue(),
-                                              original_id=document.getId(),
-                                              editable_absolute_url=document.absolute_url()))
-      return document
+    web_section_key = WEBSITE_KEY
 
-    # Draft - this is being worked on
     security.declareProtected(Permissions.AccessContentsInformation, 'getWebSiteValue')
     def getWebSiteValue(self):
         """
@@ -266,24 +132,29 @@ class WebSite(Domain):
         """
         return self
 
+    # Virtual Hosting Support
     security.declarePrivate( 'manage_beforeDelete' )
     def manage_beforeDelete(self, item, container):
       if item is self:
         handle = self.meta_type + '/' + self.getId()
         BeforeTraverse.unregisterBeforeTraverse(item, handle)
-      Domain.manage_beforeDelete(self, item, container)
+      WebSection.manage_beforeDelete(self, item, container)
 
     security.declarePrivate( 'manage_afterAdd' )
     def manage_afterAdd(self, item, container):
       if item is self:
         handle = self.meta_type + '/' + self.getId()
         BeforeTraverse.registerBeforeTraverse(item, WebSiteTraversalHook(), handle)
-      Domain.manage_afterAdd(self, item, container)
+      WebSection.manage_afterAdd(self, item, container)
 
     # Experimental methods 
-    def findUrlList(self, document):
-        """
-          Return a list of URLs which exist in the site for
-          a given document
-        """
-        pass
\ No newline at end of file
+    def getPermanentURLList(self, document):
+      """
+        Return a list of URLs which exist in the site for
+        a given document. This could be implemented either
+        by keep a history of documents which have been
+        accessed or by parsing all WebSections and listing
+        all documents in each of them to build a reverse
+        mapping of getPermanentURL
+      """
+      pass
\ No newline at end of file
diff --git a/product/ERP5/PropertySheet/WebSite.py b/product/ERP5/PropertySheet/WebSite.py
index e921a56133..963ab1cffc 100644
--- a/product/ERP5/PropertySheet/WebSite.py
+++ b/product/ERP5/PropertySheet/WebSite.py
@@ -34,10 +34,24 @@ class WebSite:
         {   'id'          : 'container_layout',
             'description' : 'ID of a page template or form which defines the rendering layout for the container',
             'type'        : 'string',
+            'default'     : None,
+            'acquisition_base_category'     : ('parent',),
+            'acquisition_portal_type'       : ('Web Section', 'Web Site'),
+            'acquisition_copy_value'        : 0,
+            'acquisition_mask_value'        : 1,
+            'acquisition_accessor_id'       : 'getContainerLayout',
+            'acquisition_depends'           : None,
             'mode'        : '' },
         {   'id'          : 'content_layout',
             'description' : 'ID of a page template or form which defines the rendering layout for contents',
             'type'        : 'string',
+            'default'     : None,
+            'acquisition_base_category'     : ('parent',),
+            'acquisition_portal_type'       : ('Web Section', 'Web Site'),
+            'acquisition_copy_value'        : 0,
+            'acquisition_mask_value'        : 1,
+            'acquisition_accessor_id'       : 'getContentLayout',
+            'acquisition_depends'           : None,
             'mode'        : '' },
         {   'id'          : 'webmaster',
             'description' : 'ID of a user which has complete access to all documents in the site.',
-- 
2.30.9