SVN: CMF/trunk/C - CMFDefault.Image and CMFDefault.File: Overridden index_html methods

Jens Vagelpohl <[email protected]>
Newsgroups gmane.comp.web.zope.cmf.cvs
Message-ID <[email protected]>
Log message for revision 70604:
  - CMFDefault.Image and CMFDefault.File: Overridden index_html methods
    add Cache Policy Manager-awareness and thus bring these implementations
    in line with CMFCore.FSFile and CMFCore.FSImage
    (http://www.zope.org/Collectors/CMF/454) 
  

Changed:
  U   CMF/trunk/CHANGES.txt
  U   CMF/trunk/CMFCore/utils.py
  U   CMF/trunk/CMFDefault/File.py
  U   CMF/trunk/CMFDefault/Image.py
  A   CMF/trunk/CMFDefault/tests/TestFile.swf
  U   CMF/trunk/CMFDefault/tests/test_File.py
  U   CMF/trunk/CMFDefault/tests/test_Image.py

-=-
Modified: CMF/trunk/CHANGES.txt
===================================================================
--- CMF/trunk/CHANGES.txt	2006-10-11 15:57:25 UTC (rev 70603)
+++ CMF/trunk/CHANGES.txt	2006-10-11 18:47:51 UTC (rev 70604)
@@ -1,3 +1,13 @@
+CMF 2.1-beta (unreleased)
+
+  New Features
+
+    - CMFDefault.Image and CMFDefault.File: Overridden index_html methods
+      add Cache Policy Manager-awareness and thus bring these implementations
+      in line with CMFCore.FSFile and CMFCore.FSImage
+      (http://www.zope.org/Collectors/CMF/454)
+
+
 CMF 2.1-alpha (2006/10/09)
 
   New Features

Modified: CMF/trunk/CMFCore/utils.py
===================================================================
--- CMF/trunk/CMFCore/utils.py	2006-10-11 15:57:25 UTC (rev 70603)
+++ CMF/trunk/CMFCore/utils.py	2006-10-11 18:47:51 UTC (rev 70604)
@@ -742,9 +742,42 @@
             break
     return p.replace('\\','/')
 
+def _OldCacheHeaders(obj):
+    # Old-style checking of modified headers
 
+    REQUEST = getattr(obj, 'REQUEST', None)
+    if REQUEST is None:
+        return False
+
+    RESPONSE = REQUEST.RESPONSE
+    header = REQUEST.get_header('If-Modified-Since', None)
+    last_mod = long(obj.modified().timeTime())
+
+    if header is not None:
+        header = header.split(';')[0]
+        # Some proxies seem to send invalid date strings for this
+        # header. If the date string is not valid, we ignore it
+        # rather than raise an error to be generally consistent
+        # with common servers such as Apache (which can usually
+        # understand the screwy date string as a lucky side effect
+        # of the way they parse it).
+        try:
+            mod_since=DateTime(header)
+            mod_since=long(mod_since.timeTime())
+        except TypeError:
+            mod_since=None
+               
+        if mod_since is not None:
+            if last_mod > 0 and last_mod <= mod_since:
+                RESPONSE.setStatus(304)
+                return True
+
+    #Last-Modified will get stomped on by a cache policy if there is
+    #one set....
+    RESPONSE.setHeader('Last-Modified', rfc1123_date(last_mod))
+
 def _FSCacheHeaders(obj):
-    # Old-style setting of modified headers
+    # Old-style setting of modified headers for FS-based objects
 
     REQUEST = getattr(obj, 'REQUEST', None)
     if REQUEST is None:

Modified: CMF/trunk/CMFDefault/File.py
===================================================================
--- CMF/trunk/CMFDefault/File.py	2006-10-11 15:57:25 UTC (rev 70603)
+++ CMF/trunk/CMFDefault/File.py	2006-10-11 18:47:51 UTC (rev 70604)
@@ -24,6 +24,10 @@
 from zope.interface import implements
 
 from Products.CMFCore.PortalContent import PortalContent
+from Products.CMFCore.utils import _OldCacheHeaders
+from Products.CMFCore.utils import _setCacheHeaders
+from Products.CMFCore.utils import _ViewEmulator
+from Products.CMFCore.utils import _checkConditionalGET
 from Products.GenericSetup.interfaces import IDAVAware
 
 from DublinCore import DefaultDublinCoreImpl
@@ -164,6 +168,45 @@
         self._edit( precondition, file )
         self.reindexObject()
 
+    security.declareProtected(View, 'index_html')
+    def index_html(self, REQUEST, RESPONSE):
+        """
+        The default view of the contents of a File or Image.
+
+        Returns the contents of the file or image.  Also, sets the
+        Content-Type HTTP header to the objects content type.
+        """
+        view = _ViewEmulator().__of__(self)
+
+        # If we have a conditional get, set status 304 and return
+        # no content 
+        if _checkConditionalGET(view, extra_context={}):
+            return ''
+
+        RESPONSE.setHeader('Content-Type', self.content_type)
+
+        # old-style If-Modified-Since header handling.
+        if self._setOldCacheHeaders():
+            # Make sure the CachingPolicyManager gets a go as well
+            _setCacheHeaders(view, extra_context={})
+            return ''
+
+        rendered = OFS.Image.File.index_html(self, REQUEST, RESPONSE)
+
+        # There are 2 Cache Managers which can be in play....
+        # need to decide which to use to determine where the cache headers
+        # are decided on.
+        if self.ZCacheable_getManager() is not None:
+            self.ZCacheable_set(None)
+        else:
+            _setCacheHeaders(view, extra_context={})
+
+        return rendered
+
+    def _setOldCacheHeaders(self):
+        # return False to disable this simple caching behaviour
+        return _OldCacheHeaders(self) 
+
     security.declareProtected(View, 'download')
     def download(self, REQUEST, RESPONSE):
         """Download this item.
@@ -180,7 +223,7 @@
 
         RESPONSE.setHeader('Content-Disposition',
                            'attachment; filename=%s' % self.getId())
-        return OFS.Image.File.index_html(self, REQUEST, RESPONSE)
+        return self.index_html(self, REQUEST, RESPONSE)
 
     security.declareProtected(View, 'Format')
     def Format(self):

Modified: CMF/trunk/CMFDefault/Image.py
===================================================================
--- CMF/trunk/CMFDefault/Image.py	2006-10-11 15:57:25 UTC (rev 70603)
+++ CMF/trunk/CMFDefault/Image.py	2006-10-11 18:47:51 UTC (rev 70604)
@@ -18,11 +18,16 @@
 
 import OFS.Image
 from AccessControl import ClassSecurityInfo
+from DateTime.DateTime import DateTime
 from Globals import InitializeClass
 from zope.component.factory import Factory
 from zope.interface import implements
 
 from Products.CMFCore.PortalContent import PortalContent
+from Products.CMFCore.utils import _OldCacheHeaders
+from Products.CMFCore.utils import _setCacheHeaders
+from Products.CMFCore.utils import _ViewEmulator
+from Products.CMFCore.utils import _checkConditionalGET
 from Products.GenericSetup.interfaces import IDAVAware
 
 from DublinCore import DefaultDublinCoreImpl
@@ -166,10 +171,34 @@
         Display the image, with or without standard_html_[header|footer],
         as appropriate.
         """
-        #if REQUEST['PATH_INFO'][-10:] == 'index_html':
-        #    return self.view(self, REQUEST)
-        return OFS.Image.Image.index_html(self, REQUEST, RESPONSE)
+        view = _ViewEmulator().__of__(self)
 
+        # If we have a conditional get, set status 304 and return
+        # no content
+        if _checkConditionalGET(view, extra_context={}):
+            return ''
+
+        # old-style If-Modified-Since header handling.
+        if self._setOldCacheHeaders():
+            # Make sure the CachingPolicyManager gets a go as well
+            _setCacheHeaders(view, extra_context={})
+            return ''
+
+        rendered = OFS.Image.Image.index_html(self, REQUEST, RESPONSE)
+
+        if self.ZCacheable_getManager() is None:
+            # not none cache manager already taken care of
+            _setCacheHeaders(view, extra_context={})
+        else:
+            self.ZCacheable_set(None)
+
+        return rendered
+
+    security.declarePrivate('_setOldCacheHeaders')
+    def _setOldCacheHeaders(self):
+        # return False to disable this simple caching behaviour
+        return _OldCacheHeaders(self)
+
     security.declareProtected(View, 'Format')
     def Format(self):
         """ Dublin Core element - resource format """

Added: CMF/trunk/CMFDefault/tests/TestFile.swf
===================================================================
(Binary files differ)


Property changes on: CMF/trunk/CMFDefault/tests/TestFile.swf
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Modified: CMF/trunk/CMFDefault/tests/test_File.py
===================================================================
--- CMF/trunk/CMFDefault/tests/test_File.py	2006-10-11 15:57:25 UTC (rev 70603)
+++ CMF/trunk/CMFDefault/tests/test_File.py	2006-10-11 18:47:51 UTC (rev 70604)
@@ -21,10 +21,14 @@
 from os.path import join as path_join
 
 from Products.CMFCore.testing import ConformsToContent
+from Products.CMFCore.tests.base.dummy import DummyCachingManagerWithPolicy
+from Products.CMFCore.tests.base.dummy import DummyCachingManager
+from Products.CMFCore.tests.base.testcase import RequestTest 
 from Products.CMFDefault import tests
 
 TESTS_HOME = tests.__path__[0]
 TEST_JPG = path_join(TESTS_HOME, 'TestImage.jpg')
+TEST_SWF = path_join(TESTS_HOME, 'TestFile.swf')
 
 
 class FileTests(ConformsToContent, unittest.TestCase):
@@ -69,10 +73,90 @@
         self.assertEqual(file.content_type, 'image/jpeg')
 
 
+class CachingTests(RequestTest):
+
+    def _getTargetClass(self):
+        from Products.CMFDefault.File import File
+
+        return File
+
+    def _makeOne(self, *args, **kw):
+        return self._getTargetClass()(*args, **kw)
+
+    def _extractFile( self ):
+
+        f = open( TEST_SWF, 'rb' )
+        try:
+            data = f.read()
+        finally:
+            f.close()
+
+        return TEST_SWF, data
+
+    def test_index_html_with_304_from_cpm( self ):
+        self.root.caching_policy_manager = DummyCachingManagerWithPolicy()
+        path, ref = self._extractFile()
+
+        from webdav.common import rfc1123_date
+        from Products.CMFCore.tests.base.dummy import FAKE_ETAG
+        
+        file = self._makeOne( 'test_file', 'test_file.swf', file=ref )
+        file = file.__of__( self.root )
+
+        mod_time = file.modified().timeTime()
+
+        self.REQUEST.environ[ 'IF_MODIFIED_SINCE'
+                            ] = '%s;' % rfc1123_date( mod_time )
+        self.REQUEST.environ[ 'IF_NONE_MATCH'
+                            ] = '%s;' % FAKE_ETAG
+
+        data = file.index_html( self.REQUEST, self.RESPONSE )
+        self.assertEqual( len(data), 0 )
+        self.assertEqual( self.RESPONSE.getStatus(), 304 )
+
+    def test_index_html_200_with_cpm( self ):
+        # should behave the same as without cpm installed
+        self.root.caching_policy_manager = DummyCachingManager()
+        path, ref = self._extractFile()
+
+        from webdav.common import rfc1123_date
+        
+        file = self._makeOne( 'test_file', 'test_file.swf', file=ref )
+        file = file.__of__( self.root )
+
+        mod_time = file.modified().timeTime()
+
+        data = file.index_html( self.REQUEST, self.RESPONSE )
+
+        self.assertEqual( len( data ), len( ref ) )
+        self.assertEqual( data, ref )
+        # ICK!  'HTTPResponse.getHeader' doesn't case-flatten the key!
+        self.assertEqual( self.RESPONSE.getHeader( 'Content-Length'.lower() )
+                        , str(len(ref)) )
+        self.assertEqual( self.RESPONSE.getHeader( 'Content-Type'.lower() )
+                        , 'application/octet-stream' )
+        self.assertEqual( self.RESPONSE.getHeader( 'Last-Modified'.lower() )
+                        , rfc1123_date( mod_time ) )
+
+    def test_caching( self ):
+        self.root.caching_policy_manager = DummyCachingManager()
+        original_len = len(self.RESPONSE.headers)
+        file = self._makeOne('test_file', 'test_file.swf')
+        file = file.__of__(self.root)
+        file.index_html(self.REQUEST, self.RESPONSE)
+        headers = self.RESPONSE.headers
+        self.failUnless(len(headers) >= original_len + 3)
+        self.failUnless('foo' in headers.keys())
+        self.failUnless('bar' in headers.keys())
+        self.assertEqual(headers['test_path'], '/test_file')
+
+
 def test_suite():
     return unittest.TestSuite((
         unittest.makeSuite(FileTests),
+        unittest.makeSuite(CachingTests),
         ))
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
+

Modified: CMF/trunk/CMFDefault/tests/test_Image.py
===================================================================
--- CMF/trunk/CMFDefault/tests/test_Image.py	2006-10-11 15:57:25 UTC (rev 70603)
+++ CMF/trunk/CMFDefault/tests/test_Image.py	2006-10-11 18:47:51 UTC (rev 70604)
@@ -32,9 +32,12 @@
 from zope.testing.cleanup import cleanUp
 
 from Products.CMFCore.testing import ConformsToContent
+from Products.CMFCore.tests.base.dummy import DummyCachingManager
+from Products.CMFCore.tests.base.dummy import DummyCachingManagerWithPolicy
 from Products.CMFCore.tests.base.dummy import DummySite
 from Products.CMFCore.tests.base.dummy import DummyTool
 from Products.CMFCore.tests.base.security import OmnipotentUser
+from Products.CMFCore.tests.base.testcase import RequestTest
 from Products.CMFCore.tests.base.testcase import SecurityRequestTest
 from Products.CMFCore.tests.base.testcase import setUpEvents
 from Products.CMFCore.tests.base.testcase import setUpGenericSetup
@@ -197,11 +200,118 @@
         review_state = self.workflow.getInfoFor(self.site.image2, 'review_state')
         self.assertEqual(review_state, 'published')
 
+class TestCaching(RequestTest):
 
+    def _extractFile( self ):
+
+        f = open( TEST_JPG, 'rb' )
+        try:
+            data = f.read()
+        finally:
+            f.close()
+
+        return TEST_JPG, data
+
+    def _getTargetClass(self):
+        from Products.CMFDefault.Image import Image
+
+        return Image
+
+    def _makeOne(self, *args, **kw):
+        return self._getTargetClass()(*args, **kw)
+
+    def test_index_html_with_304_from_cpm( self ):
+        self.root.caching_policy_manager = DummyCachingManagerWithPolicy()
+        path, ref = self._extractFile()
+
+        from webdav.common import rfc1123_date
+        from Products.CMFCore.tests.base.dummy import FAKE_ETAG
+
+        self.root.file = self._makeOne('test_file', 'test_image.jpg', file=ref)
+        file = self.root.file
+
+        mod_time = file.modified()
+
+        self.REQUEST.environ[ 'IF_MODIFIED_SINCE'
+                            ] = '%s;' % rfc1123_date( mod_time )
+        self.REQUEST.environ[ 'IF_NONE_MATCH'
+                            ] = '%s;' % FAKE_ETAG
+
+        data = file.index_html( self.REQUEST, self.RESPONSE )
+        self.assertEqual( len(data), 0 )
+        self.assertEqual( self.RESPONSE.getStatus(), 304 )
+
+    def test_caching( self ):
+        self.root.caching_policy_manager = DummyCachingManager()
+        original_len = len(self.RESPONSE.headers)
+        image = self._makeOne('test_image', 'test_image.jpg')
+        image = image.__of__(self.root)
+        image.index_html(self.REQUEST, self.RESPONSE)
+        headers = self.RESPONSE.headers
+        self.failUnless(len(headers) >= original_len + 3)
+        self.failUnless('foo' in headers.keys())
+        self.failUnless('bar' in headers.keys())
+        self.assertEqual(headers['test_path'], '/test_image')
+
+    def test_index_html_200_with_cpm( self ):
+        self.root.caching_policy_manager = DummyCachingManagerWithPolicy()
+        path, ref = self._extractFile()
+
+        from webdav.common import rfc1123_date
+
+        file = self._makeOne( 'test_file', 'test_image.jpg', file=ref )
+        file = file.__of__( self.root )
+
+        mod_time = file.modified()
+
+        data = file.index_html( self.REQUEST, self.RESPONSE )
+
+        # should behave the same as without cpm
+        self.assertEqual( len( data ), len( ref ) )
+        self.assertEqual( data, ref )
+        # ICK!  'HTTPResponse.getHeader' doesn't case-flatten the key!
+        self.assertEqual( self.RESPONSE.getHeader( 'Content-Length'.lower() )
+                        , str(len(ref)) )
+        self.assertEqual( self.RESPONSE.getHeader( 'Content-Type'.lower() )
+                        , 'image/jpeg' )
+        self.assertEqual( self.RESPONSE.getHeader( 'Last-Modified'.lower() )
+                        , rfc1123_date( mod_time ) )
+
+    def test_index_html_with_304_and_caching( self ):
+
+        # See collector #355
+        self.root.caching_policy_manager = DummyCachingManager()
+        original_len = len(self.RESPONSE.headers)
+        path, ref = self._extractFile()
+
+        from webdav.common import rfc1123_date
+
+        self.root.image = self._makeOne( 'test_image', 'test_image.gif' )
+        image = self.root.image
+        import transaction
+        transaction.savepoint(optimistic=True)
+
+        mod_time = image.modified()
+
+        self.REQUEST.environ[ 'IF_MODIFIED_SINCE'
+                            ] = '%s;' % rfc1123_date( mod_time+1 )
+
+        data = image.index_html( self.REQUEST, self.RESPONSE )
+
+        self.assertEqual( data, '' )
+        self.assertEqual( self.RESPONSE.getStatus(), 304 )
+
+        headers = self.RESPONSE.headers
+        self.failUnless(len(headers) >= original_len + 3)
+        self.failUnless('foo' in headers.keys())
+        self.failUnless('bar' in headers.keys())
+        self.assertEqual(headers['test_path'], '/test_image') 
+
 def test_suite():
     return unittest.TestSuite((
         unittest.makeSuite(TestImageElement),
         unittest.makeSuite(TestImageCopyPaste),
+        unittest.makeSuite(TestCaching),
         ))
 
 if __name__ == '__main__':
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.