From 1037888505917ec50d02cdfe22e084cacc5994b8 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 6 Mar 2013 21:16:40 +0100 Subject: [PATCH 01/13] Prevent warning: no files found matching '*.txt' under directory 'rest_framework/templates' (there are only .html files in the templates directory). --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 00e450866..15c4d0b08 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ recursive-include rest_framework/static *.js *.css *.png -recursive-include rest_framework/templates *.txt *.html +recursive-include rest_framework/templates *.html From 6bea275de815e37dc6743213eaa1e54a31c473df Mon Sep 17 00:00:00 2001 From: Kevin Stone Date: Wed, 6 Mar 2013 15:15:19 -0800 Subject: [PATCH 02/13] Added failing test cases for giving a DateField or DateTimeField a None value to serialize. Signed-off-by: Kevin Stone --- rest_framework/tests/fields.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 28f18ed89..fd6de7797 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -171,6 +171,13 @@ class DateFieldTest(TestCase): self.assertEqual('1984 - 07.31', result_1) + def test_to_native_none(self): + """ + Make sure from_native() returns None on None param. + """ + f = serializers.DateField(required=False) + self.assertEqual(None, f.to_native(None)) + class DateTimeFieldTest(TestCase): """ @@ -303,6 +310,13 @@ class DateTimeFieldTest(TestCase): self.assertEqual('1984 - 04:31', result_3) self.assertEqual('1984 - 04:31', result_4) + def test_to_native_none(self): + """ + Make sure from_native() returns None on None param. + """ + f = serializers.DateTimeField(required=False) + self.assertEqual(None, f.to_native(None)) + class TimeFieldTest(TestCase): """ From 2f8d8b499ec50bd3832d1a25fd12b671341d02e9 Mon Sep 17 00:00:00 2001 From: Kevin Stone Date: Wed, 6 Mar 2013 15:16:37 -0800 Subject: [PATCH 03/13] Patched DateField and DateTimeField to check for None values before trying to perform date(time) conversion. Signed-off-by: Kevin Stone --- rest_framework/fields.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fe555ee51..e9bae0ec9 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -534,6 +534,8 @@ class DateField(WritableField): raise ValidationError(msg) def to_native(self, value): + if value is None: + return None if isinstance(value, datetime.datetime): value = value.date() if self.format.lower() == ISO_8601: @@ -599,6 +601,8 @@ class DateTimeField(WritableField): raise ValidationError(msg) def to_native(self, value): + if value is None: + return None if self.format.lower() == ISO_8601: return value.isoformat() return value.strftime(self.format) From ad336cc636d98022ea7eda516a04a7937eb32238 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 7 Mar 2013 09:03:53 +0000 Subject: [PATCH 04/13] Fix broken `None` value for `TimeField`. Refs #707. --- rest_framework/fields.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e9bae0ec9..0a199f102 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -536,8 +536,10 @@ class DateField(WritableField): def to_native(self, value): if value is None: return None + if isinstance(value, datetime.datetime): value = value.date() + if self.format.lower() == ISO_8601: return value.isoformat() return value.strftime(self.format) @@ -603,6 +605,7 @@ class DateTimeField(WritableField): def to_native(self, value): if value is None: return None + if self.format.lower() == ISO_8601: return value.isoformat() return value.strftime(self.format) @@ -653,8 +656,12 @@ class TimeField(WritableField): raise ValidationError(msg) def to_native(self, value): + if value is None: + return None + if isinstance(value, datetime.datetime): value = value.time() + if self.format.lower() == ISO_8601: return value.isoformat() return value.strftime(self.format) From 4e80541824bab0712a816716c5c63ec5623370d8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 7 Mar 2013 09:05:13 +0000 Subject: [PATCH 05/13] Version 2.2.3 --- docs/topics/release-notes.md | 6 ++++++ rest_framework/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 42b1d8da2..535da4336 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,12 @@ You can determine your currently installed version using `pip freeze`: ## 2.2.x series +### 2.2.3 + +**Date**: 7th March 2013 + +* Bugfix: Fix None values for for `DateField`, `DateTimeField` and `TimeField`. + ### 2.2.2 **Date**: 6th March 2013 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2180c509a..1f5d6e623 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.2.2' +__version__ = '2.2.3' VERSION = __version__ # synonym From 66605acaf02d46eb899f495137afb4f9ff127ff0 Mon Sep 17 00:00:00 2001 From: Ian Dash Date: Thu, 7 Mar 2013 17:29:25 +0000 Subject: [PATCH 06/13] Errors during deserializing lists now return a list of tuples with index of bad item in data plus usual errors dict --- docs/api-guide/serializers.md | 2 ++ rest_framework/serializers.py | 17 +++++++---- rest_framework/tests/serializer.py | 45 +++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 6f1f28832..de2cf7d8f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -93,6 +93,8 @@ To serialize a queryset instead of an object instance, you should pass the `many When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages. +When deserialising a list of items, errors will be returned as a list of tuples. The first item in an error tuple will be the index of the item with the error in the original data; The second item in the tuple will be a dict with the individual errors for that item. + ### Field-level validation You can specify custom field-level validation by adding `.validate_` methods to your `Serializer` subclass. These are analagous to `.clean_` methods on Django forms, but accept slightly different arguments. diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ba9e9e9cf..80287522f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -286,8 +286,18 @@ class BaseSerializer(Field): Deserialize primitives -> objects. """ if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)): - # TODO: error data when deserializing lists - return [self.from_native(item, None) for item in data] + object_list = list() + error_list = list() + for count, item in enumerate(data): + obj = self.from_native(item, None) + if self._errors: + error_list.append((count, self._errors)) + object_list.append(obj) + if not error_list: + return object_list + + self._errors = error_list + return None self._errors = {} if data is not None or files is not None: @@ -354,9 +364,6 @@ class BaseSerializer(Field): 'Use the `many=True` flag when instantiating the serializer.', PendingDeprecationWarning, stacklevel=3) - # TODO: error data when deserializing lists - if many: - ret = [self.from_native(item, None) for item in data] ret = self.from_native(data, files) if not self._errors: diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 510650173..339109363 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -268,7 +268,16 @@ class ValidationTests(TestCase): data = ['i am', 'a', 'list'] serializer = CommentSerializer(self.comment, data=data, many=True) self.assertEqual(serializer.is_valid(), False) - self.assertEqual(serializer.errors, {'non_field_errors': ['Invalid data']}) + self.assertTrue(isinstance(serializer.errors, list)) + + self.assertEqual( + serializer.errors, + [ + (0, {'non_field_errors': ['Invalid data']}), + (1, {'non_field_errors': ['Invalid data']}), + (2, {'non_field_errors': ['Invalid data']}) + ] + ) data = 'and i am a string' serializer = CommentSerializer(self.comment, data=data) @@ -1072,3 +1081,37 @@ class NestedSerializerContextTests(TestCase): # This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data + + +class DeserializeListTestCase(TestCase): + + def setUp(self): + self.data = { + 'email': 'nobody@nowhere.com', + 'content': 'This is some test content', + 'created': datetime.datetime(2013, 3, 7), + } + + def test_no_errors(self): + data = [self.data.copy() for x in range(0, 3)] + serializer = CommentSerializer(data=data) + self.assertTrue(serializer.is_valid()) + self.assertTrue(isinstance(serializer.object, list)) + self.assertTrue( + all((isinstance(item, Comment) for item in serializer.object)) + ) + + def test_errors_return_as_list(self): + invalid_item = self.data.copy() + invalid_item['email'] = '' + data = [self.data.copy(), invalid_item, self.data.copy()] + + serializer = CommentSerializer(data=data) + self.assertFalse(serializer.is_valid()) + self.assertTrue(isinstance(serializer.errors, list)) + self.assertEqual(1, len(serializer.errors)) + expected = (1, {'email': ['This field is required.']}) + self.assertEqual( + serializer.errors[0], + expected + ) From 1a8f07def8094a1e34a656d83fc7bdba0efff184 Mon Sep 17 00:00:00 2001 From: toran billups Date: Thu, 7 Mar 2013 15:09:59 -0600 Subject: [PATCH 07/13] GenericAPIView now applies filter_backend for list and retrieve api views Before this commit only the MultipleObjectAPIView would apply a filter_backend, leaving the SingleObjectAPIView to return objects you might otherwise expect to have been filtered out. It's worth mentioning that when a SingleObjectAPIView makes a request for an object that should be excluded, a 404 is the expected result. --- rest_framework/generics.py | 20 ++++----- rest_framework/mixins.py | 4 +- rest_framework/tests/generics.py | 75 ++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 9ae8cf0aa..36ecf9150 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -18,6 +18,16 @@ class GenericAPIView(views.APIView): model = None serializer_class = None model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS + filter_backend = api_settings.FILTER_BACKEND + + def filter_queryset(self, queryset): + """ + Given a queryset, filter it with whichever filter backend is in use. + """ + if not self.filter_backend: + return queryset + backend = self.filter_backend() + return backend.filter_queryset(self.request, queryset, self) def get_serializer_context(self): """ @@ -81,16 +91,6 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): paginate_by = api_settings.PAGINATE_BY paginate_by_param = api_settings.PAGINATE_BY_PARAM pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS - filter_backend = api_settings.FILTER_BACKEND - - def filter_queryset(self, queryset): - """ - Given a queryset, filter it with whichever filter backend is in use. - """ - if not self.filter_backend: - return queryset - backend = self.filter_backend() - return backend.filter_queryset(self.request, queryset, self) def get_pagination_serializer(self, page=None): """ diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 97201c4b1..8e4012049 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -97,7 +97,9 @@ class RetrieveModelMixin(object): Should be mixed in with `SingleObjectAPIView`. """ def retrieve(self, request, *args, **kwargs): - self.object = self.get_object() + queryset = self.get_queryset() + filtered_queryset = self.filter_queryset(queryset) + self.object = self.get_object(filtered_queryset) serializer = self.get_serializer(self.object) return Response(serializer.data) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index f8f2ddaac..f70934016 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -350,3 +350,78 @@ class TestM2MBrowseableAPI(TestCase): view = ExampleView().as_view() response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class InclusiveFilterBackend(object): + def filter_queryset(self, request, queryset, view): + return queryset.filter(text='foo') + + +class ExclusiveFilterBackend(object): + def filter_queryset(self, request, queryset, view): + return queryset.filter(text='other') + + +class TestFilterBackendAppliedToViews(TestCase): + + def setUp(self): + """ + Create 3 BasicModel instances to filter on. + """ + items = ['foo', 'bar', 'baz'] + for item in items: + BasicModel(text=item).save() + self.objects = BasicModel.objects + self.data = [ + {'id': obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + self.root_view = RootView.as_view() + self.instance_view = InstanceView.as_view() + self.original_root_backend = getattr(RootView, 'filter_backend') + self.original_instance_backend = getattr(InstanceView, 'filter_backend') + + def tearDown(self): + setattr(RootView, 'filter_backend', self.original_root_backend) + setattr(InstanceView, 'filter_backend', self.original_instance_backend) + + def test_get_root_view_filters_by_name_with_filter_backend(self): + """ + GET requests to ListCreateAPIView should return filtered list. + """ + setattr(RootView, 'filter_backend', InclusiveFilterBackend) + request = factory.get('/') + response = self.root_view(request).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data, [{'id': 1, 'text': 'foo'}]) + + def test_get_root_view_filters_out_all_models_with_exclusive_filter_backend(self): + """ + GET requests to ListCreateAPIView should return empty list when all models are filtered out. + """ + setattr(RootView, 'filter_backend', ExclusiveFilterBackend) + request = factory.get('/') + response = self.root_view(request).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, []) + + def test_get_instance_view_filters_out_name_with_filter_backend(self): + """ + GET requests to RetrieveUpdateDestroyAPIView should raise 404 when model filtered out. + """ + setattr(InstanceView, 'filter_backend', ExclusiveFilterBackend) + request = factory.get('/1') + response = self.instance_view(request, pk=1).render() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.data, {'detail': 'Not found'}) + + def test_get_instance_view_will_return_single_object_when_filter_does_not_exclude_it(self): + """ + GET requests to RetrieveUpdateDestroyAPIView should return a single object when not excluded + """ + setattr(InstanceView, 'filter_backend', InclusiveFilterBackend) + request = factory.get('/1') + response = self.instance_view(request, pk=1).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'id': 1, 'text': 'foo'}) From 4d48de631baee39025da04b95f46051d7398bd6c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 8 Mar 2013 20:41:00 +0000 Subject: [PATCH 08/13] Docs on per-object filtering --- docs/api-guide/filtering.md | 8 ++++++++ docs/topics/release-notes.md | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 53ea7cbcc..ed9463681 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -140,6 +140,14 @@ For more details on using filter sets see the [django-filter documentation][djan --- +### Filtering and object lookups + +Note that if a filter backend is configured for a view, then as well as being used to filter list views, it will also be used to filter the querysets used for returning a single object. + +For instance, given the previous example, and a product with an id of `4675`, the following URL would either return the corresponding object, or return a 404 response, depending on if the filtering conditions were met by the given product instance: + + http://example.com/api/products/4675/?category=clothing&max_price=10.00 + ## Overriding the initial queryset Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this: diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 535da4336..ab675950a 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,10 @@ You can determine your currently installed version using `pip freeze`: ## 2.2.x series +### Master + +* Filtering backends are now applied to the querysets for object lookups as well as lists. (Eg you can use a filtering backend to control which objects should 404) + ### 2.2.3 **Date**: 7th March 2013 From c5b98f0d106758298edf045e7bb44ecd7e4b9629 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 8 Mar 2013 20:56:30 +0000 Subject: [PATCH 09/13] authtoken abstract if not installed. Fixes #705. --- docs/topics/release-notes.md | 1 + rest_framework/authtoken/models.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index ab675950a..a4262d981 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,6 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * Filtering backends are now applied to the querysets for object lookups as well as lists. (Eg you can use a filtering backend to control which objects should 404) +* Bugfix: Workaround for Django bug causing case where `Authtoken` could be registered for cascade delete from `User` even if not installed. ### 2.2.3 diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 7f5a75a3d..52c45ad11 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -2,6 +2,7 @@ import uuid import hmac from hashlib import sha1 from rest_framework.compat import User +from django.conf import settings from django.db import models @@ -13,6 +14,14 @@ class Token(models.Model): user = models.OneToOneField(User, related_name='auth_token') created = models.DateTimeField(auto_now_add=True) + class Meta: + # Work around for a bug in Django: + # https://code.djangoproject.com/ticket/19422 + # + # Also see corresponding ticket: + # https://github.com/tomchristie/django-rest-framework/issues/705 + abstract = 'rest_framework.authtoken' not in settings.INSTALLED_APPS + def save(self, *args, **kwargs): if not self.key: self.key = self.generate_key() From 68683b2ea2907f367fdff60de91656504a242a14 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 8 Mar 2013 22:19:09 +0000 Subject: [PATCH 10/13] Tweak implementation, and use FormSet style errors --- rest_framework/serializers.py | 24 +++++++++--------------- rest_framework/tests/serializer.py | 15 +++++---------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 80287522f..25790dbc4 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -285,20 +285,6 @@ class BaseSerializer(Field): """ Deserialize primitives -> objects. """ - if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)): - object_list = list() - error_list = list() - for count, item in enumerate(data): - obj = self.from_native(item, None) - if self._errors: - error_list.append((count, self._errors)) - object_list.append(obj) - if not error_list: - return object_list - - self._errors = error_list - return None - self._errors = {} if data is not None or files is not None: attrs = self.restore_fields(data, files) @@ -364,7 +350,15 @@ class BaseSerializer(Field): 'Use the `many=True` flag when instantiating the serializer.', PendingDeprecationWarning, stacklevel=3) - ret = self.from_native(data, files) + if many: + ret = [] + errors = [] + for item in data: + ret.append(self.from_native(item, None)) + errors.append(self._errors) + self._errors = any(errors) and errors or [] + else: + ret = self.from_native(data, files) if not self._errors: self.object = ret diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 339109363..394af8278 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -273,9 +273,9 @@ class ValidationTests(TestCase): self.assertEqual( serializer.errors, [ - (0, {'non_field_errors': ['Invalid data']}), - (1, {'non_field_errors': ['Invalid data']}), - (2, {'non_field_errors': ['Invalid data']}) + {'non_field_errors': ['Invalid data']}, + {'non_field_errors': ['Invalid data']}, + {'non_field_errors': ['Invalid data']} ] ) @@ -1108,10 +1108,5 @@ class DeserializeListTestCase(TestCase): serializer = CommentSerializer(data=data) self.assertFalse(serializer.is_valid()) - self.assertTrue(isinstance(serializer.errors, list)) - self.assertEqual(1, len(serializer.errors)) - expected = (1, {'email': ['This field is required.']}) - self.assertEqual( - serializer.errors[0], - expected - ) + expected = [{}, {'email': [u'This field is required.']}, {}] + self.assertEqual(serializer.errors, expected) From 0b6267d8cd45995585f0c02a4f9c96c0691fd32f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 8 Mar 2013 22:28:59 +0000 Subject: [PATCH 11/13] Added @bitmonkey. Thanks! For work on handling errors when deserializing lists of objects. --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 190ce490e..d6f312ed8 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -108,6 +108,7 @@ The following people have helped make REST framework great. * Omer Katz - [thedrow] * Wiliam Souza - [waa] * Jonas Braun - [iekadou] +* Ian Dash - [bitmonkey] Many thanks to everyone who's contributed to the project. @@ -250,3 +251,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [thedrow]: https://github.com/thedrow [waa]: https://github.com/wiliamsouza [iekadou]: https://github.com/iekadou +[bitmonkey]: https://github.com/bitmonkey From 28ae26466e1b1493feeba19480c6eb148d603330 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 8 Mar 2013 22:43:46 +0000 Subject: [PATCH 12/13] Py3k fixes. --- rest_framework/serializers.py | 8 ++++---- rest_framework/tests/serializer.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 25790dbc4..106e3f17a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -7,8 +7,7 @@ from django.core.paginator import Page from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict -from rest_framework.compat import get_concrete_model -from rest_framework.compat import six +from rest_framework.compat import get_concrete_model, six # Note: We do the following so that users of the framework can use this style: # @@ -326,7 +325,7 @@ class BaseSerializer(Field): if self.many is not None: many = self.many else: - many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict)) + many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict, six.text_type)) if many: return [self.to_native(item) for item in obj] @@ -344,7 +343,7 @@ class BaseSerializer(Field): if self.many is not None: many = self.many else: - many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict)) + many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type)) if many: warnings.warn('Implict list/queryset serialization is due to be deprecated. ' 'Use the `many=True` flag when instantiating the serializer.', @@ -362,6 +361,7 @@ class BaseSerializer(Field): if not self._errors: self.object = ret + return self._errors def is_valid(self): diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 394af8278..beb372c2b 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1108,5 +1108,5 @@ class DeserializeListTestCase(TestCase): serializer = CommentSerializer(data=data) self.assertFalse(serializer.is_valid()) - expected = [{}, {'email': [u'This field is required.']}, {}] + expected = [{}, {'email': ['This field is required.']}, {}] self.assertEqual(serializer.errors, expected) From 6c1fcc855a2d05732113ce260b8660a414e1961e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 8 Mar 2013 22:46:37 +0000 Subject: [PATCH 13/13] Update release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index a4262d981..eb4d378eb 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,6 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * Filtering backends are now applied to the querysets for object lookups as well as lists. (Eg you can use a filtering backend to control which objects should 404) +* Deal with error data nicely when deserializing lists of objects. * Bugfix: Workaround for Django bug causing case where `Authtoken` could be registered for cascade delete from `User` even if not installed. ### 2.2.3