aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework/resource.py
diff options
context:
space:
mode:
Diffstat (limited to 'djangorestframework/resource.py')
-rw-r--r--djangorestframework/resource.py245
1 files changed, 49 insertions, 196 deletions
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index b1f48f06..68ca6bf3 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -1,15 +1,16 @@
-from django.contrib.sites.models import Site
-from django.core.urlresolvers import reverse
-from django.http import HttpResponse
+from django.core.urlresolvers import set_script_prefix
+from django.views.decorators.csrf import csrf_exempt
+from djangorestframework.compat import View
+from djangorestframework.emitters import EmitterMixin
from djangorestframework.parsers import ParserMixin
+from djangorestframework.authenticators import AuthenticatorMixin
from djangorestframework.validators import FormValidatorMixin
from djangorestframework.content import OverloadedContentMixin
from djangorestframework.methods import OverloadedPOSTMethodMixin
from djangorestframework import emitters, parsers, authenticators
from djangorestframework.response import status, Response, ResponseException
-from decimal import Decimal
import re
# TODO: Figure how out references and named urls need to work nicely
@@ -21,10 +22,10 @@ import re
__all__ = ['Resource']
-_MSIE_USER_AGENT = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
-class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, OverloadedPOSTMethodMixin):
+class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin,
+ OverloadedContentMixin, OverloadedPOSTMethodMixin, View):
"""Handles incoming requests and maps them to REST operations,
performing authentication, input deserialization, input validation, output serialization."""
@@ -52,67 +53,21 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
# Optional form for input validation and presentation of HTML formatted responses.
form = None
+ # Allow name and description for the Resource to be set explicitly,
+ # overiding the default classname/docstring behaviour.
+ # These are used for documentation in the standard html and text emitters.
+ name = None
+ description = None
+
# Map standard HTTP methods to function calls
callmap = { 'GET': 'get', 'POST': 'post',
'PUT': 'put', 'DELETE': 'delete' }
+
# Some reserved parameters to allow us to use standard HTML forms with our resource
# Override any/all of these with None to disable them, or override them with another value to rename them.
- ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params CONTENTTYPE_PARAM = '_contenttype' # Allow override of Content-Type header in form params (allows sending arbitrary content with standard forms)
CSRF_PARAM = 'csrfmiddlewaretoken' # Django's CSRF token used in form params
- _MUNGE_IE_ACCEPT_HEADER = True
-
- def __new__(cls, *args, **kwargs):
- """Make the class callable so it can be used as a Django view."""
- self = object.__new__(cls)
- if args:
- request = args[0]
- self.__init__(request)
- return self._handle_request(request, *args[1:], **kwargs)
- else:
- self.__init__()
- return self
-
-
- def __init__(self, request=None):
- """"""
- # Setup the resource context
- self.request = request
- self.response = None
- self.form_instance = None
-
- # These sets are determined now so that overridding classes can modify the various parameter names,
- # or set them to None to disable them.
- self.RESERVED_FORM_PARAMS = set((self.METHOD_PARAM, self.CONTENTTYPE_PARAM, self.CONTENT_PARAM, self.CSRF_PARAM))
- self.RESERVED_QUERY_PARAMS = set((self.ACCEPT_QUERY_PARAM))
- self.RESERVED_FORM_PARAMS.discard(None)
- self.RESERVED_QUERY_PARAMS.discard(None)
-
-
- @property
- def name(self):
- """Provide a name for the resource.
- By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
- class_name = self.__class__.__name__
- return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).strip()
-
- @property
- def description(self):
- """Provide a description for the resource.
- By default this is the class's docstring with leading line spaces stripped."""
- return re.sub(re.compile('^ +', re.MULTILINE), '', self.__doc__)
-
- @property
- def emitted_media_types(self):
- """Return an list of all the media types that this resource can emit."""
- return [emitter.media_type for emitter in self.emitters]
-
- @property
- def default_emitter(self):
- """Return the resource's most prefered emitter.
- (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
- return self.emitters[0]
def get(self, request, auth, *args, **kwargs):
"""Must be subclassed to be implemented."""
@@ -134,12 +89,6 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
self.not_implemented('DELETE')
- def reverse(self, view, *args, **kwargs):
- """Return a fully qualified URI for a given view or resource.
- Add the domain using the Sites framework if possible, otherwise fallback to using the current request."""
- return self.add_domain(reverse(view, args=args, kwargs=kwargs))
-
-
def not_implemented(self, operation):
"""Return an HTTP 500 server error if an operation is called which has been allowed by
allowed_methods, but which has not been implemented."""
@@ -147,36 +96,6 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
- def add_domain(self, path):
- """Given a path, return an fully qualified URI.
- Use the Sites framework if possible, otherwise fallback to using the domain from the current request."""
-
- # Note that out-of-the-box the Sites framework uses the reserved domain 'example.com'
- # See RFC 2606 - http://www.faqs.org/rfcs/rfc2606.html
- try:
- site = Site.objects.get_current()
- if site.domain and site.domain != 'example.com':
- return 'http://%s%s' % (site.domain, path)
- except:
- pass
-
- return self.request.build_absolute_uri(path)
-
-
- def authenticate(self, request):
- """Attempt to authenticate the request, returning an authentication context or None.
- An authentication context may be any object, although in many cases it will be a User instance."""
-
- # Attempt authentication against each authenticator in turn,
- # and return None if no authenticators succeed in authenticating the request.
- for authenticator in self.authenticators:
- auth_context = authenticator(self).authenticate(request)
- if auth_context:
- return auth_context
-
- return None
-
-
def check_method_allowed(self, method, auth):
"""Ensure the request method is permitted for this resource, raising a ResourceException if it is not."""
@@ -198,76 +117,16 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
"""Perform any resource-specific data filtering prior to the standard HTTP
content-type serialization.
- Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can."""
+ Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.
+
+ TODO: This is going to be removed. I think that the 'fields' behaviour is going to move into
+ the EmitterMixin and Emitter classes."""
return data
+ # Session based authentication is explicitly CSRF validated, all other authentication is CSRF exempt.
- def determine_emitter(self, request):
- """Return the appropriate emitter for the output, given the client's 'Accept' header,
- and the content types that this Resource knows how to serve.
-
- See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
-
- if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
- # Use _accept parameter override
- accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
- elif self._MUNGE_IE_ACCEPT_HEADER and request.META.has_key('HTTP_USER_AGENT') and _MSIE_USER_AGENT.match(request.META['HTTP_USER_AGENT']):
- accept_list = ['text/html', '*/*']
- elif request.META.has_key('HTTP_ACCEPT'):
- # Use standard HTTP Accept negotiation
- accept_list = request.META["HTTP_ACCEPT"].split(',')
- else:
- # No accept header specified
- return self.default_emitter
-
- # Parse the accept header into a dict of {qvalue: set of media types}
- # We ignore mietype parameters
- accept_dict = {}
- for token in accept_list:
- components = token.split(';')
- mimetype = components[0].strip()
- qvalue = Decimal('1.0')
-
- if len(components) > 1:
- # Parse items that have a qvalue eg text/html;q=0.9
- try:
- (q, num) = components[-1].split('=')
- if q == 'q':
- qvalue = Decimal(num)
- except:
- # Skip malformed entries
- continue
-
- if accept_dict.has_key(qvalue):
- accept_dict[qvalue].add(mimetype)
- else:
- accept_dict[qvalue] = set((mimetype,))
-
- # Convert to a list of sets ordered by qvalue (highest first)
- accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
-
- for accept_set in accept_sets:
- # Return any exact match
- for emitter in self.emitters:
- if emitter.media_type in accept_set:
- return emitter
-
- # Return any subtype match
- for emitter in self.emitters:
- if emitter.media_type.split('/')[0] + '/*' in accept_set:
- return emitter
-
- # Return default
- if '*/*' in accept_set:
- return self.default_emitter
-
-
- raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE,
- {'detail': 'Could not statisfy the client\'s Accept header',
- 'available_types': self.emitted_media_types})
-
-
- def _handle_request(self, request, *args, **kwargs):
+ @csrf_exempt
+ def dispatch(self, request, *args, **kwargs):
"""This method is the core of Resource, through which all requests are passed.
Broadly this consists of the following procedure:
@@ -279,12 +138,23 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
4. cleanup the response data
5. serialize response data into response content, using standard HTTP content negotiation
"""
- emitter = None
+
+ self.request = request
+
+ # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
+ prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
+ set_script_prefix(prefix)
+
+ # These sets are determined now so that overridding classes can modify the various parameter names,
+ # or set them to None to disable them.
+ self.RESERVED_FORM_PARAMS = set((self.METHOD_PARAM, self.CONTENTTYPE_PARAM, self.CONTENT_PARAM, self.CSRF_PARAM))
+ self.RESERVED_QUERY_PARAMS = set((self.ACCEPT_QUERY_PARAM))
+ self.RESERVED_FORM_PARAMS.discard(None)
+ self.RESERVED_QUERY_PARAMS.discard(None)
+
method = self.determine_method(request)
try:
- # Before we attempt anything else determine what format to emit our response data with.
- emitter = self.determine_emitter(request)
# Authenticate the request, and store any context so that the resource operations can
# do more fine grained authentication if required.
@@ -301,51 +171,34 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
func = getattr(self, self.callmap.get(method, None))
# Either generate the response data, deserializing and validating any request data
- # TODO: Add support for message bodys on other HTTP methods, as it is valid.
+ # TODO: Add support for message bodys on other HTTP methods, as it is valid (although non-conventional).
if method in ('PUT', 'POST'):
(content_type, content) = self.determine_content(request)
parser_content = self.parse(content_type, content)
cleaned_content = self.validate(parser_content)
- response = func(request, auth_context, cleaned_content, *args, **kwargs)
+ response_obj = func(request, auth_context, cleaned_content, *args, **kwargs)
else:
- response = func(request, auth_context, *args, **kwargs)
+ response_obj = func(request, auth_context, *args, **kwargs)
# Allow return value to be either Response, or an object, or None
- if isinstance(response, Response):
- self.response = response
- elif response is not None:
- self.response = Response(status.HTTP_200_OK, response)
+ if isinstance(response_obj, Response):
+ response = response_obj
+ elif response_obj is not None:
+ response = Response(status.HTTP_200_OK, response_obj)
else:
- self.response = Response(status.HTTP_204_NO_CONTENT)
+ response = Response(status.HTTP_204_NO_CONTENT)
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
- self.response.cleaned_content = self.cleanup_response(self.response.raw_content)
+ response.cleaned_content = self.cleanup_response(response.raw_content)
except ResponseException, exc:
- self.response = exc.response
-
- # Fall back to the default emitter if we failed to perform content negotiation
- if emitter is None:
- emitter = self.default_emitter
-
+ response = exc.response
# Always add these headers
- self.response.headers['Allow'] = ', '.join(self.allowed_methods)
- self.response.headers['Vary'] = 'Authenticate, Allow'
-
- # Serialize the response content
- if self.response.has_content_body:
- content = emitter(self).emit(output=self.response.cleaned_content)
- else:
- content = emitter(self).emit()
-
- # Build the HTTP Response
- # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
- resp = HttpResponse(content, mimetype=emitter.media_type, status=self.response.status)
- for (key, val) in self.response.headers.items():
- resp[key] = val
-
- return resp
+ response.headers['Allow'] = ', '.join(self.allowed_methods)
+ response.headers['Vary'] = 'Authenticate, Allow'
+
+ return self.emit(response)