diff -r c67d03486d14 django/contrib/messages/middleware.py --- a/django/contrib/messages/middleware.py Wed Oct 20 20:16:36 2010 +0000 +++ b/django/contrib/messages/middleware.py Sun Oct 24 16:32:55 2010 +0600 @@ -20,6 +20,12 @@ # A higher middleware layer may return a request which does not contain # messages storage, so make no assumption that it will be there. if hasattr(request, '_messages'): + + # we have to bake the response here because there is no way to + # determine if messages where iterated in template or not + if hasattr(response, 'bake') and callable(response.bake): + response.bake() + unstored_messages = request._messages.update(response) if unstored_messages and settings.DEBUG: raise ValueError('Not all temporary messages could be stored.') diff -r c67d03486d14 django/contrib/messages/tests/base.py --- a/django/contrib/messages/tests/base.py Wed Oct 20 20:16:36 2010 +0000 +++ b/django/contrib/messages/tests/base.py Sun Oct 24 16:32:55 2010 +0600 @@ -96,7 +96,7 @@ 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) @@ -173,6 +173,27 @@ 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(self.urls + '.show_template_response') + + for level in self.levels.keys(): + add_url = reverse(self.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 diff -r c67d03486d14 django/contrib/messages/tests/urls.py --- a/django/contrib/messages/tests/urls.py Wed Oct 20 20:16:36 2010 +0000 +++ b/django/contrib/messages/tests/urls.py Sun Oct 24 16:32:55 2010 +0600 @@ -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 %} +
+{% endif %} +""" def add(request, message_type): # don't default to False here, because we want to test that it defaults @@ -16,24 +27,25 @@ 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) + return redirect('django.contrib.messages.tests.urls.show_template_response') def show(request): - t = Template("""{% if messages %} - + {% endfor %} + -{% 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), + ('^add-template-response/(debug|info|success|warning|error)/$', add_template_response), + ('^show-template-response/$', show_template_response), ) diff -r c67d03486d14 django/shortcuts/__init__.py --- a/django/shortcuts/__init__.py Wed Oct 20 20:16:36 2010 +0000 +++ b/django/shortcuts/__init__.py Sun Oct 24 16:32:55 2010 +0600 @@ -5,6 +5,7 @@ """ from django.template import loader +from django.template.response import TemplateResponse from django.http import HttpResponse, Http404 from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect from django.db.models.manager import Manager @@ -23,16 +24,16 @@ """ Returns an HttpResponseRedirect to the apropriate URL for the arguments passed. - + The arguments could be: - + * A model: the model's `get_absolute_url()` function will be called. - + * A view name, possibly with arguments: `urlresolvers.reverse()` will be used to reverse-resolve the name. - + * A URL, which will be used as-is for the redirect location. - + By default issues a temporary redirect; pass permanent=True to issue a permanent redirect """ @@ -40,11 +41,11 @@ redirect_class = HttpResponsePermanentRedirect else: redirect_class = HttpResponseRedirect - + # If it's a model, use get_absolute_url() if hasattr(to, 'get_absolute_url'): return redirect_class(to.get_absolute_url()) - + # Next try a reverse URL resolution. try: return redirect_class(urlresolvers.reverse(to, args=args, kwargs=kwargs)) @@ -55,7 +56,7 @@ # If this doesn't "feel" like a URL, re-raise. if '/' not in to and '.' not in to: raise - + # Finally, fall back and assume it's a URL return redirect_class(to) @@ -101,4 +102,6 @@ obj_list = list(queryset.filter(*args, **kwargs)) if not obj_list: raise Http404('No %s matches the given query.' % queryset.model._meta.object_name) - return obj_list \ No newline at end of file + return obj_list + +render = TemplateResponse diff -r c67d03486d14 django/template/response.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/django/template/response.py Sun Oct 24 16:32:55 2010 +0600 @@ -0,0 +1,89 @@ +from django.http import HttpResponse +from django.template import loader, Context, RequestContext + +class SimpleTemplateResponse(HttpResponse): + + def __init__(self, template, context=None, mimetype=None, status=None, + content_type=None): + # These two properties were originally called 'template' and 'context' + # but django.test.client.Client was clobbering those leading to really + # tricky-to-debug problems + self.template_name = template + self.template_context = context + self.baked = 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): + "context can be a dictionary or a context object" + if isinstance(context, Context): + return context + else: + return Context(context) + + def render(self): + template = self.resolve_template(self.template_name) + context = self.resolve_context(self.template_context) + content = template.render(context) + return content + + def bake(self): + """ + The template is baked the first time you try to access + response.content or iterate over it. This is a bit ugly, but is + necessary because Django middleware sometimes expects to be able to + over-write the content of a response. + """ + if not self.baked: + self.force_bake() + + def force_bake(self): + "Call this if you have modified the template or context but are " + "unsure if the template has already been baked." + self._set_content(self.render()) + + def __iter__(self): + self.bake() + return super(SimpleTemplateResponse, self).__iter__() + + def _get_content(self): + self.bake() + return super(SimpleTemplateResponse, self)._get_content() + + def _set_content(self, value): + "Overrides rendered content, unless you later call force_bake()" + super(SimpleTemplateResponse, self)._set_content(value) + self.baked = 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 template_context 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): + if isinstance(context, Context): + return context + else: + return RequestContext(self._request, context) + diff -r c67d03486d14 django/test/client.py --- a/django/test/client.py Wed Oct 20 20:16:36 2010 +0000 +++ b/django/test/client.py Sun Oct 24 16:32:55 2010 +0600 @@ -379,6 +379,9 @@ try: response = self.handler(environ) + if hasattr(response, 'bake') and callable(response.bake): + response.bake() + except TemplateDoesNotExist, e: # If the view raises an exception, Django will attempt to show # the 500.html template. If that template is not available, diff -r c67d03486d14 django/test/testcases.py --- a/django/test/testcases.py Wed Oct 20 20:16:36 2010 +0000 +++ b/django/test/testcases.py Sun Oct 24 16:32:55 2010 +0600 @@ -473,6 +473,9 @@ Asserts that the template with the provided name was used in rendering the response. """ + if hasattr(response, 'bake') and callable(response.bake): + response.bake() + if msg_prefix: msg_prefix += ": " diff -r c67d03486d14 django/views/generic/base.py --- a/django/views/generic/base.py Wed Oct 20 20:16:36 2010 +0000 +++ b/django/views/generic/base.py Sun Oct 24 16:32:55 2010 +0600 @@ -2,6 +2,7 @@ from django import http from django.core.exceptions import ImproperlyConfigured from django.template import RequestContext, loader +from django.template.response import SimpleTemplateResponse from django.utils.translation import ugettext_lazy as _ from django.utils.functional import update_wrapper from django.utils.log import getLogger @@ -89,24 +90,15 @@ """ template_name = None - def render_to_response(self, context): + def render_to_response(self, context, **httpresponse_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) + return SimpleTemplateResponse( + self.get_template(), + self.get_context_instance(context), + **httpresponse_kwargs + ) def get_context_instance(self, context): """ diff -r c67d03486d14 tests/regressiontests/templates/response.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/regressiontests/templates/response.py Sun Oct 24 16:32:55 2010 +0600 @@ -0,0 +1,164 @@ +import os +from django.utils import unittest +from django.test import RequestFactory +from django.conf import settings +from django.template import Template, Context, RequestContext +from django.template.response import TemplateResponse, SimpleTemplateResponse +import django.template.context + +def test_processor(request): + return {'processors': 'yes'} +test_processor_name = 'regressiontests.templates.response.test_processor' + +class BaseTemplateResponseTest(unittest.TestCase): + # tests rely on fact that global context + # processors should only work when RequestContext is used. + + def setUp(self): + self.factory = RequestFactory() + + cp = list(settings.TEMPLATE_CONTEXT_PROCESSORS) + self._old_processors = cp[:] + self._old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS + settings.TEMPLATE_CONTEXT_PROCESSORS = cp + [test_processor_name] + settings.TEMPLATE_DIRS = ( + os.path.join( + os.path.dirname(__file__), + 'templates' + ), + ) + + def tearDown(self): + settings.TEMPLATE_DIRS = self._old_TEMPLATE_DIRS + settings.TEMPLATE_CONTEXT_PROCESSORS = tuple(self._old_processors) + # Force re-evaluation of the contex processor list + django.template.context._standard_context_processors = None + + +class SimpleTemplateResponseTest(BaseTemplateResponseTest): + + def _response(self, template='foo', *args, **kwargs): + return SimpleTemplateResponse(Template(template), *args, **kwargs) + + def test_template_resolving(self): + response = SimpleTemplateResponse('first/test.html') + self.assertEqual('First template\n', response.content) + + templates = ['foo.html', 'second/test.html', 'first/test.html'] + response = SimpleTemplateResponse(templates) + self.assertEqual('Second template\n', response.content) + + response = self._response() + self.assertEqual(response.content, 'foo') + + def test_explicit_baking(self): + # explicit baking + response = self._response() + self.assertFalse(response.baked) + response.bake() + self.assertTrue(response.baked) + + def test_force_bake(self): + # response is not re-baked without the force_bake call + response = self._response() + self.assertFalse(response.baked) + self.assertEqual(response.content, 'foo') + + response.template_name = Template('bar{{ baz }}') + self.assertEqual(response.content, 'foo') + response.bake() + self.assertEqual(response.content, 'foo') + response.force_bake() + self.assertEqual(response.content, 'bar') + + response.template_context = {'baz': 'baz'} + self.assertEqual(response.content, 'bar') + response.bake() + self.assertEqual(response.content, 'bar') + response.force_bake() + self.assertEqual(response.content, 'barbaz') + + response.template_context = {'baz': 'spam'} + for x in response: + pass + self.assertEqual(response.content, 'barbaz') + + def test_baking_on_iteration(self): + # response becomes baked after first iteration + response = self._response() + self.assertFalse(response.baked) + for x in response: + pass + self.assertTrue(response.baked) + + def test_baking_on_access(self): + # the response is baked when content is accessed + response = self._response() + self.assertFalse(response.baked) + content = response.content + self.assertTrue(response.baked) + self.assertEqual(content, 'foo') + + def test_set_content(self): + # content can be overriden + response = self._response() + self.assertFalse(response.baked) + response.content = 'spam' + self.assertTrue(response.baked) + self.assertEqual(response.content, 'spam') + response.content = 'baz' + self.assertEqual(response.content, 'baz') + + def test_dict_context(self): + response = self._response('{{ foo }}{{ processors }}', + {'foo': 'bar'}) + self.assertEqual(response.template_context, {'foo': 'bar'}) + self.assertEqual(response.content, 'bar') + + def test_context_instance(self): + response = self._response('{{ foo }}{{ processors }}', + Context({'foo': 'bar'})) + self.assertEqual(response.template_context.__class__, Context) + self.assertEqual(response.content, 'bar') + + def test_kwargs(self): + response = self._response(content_type = 'application/json', status=504) + self.assertEqual(response['content-type'], 'application/json') + self.assertEqual(response.status_code, 504) + + def test_args(self): + response = SimpleTemplateResponse('', {}, 'application/json', 504) + self.assertEqual(response['content-type'], 'application/json') + self.assertEqual(response.status_code, 504) + + +class TemplateResponseTest(BaseTemplateResponseTest): + + def _response(self, template='foo', *args, **kwargs): + return TemplateResponse(self.factory.get('/'), Template(template), + *args, **kwargs) + + def test_render(self): + response = self._response('{{ foo }}{{ processors }}') + self.assertEqual(response.content, 'yes') + + def test_render_with_requestcontext(self): + response = self._response('{{ foo }}{{ processors }}', + {'foo': 'bar'}) + self.assertEqual(response.content, 'baryes') + + def test_render_with_context(self): + response = self._response('{{ foo }}{{ processors }}', + Context({'foo': 'bar'})) + self.assertEqual(response.content, 'bar') + + def test_kwargs(self): + response = self._response(content_type = 'application/json', status=504) + self.assertEqual(response['content-type'], 'application/json') + self.assertEqual(response.status_code, 504) + + def test_args(self): + response = TemplateResponse(self.factory.get('/'), '', {}, + 'application/json', 504) + self.assertEqual(response['content-type'], 'application/json') + self.assertEqual(response.status_code, 504) diff -r c67d03486d14 tests/regressiontests/templates/tests.py --- a/tests/regressiontests/templates/tests.py Wed Oct 20 20:16:36 2010 +0000 +++ b/tests/regressiontests/templates/tests.py Sun Oct 24 16:32:55 2010 +0600 @@ -27,6 +27,7 @@ from unicode import unicode_tests from nodelist import NodelistTest from smartif import * +from response import * try: from loaders import * diff -r c67d03486d14 tests/regressiontests/views/templates/debug/render_test.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/regressiontests/views/templates/debug/render_test.html Sun Oct 24 16:32:55 2010 +0600 @@ -0,0 +1,1 @@ +{{ foo }}.{{ bar }}.{{ baz }}.{{ processors }} diff -r c67d03486d14 tests/regressiontests/views/tests/__init__.py --- a/tests/regressiontests/views/tests/__init__.py Wed Oct 20 20:16:36 2010 +0000 +++ b/tests/regressiontests/views/tests/__init__.py Sun Oct 24 16:32:55 2010 +0600 @@ -7,3 +7,4 @@ from i18n import * from specials import * from static import * +from shortcuts import * diff -r c67d03486d14 tests/regressiontests/views/tests/shortcuts.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/regressiontests/views/tests/shortcuts.py Sun Oct 24 16:32:55 2010 +0600 @@ -0,0 +1,45 @@ +from django.test import RequestFactory +from django.template import Template +from django.shortcuts import render + +from regressiontests.templates.response import BaseTemplateResponseTest + +def library_view(request): + return render(request, 'debug/render_test.html', + {'foo': 'foo', 'baz': 'baz'}) + +def customized_context_view(request): + response = library_view(request) + response.template_context.update({'bar': 'bar', 'baz': 'spam'}) + return response + +def customized_template_view(request): + response = library_view(request) + response.template_name = Template('context does not matter') + return response + + +class RenderTest(BaseTemplateResponseTest): + + def setUp(self): + super(RenderTest, self).setUp() + self.request = self.factory.get('/') + + def test_library_view(self): + response = library_view(self.request) + self.assertEqual(response.content, 'foo..baz.yes\n') + + def test_customized_context(self): + response = customized_context_view(self.request) + self.assertEqual(response.content, 'foo.bar.spam.yes\n') + + def test_customized_template(self): + response = customized_template_view(self.request) + self.assertEqual(response.content, 'context does not matter') + + def test_status_and_content_type(self): + response = render(self.request, 'base.html', {'foo': 'bar'}, + 'application/json', 504) + self.assertEqual(response.status_code, 504) + self.assertEqual(response['content-type'], 'application/json') + - {% endfor %} -