mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-09-16 09:12:29 +03:00
Merge 79f3de9532
into 11147ce13e
This commit is contained in:
commit
0520bc0985
|
@ -1,10 +1,7 @@
|
||||||
"""
|
"""
|
||||||
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.RequestMixin` class into a :class:`View` class.
|
||||||
|
|
||||||
The set of authentication methods which are used is then specified by setting 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
|
||||||
|
@ -23,12 +20,6 @@ class BaseAuthentication(object):
|
||||||
All authentication classes should extend BaseAuthentication.
|
All authentication classes should extend BaseAuthentication.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, view):
|
|
||||||
"""
|
|
||||||
:class:`Authentication` classes are always passed the current view on creation.
|
|
||||||
"""
|
|
||||||
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`. [*]_
|
||||||
|
@ -87,14 +78,14 @@ class UserLoggedInAuthentication(BaseAuthentication):
|
||||||
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 user.
|
||||||
Otherwise returns :const:`None`.
|
Otherwise returns :const:`None`.
|
||||||
"""
|
"""
|
||||||
self.view.DATA # Make sure our generic parsing runs first
|
user = getattr(request._request, 'user', None)
|
||||||
|
|
||||||
if getattr(request, 'user', None) and request.user.is_active:
|
if user and user.is_active:
|
||||||
# Enforce CSRF validation for session based authentication.
|
# Enforce CSRF validation for session based authentication.
|
||||||
resp = CsrfViewMiddleware().process_view(request, None, (), {})
|
resp = CsrfViewMiddleware().process_view(request, None, (), {})
|
||||||
|
|
||||||
if resp is None: # csrf passed
|
if resp is None: # csrf passed
|
||||||
return request.user
|
return user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,27 +3,22 @@ The :mod:`mixins` module provides a set of reusable `mixin`
|
||||||
classes that can be added to a `View`.
|
classes that can be added to a `View`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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.db.models.fields.related import ForeignKey
|
||||||
from django.http import HttpResponse
|
|
||||||
from urlobject import URLObject
|
from urlobject import URLObject
|
||||||
|
|
||||||
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, ImmediateResponse
|
||||||
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
from djangorestframework.request import Request
|
||||||
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
|
|
||||||
|
|
||||||
from StringIO import StringIO
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
# Base behavior mixins
|
# Base behavior mixins
|
||||||
'RequestMixin',
|
'RequestMixin',
|
||||||
'ResponseMixin',
|
'ResponseMixin',
|
||||||
'AuthMixin',
|
'PermissionsMixin',
|
||||||
'ResourceMixin',
|
'ResourceMixin',
|
||||||
# Model behavior mixins
|
# Model behavior mixins
|
||||||
'ReadModelMixin',
|
'ReadModelMixin',
|
||||||
|
@ -39,150 +34,21 @@ __all__ = (
|
||||||
|
|
||||||
class RequestMixin(object):
|
class RequestMixin(object):
|
||||||
"""
|
"""
|
||||||
`Mixin` class to provide request parsing behavior.
|
`Mixin` class enabling the use of :class:`request.Request` in your views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_USE_FORM_OVERLOADING = True
|
request_class = Request
|
||||||
_METHOD_PARAM = '_method'
|
|
||||||
_CONTENTTYPE_PARAM = '_content_type'
|
|
||||||
_CONTENT_PARAM = '_content'
|
|
||||||
|
|
||||||
parsers = ()
|
|
||||||
"""
|
"""
|
||||||
The set of request parsers that the view can handle.
|
The class to use as a wrapper for the original request object.
|
||||||
|
|
||||||
Should be a tuple/list of classes as described in the :mod:`parsers` module.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
def create_request(self, request):
|
||||||
def method(self):
|
|
||||||
"""
|
"""
|
||||||
Returns the HTTP method.
|
Creates and returns an instance of :class:`request.Request`.
|
||||||
|
This new instance wraps the `request` passed as a parameter, and use
|
||||||
This should be used instead of just reading :const:`request.method`, as it allows the `method`
|
the parsers set on the view.
|
||||||
to be overridden by using a hidden `form` field on a form POST request.
|
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_method'):
|
return self.request_class(request, parsers=self.parsers, authentication=self.authentication)
|
||||||
self._load_method_and_content_type()
|
|
||||||
return self._method
|
|
||||||
|
|
||||||
@property
|
|
||||||
def content_type(self):
|
|
||||||
"""
|
|
||||||
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'):
|
|
||||||
self._load_method_and_content_type()
|
|
||||||
return self._content_type
|
|
||||||
|
|
||||||
@property
|
|
||||||
def DATA(self):
|
|
||||||
"""
|
|
||||||
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'):
|
|
||||||
self._load_data_and_files()
|
|
||||||
return self._data
|
|
||||||
|
|
||||||
@property
|
|
||||||
def FILES(self):
|
|
||||||
"""
|
|
||||||
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'):
|
|
||||||
self._load_data_and_files()
|
|
||||||
return self._files
|
|
||||||
|
|
||||||
def _load_data_and_files(self):
|
|
||||||
"""
|
|
||||||
Parse the request content into self.DATA and self.FILES.
|
|
||||||
"""
|
|
||||||
if not hasattr(self, '_content_type'):
|
|
||||||
self._load_method_and_content_type()
|
|
||||||
|
|
||||||
if not hasattr(self, '_data'):
|
|
||||||
(self._data, self._files) = self._parse(self._get_stream(), self._content_type)
|
|
||||||
|
|
||||||
def _load_method_and_content_type(self):
|
|
||||||
"""
|
|
||||||
Set the method and content_type, and then check if they've been overridden.
|
|
||||||
"""
|
|
||||||
self._method = self.request.method
|
|
||||||
self._content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
|
|
||||||
self._perform_form_overloading()
|
|
||||||
|
|
||||||
def _get_stream(self):
|
|
||||||
"""
|
|
||||||
Returns an object that may be used to stream the request content.
|
|
||||||
"""
|
|
||||||
request = self.request
|
|
||||||
|
|
||||||
try:
|
|
||||||
content_length = int(request.META.get('CONTENT_LENGTH', request.META.get('HTTP_CONTENT_LENGTH')))
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
content_length = 0
|
|
||||||
|
|
||||||
# TODO: Add 1.3's LimitedStream to compat and use that.
|
|
||||||
# NOTE: Currently only supports parsing request body as a stream with 1.3
|
|
||||||
if content_length == 0:
|
|
||||||
return None
|
|
||||||
elif hasattr(request, 'read'):
|
|
||||||
return request
|
|
||||||
return StringIO(request.raw_post_data)
|
|
||||||
|
|
||||||
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
|
|
||||||
overridden by setting them in hidden form fields or not.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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):
|
|
||||||
return
|
|
||||||
|
|
||||||
# At this point we're committed to parsing the request as form data.
|
|
||||||
self._data = data = self.request.POST.copy()
|
|
||||||
self._files = self.request.FILES
|
|
||||||
|
|
||||||
# Method overloading - change the method and remove the param from the content.
|
|
||||||
if self._METHOD_PARAM in data:
|
|
||||||
# NOTE: unlike `get`, `pop` on a `QueryDict` seems to return a list of values.
|
|
||||||
self._method = self._data.pop(self._METHOD_PARAM)[0].upper()
|
|
||||||
|
|
||||||
# Content overloading - modify the content type, and re-parse.
|
|
||||||
if self._CONTENT_PARAM in data and self._CONTENTTYPE_PARAM in data:
|
|
||||||
self._content_type = self._data.pop(self._CONTENTTYPE_PARAM)[0]
|
|
||||||
stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0])
|
|
||||||
(self._data, self._files) = self._parse(stream, self._content_type)
|
|
||||||
|
|
||||||
def _parse(self, stream, content_type):
|
|
||||||
"""
|
|
||||||
Parse the request content.
|
|
||||||
|
|
||||||
May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request).
|
|
||||||
"""
|
|
||||||
if stream is None or content_type is None:
|
|
||||||
return (None, None)
|
|
||||||
|
|
||||||
parsers = as_tuple(self.parsers)
|
|
||||||
|
|
||||||
for parser_cls in parsers:
|
|
||||||
parser = parser_cls(self)
|
|
||||||
if parser.can_handle_request(content_type):
|
|
||||||
return parser.parse(stream)
|
|
||||||
|
|
||||||
raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
|
||||||
{'error': 'Unsupported media type in request \'%s\'.' %
|
|
||||||
content_type})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _parsed_media_types(self):
|
def _parsed_media_types(self):
|
||||||
|
@ -203,177 +69,63 @@ class RequestMixin(object):
|
||||||
|
|
||||||
class ResponseMixin(object):
|
class ResponseMixin(object):
|
||||||
"""
|
"""
|
||||||
Adds behavior for pluggable `Renderers` to a :class:`views.View` class.
|
`Mixin` class enabling the use of :class:`response.Response` in your views.
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
|
|
||||||
_IGNORE_IE_ACCEPT_HEADER = True
|
|
||||||
|
|
||||||
renderers = ()
|
renderers = ()
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_renderers(self):
|
|
||||||
"""
|
|
||||||
Return an iterable of available renderers. Override if you want to change
|
|
||||||
this list at runtime, say depending on what settings you have enabled.
|
|
||||||
"""
|
|
||||||
return self.renderers
|
|
||||||
|
|
||||||
# TODO: wrap this behavior around dispatch(), ensuring it works
|
|
||||||
# out of the box with existing Django classes that use render_to_response.
|
|
||||||
def render(self, response):
|
|
||||||
"""
|
|
||||||
Takes a :obj:`Response` object and returns an :obj:`HttpResponse`.
|
|
||||||
"""
|
|
||||||
self.response = response
|
|
||||||
|
|
||||||
try:
|
|
||||||
renderer, media_type = self._determine_renderer(self.request)
|
|
||||||
except ErrorResponse, exc:
|
|
||||||
renderer = self._default_renderer(self)
|
|
||||||
media_type = renderer.media_type
|
|
||||||
response = exc.response
|
|
||||||
|
|
||||||
# Set the media type of the response
|
|
||||||
# Note that the renderer *could* override it in .render() if required.
|
|
||||||
response.media_type = renderer.media_type
|
|
||||||
|
|
||||||
# Serialize the response content
|
|
||||||
if response.has_content_body:
|
|
||||||
content = renderer.render(response.cleaned_content, media_type)
|
|
||||||
else:
|
|
||||||
content = renderer.render()
|
|
||||||
|
|
||||||
# Build the HTTP Response
|
|
||||||
resp = HttpResponse(content, mimetype=response.media_type, status=response.status)
|
|
||||||
for (key, val) in response.headers.items():
|
|
||||||
resp[key] = val
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def _determine_renderer(self, request):
|
|
||||||
"""
|
|
||||||
Determines the appropriate renderer for the output, given the client's 'Accept' header,
|
|
||||||
and the :attr:`renderers` set on this class.
|
|
||||||
|
|
||||||
Returns a 2-tuple of `(renderer, media_type)`
|
|
||||||
|
|
||||||
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._IGNORE_IE_ACCEPT_HEADER and
|
|
||||||
'HTTP_USER_AGENT' in request.META and
|
|
||||||
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
|
|
||||||
# Ignore MSIE's broken accept behavior and do something sensible instead
|
|
||||||
accept_list = ['text/html', '*/*']
|
|
||||||
elif 'HTTP_ACCEPT' in request.META:
|
|
||||||
# Use standard HTTP Accept negotiation
|
|
||||||
accept_list = [token.strip() for token in request.META['HTTP_ACCEPT'].split(',')]
|
|
||||||
else:
|
|
||||||
# No accept header specified
|
|
||||||
accept_list = ['*/*']
|
|
||||||
|
|
||||||
# Check the acceptable media types against each renderer,
|
|
||||||
# attempting more specific media types first
|
|
||||||
# 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)
|
|
||||||
renderers = [renderer_cls(self) for renderer_cls in self.get_renderers()]
|
|
||||||
|
|
||||||
for accepted_media_type_lst in order_by_precedence(accept_list):
|
|
||||||
for renderer in renderers:
|
|
||||||
for accepted_media_type in accepted_media_type_lst:
|
|
||||||
if renderer.can_handle_response(accepted_media_type):
|
|
||||||
return renderer, accepted_media_type
|
|
||||||
|
|
||||||
# No acceptable renderers were found
|
|
||||||
raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE,
|
|
||||||
{'detail': 'Could not satisfy the client\'s Accept header',
|
|
||||||
'available_types': self._rendered_media_types})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _rendered_media_types(self):
|
def _rendered_media_types(self):
|
||||||
"""
|
"""
|
||||||
Return an list of all the media types that this view can render.
|
Return an list of all the media types that this response can render.
|
||||||
"""
|
"""
|
||||||
return [renderer.media_type for renderer in self.renderers]
|
return [renderer.media_type for renderer in self.renderers]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _rendered_formats(self):
|
def _rendered_formats(self):
|
||||||
"""
|
"""
|
||||||
Return a list of all the formats that this view can render.
|
Return a list of all the formats that this response can render.
|
||||||
"""
|
"""
|
||||||
return [renderer.format for renderer in self.renderers]
|
return [renderer.format for renderer in self.renderers]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _default_renderer(self):
|
def _default_renderer(self):
|
||||||
"""
|
"""
|
||||||
Return the view's default renderer class.
|
Return the response's default renderer class.
|
||||||
"""
|
"""
|
||||||
return self.renderers[0]
|
return self.renderers[0]
|
||||||
|
|
||||||
|
|
||||||
########## Auth Mixin ##########
|
########## Permissions Mixin ##########
|
||||||
|
|
||||||
class AuthMixin(object):
|
class PermissionsMixin(object):
|
||||||
"""
|
"""
|
||||||
Simple :class:`mixin` class to add authentication and permission checking to a :class:`View` class.
|
Simple :class:`mixin` class to add permission checking to a :class:`View` class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
authentication = ()
|
permissions_classes = ()
|
||||||
"""
|
|
||||||
The set of authentication types that this view can handle.
|
|
||||||
|
|
||||||
Should be a tuple/list of classes as described in the :mod:`authentication` module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
permissions = ()
|
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
def get_permissions(self):
|
||||||
def user(self):
|
|
||||||
"""
|
"""
|
||||||
Returns the :obj:`user` for the current request, as determined by the set of
|
Instantiates and returns the list of permissions that this view requires.
|
||||||
:class:`authentication` classes applied to the :class:`View`.
|
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_user'):
|
return [p(self) for p in self.permissions_classes]
|
||||||
self._user = self._authenticate()
|
|
||||||
return self._user
|
|
||||||
|
|
||||||
def _authenticate(self):
|
|
||||||
"""
|
|
||||||
Attempt to authenticate the request using each authentication class in turn.
|
|
||||||
Returns a ``User`` object, which may be ``AnonymousUser``.
|
|
||||||
"""
|
|
||||||
for authentication_cls in self.authentication:
|
|
||||||
authentication = authentication_cls(self)
|
|
||||||
user = authentication.authenticate(self.request)
|
|
||||||
if user:
|
|
||||||
return user
|
|
||||||
return AnonymousUser()
|
|
||||||
|
|
||||||
# TODO: wrap this behavior around dispatch()
|
# TODO: wrap this behavior around dispatch()
|
||||||
def _check_permissions(self):
|
def check_permissions(self, user):
|
||||||
"""
|
"""
|
||||||
Check user permissions and either raise an ``ErrorResponse`` or return.
|
Check user permissions and either raise an ``ImmediateResponse`` or return.
|
||||||
"""
|
"""
|
||||||
user = self.user
|
for permission in self.get_permissions():
|
||||||
for permission_cls in self.permissions:
|
|
||||||
permission = permission_cls(self)
|
|
||||||
permission.check_permission(user)
|
permission.check_permission(user)
|
||||||
|
|
||||||
|
|
||||||
|
@ -397,10 +149,10 @@ 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.ImmediateResponse` 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.request.DATA, self.request.FILES)
|
||||||
return self._content
|
return self._content
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -408,7 +160,7 @@ 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.ImmediateResponse` with status code 400 (Bad Request).
|
||||||
"""
|
"""
|
||||||
return self.validate_request(self.request.GET)
|
return self.validate_request(self.request.GET)
|
||||||
|
|
||||||
|
@ -420,14 +172,14 @@ class ResourceMixin(object):
|
||||||
return ModelResource(self)
|
return ModelResource(self)
|
||||||
elif getattr(self, 'form', None):
|
elif getattr(self, 'form', None):
|
||||||
return FormResource(self)
|
return FormResource(self)
|
||||||
elif getattr(self, '%s_form' % self.method.lower(), None):
|
elif getattr(self, '%s_form' % self.request.method.lower(), None):
|
||||||
return FormResource(self)
|
return FormResource(self)
|
||||||
return Resource(self)
|
return Resource(self)
|
||||||
|
|
||||||
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, validated content.
|
||||||
May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
May raise an :class:`response.ImmediateResponse` with status code 400 (Bad Request) on failure.
|
||||||
"""
|
"""
|
||||||
return self._resource.validate_request(data, files)
|
return self._resource.validate_request(data, files)
|
||||||
|
|
||||||
|
@ -534,9 +286,9 @@ class ReadModelMixin(ModelMixin):
|
||||||
try:
|
try:
|
||||||
self.model_instance = self.get_instance(**query_kwargs)
|
self.model_instance = self.get_instance(**query_kwargs)
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
|
raise ImmediateResponse(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
return self.model_instance
|
return Response(self.model_instance)
|
||||||
|
|
||||||
|
|
||||||
class CreateModelMixin(ModelMixin):
|
class CreateModelMixin(ModelMixin):
|
||||||
|
@ -573,10 +325,12 @@ class CreateModelMixin(ModelMixin):
|
||||||
data[m2m_data[fieldname][0]] = related_item
|
data[m2m_data[fieldname][0]] = related_item
|
||||||
manager.through(**data).save()
|
manager.through(**data).save()
|
||||||
|
|
||||||
headers = {}
|
response = Response(instance, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
# Set headers
|
||||||
if hasattr(self.resource, 'url'):
|
if hasattr(self.resource, 'url'):
|
||||||
headers['Location'] = self.resource(self).url(instance)
|
response['Location'] = self.resource(self).url(instance)
|
||||||
return Response(status.HTTP_201_CREATED, instance, headers)
|
return response
|
||||||
|
|
||||||
|
|
||||||
class UpdateModelMixin(ModelMixin):
|
class UpdateModelMixin(ModelMixin):
|
||||||
|
@ -597,7 +351,7 @@ class UpdateModelMixin(ModelMixin):
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
self.model_instance = model(**self.get_instance_data(model, self.CONTENT, *args, **kwargs))
|
self.model_instance = model(**self.get_instance_data(model, self.CONTENT, *args, **kwargs))
|
||||||
self.model_instance.save()
|
self.model_instance.save()
|
||||||
return self.model_instance
|
return Response(self.model_instance)
|
||||||
|
|
||||||
|
|
||||||
class DeleteModelMixin(ModelMixin):
|
class DeleteModelMixin(ModelMixin):
|
||||||
|
@ -611,10 +365,10 @@ class DeleteModelMixin(ModelMixin):
|
||||||
try:
|
try:
|
||||||
instance = self.get_instance(**query_kwargs)
|
instance = self.get_instance(**query_kwargs)
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
|
raise ImmediateResponse(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
instance.delete()
|
instance.delete()
|
||||||
return
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class ListModelMixin(ModelMixin):
|
class ListModelMixin(ModelMixin):
|
||||||
|
@ -631,7 +385,7 @@ class ListModelMixin(ModelMixin):
|
||||||
if ordering:
|
if ordering:
|
||||||
queryset = queryset.order_by(*ordering)
|
queryset = queryset.order_by(*ordering)
|
||||||
|
|
||||||
return queryset
|
return Response(queryset)
|
||||||
|
|
||||||
|
|
||||||
########## Pagination Mixins ##########
|
########## Pagination Mixins ##########
|
||||||
|
@ -710,7 +464,7 @@ class PaginatorMixin(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 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.request.method.upper() != 'GET':
|
||||||
return self._resource.filter_response(obj)
|
return self._resource.filter_response(obj)
|
||||||
|
|
||||||
paginator = Paginator(obj, self.get_limit())
|
paginator = Paginator(obj, self.get_limit())
|
||||||
|
@ -718,12 +472,14 @@ class PaginatorMixin(object):
|
||||||
try:
|
try:
|
||||||
page_num = int(self.request.GET.get('page', '1'))
|
page_num = int(self.request.GET.get('page', '1'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND,
|
raise ImmediateResponse(
|
||||||
{'detail': 'That page contains no results'})
|
{'detail': 'That page contains no results'},
|
||||||
|
status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
if page_num not in paginator.page_range:
|
if page_num not in paginator.page_range:
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND,
|
raise ImmediateResponse(
|
||||||
{'detail': 'That page contains no results'})
|
{'detail': 'That page contains no results'},
|
||||||
|
status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
page = paginator.page(page_num)
|
page = paginator.page(page_num)
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ from django.http.multipartparser import MultiPartParserError
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
from djangorestframework.compat import yaml
|
from djangorestframework.compat import yaml
|
||||||
from djangorestframework.response import ErrorResponse
|
from djangorestframework.response import ImmediateResponse
|
||||||
from djangorestframework.utils.mediatypes import media_type_matches
|
from djangorestframework.utils.mediatypes import media_type_matches
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
from djangorestframework.compat import ETParseError
|
from djangorestframework.compat import ETParseError
|
||||||
|
@ -45,13 +45,6 @@ class BaseParser(object):
|
||||||
|
|
||||||
media_type = None
|
media_type = None
|
||||||
|
|
||||||
def __init__(self, view):
|
|
||||||
"""
|
|
||||||
Initialize the parser with the ``View`` instance as state,
|
|
||||||
in case the parser needs to access any metadata on the :obj:`View` object.
|
|
||||||
"""
|
|
||||||
self.view = view
|
|
||||||
|
|
||||||
def can_handle_request(self, content_type):
|
def can_handle_request(self, content_type):
|
||||||
"""
|
"""
|
||||||
Returns :const:`True` if this parser is able to deal with the given *content_type*.
|
Returns :const:`True` if this parser is able to deal with the given *content_type*.
|
||||||
|
@ -65,12 +58,12 @@ class BaseParser(object):
|
||||||
"""
|
"""
|
||||||
return media_type_matches(self.media_type, content_type)
|
return media_type_matches(self.media_type, content_type)
|
||||||
|
|
||||||
def parse(self, stream):
|
def parse(self, stream, meta, upload_handlers):
|
||||||
"""
|
"""
|
||||||
Given a *stream* to read from, return the deserialized output.
|
Given a *stream* to read from, return the deserialized output.
|
||||||
Should return a 2-tuple of (data, files).
|
Should return a 2-tuple of (data, files).
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.")
|
raise NotImplementedError(".parse() Must be overridden to be implemented.")
|
||||||
|
|
||||||
|
|
||||||
class JSONParser(BaseParser):
|
class JSONParser(BaseParser):
|
||||||
|
@ -80,7 +73,7 @@ class JSONParser(BaseParser):
|
||||||
|
|
||||||
media_type = 'application/json'
|
media_type = 'application/json'
|
||||||
|
|
||||||
def parse(self, stream):
|
def parse(self, stream, meta, upload_handlers):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
|
@ -90,8 +83,9 @@ class JSONParser(BaseParser):
|
||||||
try:
|
try:
|
||||||
return (json.load(stream), None)
|
return (json.load(stream), None)
|
||||||
except ValueError, exc:
|
except ValueError, exc:
|
||||||
raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
|
raise ImmediateResponse(
|
||||||
{'detail': 'JSON parse error - %s' % unicode(exc)})
|
{'detail': 'JSON parse error - %s' % unicode(exc)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class YAMLParser(BaseParser):
|
class YAMLParser(BaseParser):
|
||||||
|
@ -101,7 +95,7 @@ class YAMLParser(BaseParser):
|
||||||
|
|
||||||
media_type = 'application/yaml'
|
media_type = 'application/yaml'
|
||||||
|
|
||||||
def parse(self, stream):
|
def parse(self, stream, meta, upload_handlers):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
|
@ -111,8 +105,9 @@ class YAMLParser(BaseParser):
|
||||||
try:
|
try:
|
||||||
return (yaml.safe_load(stream), None)
|
return (yaml.safe_load(stream), None)
|
||||||
except (ValueError, yaml.parser.ParserError), exc:
|
except (ValueError, yaml.parser.ParserError), exc:
|
||||||
content = {'detail': 'YAML parse error - %s' % unicode(exc)}
|
raise ImmediateResponse(
|
||||||
raise ErrorResponse(status.HTTP_400_BAD_REQUEST, content)
|
{'detail': 'YAML parse error - %s' % unicode(exc)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class PlainTextParser(BaseParser):
|
class PlainTextParser(BaseParser):
|
||||||
|
@ -122,7 +117,7 @@ class PlainTextParser(BaseParser):
|
||||||
|
|
||||||
media_type = 'text/plain'
|
media_type = 'text/plain'
|
||||||
|
|
||||||
def parse(self, stream):
|
def parse(self, stream, meta, upload_handlers):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
|
@ -139,7 +134,7 @@ class FormParser(BaseParser):
|
||||||
|
|
||||||
media_type = 'application/x-www-form-urlencoded'
|
media_type = 'application/x-www-form-urlencoded'
|
||||||
|
|
||||||
def parse(self, stream):
|
def parse(self, stream, meta, upload_handlers):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
|
@ -157,20 +152,20 @@ class MultiPartParser(BaseParser):
|
||||||
|
|
||||||
media_type = 'multipart/form-data'
|
media_type = 'multipart/form-data'
|
||||||
|
|
||||||
def parse(self, stream):
|
def parse(self, stream, meta, upload_handlers):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
`data` will be a :class:`QueryDict` containing all the form parameters.
|
`data` will be a :class:`QueryDict` containing all the form parameters.
|
||||||
`files` will be a :class:`QueryDict` containing all the form files.
|
`files` will be a :class:`QueryDict` containing all the form files.
|
||||||
"""
|
"""
|
||||||
upload_handlers = self.view.request._get_upload_handlers()
|
|
||||||
try:
|
try:
|
||||||
django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers)
|
parser = DjangoMultiPartParser(meta, stream, upload_handlers)
|
||||||
return django_parser.parse()
|
return parser.parse()
|
||||||
except MultiPartParserError, exc:
|
except MultiPartParserError, exc:
|
||||||
raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
|
raise ImmediateResponse(
|
||||||
{'detail': 'multipart parse error - %s' % unicode(exc)})
|
{'detail': 'multipart parse error - %s' % unicode(exc)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class XMLParser(BaseParser):
|
class XMLParser(BaseParser):
|
||||||
|
@ -180,7 +175,7 @@ class XMLParser(BaseParser):
|
||||||
|
|
||||||
media_type = 'application/xml'
|
media_type = 'application/xml'
|
||||||
|
|
||||||
def parse(self, stream):
|
def parse(self, stream, meta, upload_handlers):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
|
@ -188,10 +183,10 @@ class XMLParser(BaseParser):
|
||||||
`files` will always be `None`.
|
`files` will always be `None`.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
tree = ET.parse(stream)
|
tree = ET.parse(stream)
|
||||||
except (ExpatError, ETParseError, ValueError), exc:
|
except (ExpatError, ETParseError, ValueError), exc:
|
||||||
content = {'detail': 'XML parse error - %s' % unicode(exc)}
|
content = {'detail': 'XML parse error - %s' % unicode(exc)}
|
||||||
raise ErrorResponse(status.HTTP_400_BAD_REQUEST, content)
|
raise ImmediateResponse(content, status=status.HTTP_400_BAD_REQUEST)
|
||||||
data = self._xml_convert(tree.getroot())
|
data = self._xml_convert(tree.getroot())
|
||||||
|
|
||||||
return (data, None)
|
return (data, None)
|
||||||
|
@ -255,4 +250,3 @@ if yaml:
|
||||||
DEFAULT_PARSERS += (YAMLParser, )
|
DEFAULT_PARSERS += (YAMLParser, )
|
||||||
else:
|
else:
|
||||||
YAMLParser = None
|
YAMLParser = None
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
"""
|
"""
|
||||||
The :mod:`permissions` module bundles a set of permission classes that are used
|
The :mod:`permissions` module bundles a set of permission classes that are used
|
||||||
for checking if a request passes a certain set of constraints. You can assign a permission
|
for checking if a request passes a certain set of constraints.
|
||||||
class to your view by setting your View's :attr:`permissions` class attribute.
|
|
||||||
|
Permission behavior is provided by mixing the :class:`mixins.PermissionsMixin` class into a :class:`View` class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
from djangorestframework.response import ErrorResponse
|
from djangorestframework.response import ImmediateResponse
|
||||||
import time
|
import time
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -23,14 +24,14 @@ __all__ = (
|
||||||
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
|
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
|
||||||
|
|
||||||
|
|
||||||
_403_FORBIDDEN_RESPONSE = ErrorResponse(
|
_403_FORBIDDEN_RESPONSE = ImmediateResponse(
|
||||||
status.HTTP_403_FORBIDDEN,
|
|
||||||
{'detail': 'You do not have permission to access this resource. ' +
|
{'detail': 'You do not have permission to access this resource. ' +
|
||||||
'You may need to login or otherwise authenticate the request.'})
|
'You may need to login or otherwise authenticate the request.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
_503_SERVICE_UNAVAILABLE = ErrorResponse(
|
_503_SERVICE_UNAVAILABLE = ImmediateResponse(
|
||||||
status.HTTP_503_SERVICE_UNAVAILABLE,
|
{'detail': 'request was throttled'},
|
||||||
{'detail': 'request was throttled'})
|
status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||||
|
|
||||||
|
|
||||||
class BasePermission(object):
|
class BasePermission(object):
|
||||||
|
@ -45,7 +46,7 @@ class BasePermission(object):
|
||||||
|
|
||||||
def check_permission(self, auth):
|
def check_permission(self, auth):
|
||||||
"""
|
"""
|
||||||
Should simply return, or raise an :exc:`response.ErrorResponse`.
|
Should simply return, or raise an :exc:`response.ImmediateResponse`.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -126,7 +127,7 @@ class DjangoModelPermissions(BasePermission):
|
||||||
try:
|
try:
|
||||||
return [perm % kwargs for perm in self.perms_map[method]]
|
return [perm % kwargs for perm in self.perms_map[method]]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED)
|
ImmediateResponse(status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
|
||||||
def check_permission(self, user):
|
def check_permission(self, user):
|
||||||
method = self.view.method
|
method = self.view.method
|
||||||
|
@ -164,7 +165,7 @@ class BaseThrottle(BasePermission):
|
||||||
def check_permission(self, auth):
|
def check_permission(self, auth):
|
||||||
"""
|
"""
|
||||||
Check the throttling.
|
Check the throttling.
|
||||||
Return `None` or raise an :exc:`.ErrorResponse`.
|
Return `None` or raise an :exc:`.ImmediateResponse`.
|
||||||
"""
|
"""
|
||||||
num, period = getattr(self.view, self.attr_name, self.default).split('/')
|
num, period = getattr(self.view, self.attr_name, self.default).split('/')
|
||||||
self.num_requests = int(num)
|
self.num_requests = int(num)
|
||||||
|
@ -200,7 +201,7 @@ class BaseThrottle(BasePermission):
|
||||||
self.history.insert(0, self.now)
|
self.history.insert(0, self.now)
|
||||||
cache.set(self.key, self.history, self.duration)
|
cache.set(self.key, self.history, self.duration)
|
||||||
header = 'status=SUCCESS; next=%s sec' % self.next()
|
header = 'status=SUCCESS; next=%s sec' % self.next()
|
||||||
self.view.add_header('X-Throttle', header)
|
self.view.headers['X-Throttle'] = header
|
||||||
|
|
||||||
def throttle_failure(self):
|
def throttle_failure(self):
|
||||||
"""
|
"""
|
||||||
|
@ -208,7 +209,7 @@ class BaseThrottle(BasePermission):
|
||||||
Raises a '503 service unavailable' response.
|
Raises a '503 service unavailable' response.
|
||||||
"""
|
"""
|
||||||
header = 'status=FAILURE; next=%s sec' % self.next()
|
header = 'status=FAILURE; next=%s sec' % self.next()
|
||||||
self.view.add_header('X-Throttle', header)
|
self.view.headers['X-Throttle'] = header
|
||||||
raise _503_SERVICE_UNAVAILABLE
|
raise _503_SERVICE_UNAVAILABLE
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
|
|
|
@ -6,20 +6,18 @@ by serializing the output along with documentation regarding the View, output st
|
||||||
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
|
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
|
||||||
"""
|
"""
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
|
||||||
from django.core.serializers.json import DateTimeAwareJSONEncoder
|
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
|
||||||
|
|
||||||
|
|
||||||
from djangorestframework.compat import yaml
|
from djangorestframework.compat import yaml
|
||||||
from djangorestframework.utils import dict2xml, url_resolves
|
from djangorestframework.utils import dict2xml
|
||||||
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
||||||
from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
|
from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
|
||||||
from djangorestframework import VERSION
|
from djangorestframework import VERSION
|
||||||
|
|
||||||
import string
|
import string
|
||||||
from urllib import quote_plus
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BaseRenderer',
|
'BaseRenderer',
|
||||||
|
@ -45,7 +43,7 @@ class BaseRenderer(object):
|
||||||
media_type = None
|
media_type = None
|
||||||
format = None
|
format = None
|
||||||
|
|
||||||
def __init__(self, view):
|
def __init__(self, view=None):
|
||||||
self.view = view
|
self.view = view
|
||||||
|
|
||||||
def can_handle_response(self, accept):
|
def can_handle_response(self, accept):
|
||||||
|
@ -60,9 +58,13 @@ class BaseRenderer(object):
|
||||||
This may be overridden to provide for other behavior, but typically you'll
|
This may be overridden to provide for other behavior, but typically you'll
|
||||||
instead want to just set the :attr:`media_type` attribute on the class.
|
instead want to just set the :attr:`media_type` attribute on the class.
|
||||||
"""
|
"""
|
||||||
format = self.view.kwargs.get(self._FORMAT_QUERY_PARAM, None)
|
# TODO: format overriding must go out of here
|
||||||
if format is None:
|
format = None
|
||||||
|
if self.view is not None:
|
||||||
|
format = self.view.kwargs.get(self._FORMAT_QUERY_PARAM, None)
|
||||||
|
if format is None and self.view is not None:
|
||||||
format = self.view.request.GET.get(self._FORMAT_QUERY_PARAM, None)
|
format = self.view.request.GET.get(self._FORMAT_QUERY_PARAM, None)
|
||||||
|
|
||||||
if format is not None:
|
if format is not None:
|
||||||
return format == self.format
|
return format == self.format
|
||||||
return media_type_matches(self.media_type, accept)
|
return media_type_matches(self.media_type, accept)
|
||||||
|
@ -211,7 +213,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Find the first valid renderer and render the content. (Don't use another documenting renderer.)
|
# Find the first valid renderer and render the content. (Don't use another documenting renderer.)
|
||||||
renderers = [renderer for renderer in view.renderers if not issubclass(renderer, DocumentingTemplateRenderer)]
|
renderers = [renderer for renderer in view.renderers
|
||||||
|
if not issubclass(renderer, DocumentingTemplateRenderer)]
|
||||||
if not renderers:
|
if not renderers:
|
||||||
return '[No renderers were found]'
|
return '[No renderers were found]'
|
||||||
|
|
||||||
|
@ -265,12 +268,12 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
|
|
||||||
# If we're not using content overloading there's no point in supplying a generic form,
|
# If we're not using content overloading there's no point in supplying a generic form,
|
||||||
# as the view won't treat the form's value as the content of the request.
|
# as the view won't treat the form's value as the content of the request.
|
||||||
if not getattr(view, '_USE_FORM_OVERLOADING', False):
|
if not getattr(view.request, '_USE_FORM_OVERLOADING', False):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
||||||
class GenericContentForm(forms.Form):
|
class GenericContentForm(forms.Form):
|
||||||
def __init__(self, view):
|
def __init__(self, view, request):
|
||||||
"""We don't know the names of the fields we want to set until the point the form is instantiated,
|
"""We don't know the names of the fields we want to set until the point the form is instantiated,
|
||||||
as they are determined by the Resource the form is being created against.
|
as they are determined by the Resource the form is being created against.
|
||||||
Add the fields dynamically."""
|
Add the fields dynamically."""
|
||||||
|
@ -279,18 +282,18 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
contenttype_choices = [(media_type, media_type) for media_type in view._parsed_media_types]
|
contenttype_choices = [(media_type, media_type) for media_type in view._parsed_media_types]
|
||||||
initial_contenttype = view._default_parser.media_type
|
initial_contenttype = view._default_parser.media_type
|
||||||
|
|
||||||
self.fields[view._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
|
self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
|
||||||
choices=contenttype_choices,
|
choices=contenttype_choices,
|
||||||
initial=initial_contenttype)
|
initial=initial_contenttype)
|
||||||
self.fields[view._CONTENT_PARAM] = forms.CharField(label='Content',
|
self.fields[request._CONTENT_PARAM] = forms.CharField(label='Content',
|
||||||
widget=forms.Textarea)
|
widget=forms.Textarea)
|
||||||
|
|
||||||
# If either of these reserved parameters are turned off then content tunneling is not possible
|
# If either of these reserved parameters are turned off then content tunneling is not possible
|
||||||
if self.view._CONTENTTYPE_PARAM is None or self.view._CONTENT_PARAM is None:
|
if self.view.request._CONTENTTYPE_PARAM is None or self.view.request._CONTENT_PARAM is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Okey doke, let's do it
|
# Okey doke, let's do it
|
||||||
return GenericContentForm(view)
|
return GenericContentForm(view, view.request)
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
try:
|
try:
|
||||||
|
@ -319,13 +322,6 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
put_form_instance = self._get_form_instance(self.view, 'put')
|
put_form_instance = self._get_form_instance(self.view, 'put')
|
||||||
post_form_instance = self._get_form_instance(self.view, 'post')
|
post_form_instance = self._get_form_instance(self.view, 'post')
|
||||||
|
|
||||||
if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL):
|
|
||||||
login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.view.request.path))
|
|
||||||
logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.view.request.path))
|
|
||||||
else:
|
|
||||||
login_url = None
|
|
||||||
logout_url = None
|
|
||||||
|
|
||||||
name = self.get_name()
|
name = self.get_name()
|
||||||
description = self.get_description()
|
description = self.get_description()
|
||||||
|
|
||||||
|
@ -341,6 +337,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
'name': name,
|
'name': name,
|
||||||
'version': VERSION,
|
'version': VERSION,
|
||||||
'breadcrumblist': breadcrumb_list,
|
'breadcrumblist': breadcrumb_list,
|
||||||
|
'allowed_methods': self.view.allowed_methods,
|
||||||
'available_formats': self.view._rendered_formats,
|
'available_formats': self.view._rendered_formats,
|
||||||
'put_form': put_form_instance,
|
'put_form': put_form_instance,
|
||||||
'post_form': post_form_instance,
|
'post_form': post_form_instance,
|
||||||
|
@ -353,8 +350,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
# Munge DELETE Response code to allow us to return content
|
# Munge DELETE Response code to allow us to return content
|
||||||
# (Do this *after* we've rendered the template so that we include
|
# (Do this *after* we've rendered the template so that we include
|
||||||
# the normal deletion response code in the output)
|
# the normal deletion response code in the output)
|
||||||
if self.view.response.status == 204:
|
if self.view.response.status_code == 204:
|
||||||
self.view.response.status = 200
|
self.view.response.status_code = 200
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
242
djangorestframework/request.py
Normal file
242
djangorestframework/request.py
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
"""
|
||||||
|
The :mod:`request` module provides a :class:`Request` class used to wrap the standard `request`
|
||||||
|
object received in all the views.
|
||||||
|
|
||||||
|
The wrapped request then offers a richer API, in particular :
|
||||||
|
|
||||||
|
- content automatically parsed according to `Content-Type` header,
|
||||||
|
and available as :meth:`.DATA<Request.DATA>`
|
||||||
|
- full support of PUT method, including support for file uploads
|
||||||
|
- form overloading of HTTP method, content type and content
|
||||||
|
"""
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
|
from djangorestframework import status
|
||||||
|
from djangorestframework.utils.mediatypes import is_form_media_type
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('Request',)
|
||||||
|
|
||||||
|
|
||||||
|
class Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _hasattr(obj, name):
|
||||||
|
return not getattr(obj, name) is Empty
|
||||||
|
|
||||||
|
|
||||||
|
class Request(object):
|
||||||
|
"""
|
||||||
|
Wrapper allowing to enhance a standard `HttpRequest` instance.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
- request(HttpRequest). The original request instance.
|
||||||
|
- parsers(list/tuple). The parsers to use for parsing the request content.
|
||||||
|
- authentications(list/tuple). The authentications used to try authenticating the request's user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_USE_FORM_OVERLOADING = True
|
||||||
|
_METHOD_PARAM = '_method'
|
||||||
|
_CONTENTTYPE_PARAM = '_content_type'
|
||||||
|
_CONTENT_PARAM = '_content'
|
||||||
|
|
||||||
|
def __init__(self, request=None, parsers=None, authentication=None):
|
||||||
|
self._request = request
|
||||||
|
self.parsers = parsers or ()
|
||||||
|
self.authentication = authentication or ()
|
||||||
|
self._data = Empty
|
||||||
|
self._files = Empty
|
||||||
|
self._method = Empty
|
||||||
|
self._content_type = Empty
|
||||||
|
self._stream = Empty
|
||||||
|
|
||||||
|
def get_parsers(self):
|
||||||
|
"""
|
||||||
|
Instantiates and returns the list of parsers the request will use.
|
||||||
|
"""
|
||||||
|
return [parser() for parser in self.parsers]
|
||||||
|
|
||||||
|
def get_authentications(self):
|
||||||
|
"""
|
||||||
|
Instantiates and returns the list of parsers the request will use.
|
||||||
|
"""
|
||||||
|
return [authentication() for authentication in self.authentication]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def method(self):
|
||||||
|
"""
|
||||||
|
Returns the HTTP method.
|
||||||
|
|
||||||
|
This allows the `method` to be overridden by using a hidden `form`
|
||||||
|
field on a form POST request.
|
||||||
|
"""
|
||||||
|
if not _hasattr(self, '_method'):
|
||||||
|
self._load_method_and_content_type()
|
||||||
|
return self._method
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_type(self):
|
||||||
|
"""
|
||||||
|
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'):
|
||||||
|
self._load_method_and_content_type()
|
||||||
|
return self._content_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stream(self):
|
||||||
|
"""
|
||||||
|
Returns an object that may be used to stream the request content.
|
||||||
|
"""
|
||||||
|
if not _hasattr(self, '_stream'):
|
||||||
|
self._load_stream()
|
||||||
|
return self._stream
|
||||||
|
|
||||||
|
@property
|
||||||
|
def DATA(self):
|
||||||
|
"""
|
||||||
|
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'):
|
||||||
|
self._load_data_and_files()
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def FILES(self):
|
||||||
|
"""
|
||||||
|
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'):
|
||||||
|
self._load_data_and_files()
|
||||||
|
return self._files
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user(self):
|
||||||
|
"""
|
||||||
|
Returns the :obj:`user` for the current request, authenticated
|
||||||
|
with the set of :class:`authentication` instances applied to the :class:`Request`.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, '_user'):
|
||||||
|
self._user = self._authenticate()
|
||||||
|
return self._user
|
||||||
|
|
||||||
|
def _load_data_and_files(self):
|
||||||
|
"""
|
||||||
|
Parses the request content into self.DATA and self.FILES.
|
||||||
|
"""
|
||||||
|
if not _hasattr(self, '_content_type'):
|
||||||
|
self._load_method_and_content_type()
|
||||||
|
|
||||||
|
if not _hasattr(self, '_data'):
|
||||||
|
(self._data, self._files) = self._parse()
|
||||||
|
|
||||||
|
def _load_method_and_content_type(self):
|
||||||
|
"""
|
||||||
|
Sets the method and content_type, and then check if they've been overridden.
|
||||||
|
"""
|
||||||
|
self._content_type = self.META.get('HTTP_CONTENT_TYPE', self.META.get('CONTENT_TYPE', ''))
|
||||||
|
self._perform_form_overloading()
|
||||||
|
# if the HTTP method was not overloaded, we take the raw HTTP method
|
||||||
|
if not _hasattr(self, '_method'):
|
||||||
|
self._method = self._request.method
|
||||||
|
|
||||||
|
def _load_stream(self):
|
||||||
|
try:
|
||||||
|
content_length = int(self.META.get('CONTENT_LENGTH',
|
||||||
|
self.META.get('HTTP_CONTENT_LENGTH')))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
content_length = 0
|
||||||
|
|
||||||
|
if content_length == 0:
|
||||||
|
self._stream = None
|
||||||
|
elif hasattr(self._request, 'read'):
|
||||||
|
self._stream = self._request
|
||||||
|
else:
|
||||||
|
self._stream = StringIO(self.raw_post_data)
|
||||||
|
|
||||||
|
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 overridden by setting them in hidden
|
||||||
|
form fields or not.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We only need to use form overloading on form POST requests.
|
||||||
|
if (not self._USE_FORM_OVERLOADING
|
||||||
|
or self._request.method != 'POST'
|
||||||
|
or not is_form_media_type(self._content_type)):
|
||||||
|
return
|
||||||
|
|
||||||
|
# At this point we're committed to parsing the request as form data.
|
||||||
|
self._data = self._request.POST
|
||||||
|
self._files = self._request.FILES
|
||||||
|
|
||||||
|
# Method overloading - change the method and remove the param from the content.
|
||||||
|
if self._METHOD_PARAM in self._data:
|
||||||
|
# NOTE: `pop` on a `QueryDict` returns a list of values.
|
||||||
|
self._method = self._data.pop(self._METHOD_PARAM)[0].upper()
|
||||||
|
|
||||||
|
# Content overloading - modify the content type, and re-parse.
|
||||||
|
if (self._CONTENT_PARAM in self._data and
|
||||||
|
self._CONTENTTYPE_PARAM in self._data):
|
||||||
|
self._content_type = self._data.pop(self._CONTENTTYPE_PARAM)[0]
|
||||||
|
self._stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0])
|
||||||
|
(self._data, self._files) = self._parse()
|
||||||
|
|
||||||
|
def _parse(self):
|
||||||
|
"""
|
||||||
|
Parse the request content.
|
||||||
|
|
||||||
|
May raise a 415 ImmediateResponse (Unsupported Media Type), or a
|
||||||
|
400 ImmediateResponse (Bad Request).
|
||||||
|
"""
|
||||||
|
if self.stream is None or self.content_type is None:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
for parser in self.get_parsers():
|
||||||
|
if parser.can_handle_request(self.content_type):
|
||||||
|
return parser.parse(self.stream, self.META, self.upload_handlers)
|
||||||
|
|
||||||
|
self._raise_415_response(self._content_type)
|
||||||
|
|
||||||
|
def _raise_415_response(self, content_type):
|
||||||
|
"""
|
||||||
|
Raise a 415 response if we cannot parse the given content type.
|
||||||
|
"""
|
||||||
|
from djangorestframework.response import ImmediateResponse
|
||||||
|
|
||||||
|
raise ImmediateResponse(
|
||||||
|
{
|
||||||
|
'error': 'Unsupported media type in request \'%s\'.'
|
||||||
|
% content_type
|
||||||
|
},
|
||||||
|
status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
|
||||||
|
|
||||||
|
def _authenticate(self):
|
||||||
|
"""
|
||||||
|
Attempt to authenticate the request using each authentication instance in turn.
|
||||||
|
Returns a ``User`` object, which may be ``AnonymousUser``.
|
||||||
|
"""
|
||||||
|
for authentication in self.get_authentications():
|
||||||
|
user = authentication.authenticate(self)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
return AnonymousUser()
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
"""
|
||||||
|
Proxy other attributes to the underlying HttpRequest object.
|
||||||
|
"""
|
||||||
|
return getattr(self._request, name)
|
|
@ -1,6 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from djangorestframework.response import ImmediateResponse
|
||||||
from djangorestframework.response import ErrorResponse
|
|
||||||
from djangorestframework.serializer import Serializer
|
from djangorestframework.serializer import Serializer
|
||||||
from djangorestframework.utils import as_tuple
|
from djangorestframework.utils import as_tuple
|
||||||
|
|
||||||
|
@ -16,14 +15,23 @@ class BaseResource(Serializer):
|
||||||
|
|
||||||
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)
|
||||||
self.view = view
|
# If a view is passed, use that. Otherwise traverse up the stack
|
||||||
self.request = getattr(view, 'request', None)
|
# to find a view we can use
|
||||||
|
if view is not None:
|
||||||
|
self.view = view
|
||||||
|
else:
|
||||||
|
for serializer in stack[::-1]:
|
||||||
|
if hasattr(serializer, 'view') \
|
||||||
|
and getattr(serializer, 'view') != None:
|
||||||
|
self.view = getattr(serializer, 'view')
|
||||||
|
break
|
||||||
|
self.request = getattr(self.view, 'request', None)
|
||||||
|
|
||||||
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
|
Typically raises a :exc:`response.ImmediateResponse` with status code
|
||||||
(Bad Request) on failure.
|
400 (Bad Request) on failure.
|
||||||
"""
|
"""
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -75,19 +83,19 @@ class FormResource(Resource):
|
||||||
"""
|
"""
|
||||||
Flag to check for unknown fields when validating a form. If set to false and
|
Flag to check for unknown fields when validating a form. If set to false and
|
||||||
we receive request data that is not expected by the form it raises an
|
we receive request data that is not expected by the form it raises an
|
||||||
:exc:`response.ErrorResponse` with status code 400. If set to true, only
|
:exc:`response.ImmediateResponse` with status code 400. If set to true, only
|
||||||
expected fields are validated.
|
expected fields are validated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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.ImmediateResponse` 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
|
Validation is standard form validation, with an additional constraint that *no extra unknown fields* may be supplied
|
||||||
if :attr:`self.allow_unknown_form_fields` is ``False``.
|
if :attr:`self.allow_unknown_form_fields` is ``False``.
|
||||||
|
|
||||||
On failure the :exc:`response.ErrorResponse` content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
|
On failure the :exc:`response.ImmediateResponse` 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:`'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', ...]}``.
|
If the :obj:`'field-errors'` key exists it is a dict of ``{'field name as string': ['errors as strings', ...]}``.
|
||||||
"""
|
"""
|
||||||
|
@ -176,7 +184,7 @@ class FormResource(Resource):
|
||||||
detail[u'field_errors'] = field_errors
|
detail[u'field_errors'] = field_errors
|
||||||
|
|
||||||
# Return HTTP 400 response (BAD REQUEST)
|
# Return HTTP 400 response (BAD REQUEST)
|
||||||
raise ErrorResponse(400, detail)
|
raise ImmediateResponse(detail, status=400)
|
||||||
|
|
||||||
def get_form_class(self, method=None):
|
def get_form_class(self, method=None):
|
||||||
"""
|
"""
|
||||||
|
@ -272,14 +280,14 @@ class ModelResource(FormResource):
|
||||||
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.ImmediateResponse` 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 supplied,
|
||||||
and that all fields specified by the fields class attribute must be supplied,
|
and that all fields specified by the fields class attribute must be supplied,
|
||||||
even if they are not validated by the form/model form.
|
even if they are not validated by the form/model form.
|
||||||
|
|
||||||
On failure the ErrorResponse content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
|
On failure the ImmediateResponse 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:`'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}.
|
If the ''field-errors'` key exists it is a dict of {field name as string: list of errors as strings}.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,44 +1,186 @@
|
||||||
"""
|
"""
|
||||||
The :mod:`response` module provides Response classes you can use in your
|
The :mod:`response` module provides :class:`Response` and :class:`ImmediateResponse` classes.
|
||||||
views to return a certain HTTP response. Typically a response is *rendered*
|
|
||||||
into a HTTP response depending on what renderers are set on your view and
|
`Response` is a subclass of `HttpResponse`, and can be similarly instantiated and returned
|
||||||
als depending on the accept header of the request.
|
from any view. It is a bit smarter than Django's `HttpResponse`, for it renders automatically
|
||||||
|
its content to a serial format by using a list of :mod:`renderers`.
|
||||||
|
|
||||||
|
To determine the content type to which it must render, default behaviour is to use standard
|
||||||
|
HTTP Accept header content negotiation. But `Response` also supports overriding the content type
|
||||||
|
by specifying an ``_accept=`` parameter in the URL. Also, `Response` will ignore `Accept` headers
|
||||||
|
from Internet Explorer user agents and use a sensible browser `Accept` header instead.
|
||||||
|
|
||||||
|
|
||||||
|
`ImmediateResponse` is an exception that inherits from `Response`. It can be used
|
||||||
|
to abort the request handling (i.e. ``View.get``, ``View.put``, ...),
|
||||||
|
and immediately returning a response.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from django.template.response import SimpleTemplateResponse
|
||||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||||
|
|
||||||
__all__ = ('Response', 'ErrorResponse')
|
from djangorestframework.utils.mediatypes import order_by_precedence
|
||||||
|
from djangorestframework.utils import MSIE_USER_AGENT_REGEX
|
||||||
# TODO: remove raw_content/cleaned_content and just use content?
|
from djangorestframework import status
|
||||||
|
|
||||||
|
|
||||||
class Response(object):
|
__all__ = ('Response', 'ImmediateResponse')
|
||||||
|
|
||||||
|
|
||||||
|
class NotAcceptable(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Response(SimpleTemplateResponse):
|
||||||
"""
|
"""
|
||||||
An HttpResponse that may include content that hasn't yet been serialized.
|
An HttpResponse that may include content that hasn't yet been serialized.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
- content(object). The raw content, not yet serialized. This must be simple Python
|
||||||
|
data that renderers can handle (e.g.: `dict`, `str`, ...)
|
||||||
|
- renderers(list/tuple). The renderers to use for rendering the response content.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, status=200, content=None, headers=None):
|
_ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
|
||||||
self.status = status
|
_IGNORE_IE_ACCEPT_HEADER = True
|
||||||
self.media_type = None
|
|
||||||
|
def __init__(self, content=None, status=None, headers=None, view=None, request=None, renderers=None):
|
||||||
|
# First argument taken by `SimpleTemplateResponse.__init__` is template_name,
|
||||||
|
# which we don't need
|
||||||
|
super(Response, self).__init__(None, status=status)
|
||||||
|
|
||||||
|
self.raw_content = content
|
||||||
self.has_content_body = content is not None
|
self.has_content_body = content is not None
|
||||||
self.raw_content = content # content prior to filtering
|
self.headers = headers and headers[:] or []
|
||||||
self.cleaned_content = content # content after filtering
|
self.view = view
|
||||||
self.headers = headers or {}
|
self.request = request
|
||||||
|
self.renderers = renderers
|
||||||
|
|
||||||
|
def get_renderers(self):
|
||||||
|
"""
|
||||||
|
Instantiates and returns the list of renderers the response will use.
|
||||||
|
"""
|
||||||
|
return [renderer(self.view) for renderer in self.renderers]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rendered_content(self):
|
||||||
|
"""
|
||||||
|
The final rendered content. Accessing this attribute triggers the
|
||||||
|
complete rendering cycle: selecting suitable renderer, setting
|
||||||
|
response's actual content type, rendering data.
|
||||||
|
"""
|
||||||
|
renderer, media_type = self._determine_renderer()
|
||||||
|
|
||||||
|
# Set the media type of the response
|
||||||
|
self['Content-Type'] = renderer.media_type
|
||||||
|
|
||||||
|
# Render the response content
|
||||||
|
if self.has_content_body:
|
||||||
|
return renderer.render(self.raw_content, media_type)
|
||||||
|
return renderer.render()
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
try:
|
||||||
|
return super(Response, self).render()
|
||||||
|
except NotAcceptable:
|
||||||
|
response = self._get_406_response()
|
||||||
|
return response.render()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_text(self):
|
def status_text(self):
|
||||||
"""
|
"""
|
||||||
Return reason text corresponding to our HTTP response status code.
|
Returns reason text corresponding to our HTTP response status code.
|
||||||
Provided for convenience.
|
Provided for convenience.
|
||||||
"""
|
"""
|
||||||
return STATUS_CODE_TEXT.get(self.status, '')
|
return STATUS_CODE_TEXT.get(self.status_code, '')
|
||||||
|
|
||||||
|
def _determine_accept_list(self):
|
||||||
|
"""
|
||||||
|
Returns a list of accepted media types. This list is determined from :
|
||||||
|
|
||||||
|
1. overload with `_ACCEPT_QUERY_PARAM`
|
||||||
|
2. `Accept` header of the request
|
||||||
|
|
||||||
|
If those are useless, a default value is returned instead.
|
||||||
|
"""
|
||||||
|
request = self.request
|
||||||
|
|
||||||
|
if self._ACCEPT_QUERY_PARAM and request.GET.get(self._ACCEPT_QUERY_PARAM, None):
|
||||||
|
# Use _accept parameter override
|
||||||
|
return [request.GET.get(self._ACCEPT_QUERY_PARAM)]
|
||||||
|
elif (self._IGNORE_IE_ACCEPT_HEADER and
|
||||||
|
'HTTP_USER_AGENT' in request.META and
|
||||||
|
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT']) and
|
||||||
|
request.META.get('HTTP_X_REQUESTED_WITH', '') != 'XMLHttpRequest'):
|
||||||
|
# Ignore MSIE's broken accept behavior and do something sensible instead
|
||||||
|
return ['text/html', '*/*']
|
||||||
|
elif 'HTTP_ACCEPT' in request.META:
|
||||||
|
# Use standard HTTP Accept negotiation
|
||||||
|
return [token.strip() for token in request.META['HTTP_ACCEPT'].split(',')]
|
||||||
|
else:
|
||||||
|
# No accept header specified
|
||||||
|
return ['*/*']
|
||||||
|
|
||||||
|
def _determine_renderer(self):
|
||||||
|
"""
|
||||||
|
Determines the appropriate renderer for the output, given the list of
|
||||||
|
accepted media types, and the :attr:`renderers` set on this class.
|
||||||
|
|
||||||
|
Returns a 2-tuple of `(renderer, media_type)`
|
||||||
|
|
||||||
|
See: RFC 2616, Section 14
|
||||||
|
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
renderers = self.get_renderers()
|
||||||
|
accepts = self._determine_accept_list()
|
||||||
|
|
||||||
|
# Not acceptable response - Ignore accept header.
|
||||||
|
if self.status_code == 406:
|
||||||
|
return (renderers[0], renderers[0].media_type)
|
||||||
|
|
||||||
|
# Check the acceptable media types against each renderer,
|
||||||
|
# attempting more specific media types first
|
||||||
|
# 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)
|
||||||
|
for media_type_list in order_by_precedence(accepts):
|
||||||
|
for renderer in renderers:
|
||||||
|
for media_type in media_type_list:
|
||||||
|
if renderer.can_handle_response(media_type):
|
||||||
|
return renderer, media_type
|
||||||
|
|
||||||
|
# No acceptable renderers were found
|
||||||
|
raise NotAcceptable
|
||||||
|
|
||||||
|
def _get_406_response(self):
|
||||||
|
renderer = self.renderers[0]
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'detail': 'Could not satisfy the client\'s Accept header',
|
||||||
|
'available_types': [renderer.media_type
|
||||||
|
for renderer in self.renderers]
|
||||||
|
},
|
||||||
|
status=status.HTTP_406_NOT_ACCEPTABLE,
|
||||||
|
view=self.view, request=self.request, renderers=[renderer])
|
||||||
|
|
||||||
|
|
||||||
class ErrorResponse(Exception):
|
class ImmediateResponse(Response, Exception):
|
||||||
"""
|
"""
|
||||||
An exception representing an Response that should be returned immediately.
|
An exception representing an Response that should be returned immediately.
|
||||||
Any content should be serialized as-is, without being filtered.
|
Any content should be serialized as-is, without being filtered.
|
||||||
"""
|
"""
|
||||||
|
#TODO: this is just a temporary fix, the whole rendering/support for ImmediateResponse, should be remade : see issue #163
|
||||||
|
|
||||||
def __init__(self, status, content=None, headers={}):
|
def render(self):
|
||||||
self.response = Response(status, content=content, headers=headers)
|
try:
|
||||||
|
return super(Response, self).render()
|
||||||
|
except ImmediateResponse:
|
||||||
|
renderer, media_type = self._determine_renderer()
|
||||||
|
self.renderers.remove(renderer)
|
||||||
|
if len(self.renderers) == 0:
|
||||||
|
raise RuntimeError('Caught an ImmediateResponse while '\
|
||||||
|
'trying to render an ImmediateResponse')
|
||||||
|
return self.render()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.response = Response(*args, **kwargs)
|
||||||
|
|
|
@ -100,6 +100,7 @@ class Serializer(object):
|
||||||
def __init__(self, depth=None, stack=[], **kwargs):
|
def __init__(self, depth=None, stack=[], **kwargs):
|
||||||
if depth is not None:
|
if depth is not None:
|
||||||
self.depth = depth
|
self.depth = depth
|
||||||
|
stack.append(self)
|
||||||
self.stack = stack
|
self.stack = stack
|
||||||
|
|
||||||
def get_fields(self, obj):
|
def get_fields(self, obj):
|
||||||
|
@ -173,11 +174,11 @@ class Serializer(object):
|
||||||
else:
|
else:
|
||||||
depth = self.depth - 1
|
depth = self.depth - 1
|
||||||
|
|
||||||
|
# detect circular references
|
||||||
if any([obj is elem for elem in self.stack]):
|
if any([obj is elem for elem in self.stack]):
|
||||||
return self.serialize_recursion(obj)
|
return self.serialize_recursion(obj)
|
||||||
else:
|
else:
|
||||||
stack = self.stack[:]
|
stack = self.stack[:]
|
||||||
stack.append(obj)
|
|
||||||
|
|
||||||
return related_serializer(depth=depth, stack=stack).serialize(
|
return related_serializer(depth=depth, stack=stack).serialize(
|
||||||
obj, request=getattr(self, 'request', None))
|
obj, request=getattr(self, 'request', None))
|
||||||
|
|
|
@ -21,13 +21,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="user-tools">
|
<div id="user-tools">
|
||||||
{% block userlinks %}
|
{% block userlinks %}
|
||||||
{% if user.is_active %}
|
{% if user.is_active %}
|
||||||
Welcome, {{ user }}.
|
Welcome, {{ user }}.
|
||||||
<a href='{% url djangorestframework:logout %}?next={{ request.path }}'>Log out</a>
|
<a href='{% url djangorestframework:logout %}?next={{ request.path }}'>Log out</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
Anonymous
|
Anonymous
|
||||||
<a href='{% url djangorestframework:login %}?next={{ request.path }}'>Log in</a>
|
<a href='{% url djangorestframework:login %}?next={{ request.path }}'>Log in</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% block nav-global %}{% endblock %}
|
{% block nav-global %}{% endblock %}
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div id="content" class="{% block coltype %}colM{% endblock %}">
|
<div id="content" class="{% block coltype %}colM{% endblock %}">
|
||||||
|
|
||||||
{% if 'OPTIONS' in view.allowed_methods %}
|
{% if 'OPTIONS' in allowed_methods %}
|
||||||
<form action="{{ request.get_full_path }}" method="post">
|
<form action="{{ request.get_full_path }}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="{{ METHOD_PARAM }}" value="OPTIONS" />
|
<input type="hidden" name="{{ METHOD_PARAM }}" value="OPTIONS" />
|
||||||
|
@ -56,12 +56,12 @@
|
||||||
<h1>{{ name }}</h1>
|
<h1>{{ name }}</h1>
|
||||||
<p>{{ description }}</p>
|
<p>{{ description }}</p>
|
||||||
<div class='module'>
|
<div class='module'>
|
||||||
<pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
|
<pre><b>{{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
|
||||||
{% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
{% for key, val in response.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ content|urlize_quoted_links }}</pre>{% endautoescape %}</div>
|
{{ content|urlize_quoted_links }}</pre>{% endautoescape %}</div>
|
||||||
|
|
||||||
{% if 'GET' in view.allowed_methods %}
|
{% if 'GET' in allowed_methods %}
|
||||||
<form>
|
<form>
|
||||||
<fieldset class='module aligned'>
|
<fieldset class='module aligned'>
|
||||||
<h2>GET {{ name }}</h2>
|
<h2>GET {{ name }}</h2>
|
||||||
|
@ -78,9 +78,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Only display the POST/PUT/DELETE forms if method tunneling via POST forms is enabled and the user has permissions on this view. #}
|
{# Only display the POST/PUT/DELETE forms if method tunneling via POST forms is enabled and the user has permissions on this view. #}
|
||||||
{% if METHOD_PARAM and response.status != 403 %}
|
{% if METHOD_PARAM and response.status_code != 403 %}
|
||||||
|
|
||||||
{% if 'POST' in view.allowed_methods %}
|
{% if 'POST' in allowed_methods %}
|
||||||
<form action="{{ request.get_full_path }}" method="post" {% if post_form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
<form action="{{ request.get_full_path }}" method="post" {% if post_form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
||||||
<fieldset class='module aligned'>
|
<fieldset class='module aligned'>
|
||||||
<h2>POST {{ name }}</h2>
|
<h2>POST {{ name }}</h2>
|
||||||
|
@ -101,7 +101,7 @@
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if 'PUT' in view.allowed_methods %}
|
{% if 'PUT' in allowed_methods %}
|
||||||
<form action="{{ request.get_full_path }}" method="post" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
<form action="{{ request.get_full_path }}" method="post" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
||||||
<fieldset class='module aligned'>
|
<fieldset class='module aligned'>
|
||||||
<h2>PUT {{ name }}</h2>
|
<h2>PUT {{ name }}</h2>
|
||||||
|
@ -123,7 +123,7 @@
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if 'DELETE' in view.allowed_methods %}
|
{% if 'DELETE' in allowed_methods %}
|
||||||
<form action="{{ request.get_full_path }}" method="post">
|
<form action="{{ request.get_full_path }}" method="post">
|
||||||
<fieldset class='module aligned'>
|
<fieldset class='module aligned'>
|
||||||
<h2>DELETE {{ name }}</h2>
|
<h2>DELETE {{ name }}</h2>
|
||||||
|
|
|
@ -10,4 +10,3 @@ for module in modules:
|
||||||
exec("from djangorestframework.tests.%s import __doc__ as module_doc" % module)
|
exec("from djangorestframework.tests.%s import __doc__ as module_doc" % module)
|
||||||
exec("from djangorestframework.tests.%s import *" % module)
|
exec("from djangorestframework.tests.%s import *" % module)
|
||||||
__test__[module] = module_doc or ""
|
__test__[module] = module_doc or ""
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
from django.conf.urls.defaults import patterns, url, include
|
from django.conf.urls.defaults import patterns, url, include
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
|
from djangorestframework.response import Response
|
||||||
|
|
||||||
|
|
||||||
# See: http://www.useragentstring.com/
|
# See: http://www.useragentstring.com/
|
||||||
|
@ -32,9 +34,10 @@ class UserAgentMungingTest(TestCase):
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
permissions = ()
|
permissions = ()
|
||||||
|
response_class = Response
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return {'a':1, 'b':2, 'c':3}
|
return self.response_class({'a':1, 'b':2, 'c':3})
|
||||||
|
|
||||||
self.req = RequestFactory()
|
self.req = RequestFactory()
|
||||||
self.MockView = MockView
|
self.MockView = MockView
|
||||||
|
@ -48,18 +51,33 @@ class UserAgentMungingTest(TestCase):
|
||||||
MSIE_7_USER_AGENT):
|
MSIE_7_USER_AGENT):
|
||||||
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
||||||
resp = self.view(req)
|
resp = self.view(req)
|
||||||
|
resp.render()
|
||||||
self.assertEqual(resp['Content-Type'], 'text/html')
|
self.assertEqual(resp['Content-Type'], 'text/html')
|
||||||
|
|
||||||
|
def test_dont_munge_msie_with_x_requested_with_header(self):
|
||||||
|
"""Send MSIE user agent strings, and an X-Requested-With header, and
|
||||||
|
ensure that we get a JSON response if we set a */* Accept header."""
|
||||||
|
for user_agent in (MSIE_9_USER_AGENT,
|
||||||
|
MSIE_8_USER_AGENT,
|
||||||
|
MSIE_7_USER_AGENT):
|
||||||
|
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
resp = self.view(req)
|
||||||
|
resp.render()
|
||||||
|
self.assertEqual(resp['Content-Type'], 'application/json')
|
||||||
|
|
||||||
def test_dont_rewrite_msie_accept_header(self):
|
def test_dont_rewrite_msie_accept_header(self):
|
||||||
"""Turn off _IGNORE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
|
"""Turn off _IGNORE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
|
||||||
that we get a JSON response if we set a */* accept header."""
|
that we get a JSON response if we set a */* accept header."""
|
||||||
view = self.MockView.as_view(_IGNORE_IE_ACCEPT_HEADER=False)
|
class IgnoreIEAcceptResponse(Response):
|
||||||
|
_IGNORE_IE_ACCEPT_HEADER=False
|
||||||
|
view = self.MockView.as_view(response_class=IgnoreIEAcceptResponse)
|
||||||
|
|
||||||
for user_agent in (MSIE_9_USER_AGENT,
|
for user_agent in (MSIE_9_USER_AGENT,
|
||||||
MSIE_8_USER_AGENT,
|
MSIE_8_USER_AGENT,
|
||||||
MSIE_7_USER_AGENT):
|
MSIE_7_USER_AGENT):
|
||||||
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
||||||
resp = view(req)
|
resp = view(req)
|
||||||
|
resp.render()
|
||||||
self.assertEqual(resp['Content-Type'], 'application/json')
|
self.assertEqual(resp['Content-Type'], 'application/json')
|
||||||
|
|
||||||
def test_dont_munge_nice_browsers_accept_header(self):
|
def test_dont_munge_nice_browsers_accept_header(self):
|
||||||
|
@ -72,5 +90,6 @@ class UserAgentMungingTest(TestCase):
|
||||||
OPERA_11_0_OPERA_USER_AGENT):
|
OPERA_11_0_OPERA_USER_AGENT):
|
||||||
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
||||||
resp = self.view(req)
|
resp = self.view(req)
|
||||||
|
resp.render()
|
||||||
self.assertEqual(resp['Content-Type'], 'application/json')
|
self.assertEqual(resp['Content-Type'], 'application/json')
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.contrib.auth.models import User
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
from djangorestframework import permissions
|
from djangorestframework import permissions
|
||||||
|
@ -11,13 +12,13 @@ import base64
|
||||||
|
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
permissions = (permissions.IsAuthenticated,)
|
permissions_classes = (permissions.IsAuthenticated,)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
return {'a': 1, 'b': 2, 'c': 3}
|
return HttpResponse({'a': 1, 'b': 2, 'c': 3})
|
||||||
|
|
||||||
def put(self, request):
|
def put(self, request):
|
||||||
return {'a': 1, 'b': 2, 'c': 3}
|
return HttpResponse({'a': 1, 'b': 2, 'c': 3})
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
(r'^$', MockView.as_view()),
|
(r'^$', MockView.as_view()),
|
||||||
|
|
|
@ -1,233 +0,0 @@
|
||||||
"""
|
|
||||||
Tests for content parsing, and form-overloaded content parsing.
|
|
||||||
"""
|
|
||||||
from django.conf.urls.defaults import patterns
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import TestCase, Client
|
|
||||||
from djangorestframework import status
|
|
||||||
from djangorestframework.authentication import UserLoggedInAuthentication
|
|
||||||
from djangorestframework.compat import RequestFactory, unittest
|
|
||||||
from djangorestframework.mixins import RequestMixin
|
|
||||||
from djangorestframework.parsers import FormParser, MultiPartParser, \
|
|
||||||
PlainTextParser, JSONParser
|
|
||||||
from djangorestframework.response import Response
|
|
||||||
from djangorestframework.views import View
|
|
||||||
|
|
||||||
class MockView(View):
|
|
||||||
authentication = (UserLoggedInAuthentication,)
|
|
||||||
def post(self, request):
|
|
||||||
if request.POST.get('example') is not None:
|
|
||||||
return Response(status.HTTP_200_OK)
|
|
||||||
|
|
||||||
return Response(status.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
|
||||||
(r'^$', MockView.as_view()),
|
|
||||||
)
|
|
||||||
|
|
||||||
class TestContentParsing(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.req = RequestFactory()
|
|
||||||
|
|
||||||
def ensure_determines_no_content_GET(self, view):
|
|
||||||
"""Ensure view.DATA returns None for GET request with no content."""
|
|
||||||
view.request = self.req.get('/')
|
|
||||||
self.assertEqual(view.DATA, None)
|
|
||||||
|
|
||||||
def ensure_determines_no_content_HEAD(self, view):
|
|
||||||
"""Ensure view.DATA returns None for HEAD request."""
|
|
||||||
view.request = self.req.head('/')
|
|
||||||
self.assertEqual(view.DATA, None)
|
|
||||||
|
|
||||||
def ensure_determines_form_content_POST(self, view):
|
|
||||||
"""Ensure view.DATA returns content for POST request with form content."""
|
|
||||||
form_data = {'qwerty': 'uiop'}
|
|
||||||
view.parsers = (FormParser, MultiPartParser)
|
|
||||||
view.request = self.req.post('/', data=form_data)
|
|
||||||
self.assertEqual(view.DATA.items(), form_data.items())
|
|
||||||
|
|
||||||
def ensure_determines_non_form_content_POST(self, view):
|
|
||||||
"""Ensure view.RAW_CONTENT returns content for POST request with non-form content."""
|
|
||||||
content = 'qwerty'
|
|
||||||
content_type = 'text/plain'
|
|
||||||
view.parsers = (PlainTextParser,)
|
|
||||||
view.request = self.req.post('/', content, content_type=content_type)
|
|
||||||
self.assertEqual(view.DATA, content)
|
|
||||||
|
|
||||||
def ensure_determines_form_content_PUT(self, view):
|
|
||||||
"""Ensure view.RAW_CONTENT returns content for PUT request with form content."""
|
|
||||||
form_data = {'qwerty': 'uiop'}
|
|
||||||
view.parsers = (FormParser, MultiPartParser)
|
|
||||||
view.request = self.req.put('/', data=form_data)
|
|
||||||
self.assertEqual(view.DATA.items(), form_data.items())
|
|
||||||
|
|
||||||
def ensure_determines_non_form_content_PUT(self, view):
|
|
||||||
"""Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
|
|
||||||
content = 'qwerty'
|
|
||||||
content_type = 'text/plain'
|
|
||||||
view.parsers = (PlainTextParser,)
|
|
||||||
view.request = self.req.post('/', content, content_type=content_type)
|
|
||||||
self.assertEqual(view.DATA, content)
|
|
||||||
|
|
||||||
def test_standard_behaviour_determines_no_content_GET(self):
|
|
||||||
"""Ensure view.DATA returns None for GET request with no content."""
|
|
||||||
self.ensure_determines_no_content_GET(RequestMixin())
|
|
||||||
|
|
||||||
def test_standard_behaviour_determines_no_content_HEAD(self):
|
|
||||||
"""Ensure view.DATA returns None for HEAD request."""
|
|
||||||
self.ensure_determines_no_content_HEAD(RequestMixin())
|
|
||||||
|
|
||||||
def test_standard_behaviour_determines_form_content_POST(self):
|
|
||||||
"""Ensure view.DATA returns content for POST request with form content."""
|
|
||||||
self.ensure_determines_form_content_POST(RequestMixin())
|
|
||||||
|
|
||||||
def test_standard_behaviour_determines_non_form_content_POST(self):
|
|
||||||
"""Ensure view.DATA returns content for POST request with non-form content."""
|
|
||||||
self.ensure_determines_non_form_content_POST(RequestMixin())
|
|
||||||
|
|
||||||
def test_standard_behaviour_determines_form_content_PUT(self):
|
|
||||||
"""Ensure view.DATA returns content for PUT request with form content."""
|
|
||||||
self.ensure_determines_form_content_PUT(RequestMixin())
|
|
||||||
|
|
||||||
def test_standard_behaviour_determines_non_form_content_PUT(self):
|
|
||||||
"""Ensure view.DATA returns content for PUT request with non-form content."""
|
|
||||||
self.ensure_determines_non_form_content_PUT(RequestMixin())
|
|
||||||
|
|
||||||
def test_overloaded_behaviour_allows_content_tunnelling(self):
|
|
||||||
"""Ensure request.DATA returns content for overloaded POST request"""
|
|
||||||
content = 'qwerty'
|
|
||||||
content_type = 'text/plain'
|
|
||||||
view = RequestMixin()
|
|
||||||
form_data = {view._CONTENT_PARAM: content,
|
|
||||||
view._CONTENTTYPE_PARAM: content_type}
|
|
||||||
view.request = self.req.post('/', form_data)
|
|
||||||
view.parsers = (PlainTextParser,)
|
|
||||||
self.assertEqual(view.DATA, content)
|
|
||||||
|
|
||||||
def test_accessing_post_after_data_form(self):
|
|
||||||
"""Ensures request.POST can be accessed after request.DATA in form request"""
|
|
||||||
form_data = {'qwerty': 'uiop'}
|
|
||||||
view = RequestMixin()
|
|
||||||
view.parsers = (FormParser, MultiPartParser)
|
|
||||||
view.request = self.req.post('/', data=form_data)
|
|
||||||
|
|
||||||
self.assertEqual(view.DATA.items(), form_data.items())
|
|
||||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
|
||||||
|
|
||||||
@unittest.skip('This test was disabled some time ago for some reason')
|
|
||||||
def test_accessing_post_after_data_for_json(self):
|
|
||||||
"""Ensures request.POST can be accessed after request.DATA in json request"""
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
|
|
||||||
data = {'qwerty': 'uiop'}
|
|
||||||
content = json.dumps(data)
|
|
||||||
content_type = 'application/json'
|
|
||||||
|
|
||||||
view = RequestMixin()
|
|
||||||
view.parsers = (JSONParser,)
|
|
||||||
|
|
||||||
view.request = self.req.post('/', content, content_type=content_type)
|
|
||||||
|
|
||||||
self.assertEqual(view.DATA.items(), data.items())
|
|
||||||
self.assertEqual(view.request.POST.items(), [])
|
|
||||||
|
|
||||||
def test_accessing_post_after_data_for_overloaded_json(self):
|
|
||||||
"""Ensures request.POST can be accessed after request.DATA in overloaded json request"""
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
|
|
||||||
data = {'qwerty': 'uiop'}
|
|
||||||
content = json.dumps(data)
|
|
||||||
content_type = 'application/json'
|
|
||||||
|
|
||||||
view = RequestMixin()
|
|
||||||
view.parsers = (JSONParser,)
|
|
||||||
|
|
||||||
form_data = {view._CONTENT_PARAM: content,
|
|
||||||
view._CONTENTTYPE_PARAM: content_type}
|
|
||||||
|
|
||||||
view.request = self.req.post('/', data=form_data)
|
|
||||||
|
|
||||||
self.assertEqual(view.DATA.items(), data.items())
|
|
||||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
|
||||||
|
|
||||||
def test_accessing_data_after_post_form(self):
|
|
||||||
"""Ensures request.DATA can be accessed after request.POST in form request"""
|
|
||||||
form_data = {'qwerty': 'uiop'}
|
|
||||||
view = RequestMixin()
|
|
||||||
view.parsers = (FormParser, MultiPartParser)
|
|
||||||
view.request = self.req.post('/', data=form_data)
|
|
||||||
|
|
||||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
|
||||||
self.assertEqual(view.DATA.items(), form_data.items())
|
|
||||||
|
|
||||||
def test_accessing_data_after_post_for_json(self):
|
|
||||||
"""Ensures request.DATA can be accessed after request.POST in json request"""
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
|
|
||||||
data = {'qwerty': 'uiop'}
|
|
||||||
content = json.dumps(data)
|
|
||||||
content_type = 'application/json'
|
|
||||||
|
|
||||||
view = RequestMixin()
|
|
||||||
view.parsers = (JSONParser,)
|
|
||||||
|
|
||||||
view.request = self.req.post('/', content, content_type=content_type)
|
|
||||||
|
|
||||||
post_items = view.request.POST.items()
|
|
||||||
|
|
||||||
self.assertEqual(len(post_items), 1)
|
|
||||||
self.assertEqual(len(post_items[0]), 2)
|
|
||||||
self.assertEqual(post_items[0][0], content)
|
|
||||||
self.assertEqual(view.DATA.items(), data.items())
|
|
||||||
|
|
||||||
def test_accessing_data_after_post_for_overloaded_json(self):
|
|
||||||
"""Ensures request.DATA can be accessed after request.POST in overloaded json request"""
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
|
|
||||||
data = {'qwerty': 'uiop'}
|
|
||||||
content = json.dumps(data)
|
|
||||||
content_type = 'application/json'
|
|
||||||
|
|
||||||
view = RequestMixin()
|
|
||||||
view.parsers = (JSONParser,)
|
|
||||||
|
|
||||||
form_data = {view._CONTENT_PARAM: content,
|
|
||||||
view._CONTENTTYPE_PARAM: content_type}
|
|
||||||
|
|
||||||
view.request = self.req.post('/', data=form_data)
|
|
||||||
|
|
||||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
|
||||||
self.assertEqual(view.DATA.items(), data.items())
|
|
||||||
|
|
||||||
class TestContentParsingWithAuthentication(TestCase):
|
|
||||||
urls = 'djangorestframework.tests.content'
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
|
||||||
self.username = 'john'
|
|
||||||
self.email = 'lennon@thebeatles.com'
|
|
||||||
self.password = 'password'
|
|
||||||
self.user = User.objects.create_user(self.username, self.email, self.password)
|
|
||||||
self.req = RequestFactory()
|
|
||||||
|
|
||||||
def test_user_logged_in_authentication_has_post_when_not_logged_in(self):
|
|
||||||
"""Ensures request.POST exists after UserLoggedInAuthentication when user doesn't log in"""
|
|
||||||
content = {'example': 'example'}
|
|
||||||
|
|
||||||
response = self.client.post('/', content)
|
|
||||||
self.assertEqual(status.HTTP_200_OK, response.status_code, "POST data is malformed")
|
|
||||||
|
|
||||||
response = self.csrf_client.post('/', content)
|
|
||||||
self.assertEqual(status.HTTP_200_OK, response.status_code, "POST data is malformed")
|
|
||||||
|
|
||||||
# def test_user_logged_in_authentication_has_post_when_logged_in(self):
|
|
||||||
# """Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
|
|
||||||
# self.client.login(username='john', password='password')
|
|
||||||
# self.csrf_client.login(username='john', password='password')
|
|
||||||
# content = {'example': 'example'}
|
|
||||||
|
|
||||||
# response = self.client.post('/', content)
|
|
||||||
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
|
||||||
|
|
||||||
# response = self.csrf_client.post('/', content)
|
|
||||||
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
|
|
@ -1,8 +1,11 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
from djangorestframework.resources import FormResource
|
from djangorestframework.resources import FormResource
|
||||||
|
from djangorestframework.response import Response
|
||||||
|
|
||||||
import StringIO
|
import StringIO
|
||||||
|
|
||||||
class UploadFilesTests(TestCase):
|
class UploadFilesTests(TestCase):
|
||||||
|
@ -20,13 +23,13 @@ class UploadFilesTests(TestCase):
|
||||||
form = FileForm
|
form = FileForm
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
return {'FILE_NAME': self.CONTENT['file'].name,
|
return Response({'FILE_NAME': self.CONTENT['file'].name,
|
||||||
'FILE_CONTENT': self.CONTENT['file'].read()}
|
'FILE_CONTENT': self.CONTENT['file'].read()})
|
||||||
|
|
||||||
file = StringIO.StringIO('stuff')
|
file = StringIO.StringIO('stuff')
|
||||||
file.name = 'stuff.txt'
|
file.name = 'stuff.txt'
|
||||||
request = self.factory.post('/', {'file': file})
|
request = self.factory.post('/', {'file': file})
|
||||||
view = MockView.as_view()
|
view = MockView.as_view()
|
||||||
response = view(request)
|
response = view(request)
|
||||||
self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}')
|
self.assertEquals(response.raw_content, {"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"})
|
||||||
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
from djangorestframework.compat import RequestFactory
|
|
||||||
from djangorestframework.mixins import RequestMixin
|
|
||||||
|
|
||||||
|
|
||||||
class TestMethodOverloading(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.req = RequestFactory()
|
|
||||||
|
|
||||||
def test_standard_behaviour_determines_GET(self):
|
|
||||||
"""GET requests identified"""
|
|
||||||
view = RequestMixin()
|
|
||||||
view.request = self.req.get('/')
|
|
||||||
self.assertEqual(view.method, 'GET')
|
|
||||||
|
|
||||||
def test_standard_behaviour_determines_POST(self):
|
|
||||||
"""POST requests identified"""
|
|
||||||
view = RequestMixin()
|
|
||||||
view.request = self.req.post('/')
|
|
||||||
self.assertEqual(view.method, 'POST')
|
|
||||||
|
|
||||||
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
|
|
||||||
"""POST requests can be overloaded to another method by setting a reserved form field"""
|
|
||||||
view = RequestMixin()
|
|
||||||
view.request = self.req.post('/', {view._METHOD_PARAM: 'DELETE'})
|
|
||||||
self.assertEqual(view.method, 'DELETE')
|
|
||||||
|
|
||||||
def test_HEAD_is_a_valid_method(self):
|
|
||||||
"""HEAD requests identified"""
|
|
||||||
view = RequestMixin()
|
|
||||||
view.request = self.req.head('/')
|
|
||||||
self.assertEqual(view.method, 'HEAD')
|
|
|
@ -6,7 +6,7 @@ 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, ReadModelMixin
|
from djangorestframework.mixins import CreateModelMixin, PaginatorMixin, ReadModelMixin
|
||||||
from djangorestframework.resources import ModelResource
|
from djangorestframework.resources import ModelResource
|
||||||
from djangorestframework.response import Response, ErrorResponse
|
from djangorestframework.response import Response, ImmediateResponse
|
||||||
from djangorestframework.tests.models import CustomUser
|
from djangorestframework.tests.models import CustomUser
|
||||||
from djangorestframework.tests.testcases import TestModelsTestCase
|
from djangorestframework.tests.testcases import TestModelsTestCase
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
|
@ -31,7 +31,7 @@ class TestModelRead(TestModelsTestCase):
|
||||||
mixin.resource = GroupResource
|
mixin.resource = GroupResource
|
||||||
|
|
||||||
response = mixin.get(request, id=group.id)
|
response = mixin.get(request, id=group.id)
|
||||||
self.assertEquals(group.name, response.name)
|
self.assertEquals(group.name, response.raw_content.name)
|
||||||
|
|
||||||
def test_read_404(self):
|
def test_read_404(self):
|
||||||
class GroupResource(ModelResource):
|
class GroupResource(ModelResource):
|
||||||
|
@ -41,7 +41,7 @@ class TestModelRead(TestModelsTestCase):
|
||||||
mixin = ReadModelMixin()
|
mixin = ReadModelMixin()
|
||||||
mixin.resource = GroupResource
|
mixin.resource = GroupResource
|
||||||
|
|
||||||
self.assertRaises(ErrorResponse, mixin.get, request, id=12345)
|
self.assertRaises(ImmediateResponse, mixin.get, request, id=12345)
|
||||||
|
|
||||||
|
|
||||||
class TestModelCreation(TestModelsTestCase):
|
class TestModelCreation(TestModelsTestCase):
|
||||||
|
@ -65,7 +65,7 @@ class TestModelCreation(TestModelsTestCase):
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.post(request)
|
||||||
self.assertEquals(1, Group.objects.count())
|
self.assertEquals(1, Group.objects.count())
|
||||||
self.assertEquals('foo', response.cleaned_content.name)
|
self.assertEquals('foo', response.raw_content.name)
|
||||||
|
|
||||||
def test_creation_with_m2m_relation(self):
|
def test_creation_with_m2m_relation(self):
|
||||||
class UserResource(ModelResource):
|
class UserResource(ModelResource):
|
||||||
|
@ -91,8 +91,8 @@ class TestModelCreation(TestModelsTestCase):
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.post(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.raw_content.groups.count())
|
||||||
self.assertEquals('foo', response.cleaned_content.groups.all()[0].name)
|
self.assertEquals('foo', response.raw_content.groups.all()[0].name)
|
||||||
|
|
||||||
def test_creation_with_m2m_relation_through(self):
|
def test_creation_with_m2m_relation_through(self):
|
||||||
"""
|
"""
|
||||||
|
@ -114,7 +114,7 @@ class TestModelCreation(TestModelsTestCase):
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.post(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.raw_content.groups.count())
|
||||||
|
|
||||||
group = Group(name='foo1')
|
group = Group(name='foo1')
|
||||||
group.save()
|
group.save()
|
||||||
|
@ -129,8 +129,8 @@ class TestModelCreation(TestModelsTestCase):
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.post(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.raw_content.groups.count())
|
||||||
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
self.assertEquals('foo1', response.raw_content.groups.all()[0].name)
|
||||||
|
|
||||||
group2 = Group(name='foo2')
|
group2 = Group(name='foo2')
|
||||||
group2.save()
|
group2.save()
|
||||||
|
@ -145,19 +145,19 @@ class TestModelCreation(TestModelsTestCase):
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.post(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.raw_content.groups.count())
|
||||||
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
self.assertEquals('foo1', response.raw_content.groups.all()[0].name)
|
||||||
self.assertEquals('foo2', response.cleaned_content.groups.all()[1].name)
|
self.assertEquals('foo2', response.raw_content.groups.all()[1].name)
|
||||||
|
|
||||||
|
|
||||||
class MockPaginatorView(PaginatorMixin, View):
|
class MockPaginatorView(PaginatorMixin, View):
|
||||||
total = 60
|
total = 60
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return range(0, self.total)
|
return Response(range(0, self.total))
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
return Response(status.HTTP_201_CREATED, {'status': 'OK'})
|
return Response({'status': 'OK'}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
class TestPagination(TestCase):
|
class TestPagination(TestCase):
|
||||||
|
@ -168,8 +168,7 @@ class TestPagination(TestCase):
|
||||||
""" Tests if pagination works without overwriting the limit """
|
""" Tests if pagination works without overwriting the limit """
|
||||||
request = self.req.get('/paginator')
|
request = self.req.get('/paginator')
|
||||||
response = MockPaginatorView.as_view()(request)
|
response = MockPaginatorView.as_view()(request)
|
||||||
|
content = response.raw_content
|
||||||
content = json.loads(response.content)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(MockPaginatorView.total, content['total'])
|
self.assertEqual(MockPaginatorView.total, content['total'])
|
||||||
|
@ -183,8 +182,7 @@ class TestPagination(TestCase):
|
||||||
|
|
||||||
request = self.req.get('/paginator')
|
request = self.req.get('/paginator')
|
||||||
response = MockPaginatorView.as_view(limit=limit)(request)
|
response = MockPaginatorView.as_view(limit=limit)(request)
|
||||||
|
content = response.raw_content
|
||||||
content = json.loads(response.content)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(content['per_page'], limit)
|
self.assertEqual(content['per_page'], limit)
|
||||||
|
@ -200,8 +198,7 @@ class TestPagination(TestCase):
|
||||||
|
|
||||||
request = self.req.get('/paginator/?limit=%d' % limit)
|
request = self.req.get('/paginator/?limit=%d' % limit)
|
||||||
response = MockPaginatorView.as_view()(request)
|
response = MockPaginatorView.as_view()(request)
|
||||||
|
content = response.raw_content
|
||||||
content = json.loads(response.content)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(MockPaginatorView.total, content['total'])
|
self.assertEqual(MockPaginatorView.total, content['total'])
|
||||||
|
@ -217,8 +214,7 @@ class TestPagination(TestCase):
|
||||||
|
|
||||||
request = self.req.get('/paginator/?limit=%d' % limit)
|
request = self.req.get('/paginator/?limit=%d' % limit)
|
||||||
response = MockPaginatorView.as_view()(request)
|
response = MockPaginatorView.as_view()(request)
|
||||||
|
content = response.raw_content
|
||||||
content = json.loads(response.content)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(MockPaginatorView.total, content['total'])
|
self.assertEqual(MockPaginatorView.total, content['total'])
|
||||||
|
@ -230,8 +226,7 @@ class TestPagination(TestCase):
|
||||||
""" Pagination should only work for GET requests """
|
""" Pagination should only work for GET requests """
|
||||||
request = self.req.post('/paginator', data={'content': 'spam'})
|
request = self.req.post('/paginator', data={'content': 'spam'})
|
||||||
response = MockPaginatorView.as_view()(request)
|
response = MockPaginatorView.as_view()(request)
|
||||||
|
content = response.raw_content
|
||||||
content = json.loads(response.content)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(None, content.get('per_page'))
|
self.assertEqual(None, content.get('per_page'))
|
||||||
|
@ -248,12 +243,12 @@ class TestPagination(TestCase):
|
||||||
""" Tests that the page range is handle correctly """
|
""" Tests that the page range is handle correctly """
|
||||||
request = self.req.get('/paginator/?page=0')
|
request = self.req.get('/paginator/?page=0')
|
||||||
response = MockPaginatorView.as_view()(request)
|
response = MockPaginatorView.as_view()(request)
|
||||||
content = json.loads(response.content)
|
content = response.raw_content
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
request = self.req.get('/paginator/')
|
request = self.req.get('/paginator/')
|
||||||
response = MockPaginatorView.as_view()(request)
|
response = MockPaginatorView.as_view()(request)
|
||||||
content = json.loads(response.content)
|
content = response.raw_content
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
|
self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
|
||||||
|
|
||||||
|
@ -261,13 +256,13 @@ class TestPagination(TestCase):
|
||||||
|
|
||||||
request = self.req.get('/paginator/?page=%d' % num_pages)
|
request = self.req.get('/paginator/?page=%d' % num_pages)
|
||||||
response = MockPaginatorView.as_view()(request)
|
response = MockPaginatorView.as_view()(request)
|
||||||
content = json.loads(response.content)
|
content = response.raw_content
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(range(MockPaginatorView.limit*(num_pages-1), MockPaginatorView.total), content['results'])
|
self.assertEqual(range(MockPaginatorView.limit*(num_pages-1), MockPaginatorView.total), content['results'])
|
||||||
|
|
||||||
request = self.req.get('/paginator/?page=%d' % (num_pages + 1,))
|
request = self.req.get('/paginator/?page=%d' % (num_pages + 1,))
|
||||||
response = MockPaginatorView.as_view()(request)
|
response = MockPaginatorView.as_view()(request)
|
||||||
content = json.loads(response.content)
|
content = response.raw_content
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
def test_existing_query_parameters_are_preserved(self):
|
def test_existing_query_parameters_are_preserved(self):
|
||||||
|
@ -275,7 +270,7 @@ class TestPagination(TestCase):
|
||||||
generating next/previous page links """
|
generating next/previous page links """
|
||||||
request = self.req.get('/paginator/?foo=bar&another=something')
|
request = self.req.get('/paginator/?foo=bar&another=something')
|
||||||
response = MockPaginatorView.as_view()(request)
|
response = MockPaginatorView.as_view()(request)
|
||||||
content = json.loads(response.content)
|
content = response.raw_content
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertTrue('foo=bar' in content['next'])
|
self.assertTrue('foo=bar' in content['next'])
|
||||||
self.assertTrue('another=something' in content['next'])
|
self.assertTrue('another=something' in content['next'])
|
||||||
|
@ -286,6 +281,6 @@ class TestPagination(TestCase):
|
||||||
paginated URLs. So page 1 should contain ?page=2, not ?page=1&page=2 """
|
paginated URLs. So page 1 should contain ?page=2, not ?page=1&page=2 """
|
||||||
request = self.req.get('/paginator/?page=1')
|
request = self.req.get('/paginator/?page=1')
|
||||||
response = MockPaginatorView.as_view()(request)
|
response = MockPaginatorView.as_view()(request)
|
||||||
content = json.loads(response.content)
|
content = response.raw_content
|
||||||
self.assertTrue('page=2' in content['next'])
|
self.assertTrue('page=2' in content['next'])
|
||||||
self.assertFalse('page=1' in content['next'])
|
self.assertFalse('page=1' in content['next'])
|
||||||
|
|
|
@ -132,30 +132,32 @@
|
||||||
# self.assertEqual(files['file1'].read(), 'blablabla')
|
# self.assertEqual(files['file1'].read(), 'blablabla')
|
||||||
|
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
from cgi import parse_qs
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.parsers import FormParser
|
from djangorestframework.parsers import FormParser
|
||||||
from djangorestframework.parsers import XMLParser
|
from djangorestframework.parsers import XMLParser
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
class Form(forms.Form):
|
class Form(forms.Form):
|
||||||
field1 = forms.CharField(max_length=3)
|
field1 = forms.CharField(max_length=3)
|
||||||
field2 = forms.CharField()
|
field2 = forms.CharField()
|
||||||
|
|
||||||
|
|
||||||
class TestFormParser(TestCase):
|
class TestFormParser(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.string = "field1=abc&field2=defghijk"
|
self.string = "field1=abc&field2=defghijk"
|
||||||
|
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
""" Make sure the `QueryDict` works OK """
|
""" Make sure the `QueryDict` works OK """
|
||||||
parser = FormParser(None)
|
parser = FormParser()
|
||||||
|
|
||||||
stream = StringIO(self.string)
|
stream = StringIO(self.string)
|
||||||
(data, files) = parser.parse(stream)
|
(data, files) = parser.parse(stream, {}, [])
|
||||||
|
|
||||||
self.assertEqual(Form(data).is_valid(), True)
|
self.assertEqual(Form(data).is_valid(), True)
|
||||||
|
|
||||||
|
|
||||||
class TestXMLParser(TestCase):
|
class TestXMLParser(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self._input = StringIO(
|
self._input = StringIO(
|
||||||
|
@ -163,13 +165,13 @@ class TestXMLParser(TestCase):
|
||||||
'<root>'
|
'<root>'
|
||||||
'<field_a>121.0</field_a>'
|
'<field_a>121.0</field_a>'
|
||||||
'<field_b>dasd</field_b>'
|
'<field_b>dasd</field_b>'
|
||||||
'<field_c></field_c>'
|
'<field_c></field_c>'
|
||||||
'<field_d>2011-12-25 12:45:00</field_d>'
|
'<field_d>2011-12-25 12:45:00</field_d>'
|
||||||
'</root>'
|
'</root>'
|
||||||
)
|
)
|
||||||
self._data = {
|
self._data = {
|
||||||
'field_a': 121,
|
'field_a': 121,
|
||||||
'field_b': 'dasd',
|
'field_b': 'dasd',
|
||||||
'field_c': None,
|
'field_c': None,
|
||||||
'field_d': datetime.datetime(2011, 12, 25, 12, 45, 00)
|
'field_d': datetime.datetime(2011, 12, 25, 12, 45, 00)
|
||||||
}
|
}
|
||||||
|
@ -183,28 +185,28 @@ class TestXMLParser(TestCase):
|
||||||
'</sub_data_list>'
|
'</sub_data_list>'
|
||||||
'<name>name</name>'
|
'<name>name</name>'
|
||||||
'</root>'
|
'</root>'
|
||||||
)
|
)
|
||||||
self._complex_data = {
|
self._complex_data = {
|
||||||
"creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
|
"creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"sub_data_list": [
|
"sub_data_list": [
|
||||||
{
|
{
|
||||||
"sub_id": 1,
|
"sub_id": 1,
|
||||||
"sub_name": "first"
|
"sub_name": "first"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"sub_id": 2,
|
"sub_id": 2,
|
||||||
"sub_name": "second"
|
"sub_name": "second"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
parser = XMLParser(None)
|
parser = XMLParser()
|
||||||
(data, files) = parser.parse(self._input)
|
(data, files) = parser.parse(self._input, {}, [])
|
||||||
self.assertEqual(data, self._data)
|
self.assertEqual(data, self._data)
|
||||||
|
|
||||||
def test_complex_data_parse(self):
|
def test_complex_data_parse(self):
|
||||||
parser = XMLParser(None)
|
parser = XMLParser()
|
||||||
(data, files) = parser.parse(self._complex_data_input)
|
(data, files) = parser.parse(self._complex_data_input, {}, [])
|
||||||
self.assertEqual(data, self._complex_data)
|
self.assertEqual(data, self._complex_data)
|
||||||
|
|
|
@ -4,18 +4,19 @@ from django.conf.urls.defaults import patterns, url, include
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
from djangorestframework.views import View
|
|
||||||
from djangorestframework.compat import View as DjangoView
|
from djangorestframework.compat import View as DjangoView
|
||||||
|
from djangorestframework.response import Response
|
||||||
|
from djangorestframework.mixins import ResponseMixin
|
||||||
|
from djangorestframework.views import View
|
||||||
from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
||||||
XMLRenderer, JSONPRenderer, DocumentingHTMLRenderer
|
XMLRenderer, JSONPRenderer, DocumentingHTMLRenderer
|
||||||
from djangorestframework.parsers import JSONParser, YAMLParser, XMLParser
|
from djangorestframework.parsers import YAMLParser, XMLParser
|
||||||
from djangorestframework.mixins import ResponseMixin
|
|
||||||
from djangorestframework.response import Response
|
|
||||||
|
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
DUMMYSTATUS = status.HTTP_200_OK
|
DUMMYSTATUS = status.HTTP_200_OK
|
||||||
DUMMYCONTENT = 'dummycontent'
|
DUMMYCONTENT = 'dummycontent'
|
||||||
|
|
||||||
|
@ -57,14 +58,14 @@ class HTMLView(View):
|
||||||
renderers = (DocumentingHTMLRenderer, )
|
renderers = (DocumentingHTMLRenderer, )
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
return 'text'
|
return 'text'
|
||||||
|
|
||||||
|
|
||||||
class HTMLView1(View):
|
class HTMLView1(View):
|
||||||
renderers = (DocumentingHTMLRenderer, JSONRenderer)
|
renderers = (DocumentingHTMLRenderer, JSONRenderer)
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
return 'text'
|
return 'text'
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
|
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
|
||||||
|
@ -167,7 +168,7 @@ class RendererIntegrationTests(TestCase):
|
||||||
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
def test_bla(self):
|
def test_bla(self): # What the f***?
|
||||||
resp = self.client.get('/?format=formatb',
|
resp = self.client.get('/?format=formatb',
|
||||||
HTTP_ACCEPT='text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8')
|
HTTP_ACCEPT='text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8')
|
||||||
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
@ -185,6 +186,7 @@ def strip_trailing_whitespace(content):
|
||||||
"""
|
"""
|
||||||
return re.sub(' +\n', '\n', content)
|
return re.sub(' +\n', '\n', content)
|
||||||
|
|
||||||
|
|
||||||
class JSONRendererTests(TestCase):
|
class JSONRendererTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests specific to the JSON Renderer
|
Tests specific to the JSON Renderer
|
||||||
|
@ -209,19 +211,17 @@ class JSONRendererTests(TestCase):
|
||||||
content = renderer.render(obj, 'application/json; indent=2')
|
content = renderer.render(obj, 'application/json; indent=2')
|
||||||
self.assertEquals(strip_trailing_whitespace(content), _indented_repr)
|
self.assertEquals(strip_trailing_whitespace(content), _indented_repr)
|
||||||
|
|
||||||
def test_render_and_parse(self):
|
|
||||||
"""
|
|
||||||
Test rendering and then parsing returns the original object.
|
|
||||||
IE obj -> render -> parse -> obj.
|
|
||||||
"""
|
|
||||||
obj = {'foo': ['bar', 'baz']}
|
|
||||||
|
|
||||||
renderer = JSONRenderer(None)
|
class MockGETView(View):
|
||||||
parser = JSONParser(None)
|
|
||||||
|
|
||||||
content = renderer.render(obj, 'application/json')
|
def get(self, request, *args, **kwargs):
|
||||||
(data, files) = parser.parse(StringIO(content))
|
return Response({'foo': ['bar', 'baz']})
|
||||||
self.assertEquals(obj, data)
|
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderers=[JSONRenderer, JSONPRenderer])),
|
||||||
|
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderers=[JSONPRenderer])),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class JSONPRendererTests(TestCase):
|
class JSONPRendererTests(TestCase):
|
||||||
|
@ -295,22 +295,21 @@ if YAMLRenderer:
|
||||||
self.assertEquals(obj, data)
|
self.assertEquals(obj, data)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class XMLRendererTestCase(TestCase):
|
class XMLRendererTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests specific to the XML Renderer
|
Tests specific to the XML Renderer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_complex_data = {
|
_complex_data = {
|
||||||
"creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
|
"creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"sub_data_list": [
|
"sub_data_list": [
|
||||||
{
|
{
|
||||||
"sub_id": 1,
|
"sub_id": 1,
|
||||||
"sub_name": "first"
|
"sub_name": "first"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"sub_id": 2,
|
"sub_id": 2,
|
||||||
"sub_name": "second"
|
"sub_name": "second"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -365,12 +364,12 @@ class XMLRendererTestCase(TestCase):
|
||||||
renderer = XMLRenderer(None)
|
renderer = XMLRenderer(None)
|
||||||
content = renderer.render({'field': None}, 'application/xml')
|
content = renderer.render({'field': None}, 'application/xml')
|
||||||
self.assertXMLContains(content, '<field></field>')
|
self.assertXMLContains(content, '<field></field>')
|
||||||
|
|
||||||
def test_render_complex_data(self):
|
def test_render_complex_data(self):
|
||||||
"""
|
"""
|
||||||
Test XML rendering.
|
Test XML rendering.
|
||||||
"""
|
"""
|
||||||
renderer = XMLRenderer(None)
|
renderer = XMLRenderer(None)
|
||||||
content = renderer.render(self._complex_data, 'application/xml')
|
content = renderer.render(self._complex_data, 'application/xml')
|
||||||
self.assertXMLContains(content, '<sub_name>first</sub_name>')
|
self.assertXMLContains(content, '<sub_name>first</sub_name>')
|
||||||
self.assertXMLContains(content, '<sub_name>second</sub_name>')
|
self.assertXMLContains(content, '<sub_name>second</sub_name>')
|
||||||
|
@ -379,11 +378,11 @@ class XMLRendererTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
Test XML rendering.
|
Test XML rendering.
|
||||||
"""
|
"""
|
||||||
renderer = XMLRenderer(None)
|
renderer = XMLRenderer(None)
|
||||||
content = StringIO(renderer.render(self._complex_data, 'application/xml'))
|
content = StringIO(renderer.render(self._complex_data, 'application/xml'))
|
||||||
|
|
||||||
parser = XMLParser(None)
|
parser = XMLParser()
|
||||||
complex_data_out, dummy = parser.parse(content)
|
complex_data_out, dummy = parser.parse(content, {}, [])
|
||||||
error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out))
|
error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out))
|
||||||
self.assertEqual(self._complex_data, complex_data_out, error_msg)
|
self.assertEqual(self._complex_data, complex_data_out, error_msg)
|
||||||
|
|
||||||
|
@ -391,22 +390,3 @@ class XMLRendererTestCase(TestCase):
|
||||||
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
|
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
|
||||||
self.assertTrue(xml.endswith('</root>'))
|
self.assertTrue(xml.endswith('</root>'))
|
||||||
self.assertTrue(string in xml, '%r not in %r' % (string, xml))
|
self.assertTrue(string in xml, '%r not in %r' % (string, xml))
|
||||||
|
|
||||||
|
|
||||||
class Issue122Tests(TestCase):
|
|
||||||
"""
|
|
||||||
Tests that covers #122.
|
|
||||||
"""
|
|
||||||
urls = 'djangorestframework.tests.renderers'
|
|
||||||
|
|
||||||
def test_only_html_renderer(self):
|
|
||||||
"""
|
|
||||||
Test if no infinite recursion occurs.
|
|
||||||
"""
|
|
||||||
resp = self.client.get('/html')
|
|
||||||
|
|
||||||
def test_html_renderer_is_first(self):
|
|
||||||
"""
|
|
||||||
Test if no infinite recursion occurs.
|
|
||||||
"""
|
|
||||||
resp = self.client.get('/html1')
|
|
||||||
|
|
259
djangorestframework/tests/request.py
Normal file
259
djangorestframework/tests/request.py
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
"""
|
||||||
|
Tests for content parsing, and form-overloaded content parsing.
|
||||||
|
"""
|
||||||
|
from django.conf.urls.defaults import patterns
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
|
from djangorestframework import status
|
||||||
|
from djangorestframework.authentication import UserLoggedInAuthentication
|
||||||
|
from djangorestframework.utils import RequestFactory
|
||||||
|
from djangorestframework.parsers import (
|
||||||
|
FormParser,
|
||||||
|
MultiPartParser,
|
||||||
|
PlainTextParser,
|
||||||
|
JSONParser
|
||||||
|
)
|
||||||
|
from djangorestframework.request import Request
|
||||||
|
from djangorestframework.response import Response
|
||||||
|
from djangorestframework.views import View
|
||||||
|
|
||||||
|
|
||||||
|
factory = RequestFactory()
|
||||||
|
|
||||||
|
|
||||||
|
class TestMethodOverloading(TestCase):
|
||||||
|
def test_GET_method(self):
|
||||||
|
"""
|
||||||
|
GET requests identified.
|
||||||
|
"""
|
||||||
|
request = factory.get('/')
|
||||||
|
self.assertEqual(request.method, 'GET')
|
||||||
|
|
||||||
|
def test_POST_method(self):
|
||||||
|
"""
|
||||||
|
POST requests identified.
|
||||||
|
"""
|
||||||
|
request = factory.post('/')
|
||||||
|
self.assertEqual(request.method, 'POST')
|
||||||
|
|
||||||
|
def test_HEAD_method(self):
|
||||||
|
"""
|
||||||
|
HEAD requests identified.
|
||||||
|
"""
|
||||||
|
request = factory.head('/')
|
||||||
|
self.assertEqual(request.method, 'HEAD')
|
||||||
|
|
||||||
|
def test_overloaded_method(self):
|
||||||
|
"""
|
||||||
|
POST requests can be overloaded to another method by setting a
|
||||||
|
reserved form field
|
||||||
|
"""
|
||||||
|
request = factory.post('/', {Request._METHOD_PARAM: 'DELETE'})
|
||||||
|
self.assertEqual(request.method, 'DELETE')
|
||||||
|
|
||||||
|
|
||||||
|
class TestContentParsing(TestCase):
|
||||||
|
def test_standard_behaviour_determines_no_content_GET(self):
|
||||||
|
"""
|
||||||
|
Ensure request.DATA returns None for GET request with no content.
|
||||||
|
"""
|
||||||
|
request = factory.get('/')
|
||||||
|
self.assertEqual(request.DATA, None)
|
||||||
|
|
||||||
|
def test_standard_behaviour_determines_no_content_HEAD(self):
|
||||||
|
"""
|
||||||
|
Ensure request.DATA returns None for HEAD request.
|
||||||
|
"""
|
||||||
|
request = factory.head('/')
|
||||||
|
self.assertEqual(request.DATA, None)
|
||||||
|
|
||||||
|
def test_standard_behaviour_determines_form_content_POST(self):
|
||||||
|
"""
|
||||||
|
Ensure request.DATA returns content for POST request with form content.
|
||||||
|
"""
|
||||||
|
data = {'qwerty': 'uiop'}
|
||||||
|
parsers = (FormParser, MultiPartParser)
|
||||||
|
request = factory.post('/', data, parser=parsers)
|
||||||
|
self.assertEqual(request.DATA.items(), data.items())
|
||||||
|
|
||||||
|
def test_standard_behaviour_determines_non_form_content_POST(self):
|
||||||
|
"""
|
||||||
|
Ensure request.DATA returns content for POST request with
|
||||||
|
non-form content.
|
||||||
|
"""
|
||||||
|
content = 'qwerty'
|
||||||
|
content_type = 'text/plain'
|
||||||
|
parsers = (PlainTextParser,)
|
||||||
|
request = factory.post('/', content, content_type=content_type,
|
||||||
|
parsers=parsers)
|
||||||
|
self.assertEqual(request.DATA, content)
|
||||||
|
|
||||||
|
def test_standard_behaviour_determines_form_content_PUT(self):
|
||||||
|
"""
|
||||||
|
Ensure request.DATA returns content for PUT request with form content.
|
||||||
|
"""
|
||||||
|
data = {'qwerty': 'uiop'}
|
||||||
|
parsers = (FormParser, MultiPartParser)
|
||||||
|
request = factory.put('/', data, parsers=parsers)
|
||||||
|
self.assertEqual(request.DATA.items(), data.items())
|
||||||
|
|
||||||
|
def test_standard_behaviour_determines_non_form_content_PUT(self):
|
||||||
|
"""
|
||||||
|
Ensure request.DATA returns content for PUT request with
|
||||||
|
non-form content.
|
||||||
|
"""
|
||||||
|
content = 'qwerty'
|
||||||
|
content_type = 'text/plain'
|
||||||
|
parsers = (PlainTextParser, )
|
||||||
|
request = factory.put('/', content, content_type=content_type,
|
||||||
|
parsers=parsers)
|
||||||
|
self.assertEqual(request.DATA, content)
|
||||||
|
|
||||||
|
def test_overloaded_behaviour_allows_content_tunnelling(self):
|
||||||
|
"""
|
||||||
|
Ensure request.DATA returns content for overloaded POST request.
|
||||||
|
"""
|
||||||
|
content = 'qwerty'
|
||||||
|
content_type = 'text/plain'
|
||||||
|
data = {
|
||||||
|
Request._CONTENT_PARAM: content,
|
||||||
|
Request._CONTENTTYPE_PARAM: content_type
|
||||||
|
}
|
||||||
|
parsers = (PlainTextParser, )
|
||||||
|
request = factory.post('/', data, parsers=parsers)
|
||||||
|
self.assertEqual(request.DATA, content)
|
||||||
|
|
||||||
|
# def test_accessing_post_after_data_form(self):
|
||||||
|
# """
|
||||||
|
# Ensures request.POST can be accessed after request.DATA in
|
||||||
|
# form request.
|
||||||
|
# """
|
||||||
|
# data = {'qwerty': 'uiop'}
|
||||||
|
# request = factory.post('/', data=data)
|
||||||
|
# self.assertEqual(request.DATA.items(), data.items())
|
||||||
|
# self.assertEqual(request.POST.items(), data.items())
|
||||||
|
|
||||||
|
# def test_accessing_post_after_data_for_json(self):
|
||||||
|
# """
|
||||||
|
# Ensures request.POST can be accessed after request.DATA in
|
||||||
|
# json request.
|
||||||
|
# """
|
||||||
|
# data = {'qwerty': 'uiop'}
|
||||||
|
# content = json.dumps(data)
|
||||||
|
# content_type = 'application/json'
|
||||||
|
# parsers = (JSONParser, )
|
||||||
|
|
||||||
|
# request = factory.post('/', content, content_type=content_type,
|
||||||
|
# parsers=parsers)
|
||||||
|
# self.assertEqual(request.DATA.items(), data.items())
|
||||||
|
# self.assertEqual(request.POST.items(), [])
|
||||||
|
|
||||||
|
# def test_accessing_post_after_data_for_overloaded_json(self):
|
||||||
|
# """
|
||||||
|
# Ensures request.POST can be accessed after request.DATA in overloaded
|
||||||
|
# json request.
|
||||||
|
# """
|
||||||
|
# data = {'qwerty': 'uiop'}
|
||||||
|
# content = json.dumps(data)
|
||||||
|
# content_type = 'application/json'
|
||||||
|
# parsers = (JSONParser, )
|
||||||
|
# form_data = {Request._CONTENT_PARAM: content,
|
||||||
|
# Request._CONTENTTYPE_PARAM: content_type}
|
||||||
|
|
||||||
|
# request = factory.post('/', form_data, parsers=parsers)
|
||||||
|
# self.assertEqual(request.DATA.items(), data.items())
|
||||||
|
# self.assertEqual(request.POST.items(), form_data.items())
|
||||||
|
|
||||||
|
# def test_accessing_data_after_post_form(self):
|
||||||
|
# """
|
||||||
|
# Ensures request.DATA can be accessed after request.POST in
|
||||||
|
# form request.
|
||||||
|
# """
|
||||||
|
# data = {'qwerty': 'uiop'}
|
||||||
|
# parsers = (FormParser, MultiPartParser)
|
||||||
|
# request = factory.post('/', data, parsers=parsers)
|
||||||
|
|
||||||
|
# self.assertEqual(request.POST.items(), data.items())
|
||||||
|
# self.assertEqual(request.DATA.items(), data.items())
|
||||||
|
|
||||||
|
# def test_accessing_data_after_post_for_json(self):
|
||||||
|
# """
|
||||||
|
# Ensures request.DATA can be accessed after request.POST in
|
||||||
|
# json request.
|
||||||
|
# """
|
||||||
|
# data = {'qwerty': 'uiop'}
|
||||||
|
# content = json.dumps(data)
|
||||||
|
# content_type = 'application/json'
|
||||||
|
# parsers = (JSONParser, )
|
||||||
|
# request = factory.post('/', content, content_type=content_type,
|
||||||
|
# parsers=parsers)
|
||||||
|
# self.assertEqual(request.POST.items(), [])
|
||||||
|
# self.assertEqual(request.DATA.items(), data.items())
|
||||||
|
|
||||||
|
# def test_accessing_data_after_post_for_overloaded_json(self):
|
||||||
|
# """
|
||||||
|
# Ensures request.DATA can be accessed after request.POST in overloaded
|
||||||
|
# json request
|
||||||
|
# """
|
||||||
|
# data = {'qwerty': 'uiop'}
|
||||||
|
# content = json.dumps(data)
|
||||||
|
# content_type = 'application/json'
|
||||||
|
# parsers = (JSONParser, )
|
||||||
|
# form_data = {Request._CONTENT_PARAM: content,
|
||||||
|
# Request._CONTENTTYPE_PARAM: content_type}
|
||||||
|
|
||||||
|
# request = factory.post('/', form_data, parsers=parsers)
|
||||||
|
# self.assertEqual(request.POST.items(), form_data.items())
|
||||||
|
# self.assertEqual(request.DATA.items(), data.items())
|
||||||
|
|
||||||
|
|
||||||
|
class MockView(View):
|
||||||
|
authentication = (UserLoggedInAuthentication,)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
if request.POST.get('example') is not None:
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
return Response(status=status.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
(r'^$', MockView.as_view()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestContentParsingWithAuthentication(TestCase):
|
||||||
|
urls = 'djangorestframework.tests.request'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||||
|
self.username = 'john'
|
||||||
|
self.email = 'lennon@thebeatles.com'
|
||||||
|
self.password = 'password'
|
||||||
|
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||||
|
|
||||||
|
def test_user_logged_in_authentication_has_POST_when_not_logged_in(self):
|
||||||
|
"""
|
||||||
|
Ensures request.POST exists after UserLoggedInAuthentication when user
|
||||||
|
doesn't log in.
|
||||||
|
"""
|
||||||
|
content = {'example': 'example'}
|
||||||
|
|
||||||
|
response = self.client.post('/', content)
|
||||||
|
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||||
|
|
||||||
|
response = self.csrf_client.post('/', content)
|
||||||
|
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
||||||
|
|
||||||
|
# def test_user_logged_in_authentication_has_post_when_logged_in(self):
|
||||||
|
# """Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
|
||||||
|
# self.client.login(username='john', password='password')
|
||||||
|
# self.csrf_client.login(username='john', password='password')
|
||||||
|
# content = {'example': 'example'}
|
||||||
|
|
||||||
|
# response = self.client.post('/', content)
|
||||||
|
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
||||||
|
|
||||||
|
# response = self.csrf_client.post('/', content)
|
||||||
|
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
|
@ -1,19 +1,309 @@
|
||||||
# Right now we expect this test to fail - I'm just going to leave it commented out.
|
import json
|
||||||
# Looking forward to actually being able to raise ExpectedFailure sometime!
|
import unittest
|
||||||
#
|
|
||||||
#from django.test import TestCase
|
|
||||||
#from djangorestframework.response import Response
|
|
||||||
#
|
|
||||||
#
|
|
||||||
#class TestResponse(TestCase):
|
|
||||||
#
|
|
||||||
# # Interface tests
|
|
||||||
#
|
|
||||||
# # This is mainly to remind myself that the Response interface needs to change slightly
|
|
||||||
# def test_response_interface(self):
|
|
||||||
# """Ensure the Response interface is as expected."""
|
|
||||||
# response = Response()
|
|
||||||
# getattr(response, 'status')
|
|
||||||
# getattr(response, 'content')
|
|
||||||
# getattr(response, 'headers')
|
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import patterns, url, include
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from djangorestframework.response import Response, NotAcceptable, ImmediateResponse
|
||||||
|
from djangorestframework.views import View
|
||||||
|
from djangorestframework.compat import RequestFactory
|
||||||
|
from djangorestframework import status
|
||||||
|
from djangorestframework.renderers import (
|
||||||
|
BaseRenderer,
|
||||||
|
JSONRenderer,
|
||||||
|
DocumentingHTMLRenderer,
|
||||||
|
DEFAULT_RENDERERS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MockPickleRenderer(BaseRenderer):
|
||||||
|
media_type = 'application/pickle'
|
||||||
|
|
||||||
|
|
||||||
|
class MockJsonRenderer(BaseRenderer):
|
||||||
|
media_type = 'application/json'
|
||||||
|
|
||||||
|
|
||||||
|
class TestResponseDetermineRenderer(TestCase):
|
||||||
|
|
||||||
|
def get_response(self, url='', accept_list=[], renderers=[]):
|
||||||
|
kwargs = {}
|
||||||
|
if accept_list is not None:
|
||||||
|
kwargs['HTTP_ACCEPT'] = ','.join(accept_list)
|
||||||
|
request = RequestFactory().get(url, **kwargs)
|
||||||
|
return Response(request=request, renderers=renderers)
|
||||||
|
|
||||||
|
def test_determine_accept_list_accept_header(self):
|
||||||
|
"""
|
||||||
|
Test that determine_accept_list takes the Accept header.
|
||||||
|
"""
|
||||||
|
accept_list = ['application/pickle', 'application/json']
|
||||||
|
response = self.get_response(accept_list=accept_list)
|
||||||
|
self.assertEqual(response._determine_accept_list(), accept_list)
|
||||||
|
|
||||||
|
def test_determine_accept_list_default(self):
|
||||||
|
"""
|
||||||
|
Test that determine_accept_list takes the default renderer if Accept is not specified.
|
||||||
|
"""
|
||||||
|
response = self.get_response(accept_list=None)
|
||||||
|
self.assertEqual(response._determine_accept_list(), ['*/*'])
|
||||||
|
|
||||||
|
def test_determine_accept_list_overriden_header(self):
|
||||||
|
"""
|
||||||
|
Test Accept header overriding.
|
||||||
|
"""
|
||||||
|
accept_list = ['application/pickle', 'application/json']
|
||||||
|
response = self.get_response(url='?_accept=application/x-www-form-urlencoded',
|
||||||
|
accept_list=accept_list)
|
||||||
|
self.assertEqual(response._determine_accept_list(), ['application/x-www-form-urlencoded'])
|
||||||
|
|
||||||
|
def test_determine_renderer(self):
|
||||||
|
"""
|
||||||
|
Test that right renderer is chosen, in the order of Accept list.
|
||||||
|
"""
|
||||||
|
accept_list = ['application/pickle', 'application/json']
|
||||||
|
renderers = (MockPickleRenderer, MockJsonRenderer)
|
||||||
|
response = self.get_response(accept_list=accept_list, renderers=renderers)
|
||||||
|
renderer, media_type = response._determine_renderer()
|
||||||
|
self.assertEqual(media_type, 'application/pickle')
|
||||||
|
self.assertTrue(isinstance(renderer, MockPickleRenderer))
|
||||||
|
|
||||||
|
renderers = (MockJsonRenderer, )
|
||||||
|
response = self.get_response(accept_list=accept_list, renderers=renderers)
|
||||||
|
renderer, media_type = response._determine_renderer()
|
||||||
|
self.assertEqual(media_type, 'application/json')
|
||||||
|
self.assertTrue(isinstance(renderer, MockJsonRenderer))
|
||||||
|
|
||||||
|
def test_determine_renderer_default(self):
|
||||||
|
"""
|
||||||
|
Test determine renderer when Accept was not specified.
|
||||||
|
"""
|
||||||
|
renderers = (MockPickleRenderer, )
|
||||||
|
response = self.get_response(accept_list=None, renderers=renderers)
|
||||||
|
renderer, media_type = response._determine_renderer()
|
||||||
|
self.assertEqual(media_type, '*/*')
|
||||||
|
self.assertTrue(isinstance(renderer, MockPickleRenderer))
|
||||||
|
|
||||||
|
def test_determine_renderer_no_renderer(self):
|
||||||
|
"""
|
||||||
|
Test determine renderer when no renderer can satisfy the Accept list.
|
||||||
|
"""
|
||||||
|
accept_list = ['application/json']
|
||||||
|
renderers = (MockPickleRenderer, )
|
||||||
|
response = self.get_response(accept_list=accept_list, renderers=renderers)
|
||||||
|
self.assertRaises(NotAcceptable, response._determine_renderer)
|
||||||
|
|
||||||
|
|
||||||
|
class TestResponseRenderContent(TestCase):
|
||||||
|
def get_response(self, url='', accept_list=[], content=None, renderers=None):
|
||||||
|
request = RequestFactory().get(url, HTTP_ACCEPT=','.join(accept_list))
|
||||||
|
return Response(request=request, content=content, renderers=renderers or DEFAULT_RENDERERS)
|
||||||
|
|
||||||
|
def test_render(self):
|
||||||
|
"""
|
||||||
|
Test rendering simple data to json.
|
||||||
|
"""
|
||||||
|
content = {'a': 1, 'b': [1, 2, 3]}
|
||||||
|
content_type = 'application/json'
|
||||||
|
response = self.get_response(accept_list=[content_type], content=content)
|
||||||
|
response = response.render()
|
||||||
|
self.assertEqual(json.loads(response.content), content)
|
||||||
|
self.assertEqual(response['Content-Type'], content_type)
|
||||||
|
|
||||||
|
def test_render_no_renderer(self):
|
||||||
|
"""
|
||||||
|
Test rendering response when no renderer can satisfy accept.
|
||||||
|
"""
|
||||||
|
content = 'bla'
|
||||||
|
content_type = 'weirdcontenttype'
|
||||||
|
response = self.get_response(accept_list=[content_type], content=content)
|
||||||
|
response = response.render()
|
||||||
|
self.assertEqual(response.status_code, 406)
|
||||||
|
self.assertIsNotNone(response.content)
|
||||||
|
|
||||||
|
# def test_render_renderer_raises_ImmediateResponse(self):
|
||||||
|
# """
|
||||||
|
# Test rendering response when renderer raises ImmediateResponse
|
||||||
|
# """
|
||||||
|
# class PickyJSONRenderer(BaseRenderer):
|
||||||
|
# """
|
||||||
|
# A renderer that doesn't make much sense, just to try
|
||||||
|
# out raising an ImmediateResponse
|
||||||
|
# """
|
||||||
|
# media_type = 'application/json'
|
||||||
|
|
||||||
|
# def render(self, obj=None, media_type=None):
|
||||||
|
# raise ImmediateResponse({'error': '!!!'}, status=400)
|
||||||
|
|
||||||
|
# response = self.get_response(
|
||||||
|
# accept_list=['application/json'],
|
||||||
|
# renderers=[PickyJSONRenderer, JSONRenderer]
|
||||||
|
# )
|
||||||
|
# response = response.render()
|
||||||
|
# self.assertEqual(response.status_code, 400)
|
||||||
|
# self.assertEqual(response.content, json.dumps({'error': '!!!'}))
|
||||||
|
|
||||||
|
|
||||||
|
DUMMYSTATUS = status.HTTP_200_OK
|
||||||
|
DUMMYCONTENT = 'dummycontent'
|
||||||
|
|
||||||
|
RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
|
||||||
|
RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x
|
||||||
|
|
||||||
|
|
||||||
|
class RendererA(BaseRenderer):
|
||||||
|
media_type = 'mock/renderera'
|
||||||
|
format = "formata"
|
||||||
|
|
||||||
|
def render(self, obj=None, media_type=None):
|
||||||
|
return RENDERER_A_SERIALIZER(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class RendererB(BaseRenderer):
|
||||||
|
media_type = 'mock/rendererb'
|
||||||
|
format = "formatb"
|
||||||
|
|
||||||
|
def render(self, obj=None, media_type=None):
|
||||||
|
return RENDERER_B_SERIALIZER(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class MockView(View):
|
||||||
|
renderers = (RendererA, RendererB)
|
||||||
|
|
||||||
|
def get(self, request, **kwargs):
|
||||||
|
return Response(DUMMYCONTENT, status=DUMMYSTATUS)
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLView(View):
|
||||||
|
renderers = (DocumentingHTMLRenderer, )
|
||||||
|
|
||||||
|
def get(self, request, **kwargs):
|
||||||
|
return Response('text')
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLView1(View):
|
||||||
|
renderers = (DocumentingHTMLRenderer, JSONRenderer)
|
||||||
|
|
||||||
|
def get(self, request, **kwargs):
|
||||||
|
return Response('text')
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
|
||||||
|
url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
|
||||||
|
url(r'^html$', HTMLView.as_view()),
|
||||||
|
url(r'^html1$', HTMLView1.as_view()),
|
||||||
|
url(r'^restframework', include('djangorestframework.urls', namespace='djangorestframework'))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Clean tests bellow - remove duplicates with above, better unit testing, ...
|
||||||
|
class RendererIntegrationTests(TestCase):
|
||||||
|
"""
|
||||||
|
End-to-end testing of renderers using an ResponseMixin on a generic view.
|
||||||
|
"""
|
||||||
|
|
||||||
|
urls = 'djangorestframework.tests.response'
|
||||||
|
|
||||||
|
def test_default_renderer_serializes_content(self):
|
||||||
|
"""If the Accept header is not set the default renderer should serialize the response."""
|
||||||
|
resp = self.client.get('/')
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererA.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_head_method_serializes_no_content(self):
|
||||||
|
"""No response must be included in HEAD requests."""
|
||||||
|
resp = self.client.head('/')
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererA.media_type)
|
||||||
|
self.assertEquals(resp.content, '')
|
||||||
|
|
||||||
|
def test_default_renderer_serializes_content_on_accept_any(self):
|
||||||
|
"""If the Accept header is set to */* the default renderer should serialize the response."""
|
||||||
|
resp = self.client.get('/', HTTP_ACCEPT='*/*')
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererA.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_specified_renderer_serializes_content_default_case(self):
|
||||||
|
"""If the Accept header is set the specified renderer should serialize the response.
|
||||||
|
(In this case we check that works for the default renderer)"""
|
||||||
|
resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type)
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererA.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_specified_renderer_serializes_content_non_default_case(self):
|
||||||
|
"""If the Accept header is set the specified renderer should serialize the response.
|
||||||
|
(In this case we check that works for a non-default renderer)"""
|
||||||
|
resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type)
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_specified_renderer_serializes_content_on_accept_query(self):
|
||||||
|
"""The '_accept' query string should behave in the same way as the Accept header."""
|
||||||
|
resp = self.client.get('/?_accept=%s' % RendererB.media_type)
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
@unittest.skip('can\'t pass because view is a simple Django view and response is an ImmediateResponse')
|
||||||
|
def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
|
||||||
|
"""If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
|
||||||
|
resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
|
||||||
|
self.assertEquals(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE)
|
||||||
|
|
||||||
|
def test_specified_renderer_serializes_content_on_format_query(self):
|
||||||
|
"""If a 'format' query is specified, the renderer with the matching
|
||||||
|
format attribute should serialize the response."""
|
||||||
|
resp = self.client.get('/?format=%s' % RendererB.format)
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_specified_renderer_serializes_content_on_format_kwargs(self):
|
||||||
|
"""If a 'format' keyword arg is specified, the renderer with the matching
|
||||||
|
format attribute should serialize the response."""
|
||||||
|
resp = self.client.get('/something.formatb')
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_specified_renderer_is_used_on_format_query_with_matching_accept(self):
|
||||||
|
"""If both a 'format' query and a matching Accept header specified,
|
||||||
|
the renderer with the matching format attribute should serialize the response."""
|
||||||
|
resp = self.client.get('/?format=%s' % RendererB.format,
|
||||||
|
HTTP_ACCEPT=RendererB.media_type)
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_conflicting_format_query_and_accept_ignores_accept(self):
|
||||||
|
"""If a 'format' query is specified that does not match the Accept
|
||||||
|
header, we should only honor the 'format' query string."""
|
||||||
|
resp = self.client.get('/?format=%s' % RendererB.format,
|
||||||
|
HTTP_ACCEPT='dummy')
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
|
||||||
|
class Issue122Tests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests that covers #122.
|
||||||
|
"""
|
||||||
|
urls = 'djangorestframework.tests.response'
|
||||||
|
|
||||||
|
def test_only_html_renderer(self):
|
||||||
|
"""
|
||||||
|
Test if no infinite recursion occurs.
|
||||||
|
"""
|
||||||
|
self.client.get('/html')
|
||||||
|
|
||||||
|
def test_html_renderer_is_first(self):
|
||||||
|
"""
|
||||||
|
Test if no infinite recursion occurs.
|
||||||
|
"""
|
||||||
|
self.client.get('/html1')
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.utils import simplejson as json
|
||||||
from djangorestframework.renderers import JSONRenderer
|
from djangorestframework.renderers import JSONRenderer
|
||||||
from djangorestframework.reverse import reverse
|
from djangorestframework.reverse import reverse
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
|
from djangorestframework.response import Response
|
||||||
|
|
||||||
|
|
||||||
class MyView(View):
|
class MyView(View):
|
||||||
|
@ -15,7 +16,8 @@ class MyView(View):
|
||||||
renderers = (JSONRenderer, )
|
renderers = (JSONRenderer, )
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return reverse('myview', request=request)
|
return Response(reverse('myview', request=request))
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^myview$', MyView.as_view(), name='myview'),
|
url(r'^myview$', MyView.as_view(), name='myview'),
|
||||||
|
|
|
@ -10,19 +10,20 @@ from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
from djangorestframework.permissions import PerUserThrottling, PerViewThrottling, PerResourceThrottling
|
from djangorestframework.permissions import PerUserThrottling, PerViewThrottling, PerResourceThrottling
|
||||||
from djangorestframework.resources import FormResource
|
from djangorestframework.resources import FormResource
|
||||||
|
from djangorestframework.response import Response
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
permissions = ( PerUserThrottling, )
|
permissions_classes = ( PerUserThrottling, )
|
||||||
throttle = '3/sec'
|
throttle = '3/sec'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return 'foo'
|
return Response('foo')
|
||||||
|
|
||||||
class MockView_PerViewThrottling(MockView):
|
class MockView_PerViewThrottling(MockView):
|
||||||
permissions = ( PerViewThrottling, )
|
permissions_classes = ( PerViewThrottling, )
|
||||||
|
|
||||||
class MockView_PerResourceThrottling(MockView):
|
class MockView_PerResourceThrottling(MockView):
|
||||||
permissions = ( PerResourceThrottling, )
|
permissions_classes = ( PerResourceThrottling, )
|
||||||
resource = FormResource
|
resource = FormResource
|
||||||
|
|
||||||
class MockView_MinuteThrottling(MockView):
|
class MockView_MinuteThrottling(MockView):
|
||||||
|
@ -53,7 +54,7 @@ class ThrottlingTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Explicitly set the timer, overriding time.time()
|
Explicitly set the timer, overriding time.time()
|
||||||
"""
|
"""
|
||||||
view.permissions[0].timer = lambda self: value
|
view.permissions_classes[0].timer = lambda self: value
|
||||||
|
|
||||||
def test_request_throttling_expires(self):
|
def test_request_throttling_expires(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django import forms
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.resources import FormResource, ModelResource
|
from djangorestframework.resources import FormResource, ModelResource
|
||||||
from djangorestframework.response import ErrorResponse
|
from djangorestframework.response import ImmediateResponse
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,10 +81,11 @@ class TestNonFieldErrors(TestCase):
|
||||||
content = {'field1': 'example1', 'field2': 'example2'}
|
content = {'field1': 'example1', 'field2': 'example2'}
|
||||||
try:
|
try:
|
||||||
MockResource(view).validate_request(content, None)
|
MockResource(view).validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ImmediateResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
response = exc.response
|
||||||
|
self.assertEqual(response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||||
else:
|
else:
|
||||||
self.fail('ErrorResponse was not raised')
|
self.fail('ImmediateResponse was not raised')
|
||||||
|
|
||||||
|
|
||||||
class TestFormValidation(TestCase):
|
class TestFormValidation(TestCase):
|
||||||
|
@ -120,14 +121,14 @@ class TestFormValidation(TestCase):
|
||||||
def validation_failure_raises_response_exception(self, validator):
|
def validation_failure_raises_response_exception(self, validator):
|
||||||
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
|
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
|
||||||
content = {}
|
content = {}
|
||||||
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
|
self.assertRaises(ImmediateResponse, validator.validate_request, content, None)
|
||||||
|
|
||||||
def validation_does_not_allow_extra_fields_by_default(self, validator):
|
def validation_does_not_allow_extra_fields_by_default(self, validator):
|
||||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
|
self.assertRaises(ImmediateResponse, validator.validate_request, content, None)
|
||||||
|
|
||||||
def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
||||||
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
||||||
|
@ -154,8 +155,9 @@ class TestFormValidation(TestCase):
|
||||||
content = {}
|
content = {}
|
||||||
try:
|
try:
|
||||||
validator.validate_request(content, None)
|
validator.validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ImmediateResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
response = exc.response
|
||||||
|
self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised')
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
|
@ -164,8 +166,9 @@ class TestFormValidation(TestCase):
|
||||||
content = {'qwerty': ''}
|
content = {'qwerty': ''}
|
||||||
try:
|
try:
|
||||||
validator.validate_request(content, None)
|
validator.validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ImmediateResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
response = exc.response
|
||||||
|
self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised')
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
|
@ -174,8 +177,9 @@ class TestFormValidation(TestCase):
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
try:
|
try:
|
||||||
validator.validate_request(content, None)
|
validator.validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ImmediateResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
|
response = exc.response
|
||||||
|
self.assertEqual(response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised')
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
|
@ -184,8 +188,9 @@ class TestFormValidation(TestCase):
|
||||||
content = {'qwerty': '', 'extra': 'extra'}
|
content = {'qwerty': '', 'extra': 'extra'}
|
||||||
try:
|
try:
|
||||||
validator.validate_request(content, None)
|
validator.validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ImmediateResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
|
response = exc.response
|
||||||
|
self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
|
||||||
'extra': ['This field does not exist.']}})
|
'extra': ['This field does not exist.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised')
|
self.fail('ResourceException was not raised')
|
||||||
|
@ -307,14 +312,14 @@ class TestModelFormValidator(TestCase):
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'}
|
content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'}
|
||||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None)
|
||||||
|
|
||||||
def test_validate_requires_fields_on_model_forms(self):
|
def test_validate_requires_fields_on_model_forms(self):
|
||||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
content = {'readonly': 'read only'}
|
content = {'readonly': 'read only'}
|
||||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None)
|
||||||
|
|
||||||
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
||||||
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
||||||
|
|
|
@ -2,16 +2,16 @@ from django.core.urlresolvers import reverse
|
||||||
from django.conf.urls.defaults import patterns, url, include
|
from django.conf.urls.defaults import patterns, url, include
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import Client
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
from djangorestframework.views import View
|
|
||||||
from djangorestframework.parsers import JSONParser
|
|
||||||
from djangorestframework.resources import ModelResource
|
from djangorestframework.resources import ModelResource
|
||||||
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
from djangorestframework.views import (
|
||||||
|
View,
|
||||||
from StringIO import StringIO
|
ListOrCreateModelView,
|
||||||
|
InstanceModelView
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
|
@ -25,6 +25,7 @@ class MockViewFinal(View):
|
||||||
def final(self, request, response, *args, **kwargs):
|
def final(self, request, response, *args, **kwargs):
|
||||||
return HttpResponse('{"test": "passed"}', content_type="application/json")
|
return HttpResponse('{"test": "passed"}', content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
class ResourceMockView(View):
|
class ResourceMockView(View):
|
||||||
"""This is a resource-based mock view"""
|
"""This is a resource-based mock view"""
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ class ResourceMockView(View):
|
||||||
|
|
||||||
form = MockForm
|
form = MockForm
|
||||||
|
|
||||||
|
|
||||||
class MockResource(ModelResource):
|
class MockResource(ModelResource):
|
||||||
"""This is a mock model-based resource"""
|
"""This is a mock model-based resource"""
|
||||||
|
|
||||||
|
@ -55,6 +57,7 @@ urlpatterns = patterns('',
|
||||||
url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')),
|
url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseViewTests(TestCase):
|
class BaseViewTests(TestCase):
|
||||||
"""Test the base view class of djangorestframework"""
|
"""Test the base view class of djangorestframework"""
|
||||||
urls = 'djangorestframework.tests.views'
|
urls = 'djangorestframework.tests.views'
|
||||||
|
@ -62,8 +65,7 @@ class BaseViewTests(TestCase):
|
||||||
def test_view_call_final(self):
|
def test_view_call_final(self):
|
||||||
response = self.client.options('/mock/final/')
|
response = self.client.options('/mock/final/')
|
||||||
self.assertEqual(response['Content-Type'].split(';')[0], "application/json")
|
self.assertEqual(response['Content-Type'].split(';')[0], "application/json")
|
||||||
parser = JSONParser(None)
|
data = json.loads(response.content)
|
||||||
(data, files) = parser.parse(StringIO(response.content))
|
|
||||||
self.assertEqual(data['test'], 'passed')
|
self.assertEqual(data['test'], 'passed')
|
||||||
|
|
||||||
def test_options_method_simple_view(self):
|
def test_options_method_simple_view(self):
|
||||||
|
@ -77,9 +79,9 @@ class BaseViewTests(TestCase):
|
||||||
self._verify_options_response(response,
|
self._verify_options_response(response,
|
||||||
name='Resource Mock',
|
name='Resource Mock',
|
||||||
description='This is a resource-based mock view',
|
description='This is a resource-based mock view',
|
||||||
fields={'foo':'BooleanField',
|
fields={'foo': 'BooleanField',
|
||||||
'bar':'IntegerField',
|
'bar': 'IntegerField',
|
||||||
'baz':'CharField',
|
'baz': 'CharField',
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_options_method_model_resource_list_view(self):
|
def test_options_method_model_resource_list_view(self):
|
||||||
|
@ -87,9 +89,9 @@ class BaseViewTests(TestCase):
|
||||||
self._verify_options_response(response,
|
self._verify_options_response(response,
|
||||||
name='Mock List',
|
name='Mock List',
|
||||||
description='This is a mock model-based resource',
|
description='This is a mock model-based resource',
|
||||||
fields={'foo':'BooleanField',
|
fields={'foo': 'BooleanField',
|
||||||
'bar':'IntegerField',
|
'bar': 'IntegerField',
|
||||||
'baz':'CharField',
|
'baz': 'CharField',
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_options_method_model_resource_detail_view(self):
|
def test_options_method_model_resource_detail_view(self):
|
||||||
|
@ -97,17 +99,16 @@ class BaseViewTests(TestCase):
|
||||||
self._verify_options_response(response,
|
self._verify_options_response(response,
|
||||||
name='Mock Instance',
|
name='Mock Instance',
|
||||||
description='This is a mock model-based resource',
|
description='This is a mock model-based resource',
|
||||||
fields={'foo':'BooleanField',
|
fields={'foo': 'BooleanField',
|
||||||
'bar':'IntegerField',
|
'bar': 'IntegerField',
|
||||||
'baz':'CharField',
|
'baz': 'CharField',
|
||||||
})
|
})
|
||||||
|
|
||||||
def _verify_options_response(self, response, name, description, fields=None, status=200,
|
def _verify_options_response(self, response, name, description, fields=None, status=200,
|
||||||
mime_type='application/json'):
|
mime_type='application/json'):
|
||||||
self.assertEqual(response.status_code, status)
|
self.assertEqual(response.status_code, status)
|
||||||
self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
|
self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
|
||||||
parser = JSONParser(None)
|
data = json.loads(response.content)
|
||||||
(data, files) = parser.parse(StringIO(response.content))
|
|
||||||
self.assertTrue('application/json' in data['renders'])
|
self.assertTrue('application/json' in data['renders'])
|
||||||
self.assertEqual(name, data['name'])
|
self.assertEqual(name, data['name'])
|
||||||
self.assertEqual(description, data['description'])
|
self.assertEqual(description, data['description'])
|
||||||
|
@ -132,6 +133,3 @@ class ExtraViewsTests(TestCase):
|
||||||
response = self.client.get(reverse('djangorestframework:logout'))
|
response = self.client.get(reverse('djangorestframework:logout'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
||||||
|
|
||||||
# TODO: Add login/logout behaviour tests
|
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
import django
|
|
||||||
from django.utils.encoding import smart_unicode
|
from django.utils.encoding import smart_unicode
|
||||||
from django.utils.xmlutils import SimplerXMLGenerator
|
from django.utils.xmlutils import SimplerXMLGenerator
|
||||||
from django.core.urlresolvers import resolve
|
from django.core.urlresolvers import resolve
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from djangorestframework.compat import StringIO
|
from djangorestframework.compat import StringIO
|
||||||
|
from djangorestframework.compat import RequestFactory as DjangoRequestFactory
|
||||||
|
from djangorestframework.request import Request
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from mediatypes import media_type_matches, is_form_media_type
|
|
||||||
from mediatypes import add_media_type_param, get_media_type_params, order_by_precedence
|
|
||||||
|
|
||||||
MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
|
MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
|
||||||
|
|
||||||
|
|
||||||
def as_tuple(obj):
|
def as_tuple(obj):
|
||||||
"""
|
"""
|
||||||
Given an object which may be a list/tuple, another object, or None,
|
Given an object which may be a list/tuple, another object, or None,
|
||||||
|
@ -55,24 +53,23 @@ class XML2Dict(object):
|
||||||
# Save attrs and text, hope there will not be a child with same name
|
# Save attrs and text, hope there will not be a child with same name
|
||||||
if node.text:
|
if node.text:
|
||||||
node_tree = node.text
|
node_tree = node.text
|
||||||
for (k,v) in node.attrib.items():
|
for (k, v) in node.attrib.items():
|
||||||
k,v = self._namespace_split(k, v)
|
k, v = self._namespace_split(k, v)
|
||||||
node_tree[k] = v
|
node_tree[k] = v
|
||||||
#Save childrens
|
#Save childrens
|
||||||
for child in node.getchildren():
|
for child in node.getchildren():
|
||||||
tag, tree = self._namespace_split(child.tag, self._parse_node(child))
|
tag, tree = self._namespace_split(child.tag, self._parse_node(child))
|
||||||
if tag not in node_tree: # the first time, so store it in dict
|
if tag not in node_tree: # the first time, so store it in dict
|
||||||
node_tree[tag] = tree
|
node_tree[tag] = tree
|
||||||
continue
|
continue
|
||||||
old = node_tree[tag]
|
old = node_tree[tag]
|
||||||
if not isinstance(old, list):
|
if not isinstance(old, list):
|
||||||
node_tree.pop(tag)
|
node_tree.pop(tag)
|
||||||
node_tree[tag] = [old] # multi times, so change old dict to a list
|
node_tree[tag] = [old] # multi times, so change old dict to a list
|
||||||
node_tree[tag].append(tree) # add the new one
|
node_tree[tag].append(tree) # add the new one
|
||||||
|
|
||||||
return node_tree
|
return node_tree
|
||||||
|
|
||||||
|
|
||||||
def _namespace_split(self, tag, value):
|
def _namespace_split(self, tag, value):
|
||||||
"""
|
"""
|
||||||
Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
|
Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
|
||||||
|
@ -135,5 +132,41 @@ class XMLRenderer():
|
||||||
xml.endDocument()
|
xml.endDocument()
|
||||||
return stream.getvalue()
|
return stream.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def dict2xml(input):
|
def dict2xml(input):
|
||||||
return XMLRenderer().dict2xml(input)
|
return XMLRenderer().dict2xml(input)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestFactory(DjangoRequestFactory):
|
||||||
|
"""
|
||||||
|
Replicate RequestFactory, but return Request, not HttpRequest.
|
||||||
|
"""
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
parsers = kwargs.pop('parsers', None)
|
||||||
|
request = super(RequestFactory, self).get(*args, **kwargs)
|
||||||
|
return Request(request, parsers)
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
parsers = kwargs.pop('parsers', None)
|
||||||
|
request = super(RequestFactory, self).post(*args, **kwargs)
|
||||||
|
return Request(request, parsers)
|
||||||
|
|
||||||
|
def put(self, *args, **kwargs):
|
||||||
|
parsers = kwargs.pop('parsers', None)
|
||||||
|
request = super(RequestFactory, self).put(*args, **kwargs)
|
||||||
|
return Request(request, parsers)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
parsers = kwargs.pop('parsers', None)
|
||||||
|
request = super(RequestFactory, self).delete(*args, **kwargs)
|
||||||
|
return Request(request, parsers)
|
||||||
|
|
||||||
|
def head(self, *args, **kwargs):
|
||||||
|
parsers = kwargs.pop('parsers', None)
|
||||||
|
request = super(RequestFactory, self).head(*args, **kwargs)
|
||||||
|
return Request(request, parsers)
|
||||||
|
|
||||||
|
def options(self, *args, **kwargs):
|
||||||
|
parsers = kwargs.pop('parsers', None)
|
||||||
|
request = super(RequestFactory, self).options(*args, **kwargs)
|
||||||
|
return Request(request, parsers)
|
||||||
|
|
|
@ -6,13 +6,12 @@ By setting or modifying class attributes on your view, you change it's predefine
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from djangorestframework.compat import View as DjangoView, apply_markdown
|
from djangorestframework.compat import View as DjangoView, apply_markdown
|
||||||
from djangorestframework.response import Response, ErrorResponse
|
from djangorestframework.response import Response, ImmediateResponse
|
||||||
from djangorestframework.mixins import *
|
from djangorestframework.mixins import *
|
||||||
from djangorestframework import resources, renderers, parsers, authentication, permissions, status
|
from djangorestframework import resources, renderers, parsers, authentication, permissions, status
|
||||||
|
|
||||||
|
@ -68,7 +67,7 @@ _resource_classes = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
class View(ResourceMixin, RequestMixin, ResponseMixin, PermissionsMixin, DjangoView):
|
||||||
"""
|
"""
|
||||||
Handles incoming requests and maps them to REST operations.
|
Handles incoming requests and maps them to REST operations.
|
||||||
Performs request deserialization, response serialization, authentication and input validation.
|
Performs request deserialization, response serialization, authentication and input validation.
|
||||||
|
@ -82,16 +81,16 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
|
|
||||||
renderers = renderers.DEFAULT_RENDERERS
|
renderers = renderers.DEFAULT_RENDERERS
|
||||||
"""
|
"""
|
||||||
List of renderers the resource can serialize the response with, ordered by preference.
|
List of renderer classes the resource can serialize the response with, ordered by preference.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parsers = parsers.DEFAULT_PARSERS
|
parsers = parsers.DEFAULT_PARSERS
|
||||||
"""
|
"""
|
||||||
List of parsers the resource can parse the request with.
|
List of parser classes the resource can parse the request with.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
authentication = (authentication.UserLoggedInAuthentication,
|
authentication = (authentication.UserLoggedInAuthentication,
|
||||||
authentication.BasicAuthentication)
|
authentication.BasicAuthentication)
|
||||||
"""
|
"""
|
||||||
List of all authenticating methods to attempt.
|
List of all authenticating methods to attempt.
|
||||||
"""
|
"""
|
||||||
|
@ -117,7 +116,15 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
"""
|
"""
|
||||||
Return the list of allowed HTTP methods, uppercased.
|
Return the list of allowed HTTP methods, uppercased.
|
||||||
"""
|
"""
|
||||||
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)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_response_headers(self):
|
||||||
|
return {
|
||||||
|
'Allow': ', '.join(self.allowed_methods),
|
||||||
|
'Vary': 'Authenticate, Accept'
|
||||||
|
}
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
"""
|
"""
|
||||||
|
@ -161,6 +168,9 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
return description
|
return description
|
||||||
|
|
||||||
def markup_description(self, description):
|
def markup_description(self, description):
|
||||||
|
"""
|
||||||
|
Apply HTML markup to the description of this view.
|
||||||
|
"""
|
||||||
if apply_markdown:
|
if apply_markdown:
|
||||||
description = apply_markdown(description)
|
description = apply_markdown(description)
|
||||||
else:
|
else:
|
||||||
|
@ -169,81 +179,73 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
|
|
||||||
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,
|
content = {
|
||||||
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
|
'detail': "Method '%s' not allowed on this resource." % request.method
|
||||||
|
}
|
||||||
|
raise ImmediateResponse(content, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
|
||||||
def initial(self, request, *args, **kargs):
|
def initial(self, request, *args, **kargs):
|
||||||
"""
|
"""
|
||||||
Hook for any code that needs to run prior to anything else.
|
This method is a hook for any code that needs to run prior to
|
||||||
Required if you want to do things like set `request.upload_handlers` before
|
anything else.
|
||||||
the authentication and dispatch handling is run.
|
Required if you want to do things like set `request.upload_handlers`
|
||||||
|
before the authentication and dispatch handling is run.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def final(self, request, response, *args, **kargs):
|
def final(self, request, response, *args, **kargs):
|
||||||
"""
|
"""
|
||||||
Hook for any code that needs to run after everything else in the view.
|
This method is a hook for any code that needs to run after everything
|
||||||
|
else in the view.
|
||||||
|
Returns the final response object.
|
||||||
"""
|
"""
|
||||||
# Always add these headers.
|
response.view = self
|
||||||
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
response.request = request
|
||||||
# sample to allow caching using Vary http header
|
response.renderers = self.renderers
|
||||||
response.headers['Vary'] = 'Authenticate, Accept'
|
for key, value in self.headers.items():
|
||||||
|
response[key] = value
|
||||||
# merge with headers possibly set at some point in the view
|
return response
|
||||||
response.headers.update(self.headers)
|
|
||||||
return self.render(response)
|
|
||||||
|
|
||||||
def add_header(self, field, value):
|
|
||||||
"""
|
|
||||||
Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class.
|
|
||||||
"""
|
|
||||||
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
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
request = self.create_request(request)
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
||||||
self.args = args
|
self.args = args
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
self.headers = {}
|
self.headers = self.default_response_headers
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.initial(request, *args, **kwargs)
|
self.initial(request, *args, **kwargs)
|
||||||
|
|
||||||
# Authenticate and check request has the relevant permissions
|
# check that user has the relevant permissions
|
||||||
self._check_permissions()
|
self.check_permissions(request.user)
|
||||||
|
|
||||||
# Get the appropriate handler method
|
# Get the appropriate handler method
|
||||||
if self.method.lower() in self.http_method_names:
|
if request.method.lower() in self.http_method_names:
|
||||||
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
|
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
|
||||||
else:
|
else:
|
||||||
handler = self.http_method_not_allowed
|
handler = self.http_method_not_allowed
|
||||||
|
|
||||||
response_obj = handler(request, *args, **kwargs)
|
response = handler(request, *args, **kwargs)
|
||||||
|
|
||||||
# Allow return value to be either HttpResponse, Response, or an object, or None
|
if isinstance(response, Response):
|
||||||
if isinstance(response_obj, HttpResponse):
|
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
||||||
return response_obj
|
response.raw_content = self.filter_response(response.raw_content)
|
||||||
elif 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)
|
except ImmediateResponse, exc:
|
||||||
response.cleaned_content = self.filter_response(response.raw_content)
|
|
||||||
|
|
||||||
except ErrorResponse, exc:
|
|
||||||
response = exc.response
|
response = exc.response
|
||||||
|
|
||||||
return self.final(request, response, *args, **kwargs)
|
self.response = self.final(request, response, *args, **kwargs)
|
||||||
|
return self.response
|
||||||
|
|
||||||
def options(self, request, *args, **kwargs):
|
def options(self, request, *args, **kwargs):
|
||||||
response_obj = {
|
content = {
|
||||||
'name': self.get_name(),
|
'name': self.get_name(),
|
||||||
'description': self.get_description(),
|
'description': self.get_description(),
|
||||||
'renders': self._rendered_media_types,
|
'renders': self._rendered_media_types,
|
||||||
|
@ -254,11 +256,8 @@ 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
|
content['fields'] = field_name_types
|
||||||
# Note 'ErrorResponse' is misleading, it's just any response
|
raise ImmediateResponse(content, status=status.HTTP_200_OK)
|
||||||
# that should be rendered and returned immediately, without any
|
|
||||||
# response filtering.
|
|
||||||
raise ErrorResponse(status.HTTP_200_OK, response_obj)
|
|
||||||
|
|
||||||
|
|
||||||
class ModelView(View):
|
class ModelView(View):
|
||||||
|
|
75
docs/howto/requestmixin.rst
Normal file
75
docs/howto/requestmixin.rst
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
Using the enhanced request in all your views
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
This example shows how you can use Django REST framework's enhanced `request` - :class:`request.Request` - in your own views, without having to use the full-blown :class:`views.View` class.
|
||||||
|
|
||||||
|
What can it do for you ? Mostly, it will take care of parsing the request's content, and handling equally all HTTP methods ...
|
||||||
|
|
||||||
|
Before
|
||||||
|
--------
|
||||||
|
|
||||||
|
In order to support `JSON` or other serial formats, you might have parsed manually the request's content with something like : ::
|
||||||
|
|
||||||
|
class MyView(View):
|
||||||
|
|
||||||
|
def put(self, request, *args, **kwargs):
|
||||||
|
content_type = request.META['CONTENT_TYPE']
|
||||||
|
if (content_type == 'application/json'):
|
||||||
|
raw_data = request.read()
|
||||||
|
parsed_data = json.loads(raw_data)
|
||||||
|
|
||||||
|
# PLUS as many `elif` as formats you wish to support ...
|
||||||
|
|
||||||
|
# then do stuff with your data :
|
||||||
|
self.do_stuff(parsed_data['bla'], parsed_data['hoho'])
|
||||||
|
|
||||||
|
# and finally respond something
|
||||||
|
|
||||||
|
... and you were unhappy because this looks hackish.
|
||||||
|
|
||||||
|
Also, you might have tried uploading files with a PUT request - *and given up* since that's complicated to achieve even with Django 1.3.
|
||||||
|
|
||||||
|
|
||||||
|
After
|
||||||
|
------
|
||||||
|
|
||||||
|
All the dirty `Content-type` checking and content reading and parsing is done for you, and you only need to do the following : ::
|
||||||
|
|
||||||
|
class MyView(MyBaseViewUsingEnhancedRequest):
|
||||||
|
|
||||||
|
def put(self, request, *args, **kwargs):
|
||||||
|
self.do_stuff(request.DATA['bla'], request.DATA['hoho'])
|
||||||
|
# and finally respond something
|
||||||
|
|
||||||
|
So the parsed content is magically available as `.DATA` on the `request` object.
|
||||||
|
|
||||||
|
Also, if you uploaded files, they are available as `.FILES`, like with a normal POST request.
|
||||||
|
|
||||||
|
.. note:: Note that all the above is also valid for a POST request.
|
||||||
|
|
||||||
|
|
||||||
|
How to add it to your custom views ?
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Now that you're convinced you need to use the enhanced request object, here is how you can add it to all your custom views : ::
|
||||||
|
|
||||||
|
from django.views.generic.base import View
|
||||||
|
|
||||||
|
from djangorestframework.mixins import RequestMixin
|
||||||
|
from djangorestframework import parsers
|
||||||
|
|
||||||
|
|
||||||
|
class MyBaseViewUsingEnhancedRequest(RequestMixin, View):
|
||||||
|
"""
|
||||||
|
Base view enabling the usage of enhanced requests with user defined views.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parser_classes = parsers.DEFAULT_PARSERS
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
request = self.prepare_request(request)
|
||||||
|
return super(MyBaseViewUsingEnhancedRequest, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
And then, use this class as a base for all your custom views.
|
||||||
|
|
||||||
|
.. note:: you can see this live in the examples.
|
5
docs/library/request.rst
Normal file
5
docs/library/request.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
:mod:`request`
|
||||||
|
=====================
|
||||||
|
|
||||||
|
.. automodule:: request
|
||||||
|
:members:
|
5
docs/library/utils.rst
Normal file
5
docs/library/utils.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
:mod:`utils`
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. automodule:: utils
|
||||||
|
:members:
|
|
@ -18,8 +18,8 @@ class BlogPostResource(ModelResource):
|
||||||
|
|
||||||
def comments(self, instance):
|
def comments(self, instance):
|
||||||
return reverse('comments',
|
return reverse('comments',
|
||||||
kwargs={'blogpost': instance.key},
|
kwargs={'blogpost': instance.key},
|
||||||
request=self.request)
|
request=self.request)
|
||||||
|
|
||||||
|
|
||||||
class CommentResource(ModelResource):
|
class CommentResource(ModelResource):
|
||||||
|
|
|
@ -57,7 +57,9 @@ class ObjectStoreRoot(View):
|
||||||
if not file.startswith('.')]
|
if not file.startswith('.')]
|
||||||
ctime_sorted_basenames = [item[0] for item in sorted([(os.path.basename(path), os.path.getctime(path)) for path in filepaths],
|
ctime_sorted_basenames = [item[0] for item in sorted([(os.path.basename(path), os.path.getctime(path)) for path in filepaths],
|
||||||
key=operator.itemgetter(1), reverse=True)]
|
key=operator.itemgetter(1), reverse=True)]
|
||||||
return [get_file_url(key, request) for key in ctime_sorted_basenames]
|
content = [get_file_url(key, request)
|
||||||
|
for key in ctime_sorted_basenames]
|
||||||
|
return Response(content)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
|
@ -69,7 +71,7 @@ class ObjectStoreRoot(View):
|
||||||
|
|
||||||
remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES)
|
remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES)
|
||||||
url = get_file_url(key, request)
|
url = get_file_url(key, request)
|
||||||
return Response(status.HTTP_201_CREATED, self.CONTENT, {'Location': url})
|
return Response(self.CONTENT, status.HTTP_201_CREATED, {'Location': url})
|
||||||
|
|
||||||
|
|
||||||
class StoredObject(View):
|
class StoredObject(View):
|
||||||
|
@ -84,8 +86,8 @@ class StoredObject(View):
|
||||||
"""
|
"""
|
||||||
filename = get_filename(key)
|
filename = get_filename(key)
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
return Response(status.HTTP_404_NOT_FOUND)
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
return pickle.load(open(filename, 'rb'))
|
return Response(pickle.load(open(filename, 'rb')))
|
||||||
|
|
||||||
def put(self, request, key):
|
def put(self, request, key):
|
||||||
"""
|
"""
|
||||||
|
@ -94,7 +96,7 @@ class StoredObject(View):
|
||||||
"""
|
"""
|
||||||
filename = get_filename(key)
|
filename = get_filename(key)
|
||||||
pickle.dump(self.CONTENT, open(filename, 'wb'))
|
pickle.dump(self.CONTENT, open(filename, 'wb'))
|
||||||
return self.CONTENT
|
return Response(self.CONTENT)
|
||||||
|
|
||||||
def delete(self, request, key):
|
def delete(self, request, key):
|
||||||
"""
|
"""
|
||||||
|
@ -102,5 +104,6 @@ class StoredObject(View):
|
||||||
"""
|
"""
|
||||||
filename = get_filename(key)
|
filename = get_filename(key)
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
return Response(status.HTTP_404_NOT_FOUND)
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
|
return Response()
|
||||||
|
|
27
examples/permissionsexample/tests.py
Normal file
27
examples/permissionsexample/tests.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.test.client import Client
|
||||||
|
|
||||||
|
|
||||||
|
class NaviguatePermissionsExamples(TestCase):
|
||||||
|
"""
|
||||||
|
Sanity checks for permissions examples
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_throttled_resource(self):
|
||||||
|
url = reverse('throttled-resource')
|
||||||
|
for i in range(0, 10):
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 503)
|
||||||
|
|
||||||
|
|
||||||
|
def test_loggedin_resource(self):
|
||||||
|
url = reverse('loggedin-resource')
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
loggedin_client = Client()
|
||||||
|
loggedin_client.login(username='test', password='test')
|
||||||
|
response = loggedin_client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
|
@ -1,4 +1,5 @@
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
|
from djangorestframework.response import Response
|
||||||
from djangorestframework.permissions import PerUserThrottling, IsAuthenticated
|
from djangorestframework.permissions import PerUserThrottling, IsAuthenticated
|
||||||
from djangorestframework.reverse import reverse
|
from djangorestframework.reverse import reverse
|
||||||
|
|
||||||
|
@ -9,16 +10,16 @@ class PermissionsExampleView(View):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return [
|
return Response([
|
||||||
{
|
{
|
||||||
'name': 'Throttling Example',
|
'name': 'Throttling Example',
|
||||||
'url': reverse('throttled-resource', request=request)
|
'url': reverse('throttled-resource', request)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Logged in example',
|
'name': 'Logged in example',
|
||||||
'url': reverse('loggedin-resource', request=request)
|
'url': reverse('loggedin-resource', request)
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
|
|
||||||
|
|
||||||
class ThrottlingExampleView(View):
|
class ThrottlingExampleView(View):
|
||||||
|
@ -29,14 +30,14 @@ class ThrottlingExampleView(View):
|
||||||
throttle will be applied until 60 seconds have passed since the first request.
|
throttle will be applied until 60 seconds have passed since the first request.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permissions = (PerUserThrottling,)
|
permissions_classes = (PerUserThrottling,)
|
||||||
throttle = '10/min'
|
throttle = '10/min'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""
|
"""
|
||||||
Handle GET requests.
|
Handle GET requests.
|
||||||
"""
|
"""
|
||||||
return "Successful response to GET request because throttle is not yet active."
|
return Response("Successful response to GET request because throttle is not yet active.")
|
||||||
|
|
||||||
|
|
||||||
class LoggedInExampleView(View):
|
class LoggedInExampleView(View):
|
||||||
|
@ -46,7 +47,7 @@ class LoggedInExampleView(View):
|
||||||
`curl -X GET -H 'Accept: application/json' -u test:test http://localhost:8000/permissions-example`
|
`curl -X GET -H 'Accept: application/json' -u test:test http://localhost:8000/permissions-example`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permissions = (IsAuthenticated, )
|
permissions_classes = (IsAuthenticated, )
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return 'You have permission to view this resource'
|
return Response('You have permission to view this resource')
|
||||||
|
|
|
@ -6,6 +6,7 @@ from pygments.styles import get_all_styles
|
||||||
LEXER_CHOICES = sorted([(item[1][0], item[0]) for item in get_all_lexers()])
|
LEXER_CHOICES = sorted([(item[1][0], item[0]) for item in get_all_lexers()])
|
||||||
STYLE_CHOICES = sorted((item, item) for item in list(get_all_styles()))
|
STYLE_CHOICES = sorted((item, item) for item in list(get_all_styles()))
|
||||||
|
|
||||||
|
|
||||||
class PygmentsForm(forms.Form):
|
class PygmentsForm(forms.Form):
|
||||||
"""A simple form with some of the most important pygments settings.
|
"""A simple form with some of the most important pygments settings.
|
||||||
The code to be highlighted can be specified either in a text field, or by URL.
|
The code to be highlighted can be specified either in a text field, or by URL.
|
||||||
|
@ -24,5 +25,3 @@ class PygmentsForm(forms.Form):
|
||||||
initial='python')
|
initial='python')
|
||||||
style = forms.ChoiceField(choices=STYLE_CHOICES,
|
style = forms.ChoiceField(choices=STYLE_CHOICES,
|
||||||
initial='friendly')
|
initial='friendly')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ class TestPygmentsExample(TestCase):
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.temp_dir = tempfile.mkdtemp()
|
self.temp_dir = tempfile.mkdtemp()
|
||||||
views.HIGHLIGHTED_CODE_DIR = self.temp_dir
|
views.HIGHLIGHTED_CODE_DIR = self.temp_dir
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(self.temp_dir)
|
shutil.rmtree(self.temp_dir)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_get_to_root(self):
|
def test_get_to_root(self):
|
||||||
'''Just do a get on the base url'''
|
'''Just do a get on the base url'''
|
||||||
request = self.factory.get('/pygments')
|
request = self.factory.get('/pygments')
|
||||||
|
@ -44,6 +44,3 @@ class TestPygmentsExample(TestCase):
|
||||||
response = view(request)
|
response = view(request)
|
||||||
response_locations = json.loads(response.content)
|
response_locations = json.loads(response.content)
|
||||||
self.assertEquals(locations, response_locations)
|
self.assertEquals(locations, response_locations)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from __future__ import with_statement # for python 2.5
|
from __future__ import with_statement # for python 2.5
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from djangorestframework.resources import FormResource
|
|
||||||
from djangorestframework.response import Response
|
from djangorestframework.response import Response
|
||||||
from djangorestframework.renderers import BaseRenderer
|
from djangorestframework.renderers import BaseRenderer
|
||||||
from djangorestframework.reverse import reverse
|
from djangorestframework.reverse import reverse
|
||||||
|
@ -64,8 +63,11 @@ class PygmentsRoot(View):
|
||||||
"""
|
"""
|
||||||
Return a list of all currently existing snippets.
|
Return a list of all currently existing snippets.
|
||||||
"""
|
"""
|
||||||
unique_ids = [os.path.split(f)[1] for f in list_dir_sorted_by_ctime(HIGHLIGHTED_CODE_DIR)]
|
unique_ids = [os.path.split(f)[1]
|
||||||
return [reverse('pygments-instance', request=request, args=[unique_id]) for unique_id in unique_ids]
|
for f in list_dir_sorted_by_ctime(HIGHLIGHTED_CODE_DIR)]
|
||||||
|
urls = [reverse('pygments-instance', args=[unique_id], request=request)
|
||||||
|
for unique_id in unique_ids]
|
||||||
|
return Response(urls)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
|
@ -85,7 +87,8 @@ class PygmentsRoot(View):
|
||||||
|
|
||||||
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
|
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
|
||||||
|
|
||||||
return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', request=request, args=[unique_id])})
|
location = reverse('pygments-instance', args=[unique_id], request=request)
|
||||||
|
return Response(status=status.HTTP_201_CREATED, headers={'Location': location})
|
||||||
|
|
||||||
|
|
||||||
class PygmentsInstance(View):
|
class PygmentsInstance(View):
|
||||||
|
@ -93,7 +96,7 @@ class PygmentsInstance(View):
|
||||||
Simply return the stored highlighted HTML file with the correct mime type.
|
Simply return the stored highlighted HTML file with the correct mime type.
|
||||||
This Resource only renders HTML and uses a standard HTML renderer rather than the renderers.DocumentingHTMLRenderer class.
|
This Resource only renders HTML and uses a standard HTML renderer rather than the renderers.DocumentingHTMLRenderer class.
|
||||||
"""
|
"""
|
||||||
renderers = (HTMLRenderer,)
|
renderers = (HTMLRenderer, )
|
||||||
|
|
||||||
def get(self, request, unique_id):
|
def get(self, request, unique_id):
|
||||||
"""
|
"""
|
||||||
|
@ -101,8 +104,8 @@ class PygmentsInstance(View):
|
||||||
"""
|
"""
|
||||||
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
|
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
|
||||||
if not os.path.exists(pathname):
|
if not os.path.exists(pathname):
|
||||||
return Response(status.HTTP_404_NOT_FOUND)
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
return open(pathname, 'r').read()
|
return Response(open(pathname, 'r').read())
|
||||||
|
|
||||||
def delete(self, request, unique_id):
|
def delete(self, request, unique_id):
|
||||||
"""
|
"""
|
||||||
|
@ -110,6 +113,6 @@ class PygmentsInstance(View):
|
||||||
"""
|
"""
|
||||||
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
|
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
|
||||||
if not os.path.exists(pathname):
|
if not os.path.exists(pathname):
|
||||||
return Response(status.HTTP_404_NOT_FOUND)
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
return os.remove(pathname)
|
os.remove(pathname)
|
||||||
|
return Response()
|
||||||
|
|
0
examples/requestexample/__init__.py
Normal file
0
examples/requestexample/__init__.py
Normal file
3
examples/requestexample/models.py
Normal file
3
examples/requestexample/models.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
0
examples/requestexample/tests.py
Normal file
0
examples/requestexample/tests.py
Normal file
9
examples/requestexample/urls.py
Normal file
9
examples/requestexample/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.conf.urls.defaults import patterns, url
|
||||||
|
from requestexample.views import RequestExampleView, EchoRequestContentView
|
||||||
|
from examples.views import ProxyView
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
url(r'^$', RequestExampleView.as_view(), name='request-example'),
|
||||||
|
url(r'^content$', ProxyView.as_view(view_class=EchoRequestContentView), name='request-content'),
|
||||||
|
)
|
43
examples/requestexample/views.py
Normal file
43
examples/requestexample/views.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from djangorestframework.compat import View
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from djangorestframework.mixins import RequestMixin
|
||||||
|
from djangorestframework.views import View as DRFView
|
||||||
|
from djangorestframework import parsers
|
||||||
|
from djangorestframework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
class RequestExampleView(DRFView):
|
||||||
|
"""
|
||||||
|
A container view for request examples.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return Response([{'name': 'request.DATA Example', 'url': reverse('request-content')},])
|
||||||
|
|
||||||
|
|
||||||
|
class MyBaseViewUsingEnhancedRequest(RequestMixin, View):
|
||||||
|
"""
|
||||||
|
Base view enabling the usage of enhanced requests with user defined views.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parsers = parsers.DEFAULT_PARSERS
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.request = request = self.create_request(request)
|
||||||
|
return super(MyBaseViewUsingEnhancedRequest, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EchoRequestContentView(MyBaseViewUsingEnhancedRequest):
|
||||||
|
"""
|
||||||
|
A view that just reads the items in `request.DATA` and echoes them back.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
return HttpResponse(("Found %s in request.DATA, content : %s" %
|
||||||
|
(type(request.DATA), request.DATA)))
|
||||||
|
|
||||||
|
def put(self, request, *args, **kwargs):
|
||||||
|
return HttpResponse(("Found %s in request.DATA, content : %s" %
|
||||||
|
(type(request.DATA), request.DATA)))
|
|
@ -1,5 +1,6 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
class MyForm(forms.Form):
|
class MyForm(forms.Form):
|
||||||
foo = forms.BooleanField(required=False)
|
foo = forms.BooleanField(required=False)
|
||||||
bar = forms.IntegerField(help_text='Must be an integer.')
|
bar = forms.IntegerField(help_text='Must be an integer.')
|
||||||
|
|
|
@ -13,18 +13,19 @@ class ExampleView(View):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""
|
"""
|
||||||
Handle GET requests, returning a list of URLs pointing to 3 other views.
|
Handle GET requests, returning a list of URLs pointing to
|
||||||
|
three other views.
|
||||||
"""
|
"""
|
||||||
resource_urls = [reverse('another-example',
|
resource_urls = [reverse('another-example',
|
||||||
kwargs={'num': num},
|
kwargs={'num': num},
|
||||||
request=request)
|
request=request)
|
||||||
for num in range(3)]
|
for num in range(3)]
|
||||||
return {"Some other resources": resource_urls}
|
return Response({"Some other resources": resource_urls})
|
||||||
|
|
||||||
|
|
||||||
class AnotherExampleView(View):
|
class AnotherExampleView(View):
|
||||||
"""
|
"""
|
||||||
A basic view, that can be handle GET and POST requests.
|
A basic view, that can handle GET and POST requests.
|
||||||
Applies some simple form validation on POST requests.
|
Applies some simple form validation on POST requests.
|
||||||
"""
|
"""
|
||||||
form = MyForm
|
form = MyForm
|
||||||
|
@ -35,8 +36,8 @@ class AnotherExampleView(View):
|
||||||
Returns a simple string indicating which view the GET request was for.
|
Returns a simple string indicating which view the GET request was for.
|
||||||
"""
|
"""
|
||||||
if int(num) > 2:
|
if int(num) > 2:
|
||||||
return Response(status.HTTP_404_NOT_FOUND)
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
return "GET request to AnotherExampleResource %s" % num
|
return Response("GET request to AnotherExampleResource %s" % num)
|
||||||
|
|
||||||
def post(self, request, num):
|
def post(self, request, num):
|
||||||
"""
|
"""
|
||||||
|
@ -44,5 +45,5 @@ class AnotherExampleView(View):
|
||||||
Returns a simple string indicating what content was supplied.
|
Returns a simple string indicating what content was supplied.
|
||||||
"""
|
"""
|
||||||
if int(num) > 2:
|
if int(num) > 2:
|
||||||
return Response(status.HTTP_404_NOT_FOUND)
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
return "POST request to AnotherExampleResource %s, with content: %s" % (num, repr(self.CONTENT))
|
return Response("POST request to AnotherExampleResource %s, with content: %s" % (num, repr(self.CONTENT)))
|
||||||
|
|
|
@ -2,36 +2,66 @@
|
||||||
|
|
||||||
from djangorestframework.reverse import reverse
|
from djangorestframework.reverse import reverse
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
|
from djangorestframework.response import Response
|
||||||
|
|
||||||
|
|
||||||
class Sandbox(View):
|
class Sandbox(View):
|
||||||
"""This is the sandbox for the examples provided with [Django REST framework](http://django-rest-framework.org).
|
"""
|
||||||
|
This is the sandbox for the examples provided with
|
||||||
|
[Django REST framework][1].
|
||||||
|
|
||||||
These examples are provided to help you get a better idea of some of the features of RESTful APIs created using the framework.
|
These examples are provided to help you get a better idea of some of the
|
||||||
|
features of RESTful APIs created using the framework.
|
||||||
|
|
||||||
All the example APIs allow anonymous access, and can be navigated either through the browser or from the command line...
|
All the example APIs allow anonymous access, and can be navigated either
|
||||||
|
through the browser or from the command line.
|
||||||
|
|
||||||
bash: curl -X GET http://api.django-rest-framework.org/ # (Use default renderer)
|
For example, to get the default representation using curl:
|
||||||
bash: curl -X GET http://api.django-rest-framework.org/ -H 'Accept: text/plain' # (Use plaintext documentation renderer)
|
|
||||||
|
bash: curl -X GET http://rest.ep.io/
|
||||||
|
|
||||||
|
Or, to get the plaintext documentation represention:
|
||||||
|
|
||||||
|
bash: curl -X GET http://rest.ep.io/ -H 'Accept: text/plain'
|
||||||
|
|
||||||
The examples provided:
|
The examples provided:
|
||||||
|
|
||||||
1. A basic example using the [Resource](http://django-rest-framework.org/library/resource.html) class.
|
1. A basic example using the [Resource][2] class.
|
||||||
2. A basic example using the [ModelResource](http://django-rest-framework.org/library/modelresource.html) class.
|
2. A basic example using the [ModelResource][3] class.
|
||||||
3. An basic example using Django 1.3's [class based views](http://docs.djangoproject.com/en/dev/topics/class-based-views/) and djangorestframework's [RendererMixin](http://django-rest-framework.org/library/renderers.html).
|
3. An basic example using Django 1.3's [class based views][4] and
|
||||||
|
djangorestframework's [RendererMixin][5].
|
||||||
4. A generic object store API.
|
4. A generic object store API.
|
||||||
5. A code highlighting API.
|
5. A code highlighting API.
|
||||||
6. A blog posts and comments API.
|
6. A blog posts and comments API.
|
||||||
7. A basic example using permissions.
|
7. A basic example using permissions.
|
||||||
|
8. A basic example using enhanced request.
|
||||||
|
|
||||||
Please feel free to browse, create, edit and delete the resources in these examples."""
|
Please feel free to browse, create, edit and delete the resources in
|
||||||
|
these examples.
|
||||||
|
|
||||||
|
[1]: http://django-rest-framework.org
|
||||||
|
[2]: http://django-rest-framework.org/library/resource.html
|
||||||
|
[3]: http://django-rest-framework.org/library/modelresource.html
|
||||||
|
[4]: http://docs.djangoproject.com/en/dev/topics/class-based-views/
|
||||||
|
[5]: http://django-rest-framework.org/library/renderers.html
|
||||||
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return [{'name': 'Simple Resource example', 'url': reverse('example-resource', request=request)},
|
return Response([
|
||||||
{'name': 'Simple ModelResource example', 'url': reverse('model-resource-root', request=request)},
|
{'name': 'Simple Resource example',
|
||||||
{'name': 'Simple Mixin-only example', 'url': reverse('mixin-view', request=request)},
|
'url': reverse('example-resource', request=request)},
|
||||||
{'name': 'Object store API', 'url': reverse('object-store-root', request=request)},
|
{'name': 'Simple ModelResource example',
|
||||||
{'name': 'Code highlighting API', 'url': reverse('pygments-root', request=request)},
|
'url': reverse('model-resource-root', request=request)},
|
||||||
{'name': 'Blog posts API', 'url': reverse('blog-posts-root', request=request)},
|
{'name': 'Simple Mixin-only example',
|
||||||
{'name': 'Permissions example', 'url': reverse('permissions-example', request=request)}
|
'url': reverse('mixin-view', request=request)},
|
||||||
]
|
{'name': 'Object store API',
|
||||||
|
'url': reverse('object-store-root', request=request)},
|
||||||
|
{'name': 'Code highlighting API',
|
||||||
|
'url': reverse('pygments-root', request=request)},
|
||||||
|
{'name': 'Blog posts API',
|
||||||
|
'url': reverse('blog-posts-root', request=request)},
|
||||||
|
{'name': 'Permissions example',
|
||||||
|
'url': reverse('permissions-example', request=request)},
|
||||||
|
{'name': 'Simple request mixin example',
|
||||||
|
'url': reverse('request-example', request=request)}
|
||||||
|
])
|
||||||
|
|
|
@ -106,6 +106,7 @@ INSTALLED_APPS = (
|
||||||
'pygments_api',
|
'pygments_api',
|
||||||
'blogpost',
|
'blogpost',
|
||||||
'permissionsexample',
|
'permissionsexample',
|
||||||
|
'requestexample',
|
||||||
)
|
)
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
32
examples/views.py
Normal file
32
examples/views.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from djangorestframework.views import View
|
||||||
|
from djangorestframework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyView(View):
|
||||||
|
"""
|
||||||
|
A view that just acts as a proxy to call non-djangorestframework views, while still
|
||||||
|
displaying the browsable API interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
view_class = None
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.request = request = self.create_request(request)
|
||||||
|
if request.method in ['PUT', 'POST']:
|
||||||
|
self.response = self.view_class.as_view()(request, *args, **kwargs)
|
||||||
|
return super(ProxyView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
def put(self, request, *args, **kwargs):
|
||||||
|
return Response(self.response.content)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
return Response(self.response.content)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.view_class.__name__
|
||||||
|
|
||||||
|
def get_description(self, html):
|
||||||
|
return self.view_class.__doc__
|
Loading…
Reference in New Issue
Block a user