mirror of
				https://github.com/django/django.git
				synced 2025-11-03 21:25:09 +00:00 
			
		
		
		
	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
					
				
					 9 changed files with 256 additions and 8 deletions
				
			
		| 
						 | 
				
			
			@ -52,6 +52,7 @@ from django.conf import settings
 | 
			
		|||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateCacheMiddleware(object):
 | 
			
		||||
    """
 | 
			
		||||
    Response-phase cache middleware that updates the cache if the response is
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +88,12 @@ class UpdateCacheMiddleware(object):
 | 
			
		|||
        patch_response_headers(response, timeout)
 | 
			
		||||
        if timeout:
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
class FetchFromCacheMiddleware(object):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,12 +19,30 @@ class SimpleTemplateResponse(HttpResponse):
 | 
			
		|||
        # a final response.
 | 
			
		||||
        self._is_rendered = False
 | 
			
		||||
 | 
			
		||||
        self._post_render_callbacks = []
 | 
			
		||||
 | 
			
		||||
        # 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 __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):
 | 
			
		||||
        "Accepts a template object, path-to-template or list of paths"
 | 
			
		||||
        if isinstance(template, (list, tuple)):
 | 
			
		||||
| 
						 | 
				
			
			@ -57,6 +75,16 @@ class SimpleTemplateResponse(HttpResponse):
 | 
			
		|||
        content = template.render(context)
 | 
			
		||||
        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):
 | 
			
		||||
        """Render (thereby finalizing) the content of the response.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -66,6 +94,8 @@ class SimpleTemplateResponse(HttpResponse):
 | 
			
		|||
        """
 | 
			
		||||
        if not self._is_rendered:
 | 
			
		||||
            self._set_content(self.rendered_content)
 | 
			
		||||
            for post_callback in self._post_render_callbacks:
 | 
			
		||||
                post_callback(self)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    is_rendered = property(lambda self: self._is_rendered)
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +111,7 @@ class SimpleTemplateResponse(HttpResponse):
 | 
			
		|||
        return super(SimpleTemplateResponse, self)._get_content()
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        self._is_rendered = True
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -101,6 +131,20 @@ class TemplateResponse(SimpleTemplateResponse):
 | 
			
		|||
        super(TemplateResponse, self).__init__(
 | 
			
		||||
            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):
 | 
			
		||||
        """Convert context data into a full RequestContext object
 | 
			
		||||
        (assuming it isn't already a Context object).
 | 
			
		||||
| 
						 | 
				
			
			@ -109,3 +153,5 @@ class TemplateResponse(SimpleTemplateResponse):
 | 
			
		|||
            return context
 | 
			
		||||
        else:
 | 
			
		||||
            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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Methods
 | 
			
		||||
-------
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -106,6 +105,20 @@ Methods
 | 
			
		|||
 | 
			
		||||
    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():
 | 
			
		||||
 | 
			
		||||
    Sets :attr:`response.content` to the result obtained by
 | 
			
		||||
| 
						 | 
				
			
			@ -211,6 +224,50 @@ the content of the response manually::
 | 
			
		|||
    >>> print t.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
 | 
			
		||||
=================================================
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
import time
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
from django.core.exceptions import ImproperlyConfigured
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +159,7 @@ class TemplateViewTest(TestCase):
 | 
			
		|||
    def _assert_about(self, response):
 | 
			
		||||
        response.render()
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        self.assertEqual(response.content, '<h1>About</h1>')
 | 
			
		||||
        self.assertContains(response, '<h1>About</h1>')
 | 
			
		||||
 | 
			
		||||
    def test_get(self):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -197,6 +198,28 @@ class TemplateViewTest(TestCase):
 | 
			
		|||
        self.assertEqual(response.context['params'], {'foo': 'bar'})
 | 
			
		||||
        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):
 | 
			
		||||
    rf = RequestFactory()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1,2 @@
 | 
			
		|||
<h1>About</h1>
 | 
			
		||||
{% now "U.u" %}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
from django.conf.urls.defaults import *
 | 
			
		||||
from django.views.generic import TemplateView
 | 
			
		||||
from django.views.decorators.cache import cache_page
 | 
			
		||||
 | 
			
		||||
import views
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +16,9 @@ urlpatterns = patterns('',
 | 
			
		|||
    (r'^template/custom/(?P<foo>\w+)/$',
 | 
			
		||||
        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
 | 
			
		||||
    (r'^detail/obj/$',
 | 
			
		||||
        views.ObjectDetail.as_view()),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,12 @@
 | 
			
		|||
# coding: utf-8
 | 
			
		||||
from django.conf.urls.defaults import *
 | 
			
		||||
 | 
			
		||||
from regressiontests.templates import views
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
urlpatterns = patterns('',
 | 
			
		||||
    # 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...
 | 
			
		||||
    url(r'^snark/', views.snark, name='snark'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,7 @@
 | 
			
		|||
from datetime import datetime
 | 
			
		||||
import os
 | 
			
		||||
import pickle
 | 
			
		||||
import time
 | 
			
		||||
from django.utils import unittest
 | 
			
		||||
from django.test import RequestFactory, TestCase
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
| 
						 | 
				
			
			@ -147,6 +150,49 @@ class SimpleTemplateResponseTest(BaseTemplateResponseTest):
 | 
			
		|||
        self.assertEqual(response['content-type'], 'application/json')
 | 
			
		||||
        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):
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -187,6 +233,33 @@ class TemplateResponseTest(BaseTemplateResponseTest):
 | 
			
		|||
 | 
			
		||||
        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):
 | 
			
		||||
    urls = 'regressiontests.templates.urls'
 | 
			
		||||
| 
						 | 
				
			
			@ -203,6 +276,41 @@ class CustomURLConfTest(TestCase):
 | 
			
		|||
    def test_custom_urlconf(self):
 | 
			
		||||
        response = self.client.get('/template_response_view/')
 | 
			
		||||
        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" %}
 | 
			
		||||
{% now "U.u" %}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue