Code

Ticket #5791: 5791.3.diff

File 5791.3.diff, 12.8 KB (added by isagalaev, 6 years ago)

Patch updated to current trunk + new docs structure

Line 
1=== modified file 'django/views/decorators/http.py'
2--- django/views/decorators/http.py     2008-02-25 06:02:35 +0000
3+++ django/views/decorators/http.py     2008-11-16 21:14:28 +0000
4@@ -7,9 +7,12 @@
5 except ImportError:
6     from django.utils.functional import wraps  # Python 2.3, 2.4 fallback.
7 
8+from calendar import timegm
9+from datetime import timedelta
10+from email.Utils import formatdate
11 from django.utils.decorators import decorator_from_middleware
12 from django.middleware.http import ConditionalGetMiddleware
13-from django.http import HttpResponseNotAllowed
14+from django.http import HttpResponseNotAllowed, HttpResponseNotModified
15 
16 conditional_page = decorator_from_middleware(ConditionalGetMiddleware)
17 
18@@ -36,4 +39,71 @@
19 require_GET.__doc__ = "Decorator to require that a view only accept the GET method."
20 
21 require_POST = require_http_methods(["POST"])
22-require_POST.__doc__ = "Decorator to require that a view only accept the POST method."
23\ No newline at end of file
24+require_POST.__doc__ = "Decorator to require that a view only accept the POST method."
25+
26+def condition(etag=None, last_modified=None):
27+    """
28+    Decorator to support conditional get for a view.  It takes as parameters
29+    user-defined functions that calculate etag and/or last modified time.
30+   
31+    Both functions are passed the same parameters as the view itself. "last_modified"
32+    should return a standard datetime value and "etag" should return a string.
33+   
34+    Example usage with last_modified::
35+
36+        @condition(last_modified=lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time)
37+        def my_object_view(request, obj_id):
38+            # ...
39+
40+    You can pass last_modified, etag or both of them (if you really need it).
41+    """
42+    def decorator(func):
43+        def inner(request, *args, **kwargs):
44+            if request.method not in ('GET', 'HEAD'):
45+                return func(request, *args, **kwargs)
46+
47+            # Get HTTP request headers
48+            if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None)
49+            if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None)
50+            if if_none_match:
51+                if_none_match = [e.strip() for e in if_none_match.split(',')]
52+
53+            # Get and convert user-defined values
54+            if last_modified is not None:
55+                dt = last_modified(request, *args, **kwargs)
56+                last_modified_value = dt and (formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT')
57+            else:
58+                last_modified_value = None
59+               
60+            if etag is not None:
61+                etag_value = etag(request, *args, **kwargs)
62+            else:
63+                etag_value = None
64+
65+            # Calculate "not modified" condition
66+            not_modified = (if_modified_since or if_none_match) and \
67+                           (not if_modified_since or last_modified_value == if_modified_since) and \
68+                           (not if_none_match or etag_value in if_none_match)
69+
70+            # Create appropriate response
71+            if not_modified:
72+                response = HttpResponseNotModified()
73+            else:
74+                response = func(request, *args, **kwargs)
75+
76+            # Set relevant headers for response
77+            if last_modified_value and not response.has_header('Last-Modified'):
78+                response['Last-Modified'] = last_modified_value
79+            if etag_value and not response.has_header('ETag'):
80+                response['ETag'] = etag_value
81+
82+            return response
83+        return inner
84+    return decorator
85+
86+# Shortcut decorators for common cases based on ETag or Last-Modified only
87+def etag(callable):
88+    return condition(etag=callable)
89+
90+def last_modified(callable):
91+    return condition(last_modified=callable)
92\ No newline at end of file
93
94=== modified file 'docs/index.txt'
95--- docs/index.txt      2008-10-05 06:38:59 +0000
96+++ docs/index.txt      2008-11-16 21:18:17 +0000
97@@ -74,6 +74,7 @@
98 
99     * :ref:`Authentication <topics-auth>`
100     * :ref:`Caching <topics-cache>`
101+    * :ref:`Conditional get <topics-conditional-get>`
102     * :ref:`E-mail <topics-email>`
103     * :ref:`File-access APIs <topics-files>`
104     * :ref:`topics-i18n`
105
106=== added file 'docs/topics/conditional-get.txt'
107--- docs/topics/conditional-get.txt     1970-01-01 00:00:00 +0000
108+++ docs/topics/conditional-get.txt     2008-11-16 21:14:28 +0000
109@@ -0,0 +1,60 @@
110+========================
111+Per-view conditional get
112+========================
113+
114+`Conditional get`_ is a feature of HTTP that allows you to omit sending a response
115+if the client signals in a request that it has a cached copy of reponse. To control
116+whether the cached copy is expired the client may use time of its last modification
117+or an "entity tag" - a short value that changes whenever the response itself changes
118+(typically it's simply a hash over the response).
119+
120+.. _`Conditional get`: http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3
121+
122+Django allows simple usage of this feature with ``ConditionalGetMiddleware`` and
123+``CommonMiddleware`` (see `middleware documentation`_ for their usage). But, while
124+simple, they both have limitations:
125+
126+* they are applied gloablly to all views in your project
127+* they don't spare you from generating the response itself, which may be expensive
128+
129+.. _`middleware documentation`: ../middleware/
130+
131+Decorators
132+==========
133+
134+When you need more fine-grained control you may use per-view conditional get
135+decorators.
136+
137+Decorators ``etag`` and ``last_modified`` accept a user-defined function that
138+takes the same parameters as the view itself. ``last_modified`` should return a
139+standard datetime value and ``etag`` should return a string.
140+
141+Example usage of ``last_modified``::
142+
143+    @last_modified(lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time)
144+    def my_object_view(request, obj_id):
145+        # Expensive generation of response with MyObject instance
146+
147+In this example we're taking advantage of the fact that MyObject instance stores
148+the time when it was last updated in a field.
149+
150+Usage of ``etag`` is similar.
151+
152+HTTP allows to use both "ETag" and "Last-Modified" headers in your response. Then
153+a response is considered not modified only if the client sends both headers back and
154+they're both equal to the response headers. This means that you can't just chain
155+decorators on your view:
156+
157+    @etag(etag_func)
158+    @last_modified(last_modified_func)
159+    def my_view(request):
160+        # ...
161+
162+Because the first decorator doesn't know anything about the second and can answer
163+that the response is not modified even if the second decorators would think otherwise.
164+In this case you should use a more general decorator - ``condition`` that accepts
165+two functions at once:
166+
167+    @condition(etag_func, last_modified_func)
168+    def my_view(request):
169+        # ...
170
171=== added directory 'tests/regressiontests/conditional_get'
172=== added file 'tests/regressiontests/conditional_get/__init__.py'
173--- tests/regressiontests/conditional_get/__init__.py   1970-01-01 00:00:00 +0000
174+++ tests/regressiontests/conditional_get/__init__.py   2008-11-16 21:14:28 +0000
175@@ -0,0 +1,1 @@
176+# -*- coding:utf-8 -*-
177
178=== added file 'tests/regressiontests/conditional_get/models.py'
179--- tests/regressiontests/conditional_get/models.py     1970-01-01 00:00:00 +0000
180+++ tests/regressiontests/conditional_get/models.py     2008-11-16 21:14:28 +0000
181@@ -0,0 +1,84 @@
182+# -*- coding:utf-8 -*-
183+from datetime import datetime, timedelta
184+from calendar import timegm
185+from django.test import TestCase
186+
187+FULL_RESPONSE = 'Test conditional get response'
188+LAST_MODIFIED = datetime(2007, 10, 21, 23, 21, 47)
189+LAST_MODIFIED_STR = 'Sun, 21 Oct 2007 23:21:47 GMT'
190+EXPIRED_LAST_MODIFIED_STR = 'Sat, 20 Oct 2007 23:21:47 GMT'
191+ETAG = '"b4246ffc4f62314ca13147c9d4f76974"'
192+EXPIRED_ETAG = '"7fae4cd4b0f81e7d2914700043aa8ed6"'
193+
194+class ConditionalGet(TestCase):
195+    def assertFullResponse(self, response, check_last_modified=True, check_etag=True):
196+        self.assertEquals(response.status_code, 200)
197+        self.assertEquals(response.content, FULL_RESPONSE)
198+        if check_last_modified:
199+            self.assertEquals(response['Last-Modified'], LAST_MODIFIED_STR)
200+        if check_etag:
201+            self.assertEquals(response['ETag'], ETAG)
202+
203+    def assertNotModified(self, response):
204+        self.assertEquals(response.status_code, 304)
205+        self.assertEquals(response.content, '')
206+
207+    def testWithoutConditions(self):
208+        response = self.client.get('/condition/')
209+        self.assertFullResponse(response)
210+
211+    def testIfModifiedSince(self):
212+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
213+        response = self.client.get('/condition/')
214+        self.assertNotModified(response)
215+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
216+        response = self.client.get('/condition/')
217+        self.assertFullResponse(response)
218+
219+    def testIfNoneMatch(self):
220+        self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG
221+        response = self.client.get('/condition/')
222+        self.assertNotModified(response)
223+        self.client.defaults['HTTP_IF_NONE_MATCH'] = EXPIRED_ETAG
224+        response = self.client.get('/condition/')
225+        self.assertFullResponse(response)
226+       
227+        # Several etags in If-None-Match is a bit exotic but why not?
228+        self.client.defaults['HTTP_IF_NONE_MATCH'] = ', '.join([ETAG, EXPIRED_ETAG])
229+        response = self.client.get('/condition/')
230+        self.assertNotModified(response)
231+   
232+    def testBothHeaders(self):
233+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
234+        self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG
235+        response = self.client.get('/condition/')
236+        self.assertNotModified(response)
237+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
238+        self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG
239+        response = self.client.get('/condition/')
240+        self.assertFullResponse(response)
241+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
242+        self.client.defaults['HTTP_IF_NONE_MATCH'] = EXPIRED_ETAG
243+        response = self.client.get('/condition/')
244+        self.assertFullResponse(response)
245+
246+    def testSingleConditions(self):
247+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
248+        response = self.client.get('/condition/last_modified/')
249+        self.assertNotModified(response)
250+        response = self.client.get('/condition/etag/')
251+        self.assertFullResponse(response, check_last_modified=False)
252+       
253+        del self.client.defaults['HTTP_IF_MODIFIED_SINCE']
254+        self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG
255+        response = self.client.get('/condition/etag/')
256+        self.assertNotModified(response)
257+        response = self.client.get('/condition/last_modified/')
258+        self.assertFullResponse(response, check_etag=False)
259+       
260+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
261+        self.client.defaults['HTTP_IF_NONE_MATCH'] = EXPIRED_ETAG
262+        response = self.client.get('/condition/last_modified/')
263+        self.assertFullResponse(response, check_etag=False)
264+        response = self.client.get('/condition/etag/')
265+        self.assertFullResponse(response, check_last_modified=False)
266\ No newline at end of file
267
268=== added file 'tests/regressiontests/conditional_get/urls.py'
269--- tests/regressiontests/conditional_get/urls.py       1970-01-01 00:00:00 +0000
270+++ tests/regressiontests/conditional_get/urls.py       2008-11-16 21:14:28 +0000
271@@ -0,0 +1,8 @@
272+from django.conf.urls.defaults import *
273+import views
274+
275+urlpatterns = patterns('',
276+    ('^$', views.index),
277+    ('^last_modified/$', views.last_modified),
278+    ('^etag/$', views.etag),
279+)
280
281=== added file 'tests/regressiontests/conditional_get/views.py'
282--- tests/regressiontests/conditional_get/views.py      1970-01-01 00:00:00 +0000
283+++ tests/regressiontests/conditional_get/views.py      2008-11-16 21:14:28 +0000
284@@ -0,0 +1,17 @@
285+# -*- coding:utf-8 -*-
286+from django.views.decorators.http import condition
287+from django.http import HttpResponse
288+
289+from models import FULL_RESPONSE, LAST_MODIFIED, ETAG
290+
291+@condition(lambda r: ETAG, lambda r: LAST_MODIFIED)
292+def index(request):
293+    return HttpResponse(FULL_RESPONSE)
294+
295+@condition(last_modified=lambda r: LAST_MODIFIED)
296+def last_modified(request):
297+    return HttpResponse(FULL_RESPONSE)
298+
299+@condition(etag=lambda r: ETAG)
300+def etag(request):
301+    return HttpResponse(FULL_RESPONSE)
302\ No newline at end of file
303
304=== modified file 'tests/urls.py'
305--- tests/urls.py       2008-11-13 19:03:44 +0000
306+++ tests/urls.py       2008-11-16 21:14:28 +0000
307@@ -29,4 +29,7 @@
308 
309     # test urlconf for syndication tests
310     (r'^syndication/', include('regressiontests.syndication.urls')),
311+
312+    # conditional get views
313+    (r'condition/', include('regressiontests.conditional_get.urls')),
314 )
315