|
0
|
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 |
|