mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-18 12:12:19 +03:00
Merge 9dbe8b646e
into ccbb536896
This commit is contained in:
commit
102d7cd89b
|
@ -1,10 +1,13 @@
|
||||||
"""
|
"""
|
||||||
The :mod:`authentication` module provides a set of pluggable authentication classes.
|
The :mod:`authentication` module provides a set of pluggable authentication
|
||||||
|
classes.
|
||||||
|
|
||||||
Authentication behavior is provided by mixing the :class:`mixins.AuthMixin` class into a :class:`View` class.
|
Authentication behavior is provided by mixing the :class:`mixins.AuthMixin`
|
||||||
|
class into a :class:`View` class.
|
||||||
|
|
||||||
The set of authentication methods which are used is then specified by setting the
|
The set of authentication methods which are used is then specified by setting
|
||||||
:attr:`authentication` attribute on the :class:`View` class, and listing a set of :class:`authentication` classes.
|
the :attr:`authentication` attribute on the :class:`View` class, and listing a
|
||||||
|
set of :class:`authentication` classes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
|
@ -26,23 +29,25 @@ class BaseAuthentication(object):
|
||||||
|
|
||||||
def __init__(self, view):
|
def __init__(self, view):
|
||||||
"""
|
"""
|
||||||
:class:`Authentication` classes are always passed the current view on creation.
|
:class:`Authentication` classes are always passed the current view on
|
||||||
|
creation.
|
||||||
"""
|
"""
|
||||||
self.view = view
|
self.view = view
|
||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
"""
|
"""
|
||||||
Authenticate the :obj:`request` and return a :obj:`User` or :const:`None`. [*]_
|
Authenticate the :obj:`request` and return a :obj:`User` or
|
||||||
|
:const:`None`. [*]_
|
||||||
|
|
||||||
.. [*] The authentication context *will* typically be a :obj:`User`,
|
.. [*] The authentication context *will* typically be a :obj:`User`,
|
||||||
but it need not be. It can be any user-like object so long as the
|
but it need not be. It can be any user-like object so long as the
|
||||||
permissions classes (see the :mod:`permissions` module) on the view can
|
permissions classes (see the :mod:`permissions` module) on the view
|
||||||
handle the object and use it to determine if the request has the required
|
can handle the object and use it to determine if the request has
|
||||||
permissions or not.
|
the required permissions or not.
|
||||||
|
|
||||||
This can be an important distinction if you're implementing some token
|
This can be an important distinction if you're implementing some
|
||||||
based authentication mechanism, where the authentication context
|
token based authentication mechanism, where the authentication
|
||||||
may be more involved than simply mapping to a :obj:`User`.
|
context may be more involved than simply mapping to a :obj:`User`.
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -51,11 +56,17 @@ class BasicAuthentication(BaseAuthentication):
|
||||||
"""
|
"""
|
||||||
Use HTTP Basic authentication.
|
Use HTTP Basic authentication.
|
||||||
"""
|
"""
|
||||||
|
def _authenticate_user(self, username, password):
|
||||||
|
user = authenticate(username=username, password=password)
|
||||||
|
if user and user.is_active:
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
"""
|
"""
|
||||||
Returns a :obj:`User` if a correct username and password have been supplied
|
Returns a :obj:`User` if a correct username and password have been
|
||||||
using HTTP Basic authentication. Otherwise returns :const:`None`.
|
supplied using HTTP Basic authentication.
|
||||||
|
Otherwise returns :const:`None`.
|
||||||
"""
|
"""
|
||||||
from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
|
from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
|
||||||
|
|
||||||
|
@ -68,11 +79,12 @@ class BasicAuthentication(BaseAuthentication):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
uname, passwd = smart_unicode(auth_parts[0]), smart_unicode(auth_parts[2])
|
username = smart_unicode(auth_parts[0])
|
||||||
|
password = smart_unicode(auth_parts[2])
|
||||||
except DjangoUnicodeDecodeError:
|
except DjangoUnicodeDecodeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user = authenticate(username=uname, password=passwd)
|
user = authenticate(username=username, password=password)
|
||||||
if user is not None and user.is_active:
|
if user is not None and user.is_active:
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
|
@ -85,8 +97,8 @@ class UserLoggedInAuthentication(BaseAuthentication):
|
||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
"""
|
"""
|
||||||
Returns a :obj:`User` if the request session currently has a logged in user.
|
Returns a :obj:`User` if the request session currently has a logged in
|
||||||
Otherwise returns :const:`None`.
|
user. Otherwise returns :const:`None`.
|
||||||
"""
|
"""
|
||||||
# TODO: Might be cleaner to switch this back to using request.POST,
|
# TODO: Might be cleaner to switch this back to using request.POST,
|
||||||
# and let FormParser/MultiPartParser deal with the consequences.
|
# and let FormParser/MultiPartParser deal with the consequences.
|
||||||
|
|
|
@ -5,14 +5,13 @@ classes that can be added to a `View`.
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models.fields.related import ForeignKey
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
from djangorestframework.renderers import BaseRenderer
|
from djangorestframework.renderers import BaseRenderer
|
||||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
from djangorestframework.resources import Resource, FormResource, ModelResource
|
||||||
from djangorestframework.response import Response, ErrorResponse
|
from djangorestframework.response import Response, ErrorResponse
|
||||||
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
from djangorestframework.utils import MSIE_USER_AGENT_REGEX
|
||||||
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
|
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
|
||||||
|
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
@ -27,11 +26,7 @@ __all__ = (
|
||||||
# Reverse URL lookup behavior
|
# Reverse URL lookup behavior
|
||||||
'InstanceMixin',
|
'InstanceMixin',
|
||||||
# Model behavior mixins
|
# Model behavior mixins
|
||||||
'ReadModelMixin',
|
'ModelMixin',
|
||||||
'CreateModelMixin',
|
|
||||||
'UpdateModelMixin',
|
|
||||||
'DeleteModelMixin',
|
|
||||||
'ListModelMixin'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,14 +54,14 @@ class RequestMixin(object):
|
||||||
"""
|
"""
|
||||||
Returns the HTTP method.
|
Returns the HTTP method.
|
||||||
|
|
||||||
This should be used instead of just reading :const:`request.method`, as it allows the `method`
|
This should be used instead of just reading :const:`request.method`, as
|
||||||
to be overridden by using a hidden `form` field on a form POST request.
|
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()
|
||||||
return self._method
|
return self._method
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content_type(self):
|
def content_type(self):
|
||||||
"""
|
"""
|
||||||
|
@ -80,7 +75,6 @@ class RequestMixin(object):
|
||||||
self._load_method_and_content_type()
|
self._load_method_and_content_type()
|
||||||
return self._content_type
|
return self._content_type
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def DATA(self):
|
def DATA(self):
|
||||||
"""
|
"""
|
||||||
|
@ -93,7 +87,6 @@ class RequestMixin(object):
|
||||||
self._load_data_and_files()
|
self._load_data_and_files()
|
||||||
return self._data
|
return self._data
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def FILES(self):
|
def FILES(self):
|
||||||
"""
|
"""
|
||||||
|
@ -105,7 +98,6 @@ class RequestMixin(object):
|
||||||
self._load_data_and_files()
|
self._load_data_and_files()
|
||||||
return self._files
|
return self._files
|
||||||
|
|
||||||
|
|
||||||
def _load_data_and_files(self):
|
def _load_data_and_files(self):
|
||||||
"""
|
"""
|
||||||
Parse the request content into self.DATA and self.FILES.
|
Parse the request content into self.DATA and self.FILES.
|
||||||
|
@ -114,18 +106,19 @@ class RequestMixin(object):
|
||||||
self._load_method_and_content_type()
|
self._load_method_and_content_type()
|
||||||
|
|
||||||
if not hasattr(self, '_data'):
|
if not hasattr(self, '_data'):
|
||||||
(self._data, self._files) = self._parse(self._get_stream(), self._content_type)
|
(self._data, self._files) = self._parse(self._get_stream(),
|
||||||
|
self._content_type)
|
||||||
|
|
||||||
def _load_method_and_content_type(self):
|
def _load_method_and_content_type(self):
|
||||||
"""
|
"""
|
||||||
Set the method and content_type, and then check if they've been overridden.
|
Set the method and content_type, and then check if they've been
|
||||||
|
overridden.
|
||||||
"""
|
"""
|
||||||
self._method = self.request.method
|
self._method = self.request.method
|
||||||
self._content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
|
self._content_type = self.request.META.get('HTTP_CONTENT_TYPE',
|
||||||
|
self.request.META.get('CONTENT_TYPE', ''))
|
||||||
self._perform_form_overloading()
|
self._perform_form_overloading()
|
||||||
|
|
||||||
|
|
||||||
def _get_stream(self):
|
def _get_stream(self):
|
||||||
"""
|
"""
|
||||||
Returns an object that may be used to stream the request content.
|
Returns an object that may be used to stream the request content.
|
||||||
|
@ -133,7 +126,8 @@ class RequestMixin(object):
|
||||||
request = self.request
|
request = self.request
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content_length = int(request.META.get('CONTENT_LENGTH', request.META.get('HTTP_CONTENT_LENGTH')))
|
content_length = int(request.META.get('CONTENT_LENGTH',
|
||||||
|
request.META.get('HTTP_CONTENT_LENGTH')))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
content_length = 0
|
content_length = 0
|
||||||
|
|
||||||
|
@ -142,18 +136,20 @@ class RequestMixin(object):
|
||||||
if content_length == 0:
|
if content_length == 0:
|
||||||
return None
|
return None
|
||||||
elif hasattr(request, 'read'):
|
elif hasattr(request, 'read'):
|
||||||
return request
|
return request
|
||||||
return StringIO(request.raw_post_data)
|
return StringIO(request.raw_post_data)
|
||||||
|
|
||||||
|
|
||||||
def _perform_form_overloading(self):
|
def _perform_form_overloading(self):
|
||||||
"""
|
"""
|
||||||
If this is a form POST request, then we need to check if the method and content/content_type have been
|
If this is a form POST request, then we need to check if the method and
|
||||||
overridden by setting them in hidden form fields or not.
|
content/content_type have been overridden by setting them in hidden
|
||||||
|
form fields or not.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# We only need to use form overloading on form POST requests.
|
# We only need to use form overloading on form POST requests.
|
||||||
if not self._USE_FORM_OVERLOADING or self._method != 'POST' or not is_form_media_type(self._content_type):
|
if (not self._USE_FORM_OVERLOADING
|
||||||
|
or self._method != 'POST'
|
||||||
|
or not is_form_media_type(self._content_type)):
|
||||||
return
|
return
|
||||||
|
|
||||||
# At this point we're committed to parsing the request as form data.
|
# At this point we're committed to parsing the request as form data.
|
||||||
|
@ -171,27 +167,24 @@ class RequestMixin(object):
|
||||||
stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0])
|
stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0])
|
||||||
(self._data, self._files) = self._parse(stream, self._content_type)
|
(self._data, self._files) = self._parse(stream, self._content_type)
|
||||||
|
|
||||||
|
|
||||||
def _parse(self, stream, content_type):
|
def _parse(self, stream, content_type):
|
||||||
"""
|
"""
|
||||||
Parse the request content.
|
Parse the request content.
|
||||||
|
|
||||||
May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request).
|
May raise a 415 ErrorResponse (Unsupported Media Type), or a 400
|
||||||
|
ErrorResponse (Bad Request).
|
||||||
"""
|
"""
|
||||||
if stream is None or content_type is None:
|
if stream is None or content_type is None:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
parsers = as_tuple(self.parsers)
|
for parser_cls in self.parsers:
|
||||||
|
|
||||||
for parser_cls in parsers:
|
|
||||||
parser = parser_cls(self)
|
parser = parser_cls(self)
|
||||||
if parser.can_handle_request(content_type):
|
if parser.can_handle_request(content_type):
|
||||||
return parser.parse(stream)
|
return parser.parse(stream)
|
||||||
|
|
||||||
raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
error = {'error':
|
||||||
{'error': 'Unsupported media type in request \'%s\'.' %
|
"Unsupported media type in request '%s'." % content_type}
|
||||||
content_type})
|
raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, error)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _parsed_media_types(self):
|
def _parsed_media_types(self):
|
||||||
|
@ -200,7 +193,6 @@ class RequestMixin(object):
|
||||||
"""
|
"""
|
||||||
return [parser.media_type for parser in self.parsers]
|
return [parser.media_type for parser in self.parsers]
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _default_parser(self):
|
def _default_parser(self):
|
||||||
"""
|
"""
|
||||||
|
@ -209,29 +201,34 @@ class RequestMixin(object):
|
||||||
return self.parsers[0]
|
return self.parsers[0]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
########## ResponseMixin ##########
|
########## ResponseMixin ##########
|
||||||
|
|
||||||
|
|
||||||
class ResponseMixin(object):
|
class ResponseMixin(object):
|
||||||
"""
|
"""
|
||||||
Adds behavior for pluggable `Renderers` to a :class:`views.View` class.
|
Adds behavior for pluggable `Renderers` to a :class:`views.View` class.
|
||||||
|
|
||||||
Default behavior is to use standard HTTP Accept header content negotiation.
|
Default behavior is to use standard HTTP Accept header content negotiation.
|
||||||
Also supports overriding the content type by specifying an ``_accept=`` parameter in the URL.
|
|
||||||
Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead.
|
Also supports overriding the content type by specifying an ``_accept=``
|
||||||
|
parameter in the URL.
|
||||||
|
|
||||||
|
Ignores Accept headers from Internet Explorer user agents and uses a
|
||||||
|
sensible browser Accept header instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
|
# Allow override of Accept header in URL query params
|
||||||
|
_ACCEPT_QUERY_PARAM = '_accept'
|
||||||
_IGNORE_IE_ACCEPT_HEADER = True
|
_IGNORE_IE_ACCEPT_HEADER = True
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The set of response renderers that the view can handle.
|
The set of response renderers that the view can handle.
|
||||||
|
|
||||||
Should be a tuple/list of classes as described in the :mod:`renderers` module.
|
Should be a tuple/list of classes as described in the :mod:`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):
|
||||||
|
@ -258,34 +255,41 @@ class ResponseMixin(object):
|
||||||
content = renderer.render()
|
content = renderer.render()
|
||||||
|
|
||||||
# Build the HTTP Response
|
# Build the HTTP Response
|
||||||
resp = HttpResponse(content, mimetype=response.media_type, status=response.status)
|
resp = HttpResponse(content, mimetype=response.media_type,
|
||||||
|
status=response.status)
|
||||||
for (key, val) in response.headers.items():
|
for (key, val) in response.headers.items():
|
||||||
resp[key] = val
|
resp[key] = val
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def _determine_renderer(self, request):
|
def _determine_renderer(self, request):
|
||||||
"""
|
"""
|
||||||
Determines the appropriate renderer for the output, given the client's 'Accept' header,
|
Determines the appropriate renderer for the output, given the client's
|
||||||
and the :attr:`renderers` set on this class.
|
'Accept' header, and the :attr:`renderers` set on this class.
|
||||||
|
|
||||||
Returns a 2-tuple of `(renderer, media_type)`
|
Returns a 2-tuple of `(renderer, media_type)`
|
||||||
|
|
||||||
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
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):
|
if (self._ACCEPT_QUERY_PARAM and
|
||||||
|
request.GET.get(self._ACCEPT_QUERY_PARAM, None)):
|
||||||
# Use _accept parameter override
|
# Use _accept parameter override
|
||||||
accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)]
|
accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)]
|
||||||
|
|
||||||
elif (self._IGNORE_IE_ACCEPT_HEADER and
|
elif (self._IGNORE_IE_ACCEPT_HEADER and
|
||||||
request.META.has_key('HTTP_USER_AGENT') and
|
'HTTP_USER_AGENT' in request.META and
|
||||||
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
|
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
|
||||||
# Ignore MSIE's broken accept behavior and do something sensible instead
|
# Ignore MSIE's broken accept behavior and do something sensible
|
||||||
|
# instead.
|
||||||
accept_list = ['text/html', '*/*']
|
accept_list = ['text/html', '*/*']
|
||||||
elif request.META.has_key('HTTP_ACCEPT'):
|
|
||||||
|
elif 'HTTP_ACCEPT' in request.META:
|
||||||
# Use standard HTTP Accept negotiation
|
# Use standard HTTP Accept negotiation
|
||||||
accept_list = [token.strip() for token in request.META["HTTP_ACCEPT"].split(',')]
|
accept_list = [token.strip() for token in
|
||||||
|
request.META['HTTP_ACCEPT'].split(',')]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# No accept header specified
|
# No accept header specified
|
||||||
accept_list = ['*/*']
|
accept_list = ['*/*']
|
||||||
|
@ -293,7 +297,7 @@ class ResponseMixin(object):
|
||||||
# Check the acceptable media types against each renderer,
|
# Check the acceptable media types against each renderer,
|
||||||
# attempting more specific media types first
|
# attempting more specific media types first
|
||||||
# NB. The inner loop here isn't as bad as it first looks :)
|
# NB. The inner loop here isn't as bad as it first looks :)
|
||||||
# Worst case is we're looping over len(accept_list) * len(self.renderers)
|
# Worst case is: len(accept_list) * len(self.renderers)
|
||||||
renderers = [renderer_cls(self) for renderer_cls in self.renderers]
|
renderers = [renderer_cls(self) for renderer_cls in self.renderers]
|
||||||
|
|
||||||
for accepted_media_type_lst in order_by_precedence(accept_list):
|
for accepted_media_type_lst in order_by_precedence(accept_list):
|
||||||
|
@ -303,10 +307,9 @@ class ResponseMixin(object):
|
||||||
return renderer, accepted_media_type
|
return renderer, accepted_media_type
|
||||||
|
|
||||||
# No acceptable renderers were found
|
# No acceptable renderers were found
|
||||||
raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE,
|
error = {'detail': "Could not satisfy the client's Accept header",
|
||||||
{'detail': 'Could not satisfy the client\'s Accept header',
|
'available_types': self._rendered_media_types}
|
||||||
'available_types': self._rendered_media_types})
|
raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE, error)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _rendered_media_types(self):
|
def _rendered_media_types(self):
|
||||||
|
@ -334,39 +337,40 @@ class ResponseMixin(object):
|
||||||
|
|
||||||
class AuthMixin(object):
|
class AuthMixin(object):
|
||||||
"""
|
"""
|
||||||
Simple :class:`mixin` class to add authentication and permission checking to a :class:`View` class.
|
Simple :class:`mixin` class to add authentication and permission checking
|
||||||
|
to a :class:`View` class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The set of authentication types that this view can handle.
|
The set of authentication types that this view can handle.
|
||||||
|
|
||||||
Should be a tuple/list of classes as described in the :mod:`authentication` module.
|
Should be a tuple/list of classes as described in the :mod:`authentication`
|
||||||
|
module.
|
||||||
"""
|
"""
|
||||||
authentication = ()
|
authentication = ()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The set of permissions that will be enforced on this view.
|
The set of permissions that will be enforced on this view.
|
||||||
|
|
||||||
Should be a tuple/list of classes as described in the :mod:`permissions` module.
|
Should be a tuple/list of classes as described in the :mod:`permissions`
|
||||||
|
module.
|
||||||
"""
|
"""
|
||||||
permissions = ()
|
permissions = ()
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user(self):
|
def user(self):
|
||||||
"""
|
"""
|
||||||
Returns the :obj:`user` for the current request, as determined by the set of
|
Returns the :obj:`user` for the current request, as determined by the
|
||||||
:class:`authentication` classes applied to the :class:`View`.
|
set of :class:`authentication` classes applied to the :class:`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):
|
||||||
"""
|
"""
|
||||||
Attempt to authenticate the request using each authentication class in turn.
|
Attempt to authenticate the request using each authentication class in
|
||||||
Returns a ``User`` object, which may be ``AnonymousUser``.
|
turn. Returns a ``User`` object, which may be ``AnonymousUser``.
|
||||||
"""
|
"""
|
||||||
for authentication_cls in self.authentication:
|
for authentication_cls in self.authentication:
|
||||||
authentication = authentication_cls(self)
|
authentication = authentication_cls(self)
|
||||||
|
@ -375,7 +379,6 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
@ -395,10 +398,12 @@ class ResourceMixin(object):
|
||||||
|
|
||||||
Should be a class as described in the :mod:`resources` module.
|
Should be a class as described in the :mod:`resources` module.
|
||||||
|
|
||||||
The :obj:`resource` is an object that maps a view onto it's representation on the server.
|
The :obj:`resource` is an object that maps a view onto it's representation
|
||||||
|
on the server.
|
||||||
|
|
||||||
It provides validation on the content of incoming requests,
|
It provides validation on the content of incoming requests,
|
||||||
and filters the object representation into a serializable object for the response.
|
and filters the object representation into a serializable object for the
|
||||||
|
response.
|
||||||
"""
|
"""
|
||||||
resource = None
|
resource = None
|
||||||
|
|
||||||
|
@ -407,7 +412,8 @@ class ResourceMixin(object):
|
||||||
"""
|
"""
|
||||||
Returns the cleaned, validated request content.
|
Returns the cleaned, validated request content.
|
||||||
|
|
||||||
May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request).
|
May raise an :class:`response.ErrorResponse` with status code 400
|
||||||
|
(Bad Request).
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_content'):
|
if not hasattr(self, '_content'):
|
||||||
self._content = self.validate_request(self.DATA, self.FILES)
|
self._content = self.validate_request(self.DATA, self.FILES)
|
||||||
|
@ -418,7 +424,8 @@ class ResourceMixin(object):
|
||||||
"""
|
"""
|
||||||
Returns the cleaned, validated query parameters.
|
Returns the cleaned, validated query parameters.
|
||||||
|
|
||||||
May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request).
|
May raise an :class:`response.ErrorResponse` with status code 400
|
||||||
|
(Bad Request).
|
||||||
"""
|
"""
|
||||||
return self.validate_request(self.request.GET)
|
return self.validate_request(self.request.GET)
|
||||||
|
|
||||||
|
@ -436,8 +443,10 @@ class ResourceMixin(object):
|
||||||
|
|
||||||
def validate_request(self, data, files=None):
|
def validate_request(self, data, files=None):
|
||||||
"""
|
"""
|
||||||
Given the request *data* and optional *files*, return the cleaned, validated content.
|
Given the request *data* and optional *files*, return the cleaned,
|
||||||
May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
validated content.
|
||||||
|
May raise an :class:`response.ErrorResponse` with status code 400
|
||||||
|
(Bad Request) on failure.
|
||||||
"""
|
"""
|
||||||
return self._resource.validate_request(data, files)
|
return self._resource.validate_request(data, files)
|
||||||
|
|
||||||
|
@ -454,26 +463,28 @@ class ResourceMixin(object):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##########
|
##########
|
||||||
|
|
||||||
|
|
||||||
class InstanceMixin(object):
|
class InstanceMixin(object):
|
||||||
"""
|
"""
|
||||||
`Mixin` class that is used to identify a `View` class as being the canonical identifier
|
`Mixin` class that is used to identify a `View` class as being the
|
||||||
for the resources it is mapped to.
|
canonical identifier for the resources it is mapped to.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, **initkwargs):
|
def as_view(cls, **initkwargs):
|
||||||
"""
|
"""
|
||||||
Store the callable object on the resource class that has been associated with this view.
|
Store the callable object on the resource class that has been
|
||||||
|
associated with this view.
|
||||||
"""
|
"""
|
||||||
view = super(InstanceMixin, cls).as_view(**initkwargs)
|
view = super(InstanceMixin, cls).as_view(**initkwargs)
|
||||||
resource = getattr(cls(**initkwargs), 'resource', None)
|
resource = getattr(cls(**initkwargs), 'resource', None)
|
||||||
if resource:
|
if resource:
|
||||||
# We do a little dance when we store the view callable...
|
# We do a little dance when we store the view callable...
|
||||||
# we need to store it wrapped in a 1-tuple, so that inspect will treat it
|
# we need to store it wrapped in a 1-tuple, so that inspect will
|
||||||
# as a function when we later look it up (rather than turning it into a method).
|
# treat it as a function when we later look it up (rather than
|
||||||
|
# turning it into a method).
|
||||||
# This makes sure our URL reversing works ok.
|
# This makes sure our URL reversing works ok.
|
||||||
resource.view_callable = (view,)
|
resource.view_callable = (view,)
|
||||||
return view
|
return view
|
||||||
|
@ -481,59 +492,57 @@ class InstanceMixin(object):
|
||||||
|
|
||||||
########## Model Mixins ##########
|
########## Model Mixins ##########
|
||||||
|
|
||||||
class ReadModelMixin(object):
|
|
||||||
"""
|
class ModelMixin(object):
|
||||||
Behavior to read a `model` instance on GET requests
|
def get_model(self):
|
||||||
"""
|
"""
|
||||||
def get(self, request, *args, **kwargs):
|
Return the model class for this view.
|
||||||
model = self.resource.model
|
"""
|
||||||
|
return getattr(self, 'model', self.resource.model)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
Return the queryset that should be used when retrieving or listing
|
||||||
|
instances.
|
||||||
|
"""
|
||||||
|
return getattr(self, 'queryset',
|
||||||
|
getattr(self.resource, 'queryset',
|
||||||
|
self.get_model().objects.all()))
|
||||||
|
|
||||||
|
def get_ordering(self):
|
||||||
|
"""
|
||||||
|
Return the ordering that should be used when listing instances.
|
||||||
|
"""
|
||||||
|
return getattr(self, 'ordering',
|
||||||
|
getattr(self.resource, 'ordering',
|
||||||
|
None))
|
||||||
|
|
||||||
|
# Underlying instance API...
|
||||||
|
|
||||||
|
def get_instance(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Return a model instance or None.
|
||||||
|
"""
|
||||||
|
model = self.get_model()
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args:
|
return queryset.get(**kwargs)
|
||||||
# If we have any none kwargs then assume the last represents the primrary key
|
|
||||||
self.model_instance = model.objects.get(pk=args[-1], **kwargs)
|
|
||||||
else:
|
|
||||||
# Otherwise assume the kwargs uniquely identify the model
|
|
||||||
filtered_keywords = kwargs.copy()
|
|
||||||
if BaseRenderer._FORMAT_QUERY_PARAM in filtered_keywords:
|
|
||||||
del filtered_keywords[BaseRenderer._FORMAT_QUERY_PARAM]
|
|
||||||
self.model_instance = model.objects.get(**filtered_keywords)
|
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
|
return None
|
||||||
|
|
||||||
return self.model_instance
|
def create_instance(self, *args, **kwargs):
|
||||||
|
model = self.get_model()
|
||||||
|
|
||||||
|
|
||||||
class CreateModelMixin(object):
|
|
||||||
"""
|
|
||||||
Behavior to create a `model` instance on POST requests
|
|
||||||
"""
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
model = self.resource.model
|
|
||||||
|
|
||||||
# Copy the dict to keep self.CONTENT intact
|
|
||||||
content = dict(self.CONTENT)
|
|
||||||
m2m_data = {}
|
m2m_data = {}
|
||||||
|
for field in model._meta.many_to_many:
|
||||||
for field in model._meta.fields:
|
if field.name in kwargs:
|
||||||
if isinstance(field, ForeignKey) and kwargs.has_key(field.name):
|
m2m_data[field.name] = (
|
||||||
# translate 'related_field' kwargs into 'related_field_id'
|
field.m2m_reverse_field_name(), kwargs[field.name]
|
||||||
kwargs[field.name + '_id'] = kwargs[field.name]
|
)
|
||||||
del kwargs[field.name]
|
del kwargs[field.name]
|
||||||
|
|
||||||
for field in model._meta.many_to_many:
|
instance = model(**kwargs)
|
||||||
if content.has_key(field.name):
|
|
||||||
m2m_data[field.name] = (
|
|
||||||
field.m2m_reverse_field_name(), content[field.name]
|
|
||||||
)
|
|
||||||
del content[field.name]
|
|
||||||
|
|
||||||
all_kw_args = dict(content.items() + kwargs.items())
|
|
||||||
|
|
||||||
if args:
|
|
||||||
instance = model(pk=args[-1], **all_kw_args)
|
|
||||||
else:
|
|
||||||
instance = model(**all_kw_args)
|
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
for fieldname in m2m_data:
|
for fieldname in m2m_data:
|
||||||
|
@ -549,93 +558,83 @@ class CreateModelMixin(object):
|
||||||
data[m2m_data[fieldname][0]] = related_item
|
data[m2m_data[fieldname][0]] = related_item
|
||||||
manager.through(**data).save()
|
manager.through(**data).save()
|
||||||
|
|
||||||
headers = {}
|
return instance
|
||||||
if hasattr(instance, 'get_absolute_url'):
|
|
||||||
headers['Location'] = self.resource(self).url(instance)
|
|
||||||
return Response(status.HTTP_201_CREATED, instance, headers)
|
|
||||||
|
|
||||||
|
def update_instance(self, instance, *args, **kwargs):
|
||||||
|
for (key, val) in kwargs.items():
|
||||||
|
setattr(instance, key, val)
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
class UpdateModelMixin(object):
|
def delete_instance(self, instance, *args, **kwargs):
|
||||||
"""
|
|
||||||
Behavior to update a `model` instance on PUT requests
|
|
||||||
"""
|
|
||||||
def put(self, request, *args, **kwargs):
|
|
||||||
model = self.resource.model
|
|
||||||
|
|
||||||
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
|
|
||||||
try:
|
|
||||||
if args:
|
|
||||||
# If we have any none kwargs then assume the last represents the primary key
|
|
||||||
self.model_instance = model.objects.get(pk=args[-1], **kwargs)
|
|
||||||
else:
|
|
||||||
# Otherwise assume the kwargs uniquely identify the model
|
|
||||||
self.model_instance = model.objects.get(**kwargs)
|
|
||||||
|
|
||||||
for (key, val) in self.CONTENT.items():
|
|
||||||
setattr(self.model_instance, key, val)
|
|
||||||
except model.DoesNotExist:
|
|
||||||
self.model_instance = model(**self.CONTENT)
|
|
||||||
self.model_instance.save()
|
|
||||||
|
|
||||||
self.model_instance.save()
|
|
||||||
return self.model_instance
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteModelMixin(object):
|
|
||||||
"""
|
|
||||||
Behavior to delete a `model` instance on DELETE requests
|
|
||||||
"""
|
|
||||||
def delete(self, request, *args, **kwargs):
|
|
||||||
model = self.resource.model
|
|
||||||
|
|
||||||
try:
|
|
||||||
if args:
|
|
||||||
# If we have any none kwargs then assume the last represents the primrary key
|
|
||||||
instance = model.objects.get(pk=args[-1], **kwargs)
|
|
||||||
else:
|
|
||||||
# Otherwise assume the kwargs uniquely identify the model
|
|
||||||
instance = model.objects.get(**kwargs)
|
|
||||||
except model.DoesNotExist:
|
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
|
|
||||||
|
|
||||||
instance.delete()
|
instance.delete()
|
||||||
return
|
return instance
|
||||||
|
|
||||||
|
def list_instances(self, *args, **kwargs):
|
||||||
class ListModelMixin(object):
|
queryset = self.get_queryset()
|
||||||
"""
|
ordering = self.get_ordering()
|
||||||
Behavior to list a set of `model` instances on GET requests
|
|
||||||
"""
|
|
||||||
|
|
||||||
# NB. Not obvious to me if it would be better to set this on the resource?
|
|
||||||
#
|
|
||||||
# Presumably it's more useful to have on the view, because that way you can
|
|
||||||
# have multiple views across different querysets mapping to the same resource.
|
|
||||||
#
|
|
||||||
# Perhaps it ought to be:
|
|
||||||
#
|
|
||||||
# 1) View.queryset
|
|
||||||
# 2) if None fall back to Resource.queryset
|
|
||||||
# 3) if None fall back to Resource.model.objects.all()
|
|
||||||
#
|
|
||||||
# Any feedback welcomed.
|
|
||||||
queryset = None
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
model = self.resource.model
|
|
||||||
|
|
||||||
queryset = self.queryset if self.queryset is not None else model.objects.all()
|
|
||||||
|
|
||||||
if hasattr(self, 'resource'):
|
|
||||||
ordering = getattr(self.resource, 'ordering', None)
|
|
||||||
else:
|
|
||||||
ordering = None
|
|
||||||
|
|
||||||
if ordering:
|
if ordering:
|
||||||
args = as_tuple(ordering)
|
queryset = queryset.order_by(ordering)
|
||||||
queryset = queryset.order_by(*args)
|
|
||||||
return queryset.filter(**kwargs)
|
return queryset.filter(**kwargs)
|
||||||
|
|
||||||
|
# Request/Response layer...
|
||||||
|
|
||||||
|
def _get_url_kwargs(self, kwargs):
|
||||||
|
format_arg = BaseRenderer._FORMAT_QUERY_PARAM
|
||||||
|
if format_arg in kwargs:
|
||||||
|
kwargs = kwargs.copy()
|
||||||
|
del kwargs[format_arg]
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def _get_content_kwargs(self, kwargs):
|
||||||
|
return dict(self._get_url_kwargs(kwargs).items() +
|
||||||
|
self.CONTENT.items())
|
||||||
|
|
||||||
|
def read(self, request, *args, **kwargs):
|
||||||
|
kwargs = self._get_url_kwargs(kwargs)
|
||||||
|
instance = self.get_instance(**kwargs)
|
||||||
|
|
||||||
|
if instance is None:
|
||||||
|
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
kwargs = self._get_url_kwargs(kwargs)
|
||||||
|
instance = self.get_instance(**kwargs)
|
||||||
|
|
||||||
|
kwargs = self._get_content_kwargs(kwargs)
|
||||||
|
if instance:
|
||||||
|
instance = self.update_instance(instance, **kwargs)
|
||||||
|
else:
|
||||||
|
instance = self.create_instance(**kwargs)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
kwargs = self._get_content_kwargs(kwargs)
|
||||||
|
instance = self.create_instance(**kwargs)
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
try:
|
||||||
|
headers['Location'] = self.resource(self).url(instance)
|
||||||
|
except: # TODO: _SkipField should not really happen.
|
||||||
|
pass
|
||||||
|
|
||||||
|
return Response(status.HTTP_201_CREATED, instance, headers)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
kwargs = self._get_url_kwargs(kwargs)
|
||||||
|
instance = self.delete_instance(**kwargs)
|
||||||
|
if not instance:
|
||||||
|
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
return self.list_instances(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
########## Pagination Mixins ##########
|
########## Pagination Mixins ##########
|
||||||
|
|
||||||
|
@ -658,7 +657,7 @@ class PaginatorMixin(object):
|
||||||
return self.limit
|
return self.limit
|
||||||
|
|
||||||
def url_with_page_number(self, page_number):
|
def url_with_page_number(self, page_number):
|
||||||
""" Constructs a url used for getting the next/previous urls """
|
"""Constructs a url used for getting the next/previous urls."""
|
||||||
url = "%s?page=%d" % (self.request.path, page_number)
|
url = "%s?page=%d" % (self.request.path, page_number)
|
||||||
|
|
||||||
limit = self.get_limit()
|
limit = self.get_limit()
|
||||||
|
@ -668,21 +667,21 @@ class PaginatorMixin(object):
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def next(self, page):
|
def next(self, page):
|
||||||
""" Returns a url to the next page of results (if any) """
|
"""Returns a url to the next page of results. (If any exists.)"""
|
||||||
if not page.has_next():
|
if not page.has_next():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self.url_with_page_number(page.next_page_number())
|
return self.url_with_page_number(page.next_page_number())
|
||||||
|
|
||||||
def previous(self, page):
|
def previous(self, page):
|
||||||
""" Returns a url to the previous page of results (if any) """
|
"""Returns a url to the previous page of results. (If any exists.)"""
|
||||||
if not page.has_previous():
|
if not page.has_previous():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self.url_with_page_number(page.previous_page_number())
|
return self.url_with_page_number(page.previous_page_number())
|
||||||
|
|
||||||
def serialize_page_info(self, page):
|
def serialize_page_info(self, page):
|
||||||
""" This is some useful information that is added to the response """
|
"""This is some useful information that is added to the response."""
|
||||||
return {
|
return {
|
||||||
'next': self.next(page),
|
'next': self.next(page),
|
||||||
'page': page.number,
|
'page': page.number,
|
||||||
|
@ -696,14 +695,15 @@ class PaginatorMixin(object):
|
||||||
"""
|
"""
|
||||||
Given the response content, paginate and then serialize.
|
Given the response content, paginate and then serialize.
|
||||||
|
|
||||||
The response is modified to include to useful data relating to the number
|
The response is modified to include to useful data relating to the
|
||||||
of objects, number of pages, next/previous urls etc. etc.
|
number of objects, number of pages, next/previous urls etc. etc.
|
||||||
|
|
||||||
The serialised objects are put into `results` on this new, modified
|
The serialised objects are put into `results` on this new, modified
|
||||||
response
|
response
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# We don't want to paginate responses for anything other than GET requests
|
# We don't want to paginate responses for anything other than GET
|
||||||
|
# requests
|
||||||
if self.method.upper() != 'GET':
|
if self.method.upper() != 'GET':
|
||||||
return self._resource.filter_response(obj)
|
return self._resource.filter_response(obj)
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,19 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch
|
from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
from django.db.models.fields.related import RelatedField
|
|
||||||
from django.utils.encoding import smart_unicode
|
|
||||||
|
|
||||||
from djangorestframework.response import ErrorResponse
|
from djangorestframework.response import ErrorResponse
|
||||||
from djangorestframework.serializer import Serializer, _SkipField
|
from djangorestframework.serializer import Serializer, _SkipField
|
||||||
from djangorestframework.utils import as_tuple
|
|
||||||
|
|
||||||
import decimal
|
|
||||||
import inspect
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BaseResource(Serializer):
|
class BaseResource(Serializer):
|
||||||
"""
|
"""
|
||||||
Base class for all Resource classes, which simply defines the interface they provide.
|
Base class for all Resource classes, which simply defines the interface
|
||||||
|
they provide.
|
||||||
"""
|
"""
|
||||||
fields = None
|
fields = ()
|
||||||
include = None
|
include = ()
|
||||||
exclude = None
|
exclude = ()
|
||||||
|
|
||||||
def __init__(self, view=None, depth=None, stack=[], **kwargs):
|
def __init__(self, view=None, depth=None, stack=[], **kwargs):
|
||||||
super(BaseResource, self).__init__(depth, stack, **kwargs)
|
super(BaseResource, self).__init__(depth, stack, **kwargs)
|
||||||
|
@ -31,7 +22,8 @@ class BaseResource(Serializer):
|
||||||
def validate_request(self, data, files=None):
|
def validate_request(self, data, files=None):
|
||||||
"""
|
"""
|
||||||
Given the request content return the cleaned, validated content.
|
Given the request content return the cleaned, validated content.
|
||||||
Typically raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
Typically raises a :exc:`response.ErrorResponse` with status code 400
|
||||||
|
(Bad Request) on failure.
|
||||||
"""
|
"""
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -45,18 +37,20 @@ class BaseResource(Serializer):
|
||||||
class Resource(BaseResource):
|
class Resource(BaseResource):
|
||||||
"""
|
"""
|
||||||
A Resource determines how a python object maps to some serializable data.
|
A Resource determines how a python object maps to some serializable data.
|
||||||
Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets.
|
Objects that a resource can act on include plain Python object instances,
|
||||||
|
Django Models, and Django QuerySets.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The model attribute refers to the Django Model which this Resource maps to.
|
# The model attribute refers to the Django Model which this Resource maps
|
||||||
# (The Model's class, rather than an instance of the Model)
|
# to. (The Model's class, rather than an instance of the Model)
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
# By default the set of returned fields will be the set of:
|
# By default the set of returned fields will be the set of:
|
||||||
#
|
#
|
||||||
# 0. All the fields on the model, excluding 'id'.
|
# 0. All the fields on the model, excluding 'id'.
|
||||||
# 1. All the properties on the model.
|
# 1. All the properties on the model.
|
||||||
# 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
|
# 2. The absolute_url of the model, if a get_absolute_url method exists for
|
||||||
|
# the model.
|
||||||
#
|
#
|
||||||
# If you wish to override this behaviour,
|
# If you wish to override this behaviour,
|
||||||
# you should explicitly set the fields attribute on your class.
|
# you should explicitly set the fields attribute on your class.
|
||||||
|
@ -66,42 +60,51 @@ class Resource(BaseResource):
|
||||||
class FormResource(Resource):
|
class FormResource(Resource):
|
||||||
"""
|
"""
|
||||||
Resource class that uses forms for validation.
|
Resource class that uses forms for validation.
|
||||||
Also provides a :meth:`get_bound_form` method which may be used by some renderers.
|
Also provides a :meth:`get_bound_form` method which may be used by some
|
||||||
|
renderers.
|
||||||
|
|
||||||
On calling :meth:`validate_request` this validator may set a :attr:`bound_form_instance` attribute on the
|
On calling :meth:`validate_request` this validator may set a
|
||||||
view, which may be used by some renderers.
|
:attr:`bound_form_instance` attribute on the view, which may be used by
|
||||||
|
some renderers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
form = None
|
form = None
|
||||||
"""
|
"""
|
||||||
The :class:`Form` class that should be used for request validation.
|
The :class:`Form` class that should be used for request validation.
|
||||||
This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
|
This can be overridden by a :attr:`form` attribute on the
|
||||||
|
:class:`views.View`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def validate_request(self, data, files=None):
|
def validate_request(self, data, files=None):
|
||||||
"""
|
"""
|
||||||
Given some content as input return some cleaned, validated content.
|
Given some content as input return some cleaned, validated content.
|
||||||
Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
|
||||||
|
|
||||||
Validation is standard form validation, with an additional constraint that *no extra unknown fields* may be supplied.
|
Raises a :exc:`response.ErrorResponse` with status code 400
|
||||||
|
# (Bad Request) on failure.
|
||||||
|
|
||||||
On failure the :exc:`response.ErrorResponse` content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
|
Validation is standard form validation, with an additional constraint
|
||||||
If the :obj:`'errors'` key exists it is a list of strings of non-field errors.
|
that *no extra unknown fields* may be supplied.
|
||||||
If the :obj:`'field-errors'` key exists it is a dict of ``{'field name as string': ['errors as strings', ...]}``.
|
|
||||||
|
On failure the :exc:`response.ErrorResponse` content is a dict which
|
||||||
|
may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
|
||||||
|
If the :obj:`'errors'` key exists it is a list of strings of non-field
|
||||||
|
errors.
|
||||||
|
If the :obj:`'field-errors'` key exists it is a dict of
|
||||||
|
``{'field name as string': ['errors as strings', ...]}``.
|
||||||
"""
|
"""
|
||||||
return self._validate(data, files)
|
return self._validate(data, files)
|
||||||
|
|
||||||
|
|
||||||
def _validate(self, data, files, allowed_extra_fields=(), fake_data=None):
|
def _validate(self, data, files, allowed_extra_fields=(), fake_data=None):
|
||||||
"""
|
"""
|
||||||
Wrapped by validate to hide the extra flags that are used in the implementation.
|
Wrapped by validate to hide the extra flags that are used in the
|
||||||
|
implementation.
|
||||||
|
|
||||||
allowed_extra_fields is a list of fields which are not defined by the form, but which we still
|
allowed_extra_fields is a list of fields which are not defined by the
|
||||||
expect to see on the input.
|
form, but which we still expect to see on the input.
|
||||||
|
|
||||||
fake_data is a string that should be used as an extra key, as a kludge to force .errors
|
fake_data is a string that should be used as an extra key, as a kludge
|
||||||
to be populated when an empty dict is supplied in `data`
|
to force `.errors` to be populated when an empty dict is supplied in
|
||||||
|
`data`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# We'd like nice error messages even if no content is supplied.
|
# We'd like nice error messages even if no content is supplied.
|
||||||
|
@ -129,7 +132,7 @@ class FormResource(Resource):
|
||||||
|
|
||||||
# In addition to regular validation we also ensure no additional fields are being passed in...
|
# In addition to regular validation we also ensure no additional fields are being passed in...
|
||||||
unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
|
unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
|
||||||
unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept', '_method')) # TODO: Ugh.
|
unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept', '_method')) # TODO: Ugh.
|
||||||
|
|
||||||
# Check using both regular validation, and our stricter no additional fields rule
|
# Check using both regular validation, and our stricter no additional fields rule
|
||||||
if bound_form.is_valid() and not unknown_fields:
|
if bound_form.is_valid() and not unknown_fields:
|
||||||
|
@ -178,7 +181,6 @@ class FormResource(Resource):
|
||||||
# Return HTTP 400 response (BAD REQUEST)
|
# Return HTTP 400 response (BAD REQUEST)
|
||||||
raise ErrorResponse(400, detail)
|
raise ErrorResponse(400, detail)
|
||||||
|
|
||||||
|
|
||||||
def get_form_class(self, method=None):
|
def get_form_class(self, method=None):
|
||||||
"""
|
"""
|
||||||
Returns the form class used to validate this resource.
|
Returns the form class used to validate this resource.
|
||||||
|
@ -217,7 +219,6 @@ class FormResource(Resource):
|
||||||
return form()
|
return form()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#class _RegisterModelResource(type):
|
#class _RegisterModelResource(type):
|
||||||
# """
|
# """
|
||||||
# Auto register new ModelResource classes into ``_model_to_resource``
|
# Auto register new ModelResource classes into ``_model_to_resource``
|
||||||
|
@ -230,11 +231,12 @@ class FormResource(Resource):
|
||||||
# return resource_cls
|
# return resource_cls
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ModelResource(FormResource):
|
class ModelResource(FormResource):
|
||||||
"""
|
"""
|
||||||
Resource class that uses forms for validation and otherwise falls back to a model form if no form is set.
|
Resource class that uses forms for validation and otherwise falls back to a
|
||||||
Also provides a :meth:`get_bound_form` method which may be used by some renderers.
|
model form if no form is set.
|
||||||
|
Also provides a :meth:`get_bound_form` method which may be used by some
|
||||||
|
renderers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Auto-register new ModelResource classes into _model_to_resource
|
# Auto-register new ModelResource classes into _model_to_resource
|
||||||
|
@ -245,14 +247,16 @@ class ModelResource(FormResource):
|
||||||
The form class that should be used for request validation.
|
The form class that should be used for request validation.
|
||||||
If set to :const:`None` then the default model form validation will be used.
|
If set to :const:`None` then the default model form validation will be used.
|
||||||
|
|
||||||
This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
|
This can be overridden by a :attr:`form` attribute on the
|
||||||
|
:class:`views.View`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = None
|
model = None
|
||||||
"""
|
"""
|
||||||
The model class which this resource maps to.
|
The model class which this resource maps to.
|
||||||
|
|
||||||
This can be overridden by a :attr:`model` attribute on the :class:`views.View`.
|
This can be overridden by a :attr:`model` attribute on the
|
||||||
|
:class:`views.View`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fields = None
|
fields = None
|
||||||
|
@ -261,22 +265,27 @@ class ModelResource(FormResource):
|
||||||
|
|
||||||
May be any of:
|
May be any of:
|
||||||
|
|
||||||
The name of a model field. To view nested resources, give the field as a tuple of ("fieldName", resource) where `resource` may be any of ModelResource reference, the name of a ModelResourc reference as a string or a tuple of strings representing fields on the nested model.
|
The name of a model field. To view nested resources, give the field as a
|
||||||
|
tuple of ("fieldName", resource) where `resource` may be any of
|
||||||
|
ModelResource reference, the name of a ModelResourc reference as a string
|
||||||
|
or a tuple of strings representing fields on the nested model.
|
||||||
The name of an attribute on the model.
|
The name of an attribute on the model.
|
||||||
The name of an attribute on the resource.
|
The name of an attribute on the resource.
|
||||||
The name of a method on the model, with a signature like ``func(self)``.
|
The name of a method on the model, with a signature like ``func(self)``.
|
||||||
The name of a method on the resource, with a signature like ``func(self, instance)``.
|
The name of a method on the resource, with a signature like
|
||||||
|
``func(self, instance)``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
exclude = ('id', 'pk')
|
exclude = ('id', 'pk')
|
||||||
"""
|
"""
|
||||||
The list of fields to exclude. This is only used if :attr:`fields` is not set.
|
The list of fields to exclude. This is only used if :attr:`fields` is not
|
||||||
|
set.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
include = ('url',)
|
include = ('url',)
|
||||||
"""
|
"""
|
||||||
The list of extra fields to include. This is only used if :attr:`fields` is not set.
|
The list of extra fields to include. This is only used if :attr:`fields`
|
||||||
|
is not set.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, view=None, depth=None, stack=[], **kwargs):
|
def __init__(self, view=None, depth=None, stack=[], **kwargs):
|
||||||
|
@ -289,30 +298,36 @@ class ModelResource(FormResource):
|
||||||
|
|
||||||
self.model = getattr(view, 'model', None) or self.model
|
self.model = getattr(view, 'model', None) or self.model
|
||||||
|
|
||||||
|
|
||||||
def validate_request(self, data, files=None):
|
def validate_request(self, data, files=None):
|
||||||
"""
|
"""
|
||||||
Given some content as input return some cleaned, validated content.
|
Given some content as input return some cleaned, validated content.
|
||||||
Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
|
||||||
|
Raises a :exc:`response.ErrorResponse` with status code 400
|
||||||
|
(Bad Request) on failure.
|
||||||
|
|
||||||
Validation is standard form or model form validation,
|
Validation is standard form or model form validation,
|
||||||
with an additional constraint that no extra unknown fields may be supplied,
|
with an additional constraint that no extra unknown fields may be
|
||||||
and that all fields specified by the fields class attribute must be supplied,
|
supplied, and that all fields specified by the fields class attribute
|
||||||
even if they are not validated by the form/model form.
|
must be supplied, even if they are not validated by the Form/ModelForm.
|
||||||
|
|
||||||
On failure the ErrorResponse content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
|
On failure the ErrorResponse content is a dict which may contain
|
||||||
If the :obj:`'errors'` key exists it is a list of strings of non-field errors.
|
:obj:`'errors'` and :obj:`'field-errors'` keys.
|
||||||
If the ''field-errors'` key exists it is a dict of {field name as string: list of errors as strings}.
|
If the :obj:`'errors'` key exists it is a list of strings of non-field
|
||||||
|
errors.
|
||||||
|
If the ''field-errors'` key exists it is a dict of
|
||||||
|
`{field name as string: list of errors as strings}`.
|
||||||
"""
|
"""
|
||||||
return self._validate(data, files, allowed_extra_fields=self._property_fields_set)
|
return self._validate(data, files,
|
||||||
|
allowed_extra_fields=self._property_fields_set)
|
||||||
|
|
||||||
def get_bound_form(self, data=None, files=None, method=None):
|
def get_bound_form(self, data=None, files=None, method=None):
|
||||||
"""
|
"""
|
||||||
Given some content return a ``Form`` instance bound to that content.
|
Given some content return a ``Form`` instance bound to that content.
|
||||||
|
|
||||||
If the :attr:`form` class attribute has been explicitly set then that class will be used
|
If the :attr:`form` class attribute has been explicitly set then that
|
||||||
to create the Form, otherwise the model will be used to create a ModelForm.
|
class will be used
|
||||||
|
to create the Form, otherwise the model will be used to create a
|
||||||
|
ModelForm.
|
||||||
"""
|
"""
|
||||||
form = self.get_form_class(method)
|
form = self.get_form_class(method)
|
||||||
|
|
||||||
|
@ -339,14 +354,16 @@ class ModelResource(FormResource):
|
||||||
|
|
||||||
return form()
|
return form()
|
||||||
|
|
||||||
|
|
||||||
def url(self, instance):
|
def url(self, instance):
|
||||||
"""
|
"""
|
||||||
Attempts to reverse resolve the url of the given model *instance* for this resource.
|
Attempts to reverse resolve the url of the given model *instance* for
|
||||||
|
this resource.
|
||||||
|
|
||||||
Requires a ``View`` with :class:`mixins.InstanceMixin` to have been created for this resource.
|
Requires a ``View`` with :class:`mixins.InstanceMixin` to have been
|
||||||
|
created for this resource.
|
||||||
|
|
||||||
This method can be overridden if you need to set the resource url reversing explicitly.
|
This method can be overridden if you need to set the resource url
|
||||||
|
reversing explicitly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not hasattr(self, 'view_callable'):
|
if not hasattr(self, 'view_callable'):
|
||||||
|
@ -363,7 +380,9 @@ class ModelResource(FormResource):
|
||||||
# Note: defaults = tuple_item[2] for django >= 1.3
|
# Note: defaults = tuple_item[2] for django >= 1.3
|
||||||
for result, params in possibility:
|
for result, params in possibility:
|
||||||
|
|
||||||
#instance_attrs = dict([ (param, getattr(instance, param)) for param in params if hasattr(instance, param) ])
|
# instance_attrs = dict([ (param, getattr(instance, param))
|
||||||
|
# for param in params
|
||||||
|
# if hasattr(instance, param) ])
|
||||||
|
|
||||||
instance_attrs = {}
|
instance_attrs = {}
|
||||||
for param in params:
|
for param in params:
|
||||||
|
@ -381,7 +400,6 @@ class ModelResource(FormResource):
|
||||||
pass
|
pass
|
||||||
raise _SkipField
|
raise _SkipField
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _model_fields_set(self):
|
def _model_fields_set(self):
|
||||||
"""
|
"""
|
||||||
|
@ -389,11 +407,12 @@ class ModelResource(FormResource):
|
||||||
"""
|
"""
|
||||||
model_fields = set(field.name for field in self.model._meta.fields)
|
model_fields = set(field.name for field in self.model._meta.fields)
|
||||||
|
|
||||||
if fields:
|
if self.fields:
|
||||||
return model_fields & set(as_tuple(self.fields))
|
return model_fields & set(self.fields)
|
||||||
|
|
||||||
return model_fields - set(as_tuple(self.exclude))
|
return model_fields - set(as_tuple(self.exclude))
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _property_fields_set(self):
|
def _property_fields_set(self):
|
||||||
"""
|
"""
|
||||||
|
@ -404,6 +423,6 @@ class ModelResource(FormResource):
|
||||||
and not attr.startswith('_'))
|
and not attr.startswith('_'))
|
||||||
|
|
||||||
if self.fields:
|
if self.fields:
|
||||||
return property_fields & set(as_tuple(self.fields))
|
return property_fields & set(self.fields)
|
||||||
|
|
||||||
return property_fields.union(set(as_tuple(self.include))) - set(as_tuple(self.exclude))
|
return property_fields.union(set(self.include)) - set(self.exclude)
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.utils import simplejson as json
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from djangorestframework.mixins import CreateModelMixin, PaginatorMixin
|
from djangorestframework.mixins import PaginatorMixin, ModelMixin
|
||||||
from djangorestframework.resources import ModelResource
|
from djangorestframework.resources import ModelResource
|
||||||
from djangorestframework.response import Response
|
from djangorestframework.response import Response
|
||||||
from djangorestframework.tests.models import CustomUser
|
from djangorestframework.tests.models import CustomUser
|
||||||
|
@ -27,11 +27,11 @@ class TestModelCreation(TestModelsTestCase):
|
||||||
|
|
||||||
form_data = {'name': 'foo'}
|
form_data = {'name': 'foo'}
|
||||||
request = self.req.post('/groups', data=form_data)
|
request = self.req.post('/groups', data=form_data)
|
||||||
mixin = CreateModelMixin()
|
mixin = ModelMixin()
|
||||||
mixin.resource = GroupResource
|
mixin.resource = GroupResource
|
||||||
mixin.CONTENT = form_data
|
mixin.CONTENT = form_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.create(request)
|
||||||
self.assertEquals(1, Group.objects.count())
|
self.assertEquals(1, Group.objects.count())
|
||||||
self.assertEquals('foo', response.cleaned_content.name)
|
self.assertEquals('foo', response.cleaned_content.name)
|
||||||
|
|
||||||
|
@ -53,11 +53,11 @@ class TestModelCreation(TestModelsTestCase):
|
||||||
request = self.req.post('/groups', data=form_data)
|
request = self.req.post('/groups', data=form_data)
|
||||||
cleaned_data = dict(form_data)
|
cleaned_data = dict(form_data)
|
||||||
cleaned_data['groups'] = [group]
|
cleaned_data['groups'] = [group]
|
||||||
mixin = CreateModelMixin()
|
mixin = ModelMixin()
|
||||||
mixin.resource = UserResource
|
mixin.resource = UserResource
|
||||||
mixin.CONTENT = cleaned_data
|
mixin.CONTENT = cleaned_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.create(request)
|
||||||
self.assertEquals(1, User.objects.count())
|
self.assertEquals(1, User.objects.count())
|
||||||
self.assertEquals(1, response.cleaned_content.groups.count())
|
self.assertEquals(1, response.cleaned_content.groups.count())
|
||||||
self.assertEquals('foo', response.cleaned_content.groups.all()[0].name)
|
self.assertEquals('foo', response.cleaned_content.groups.all()[0].name)
|
||||||
|
@ -76,11 +76,11 @@ class TestModelCreation(TestModelsTestCase):
|
||||||
request = self.req.post('/groups', data=form_data)
|
request = self.req.post('/groups', data=form_data)
|
||||||
cleaned_data = dict(form_data)
|
cleaned_data = dict(form_data)
|
||||||
cleaned_data['groups'] = []
|
cleaned_data['groups'] = []
|
||||||
mixin = CreateModelMixin()
|
mixin = ModelMixin()
|
||||||
mixin.resource = UserResource
|
mixin.resource = UserResource
|
||||||
mixin.CONTENT = cleaned_data
|
mixin.CONTENT = cleaned_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.create(request)
|
||||||
self.assertEquals(1, CustomUser.objects.count())
|
self.assertEquals(1, CustomUser.objects.count())
|
||||||
self.assertEquals(0, response.cleaned_content.groups.count())
|
self.assertEquals(0, response.cleaned_content.groups.count())
|
||||||
|
|
||||||
|
@ -91,11 +91,11 @@ class TestModelCreation(TestModelsTestCase):
|
||||||
request = self.req.post('/groups', data=form_data)
|
request = self.req.post('/groups', data=form_data)
|
||||||
cleaned_data = dict(form_data)
|
cleaned_data = dict(form_data)
|
||||||
cleaned_data['groups'] = [group]
|
cleaned_data['groups'] = [group]
|
||||||
mixin = CreateModelMixin()
|
mixin = ModelMixin()
|
||||||
mixin.resource = UserResource
|
mixin.resource = UserResource
|
||||||
mixin.CONTENT = cleaned_data
|
mixin.CONTENT = cleaned_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.create(request)
|
||||||
self.assertEquals(2, CustomUser.objects.count())
|
self.assertEquals(2, CustomUser.objects.count())
|
||||||
self.assertEquals(1, response.cleaned_content.groups.count())
|
self.assertEquals(1, response.cleaned_content.groups.count())
|
||||||
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
||||||
|
@ -107,11 +107,11 @@ class TestModelCreation(TestModelsTestCase):
|
||||||
request = self.req.post('/groups', data=form_data)
|
request = self.req.post('/groups', data=form_data)
|
||||||
cleaned_data = dict(form_data)
|
cleaned_data = dict(form_data)
|
||||||
cleaned_data['groups'] = [group, group2]
|
cleaned_data['groups'] = [group, group2]
|
||||||
mixin = CreateModelMixin()
|
mixin = ModelMixin()
|
||||||
mixin.resource = UserResource
|
mixin.resource = UserResource
|
||||||
mixin.CONTENT = cleaned_data
|
mixin.CONTENT = cleaned_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.create(request)
|
||||||
self.assertEquals(3, CustomUser.objects.count())
|
self.assertEquals(3, CustomUser.objects.count())
|
||||||
self.assertEquals(2, response.cleaned_content.groups.count())
|
self.assertEquals(2, response.cleaned_content.groups.count())
|
||||||
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
||||||
|
|
|
@ -156,7 +156,8 @@ class RendererIntegrationTests(TestCase):
|
||||||
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||||
_indented_repr = '{\n "foo": [\n "bar", \n "baz"\n ]\n}'
|
|
||||||
|
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
|
||||||
|
|
||||||
|
|
||||||
class JSONRendererTests(TestCase):
|
class JSONRendererTests(TestCase):
|
||||||
|
@ -179,7 +180,7 @@ class JSONRendererTests(TestCase):
|
||||||
"""
|
"""
|
||||||
obj = {'foo': ['bar', 'baz']}
|
obj = {'foo': ['bar', 'baz']}
|
||||||
renderer = JSONRenderer(None)
|
renderer = JSONRenderer(None)
|
||||||
content = renderer.render(obj, 'application/json; indent=2')
|
content = renderer.render(obj, 'application/json; indent=4')
|
||||||
self.assertEquals(content, _indented_repr)
|
self.assertEquals(content, _indented_repr)
|
||||||
|
|
||||||
def test_render_and_parse(self):
|
def test_render_and_parse(self):
|
||||||
|
@ -239,6 +240,7 @@ class JSONPRendererTests(TestCase):
|
||||||
if YAMLRenderer:
|
if YAMLRenderer:
|
||||||
_yaml_repr = 'foo: [bar, baz]\n'
|
_yaml_repr = 'foo: [bar, baz]\n'
|
||||||
|
|
||||||
|
|
||||||
class YAMLRendererTests(TestCase):
|
class YAMLRendererTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests specific to the JSON Renderer
|
Tests specific to the JSON Renderer
|
||||||
|
@ -253,6 +255,7 @@ if YAMLRenderer:
|
||||||
content = renderer.render(obj, 'application/yaml')
|
content = renderer.render(obj, 'application/yaml')
|
||||||
self.assertEquals(content, _yaml_repr)
|
self.assertEquals(content, _yaml_repr)
|
||||||
|
|
||||||
|
|
||||||
def test_render_and_parse(self):
|
def test_render_and_parse(self):
|
||||||
"""
|
"""
|
||||||
Test rendering and then parsing returns the original object.
|
Test rendering and then parsing returns the original object.
|
||||||
|
|
|
@ -25,7 +25,6 @@ __all__ = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
"""
|
"""
|
||||||
Handles incoming requests and maps them to REST operations.
|
Handles incoming requests and maps them to REST operations.
|
||||||
|
@ -51,14 +50,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
"""
|
"""
|
||||||
List of all authenticating methods to attempt.
|
List of all authenticating methods to attempt.
|
||||||
"""
|
"""
|
||||||
authentication = ( authentication.UserLoggedInAuthentication,
|
authentication = (authentication.UserLoggedInAuthentication,
|
||||||
authentication.BasicAuthentication )
|
authentication.BasicAuthentication)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
List of all permissions that must be checked.
|
List of all permissions that must be checked.
|
||||||
"""
|
"""
|
||||||
permissions = ( permissions.FullAnonAccess, )
|
permissions = (permissions.FullAnonAccess,)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, **initkwargs):
|
def as_view(cls, **initkwargs):
|
||||||
|
@ -71,7 +69,6 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
view.cls_instance = cls(**initkwargs)
|
view.cls_instance = cls(**initkwargs)
|
||||||
return view
|
return view
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def allowed_methods(self):
|
def allowed_methods(self):
|
||||||
"""
|
"""
|
||||||
|
@ -79,7 +76,6 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
|
@ -87,23 +83,20 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
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})
|
||||||
|
|
||||||
|
|
||||||
def initial(self, request, *args, **kargs):
|
def initial(self, request, *args, **kargs):
|
||||||
"""
|
"""
|
||||||
Hook for any code that needs to run prior to anything else.
|
Hook for any code that needs to run prior to anything else.
|
||||||
Required if you want to do things like set `request.upload_handlers` before
|
Required if you want to do things like set `request.upload_handlers`
|
||||||
the authentication and dispatch handling is run.
|
before the authentication and dispatch handling is run.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def add_header(self, field, value):
|
def add_header(self, field, value):
|
||||||
"""
|
"""
|
||||||
Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class.
|
Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class.
|
||||||
"""
|
"""
|
||||||
self.headers[field] = value
|
self.headers[field] = value
|
||||||
|
|
||||||
|
|
||||||
# Note: session based authentication is explicitly CSRF validated,
|
# Note: session based authentication is explicitly CSRF validated,
|
||||||
# all other authentication is CSRF exempt.
|
# all other authentication is CSRF exempt.
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
@ -143,11 +136,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
response = Response(status.HTTP_204_NO_CONTENT)
|
response = Response(status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
if request.method == 'OPTIONS':
|
if request.method == 'OPTIONS':
|
||||||
# do not filter the response for HTTP OPTIONS, else the response fields are lost,
|
# do not filter the response for HTTP OPTIONS,
|
||||||
|
# else the response fields are lost,
|
||||||
# as they do not correspond with model fields
|
# as they do not correspond with model fields
|
||||||
response.cleaned_content = response.raw_content
|
response.cleaned_content = response.raw_content
|
||||||
else:
|
else:
|
||||||
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
# Pre-serialize filtering (eg filter complex objects into
|
||||||
|
# natively serializable types)
|
||||||
response.cleaned_content = self.filter_response(response.raw_content)
|
response.cleaned_content = self.filter_response(response.raw_content)
|
||||||
|
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
|
@ -155,8 +150,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
|
|
||||||
# Always add these headers.
|
# Always add these headers.
|
||||||
#
|
#
|
||||||
# TODO - this isn't actually the correct way to set the vary header,
|
# TODO - this isn't really the correct way to set the Vary header,
|
||||||
# also it's currently sub-optimal for HTTP caching - need to sort that out.
|
# also it's currently sub-optimal for HTTP caching.
|
||||||
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
||||||
response.headers['Vary'] = 'Authenticate, Accept'
|
response.headers['Vary'] = 'Authenticate, Accept'
|
||||||
|
|
||||||
|
@ -168,7 +163,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
return self.render(response)
|
return self.render(response)
|
||||||
|
|
||||||
def options(self, request, *args, **kwargs):
|
def options(self, request, *args, **kwargs):
|
||||||
response_obj = {
|
ret = {
|
||||||
'name': get_name(self),
|
'name': get_name(self),
|
||||||
'description': get_description(self),
|
'description': get_description(self),
|
||||||
'renders': self._rendered_media_types,
|
'renders': self._rendered_media_types,
|
||||||
|
@ -179,30 +174,46 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
field_name_types = {}
|
field_name_types = {}
|
||||||
for name, field in form.fields.iteritems():
|
for name, field in form.fields.iteritems():
|
||||||
field_name_types[name] = field.__class__.__name__
|
field_name_types[name] = field.__class__.__name__
|
||||||
response_obj['fields'] = field_name_types
|
ret['fields'] = field_name_types
|
||||||
return response_obj
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class ModelView(View):
|
class ModelView(ModelMixin, View):
|
||||||
"""
|
"""
|
||||||
A RESTful view that maps to a model in the database.
|
A RESTful view that maps to a model in the database.
|
||||||
"""
|
"""
|
||||||
resource = resources.ModelResource
|
resource = resources.ModelResource
|
||||||
|
|
||||||
class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView):
|
|
||||||
|
class InstanceModelView(InstanceMixin, ModelView):
|
||||||
"""
|
"""
|
||||||
A view which provides default operations for read/update/delete against a model instance.
|
A view which provides default operations for read/update/delete against a
|
||||||
|
model instance. This view is also treated as the Canonical identifier
|
||||||
|
of the instances.
|
||||||
"""
|
"""
|
||||||
_suffix = 'Instance'
|
_suffix = 'Instance'
|
||||||
|
|
||||||
class ListModelView(ListModelMixin, ModelView):
|
get = ModelMixin.read
|
||||||
|
put = ModelMixin.update
|
||||||
|
delete = ModelMixin.destroy
|
||||||
|
|
||||||
|
|
||||||
|
class ListModelView(ModelView):
|
||||||
"""
|
"""
|
||||||
A view which provides default operations for list, against a model in the database.
|
A view which provides default operations for list, against a model in the
|
||||||
|
database.
|
||||||
"""
|
"""
|
||||||
_suffix = 'List'
|
_suffix = 'List'
|
||||||
|
|
||||||
class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView):
|
get = ModelMixin.list
|
||||||
|
|
||||||
|
|
||||||
|
class ListOrCreateModelView(ModelView):
|
||||||
"""
|
"""
|
||||||
A view which provides default operations for list and create, against a model in the database.
|
A view which provides default operations for list and create, against a
|
||||||
|
model in the database.
|
||||||
"""
|
"""
|
||||||
_suffix = 'List'
|
_suffix = 'List'
|
||||||
|
|
||||||
|
get = ModelMixin.list
|
||||||
|
post = ModelMixin.create
|
||||||
|
|
Loading…
Reference in New Issue
Block a user