mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-10-30 15:37:50 +03:00 
			
		
		
		
	data flattening needs to go into resource
This commit is contained in:
		
							parent
							
								
									49d4e50342
								
							
						
					
					
						commit
						8c3280f9c0
					
				|  | @ -26,7 +26,7 @@ __all__ = ( | |||
|     'ResponseMixin', | ||||
|     'AuthMixin', | ||||
|     'ResourceMixin', | ||||
|     # | ||||
|     # Reverse URL lookup behavior | ||||
|     'InstanceMixin', | ||||
|     # Model behavior mixins | ||||
|     'ReadModelMixin', | ||||
|  | @ -360,7 +360,6 @@ class AuthMixin(object): | |||
|     """ | ||||
|     The set of authentication types that this view can handle. | ||||
|      | ||||
|      | ||||
|     Should be a tuple/list of classes as described in the ``authentication`` module.     | ||||
|     """ | ||||
|     authentication = () | ||||
|  |  | |||
|  | @ -63,9 +63,18 @@ class BaseParser(object): | |||
| 
 | ||||
| 
 | ||||
| class JSONParser(BaseParser): | ||||
|     """ | ||||
|     JSON parser. | ||||
|     """ | ||||
|     media_type = 'application/json' | ||||
| 
 | ||||
|     def parse(self, stream): | ||||
|         """ | ||||
|         Returns a 2-tuple of `(data, files)`. | ||||
| 
 | ||||
|         `data` will be an object which is the parsed content of the response. | ||||
|         `files` will always be `None`. | ||||
|         """ | ||||
|         try: | ||||
|             return (json.load(stream), None) | ||||
|         except ValueError, exc: | ||||
|  | @ -73,103 +82,55 @@ class JSONParser(BaseParser): | |||
|                                 {'detail': 'JSON parse error - %s' % unicode(exc)}) | ||||
| 
 | ||||
| 
 | ||||
| class DataFlatener(object): | ||||
|     """Utility object for flattening dictionaries of lists. Useful for "urlencoded" decoded data.""" | ||||
| 
 | ||||
|     def flatten_data(self, data): | ||||
|         """Given a data dictionary {<key>: <value_list>}, returns a flattened dictionary | ||||
|         with information provided by the method "is_a_list".""" | ||||
|         flatdata = dict() | ||||
|         for key, val_list in data.items(): | ||||
|             if self.is_a_list(key, val_list): | ||||
|                 flatdata[key] = val_list | ||||
|             else: | ||||
|                 if val_list: | ||||
|                     flatdata[key] = val_list[0] | ||||
|                 else: | ||||
|                     # 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_list): | ||||
|         """Returns True if the parameter with name *key* is expected to be a list, or False otherwise. | ||||
|         *val_list* which is the received value for parameter *key* can be used to guess the answer.""" | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| class PlainTextParser(BaseParser): | ||||
|     """ | ||||
|     Plain text parser. | ||||
|      | ||||
|     Simply returns the content of the stream. | ||||
|     """ | ||||
|     media_type = 'text/plain' | ||||
| 
 | ||||
|     def parse(self, stream): | ||||
|         """ | ||||
|         Returns a 2-tuple of `(data, files)`. | ||||
|          | ||||
|         `data` will simply be a string representing the body of the request. | ||||
|         `files` will always be `None`. | ||||
|         """ | ||||
|         return (stream.read(), None) | ||||
| 
 | ||||
| 
 | ||||
| class FormParser(BaseParser, DataFlatener): | ||||
| class FormParser(BaseParser): | ||||
|     """ | ||||
|     Parser for form data. | ||||
|     """ | ||||
|     The default parser for form data. | ||||
|     Return a dict containing a single value for each non-reserved parameter. | ||||
| 
 | ||||
|     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' | ||||
| 
 | ||||
|     """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, stream): | ||||
|         """ | ||||
|         Returns a 2-tuple of `(data, files)`. | ||||
|          | ||||
|         `data` will be a `QueryDict` containing all the form parameters. | ||||
|         `files` will always be `None`. | ||||
|         """ | ||||
|         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(): | ||||
|             self.remove_empty_val(val_list) | ||||
|         data = self.flatten_data(data) | ||||
| 
 | ||||
|         # Strip any parameters that we are treating as reserved | ||||
|         for key in data.keys(): | ||||
|             if key in self.RESERVED_FORM_PARAMS: | ||||
|                 data.pop(key) | ||||
| 
 | ||||
|         return (data, None) | ||||
| 
 | ||||
|     def remove_empty_val(self, val_list): | ||||
|         """ """ | ||||
|         while(1): # Because there might be several times EMPTY_VALUE in the list | ||||
|             try:  | ||||
|                 ind = val_list.index(self.EMPTY_VALUE) | ||||
|             except ValueError: | ||||
|                 break | ||||
|             else: | ||||
|                 val_list.pop(ind)  | ||||
| 
 | ||||
| class MultiPartParser(BaseParser): | ||||
|     """ | ||||
|     Parser for multipart form data, which may include file data. | ||||
|     """ | ||||
| 
 | ||||
| class MultiPartParser(BaseParser, DataFlatener): | ||||
|     media_type = 'multipart/form-data' | ||||
|     RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',) | ||||
| 
 | ||||
|     def parse(self, stream): | ||||
|         """ | ||||
|         Returns a 2-tuple of `(data, files)`. | ||||
|          | ||||
|         `data` will be a `QueryDict` containing all the form parameters. | ||||
|         `files` will be a `QueryDict` containing all the form files. | ||||
|         """ | ||||
|         upload_handlers = self.view.request._get_upload_handlers() | ||||
|         django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers) | ||||
|         data, files = django_parser.parse() | ||||
|         return django_parser.parse() | ||||
| 
 | ||||
|         # Flatening data, files and combining them | ||||
|         data = self.flatten_data(dict(data.iterlists())) | ||||
|         files = self.flatten_data(dict(files.iterlists())) | ||||
| 
 | ||||
|         # Strip any parameters that we are treating as reserved | ||||
|         for key in data.keys(): | ||||
|             if key in self.RESERVED_FORM_PARAMS: | ||||
|                 data.pop(key) | ||||
|          | ||||
|         return (data, files) | ||||
|  |  | |||
|  | @ -59,6 +59,7 @@ class IsAuthenticated(BasePermission): | |||
|         if not user.is_authenticated(): | ||||
|             raise _403_FORBIDDEN_RESPONSE  | ||||
| 
 | ||||
| 
 | ||||
| class IsAdminUser(): | ||||
|     """ | ||||
|     Allows access only to admin users. | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ class TestContentParsing(TestCase): | |||
|         form_data = {'qwerty': 'uiop'} | ||||
|         view.parsers = (FormParser, MultiPartParser) | ||||
|         view.request = self.req.put('/', data=form_data) | ||||
|         self.assertEqual(view.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.""" | ||||
|  |  | |||
|  | @ -1,133 +1,133 @@ | |||
| """ | ||||
| .. | ||||
|     >>> from djangorestframework.parsers import FormParser | ||||
|     >>> from djangorestframework.compat import RequestFactory | ||||
|     >>> from djangorestframework.views import BaseView | ||||
|     >>> from StringIO import StringIO | ||||
|     >>> from urllib import urlencode | ||||
|     >>> req = RequestFactory().get('/') | ||||
|     >>> some_view = BaseView() | ||||
|     >>> some_view.request = req  # Make as if this request had been dispatched | ||||
| 
 | ||||
| FormParser | ||||
| ============ | ||||
| 
 | ||||
| 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 : | ||||
| 
 | ||||
|     >>> (data, files) = FormParser(some_view).parse(StringIO(inpt)) | ||||
|     >>> data == {'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. | ||||
| 
 | ||||
|     >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) | ||||
|     >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} | ||||
|     True | ||||
| 
 | ||||
| .. note:: The same functionality is available for :class:`parsers.MultiPartParser`. | ||||
| 
 | ||||
| Submitting an empty list | ||||
| -------------------------- | ||||
| 
 | ||||
| When submitting an empty select multiple, like this one :: | ||||
| 
 | ||||
|     <select multiple="multiple" name="key2"></select> | ||||
| 
 | ||||
| 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 :: | ||||
| 
 | ||||
|     <select multiple="multiple" name="key2"><option value="_empty"></select> | ||||
| 
 | ||||
| :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. | ||||
| 
 | ||||
|     >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) | ||||
|     >>> data == {'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' | ||||
|     ...  | ||||
|     >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) | ||||
|     >>> data == {'key1': 'blo1', 'key2': []} | ||||
|     True | ||||
| 
 | ||||
| Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`. | ||||
| """ | ||||
| import httplib, mimetypes | ||||
| from tempfile import TemporaryFile | ||||
| from django.test import TestCase | ||||
| from djangorestframework.compat import RequestFactory | ||||
| from djangorestframework.parsers import MultiPartParser | ||||
| from djangorestframework.views import BaseView | ||||
| from StringIO import StringIO | ||||
| 
 | ||||
| def encode_multipart_formdata(fields, files): | ||||
|     """For testing multipart parser. | ||||
|     fields is a sequence of (name, value) elements for regular form fields. | ||||
|     files is a sequence of (name, filename, value) elements for data to be uploaded as files | ||||
|     Return (content_type, body).""" | ||||
|     BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' | ||||
|     CRLF = '\r\n' | ||||
|     L = [] | ||||
|     for (key, value) in fields: | ||||
|         L.append('--' + BOUNDARY) | ||||
|         L.append('Content-Disposition: form-data; name="%s"' % key) | ||||
|         L.append('') | ||||
|         L.append(value) | ||||
|     for (key, filename, value) in files: | ||||
|         L.append('--' + BOUNDARY) | ||||
|         L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) | ||||
|         L.append('Content-Type: %s' % get_content_type(filename)) | ||||
|         L.append('') | ||||
|         L.append(value) | ||||
|     L.append('--' + BOUNDARY + '--') | ||||
|     L.append('') | ||||
|     body = CRLF.join(L) | ||||
|     content_type = 'multipart/form-data; boundary=%s' % BOUNDARY | ||||
|     return content_type, body | ||||
| 
 | ||||
| def get_content_type(filename): | ||||
|     return mimetypes.guess_type(filename)[0] or 'application/octet-stream' | ||||
| 
 | ||||
| class TestMultiPartParser(TestCase): | ||||
|     def setUp(self): | ||||
|         self.req = RequestFactory() | ||||
|         self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')], | ||||
|         [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')]) | ||||
| 
 | ||||
|     def test_multipartparser(self): | ||||
|         """Ensure that MultiPartParser can parse multipart/form-data that contains a mix of several files and parameters.""" | ||||
|         post_req = RequestFactory().post('/', self.body, content_type=self.content_type) | ||||
|         view = BaseView() | ||||
|         view.request = post_req | ||||
|         (data, files) = MultiPartParser(view).parse(StringIO(self.body)) | ||||
|         self.assertEqual(data['key1'], 'val1') | ||||
|         self.assertEqual(files['file1'].read(), 'blablabla') | ||||
| # """ | ||||
| # .. | ||||
| #     >>> from djangorestframework.parsers import FormParser | ||||
| #     >>> from djangorestframework.compat import RequestFactory | ||||
| #     >>> from djangorestframework.views import BaseView | ||||
| #     >>> from StringIO import StringIO | ||||
| #     >>> from urllib import urlencode | ||||
| #     >>> req = RequestFactory().get('/') | ||||
| #     >>> some_view = BaseView() | ||||
| #     >>> some_view.request = req  # Make as if this request had been dispatched | ||||
| #  | ||||
| # FormParser | ||||
| # ============ | ||||
| #  | ||||
| # 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 : | ||||
| #  | ||||
| #     >>> (data, files) = FormParser(some_view).parse(StringIO(inpt)) | ||||
| #     >>> data == {'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. | ||||
| #  | ||||
| #     >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) | ||||
| #     >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} | ||||
| #     True | ||||
| #  | ||||
| # .. note:: The same functionality is available for :class:`parsers.MultiPartParser`. | ||||
| #  | ||||
| # Submitting an empty list | ||||
| # -------------------------- | ||||
| #  | ||||
| # When submitting an empty select multiple, like this one :: | ||||
| #  | ||||
| #     <select multiple="multiple" name="key2"></select> | ||||
| #  | ||||
| # 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 :: | ||||
| #  | ||||
| #     <select multiple="multiple" name="key2"><option value="_empty"></select> | ||||
| #  | ||||
| # :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. | ||||
| #  | ||||
| #     >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) | ||||
| #     >>> data == {'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' | ||||
| #     ...  | ||||
| #     >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) | ||||
| #     >>> data == {'key1': 'blo1', 'key2': []} | ||||
| #     True | ||||
| #  | ||||
| # Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`. | ||||
| # """ | ||||
| # import httplib, mimetypes | ||||
| # from tempfile import TemporaryFile | ||||
| # from django.test import TestCase | ||||
| # from djangorestframework.compat import RequestFactory | ||||
| # from djangorestframework.parsers import MultiPartParser | ||||
| # from djangorestframework.views import BaseView | ||||
| # from StringIO import StringIO | ||||
| #  | ||||
| # def encode_multipart_formdata(fields, files): | ||||
| #     """For testing multipart parser. | ||||
| #     fields is a sequence of (name, value) elements for regular form fields. | ||||
| #     files is a sequence of (name, filename, value) elements for data to be uploaded as files | ||||
| #     Return (content_type, body).""" | ||||
| #     BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' | ||||
| #     CRLF = '\r\n' | ||||
| #     L = [] | ||||
| #     for (key, value) in fields: | ||||
| #         L.append('--' + BOUNDARY) | ||||
| #         L.append('Content-Disposition: form-data; name="%s"' % key) | ||||
| #         L.append('') | ||||
| #         L.append(value) | ||||
| #     for (key, filename, value) in files: | ||||
| #         L.append('--' + BOUNDARY) | ||||
| #         L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) | ||||
| #         L.append('Content-Type: %s' % get_content_type(filename)) | ||||
| #         L.append('') | ||||
| #         L.append(value) | ||||
| #     L.append('--' + BOUNDARY + '--') | ||||
| #     L.append('') | ||||
| #     body = CRLF.join(L) | ||||
| #     content_type = 'multipart/form-data; boundary=%s' % BOUNDARY | ||||
| #     return content_type, body | ||||
| #  | ||||
| # def get_content_type(filename): | ||||
| #     return mimetypes.guess_type(filename)[0] or 'application/octet-stream' | ||||
| #  | ||||
| #class TestMultiPartParser(TestCase): | ||||
| #    def setUp(self): | ||||
| #        self.req = RequestFactory() | ||||
| #        self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')], | ||||
| #        [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')]) | ||||
| # | ||||
| #    def test_multipartparser(self): | ||||
| #        """Ensure that MultiPartParser can parse multipart/form-data that contains a mix of several files and parameters.""" | ||||
| #        post_req = RequestFactory().post('/', self.body, content_type=self.content_type) | ||||
| #        view = BaseView() | ||||
| #        view.request = post_req | ||||
| #        (data, files) = MultiPartParser(view).parse(StringIO(self.body)) | ||||
| #        self.assertEqual(data['key1'], 'val1') | ||||
| #        self.assertEqual(files['file1'].read(), 'blablabla') | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ class MyModelResource(ModelResource): | |||
|     fields = ('foo', 'bar', 'baz', 'url') | ||||
|     ordering = ('created',) | ||||
| 
 | ||||
| urlpatterns = patterns('modelresourceexample.views', | ||||
| urlpatterns = patterns('', | ||||
|     url(r'^$',          ListOrCreateModelView.as_view(resource=MyModelResource), name='model-resource-root'), | ||||
|     url(r'^([0-9]+)/$', InstanceModelView.as_view(resource=MyModelResource)), | ||||
| ) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user