Skip to content

Commit

Permalink
Fixed #12815 -- Added TemplateResponse, a lazy-evaluated Response cla…
Browse files Browse the repository at this point in the history
…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
freakboy3742 committed Dec 7, 2010
1 parent 22fc30b commit e0dcd76
Show file tree
Hide file tree
Showing 19 changed files with 842 additions and 210 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -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/>
Expand Down
22 changes: 21 additions & 1 deletion django/contrib/messages/tests/base.py
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
34 changes: 24 additions & 10 deletions django/contrib/messages/tests/urls.py
Expand Up @@ -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
Expand All @@ -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),
)
15 changes: 12 additions & 3 deletions django/core/handlers/base.py
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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'):
Expand Down Expand Up @@ -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)
Expand Down
108 changes: 108 additions & 0 deletions django/template/response.py
@@ -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)
51 changes: 11 additions & 40 deletions django/views/generic/base.py
@@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down
4 changes: 3 additions & 1 deletion docs/index.txt
Expand Up @@ -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>` |
Expand Down

0 comments on commit e0dcd76

Please sign in to comment.