Author: russellm
Date: 2011-01-24 08:24:35 -0600 (Mon, 24 Jan 2011)
New Revision: 15295

Modified:
   django/trunk/django/middleware/cache.py
   django/trunk/django/template/response.py
   django/trunk/docs/ref/template-response.txt
   django/trunk/tests/regressiontests/generic_views/base.py
   
django/trunk/tests/regressiontests/generic_views/templates/generic_views/about.html
   django/trunk/tests/regressiontests/generic_views/urls.py
   django/trunk/tests/regressiontests/templates/alternate_urls.py
   django/trunk/tests/regressiontests/templates/response.py
   django/trunk/tests/regressiontests/templates/templates/response.html
Log:
Fixed #15012 -- Added post-rendering callbacks to TemplateResponse so that 
decorators (in particular, the cache decorator) can defer processing until 
after rendering has occurred. Thanks to Joshua Ginsberg for the draft patch.

Modified: django/trunk/django/middleware/cache.py
===================================================================
--- django/trunk/django/middleware/cache.py     2011-01-24 11:49:11 UTC (rev 
15294)
+++ django/trunk/django/middleware/cache.py     2011-01-24 14:24:35 UTC (rev 
15295)
@@ -52,6 +52,7 @@
 from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS
 from django.utils.cache import get_cache_key, learn_cache_key, 
patch_response_headers, get_max_age
 
+
 class UpdateCacheMiddleware(object):
     """
     Response-phase cache middleware that updates the cache if the response is
@@ -87,7 +88,12 @@
         patch_response_headers(response, timeout)
         if timeout:
             cache_key = learn_cache_key(request, response, timeout, 
self.key_prefix, cache=self.cache)
-            self.cache.set(cache_key, response, timeout)
+            if hasattr(response, 'render') and callable(response.render):
+                response.add_post_render_callback(
+                    lambda r: self.cache.set(cache_key, r, timeout)
+                )
+            else:
+                self.cache.set(cache_key, response, timeout)
         return response
 
 class FetchFromCacheMiddleware(object):

Modified: django/trunk/django/template/response.py
===================================================================
--- django/trunk/django/template/response.py    2011-01-24 11:49:11 UTC (rev 
15294)
+++ django/trunk/django/template/response.py    2011-01-24 14:24:35 UTC (rev 
15295)
@@ -19,12 +19,30 @@
         # a final response.
         self._is_rendered = False
 
+        self._post_render_callbacks = []
+
         # content argument doesn't make sense here because it will be replaced
         # with rendered template so we always pass empty string in order to
         # prevent errors and provide shorter signature.
         super(SimpleTemplateResponse, self).__init__('', mimetype, status,
                                                      content_type)
 
+    def __getstate__(self):
+        """Pickling support function.
+
+        Ensures that the object can't be pickled before it has been
+        rendered, and that the pickled state only includes rendered
+        data, not the data used to construct the response.
+        """
+        obj_dict = self.__dict__.copy()
+        if not self._is_rendered:
+            raise ContentNotRenderedError('The response content must be 
rendered before it can be pickled.')
+        del obj_dict['template_name']
+        del obj_dict['context_data']
+        del obj_dict['_post_render_callbacks']
+
+        return obj_dict
+
     def resolve_template(self, template):
         "Accepts a template object, path-to-template or list of paths"
         if isinstance(template, (list, tuple)):
@@ -57,6 +75,16 @@
         content = template.render(context)
         return content
 
+    def add_post_render_callback(self, callback):
+        """Add a new post-rendering callback.
+
+        If the response has already been rendered, invoke the callback 
immediately.
+        """
+        if self._is_rendered:
+            callback(self)
+        else:
+            self._post_render_callbacks.append(callback)
+
     def render(self):
         """Render (thereby finalizing) the content of the response.
 
@@ -66,6 +94,8 @@
         """
         if not self._is_rendered:
             self._set_content(self.rendered_content)
+            for post_callback in self._post_render_callbacks:
+                post_callback(self)
         return self
 
     is_rendered = property(lambda self: self._is_rendered)
@@ -81,7 +111,7 @@
         return super(SimpleTemplateResponse, self)._get_content()
 
     def _set_content(self, value):
-        "Overrides rendered content, unless you later call render()"
+        "Sets the content for the response"
         super(SimpleTemplateResponse, self)._set_content(value)
         self._is_rendered = True
 
@@ -101,6 +131,20 @@
         super(TemplateResponse, self).__init__(
             template, context, mimetype, status, content_type)
 
+    def __getstate__(self):
+        """Pickling support function.
+
+        Ensures that the object can't be pickled before it has been
+        rendered, and that the pickled state only includes rendered
+        data, not the data used to construct the response.
+        """
+        obj_dict = super(TemplateResponse, self).__getstate__()
+
+        del obj_dict['_request']
+        del obj_dict['_current_app']
+
+        return obj_dict
+
     def resolve_context(self, context):
         """Convert context data into a full RequestContext object
         (assuming it isn't already a Context object).
@@ -109,3 +153,5 @@
             return context
         else:
             return RequestContext(self._request, context, 
current_app=self._current_app)
+
+

Modified: django/trunk/docs/ref/template-response.txt
===================================================================
--- django/trunk/docs/ref/template-response.txt 2011-01-24 11:49:11 UTC (rev 
15294)
+++ django/trunk/docs/ref/template-response.txt 2011-01-24 14:24:35 UTC (rev 
15295)
@@ -55,7 +55,6 @@
 
     A boolean indicating whether the response content has been rendered.
 
-
 Methods
 -------
 
@@ -106,6 +105,20 @@
 
     Override this method in order to customize template rendering.
 
+.. method:: SimpleTemplateResponse.add_post_rendering_callback
+
+    Add a callback that will be invoked after rendering has taken
+    place. This hook can be used to defer certain processing
+    operations (such as caching) until after rendering has occurred.
+
+    If the :class:`~django.template.response.SimpleTemplateResponse`
+    has already been rendered, the callback will be invoked
+    immediately.
+
+    When called, callbacks will be passed a single argument -- the
+    rendered :class:`~django.template.response.SimpleTemplateResponse`
+    instance.
+
 .. method:: SimpleTemplateResponse.render():
 
     Sets :attr:`response.content` to the result obtained by
@@ -211,6 +224,50 @@
     >>> print t.content
     New content
 
+Post-render callbacks
+---------------------
+
+Some operations -- such as caching -- cannot be performed on an
+unrendered template. They must be performed on a fully complete and
+rendered response.
+
+If you're using middleware, the solution is easy. Middleware provides
+multiple opportunities to process a response on exit from a view. If
+you put behavior in the Response middleware is guaranteed to execute
+after template rendering has taken place.
+
+However, if you're using a decorator, the same opportunities do not
+exist. Any behavior defined in a decorator is handled immediately.
+
+To compensate for this (and any other analogous use cases),
+:class:`TemplateResponse` allows you to register callbacks that will
+be invoked when rendering has completed. Using this callback, you can
+defer critical processing until a point where you can guarantee that
+rendered content will be available.
+
+To define a post-render callback, just define a function that takes
+a single argument -- response -- and register that function with
+the template response::
+
+    def my_render_callback(response):
+        # Do content-sensitive processing
+        do_post_processing()
+
+    def my_view(request):
+        # Create a response
+        response = TemplateResponse(request, 'mytemplate.html', {})
+        # Register the callback
+        response.add_post_render_callback(my_render_callback)
+        # Return the response
+        return response
+
+``my_render_callback()`` will be invoked after the ``mytemplate.html``
+has been rendered, and will be provided the fully rendered
+:class:`TemplateResponse` instance as an argument.
+
+If the template has already been rendered, the callback will be
+invoked immediately.
+
 Using TemplateResponse and SimpleTemplateResponse
 =================================================
 

Modified: django/trunk/tests/regressiontests/generic_views/base.py
===================================================================
--- django/trunk/tests/regressiontests/generic_views/base.py    2011-01-24 
11:49:11 UTC (rev 15294)
+++ django/trunk/tests/regressiontests/generic_views/base.py    2011-01-24 
14:24:35 UTC (rev 15295)
@@ -1,3 +1,4 @@
+import time
 import unittest
 
 from django.core.exceptions import ImproperlyConfigured
@@ -158,7 +159,7 @@
     def _assert_about(self, response):
         response.render()
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.content, '<h1>About</h1>')
+        self.assertContains(response, '<h1>About</h1>')
 
     def test_get(self):
         """
@@ -197,6 +198,28 @@
         self.assertEqual(response.context['params'], {'foo': 'bar'})
         self.assertEqual(response.context['key'], 'value')
 
+    def test_cached_views(self):
+        """
+        A template view can be cached
+        """
+        response = self.client.get('/template/cached/bar/')
+        self.assertEqual(response.status_code, 200)
+
+        time.sleep(1.0)
+
+        response2 = self.client.get('/template/cached/bar/')
+        self.assertEqual(response2.status_code, 200)
+
+        self.assertEqual(response.content, response2.content)
+
+        time.sleep(2.0)
+
+        # Let the cache expire and test again
+        response2 = self.client.get('/template/cached/bar/')
+        self.assertEqual(response2.status_code, 200)
+
+        self.assertNotEqual(response.content, response2.content)
+
 class RedirectViewTest(unittest.TestCase):
     rf = RequestFactory()
 

Modified: 
django/trunk/tests/regressiontests/generic_views/templates/generic_views/about.html
===================================================================
--- 
django/trunk/tests/regressiontests/generic_views/templates/generic_views/about.html
 2011-01-24 11:49:11 UTC (rev 15294)
+++ 
django/trunk/tests/regressiontests/generic_views/templates/generic_views/about.html
 2011-01-24 14:24:35 UTC (rev 15295)
@@ -1 +1,2 @@
-<h1>About</h1>
\ No newline at end of file
+<h1>About</h1>
+{% now "U.u" %}

Modified: django/trunk/tests/regressiontests/generic_views/urls.py
===================================================================
--- django/trunk/tests/regressiontests/generic_views/urls.py    2011-01-24 
11:49:11 UTC (rev 15294)
+++ django/trunk/tests/regressiontests/generic_views/urls.py    2011-01-24 
14:24:35 UTC (rev 15295)
@@ -1,5 +1,6 @@
 from django.conf.urls.defaults import *
 from django.views.generic import TemplateView
+from django.views.decorators.cache import cache_page
 
 import views
 
@@ -15,6 +16,9 @@
     (r'^template/custom/(?P<foo>\w+)/$',
         
views.CustomTemplateView.as_view(template_name='generic_views/about.html')),
 
+    (r'^template/cached/(?P<foo>\w+)/$',
+        
cache_page(2.0)(TemplateView.as_view(template_name='generic_views/about.html'))),
+
     # DetailView
     (r'^detail/obj/$',
         views.ObjectDetail.as_view()),

Modified: django/trunk/tests/regressiontests/templates/alternate_urls.py
===================================================================
--- django/trunk/tests/regressiontests/templates/alternate_urls.py      
2011-01-24 11:49:11 UTC (rev 15294)
+++ django/trunk/tests/regressiontests/templates/alternate_urls.py      
2011-01-24 14:24:35 UTC (rev 15295)
@@ -1,10 +1,12 @@
 # coding: utf-8
 from django.conf.urls.defaults import *
+
 from regressiontests.templates import views
 
+
 urlpatterns = patterns('',
     # View returning a template response
-    (r'^template_response_view/', views.template_response_view),
+    (r'^template_response_view/$', views.template_response_view),
 
     # A view that can be hard to find...
     url(r'^snark/', views.snark, name='snark'),

Modified: django/trunk/tests/regressiontests/templates/response.py
===================================================================
--- django/trunk/tests/regressiontests/templates/response.py    2011-01-24 
11:49:11 UTC (rev 15294)
+++ django/trunk/tests/regressiontests/templates/response.py    2011-01-24 
14:24:35 UTC (rev 15295)
@@ -1,4 +1,7 @@
+from datetime import datetime
 import os
+import pickle
+import time
 from django.utils import unittest
 from django.test import RequestFactory, TestCase
 from django.conf import settings
@@ -147,7 +150,50 @@
         self.assertEqual(response['content-type'], 'application/json')
         self.assertEqual(response.status_code, 504)
 
+    def test_post_callbacks(self):
+        "Rendering a template response triggers the post-render callbacks"
+        post = []
 
+        def post1(obj):
+            post.append('post1')
+        def post2(obj):
+            post.append('post2')
+
+        response = SimpleTemplateResponse('first/test.html', {})
+        response.add_post_render_callback(post1)
+        response.add_post_render_callback(post2)
+
+        # When the content is rendered, all the callbacks are invoked, too.
+        response.render()
+        self.assertEqual('First template\n', response.content)
+        self.assertEquals(post, ['post1','post2'])
+
+
+    def test_pickling(self):
+        # Create a template response. The context is
+        # known to be unpickleable (e.g., a function).
+        response = SimpleTemplateResponse('first/test.html', {
+                'value': 123,
+                'fn': datetime.now,
+            })
+        self.assertRaises(ContentNotRenderedError,
+                          pickle.dumps, response)
+
+        # But if we render the response, we can pickle it.
+        response.render()
+        pickled_response = pickle.dumps(response)
+        unpickled_response = pickle.loads(pickled_response)
+
+        self.assertEquals(unpickled_response.content, response.content)
+        self.assertEquals(unpickled_response['content-type'], 
response['content-type'])
+        self.assertEquals(unpickled_response.status_code, response.status_code)
+
+        # ...and the unpickled reponse doesn't have the
+        # template-related attributes, so it can't be re-rendered
+        self.assertFalse(hasattr(unpickled_response, 'template_name'))
+        self.assertFalse(hasattr(unpickled_response, 'context_data'))
+        self.assertFalse(hasattr(unpickled_response, '_post_render_callbacks'))
+
 class TemplateResponseTest(BaseTemplateResponseTest):
 
     def _response(self, template='foo', *args, **kwargs):
@@ -187,7 +233,34 @@
 
         self.assertEqual(rc.current_app, 'foobar')
 
+    def test_pickling(self):
+        # Create a template response. The context is
+        # known to be unpickleable (e.g., a function).
+        response = TemplateResponse(self.factory.get('/'),
+            'first/test.html', {
+                'value': 123,
+                'fn': datetime.now,
+            })
+        self.assertRaises(ContentNotRenderedError,
+                          pickle.dumps, response)
 
+        # But if we render the response, we can pickle it.
+        response.render()
+        pickled_response = pickle.dumps(response)
+        unpickled_response = pickle.loads(pickled_response)
+
+        self.assertEquals(unpickled_response.content, response.content)
+        self.assertEquals(unpickled_response['content-type'], 
response['content-type'])
+        self.assertEquals(unpickled_response.status_code, response.status_code)
+
+        # ...and the unpickled reponse doesn't have the
+        # template-related attributes, so it can't be re-rendered
+        self.assertFalse(hasattr(unpickled_response, '_request'))
+        self.assertFalse(hasattr(unpickled_response, 'template_name'))
+        self.assertFalse(hasattr(unpickled_response, 'context_data'))
+        self.assertFalse(hasattr(unpickled_response, '_post_render_callbacks'))
+
+
 class CustomURLConfTest(TestCase):
     urls = 'regressiontests.templates.urls'
 
@@ -203,6 +276,41 @@
     def test_custom_urlconf(self):
         response = self.client.get('/template_response_view/')
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.content, 'This is where you can find the 
snark: /snark/')
+        self.assertContains(response, 'This is where you can find the snark: 
/snark/')
 
 
+class CacheMiddlewareTest(TestCase):
+    urls = 'regressiontests.templates.alternate_urls'
+
+    def setUp(self):
+        self.old_MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES
+        self.CACHE_MIDDLEWARE_SECONDS = settings.CACHE_MIDDLEWARE_SECONDS
+
+        settings.CACHE_MIDDLEWARE_SECONDS = 2.0
+        settings.MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES) + [
+            'django.middleware.cache.FetchFromCacheMiddleware',
+            'django.middleware.cache.UpdateCacheMiddleware',
+        ]
+
+    def tearDown(self):
+        settings.MIDDLEWARE_CLASSES = self.old_MIDDLEWARE_CLASSES
+        settings.CACHE_MIDDLEWARE_SECONDS = self.CACHE_MIDDLEWARE_SECONDS
+
+    def test_middleware_caching(self):
+        response = self.client.get('/template_response_view/')
+        self.assertEqual(response.status_code, 200)
+
+        time.sleep(1.0)
+
+        response2 = self.client.get('/template_response_view/')
+        self.assertEqual(response2.status_code, 200)
+
+        self.assertEqual(response.content, response2.content)
+
+        time.sleep(2.0)
+
+        # Let the cache expire and test again
+        response2 = self.client.get('/template_response_view/')
+        self.assertEqual(response2.status_code, 200)
+
+        self.assertNotEqual(response.content, response2.content)

Modified: django/trunk/tests/regressiontests/templates/templates/response.html
===================================================================
--- django/trunk/tests/regressiontests/templates/templates/response.html        
2011-01-24 11:49:11 UTC (rev 15294)
+++ django/trunk/tests/regressiontests/templates/templates/response.html        
2011-01-24 14:24:35 UTC (rev 15295)
@@ -1 +1,2 @@
-{% load url from future %}This is where you can find the snark: {% url "snark" 
%}
\ No newline at end of file
+{% load url from future %}This is where you can find the snark: {% url "snark" 
%}
+{% now "U.u" %}
\ No newline at end of file

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to django-updates@googlegroups.com.
To unsubscribe from this group, send email to 
django-updates+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en.

Reply via email to