Commit e0dcd766 authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

Fixed #12815 -- Added TemplateResponse, a lazy-evaluated Response class....

Fixed #12815 -- Added TemplateResponse, a lazy-evaluated Response class. 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
parent 22fc30be
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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/>
+21 −1
Original line number Diff line number Diff line
@@ -103,7 +103,7 @@ class BaseTest(TestCase):
        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 @@ class BaseTest(TestCase):
            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
+24 −10
Original line number Diff line number Diff line
@@ -2,9 +2,20 @@ from django.conf.urls.defaults import *
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),
)
+12 −3
Original line number Diff line number Diff line
@@ -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 @@ class BaseHandler(object):
        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 @@ class BaseHandler(object):
                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 @@ class BaseHandler(object):
                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 @@ class BaseHandler(object):
            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)
+108 −0
Original line number Diff line number Diff line
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)
Loading