|
1 """ |
|
2 Decorators for views based on HTTP headers. |
|
3 """ |
|
4 |
|
5 try: |
|
6 from functools import wraps |
|
7 except ImportError: |
|
8 from django.utils.functional import wraps # Python 2.3, 2.4 fallback. |
|
9 |
|
10 from calendar import timegm |
|
11 from datetime import timedelta |
|
12 from email.Utils import formatdate |
|
13 |
|
14 from django.utils.decorators import decorator_from_middleware |
|
15 from django.utils.http import parse_etags, quote_etag |
|
16 from django.middleware.http import ConditionalGetMiddleware |
|
17 from django.http import HttpResponseNotAllowed, HttpResponseNotModified, HttpResponse |
|
18 |
|
19 |
|
20 conditional_page = decorator_from_middleware(ConditionalGetMiddleware) |
|
21 |
|
22 def require_http_methods(request_method_list): |
|
23 """ |
|
24 Decorator to make a view only accept particular request methods. Usage:: |
|
25 |
|
26 @require_http_methods(["GET", "POST"]) |
|
27 def my_view(request): |
|
28 # I can assume now that only GET or POST requests make it this far |
|
29 # ... |
|
30 |
|
31 Note that request methods should be in uppercase. |
|
32 """ |
|
33 def decorator(func): |
|
34 def inner(request, *args, **kwargs): |
|
35 if request.method not in request_method_list: |
|
36 return HttpResponseNotAllowed(request_method_list) |
|
37 return func(request, *args, **kwargs) |
|
38 return wraps(func)(inner) |
|
39 return decorator |
|
40 |
|
41 require_GET = require_http_methods(["GET"]) |
|
42 require_GET.__doc__ = "Decorator to require that a view only accept the GET method." |
|
43 |
|
44 require_POST = require_http_methods(["POST"]) |
|
45 require_POST.__doc__ = "Decorator to require that a view only accept the POST method." |
|
46 |
|
47 def condition(etag_func=None, last_modified_func=None): |
|
48 """ |
|
49 Decorator to support conditional retrieval (or change) for a view |
|
50 function. |
|
51 |
|
52 The parameters are callables to compute the ETag and last modified time for |
|
53 the requested resource, respectively. The callables are passed the same |
|
54 parameters as the view itself. The Etag function should return a string (or |
|
55 None if the resource doesn't exist), whilst the last_modified function |
|
56 should return a datetime object (or None if the resource doesn't exist). |
|
57 |
|
58 If both parameters are provided, all the preconditions must be met before |
|
59 the view is processed. |
|
60 |
|
61 This decorator will either pass control to the wrapped view function or |
|
62 return an HTTP 304 response (unmodified) or 412 response (preconditions |
|
63 failed), depending upon the request method. |
|
64 |
|
65 Any behavior marked as "undefined" in the HTTP spec (e.g. If-none-match |
|
66 plus If-modified-since headers) will result in the view function being |
|
67 called. |
|
68 """ |
|
69 def decorator(func): |
|
70 def inner(request, *args, **kwargs): |
|
71 # Get HTTP request headers |
|
72 if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE") |
|
73 if_none_match = request.META.get("HTTP_IF_NONE_MATCH") |
|
74 if_match = request.META.get("HTTP_IF_MATCH") |
|
75 if if_none_match or if_match: |
|
76 # There can be more than one ETag in the request, so we |
|
77 # consider the list of values. |
|
78 try: |
|
79 etags = parse_etags(if_none_match or if_match) |
|
80 except ValueError: |
|
81 # In case of invalid etag ignore all ETag headers. |
|
82 # Apparently Opera sends invalidly quoted headers at times |
|
83 # (we should be returning a 400 response, but that's a |
|
84 # little extreme) -- this is Django bug #10681. |
|
85 if_none_match = None |
|
86 if_match = None |
|
87 |
|
88 # Compute values (if any) for the requested resource. |
|
89 if etag_func: |
|
90 res_etag = etag_func(request, *args, **kwargs) |
|
91 else: |
|
92 res_etag = None |
|
93 if last_modified_func: |
|
94 dt = last_modified_func(request, *args, **kwargs) |
|
95 if dt: |
|
96 res_last_modified = formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT' |
|
97 else: |
|
98 res_last_modified = None |
|
99 else: |
|
100 res_last_modified = None |
|
101 |
|
102 response = None |
|
103 if not ((if_match and (if_modified_since or if_none_match)) or |
|
104 (if_match and if_none_match)): |
|
105 # We only get here if no undefined combinations of headers are |
|
106 # specified. |
|
107 if ((if_none_match and (res_etag in etags or |
|
108 "*" in etags and res_etag)) and |
|
109 (not if_modified_since or |
|
110 res_last_modified == if_modified_since)): |
|
111 if request.method in ("GET", "HEAD"): |
|
112 response = HttpResponseNotModified() |
|
113 else: |
|
114 response = HttpResponse(status=412) |
|
115 elif if_match and ((not res_etag and "*" in etags) or |
|
116 (res_etag and res_etag not in etags)): |
|
117 response = HttpResponse(status=412) |
|
118 elif (not if_none_match and if_modified_since and |
|
119 request.method == "GET" and |
|
120 res_last_modified == if_modified_since): |
|
121 response = HttpResponseNotModified() |
|
122 |
|
123 if response is None: |
|
124 response = func(request, *args, **kwargs) |
|
125 |
|
126 # Set relevant headers on the response if they don't already exist. |
|
127 if res_last_modified and not response.has_header('Last-Modified'): |
|
128 response['Last-Modified'] = res_last_modified |
|
129 if res_etag and not response.has_header('ETag'): |
|
130 response['ETag'] = quote_etag(res_etag) |
|
131 |
|
132 return response |
|
133 |
|
134 return inner |
|
135 return decorator |
|
136 |
|
137 # Shortcut decorators for common cases based on ETag or Last-Modified only |
|
138 def etag(etag_func): |
|
139 return condition(etag_func=etag_func) |
|
140 |
|
141 def last_modified(last_modified_func): |
|
142 return condition(last_modified_func=last_modified_func) |
|
143 |