浏览代码

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
Malcolm Tredinnick 16 年之前
父节点
当前提交
e5a8d9e810

+ 4 - 4
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)
 

+ 128 - 52
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
-==========
-
-When you need more fine-grained control you may use per-view conditional
-processing functions. 
+The ``condition`` decorator
+===========================
 
-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.
+
+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):
+        ...
+
+    class Entry(models.Model):
+        blog = models.ForeignKey(Blog)
+        published = models.DateTimeField(default=datetime.datetime.now)
+        ...
+
+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::
+
+    from django.db.models import Max
+
+    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::
 
-    # 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
+    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):
         ...
 
-Of course, you can always use the non-decorator form if you're using Python
-2.3 or don't like the decorator syntax::
+...or::
 
-    def my_object_view(request, obj_id):
+    def front_page(request, blog_id):
         ...
-    my_object_view = last_modified(my_func)(my_object_view)
+    front_page = last_modified(latest_entry)(front_page)
 
-Using the ``etag`` decorator is similar.
+Use ``condition`` when testing both conditions
+------------------------------------------------
 
-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.
+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.
 
-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::
+::
 
     # 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.
+

+ 15 - 0
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"')

+ 4 - 2
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),
 )

+ 15 - 6
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)
+