Permalink
Browse files

Fixed #12815 -- Added TemplateResponse, a lazy-evaluated Response cla…

…ss. Thanks to Simon Willison for the original idea, and to Mikhail Korobov and Ivan Sagalaev for their assistance, including the draft patch from Mikhail.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14850 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent 22fc30b commit e0dcd7666aec15a2348bef346b4ce683ddf376b3 @freakboy3742 freakboy3742 committed Dec 7, 2010
View
@@ -273,6 +273,7 @@ answer newbie questions, and generally made Django that much better:
Igor Kolar <ike@email.si>
Tomáš Kopeček <permonik@m6.cz>
Gasper Koren
+ Mikhail Korobov <kmike84@googlemail.com>
Martin Kosír <martin@martinkosir.net>
Arthur Koziel <http://arthurkoziel.com>
Meir Kriheli <http://mksoft.co.il/>
@@ -103,7 +103,7 @@ def test_add(self):
storage = self.get_storage()
self.assertFalse(storage.added_new)
storage.add(constants.INFO, 'Test message 1')
- self.assert_(storage.added_new)
+ self.assertTrue(storage.added_new)
storage.add(constants.INFO, 'Test message 2', extra_tags='tag')
self.assertEqual(len(storage), 2)
@@ -180,6 +180,26 @@ def test_full_request_response_cycle(self):
for msg in data['messages']:
self.assertContains(response, msg)
+ def test_with_template_response(self):
+ settings.MESSAGE_LEVEL = constants.DEBUG
+ data = {
+ 'messages': ['Test message %d' % x for x in xrange(10)],
+ }
+ show_url = reverse('django.contrib.messages.tests.urls.show_template_response')
+ for level in self.levels.keys():
+ add_url = reverse('django.contrib.messages.tests.urls.add_template_response',
+ args=(level,))
+ response = self.client.post(add_url, data, follow=True)
+ self.assertRedirects(response, show_url)
+ self.assertTrue('messages' in response.context)
+ for msg in data['messages']:
+ self.assertContains(response, msg)
+
+ # there shouldn't be any messages on second GET request
+ response = self.client.get(show_url)
+ for msg in data['messages']:
+ self.assertNotContains(response, msg)
+
def test_multiple_posts(self):
"""
Tests that messages persist properly when multiple POSTs are made
@@ -2,9 +2,20 @@
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, HttpResponse
-from django.shortcuts import render_to_response
+from django.shortcuts import render_to_response, redirect
from django.template import RequestContext, Template
+from django.template.response import TemplateResponse
+TEMPLATE = """{% if messages %}
+<ul class="messages">
+ {% for message in messages %}
+ <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
+ {{ message }}
+ </li>
+ {% endfor %}
+</ul>
+{% endif %}
+"""
def add(request, message_type):
# don't default to False here, because we want to test that it defaults
@@ -16,24 +27,27 @@ def add(request, message_type):
fail_silently=fail_silently)
else:
getattr(messages, message_type)(request, msg)
+
show_url = reverse('django.contrib.messages.tests.urls.show')
return HttpResponseRedirect(show_url)
+def add_template_response(request, message_type):
+ for msg in request.POST.getlist('messages'):
+ getattr(messages, message_type)(request, msg)
+
+ show_url = reverse('django.contrib.messages.tests.urls.show_template_response')
+ return HttpResponseRedirect(show_url)
def show(request):
- t = Template("""{% if messages %}
-<ul class="messages">
- {% for message in messages %}
- <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
- {{ message }}
- </li>
- {% endfor %}
-</ul>
-{% endif %}""")
+ t = Template(TEMPLATE)
return HttpResponse(t.render(RequestContext(request)))
+def show_template_response(request):
+ return TemplateResponse(request, Template(TEMPLATE))
urlpatterns = patterns('',
('^add/(debug|info|success|warning|error)/$', add),
('^show/$', show),
+ ('^template_response/add/(debug|info|success|warning|error)/$', add_template_response),
+ ('^template_response/show/$', show_template_response),
)
@@ -21,6 +21,7 @@ class BaseHandler(object):
def __init__(self):
self._request_middleware = self._view_middleware = self._response_middleware = self._exception_middleware = None
+
def load_middleware(self):
"""
Populate middleware lists from settings.MIDDLEWARE_CLASSES.
@@ -30,16 +31,16 @@ def load_middleware(self):
from django.conf import settings
from django.core import exceptions
self._view_middleware = []
+ self._template_response_middleware = []
self._response_middleware = []
self._exception_middleware = []
request_middleware = []
for middleware_path in settings.MIDDLEWARE_CLASSES:
try:
- dot = middleware_path.rindex('.')
+ mw_module, mw_classname = middleware_path.rsplit('.', 1)
except ValueError:
raise exceptions.ImproperlyConfigured('%s isn\'t a middleware module' % middleware_path)
- mw_module, mw_classname = middleware_path[:dot], middleware_path[dot+1:]
try:
mod = import_module(mw_module)
except ImportError, e:
@@ -48,7 +49,6 @@ def load_middleware(self):
mw_class = getattr(mod, mw_classname)
except AttributeError:
raise exceptions.ImproperlyConfigured('Middleware module "%s" does not define a "%s" class' % (mw_module, mw_classname))
-
try:
mw_instance = mw_class()
except exceptions.MiddlewareNotUsed:
@@ -58,6 +58,8 @@ def load_middleware(self):
request_middleware.append(mw_instance.process_request)
if hasattr(mw_instance, 'process_view'):
self._view_middleware.append(mw_instance.process_view)
+ if hasattr(mw_instance, 'process_template_response'):
+ self._template_response_middleware.insert(0, mw_instance.process_template_response)
if hasattr(mw_instance, 'process_response'):
self._response_middleware.insert(0, mw_instance.process_response)
if hasattr(mw_instance, 'process_exception'):
@@ -164,6 +166,13 @@ def get_response(self, request):
urlresolvers.set_urlconf(None)
try:
+ # If the response supports deferred rendering, apply template
+ # response middleware and the render the response
+ if hasattr(response, 'render') and callable(response.render):
+ for middleware_method in self._template_response_middleware:
+ response = middleware_method(request, response)
+ response.render()
+
# Apply response middleware, regardless of the response
for middleware_method in self._response_middleware:
response = middleware_method(request, response)
@@ -0,0 +1,108 @@
+from django.http import HttpResponse
+from django.template import loader, Context, RequestContext
+
+class ContentNotRenderedError(Exception):
+ pass
+
+class SimpleTemplateResponse(HttpResponse):
+
+ def __init__(self, template, context=None, mimetype=None, status=None,
+ content_type=None):
+ # It would seem obvious to call these next two members 'template' and
+ # 'context', but those names are reserved as part of the test Client API.
+ # To avoid the name collision, we use
+ # tricky-to-debug problems
+ self.template_name = template
+ self.context_data = context
+
+ # _is_rendered tracks whether the template and context has been baked into
+ # a final response.
+ self._is_rendered = False
+
+ # 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 resolve_template(self, template):
+ "Accepts a template object, path-to-template or list of paths"
+ if isinstance(template, (list, tuple)):
+ return loader.select_template(template)
+ elif isinstance(template, basestring):
+ return loader.get_template(template)
+ else:
+ return template
+
+ def resolve_context(self, context):
+ """Convert context data into a full Context object
+ (assuming it isn't already a Context object).
+ """
+ if isinstance(context, Context):
+ return context
+ else:
+ return Context(context)
+
+ @property
+ def rendered_content(self):
+ """Returns the freshly rendered content for the template and context
+ described by the TemplateResponse.
+
+ This *does not* set the final content of the response. To set the
+ response content, you must either call render(), or set the
+ content explicitly using the value of this property.
+ """
+ template = self.resolve_template(self.template_name)
+ context = self.resolve_context(self.context_data)
+ content = template.render(context)
+ return content
+
+ def render(self):
+ """Render (thereby finalizing) the content of the response.
+
+ If the content has already been rendered, this is a no-op.
+
+ Returns the baked response instance.
+ """
+ if not self._is_rendered:
+ self._set_content(self.rendered_content)
+ return self
+
+ is_rendered = property(lambda self: self._is_rendered)
+
+ def __iter__(self):
+ if not self._is_rendered:
+ raise ContentNotRenderedError('The response content must be rendered before it can be iterated over.')
+ return super(SimpleTemplateResponse, self).__iter__()
+
+ def _get_content(self):
+ if not self._is_rendered:
+ raise ContentNotRenderedError('The response content must be rendered before it can be accessed.')
+ return super(SimpleTemplateResponse, self)._get_content()
+
+ def _set_content(self, value):
+ "Overrides rendered content, unless you later call render()"
+ super(SimpleTemplateResponse, self)._set_content(value)
+ self._is_rendered = True
+
+ content = property(_get_content, _set_content)
+
+
+class TemplateResponse(SimpleTemplateResponse):
+ def __init__(self, request, template, context=None, mimetype=None,
+ status=None, content_type=None):
+ # self.request gets over-written by django.test.client.Client - and
+ # unlike context_data and template_name the _request should not
+ # be considered part of the public API.
+ self._request = request
+ super(TemplateResponse, self).__init__(
+ template, context, mimetype, status, content_type)
+
+ def resolve_context(self, context):
+ """Convert context data into a full RequestContext object
+ (assuming it isn't already a Context object).
+ """
+ if isinstance(context, Context):
+ return context
+ else:
+ return RequestContext(self._request, context)
@@ -1,6 +1,7 @@
from django import http
from django.core.exceptions import ImproperlyConfigured
from django.template import RequestContext, loader
+from django.template.response import TemplateResponse
from django.utils.functional import update_wrapper
from django.utils.log import getLogger
from django.utils.decorators import classonlymethod
@@ -81,59 +82,29 @@ class TemplateResponseMixin(object):
A mixin that can be used to render a template.
"""
template_name = None
+ response_class = TemplateResponse
- def render_to_response(self, context):
+ def render_to_response(self, context, **response_kwargs):
"""
Returns a response with a template rendered with the given context.
"""
- return self.get_response(self.render_template(context))
-
- def get_response(self, content, **httpresponse_kwargs):
- """
- Construct an `HttpResponse` object.
- """
- return http.HttpResponse(content, **httpresponse_kwargs)
-
- def render_template(self, context):
- """
- Render the template with a given context.
- """
- context_instance = self.get_context_instance(context)
- return self.get_template().render(context_instance)
-
- def get_context_instance(self, context):
- """
- Get the template context instance. Must return a Context (or subclass)
- instance.
- """
- return RequestContext(self.request, context)
-
- def get_template(self):
- """
- Get a ``Template`` object for the given request.
- """
- names = self.get_template_names()
- if not names:
- raise ImproperlyConfigured(u"'%s' must provide template_name."
- % self.__class__.__name__)
- return self.load_template(names)
+ return self.response_class(
+ request = self.request,
+ template = self.get_template_names(),
+ context = context,
+ **response_kwargs
+ )
def get_template_names(self):
"""
- Return a list of template names to be used for the request. Must return
- a list. May not be called if get_template is overridden.
+ Returns a list of template names to be used for the request. Must return
+ a list. May not be called if render_to_response is overridden.
"""
if self.template_name is None:
return []
else:
return [self.template_name]
- def load_template(self, names):
- """
- Load a list of templates using the default template loader.
- """
- return loader.select_template(names)
-
class TemplateView(TemplateResponseMixin, View):
"""
View
@@ -93,7 +93,9 @@ The view layer
:doc:`View functions <topics/http/views>` |
:doc:`Shortcuts <topics/http/shortcuts>`
- * **Reference:** :doc:`Request/response objects <ref/request-response>`
+ * **Reference:**
+ :doc:`Request/response objects <ref/request-response>` |
+ :doc:`TemplateResponse objects <ref/template-response>`
* **File uploads:**
:doc:`Overview <topics/http/file-uploads>` |
Oops, something went wrong.

0 comments on commit e0dcd76

Please sign in to comment.