.DATA, .FILES, overloaded HTTP method, content type and content available directly on the request - see #128

This commit is contained in:
Sébastien Piquemal 2012-01-22 21:28:34 +02:00
parent d1ce9d3914
commit ab0b72a7c1
10 changed files with 510 additions and 427 deletions

View File

@ -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 = {}

View File

@ -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,149 +55,27 @@ 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())

View File

@ -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)})

View File

@ -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):
"""

View 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]

View File

@ -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")

View File

@ -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')

View File

@ -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):

View 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")

View File

@ -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,14 +128,17 @@ 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
@ -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: