diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py index b61af32a2..20a5f34a7 100644 --- a/djangorestframework/authentication.py +++ b/djangorestframework/authentication.py @@ -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 = {} diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 7f0870f85..d016b0f12 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -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()) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index c8a014aef..e56ea0256 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -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)}) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index bb186b0a9..683024efb 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -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): """ diff --git a/djangorestframework/request.py b/djangorestframework/request.py new file mode 100644 index 000000000..c0ae46de3 --- /dev/null +++ b/djangorestframework/request.py @@ -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] diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py deleted file mode 100644 index 6bae8feb3..000000000 --- a/djangorestframework/tests/content.py +++ /dev/null @@ -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") diff --git a/djangorestframework/tests/methods.py b/djangorestframework/tests/methods.py index 4b90a21f1..e69de29bb 100644 --- a/djangorestframework/tests/methods.py +++ b/djangorestframework/tests/methods.py @@ -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') diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index 84e4390b8..adb46f7fa 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -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): diff --git a/djangorestframework/tests/request.py b/djangorestframework/tests/request.py new file mode 100644 index 000000000..7f3a485ef --- /dev/null +++ b/djangorestframework/tests/request.py @@ -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") diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 9f53868b3..37eec89fb 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -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: