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
This commit is contained in:
Malcolm Tredinnick 2009-03-24 03:01:46 +00:00
parent 2fb7f5ea2b
commit e5a8d9e810
5 changed files with 167 additions and 65 deletions

View File

@ -127,9 +127,9 @@ def condition(etag_func=None, last_modified_func=None):
return decorator return decorator
# Shortcut decorators for common cases based on ETag or Last-Modified only # Shortcut decorators for common cases based on ETag or Last-Modified only
def etag(callable): def etag(etag_func):
return condition(etag=callable) return condition(etag_func=etag_func)
def last_modified(callable): def last_modified(last_modified_func):
return condition(last_modified=callable) return condition(last_modified_func=last_modified_func)

View File

@ -28,61 +28,119 @@ client that nothing has changed.
.. _If-none-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 .. _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 .. _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.
.. conditional-decorators:
Decorators
==========
When you need more fine-grained control you may use per-view conditional When you need more fine-grained control you may use per-view conditional
processing functions. processing functions.
The decorators ``django.views.decorators.http.etag`` and .. conditional-decorators:
``django.views.decorators.http.last_modified`` each accept a user-defined
function that takes the same parameters as the view itself. The function The ``condition`` decorator
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 Sometimes (in fact, quite often) you can create functions to rapidly compute the ETag_
representing the `Etag`_ for the resource, or ``None`` if it doesn't exist. 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 .. _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. The ``condition`` decorator's signature looks like this::
@last_modified(lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time)
def my_object_view(request, obj_id): condition(etag_func=None, last_modified_func=None)
# Expensive generation of response with MyObject instance
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 class Entry(models.Model):
2.3 or don't like the decorator syntax:: blog = models.ForeignKey(Blog)
published = models.DateTimeField(default=datetime.datetime.now)
def my_object_view(request, obj_id):
... ...
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 from django.db.models import Max
``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.
HTTP allows to use both "ETag" and "Last-Modified" headers in your response. def latest_entry(request, blog_id):
Then a response is considered not modified only if the client sends both return Entry.objects.filter(blog=blog_id).aggregate(Max("published"))
headers back and they're both equal to the response headers. This means that
you can't just chain decorators on your view:: 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! # Bad code. Don't do this!
@etag(etag_func) @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 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 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 - determine otherwise. The ``condition`` decorator uses both callback functions
``django.views.decorator.http.condition`` that accepts two functions at once:: simultaneously to work out the right action to take.
# The correct way to implement the above example
@condition(etag_func, last_modified_func)
def my_view(request):
# ...
Using the decorators with other HTTP methods 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 ``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``, 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 ``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/``. 1. Client requests ``/foo/``.
2. Server responds with some content with an ETag of ``"abcd1234"``. 2. Server responds with some content with an ETag of ``"abcd1234"``.
3. Client sends and HTTP ``PUT`` request to ``/foo/`` to update the 3. Client sends an HTTP ``PUT`` request to ``/foo/`` to update the
resource. It sends an ``If-Match: "abcd1234"`` header to specify the resource. It also sends an ``If-Match: "abcd1234"`` header to specify
version it is trying to update. the version it is trying to update.
4. Server checks to see if the resource has changed, by computing the ETag 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). 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, 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 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, 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. 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.

View File

@ -98,6 +98,21 @@ class ConditionalGet(TestCase):
response = self.client.get('/condition/etag/') response = self.client.get('/condition/etag/')
self.assertFullResponse(response, check_last_modified=False) 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): class ETagProcesing(TestCase):
def testParsing(self): def testParsing(self):
etags = parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"') etags = parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"')

View File

@ -3,6 +3,8 @@ import views
urlpatterns = patterns('', urlpatterns = patterns('',
('^$', views.index), ('^$', views.index),
('^last_modified/$', views.last_modified), ('^last_modified/$', views.last_modified_view1),
('^etag/$', views.etag), ('^last_modified2/$', views.last_modified_view2),
('^etag/$', views.etag_view1),
('^etag2/$', views.etag_view2),
) )

View File

@ -1,17 +1,26 @@
# -*- coding:utf-8 -*- # -*- 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 django.http import HttpResponse
from models import FULL_RESPONSE, LAST_MODIFIED, ETAG from models import FULL_RESPONSE, LAST_MODIFIED, ETAG
@condition(lambda r: ETAG, lambda r: LAST_MODIFIED)
def index(request): def index(request):
return HttpResponse(FULL_RESPONSE) 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_view1(request):
def last_modified(request):
return HttpResponse(FULL_RESPONSE) 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 last_modified_view2(request):
def etag(request):
return HttpResponse(FULL_RESPONSE) 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)