[Checkins] SVN: CMF/trunk/ - CMFCore.CachingPolicyManager: Prevent firing of caching policies

Jens Vagelpohl jens at dataflake.org
Sun Nov 12 12:06:05 EST 2006


Log message for revision 71111:
  - CMFCore.CachingPolicyManager: Prevent firing of caching policies
    for templates (DTML or ZPT) that are rendered in-line (without a
    separate request) while rendering the requested content item's view.
    (http://www.zope.org/Collectors/CMF/456)
  

Changed:
  U   CMF/trunk/CHANGES.txt
  U   CMF/trunk/CMFActionIcons/DEPENDENCIES.txt
  U   CMF/trunk/CMFCalendar/DEPENDENCIES.txt
  U   CMF/trunk/CMFCore/DEPENDENCIES.txt
  U   CMF/trunk/CMFCore/tests/base/dummy.py
  U   CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py
  U   CMF/trunk/CMFCore/utils.py
  U   CMF/trunk/CMFDefault/DEPENDENCIES.txt
  U   CMF/trunk/CMFTopic/DEPENDENCIES.txt
  U   CMF/trunk/CMFUid/DEPENDENCIES.txt
  U   CMF/trunk/DCWorkflow/DEPENDENCIES.txt

-=-
Modified: CMF/trunk/CHANGES.txt
===================================================================
--- CMF/trunk/CHANGES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CHANGES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -27,10 +27,15 @@
 
   Bug Fixes
 
+    - CMFCore.CachingPolicyManager: Prevent firing of caching policies
+      for templates (DTML or ZPT) that are rendered in-line (without a 
+      separate request) while rendering the requested content item's view.
+      (http://www.zope.org/Collectors/CMF/456)
+
     - CMFDefault RegistrationTool: Fixed too restrictive email checking.
       The new 'checkEmailAddress' function is now used.
 
-    - Fixed test breakage induced by use of Z3 pagetempalates in Zope 2.10+.
+    - Fixed test breakage induced by use of Z3 pagetemplates in Zope 2.10+.
 
     - CMFDefault skins: Fixed encoding issues in welcome and reminder emails.
       'password_email' and 'registered_email' now encode their return value
@@ -38,7 +43,7 @@
 
   Others
 
-    - The CMF now depends on Zope 2.10.0.
+    - The CMF now depends on Zope 2.10.1
 
 
 CMF 2.1.0-alpha (2006/10/09)

Modified: CMF/trunk/CMFActionIcons/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFActionIcons/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFActionIcons/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,3 +1,3 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 GenericSetup

Modified: CMF/trunk/CMFCalendar/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFCalendar/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFCalendar/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,4 +1,4 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 CMFDefault
 GenericSetup

Modified: CMF/trunk/CMFCore/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFCore/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFCore/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,2 +1,2 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 GenericSetup

Modified: CMF/trunk/CMFCore/tests/base/dummy.py
===================================================================
--- CMF/trunk/CMFCore/tests/base/dummy.py	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFCore/tests/base/dummy.py	2006-11-12 17:06:03 UTC (rev 71111)
@@ -118,6 +118,7 @@
         self.reset()
         self.catalog = kw.get('catalog',0)
         self.url = kw.get('url',None)
+        self.view_id = kw.get('view_id',None)
 
     def manage_afterAdd(self, item, container):
         self.after_add_called = 1
@@ -162,6 +163,17 @@
     def Type( self ):
         return 'Dummy Content Title'
 
+    def __call__(self):
+        if self.view_id is None:
+           return DummyContent.inheritedAttribute('__call__')(self)
+        else:
+           # view_id control for testing
+           template = getattr(self, self.view_id)
+           if getattr(aq_base(template), 'isDocTemp', 0):
+               return template(self, self.REQUEST, self.REQUEST['RESPONSE'])
+           else:
+               return template()
+
 DummyFactory = Factory(DummyContent)
 
 

Modified: CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py
===================================================================
--- CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py	2006-11-12 17:06:03 UTC (rev 71111)
@@ -20,12 +20,14 @@
 
 import base64
 import os
+from os.path import join as path_join
 
 from AccessControl.SecurityManagement import newSecurityManager
 from App.Common import rfc1123_date
 from DateTime.DateTime import DateTime
 
 from Products.CMFCore.FSPageTemplate import FSPageTemplate
+from Products.CMFCore.FSDTMLMethod import FSDTMLMethod
 from Products.CMFCore.testing import FunctionalZCMLLayer
 from Products.CMFCore.testing import TraversingZCMLLayer
 from Products.CMFCore.tests.base.dummy import DummyContent
@@ -842,12 +844,341 @@
         self.assertEqual(response.getStatus(), 200)
         self._cleanup()
 
+class FSObjMaker(FSDVTest):
 
+    def _makeFSPageTemplate( self, id, filename ):
+        path = path_join(self.skin_path_name, filename)
+        return FSPageTemplate( id, path )
+
+    def _makeFSDTMLMethod( self, id, filename ):
+        path = path_join(self.skin_path_name, filename)
+        return FSDTMLMethod( id, path )
+
+class NestedTemplateTests( RequestTest, FSObjMaker ):
+
+    layer = TraversingZCMLLayer
+
+    def setUp(self):
+        FSObjMaker.setUp(self)
+        RequestTest.setUp(self)
+
+        # Create a fake portal and the tools we need
+        self.portal = DummySite(id='portal').__of__(self.root)
+        self.portal._setObject('portal_types', DummyTool())
+
+        from Products.CMFCore import CachingPolicyManager
+        CachingPolicyManager.manage_addCachingPolicyManager(self.portal)
+
+    def tearDown(self):
+        RequestTest.tearDown(self)
+        FSObjMaker.tearDown(self)
+
+    def test_subtemplate_cpm_1( self ):
+        # test that subtemplates dont call the cpm
+        # set up site
+        portal = self.portal
+        now = DateTime()
+        cpm = portal.caching_policy_manager
+        cpm.addPolicy(policy_id = 'policy_op2',
+                      predicate = 'python:view=="output_page_2"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      s_max_age_secs=100
+                      )
+
+        content = DummyContent(id='content', view_id='output_page_1')
+        content.modified_date = now
+        portal._setObject('content', content)
+
+        output_page_1 = self._makeFSPageTemplate('output_page_1', 'output_page_1.zpt')
+        output_page_2 = self._makeFSPageTemplate('output_page_2', 'output_page_2.zpt')
+        portal._setObject('output_page_1', output_page_1)
+        portal._setObject('output_page_2', output_page_2)
+
+        portal.content()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+
+    def test_subtemplate_cpm_2( self ):
+        # test that calling content from a template doesnt call the cpm
+        # just calling an FSDTMLMethod directly from another template does
+        # not activate the bug because RESPONSE is not passed in
+        portal = self.portal
+        now = DateTime()
+        cpm = portal.caching_policy_manager
+        cpm.addPolicy(policy_id = 'policy_op4',
+                      predicate = 'python:view=="output_page_4"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      s_max_age_secs=100
+                      )
+
+        content = DummyContent(id='content', view_id='output_page_3')
+        content.modified_date = now
+        portal._setObject('content', content)
+        content2 = DummyContent(id='content2', view_id='output_page_4')
+        content2.modified_date = now
+        portal._setObject('content2', content2)
+
+        output_page_3 = self._makeFSDTMLMethod('output_page_3', 'output_page_3.dtml')
+        output_page_4 = self._makeFSDTMLMethod('output_page_4', 'output_page_4.dtml')
+        portal._setObject('output_page_4',output_page_4)
+        portal._setObject('output_page_3',output_page_3)
+
+        # call the content
+        portal.content()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+
+    def test_subtemplate_cpm_3( self ):
+        # test a bigger mix of zpt templates
+        # set up site
+        portal = self.portal
+        now = DateTime()
+        cpm = portal.caching_policy_manager
+        cpm.addPolicy(policy_id = 'policy_nv1',
+                      predicate = 'python:view=="nested_view_1"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      s_max_age_secs=100
+                      )
+
+        doc1 = DummyContent(id='doc1', view_id='nested_view')
+        doc1.modified_date = now
+        portal._setObject('doc1',doc1)
+        doc2 = DummyContent(id='doc2', view_id='nested_view_1')
+        doc2.modified_date = now
+        portal._setObject('doc2',doc2)
+        doc3 = DummyContent(id='doc3', view_id='nested_view_2')
+        doc3.modified_date = now
+        portal._setObject('doc3',doc3)
+
+        nested_view = self._makeFSPageTemplate('nested_view', 'nested_view.zpt')
+        nested_view_1 = self._makeFSPageTemplate('nested_view_1', 'nested_view_1.zpt')
+        nested_view_2 = self._makeFSPageTemplate('nested_view_2', 'nested_view_2.zpt')
+        portal._setObject('nested_view', nested_view)
+        portal._setObject('nested_view_1', nested_view_1)
+        portal._setObject('nested_view_2', nested_view_2)
+
+        data = portal.doc1()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+
+    def test_mixed_subtemplate_cpm( self ):
+        # test a mix of zpt and dtml templates
+        # set up site
+        now = DateTime()
+        portal = self.portal
+        cpm = portal.caching_policy_manager
+        cpm.addPolicy(policy_id = 'policy_nv1',
+                      predicate = 'python:view=="nested_view_1"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      s_max_age_secs=100
+                      )
+
+        doc1 = DummyContent(id='doc1', view_id='nested_view', modified_date=now)
+        portal._setObject('doc1', doc1)
+        doc2 = DummyContent(id='doc2', view_id='nested_view_1', modified_date=now)
+        portal._setObject('doc2', doc2)
+        doc3 = DummyContent(id='doc3', view_id='nested_view_2', modified_date=now)
+        portal._setObject('doc3', doc3)
+
+        nested_view = self._makeFSPageTemplate('nested_view', 'nested_view.zpt')
+        nested_view_1 = self._makeFSPageTemplate('nested_view_1', 'nested_view_1.zpt')
+        nested_view_2 = self._makeFSDTMLMethod('nested_view_2', 'nested_view_2.dtml')
+        portal._setObject('nested_view', nested_view)
+        portal._setObject('nested_view_1', nested_view_1)
+        portal._setObject('nested_view_2', nested_view_2)
+
+        portal.doc1()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+
+    def test_fireForSubtemplates(self):
+        # This is a FSPageTemplate that will be used as the View for 
+        # our content objects. It doesn't matter what it returns.
+        dv = self._makeFSPageTemplate('dummy_view', 'testPT_CPM1.zpt')
+        self.portal._setObject('dummy_view', dv)
+
+        # These are the subtemplates we use
+        sv1 = self._makeFSPageTemplate('subview_1', 'testPT_CPM2.zpt')
+        sv2 = self._makeFSDTMLMethod('subview_2', 'testDTML_CPM3.dtml')
+        self.portal._setObject('subview_1', sv1)
+        self.portal._setObject( 'subview_2', sv2)
+
+        for i in (1,2,3):
+            id = 'doc%i' % i
+            title = 'Document %i' % i
+            description = 'This is document %i' % i
+            modified_date = DateTime()
+            doc = DummyContent(id)
+            doc.title = title
+            doc.description = description
+            doc.modified_date = modified_date
+            self.portal._setObject(id, doc)
+
+        cpm = self.portal.caching_policy_manager
+
+        # This policy only applies to doc2.
+        cpm.addPolicy(policy_id = 'policy_doc2',
+                      predicate = 'python:object.getId()=="doc2"',
+                      mtime_func = '',
+                      max_age_secs = 200,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc2',
+                      etag_func = '',
+                      pre_check=1
+                      )
+
+        # This policy only applies to doc3. 
+        cpm.addPolicy(policy_id = 'policy_doc3',
+                      predicate = 'python:object.getId()=="doc3"',
+                      mtime_func = '',
+                      max_age_secs = 300,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc3',
+                      etag_func = '',
+                      post_check=1
+                      )
+
+        # http://www.zope.org/Collectors/CMF/456
+        # In cases where one view (ZPT or DTML) is rendered from another
+        # view, we want to ensure only the view requested by the visitor
+        # will get caching rules applied.
+        self.portal.doc1.dummy_view()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+        
+    def test_fireForSubtemplates2(self):
+        # This is a FSPageTemplate that will be used as the View for 
+        # our content objects. It doesn't matter what it returns.
+        dv = self._makeFSPageTemplate('dummy_view', 'testPT_CPM1.zpt')
+        self.portal._setObject('dummy_view', dv)
+
+        # These are the subtemplates we use
+        sv1 = self._makeFSPageTemplate('subview_1', 'testPT_CPM2.zpt')
+        sv2 = self._makeFSDTMLMethod('subview_2', 'testDTML_CPM3.dtml')
+        self.portal._setObject('subview_1', sv1)
+        self.portal._setObject( 'subview_2', sv2)
+
+        for i in (1,2,3):
+            id = 'doc%i' % i
+            title = 'Document %i' % i
+            description = 'This is document %i' % i
+            modified_date = DateTime()
+            doc = DummyContent(id)
+            doc.title = title
+            doc.description = description
+            doc.modified_date = modified_date
+            self.portal._setObject(id, doc)
+
+        cpm = self.portal.caching_policy_manager
+
+        # This policy only applies to doc1.
+        cpm.addPolicy(policy_id = 'policy_doc1',
+                      predicate = 'python:object.getId()=="doc1"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      s_max_age_secs=100
+                      )
+
+        # This policy only applies to doc2.
+        cpm.addPolicy(policy_id = 'policy_doc2',
+                      predicate = 'python:object.getId()=="doc2"',
+                      mtime_func = '',
+                      max_age_secs = 200,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc2',
+                      etag_func = '',
+                      pre_check=1
+                      )
+
+        # This policy only applies to doc3. 
+        cpm.addPolicy(policy_id = 'policy_doc3',
+                      predicate = 'python:object.getId()=="doc3"',
+                      mtime_func = '',
+                      max_age_secs = 300,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc3',
+                      etag_func = '',
+                      post_check=1
+                      )
+
+        # http://www.zope.org/Collectors/CMF/456
+        # In cases where one view (ZPT or DTML) is rendered from another
+        # view, we want to ensure only the view requested by the visitor
+        # will get caching rules applied.
+        self.portal.doc1.dummy_view()
+
+        # We want to make sure the correct policy (policy_doc1) has fired
+        # Just to be sure, change headers so they are definitely all 
+        # lower-cased
+        headers = {}
+        header_info = self.RESPONSE.headers.items()
+        [headers.__setitem__(x[0].lower(), x[1]) for x in header_info]
+
+        self.failUnless(headers.get('x-cache-headers-set-by'))
+        self.assertEquals(headers.get('vary'), 'doc1')
+        self.assertEquals( headers.get('cache-control')
+                         , 'max-age=100, s-maxage=100'
+                         )
+        
+
 def test_suite():
     return unittest.TestSuite((
         unittest.makeSuite(CachingPolicyTests),
         unittest.makeSuite(CachingPolicyManagerTests),
         unittest.makeSuite(CachingPolicyManager304Tests),
+        unittest.makeSuite(NestedTemplateTests),
         ))
 
 if __name__ == '__main__':

Modified: CMF/trunk/CMFCore/utils.py
===================================================================
--- CMF/trunk/CMFCore/utils.py	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFCore/utils.py	2006-11-12 17:06:03 UTC (rev 71111)
@@ -51,6 +51,7 @@
 from exceptions import AccessControl_Unauthorized
 from exceptions import NotFound
 
+SUBTEMPLATE = '__SUBTEMPLATE__'
 
 security = ModuleSecurityInfo( 'Products.CMFCore.utils' )
 
@@ -289,6 +290,12 @@
     if REQUEST is None:
         return False
 
+    # check whether we need to suppress subtemplates
+    call_count = getattr(REQUEST, SUBTEMPLATE, 0)
+    setattr(REQUEST, SUBTEMPLATE, call_count+1)
+    if call_count != 0:
+        return False
+
     if_modified_since = REQUEST.get_header('If-Modified-Since', None)
     if_none_match = REQUEST.get_header('If-None-Match', None)
 
@@ -357,9 +364,10 @@
     if content_etag:
         response.setHeader('ETag', content_etag, literal=1)
     response.setStatus(304)
+    delattr(REQUEST, SUBTEMPLATE)
             
     return True
-    
+   
 
 security.declarePrivate('_setCacheHeaders')
 def _setCacheHeaders(obj, extra_context):
@@ -367,6 +375,14 @@
     REQUEST = getattr(obj, 'REQUEST', None)
 
     if REQUEST is not None:
+        call_count = getattr(REQUEST, SUBTEMPLATE, 1) - 1
+        setattr(REQUEST, SUBTEMPLATE, call_count)
+        if call_count != 0:
+           return
+
+        # cleanup
+        delattr(REQUEST, SUBTEMPLATE)
+
         content = aq_parent(obj)
         manager = getToolByName(obj, 'caching_policy_manager', None)
         if manager is not None:

Modified: CMF/trunk/CMFDefault/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFDefault/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFDefault/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,3 +1,3 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 GenericSetup

Modified: CMF/trunk/CMFTopic/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFTopic/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFTopic/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,4 +1,4 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 CMFDefault
 GenericSetup

Modified: CMF/trunk/CMFUid/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFUid/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFUid/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,3 +1,3 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 GenericSetup

Modified: CMF/trunk/DCWorkflow/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/DCWorkflow/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/DCWorkflow/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,3 +1,3 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 GenericSetup



More information about the Checkins mailing list