From ab0b72a7c1a17c6d85530514caa51ce5bd77b592 Mon Sep 17 00:00:00 2001 From: Sébastien Piquemal Date: Sun, 22 Jan 2012 21:28:34 +0200 Subject: .DATA, .FILES, overloaded HTTP method, content type and content available directly on the request - see #128 --- djangorestframework/request.py | 217 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 djangorestframework/request.py (limited to 'djangorestframework/request.py') diff --git a/djangorestframework/request.py b/djangorestframework/request.py new file mode 100644 index 00000000..c0ae46de --- /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] -- cgit v1.2.3 From 714a90d7559885c15e5b2c86ef6f457fdf857ee0 Mon Sep 17 00:00:00 2001 From: Sébastien Piquemal Date: Tue, 24 Jan 2012 21:21:10 +0200 Subject: documentation for request module --- djangorestframework/request.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'djangorestframework/request.py') diff --git a/djangorestframework/request.py b/djangorestframework/request.py index c0ae46de..40d92eef 100644 --- a/djangorestframework/request.py +++ b/djangorestframework/request.py @@ -1,9 +1,12 @@ """ -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. +The :mod:`request` module provides a :class:`Request` class that can be used +to enhance the standard `request` object received in all the views. + +This enhanced request object offers the following : + + - content automatically parsed according to `Content-Type` header, and available as :meth:`request.DATA` + - full support of PUT method, including support for file uploads + - form overloading of HTTP method, content type and content """ from django.http import HttpRequest @@ -14,7 +17,7 @@ from djangorestframework.utils import as_tuple from StringIO import StringIO -__all__ = ('Request') +__all__ = ('Request',) def request_class_factory(request): @@ -30,6 +33,9 @@ def request_class_factory(request): class Request(object): + """ + A mixin class allowing to enhance Django's standard HttpRequest. + """ _USE_FORM_OVERLOADING = True _METHOD_PARAM = '_method' -- cgit v1.2.3 From 152c385f4de37558fe4e522abad5b97f0cf7ddce Mon Sep 17 00:00:00 2001 From: Sébastien Piquemal Date: Wed, 25 Jan 2012 00:11:54 +0200 Subject: enhanced request how-to + example --- djangorestframework/request.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'djangorestframework/request.py') diff --git a/djangorestframework/request.py b/djangorestframework/request.py index 40d92eef..1674167d 100644 --- a/djangorestframework/request.py +++ b/djangorestframework/request.py @@ -11,6 +11,8 @@ This enhanced request object offers the following : from django.http import HttpRequest +from djangorestframework.response import ErrorResponse +from djangorestframework import status from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence from djangorestframework.utils import as_tuple -- cgit v1.2.3 From 5bb6301b7f53e3815ab1a81a5fa38721dc95b113 Mon Sep 17 00:00:00 2001 From: Sébastien Piquemal Date: Thu, 2 Feb 2012 18:19:44 +0200 Subject: Response as a subclass of HttpResponse - first draft, not quite there yet. --- djangorestframework/request.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'djangorestframework/request.py') diff --git a/djangorestframework/request.py b/djangorestframework/request.py index 1674167d..ee43857e 100644 --- a/djangorestframework/request.py +++ b/djangorestframework/request.py @@ -206,9 +206,9 @@ class Request(object): 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}) + raise ErrorResponse(content={'error': + 'Unsupported media type in request \'%s\'.' % content_type}, + status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) @property def _parsed_media_types(self): -- cgit v1.2.3 From ca96b4523b4c09489e4bfe726a894a5c6ada78aa Mon Sep 17 00:00:00 2001 From: Sébastien Piquemal Date: Tue, 7 Feb 2012 13:15:30 +0200 Subject: cleaned a bit Response/ResponseMixin code, added some documentation + renamed ErrorResponse to ImmediateResponse --- djangorestframework/request.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'djangorestframework/request.py') diff --git a/djangorestframework/request.py b/djangorestframework/request.py index ee43857e..21538aec 100644 --- a/djangorestframework/request.py +++ b/djangorestframework/request.py @@ -11,7 +11,7 @@ This enhanced request object offers the following : from django.http import HttpRequest -from djangorestframework.response import ErrorResponse +from djangorestframework.response import ImmediateResponse from djangorestframework import status from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence from djangorestframework.utils import as_tuple @@ -194,7 +194,7 @@ class Request(object): """ Parse the request content. - May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request). + May raise a 415 ImmediateResponse (Unsupported Media Type), or a 400 ImmediateResponse (Bad Request). """ if stream is None or content_type is None: return (None, None) @@ -206,7 +206,7 @@ class Request(object): if parser.can_handle_request(content_type): return parser.parse(stream) - raise ErrorResponse(content={'error': + raise ImmediateResponse(content={'error': 'Unsupported media type in request \'%s\'.' % content_type}, status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) -- cgit v1.2.3 From 21292d31e7ad5ec731c9ef3e471f90cb29054686 Mon Sep 17 00:00:00 2001 From: Sébastien Piquemal Date: Tue, 7 Feb 2012 15:38:54 +0200 Subject: cleaned Request/Response/mixins to have similar interface --- djangorestframework/request.py | 95 +++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 56 deletions(-) (limited to 'djangorestframework/request.py') diff --git a/djangorestframework/request.py b/djangorestframework/request.py index 21538aec..cd6e3097 100644 --- a/djangorestframework/request.py +++ b/djangorestframework/request.py @@ -1,10 +1,10 @@ """ The :mod:`request` module provides a :class:`Request` class that can be used -to enhance the standard `request` object received in all the views. +to wrap the standard `request` object received in all the views, and upgrade its API. -This enhanced request object offers the following : +The wrapped request then offer the following : - - content automatically parsed according to `Content-Type` header, and available as :meth:`request.DATA` + - content automatically parsed according to `Content-Type` header, and available as :meth:`.DATA` - full support of PUT method, including support for file uploads - form overloading of HTTP method, content type and content """ @@ -22,21 +22,9 @@ 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): """ - A mixin class allowing to enhance Django's standard HttpRequest. + A wrapper allowing to enhance Django's standard HttpRequest. """ _USE_FORM_OVERLOADING = True @@ -44,24 +32,14 @@ class Request(object): _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 + def __init__(self, request=None, parsers=None): + """ + `parsers` is a list/tuple of parser instances and represents the set of psrsers + that the response can handle. + """ + self.request = request + if parsers is not None: + self.parsers = parsers @property def method(self): @@ -111,22 +89,6 @@ class Request(object): 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. @@ -145,7 +107,7 @@ class Request(object): 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 + self._method = self.request.method def _get_stream(self): """ @@ -172,7 +134,8 @@ class Request(object): """ # 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): + if (not self._USE_FORM_OVERLOADING or self.request.method != 'POST' + or not is_form_media_type(self._content_type)): return # At this point we're committed to parsing the request as form data. @@ -199,10 +162,7 @@ class Request(object): 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) + for parser in as_tuple(self.parsers): if parser.can_handle_request(content_type): return parser.parse(stream) @@ -223,3 +183,26 @@ class Request(object): Return the view's default parser class. """ return self.parsers[0] + + def _get_parsers(self): + """ + This just provides a default when parsers havent' been set. + """ + if hasattr(self, '_parsers'): + return self._parsers + return () + + def _set_parsers(self, value): + self._parsers = value + + parsers = property(_get_parsers, _set_parsers) + + def __getattr__(self, name): + """ + When an attribute is not present on the calling instance, try to get it + from the original request. + """ + if hasattr(self.request, name): + return getattr(self.request, name) + else: + return super(Request, self).__getattribute__(name) -- cgit v1.2.3 From 6963fd3623ee217fe489abb25f0ffa8c0781e4cd Mon Sep 17 00:00:00 2001 From: Sébastien Piquemal Date: Tue, 7 Feb 2012 16:22:14 +0200 Subject: some docs for Request/Response/mixins --- djangorestframework/request.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) (limited to 'djangorestframework/request.py') diff --git a/djangorestframework/request.py b/djangorestframework/request.py index cd6e3097..8cf95f18 100644 --- a/djangorestframework/request.py +++ b/djangorestframework/request.py @@ -1,8 +1,8 @@ """ -The :mod:`request` module provides a :class:`Request` class that can be used -to wrap the standard `request` object received in all the views, and upgrade its API. +The :mod:`request` module provides a :class:`Request` class used to wrap the standard `request` +object received in all the views. -The wrapped request then offer the following : +The wrapped request then offers a richer API, in particular : - content automatically parsed according to `Content-Type` header, and available as :meth:`.DATA` - full support of PUT method, including support for file uploads @@ -24,7 +24,11 @@ __all__ = ('Request',) class Request(object): """ - A wrapper allowing to enhance Django's standard HttpRequest. + Wrapper allowing to enhance a standard `HttpRequest` instance. + + Kwargs: + - request(HttpRequest). The original request instance. + - parsers(list/tuple). The parsers to use for parsing the request content. """ _USE_FORM_OVERLOADING = True @@ -33,10 +37,6 @@ class Request(object): _CONTENT_PARAM = '_content' def __init__(self, request=None, parsers=None): - """ - `parsers` is a list/tuple of parser instances and represents the set of psrsers - that the response can handle. - """ self.request = request if parsers is not None: self.parsers = parsers @@ -185,9 +185,6 @@ class Request(object): return self.parsers[0] def _get_parsers(self): - """ - This just provides a default when parsers havent' been set. - """ if hasattr(self, '_parsers'): return self._parsers return () -- cgit v1.2.3 From db0b01037a95946938ccd44eae14d8779bfff1a9 Mon Sep 17 00:00:00 2001 From: Sébastien Piquemal Date: Fri, 10 Feb 2012 10:18:39 +0200 Subject: made suggested fixes --- djangorestframework/request.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'djangorestframework/request.py') diff --git a/djangorestframework/request.py b/djangorestframework/request.py index 8cf95f18..d4ea1e01 100644 --- a/djangorestframework/request.py +++ b/djangorestframework/request.py @@ -166,9 +166,9 @@ class Request(object): if parser.can_handle_request(content_type): return parser.parse(stream) - raise ImmediateResponse(content={'error': - 'Unsupported media type in request \'%s\'.' % content_type}, - status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) + raise ImmediateResponse({ + 'error': 'Unsupported media type in request \'%s\'.' % content_type}, + status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) @property def _parsed_media_types(self): -- cgit v1.2.3