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/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
|
||||
|
||||
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 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_<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]
|
||||
* Wiliam Souza - [waa]
|
||||
* Jonas Braun - [iekadou]
|
||||
* Ian Dash - [bitmonkey]
|
||||
* Pierre Dulac - [dulaccc]
|
||||
|
||||
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
|
||||
[waa]: https://github.com/wiliamsouza
|
||||
[iekadou]: https://github.com/iekadou
|
||||
[bitmonkey]: https://github.com/bitmonkey
|
||||
[dulaccc]: https://github.com/dulaccc
|
||||
|
|
|
@ -40,6 +40,18 @@ 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)
|
||||
* 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
|
||||
|
||||
**Date**: 6th March 2013
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
__version__ = '2.2.2'
|
||||
__version__ = '2.2.3'
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -534,8 +534,12 @@ 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:
|
||||
return value.isoformat()
|
||||
return value.strftime(self.format)
|
||||
|
@ -599,6 +603,9 @@ 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)
|
||||
|
@ -649,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)
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
#
|
||||
|
@ -285,10 +284,6 @@ 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]
|
||||
|
||||
self._errors = {}
|
||||
if data is not None or files is not None:
|
||||
attrs = self.restore_fields(data, files)
|
||||
|
@ -330,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]
|
||||
|
@ -348,19 +343,25 @@ 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.',
|
||||
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)
|
||||
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
|
||||
|
||||
return self._errors
|
||||
|
||||
def is_valid(self):
|
||||
|
|
|
@ -145,7 +145,7 @@ class SessionAuthTests(TestCase):
|
|||
|
||||
class TokenAuthTests(TestCase):
|
||||
"""Token authentication"""
|
||||
urls = 'rest_framework.tests.authentication'
|
||||
urls = '§.tests.authentication'
|
||||
|
||||
def setUp(self):
|
||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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'})
|
||||
|
|
|
@ -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,
|
||||
[
|
||||
{'non_field_errors': ['Invalid data']},
|
||||
{'non_field_errors': ['Invalid data']},
|
||||
{'non_field_errors': ['Invalid data']}
|
||||
]
|
||||
)
|
||||
|
||||
data = 'and i am a string'
|
||||
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
|
||||
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