mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 16:24:18 +03:00
.DATA, .FILES, overloaded HTTP method, content type and content available directly on the request - see #128
This commit is contained in:
parent
d1ce9d3914
commit
ab0b72a7c1
|
@ -95,8 +95,8 @@ class UserLoggedInAuthentication(BaseAuthentication):
|
|||
# Temporarily replace request.POST with .DATA, to use our generic parsing.
|
||||
# If DATA is not dict-like, use an empty dict.
|
||||
if request.method.upper() == 'POST':
|
||||
if hasattr(self.view.DATA, 'get'):
|
||||
request._post = self.view.DATA
|
||||
if hasattr(request.DATA, 'get'):
|
||||
request._post = request.DATA
|
||||
else:
|
||||
request._post = {}
|
||||
|
||||
|
|
|
@ -14,11 +14,10 @@ from djangorestframework import status
|
|||
from djangorestframework.renderers import BaseRenderer
|
||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
||||
from djangorestframework.response import Response, ErrorResponse
|
||||
from djangorestframework.request import request_class_factory
|
||||
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
||||
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
|
||||
|
||||
from StringIO import StringIO
|
||||
|
||||
|
||||
__all__ = (
|
||||
# Base behavior mixins
|
||||
|
@ -56,150 +55,28 @@ class RequestMixin(object):
|
|||
Should be a tuple/list of classes as described in the :mod:`parsers` module.
|
||||
"""
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
def get_request_class(self):
|
||||
"""
|
||||
Returns the HTTP method.
|
||||
|
||||
This should be used instead of just reading :const:`request.method`, as it allows the `method`
|
||||
to be overridden by using a hidden `form` field on a form POST request.
|
||||
Returns a custom subclass of Django's `HttpRequest`, providing new facilities
|
||||
such as direct access to the parsed request content.
|
||||
"""
|
||||
if not hasattr(self, '_method'):
|
||||
self._load_method_and_content_type()
|
||||
return self._method
|
||||
if not hasattr(self, '_request_class'):
|
||||
self._request_class = request_class_factory(self.request)
|
||||
self._request_class._USE_FORM_OVERLOADING = self._USE_FORM_OVERLOADING
|
||||
self._request_class._METHOD_PARAM = self._METHOD_PARAM
|
||||
self._request_class._CONTENTTYPE_PARAM = self._CONTENTTYPE_PARAM
|
||||
self._request_class._CONTENT_PARAM = self._CONTENT_PARAM
|
||||
self._request_class.parsers = self.parsers
|
||||
return self._request_class
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
def get_request(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.
|
||||
Returns a custom request instance, with data and attributes copied from the
|
||||
original 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
|
||||
def _parsed_media_types(self):
|
||||
"""
|
||||
Return a list of all the media types that this view can parse.
|
||||
"""
|
||||
return [parser.media_type for parser in self.parsers]
|
||||
|
||||
@property
|
||||
def _default_parser(self):
|
||||
"""
|
||||
Return the view's default parser class.
|
||||
"""
|
||||
return self.parsers[0]
|
||||
|
||||
request_class = self.get_request_class()
|
||||
return request_class(self.request)
|
||||
|
||||
|
||||
########## ResponseMixin ##########
|
||||
|
||||
|
@ -395,7 +272,7 @@ class ResourceMixin(object):
|
|||
May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request).
|
||||
"""
|
||||
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
|
||||
|
||||
@property
|
||||
|
@ -415,7 +292,7 @@ class ResourceMixin(object):
|
|||
return ModelResource(self)
|
||||
elif getattr(self, 'form', None):
|
||||
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 Resource(self)
|
||||
|
||||
|
@ -752,7 +629,7 @@ class PaginatorMixin(object):
|
|||
"""
|
||||
|
||||
# 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)
|
||||
|
||||
paginator = Paginator(obj, self.get_limit())
|
||||
|
|
|
@ -165,9 +165,10 @@ class MultiPartParser(BaseParser):
|
|||
`data` will be a :class:`QueryDict` containing all the form parameters.
|
||||
`files` will be a :class:`QueryDict` containing all the form files.
|
||||
"""
|
||||
upload_handlers = self.view.request._get_upload_handlers()
|
||||
# TODO: now self.view is in fact request, but should disappear ...
|
||||
upload_handlers = self.view._get_upload_handlers()
|
||||
try:
|
||||
django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers)
|
||||
django_parser = DjangoMultiPartParser(self.view.META, stream, upload_handlers)
|
||||
except MultiPartParserError, exc:
|
||||
raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
|
||||
{'detail': 'multipart parse error - %s' % unicode(exc)})
|
||||
|
|
|
@ -269,32 +269,32 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
|||
|
||||
# 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.
|
||||
if not getattr(view, '_USE_FORM_OVERLOADING', False):
|
||||
if not getattr(view.request, '_USE_FORM_OVERLOADING', False):
|
||||
return None
|
||||
|
||||
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
||||
class GenericContentForm(forms.Form):
|
||||
def __init__(self, view):
|
||||
def __init__(self, request):
|
||||
"""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.
|
||||
Add the fields dynamically."""
|
||||
super(GenericContentForm, self).__init__()
|
||||
|
||||
contenttype_choices = [(media_type, media_type) for media_type in view._parsed_media_types]
|
||||
initial_contenttype = view._default_parser.media_type
|
||||
contenttype_choices = [(media_type, media_type) for media_type in request._parsed_media_types]
|
||||
initial_contenttype = request._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,
|
||||
initial=initial_contenttype)
|
||||
self.fields[view._CONTENT_PARAM] = forms.CharField(label='Content',
|
||||
self.fields[request._CONTENT_PARAM] = forms.CharField(label='Content',
|
||||
widget=forms.Textarea)
|
||||
|
||||
# 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
|
||||
|
||||
# Okey doke, let's do it
|
||||
return GenericContentForm(view)
|
||||
return GenericContentForm(view.request)
|
||||
|
||||
def render(self, obj=None, media_type=None):
|
||||
"""
|
||||
|
|
217
djangorestframework/request.py
Normal file
217
djangorestframework/request.py
Normal file
|
@ -0,0 +1,217 @@
|
|||
"""
|
||||
The :mod:`request` module provides a `Request` class that can be used
|
||||
to replace the standard Django request passed to the views.
|
||||
This replacement `Request` provides many facilities, like automatically
|
||||
parsed request content, form overloading of method/content type/content,
|
||||
better support for HTTP PUT method.
|
||||
"""
|
||||
|
||||
from django.http import HttpRequest
|
||||
|
||||
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
|
||||
from djangorestframework.utils import as_tuple
|
||||
|
||||
from StringIO import StringIO
|
||||
|
||||
|
||||
__all__ = ('Request')
|
||||
|
||||
|
||||
def request_class_factory(request):
|
||||
"""
|
||||
Builds and returns a request class, to be used as a replacement of Django's built-in.
|
||||
|
||||
In fact :class:`request.Request` needs to be mixed-in with a subclass of `HttpRequest` for use,
|
||||
and we cannot do that before knowing which subclass of `HttpRequest` is used. So this function
|
||||
takes a request instance as only argument, and returns a properly mixed-in request class.
|
||||
"""
|
||||
request_class = type(request)
|
||||
return type(request_class.__name__, (Request, request_class), {})
|
||||
|
||||
|
||||
class Request(object):
|
||||
|
||||
_USE_FORM_OVERLOADING = True
|
||||
_METHOD_PARAM = '_method'
|
||||
_CONTENTTYPE_PARAM = '_content_type'
|
||||
_CONTENT_PARAM = '_content'
|
||||
|
||||
parsers = ()
|
||||
"""
|
||||
The set of parsers that the request can handle.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`parsers` module.
|
||||
"""
|
||||
|
||||
def __init__(self, request):
|
||||
# this allows to "copy" a request object into a new instance
|
||||
# of our custom request class.
|
||||
|
||||
# First, we prepare the attributes to copy.
|
||||
attrs_dict = request.__dict__.copy()
|
||||
attrs_dict.pop('method', None)
|
||||
attrs_dict['_raw_method'] = request.method
|
||||
|
||||
# Then, put them in the instance's own __dict__
|
||||
self.__dict__ = attrs_dict
|
||||
|
||||
@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 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_post_and_files(self):
|
||||
"""
|
||||
Overrides the parent's `_load_post_and_files` to isolate it
|
||||
from the form overloading mechanism (see: `_perform_form_overloading`).
|
||||
"""
|
||||
# When self.POST or self.FILES are called they need to know the original
|
||||
# HTTP method, not our overloaded HTTP method. So, we save our overloaded
|
||||
# HTTP method and restore it after the call to parent.
|
||||
method_mem = getattr(self, '_method', None)
|
||||
self._method = self._raw_method
|
||||
super(Request, self)._load_post_and_files()
|
||||
if method_mem is None:
|
||||
del self._method
|
||||
else:
|
||||
self._method = method_mem
|
||||
|
||||
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(self._get_stream(), self._content_type)
|
||||
|
||||
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._raw_method
|
||||
|
||||
def _get_stream(self):
|
||||
"""
|
||||
Returns an object that may be used to stream the request content.
|
||||
"""
|
||||
|
||||
try:
|
||||
content_length = int(self.META.get('CONTENT_LENGTH', self.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(self, 'read'):
|
||||
return self
|
||||
return 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._raw_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.POST.copy()
|
||||
self._files = self.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
|
||||
def _parsed_media_types(self):
|
||||
"""
|
||||
Return a list of all the media types that this view can parse.
|
||||
"""
|
||||
return [parser.media_type for parser in self.parsers]
|
||||
|
||||
@property
|
||||
def _default_parser(self):
|
||||
"""
|
||||
Return the view's default parser class.
|
||||
"""
|
||||
return self.parsers[0]
|
|
@ -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,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')
|
|
@ -172,7 +172,7 @@ class RendererIntegrationTests(TestCase):
|
|||
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||
|
||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||
_indented_repr = '{\n "foo": [\n "bar", \n "baz"\n ]\n}'
|
||||
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
|
||||
|
||||
|
||||
class JSONRendererTests(TestCase):
|
||||
|
|
250
djangorestframework/tests/request.py
Normal file
250
djangorestframework/tests/request.py
Normal file
|
@ -0,0 +1,250 @@
|
|||
"""
|
||||
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.request import Request
|
||||
from djangorestframework.views import View
|
||||
from djangorestframework.request import request_class_factory
|
||||
|
||||
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()),
|
||||
)
|
||||
|
||||
request_class = request_class_factory(RequestFactory().get('/'))
|
||||
|
||||
|
||||
class RequestTestCase(TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
request_class.parsers = ()
|
||||
|
||||
def build_request(self, method, *args, **kwargs):
|
||||
factory = RequestFactory()
|
||||
method = getattr(factory, method)
|
||||
original_request = method(*args, **kwargs)
|
||||
return request_class(original_request)
|
||||
|
||||
|
||||
class TestMethodOverloading(RequestTestCase):
|
||||
|
||||
def test_standard_behaviour_determines_GET(self):
|
||||
"""GET requests identified"""
|
||||
request = self.build_request('get', '/')
|
||||
self.assertEqual(request.method, 'GET')
|
||||
|
||||
def test_standard_behaviour_determines_POST(self):
|
||||
"""POST requests identified"""
|
||||
request = self.build_request('post', '/')
|
||||
self.assertEqual(request.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"""
|
||||
request = self.build_request('post', '/', {Request._METHOD_PARAM: 'DELETE'})
|
||||
self.assertEqual(request.method, 'DELETE')
|
||||
|
||||
def test_HEAD_is_a_valid_method(self):
|
||||
"""HEAD requests identified"""
|
||||
request = request = self.build_request('head', '/')
|
||||
self.assertEqual(request.method, 'HEAD')
|
||||
|
||||
|
||||
class TestContentParsing(RequestTestCase):
|
||||
#TODO: is there any reason why many test cases documented as testing a PUT,
|
||||
# in fact use a POST !?
|
||||
|
||||
def tearDown(self):
|
||||
request_class.parsers = ()
|
||||
|
||||
def build_request(self, method, *args, **kwargs):
|
||||
factory = RequestFactory()
|
||||
method = getattr(factory, method)
|
||||
original_request = method(*args, **kwargs)
|
||||
return request_class(original_request)
|
||||
|
||||
def test_standard_behaviour_determines_no_content_GET(self):
|
||||
"""Ensure request.DATA returns None for GET request with no content."""
|
||||
request = self.build_request('get', '/')
|
||||
self.assertEqual(request.DATA, None)
|
||||
|
||||
def test_standard_behaviour_determines_no_content_HEAD(self):
|
||||
"""Ensure request.DATA returns None for HEAD request."""
|
||||
request = self.build_request('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."""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
request_class.parsers = (FormParser, MultiPartParser)
|
||||
request = self.build_request('post', '/', data=form_data)
|
||||
self.assertEqual(request.DATA.items(), form_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'
|
||||
request_class.parsers = (PlainTextParser,)
|
||||
request = self.build_request('post', '/', content, content_type=content_type)
|
||||
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."""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
request_class.parsers = (FormParser, MultiPartParser)
|
||||
request = self.build_request('put', '/', data=form_data)
|
||||
self.assertEqual(request.DATA.items(), form_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'
|
||||
request_class.parsers = (PlainTextParser,)
|
||||
request = self.build_request('post', '/', content, content_type=content_type)
|
||||
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'
|
||||
form_data = {Request._CONTENT_PARAM: content,
|
||||
Request._CONTENTTYPE_PARAM: content_type}
|
||||
request_class.parsers = (PlainTextParser,)
|
||||
request = self.build_request('post', '/', form_data)
|
||||
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"""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
request_class.parsers = (FormParser, MultiPartParser)
|
||||
request = self.build_request('post', '/', data=form_data)
|
||||
|
||||
self.assertEqual(request.DATA.items(), form_data.items())
|
||||
self.assertEqual(request.POST.items(), form_data.items())
|
||||
|
||||
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'
|
||||
|
||||
request_class.parsers = (JSONParser,)
|
||||
|
||||
request = self.build_request('post', '/', content, content_type=content_type)
|
||||
|
||||
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"""
|
||||
from django.utils import simplejson as json
|
||||
|
||||
data = {'qwerty': 'uiop'}
|
||||
content = json.dumps(data)
|
||||
content_type = 'application/json'
|
||||
|
||||
request_class.parsers = (JSONParser,)
|
||||
|
||||
form_data = {Request._CONTENT_PARAM: content,
|
||||
Request._CONTENTTYPE_PARAM: content_type}
|
||||
|
||||
request = self.build_request('post', '/', data=form_data)
|
||||
|
||||
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"""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
request_class.parsers = (FormParser, MultiPartParser)
|
||||
request = self.build_request('post', '/', data=form_data)
|
||||
|
||||
self.assertEqual(request.POST.items(), form_data.items())
|
||||
self.assertEqual(request.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'
|
||||
|
||||
request_class.parsers = (JSONParser,)
|
||||
|
||||
request = self.build_request('post', '/', content, content_type=content_type)
|
||||
|
||||
post_items = 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(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"""
|
||||
from django.utils import simplejson as json
|
||||
|
||||
data = {'qwerty': 'uiop'}
|
||||
content = json.dumps(data)
|
||||
content_type = 'application/json'
|
||||
|
||||
request_class.parsers = (JSONParser,)
|
||||
|
||||
form_data = {Request._CONTENT_PARAM: content,
|
||||
Request._CONTENTTYPE_PARAM: content_type}
|
||||
|
||||
request = self.build_request('post', '/', data=form_data)
|
||||
self.assertEqual(request.POST.items(), form_data.items())
|
||||
self.assertEqual(request.DATA.items(), data.items())
|
||||
|
||||
|
||||
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)
|
||||
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")
|
|
@ -81,7 +81,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
|||
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,
|
||||
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
|
||||
{'detail': 'Method \'%s\' not allowed on this resource.' % request.method})
|
||||
|
||||
def initial(self, request, *args, **kargs):
|
||||
"""
|
||||
|
@ -128,17 +128,20 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
|||
self.headers = {}
|
||||
|
||||
try:
|
||||
# Get a custom request, built form the original request instance
|
||||
self.request = request = self.get_request()
|
||||
|
||||
self.initial(request, *args, **kwargs)
|
||||
|
||||
# Authenticate and check request has the relevant permissions
|
||||
self._check_permissions()
|
||||
|
||||
# Get the appropriate handler method
|
||||
if self.method.lower() in self.http_method_names:
|
||||
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
|
||||
if request.method.lower() in self.http_method_names:
|
||||
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
|
||||
else:
|
||||
handler = self.http_method_not_allowed
|
||||
|
||||
|
||||
response_obj = handler(request, *args, **kwargs)
|
||||
|
||||
# Allow return value to be either HttpResponse, Response, or an object, or None
|
||||
|
@ -164,7 +167,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
|||
'name': get_name(self),
|
||||
'description': get_description(self),
|
||||
'renders': self._rendered_media_types,
|
||||
'parses': self._parsed_media_types,
|
||||
'parses': request._parsed_media_types,
|
||||
}
|
||||
form = self.get_bound_form()
|
||||
if form is not None:
|
||||
|
|
Loading…
Reference in New Issue
Block a user