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.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@15295 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
3d7afd5d2b
commit
3f528e10d5
|
@ -52,6 +52,7 @@ from django.conf import settings
|
||||||
from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS
|
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
|
from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age
|
||||||
|
|
||||||
|
|
||||||
class UpdateCacheMiddleware(object):
|
class UpdateCacheMiddleware(object):
|
||||||
"""
|
"""
|
||||||
Response-phase cache middleware that updates the cache if the response is
|
Response-phase cache middleware that updates the cache if the response is
|
||||||
|
@ -87,7 +88,12 @@ class UpdateCacheMiddleware(object):
|
||||||
patch_response_headers(response, timeout)
|
patch_response_headers(response, timeout)
|
||||||
if timeout:
|
if timeout:
|
||||||
cache_key = learn_cache_key(request, response, timeout, self.key_prefix, cache=self.cache)
|
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
|
return response
|
||||||
|
|
||||||
class FetchFromCacheMiddleware(object):
|
class FetchFromCacheMiddleware(object):
|
||||||
|
|
|
@ -19,12 +19,30 @@ class SimpleTemplateResponse(HttpResponse):
|
||||||
# a final response.
|
# a final response.
|
||||||
self._is_rendered = False
|
self._is_rendered = False
|
||||||
|
|
||||||
|
self._post_render_callbacks = []
|
||||||
|
|
||||||
# content argument doesn't make sense here because it will be replaced
|
# content argument doesn't make sense here because it will be replaced
|
||||||
# with rendered template so we always pass empty string in order to
|
# with rendered template so we always pass empty string in order to
|
||||||
# prevent errors and provide shorter signature.
|
# prevent errors and provide shorter signature.
|
||||||
super(SimpleTemplateResponse, self).__init__('', mimetype, status,
|
super(SimpleTemplateResponse, self).__init__('', mimetype, status,
|
||||||
content_type)
|
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):
|
def resolve_template(self, template):
|
||||||
"Accepts a template object, path-to-template or list of paths"
|
"Accepts a template object, path-to-template or list of paths"
|
||||||
if isinstance(template, (list, tuple)):
|
if isinstance(template, (list, tuple)):
|
||||||
|
@ -57,6 +75,16 @@ class SimpleTemplateResponse(HttpResponse):
|
||||||
content = template.render(context)
|
content = template.render(context)
|
||||||
return content
|
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):
|
def render(self):
|
||||||
"""Render (thereby finalizing) the content of the response.
|
"""Render (thereby finalizing) the content of the response.
|
||||||
|
|
||||||
|
@ -66,6 +94,8 @@ class SimpleTemplateResponse(HttpResponse):
|
||||||
"""
|
"""
|
||||||
if not self._is_rendered:
|
if not self._is_rendered:
|
||||||
self._set_content(self.rendered_content)
|
self._set_content(self.rendered_content)
|
||||||
|
for post_callback in self._post_render_callbacks:
|
||||||
|
post_callback(self)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
is_rendered = property(lambda self: self._is_rendered)
|
is_rendered = property(lambda self: self._is_rendered)
|
||||||
|
@ -81,7 +111,7 @@ class SimpleTemplateResponse(HttpResponse):
|
||||||
return super(SimpleTemplateResponse, self)._get_content()
|
return super(SimpleTemplateResponse, self)._get_content()
|
||||||
|
|
||||||
def _set_content(self, value):
|
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)
|
super(SimpleTemplateResponse, self)._set_content(value)
|
||||||
self._is_rendered = True
|
self._is_rendered = True
|
||||||
|
|
||||||
|
@ -101,6 +131,20 @@ class TemplateResponse(SimpleTemplateResponse):
|
||||||
super(TemplateResponse, self).__init__(
|
super(TemplateResponse, self).__init__(
|
||||||
template, context, mimetype, status, content_type)
|
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):
|
def resolve_context(self, context):
|
||||||
"""Convert context data into a full RequestContext object
|
"""Convert context data into a full RequestContext object
|
||||||
(assuming it isn't already a Context object).
|
(assuming it isn't already a Context object).
|
||||||
|
@ -109,3 +153,5 @@ class TemplateResponse(SimpleTemplateResponse):
|
||||||
return context
|
return context
|
||||||
else:
|
else:
|
||||||
return RequestContext(self._request, context, current_app=self._current_app)
|
return RequestContext(self._request, context, current_app=self._current_app)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,6 @@ Attributes
|
||||||
|
|
||||||
A boolean indicating whether the response content has been rendered.
|
A boolean indicating whether the response content has been rendered.
|
||||||
|
|
||||||
|
|
||||||
Methods
|
Methods
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
@ -106,6 +105,20 @@ Methods
|
||||||
|
|
||||||
Override this method in order to customize template rendering.
|
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():
|
.. method:: SimpleTemplateResponse.render():
|
||||||
|
|
||||||
Sets :attr:`response.content` to the result obtained by
|
Sets :attr:`response.content` to the result obtained by
|
||||||
|
@ -211,6 +224,50 @@ the content of the response manually::
|
||||||
>>> print t.content
|
>>> print t.content
|
||||||
New 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
|
Using TemplateResponse and SimpleTemplateResponse
|
||||||
=================================================
|
=================================================
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
@ -158,7 +159,7 @@ class TemplateViewTest(TestCase):
|
||||||
def _assert_about(self, response):
|
def _assert_about(self, response):
|
||||||
response.render()
|
response.render()
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.content, '<h1>About</h1>')
|
self.assertContains(response, '<h1>About</h1>')
|
||||||
|
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
"""
|
"""
|
||||||
|
@ -197,6 +198,28 @@ class TemplateViewTest(TestCase):
|
||||||
self.assertEqual(response.context['params'], {'foo': 'bar'})
|
self.assertEqual(response.context['params'], {'foo': 'bar'})
|
||||||
self.assertEqual(response.context['key'], 'value')
|
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):
|
class RedirectViewTest(unittest.TestCase):
|
||||||
rf = RequestFactory()
|
rf = RequestFactory()
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
<h1>About</h1>
|
<h1>About</h1>
|
||||||
|
{% now "U.u" %}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import *
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
from django.views.decorators.cache import cache_page
|
||||||
|
|
||||||
import views
|
import views
|
||||||
|
|
||||||
|
@ -15,6 +16,9 @@ urlpatterns = patterns('',
|
||||||
(r'^template/custom/(?P<foo>\w+)/$',
|
(r'^template/custom/(?P<foo>\w+)/$',
|
||||||
views.CustomTemplateView.as_view(template_name='generic_views/about.html')),
|
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
|
# DetailView
|
||||||
(r'^detail/obj/$',
|
(r'^detail/obj/$',
|
||||||
views.ObjectDetail.as_view()),
|
views.ObjectDetail.as_view()),
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
from regressiontests.templates import views
|
from regressiontests.templates import views
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
# View returning a template response
|
# 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...
|
# A view that can be hard to find...
|
||||||
url(r'^snark/', views.snark, name='snark'),
|
url(r'^snark/', views.snark, name='snark'),
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
|
import pickle
|
||||||
|
import time
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import RequestFactory, TestCase
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -147,6 +150,49 @@ class SimpleTemplateResponseTest(BaseTemplateResponseTest):
|
||||||
self.assertEqual(response['content-type'], 'application/json')
|
self.assertEqual(response['content-type'], 'application/json')
|
||||||
self.assertEqual(response.status_code, 504)
|
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):
|
class TemplateResponseTest(BaseTemplateResponseTest):
|
||||||
|
|
||||||
|
@ -187,6 +233,33 @@ class TemplateResponseTest(BaseTemplateResponseTest):
|
||||||
|
|
||||||
self.assertEqual(rc.current_app, 'foobar')
|
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):
|
class CustomURLConfTest(TestCase):
|
||||||
urls = 'regressiontests.templates.urls'
|
urls = 'regressiontests.templates.urls'
|
||||||
|
@ -203,6 +276,41 @@ class CustomURLConfTest(TestCase):
|
||||||
def test_custom_urlconf(self):
|
def test_custom_urlconf(self):
|
||||||
response = self.client.get('/template_response_view/')
|
response = self.client.get('/template_response_view/')
|
||||||
self.assertEqual(response.status_code, 200)
|
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)
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
{% load url from future %}This is where you can find the snark: {% url "snark" %}
|
{% load url from future %}This is where you can find the snark: {% url "snark" %}
|
||||||
|
{% now "U.u" %}
|
Loading…
Reference in New Issue