Skip to content

Instantly share code, notes, and snippets.

@mmalone
Created May 26, 2009 20:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mmalone/118277 to your computer and use it in GitHub Desktop.
Save mmalone/118277 to your computer and use it in GitHub Desktop.
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