Ticket #5791: 5791.5.diff
File 5791.5.diff, 14.0 KB (added by , 16 years ago) |
---|
-
django/views/decorators/http.py
=== modified file 'django/views/decorators/http.py'
7 7 except ImportError: 8 8 from django.utils.functional import wraps # Python 2.3, 2.4 fallback. 9 9 10 import re 11 from calendar import timegm 12 from datetime import timedelta 13 from email.Utils import formatdate 10 14 from django.utils.decorators import decorator_from_middleware 11 15 from django.middleware.http import ConditionalGetMiddleware 12 from django.http import HttpResponseNotAllowed 16 from django.http import HttpResponseNotAllowed, HttpResponseNotModified 13 17 14 18 conditional_page = decorator_from_middleware(ConditionalGetMiddleware) 15 19 … … 36 40 require_GET.__doc__ = "Decorator to require that a view only accept the GET method." 37 41 38 42 require_POST = require_http_methods(["POST"]) 39 require_POST.__doc__ = "Decorator to require that a view only accept the POST method." 40 No newline at end of file 43 require_POST.__doc__ = "Decorator to require that a view only accept the POST method." 44 45 def _parse_etags(etag_str): 46 """ 47 Parses a string with one or several etags passed in If-None-Match and 48 If-Match headers by the rules in RFC 2616. Returns a list of etags 49 without surrounding double quotes (") and unescaped from \<CHAR>. 50 """ 51 etags = re.findall(r'(?:W/)?"((?:\\.|[^"])*)"', etag_str) 52 if not etags: 53 # etag_str has wrong format, treat it as an opaque string then 54 return [etag_str] 55 etags = [e.decode('string_escape') for e in etags] 56 return etags 57 58 def _quote_etag(etag): 59 ''' 60 Wraps a string in double quotes escaping contents as necesary. 61 ''' 62 return '"%s"' % etag.replace('\\', '\\\\').replace('"', '\\"') 63 64 def condition(etag=None, last_modified=None): 65 """ 66 Decorator to support conditional get for a view. It takes as parameters 67 user-defined functions that calculate etag and/or last modified time. 68 69 Both functions are passed the same parameters as the view itself. "last_modified" 70 should return a standard datetime value and "etag" should return a string. 71 72 Example usage with last_modified:: 73 74 @condition(last_modified=lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time) 75 def my_object_view(request, obj_id): 76 # ... 77 78 You can pass last_modified, etag or both of them (if you really need it). 79 """ 80 def decorator(func): 81 def inner(request, *args, **kwargs): 82 if request.method not in ('GET', 'HEAD'): 83 return func(request, *args, **kwargs) 84 85 # Get HTTP request headers 86 if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None) 87 if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None) 88 if if_none_match: 89 if_none_match = _parse_etags(if_none_match) 90 91 # Get and convert user-defined values 92 if last_modified is not None: 93 dt = last_modified(request, *args, **kwargs) 94 last_modified_value = dt and (formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT') 95 else: 96 last_modified_value = None 97 98 if etag is not None: 99 etag_value = etag(request, *args, **kwargs) 100 else: 101 etag_value = None 102 103 # Calculate "not modified" condition 104 not_modified = (if_modified_since or if_none_match) and \ 105 (not if_modified_since or last_modified_value == if_modified_since) and \ 106 (not if_none_match or etag_value in if_none_match) 107 108 # Create appropriate response 109 if not_modified: 110 response = HttpResponseNotModified() 111 else: 112 response = func(request, *args, **kwargs) 113 114 # Set relevant headers for response 115 if last_modified_value and not response.has_header('Last-Modified'): 116 response['Last-Modified'] = last_modified_value 117 if etag_value and not response.has_header('ETag'): 118 response['ETag'] = _quote_etag(etag_value) 119 120 return response 121 return inner 122 return decorator 123 124 # Shortcut decorators for common cases based on ETag or Last-Modified only 125 def etag(callable): 126 return condition(etag=callable) 127 128 def last_modified(callable): 129 return condition(last_modified=callable) -
docs/index.txt
=== modified file 'docs/index.txt'
81 81 * :ref:`Admin site <ref-contrib-admin>` 82 82 * :ref:`Authentication <topics-auth>` 83 83 * :ref:`Cache system <topics-cache>` 84 * :ref:`Conditional get <topics-conditional-get>` 84 85 * :ref:`Comments <ref-contrib-comments-index>` 85 86 * :ref:`Content types <ref-contrib-contenttypes>` 86 87 * :ref:`Cross Site Request Forgery protection <ref-contrib-csrf>` -
docs/topics/conditional-get.txt
=== added file 'docs/topics/conditional-get.txt'
1 ======================== 2 Per-view conditional get 3 ======================== 4 5 `Conditional get`_ is a feature of HTTP that allows you to omit sending a response 6 if the client signals in a request that it has a cached copy of reponse. To control 7 whether the cached copy is expired the client may use time of its last modification 8 or an "entity tag" - a short value that changes whenever the response itself changes 9 (typically it's simply a hash over the response). 10 11 .. _`Conditional get`: http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3 12 13 Django allows simple usage of this feature with ``ConditionalGetMiddleware`` and 14 ``CommonMiddleware`` (see `middleware documentation`_ for their usage). But, while 15 simple, they both have limitations: 16 17 * they are applied gloablly to all views in your project 18 * they don't spare you from generating the response itself, which may be expensive 19 20 .. _`middleware documentation`: ../middleware/ 21 22 Decorators 23 ========== 24 25 When you need more fine-grained control you may use per-view conditional get 26 decorators. 27 28 Decorators ``etag`` and ``last_modified`` accept a user-defined function that 29 takes the same parameters as the view itself. ``last_modified`` should return a 30 standard datetime value and ``etag`` should return a string (you don't need to 31 wrap it in double quotes, Django will escape the string for you). 32 33 Example usage of ``last_modified``:: 34 35 @last_modified(lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time) 36 def my_object_view(request, obj_id): 37 # Expensive generation of response with MyObject instance 38 39 In this example we're taking advantage of the fact that MyObject instance stores 40 the time when it was last updated in a field. 41 42 Usage of ``etag`` is similar. 43 44 HTTP allows to use both "ETag" and "Last-Modified" headers in your response. Then 45 a response is considered not modified only if the client sends both headers back and 46 they're both equal to the response headers. This means that you can't just chain 47 decorators on your view: 48 49 @etag(etag_func) 50 @last_modified(last_modified_func) 51 def my_view(request): 52 # ... 53 54 Because the first decorator doesn't know anything about the second and can answer 55 that the response is not modified even if the second decorators would think otherwise. 56 In this case you should use a more general decorator - ``condition`` that accepts 57 two functions at once: 58 59 @condition(etag_func, last_modified_func) 60 def my_view(request): 61 # ... -
tests/regressiontests/conditional_get/__init__.py
=== added directory 'tests/regressiontests/conditional_get' === added file 'tests/regressiontests/conditional_get/__init__.py'
1 # -*- coding:utf-8 -*- -
tests/regressiontests/conditional_get/models.py
=== added file 'tests/regressiontests/conditional_get/models.py'
1 # -*- coding:utf-8 -*- 2 from datetime import datetime, timedelta 3 from calendar import timegm 4 from django.test import TestCase 5 6 from django.views.decorators.http import _parse_etags, _quote_etag 7 8 FULL_RESPONSE = 'Test conditional get response' 9 LAST_MODIFIED = datetime(2007, 10, 21, 23, 21, 47) 10 LAST_MODIFIED_STR = 'Sun, 21 Oct 2007 23:21:47 GMT' 11 EXPIRED_LAST_MODIFIED_STR = 'Sat, 20 Oct 2007 23:21:47 GMT' 12 ETAG = 'b4246ffc4f62314ca13147c9d4f76974' 13 EXPIRED_ETAG = '7fae4cd4b0f81e7d2914700043aa8ed6' 14 15 class ConditionalGet(TestCase): 16 def assertFullResponse(self, response, check_last_modified=True, check_etag=True): 17 self.assertEquals(response.status_code, 200) 18 self.assertEquals(response.content, FULL_RESPONSE) 19 if check_last_modified: 20 self.assertEquals(response['Last-Modified'], LAST_MODIFIED_STR) 21 if check_etag: 22 self.assertEquals(response['ETag'], '"%s"' % ETAG) 23 24 def assertNotModified(self, response): 25 self.assertEquals(response.status_code, 304) 26 self.assertEquals(response.content, '') 27 28 def testWithoutConditions(self): 29 response = self.client.get('/condition/') 30 self.assertFullResponse(response) 31 32 def testIfModifiedSince(self): 33 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR 34 response = self.client.get('/condition/') 35 self.assertNotModified(response) 36 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR 37 response = self.client.get('/condition/') 38 self.assertFullResponse(response) 39 40 def testIfNoneMatch(self): 41 self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG 42 response = self.client.get('/condition/') 43 self.assertNotModified(response) 44 self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % EXPIRED_ETAG 45 response = self.client.get('/condition/') 46 self.assertFullResponse(response) 47 48 # Several etags in If-None-Match is a bit exotic but why not? 49 self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s", "%s"' % (ETAG, EXPIRED_ETAG) 50 response = self.client.get('/condition/') 51 self.assertNotModified(response) 52 53 def testBothHeaders(self): 54 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR 55 self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG 56 response = self.client.get('/condition/') 57 self.assertNotModified(response) 58 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR 59 self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG 60 response = self.client.get('/condition/') 61 self.assertFullResponse(response) 62 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR 63 self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % EXPIRED_ETAG 64 response = self.client.get('/condition/') 65 self.assertFullResponse(response) 66 67 def testSingleConditions(self): 68 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR 69 response = self.client.get('/condition/last_modified/') 70 self.assertNotModified(response) 71 response = self.client.get('/condition/etag/') 72 self.assertFullResponse(response, check_last_modified=False) 73 74 del self.client.defaults['HTTP_IF_MODIFIED_SINCE'] 75 self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG 76 response = self.client.get('/condition/etag/') 77 self.assertNotModified(response) 78 response = self.client.get('/condition/last_modified/') 79 self.assertFullResponse(response, check_etag=False) 80 81 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR 82 self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % EXPIRED_ETAG 83 response = self.client.get('/condition/last_modified/') 84 self.assertFullResponse(response, check_etag=False) 85 response = self.client.get('/condition/etag/') 86 self.assertFullResponse(response, check_last_modified=False) 87 88 89 class ETagProcesing(TestCase): 90 def testParsing(self): 91 etags = _parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"') 92 self.assertEquals(etags, ['', 'etag', 'e"t"ag', r'e\tag', 'weak']) 93 94 def testQuoting(self): 95 quoted_etag = _quote_etag(r'e\t"ag') 96 self.assertEquals(quoted_etag, r'"e\\t\"ag"') -
tests/regressiontests/conditional_get/urls.py
=== added file 'tests/regressiontests/conditional_get/urls.py'
1 from django.conf.urls.defaults import * 2 import views 3 4 urlpatterns = patterns('', 5 ('^$', views.index), 6 ('^last_modified/$', views.last_modified), 7 ('^etag/$', views.etag), 8 ) -
tests/regressiontests/conditional_get/views.py
=== added file 'tests/regressiontests/conditional_get/views.py'
1 # -*- coding:utf-8 -*- 2 from django.views.decorators.http import condition 3 from django.http import HttpResponse 4 5 from models import FULL_RESPONSE, LAST_MODIFIED, ETAG 6 7 @condition(lambda r: ETAG, lambda r: LAST_MODIFIED) 8 def index(request): 9 return HttpResponse(FULL_RESPONSE) 10 11 @condition(last_modified=lambda r: LAST_MODIFIED) 12 def last_modified(request): 13 return HttpResponse(FULL_RESPONSE) 14 15 @condition(etag=lambda r: ETAG) 16 def etag(request): 17 return HttpResponse(FULL_RESPONSE) 18 No newline at end of file -
tests/urls.py
=== modified file 'tests/urls.py'
29 29 30 30 # test urlconf for syndication tests 31 31 (r'^syndication/', include('regressiontests.syndication.urls')), 32 33 # conditional get views 34 (r'condition/', include('regressiontests.conditional_get.urls')), 32 35 )