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:
parent
2fb7f5ea2b
commit
e5a8d9e810
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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"')
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue