mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-11 12:17:24 +03:00
226 lines
7.9 KiB
Python
226 lines
7.9 KiB
Python
"""
|
|
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<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
|
|
|
|
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
|
|
|
|
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.
|
|
"""
|
|
|
|
_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 ImmediateResponse (Unsupported Media Type), or a 400 ImmediateResponse (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 ImmediateResponse(content={'error':
|
|
'Unsupported media type in request \'%s\'.' % content_type},
|
|
status=status.HTTP_415_UNSUPPORTED_MEDIA_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]
|