Created
May 26, 2009 20:31
-
-
Save mmalone/118277 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import operator | |
from django import http | |
def parse_etags(etag_str): | |
""" | |
Parses a string with one or several etags passed in If-None-Match and | |
If-Match headers by the rules in RFC 2616. Returns a list of etags | |
without surrounding double quotes (") and unescaped from \<CHAR>. | |
""" | |
etags = re.findall(r'(?:W/)?"((?:\\.|[^"])*)"', etag_str) | |
if not etags: | |
# etag_str has wrong format, treat it as an opaque string then | |
return [etag_str] | |
etags = [e.decode('string_escape') for e in etags] | |
return etags | |
def quote_etag(etag): | |
""" | |
Wraps a string in double quotes escaping contents as necessary. | |
""" | |
return '"%s"' % etag.replace('\\', '\\\\').replace('"', '\\"') | |
class RestView(http.HttpResponse): | |
methods = ('OPTIONS', 'GET', 'HEAD') | |
allowed_methods = methods | |
def setup(self, request, *args, **kwargs): | |
""" | |
The setup method is called before dispatching to the appropriate method based | |
on the HTTP request method to allow for any global setup that needs to be done | |
on a per-request basis for any method. | |
""" | |
pass | |
def _check_request_allowed(self, request, *args, **kwargs): | |
""" | |
This method determines whether the user is allowed to perform the request | |
method that was sent to the server. | |
""" | |
if request.method.lower() not in (method.lower() for method in self.methods): | |
return False, http.HttpResponseNotAllowed(self.allowed_methods) | |
elif request.method.lower() not in (method.lower() for method in self.allowed_methods): | |
if request.user.is_authenticated(): | |
return False, http.HttpResponse(status=403) | |
else: | |
return False, http.HttpResponse(status=401) | |
return True, None | |
def dispatch(self, request, *args, **kwargs): | |
""" | |
This method dispatches the request to the appropriate method based on the | |
HTTP request method. | |
""" | |
allowed, response = self._check_request_allowed(request, *args, **kwargs) | |
if not allowed: | |
return response | |
if not hasattr(self, request.method.lower()) or \ | |
not operator.isCallable(getattr(self, request.method.lower())): | |
raise Exception("Allowed view method %s does not exist." % request.method.lower()) | |
return getattr(self, request.method.lower())(request, *args, **kwargs) | |
def conditional_dispatch(self, request, *args, **kwargs): | |
""" | |
For GET, HEAD, and PUT requests, this method calls the ``etag()`` and | |
``last_modified()`` methods and then checks whether a the appropriate | |
preconditions are satisfied before continuing. | |
""" | |
allowed, response = self._check_request_allowed(request, *args, **kwargs) | |
if not allowed: | |
return response | |
if request.method not in ('GET', 'HEAD', 'PUT'): | |
return self.dispatch(request, *args, **kwargs) | |
last_modified = str(self.last_modified(request, *args, **kwargs)) | |
etag = self.etag(request, *args, **kwargs) | |
if request.method in ('GET', 'HEAD'): | |
# Get HTTP request headers | |
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None) | |
if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None) | |
if if_none_match: | |
if_none_match = parse_etags(if_none_match) | |
# Calculate "not modified" condition | |
not_modified = (if_modified_since or if_none_match) and \ | |
(not if_modified_since or last_modified == if_modified_since) and \ | |
(not if_none_match or etag in if_none_match) | |
# Create appropriate response | |
if not_modified: | |
response = http.HttpResponseNotModified() | |
else: | |
response = self.dispatch(request, *args, **kwargs) | |
else: # method == 'PUT' | |
# Get the HTTP request headers | |
if_match = request.META.get('HTTP_IF_MATCH', None) | |
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE', None) | |
if if_match: | |
if_match = parse_etags(if_match) | |
# Calculate "modified" condition | |
modified = (if_unmodified_since and last_modified != if_unmodified_since) or \ | |
(if_match and etag not in if_match) | |
# Create appropriate response | |
if modified: | |
response = http.HttpResponse(status=412) # precondition failed | |
else: | |
response = self.dispatch(request, *args, **kwargs) | |
return response | |
def last_modified(self, request, *args, **kwargs): | |
return None | |
def etag(self, request, *args, **kwargs): | |
return None | |
def get(self, request, *args, **kwargs): | |
return http.HttpResponse() | |
def _head_response(self, request, *args, **kwargs): | |
response = http.HttpResponse() | |
response['Allow'] = ', '.join(self.allowed_methods) | |
return response | |
def options(self, request, *args, **kwargs): | |
return self._head_response(request, *args, **kwargs) | |
def head(self, request, *args, **kwargs): | |
return self._head_response(request, *args, **kwargs) | |
def _update(self, response): | |
""" | |
Merge the info from another response with this instance. | |
This method simply copies the attributes from the given response to | |
this instance, with the exceptions of the ``_headers`` and ``cookies`` | |
dictionaries, whose ``update`` methods are called. This means that any | |
headers or cookies which are present in this response but not the | |
argument are preserved. | |
""" | |
self._charset = response._charset | |
self._is_string = response._is_string | |
self._container = response._container | |
self._headers.update(response._headers) | |
self.cookies.update(response.cookies) | |
self.status_code = response.status_code | |
def __init__(self, request, *args, **kwargs): | |
super(RestView, self).__init__() | |
self.setup(request, *args, **kwargs) | |
object = self.conditional_dispatch(request, *args, **kwargs) | |
self._update(object) | |
# Set conditional response headers | |
last_modified = self.last_modified(request, *args, **kwargs) | |
etag = self.etag(request, *args, **kwargs) | |
if last_modified and not self.has_header('Last-Modified'): | |
self['Last-Modified'] = last_modified | |
if etag and not self.has_header('ETag'): | |
self['ETag'] = quote_etag(etag) | |
# Set allow headers | |
self['Allow'] = ', '.join(self.allowed_methods) | |
# Drop content-type if it's a 204 | |
if self.status_code == 204: | |
del self['content-type'] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment