diff options
| author | tom christie tom@tomchristie.com | 2011-04-02 16:32:37 +0100 |
|---|---|---|
| committer | tom christie tom@tomchristie.com | 2011-04-02 16:32:37 +0100 |
| commit | 4687db680cda52e9836743940e4cf7279b307294 (patch) | |
| tree | 23b9b22eee3c08f6de09295b3c6630f5fb0730fa /djangorestframework/parsers.py | |
| parent | 8845b281fe9aafbc9f9b2a283fafbde9787f4734 (diff) | |
| download | django-rest-framework-4687db680cda52e9836743940e4cf7279b307294.tar.bz2 | |
Refactor to use self.CONTENT to access request body. Get file upload working
Diffstat (limited to 'djangorestframework/parsers.py')
| -rw-r--r-- | djangorestframework/parsers.py | 123 |
1 files changed, 73 insertions, 50 deletions
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 3bd020ad..1503342c 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -1,9 +1,18 @@ -from StringIO import StringIO +"""Django supports parsing the content of an HTTP request, but only for form POST requests. +That behaviour is sufficient for dealing with standard HTML forms, but it doesn't map well +to general HTTP requests. -from django.http.multipartparser import MultiPartParser as DjangoMPParser +We need a method to be able to: +1) Determine the parsed content on a request for methods other than POST (eg typically also PUT) +2) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded + and multipart/form-data. (eg also handle multipart/json) +""" +from django.http.multipartparser import MultiPartParser as DjangoMPParser from djangorestframework.response import ResponseException from djangorestframework import status +from djangorestframework.utils import as_tuple +from djangorestframework.mediatypes import MediaType try: import json @@ -18,22 +27,27 @@ except ImportError: class ParserMixin(object): parsers = () - def parse(self, content_type, content): - # See RFC 2616 sec 3 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 - split = content_type.split(';', 1) - if len(split) > 1: - content_type = split[0] - content_type = content_type.strip() + def parse(self, stream, content_type): + """ + Parse the request content. - media_type_to_parser = dict([(parser.media_type, parser) for parser in self.parsers]) + May raise a 415 ResponseException (Unsupported Media Type), + or a 400 ResponseException (Bad Request). + """ + parsers = as_tuple(self.parsers) - try: - parser = media_type_to_parser[content_type] - except KeyError: + parser = None + for parser_cls in parsers: + if parser_cls.handles(content_type): + parser = parser_cls(self) + break + + if parser is None: raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, - {'error': 'Unsupported media type in request \'%s\'.' % content_type}) - - return parser(self).parse(content) + {'error': 'Unsupported media type in request \'%s\'.' % + content_type.media_type}) + + return parser.parse(stream) @property def parsed_media_types(self): @@ -48,36 +62,41 @@ class ParserMixin(object): class BaseParser(object): - """All parsers should extend BaseParser, specifing a media_type attribute, + """All parsers should extend BaseParser, specifying a media_type attribute, and overriding the parse() method.""" - media_type = None - def __init__(self, resource): - """Initialise the parser with the Resource instance as state, - in case the parser needs to access any metadata on the Resource object.""" - self.resource = resource + def __init__(self, view): + """ + Initialise the parser with the View instance as state, + in case the parser needs to access any metadata on the View object. + + """ + self.view = view - def parse(self, input): - """Given some serialized input, return the deserialized output. - The input will be the raw request content body. The return value may be of - any type, but for many parsers/inputs it might typically be a dict.""" - return input + @classmethod + def handles(self, media_type): + """ + Returns `True` if this parser is able to deal with the given MediaType. + """ + return media_type.match(self.media_type) + + def parse(self, stream): + """Given a stream to read from, return the deserialized output. + The return value may be of any type, but for many parsers it might typically be a dict-like object.""" + raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.") class JSONParser(BaseParser): - media_type = 'application/json' + media_type = MediaType('application/json') - def parse(self, input): + def parse(self, stream): try: - return json.loads(input) + return json.load(stream) except ValueError, exc: raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)}) -class XMLParser(BaseParser): - media_type = 'application/xml' - class DataFlatener(object): """Utility object for flatening dictionaries of lists. Useful for "urlencoded" decoded data.""" @@ -102,6 +121,7 @@ class DataFlatener(object): *val_list* which is the received value for parameter *key* can be used to guess the answer.""" return False + class FormParser(BaseParser, DataFlatener): """The default parser for form data. Return a dict containing a single value for each non-reserved parameter. @@ -109,16 +129,17 @@ class FormParser(BaseParser, DataFlatener): In order to handle select multiple (and having possibly more than a single value for each parameter), you can customize the output by subclassing the method 'is_a_list'.""" - media_type = 'application/x-www-form-urlencoded' + media_type = MediaType('application/x-www-form-urlencoded') """The value of the parameter when the select multiple is empty. Browsers are usually stripping the select multiple that have no option selected from the parameters sent. A common hack to avoid this is to send the parameter with a value specifying that the list is empty. This value will always be stripped before the data is returned.""" EMPTY_VALUE = '_empty' + RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',) - def parse(self, input): - data = parse_qs(input, keep_blank_values=True) + def parse(self, stream): + data = parse_qs(stream.read(), keep_blank_values=True) # removing EMPTY_VALUEs from the lists and flatening the data for key, val_list in data.items(): @@ -127,8 +148,9 @@ class FormParser(BaseParser, DataFlatener): # Strip any parameters that we are treating as reserved for key in data.keys(): - if key in self.resource.RESERVED_FORM_PARAMS: + if key in self.RESERVED_FORM_PARAMS: data.pop(key) + return data def remove_empty_val(self, val_list): @@ -141,27 +163,28 @@ class FormParser(BaseParser, DataFlatener): else: val_list.pop(ind) -# TODO: Allow parsers to specify multiple media_types -class MultipartParser(BaseParser, DataFlatener): - media_type = 'multipart/form-data' - def parse(self, input): +class MultipartData(dict): + def __init__(self, data, files): + dict.__init__(self, data) + self.FILES = files + +class MultipartParser(BaseParser, DataFlatener): + media_type = MediaType('multipart/form-data') + RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',) - request = self.resource.request - #TODO : that's pretty dumb : files are loaded with - #upload_handlers, but as we read the request body completely (input), - #then it kind of misses the point. Why not input as a stream ? - upload_handlers = request._get_upload_handlers() - django_mpp = DjangoMPParser(request.META, StringIO(input), upload_handlers) + def parse(self, stream): + upload_handlers = self.view.request._get_upload_handlers() + django_mpp = DjangoMPParser(self.view.request.META, stream, upload_handlers) data, files = django_mpp.parse() # Flatening data, files and combining them data = self.flatten_data(dict(data.iterlists())) files = self.flatten_data(dict(files.iterlists())) - data.update(files) - + # Strip any parameters that we are treating as reserved for key in data.keys(): - if key in self.resource.RESERVED_FORM_PARAMS: + if key in self.RESERVED_FORM_PARAMS: data.pop(key) - return data + + return MultipartData(data, files) |
