Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
  • Loading branch information...
commit e5a8d9e810a00898345e15f1af5c4099e746c7a0 1 parent 2fb7f5e
Malcolm Tredinnick authored March 24, 2009
8  django/views/decorators/http.py
@@ -127,9 +127,9 @@ def inner(request, *args, **kwargs):
127 127
     return decorator
128 128
 
129 129
 # Shortcut decorators for common cases based on ETag or Last-Modified only
130  
-def etag(callable):
131  
-    return condition(etag=callable)
  130
+def etag(etag_func):
  131
+    return condition(etag_func=etag_func)
132 132
 
133  
-def last_modified(callable):
134  
-    return condition(last_modified=callable)
  133
+def last_modified(last_modified_func):
  134
+    return condition(last_modified_func=last_modified_func)
135 135
 
180  docs/topics/conditional-view-processing.txt
@@ -28,61 +28,119 @@ client that nothing has changed.
28 28
 .. _If-none-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
29 29
 .. _If-modified-since: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25
30 30
 
31  
-Django allows simple usage of this feature with
32  
-:class:`django.middleware.http.ConditionalGetMiddleware` and
33  
-:class:`~django.middleware.common.CommonMiddleware`. However, whilst being
34  
-easy to use and suitable for many situations, they both have limitations for
35  
-advanced usage:
36  
-
37  
-    * They are applied globally to all views in your project
38  
-    * They don't save you from generating the response itself, which may be
39  
-      expensive
40  
-    * They are only appropriate for HTTP ``GET`` requests.
  31
+When you need more fine-grained control you may use per-view conditional
  32
+processing functions.
41 33
 
42 34
 .. conditional-decorators:
43 35
 
44  
-Decorators
45  
-==========
46  
-
47  
-When you need more fine-grained control you may use per-view conditional
48  
-processing functions. 
  36
+The ``condition`` decorator
  37
+===========================
49 38
 
50  
-The decorators ``django.views.decorators.http.etag`` and
51  
-``django.views.decorators.http.last_modified`` each accept a user-defined
52  
-function that takes the same parameters as the view itself. The function
53  
-passed ``last_modified`` should return a standard datetime value specifying
54  
-the last time the resource was modified, or ``None`` if the resource doesn't
55  
-exist. The function passed to the ``etag`` decorator should return a string
56  
-representing the `Etag`_ for the resource, or ``None`` if it doesn't exist.
  39
+Sometimes (in fact, quite often) you can create functions to rapidly compute the ETag_
  40
+value or the last-modified time for a resource, **without** needing to do all
  41
+the computations needed to construct the full view. Django can then use these
  42
+functions to provide an "early bailout" option for the view processing.
  43
+Telling the client that the content has not been modified since the last
  44
+request, perhaps.
57 45
 
58 46
 .. _ETag: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
59 47
 
60  
-For example::
  48
+These two functions are passed as parameters the
  49
+``django.views.decorators.http.condition`` decorator. This decorator uses
  50
+the two functions (you only need to supply one, if you can't compute both
  51
+quantities easily and quickly) to work out if the headers in the HTTP request
  52
+match those on the resource. If they don't match, a new copy of the resource
  53
+must be computed and your normal view is called.
  54
+
  55
+The ``condition`` decorator's signature looks like this::
  56
+
  57
+    condition(etag_func=None, last_modified_func=None)
  58
+
  59
+The two functions, to compute the ETag and the last modified time, will be
  60
+passed the incoming ``request`` object and the same parameters, in the same
  61
+order, as the view function they are helping to wrap. The function passed
  62
+``last_modified`` should return a standard datetime value specifying the last
  63
+time the resource was modified, or ``None`` if the resource doesn't exist. The
  64
+function passed to the ``etag`` decorator should return a string representing
  65
+the `Etag`_ for the resource, or ``None`` if it doesn't exist.
  66
+
  67
+Using this feature usefully is probably best explained with an example.
  68
+Suppose you have this pair of models, representing a simple blog system::
  69
+
  70
+    import datetime
  71
+    from django.db import models
  72
+
  73
+    class Blog(models.Model):
  74
+        ...
  75
+
  76
+    class Entry(models.Model):
  77
+        blog = models.ForeignKey(Blog)
  78
+        published = models.DateTimeField(default=datetime.datetime.now)
  79
+        ...
  80
+
  81
+If the front page, displaying the latest blog entries, only changes when you
  82
+add a new blog entry, you can compute the last modified time very quickly. You
  83
+need the latest ``published`` date for every entry associated with that blog.
  84
+One way to do this would be::
  85
+
  86
+    from django.db.models import Max
  87
+
  88
+    def latest_entry(request, blog_id):
  89
+        return Entry.objects.filter(blog=blog_id).aggregate(Max("published"))
  90
+
  91
+You can then use this function to provide early detection of an unchanged page
  92
+for your front page view::
61 93
 
62  
-    # Compute the last-modified time from when the object was last saved.
63  
-    @last_modified(lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time)
64  
-    def my_object_view(request, obj_id):
65  
-        # Expensive generation of response with MyObject instance
  94
+    from django.views.decorators.http import condition
  95
+
  96
+    @condition(last_modified_func=latest_entry)
  97
+    def front_page(request, blog_id):
  98
+        ...
  99
+
  100
+Of course, if you're using Python 2.3 or prefer not to use the decorator
  101
+syntax, you can write the same code as follows, there is no difference::
  102
+
  103
+    def front_page(request, blog_id):
  104
+        ...
  105
+    front_page = condition(last_modified_func=latest_entry)(front_page)
  106
+
  107
+Shortcuts for only computing one value
  108
+======================================
  109
+
  110
+As a general rule, if you can provide functions to compute *both* the ETag and
  111
+the last modified time, you should do so. You don't know which headers any
  112
+given HTTP client will send you, so be prepared to handle both. However,
  113
+sometimes only one value is easy to compute and Django provides decorators
  114
+that handle only ETag or only last-modified computations.
  115
+
  116
+The ``django.views.decorators.http.etag`` and
  117
+``django.views.decorators.http.last_modified`` decorators are passed the same
  118
+type of functions as the ``condition`` decorator. Their signatures are::
  119
+
  120
+    etag(etag_func)
  121
+    last_modified(last_modified_func)
  122
+
  123
+We could write the earlier example, which only uses a last-modified function,
  124
+using one of these decorators::
  125
+
  126
+    @last_modified(latest_entry)
  127
+    def front_page(request, blog_id):
66 128
         ...
67 129
 
68  
-Of course, you can always use the non-decorator form if you're using Python
69  
-2.3 or don't like the decorator syntax::
  130
+...or::
70 131
 
71  
-    def my_object_view(request, obj_id):
  132
+    def front_page(request, blog_id):
72 133
         ...
73  
-    my_object_view = last_modified(my_func)(my_object_view)
  134
+    front_page = last_modified(latest_entry)(front_page)
74 135
 
75  
-Using the ``etag`` decorator is similar.
  136
+Use ``condition`` when testing both conditions
  137
+------------------------------------------------
76 138
 
77  
-In practice, though, you won't know if the client is going to send the
78  
-``Last-modified`` or the ``If-none-match`` header. If you can quickly compute
79  
-both values and want to short-circuit as often as possible, you'll need to use
80  
-the ``conditional`` decorator described below.
  139
+It might look nicer to some people to try and chain the ``etag`` and
  140
+``last_modified`` decorators if you want to test both preconditions. However,
  141
+this would lead to incorrect behavior.
81 142
 
82  
-HTTP allows to use both "ETag" and "Last-Modified" headers in your response.
83  
-Then a response is considered not modified only if the client sends both
84  
-headers back and they're both equal to the response headers. This means that
85  
-you can't just chain decorators on your view::
  143
+::
86 144
 
87 145
     # Bad code. Don't do this!
88 146
     @etag(etag_func)
@@ -94,18 +152,13 @@ you can't just chain decorators on your view::
94 152
 
95 153
 The first decorator doesn't know anything about the second and might
96 154
 answer that the response is not modified even if the second decorators would
97  
-determine otherwise. In this case you should use a more general decorator -
98  
-``django.views.decorator.http.condition`` that accepts two functions at once::
99  
-
100  
-    # The correct way to implement the above example
101  
-    @condition(etag_func, last_modified_func)
102  
-    def my_view(request):
103  
-        # ...
  155
+determine otherwise. The ``condition`` decorator uses both callback functions
  156
+simultaneously to work out the right action to take.
104 157
 
105 158
 Using the decorators with other HTTP methods
106 159
 ============================================
107 160
 
108  
-The ``conditional`` decorator is useful for more than only ``GET`` and
  161
+The ``condition`` decorator is useful for more than only ``GET`` and
109 162
 ``HEAD`` requests (``HEAD`` requests are the same as ``GET`` in this
110 163
 situation). It can be used also to be used to provide checking for ``POST``,
111 164
 ``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:
116 169
 
117 170
     1. Client requests ``/foo/``.
118 171
     2. Server responds with some content with an ETag of ``"abcd1234"``.
119  
-    3. Client sends and HTTP ``PUT`` request to ``/foo/`` to update the
120  
-       resource. It sends an ``If-Match: "abcd1234"`` header to specify the
121  
-       version it is trying to update.
  172
+    3. Client sends an HTTP ``PUT`` request to ``/foo/`` to update the
  173
+       resource. It also sends an ``If-Match: "abcd1234"`` header to specify
  174
+       the version it is trying to update.
122 175
     4. Server checks to see if the resource has changed, by computing the ETag
123 176
        the same way it does for a ``GET`` request (using the same function).
124 177
        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:
129 182
 
130 183
 The important thing this example shows is that the same functions can be used
131 184
 to compute the ETag and last modification values in all situations. In fact,
132  
-you *should* use the same functions, so that the same values are returned
  185
+you **should** use the same functions, so that the same values are returned
133 186
 every time.
134 187
 
  188
+Comparison with middleware conditional processing
  189
+=================================================
  190
+
  191
+You may notice that Django already provides simple and straightforward
  192
+conditional ``GET`` handling via the
  193
+:class:`django.middleware.http.ConditionalGetMiddleware` and
  194
+:class:`~django.middleware.common.CommonMiddleware`. Whilst certainly being
  195
+easy to use and suitable for many situations, those pieces of middleware
  196
+functionality have limitations for advanced usage:
  197
+
  198
+    * They are applied globally to all views in your project
  199
+    * They don't save you from generating the response itself, which may be
  200
+      expensive
  201
+    * They are only appropriate for HTTP ``GET`` requests.
  202
+
  203
+You should choose the most appropriate tool for your particular problem here.
  204
+If you have a way to compute ETags and modification times quickly and if some
  205
+view takes a while to generate the content, you should consider using the
  206
+``condition`` decorator described in this document. If everything already runs
  207
+fairly quickly, stick to using the middleware and the amount of network
  208
+traffic sent back to the clients will still be reduced if the view hasn't
  209
+changed.
  210
+
15  tests/regressiontests/conditional_processing/models.py
@@ -98,6 +98,21 @@ def testSingleCondition4(self):
98 98
         response = self.client.get('/condition/etag/')
99 99
         self.assertFullResponse(response, check_last_modified=False)
100 100
 
  101
+    def testSingleCondition5(self):
  102
+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
  103
+        response = self.client.get('/condition/last_modified2/')
  104
+        self.assertNotModified(response)
  105
+        response = self.client.get('/condition/etag2/')
  106
+        self.assertFullResponse(response, check_last_modified=False)
  107
+
  108
+    def testSingleCondition6(self):
  109
+        self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG
  110
+        response = self.client.get('/condition/etag2/')
  111
+        self.assertNotModified(response)
  112
+        response = self.client.get('/condition/last_modified2/')
  113
+        self.assertFullResponse(response, check_etag=False)
  114
+
  115
+
101 116
 class ETagProcesing(TestCase):
102 117
     def testParsing(self):
103 118
         etags = parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"')
6  tests/regressiontests/conditional_processing/urls.py
@@ -3,6 +3,8 @@
3 3
 
4 4
 urlpatterns = patterns('',
5 5
     ('^$', views.index),
6  
-    ('^last_modified/$', views.last_modified),
7  
-    ('^etag/$', views.etag),
  6
+    ('^last_modified/$', views.last_modified_view1),
  7
+    ('^last_modified2/$', views.last_modified_view2),
  8
+    ('^etag/$', views.etag_view1),
  9
+    ('^etag2/$', views.etag_view2),
8 10
 )
21  tests/regressiontests/conditional_processing/views.py
... ...
@@ -1,17 +1,26 @@
1 1
 # -*- coding:utf-8 -*-
2  
-from django.views.decorators.http import condition
  2
+from django.views.decorators.http import condition, etag, last_modified
3 3
 from django.http import HttpResponse
4 4
 
5 5
 from models import FULL_RESPONSE, LAST_MODIFIED, ETAG
6 6
 
7  
-@condition(lambda r: ETAG, lambda r: LAST_MODIFIED)
8 7
 def index(request):
9 8
     return HttpResponse(FULL_RESPONSE)
  9
+index = condition(lambda r: ETAG, lambda r: LAST_MODIFIED)(index)
10 10
 
11  
-@condition(last_modified_func=lambda r: LAST_MODIFIED)
12  
-def last_modified(request):
  11
+def last_modified_view1(request):
13 12
     return HttpResponse(FULL_RESPONSE)
  13
+last_modified_view1 = condition(last_modified_func=lambda r: LAST_MODIFIED)(last_modified_view1)
14 14
 
15  
-@condition(etag_func=lambda r: ETAG)
16  
-def etag(request):
  15
+def last_modified_view2(request):
17 16
     return HttpResponse(FULL_RESPONSE)
  17
+last_modified_view2 = last_modified(lambda r: LAST_MODIFIED)(last_modified_view2)
  18
+
  19
+def etag_view1(request):
  20
+    return HttpResponse(FULL_RESPONSE)
  21
+etag_view1 = condition(etag_func=lambda r: ETAG)(etag_view1)
  22
+
  23
+def etag_view2(request):
  24
+    return HttpResponse(FULL_RESPONSE)
  25
+etag_view2 = etag(lambda r: ETAG)(etag_view2)
  26
+

0 notes on commit e5a8d9e

Please sign in to comment.
Something went wrong with that request. Please try again.