diff --git a/djangorestframework/content.py b/djangorestframework/content.py index f9a3c6de6..cfdd33be5 100644 --- a/djangorestframework/content.py +++ b/djangorestframework/content.py @@ -50,7 +50,7 @@ class OverloadedContentMixin(ContentMixin): content_type = None if self.CONTENTTYPE_PARAM and request.POST.get(self.CONTENTTYPE_PARAM, None): content_type = request.POST.get(self.CONTENTTYPE_PARAM, None) - request.META['CONTENT_TYPE'] = content_type + request.META['CONTENT_TYPE'] = content_type # TODO : VERY BAD, avoid modifying original request. return (content_type, request.POST[self.CONTENT_PARAM]) else: diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 081fa6897..d324f687b 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -84,23 +84,23 @@ class DataFlatener(object): def flatten_data(self, data): """Given a data dictionary {: }, returns a flattened dictionary with information provided by the method "is_a_list".""" + data = data.copy() flatdata = dict() - for key, attr_value in data.items(): - if self.is_a_list(key): - if isinstance(attr_value, list): - flatdata[key] = attr_value - else: - flatdata[key] = [attr_value] + for key, val_list in data.items(): + if self.is_a_list(key, val_list): + flatdata[key] = val_list else: - if isinstance(attr_value, list): - flatdata[key] = attr_value[0] + if val_list: + flatdata[key] = val_list[0] else: - flatdata[key] = attr_value + # If the list is empty, but the parameter is not a list, + # we strip this parameter. + data.pop(key) return flatdata - def is_a_list(self, key, val): + def is_a_list(self, key, val_list): """Returns True if the parameter with name *key* is expected to be a list, or False otherwise. - *val* which is the received value for parameter *key* can be used to guess the answer.""" + *val_list* which is the received value for parameter *key* can be used to guess the answer.""" return False class FormParser(BaseParser, DataFlatener): @@ -121,12 +121,12 @@ class FormParser(BaseParser, DataFlatener): EMPTY_VALUE = '_empty' def parse(self, input): - data = parse_qs(input) + data = parse_qs(input, keep_blank_values=True) - # Flatening data and removing EMPTY_VALUEs from the lists + # removing EMPTY_VALUEs from the lists and flatening the data + for key, val_list in data.items(): + self.remove_empty_val(val_list) data = self.flatten_data(data) - for key in filter(lambda k: self.is_a_list(k), data): - self.remove_empty_val(data[key]) # Strip any parameters that we are treating as reserved for key in data.keys(): diff --git a/djangorestframework/tests/__init__.py b/djangorestframework/tests/__init__.py index 55e386b52..5d5b652ab 100644 --- a/djangorestframework/tests/__init__.py +++ b/djangorestframework/tests/__init__.py @@ -10,5 +10,5 @@ __test__ = dict() for module in modules: exec("from djangorestframework.tests.%s import __doc__ as module_doc" % module) exec("from djangorestframework.tests.%s import *" % module) - __test__['%s' % module] = module_doc or "" - + __test__[module] = module_doc or "" + diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py new file mode 100644 index 000000000..a2831b63b --- /dev/null +++ b/djangorestframework/tests/parsers.py @@ -0,0 +1,72 @@ +""" +.. + >>> from djangorestframework.parsers import FormParser + >>> from djangorestframework.resource import Resource + >>> from djangorestframework.compat import RequestFactory + >>> from urllib import urlencode + >>> req = RequestFactory().get('/') + >>> some_resource = Resource() + >>> trash = some_resource.dispatch(req)# Some variables are set only when calling dispatch + +Data flatening +---------------- + +Here is some example data, which would eventually be sent along with a post request : + + >>> inpt = urlencode([ + ... ('key1', 'bla1'), + ... ('key2', 'blo1'), ('key2', 'blo2'), + ... ]) + +Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter : + + >>> FormParser(some_resource).parse(inpt) == {'key1': 'bla1', 'key2': 'blo1'} + True + +However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` : + + >>> class MyFormParser(FormParser): + ... + ... def is_a_list(self, key, val_list): + ... return len(val_list) > 1 + +This new parser only flattens the lists of parameters that contain a single value. + + >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} + True + +Submitting an empty list +-------------------------- + +When submitting an empty select multiple, like this one :: + + + +The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty :: + + + +:class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data : + + >>> inpt = urlencode([ + ... ('key1', 'blo1'), ('key1', '_empty'), + ... ('key2', '_empty'), + ... ]) + +:class:`parsers.FormParser` strips the values ``_empty`` from all the lists. + + >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'blo1'} + True + +Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it. + + >>> class MyFormParser(FormParser): + ... + ... def is_a_list(self, key, val_list): + ... return key == 'key2' + ... + >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'blo1', 'key2': []} + True + +Better like that. Note also that you can configure something else than ``_empty`` for the empty value by setting :class:`parsers.FormParser.EMPTY_VALUE`. +"""