From e5a8d9e810a00898345e15f1af5c4099e746c7a0 Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Tue, 24 Mar 2009 03:01:46 +0000 Subject: [PATCH] A bunch of improvements for conditional HTTP processing. Fixed some typos in the code (fixed #10586). Added more tests. Made the tests compatible with Python 2.3. Improved the documentation by putting the good news and common use-case right up front. git-svn-id: http://code.djangoproject.com/svn/django/trunk@10134 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/views/decorators/http.py | 8 +- docs/topics/conditional-view-processing.txt | 182 +++++++++++++----- .../conditional_processing/models.py | 15 ++ .../conditional_processing/urls.py | 6 +- .../conditional_processing/views.py | 21 +- 5 files changed, 167 insertions(+), 65 deletions(-) diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index 596409dd90..a0d02bc8bb 100644 --- a/django/views/decorators/http.py +++ b/django/views/decorators/http.py @@ -127,9 +127,9 @@ def condition(etag_func=None, last_modified_func=None): return decorator # Shortcut decorators for common cases based on ETag or Last-Modified only -def etag(callable): - return condition(etag=callable) +def etag(etag_func): + return condition(etag_func=etag_func) -def last_modified(callable): - return condition(last_modified=callable) +def last_modified(last_modified_func): + return condition(last_modified_func=last_modified_func) diff --git a/docs/topics/conditional-view-processing.txt b/docs/topics/conditional-view-processing.txt index 95ad52878b..eb591a1d9c 100644 --- a/docs/topics/conditional-view-processing.txt +++ b/docs/topics/conditional-view-processing.txt @@ -28,61 +28,119 @@ client that nothing has changed. .. _If-none-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 .. _If-modified-since: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25 -Django allows simple usage of this feature with -:class:`django.middleware.http.ConditionalGetMiddleware` and -:class:`~django.middleware.common.CommonMiddleware`. However, whilst being -easy to use and suitable for many situations, they both have limitations for -advanced usage: - - * They are applied globally to all views in your project - * They don't save you from generating the response itself, which may be - expensive - * They are only appropriate for HTTP ``GET`` requests. +When you need more fine-grained control you may use per-view conditional +processing functions. .. conditional-decorators: -Decorators -========== +The ``condition`` decorator +=========================== -When you need more fine-grained control you may use per-view conditional -processing functions. - -The decorators ``django.views.decorators.http.etag`` and -``django.views.decorators.http.last_modified`` each accept a user-defined -function that takes the same parameters as the view itself. The function -passed ``last_modified`` should return a standard datetime value specifying -the last time the resource was modified, or ``None`` if the resource doesn't -exist. The function passed to the ``etag`` decorator should return a string -representing the `Etag`_ for the resource, or ``None`` if it doesn't exist. +Sometimes (in fact, quite often) you can create functions to rapidly compute the ETag_ +value or the last-modified time for a resource, **without** needing to do all +the computations needed to construct the full view. Django can then use these +functions to provide an "early bailout" option for the view processing. +Telling the client that the content has not been modified since the last +request, perhaps. .. _ETag: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11 -For example:: +These two functions are passed as parameters the +``django.views.decorators.http.condition`` decorator. This decorator uses +the two functions (you only need to supply one, if you can't compute both +quantities easily and quickly) to work out if the headers in the HTTP request +match those on the resource. If they don't match, a new copy of the resource +must be computed and your normal view is called. - # Compute the last-modified time from when the object was last saved. - @last_modified(lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time) - def my_object_view(request, obj_id): - # Expensive generation of response with MyObject instance +The ``condition`` decorator's signature looks like this:: + + condition(etag_func=None, last_modified_func=None) + +The two functions, to compute the ETag and the last modified time, will be +passed the incoming ``request`` object and the same parameters, in the same +order, as the view function they are helping to wrap. The function passed +``last_modified`` should return a standard datetime value specifying the last +time the resource was modified, or ``None`` if the resource doesn't exist. The +function passed to the ``etag`` decorator should return a string representing +the `Etag`_ for the resource, or ``None`` if it doesn't exist. + +Using this feature usefully is probably best explained with an example. +Suppose you have this pair of models, representing a simple blog system:: + + import datetime + from django.db import models + + class Blog(models.Model): ... -Of course, you can always use the non-decorator form if you're using Python -2.3 or don't like the decorator syntax:: - - def my_object_view(request, obj_id): + class Entry(models.Model): + blog = models.ForeignKey(Blog) + published = models.DateTimeField(default=datetime.datetime.now) ... - my_object_view = last_modified(my_func)(my_object_view) -Using the ``etag`` decorator is similar. +If the front page, displaying the latest blog entries, only changes when you +add a new blog entry, you can compute the last modified time very quickly. You +need the latest ``published`` date for every entry associated with that blog. +One way to do this would be:: -In practice, though, you won't know if the client is going to send the -``Last-modified`` or the ``If-none-match`` header. If you can quickly compute -both values and want to short-circuit as often as possible, you'll need to use -the ``conditional`` decorator described below. + from django.db.models import Max -HTTP allows to use both "ETag" and "Last-Modified" headers in your response. -Then a response is considered not modified only if the client sends both -headers back and they're both equal to the response headers. This means that -you can't just chain decorators on your view:: + def latest_entry(request, blog_id): + return Entry.objects.filter(blog=blog_id).aggregate(Max("published")) + +You can then use this function to provide early detection of an unchanged page +for your front page view:: + + from django.views.decorators.http import condition + + @condition(last_modified_func=latest_entry) + def front_page(request, blog_id): + ... + +Of course, if you're using Python 2.3 or prefer not to use the decorator +syntax, you can write the same code as follows, there is no difference:: + + def front_page(request, blog_id): + ... + front_page = condition(last_modified_func=latest_entry)(front_page) + +Shortcuts for only computing one value +====================================== + +As a general rule, if you can provide functions to compute *both* the ETag and +the last modified time, you should do so. You don't know which headers any +given HTTP client will send you, so be prepared to handle both. However, +sometimes only one value is easy to compute and Django provides decorators +that handle only ETag or only last-modified computations. + +The ``django.views.decorators.http.etag`` and +``django.views.decorators.http.last_modified`` decorators are passed the same +type of functions as the ``condition`` decorator. Their signatures are:: + + etag(etag_func) + last_modified(last_modified_func) + +We could write the earlier example, which only uses a last-modified function, +using one of these decorators:: + + @last_modified(latest_entry) + def front_page(request, blog_id): + ... + +...or:: + + def front_page(request, blog_id): + ... + front_page = last_modified(latest_entry)(front_page) + +Use ``condition`` when testing both conditions +------------------------------------------------ + +It might look nicer to some people to try and chain the ``etag`` and +``last_modified`` decorators if you want to test both preconditions. However, +this would lead to incorrect behavior. + +:: # Bad code. Don't do this! @etag(etag_func) @@ -94,18 +152,13 @@ you can't just chain decorators on your view:: The first decorator doesn't know anything about the second and might answer that the response is not modified even if the second decorators would -determine otherwise. In this case you should use a more general decorator - -``django.views.decorator.http.condition`` that accepts two functions at once:: - - # The correct way to implement the above example - @condition(etag_func, last_modified_func) - def my_view(request): - # ... +determine otherwise. The ``condition`` decorator uses both callback functions +simultaneously to work out the right action to take. Using the decorators with other HTTP methods ============================================ -The ``conditional`` decorator is useful for more than only ``GET`` and +The ``condition`` decorator is useful for more than only ``GET`` and ``HEAD`` requests (``HEAD`` requests are the same as ``GET`` in this situation). It can be used also to be used to provide checking for ``POST``, ``PUT`` and ``DELETE`` requests. In these situations, the idea isn't to return @@ -116,9 +169,9 @@ For example, consider the following exchange between the client and server: 1. Client requests ``/foo/``. 2. Server responds with some content with an ETag of ``"abcd1234"``. - 3. Client sends and HTTP ``PUT`` request to ``/foo/`` to update the - resource. It sends an ``If-Match: "abcd1234"`` header to specify the - version it is trying to update. + 3. Client sends an HTTP ``PUT`` request to ``/foo/`` to update the + resource. It also sends an ``If-Match: "abcd1234"`` header to specify + the version it is trying to update. 4. Server checks to see if the resource has changed, by computing the ETag the same way it does for a ``GET`` request (using the same function). If the resource *has* changed, it will return a 412 status code code, @@ -129,6 +182,29 @@ For example, consider the following exchange between the client and server: The important thing this example shows is that the same functions can be used to compute the ETag and last modification values in all situations. In fact, -you *should* use the same functions, so that the same values are returned +you **should** use the same functions, so that the same values are returned every time. +Comparison with middleware conditional processing +================================================= + +You may notice that Django already provides simple and straightforward +conditional ``GET`` handling via the +:class:`django.middleware.http.ConditionalGetMiddleware` and +:class:`~django.middleware.common.CommonMiddleware`. Whilst certainly being +easy to use and suitable for many situations, those pieces of middleware +functionality have limitations for advanced usage: + + * They are applied globally to all views in your project + * They don't save you from generating the response itself, which may be + expensive + * They are only appropriate for HTTP ``GET`` requests. + +You should choose the most appropriate tool for your particular problem here. +If you have a way to compute ETags and modification times quickly and if some +view takes a while to generate the content, you should consider using the +``condition`` decorator described in this document. If everything already runs +fairly quickly, stick to using the middleware and the amount of network +traffic sent back to the clients will still be reduced if the view hasn't +changed. + diff --git a/tests/regressiontests/conditional_processing/models.py b/tests/regressiontests/conditional_processing/models.py index 120a673859..756244ab3b 100644 --- a/tests/regressiontests/conditional_processing/models.py +++ b/tests/regressiontests/conditional_processing/models.py @@ -98,6 +98,21 @@ class ConditionalGet(TestCase): response = self.client.get('/condition/etag/') self.assertFullResponse(response, check_last_modified=False) + def testSingleCondition5(self): + self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR + response = self.client.get('/condition/last_modified2/') + self.assertNotModified(response) + response = self.client.get('/condition/etag2/') + self.assertFullResponse(response, check_last_modified=False) + + def testSingleCondition6(self): + self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG + response = self.client.get('/condition/etag2/') + self.assertNotModified(response) + response = self.client.get('/condition/last_modified2/') + self.assertFullResponse(response, check_etag=False) + + class ETagProcesing(TestCase): def testParsing(self): etags = parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"') diff --git a/tests/regressiontests/conditional_processing/urls.py b/tests/regressiontests/conditional_processing/urls.py index 938a4e4833..4dbe11a230 100644 --- a/tests/regressiontests/conditional_processing/urls.py +++ b/tests/regressiontests/conditional_processing/urls.py @@ -3,6 +3,8 @@ import views urlpatterns = patterns('', ('^$', views.index), - ('^last_modified/$', views.last_modified), - ('^etag/$', views.etag), + ('^last_modified/$', views.last_modified_view1), + ('^last_modified2/$', views.last_modified_view2), + ('^etag/$', views.etag_view1), + ('^etag2/$', views.etag_view2), ) diff --git a/tests/regressiontests/conditional_processing/views.py b/tests/regressiontests/conditional_processing/views.py index c88236e21b..df4928133d 100644 --- a/tests/regressiontests/conditional_processing/views.py +++ b/tests/regressiontests/conditional_processing/views.py @@ -1,17 +1,26 @@ # -*- coding:utf-8 -*- -from django.views.decorators.http import condition +from django.views.decorators.http import condition, etag, last_modified from django.http import HttpResponse from models import FULL_RESPONSE, LAST_MODIFIED, ETAG -@condition(lambda r: ETAG, lambda r: LAST_MODIFIED) def index(request): return HttpResponse(FULL_RESPONSE) +index = condition(lambda r: ETAG, lambda r: LAST_MODIFIED)(index) -@condition(last_modified_func=lambda r: LAST_MODIFIED) -def last_modified(request): +def last_modified_view1(request): return HttpResponse(FULL_RESPONSE) +last_modified_view1 = condition(last_modified_func=lambda r: LAST_MODIFIED)(last_modified_view1) -@condition(etag_func=lambda r: ETAG) -def etag(request): +def last_modified_view2(request): return HttpResponse(FULL_RESPONSE) +last_modified_view2 = last_modified(lambda r: LAST_MODIFIED)(last_modified_view2) + +def etag_view1(request): + return HttpResponse(FULL_RESPONSE) +etag_view1 = condition(etag_func=lambda r: ETAG)(etag_view1) + +def etag_view2(request): + return HttpResponse(FULL_RESPONSE) +etag_view2 = etag(lambda r: ETAG)(etag_view2) +