aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2013-03-08 23:00:23 +0000
committerTom Christie2013-03-08 23:00:23 +0000
commit5e993f39294da5e8650c7ac21aeb3da02012b775 (patch)
treed95dfd6c09b6d4b8dfc869f103cb59c65488dcac
parent2596c12a21003d230beb101aa93ddf83a1995305 (diff)
parent6c1fcc855a2d05732113ce260b8660a414e1961e (diff)
downloaddjango-rest-framework-5e993f39294da5e8650c7ac21aeb3da02012b775.tar.bz2
Merge
-rw-r--r--MANIFEST.in2
-rw-r--r--docs/api-guide/filtering.md8
-rw-r--r--docs/api-guide/serializers.md2
-rw-r--r--docs/topics/credits.md2
-rw-r--r--docs/topics/release-notes.md12
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/authtoken/models.py9
-rw-r--r--rest_framework/fields.py11
-rw-r--r--rest_framework/generics.py20
-rw-r--r--rest_framework/mixins.py4
-rw-r--r--rest_framework/serializers.py23
-rw-r--r--rest_framework/tests/authentication.py2
-rw-r--r--rest_framework/tests/fields.py14
-rw-r--r--rest_framework/tests/generics.py75
-rw-r--r--rest_framework/tests/serializer.py40
15 files changed, 200 insertions, 26 deletions
diff --git a/MANIFEST.in b/MANIFEST.in
index 00e45086..15c4d0b0 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
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index 53ea7cbc..ed946368 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/api-guide/serializers.md b/docs/api-guide/serializers.md
index 6f1f2883..de2cf7d8 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_<fieldname>` methods to your `Serializer` subclass. These are analagous to `.clean_<fieldname>` methods on Django forms, but accept slightly different arguments.
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index 0899632f..bdd3e27e 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]
* 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
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 42b1d8da..eb4d378e 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -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
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 2180c509..1f5d6e62 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
diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py
index 7f5a75a3..52c45ad1 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()
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index fe555ee5..0a199f10 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -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)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 9ae8cf0a..36ecf915 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 97201c4b..8e401204 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/serializers.py b/rest_framework/serializers.py
index ba9e9e9c..106e3f17 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:
#
@@ -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):
diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py
index 9e86881a..54ab0c94 100644
--- a/rest_framework/tests/authentication.py
+++ b/rest_framework/tests/authentication.py
@@ -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)
diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py
index 28f18ed8..fd6de779 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):
"""
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index f8f2ddaa..f7093401 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'})
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 51065017..beb372c2 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,
+ [
+ {'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)