mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-02 20:54:42 +03:00
Merge
This commit is contained in:
commit
5e993f3929
|
@ -1,2 +1,2 @@
|
||||||
recursive-include rest_framework/static *.js *.css *.png
|
recursive-include rest_framework/static *.js *.css *.png
|
||||||
recursive-include rest_framework/templates *.txt *.html
|
recursive-include rest_framework/templates *.html
|
||||||
|
|
|
@ -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
|
## 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:
|
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:
|
||||||
|
|
|
@ -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 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
|
### Field-level validation
|
||||||
|
|
||||||
You can specify custom field-level validation by adding `.validate_<fieldname>` methods to your `Serializer` subclass. These are analagous to `.clean_<fieldname>` methods on Django forms, but accept slightly different arguments.
|
You can specify custom field-level validation by adding `.validate_<fieldname>` methods to your `Serializer` subclass. These are analagous to `.clean_<fieldname>` methods on Django forms, but accept slightly different arguments.
|
||||||
|
|
|
@ -108,6 +108,7 @@ The following people have helped make REST framework great.
|
||||||
* Omer Katz - [thedrow]
|
* Omer Katz - [thedrow]
|
||||||
* Wiliam Souza - [waa]
|
* Wiliam Souza - [waa]
|
||||||
* Jonas Braun - [iekadou]
|
* Jonas Braun - [iekadou]
|
||||||
|
* Ian Dash - [bitmonkey]
|
||||||
* Pierre Dulac - [dulaccc]
|
* Pierre Dulac - [dulaccc]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
@ -251,4 +252,5 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||||
[thedrow]: https://github.com/thedrow
|
[thedrow]: https://github.com/thedrow
|
||||||
[waa]: https://github.com/wiliamsouza
|
[waa]: https://github.com/wiliamsouza
|
||||||
[iekadou]: https://github.com/iekadou
|
[iekadou]: https://github.com/iekadou
|
||||||
|
[bitmonkey]: https://github.com/bitmonkey
|
||||||
[dulaccc]: https://github.com/dulaccc
|
[dulaccc]: https://github.com/dulaccc
|
||||||
|
|
|
@ -40,6 +40,18 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
## 2.2.x series
|
## 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)
|
||||||
|
* 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
|
||||||
|
|
||||||
|
**Date**: 7th March 2013
|
||||||
|
|
||||||
|
* Bugfix: Fix None values for for `DateField`, `DateTimeField` and `TimeField`.
|
||||||
|
|
||||||
### 2.2.2
|
### 2.2.2
|
||||||
|
|
||||||
**Date**: 6th March 2013
|
**Date**: 6th March 2013
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
__version__ = '2.2.2'
|
__version__ = '2.2.3'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import uuid
|
||||||
import hmac
|
import hmac
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from rest_framework.compat import User
|
from rest_framework.compat import User
|
||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,6 +14,14 @@ class Token(models.Model):
|
||||||
user = models.OneToOneField(User, related_name='auth_token')
|
user = models.OneToOneField(User, related_name='auth_token')
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
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):
|
def save(self, *args, **kwargs):
|
||||||
if not self.key:
|
if not self.key:
|
||||||
self.key = self.generate_key()
|
self.key = self.generate_key()
|
||||||
|
|
|
@ -534,8 +534,12 @@ class DateField(WritableField):
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
|
|
||||||
def to_native(self, value):
|
def to_native(self, value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
if isinstance(value, datetime.datetime):
|
if isinstance(value, datetime.datetime):
|
||||||
value = value.date()
|
value = value.date()
|
||||||
|
|
||||||
if self.format.lower() == ISO_8601:
|
if self.format.lower() == ISO_8601:
|
||||||
return value.isoformat()
|
return value.isoformat()
|
||||||
return value.strftime(self.format)
|
return value.strftime(self.format)
|
||||||
|
@ -599,6 +603,9 @@ class DateTimeField(WritableField):
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
|
|
||||||
def to_native(self, value):
|
def to_native(self, value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
if self.format.lower() == ISO_8601:
|
if self.format.lower() == ISO_8601:
|
||||||
return value.isoformat()
|
return value.isoformat()
|
||||||
return value.strftime(self.format)
|
return value.strftime(self.format)
|
||||||
|
@ -649,8 +656,12 @@ class TimeField(WritableField):
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
|
|
||||||
def to_native(self, value):
|
def to_native(self, value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
if isinstance(value, datetime.datetime):
|
if isinstance(value, datetime.datetime):
|
||||||
value = value.time()
|
value = value.time()
|
||||||
|
|
||||||
if self.format.lower() == ISO_8601:
|
if self.format.lower() == ISO_8601:
|
||||||
return value.isoformat()
|
return value.isoformat()
|
||||||
return value.strftime(self.format)
|
return value.strftime(self.format)
|
||||||
|
|
|
@ -18,6 +18,16 @@ class GenericAPIView(views.APIView):
|
||||||
model = None
|
model = None
|
||||||
serializer_class = None
|
serializer_class = None
|
||||||
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
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):
|
def get_serializer_context(self):
|
||||||
"""
|
"""
|
||||||
|
@ -81,16 +91,6 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
|
||||||
paginate_by = api_settings.PAGINATE_BY
|
paginate_by = api_settings.PAGINATE_BY
|
||||||
paginate_by_param = api_settings.PAGINATE_BY_PARAM
|
paginate_by_param = api_settings.PAGINATE_BY_PARAM
|
||||||
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
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):
|
def get_pagination_serializer(self, page=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -97,7 +97,9 @@ class RetrieveModelMixin(object):
|
||||||
Should be mixed in with `SingleObjectAPIView`.
|
Should be mixed in with `SingleObjectAPIView`.
|
||||||
"""
|
"""
|
||||||
def retrieve(self, request, *args, **kwargs):
|
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)
|
serializer = self.get_serializer(self.object)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,7 @@ from django.core.paginator import Page
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
from rest_framework.compat import get_concrete_model
|
from rest_framework.compat import get_concrete_model, six
|
||||||
from rest_framework.compat import six
|
|
||||||
|
|
||||||
# Note: We do the following so that users of the framework can use this style:
|
# Note: We do the following so that users of the framework can use this style:
|
||||||
#
|
#
|
||||||
|
@ -285,10 +284,6 @@ class BaseSerializer(Field):
|
||||||
"""
|
"""
|
||||||
Deserialize primitives -> objects.
|
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]
|
|
||||||
|
|
||||||
self._errors = {}
|
self._errors = {}
|
||||||
if data is not None or files is not None:
|
if data is not None or files is not None:
|
||||||
attrs = self.restore_fields(data, files)
|
attrs = self.restore_fields(data, files)
|
||||||
|
@ -330,7 +325,7 @@ class BaseSerializer(Field):
|
||||||
if self.many is not None:
|
if self.many is not None:
|
||||||
many = self.many
|
many = self.many
|
||||||
else:
|
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:
|
if many:
|
||||||
return [self.to_native(item) for item in obj]
|
return [self.to_native(item) for item in obj]
|
||||||
|
@ -348,19 +343,25 @@ class BaseSerializer(Field):
|
||||||
if self.many is not None:
|
if self.many is not None:
|
||||||
many = self.many
|
many = self.many
|
||||||
else:
|
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:
|
if many:
|
||||||
warnings.warn('Implict list/queryset serialization is due to be deprecated. '
|
warnings.warn('Implict list/queryset serialization is due to be deprecated. '
|
||||||
'Use the `many=True` flag when instantiating the serializer.',
|
'Use the `many=True` flag when instantiating the serializer.',
|
||||||
PendingDeprecationWarning, stacklevel=3)
|
PendingDeprecationWarning, stacklevel=3)
|
||||||
|
|
||||||
# TODO: error data when deserializing lists
|
|
||||||
if many:
|
if many:
|
||||||
ret = [self.from_native(item, None) for item in data]
|
ret = []
|
||||||
ret = self.from_native(data, files)
|
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:
|
if not self._errors:
|
||||||
self.object = ret
|
self.object = ret
|
||||||
|
|
||||||
return self._errors
|
return self._errors
|
||||||
|
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
|
|
|
@ -145,7 +145,7 @@ class SessionAuthTests(TestCase):
|
||||||
|
|
||||||
class TokenAuthTests(TestCase):
|
class TokenAuthTests(TestCase):
|
||||||
"""Token authentication"""
|
"""Token authentication"""
|
||||||
urls = 'rest_framework.tests.authentication'
|
urls = '§.tests.authentication'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||||
|
|
|
@ -171,6 +171,13 @@ class DateFieldTest(TestCase):
|
||||||
|
|
||||||
self.assertEqual('1984 - 07.31', result_1)
|
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):
|
class DateTimeFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -303,6 +310,13 @@ class DateTimeFieldTest(TestCase):
|
||||||
self.assertEqual('1984 - 04:31', result_3)
|
self.assertEqual('1984 - 04:31', result_3)
|
||||||
self.assertEqual('1984 - 04:31', result_4)
|
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):
|
class TimeFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -350,3 +350,78 @@ class TestM2MBrowseableAPI(TestCase):
|
||||||
view = ExampleView().as_view()
|
view = ExampleView().as_view()
|
||||||
response = view(request).render()
|
response = view(request).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
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'})
|
||||||
|
|
|
@ -268,7 +268,16 @@ class ValidationTests(TestCase):
|
||||||
data = ['i am', 'a', 'list']
|
data = ['i am', 'a', 'list']
|
||||||
serializer = CommentSerializer(self.comment, data=data, many=True)
|
serializer = CommentSerializer(self.comment, data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
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,
|
||||||
|
[
|
||||||
|
{'non_field_errors': ['Invalid data']},
|
||||||
|
{'non_field_errors': ['Invalid data']},
|
||||||
|
{'non_field_errors': ['Invalid data']}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
data = 'and i am a string'
|
data = 'and i am a string'
|
||||||
serializer = CommentSerializer(self.comment, data=data)
|
serializer = CommentSerializer(self.comment, data=data)
|
||||||
|
@ -1072,3 +1081,32 @@ class NestedSerializerContextTests(TestCase):
|
||||||
|
|
||||||
# This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers
|
# This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers
|
||||||
AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data
|
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())
|
||||||
|
expected = [{}, {'email': ['This field is required.']}, {}]
|
||||||
|
self.assertEqual(serializer.errors, expected)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user