This commit is contained in:
Tom Christie 2011-05-13 09:59:36 +01:00
parent 44c8b89c60
commit 8f6bcac7f3
6 changed files with 115 additions and 60 deletions

View File

@ -46,12 +46,20 @@ class RequestMixin(object):
_CONTENTTYPE_PARAM = '_content_type' _CONTENTTYPE_PARAM = '_content_type'
_CONTENT_PARAM = '_content' _CONTENT_PARAM = '_content'
"""
The set of request parsers that the view can handle.
Should be a tuple/list of classes as described in the ``parsers`` module.
"""
parsers = () parsers = ()
@property @property
def method(self): def method(self):
""" """
Returns the HTTP method. Returns the HTTP method.
This should be used instead of ``request.method``, as it allows the method
to be overridden by using a hidden form field on a form POST request.
""" """
if not hasattr(self, '_method'): if not hasattr(self, '_method'):
self._load_method_and_content_type() self._load_method_and_content_type()
@ -62,6 +70,10 @@ class RequestMixin(object):
def content_type(self): def content_type(self):
""" """
Returns the content type header. Returns the content type header.
This should be used instead of ``request.META.get('HTTP_CONTENT_TYPE')``,
as it allows the content type to be overridden by using a hidden form
field on a form POST request.
""" """
if not hasattr(self, '_content_type'): if not hasattr(self, '_content_type'):
self._load_method_and_content_type() self._load_method_and_content_type()
@ -71,7 +83,10 @@ class RequestMixin(object):
@property @property
def DATA(self): def DATA(self):
""" """
Returns the request data. Parses the request body and returns the data.
Similar to ``request.POST``, except that it handles arbitrary parsers,
and also works on methods other than POST (eg PUT).
""" """
if not hasattr(self, '_data'): if not hasattr(self, '_data'):
self._load_data_and_files() self._load_data_and_files()
@ -81,7 +96,9 @@ class RequestMixin(object):
@property @property
def FILES(self): def FILES(self):
""" """
Returns the request files. Parses the request body and returns the files.
Similar to request.FILES, except that it handles arbitrary parsers,
and also works on methods other than POST (eg PUT).
""" """
if not hasattr(self, '_files'): if not hasattr(self, '_files'):
self._load_data_and_files() self._load_data_and_files()
@ -205,8 +222,14 @@ class ResponseMixin(object):
_ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params _ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
_IGNORE_IE_ACCEPT_HEADER = True _IGNORE_IE_ACCEPT_HEADER = True
"""
The set of response renderers that the view can handle.
Should be a tuple/list of classes as described in the ``renderers`` module.
"""
renderers = () renderers = ()
# TODO: wrap this behavior around dispatch(), ensuring it works # TODO: wrap this behavior around dispatch(), ensuring it works
# out of the box with existing Django classes that use render_to_response. # out of the box with existing Django classes that use render_to_response.
def render(self, response): def render(self, response):
@ -330,14 +353,33 @@ class AuthMixin(object):
""" """
Simple mixin class to add authentication and permission checking to a ``View`` class. Simple mixin class to add authentication and permission checking to a ``View`` class.
""" """
"""
The set of authentication types that this view can handle.
Should be a tuple/list of classes as described in the ``authentication`` module.
"""
authentication = () authentication = ()
"""
The set of permissions that will be enforced on this view.
Should be a tuple/list of classes as described in the ``permissions`` module.
"""
permissions = () permissions = ()
@property @property
def user(self): def user(self):
"""
Returns the user for the current request, as determined by the set of
authentication classes applied to the ``View``.
"""
if not hasattr(self, '_user'): if not hasattr(self, '_user'):
self._user = self._authenticate() self._user = self._authenticate()
return self._user return self._user
def _authenticate(self): def _authenticate(self):
""" """
@ -351,6 +393,7 @@ class AuthMixin(object):
return user return user
return AnonymousUser() return AnonymousUser()
# TODO: wrap this behavior around dispatch() # TODO: wrap this behavior around dispatch()
def _check_permissions(self): def _check_permissions(self):
""" """
@ -359,7 +402,7 @@ class AuthMixin(object):
user = self.user user = self.user
for permission_cls in self.permissions: for permission_cls in self.permissions:
permission = permission_cls(self) permission = permission_cls(self)
permission.check_permission(user) permission.check_permission(user)
########## Resource Mixin ########## ########## Resource Mixin ##########

View File

@ -111,7 +111,8 @@ class PlainTextParser(BaseParser):
class FormParser(BaseParser, DataFlatener): class FormParser(BaseParser, DataFlatener):
"""The default parser for form data. """
The default parser for form data.
Return a dict containing a single value for each non-reserved parameter. Return a dict containing a single value for each non-reserved parameter.
In order to handle select multiple (and having possibly more than a single value for each parameter), In order to handle select multiple (and having possibly more than a single value for each parameter),
@ -122,7 +123,8 @@ class FormParser(BaseParser, DataFlatener):
"""The value of the parameter when the select multiple is empty. """The value of the parameter when the select multiple is empty.
Browsers are usually stripping the select multiple that have no option selected from the parameters sent. Browsers are usually stripping the select multiple that have no option selected from the parameters sent.
A common hack to avoid this is to send the parameter with a value specifying that the list is empty. A common hack to avoid this is to send the parameter with a value specifying that the list is empty.
This value will always be stripped before the data is returned.""" This value will always be stripped before the data is returned.
"""
EMPTY_VALUE = '_empty' EMPTY_VALUE = '_empty'
RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',) RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',)

View File

@ -7,6 +7,7 @@ and providing forms and links depending on the allowed methods, renderers and pa
""" """
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.serializers.json import DateTimeAwareJSONEncoder
from django.template import RequestContext, loader from django.template import RequestContext, loader
from django.utils import simplejson as json from django.utils import simplejson as json
@ -81,7 +82,7 @@ class JSONRenderer(BaseRenderer):
except (ValueError, TypeError): except (ValueError, TypeError):
indent = None indent = None
return json.dumps(obj, indent=indent, sort_keys=sort_keys) return json.dumps(obj, cls=DateTimeAwareJSONEncoder, indent=indent, sort_keys=sort_keys)
class XMLRenderer(BaseRenderer): class XMLRenderer(BaseRenderer):

View File

@ -16,9 +16,14 @@ def _model_to_dict(instance, fields=None, exclude=None):
""" """
opts = instance._meta opts = instance._meta
data = {} data = {}
#print [rel.name for rel in opts.get_all_related_objects()]
#related = [rel.get_accessor_name() for rel in opts.get_all_related_objects()]
#print [getattr(instance, rel) for rel in related]
for f in opts.fields + opts.many_to_many: for f in opts.fields + opts.many_to_many:
if not f.editable: #if not f.editable:
continue # continue
if fields and not f.name in fields: if fields and not f.name in fields:
continue continue
if exclude and f.name in exclude: if exclude and f.name in exclude:
@ -27,6 +32,15 @@ def _model_to_dict(instance, fields=None, exclude=None):
data[f.name] = getattr(instance, f.name) data[f.name] = getattr(instance, f.name)
else: else:
data[f.name] = f.value_from_object(instance) data[f.name] = f.value_from_object(instance)
#print fields - (opts.fields + opts.many_to_many)
#for related in [rel.get_accessor_name() for rel in opts.get_all_related_objects()]:
# if fields and not related in fields:
# continue
# if exclude and related in exclude:
# continue
# data[related] = getattr(instance, related)
return data return data
@ -127,6 +141,8 @@ class Resource(BaseResource):
A (horrible) munging of Piston's pre-serialization. Returns a dict. A (horrible) munging of Piston's pre-serialization. Returns a dict.
""" """
return _object_to_data(obj)
def _any(thing, fields=()): def _any(thing, fields=()):
""" """
Dispatch, all types are routed through here. Dispatch, all types are routed through here.

View File

@ -1,6 +1,6 @@
"""Tests for the resource module""" """Tests for the resource module"""
from django.test import TestCase from django.test import TestCase
from djangorestframework.resource import _object_to_data from djangorestframework.resources import _object_to_data
import datetime import datetime
import decimal import decimal

View File

@ -56,9 +56,10 @@ class BaseView(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, View):
""" """
return [method.upper() for method in self.http_method_names if hasattr(self, method)] return [method.upper() for method in self.http_method_names if hasattr(self, method)]
def http_method_not_allowed(self, request, *args, **kwargs): def http_method_not_allowed(self, request, *args, **kwargs):
""" """
Return an HTTP 405 error if an operation is called which does not have a handler method. Return an HTTP 405 error if an operation is called which does not have a handler method.
""" """
raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED, raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method}) {'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
@ -68,56 +69,48 @@ class BaseView(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, View):
# all other authentication is CSRF exempt. # all other authentication is CSRF exempt.
@csrf_exempt @csrf_exempt
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: self.request = request
self.request = request self.args = args
self.args = args self.kwargs = kwargs
self.kwargs = kwargs
# 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)
try:
# Authenticate and check request is has the relevant permissions
self._check_permissions()
# Get the appropriate handler method
if self.method.lower() in self.http_method_names:
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response_obj = handler(request, *args, **kwargs)
# Allow return value to be either Response, or an object, or None
if isinstance(response_obj, Response):
response = response_obj
elif response_obj is not None:
response = Response(status.HTTP_200_OK, response_obj)
else:
response = Response(status.HTTP_204_NO_CONTENT)
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
response.cleaned_content = self.object_to_data(response.raw_content)
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here. except ErrorResponse, exc:
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host()) response = exc.response
set_script_prefix(prefix)
# Always add these headers.
try: #
# Authenticate and check request is has the relevant permissions # TODO - this isn't actually the correct way to set the vary header,
self._check_permissions() # also it's currently sub-obtimal for HTTP caching - need to sort that out.
response.headers['Allow'] = ', '.join(self.allowed_methods)
# Get the appropriate handler method response.headers['Vary'] = 'Authenticate, Accept'
if self.method.lower() in self.http_method_names:
handler = getattr(self, self.method.lower(), self.http_method_not_allowed) return self.render(response)
else:
handler = self.http_method_not_allowed
response_obj = handler(request, *args, **kwargs)
# Allow return value to be either Response, or an object, or None
if isinstance(response_obj, Response):
response = response_obj
elif response_obj is not None:
response = Response(status.HTTP_200_OK, response_obj)
else:
response = Response(status.HTTP_204_NO_CONTENT)
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
response.cleaned_content = self.object_to_data(response.raw_content)
except ErrorResponse, exc:
response = exc.response
except:
import traceback
traceback.print_exc()
raise
# Always add these headers.
#
# TODO - this isn't actually the correct way to set the vary header,
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
response.headers['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept'
return self.render(response)
except:
import traceback
traceback.print_exc()