From 3d85473edf847ba64aa499b336ca21f6b3d3c6b8 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Wed, 18 Feb 2015 21:00:12 +0300 Subject: [PATCH 01/28] Fix UniqueTogetherValidator for NULL values --- rest_framework/validators.py | 4 +++- tests/test_validators.py | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index e3719b8d5..c030abdba 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -138,7 +138,9 @@ class UniqueTogetherValidator: queryset = self.queryset queryset = self.filter_queryset(attrs, queryset) queryset = self.exclude_current_instance(attrs, queryset) - if queryset.exists(): + + # Ignore validation if any field is None + if None not in attrs.values() and queryset.exists(): field_names = ', '.join(self.fields) raise ValidationError(self.message.format(field_names=field_names)) diff --git a/tests/test_validators.py b/tests/test_validators.py index 072cec360..185febf83 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -76,8 +76,8 @@ class TestUniquenessValidation(TestCase): # ----------------------------------- class UniquenessTogetherModel(models.Model): - race_name = models.CharField(max_length=100) - position = models.IntegerField() + race_name = models.CharField(max_length=100, null=True) + position = models.IntegerField(null=True) class Meta: unique_together = ('race_name', 'position') @@ -108,8 +108,8 @@ class TestUniquenessTogetherValidation(TestCase): expected = dedent(""" UniquenessTogetherSerializer(): id = IntegerField(label='ID', read_only=True) - race_name = CharField(max_length=100, required=True) - position = IntegerField(required=True) + race_name = CharField(allow_null=True, max_length=100, required=True) + position = IntegerField(allow_null=True, required=True) class Meta: validators = [] """) @@ -178,10 +178,20 @@ class TestUniquenessTogetherValidation(TestCase): expected = dedent(""" ExcludedFieldSerializer(): id = IntegerField(label='ID', read_only=True) - race_name = CharField(max_length=100) + race_name = CharField(allow_null=True, max_length=100, required=False) """) assert repr(serializer) == expected + def test_ignore_validation_for_null_fields(self): + UniquenessTogetherModel.objects.create( + race_name=None, + position=None + ) + data = {'race_name': None, 'position': None} + serializer = UniquenessTogetherSerializer(data=data) + + assert serializer.is_valid() + # Tests for `UniqueForDateValidator` # ---------------------------------- From fe8d95f93e11d801d07c8852b12abb4f6b21e1e6 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Thu, 19 Feb 2015 18:03:44 +0300 Subject: [PATCH 02/28] Skip validation of NULL field only if it part of unique_together --- rest_framework/validators.py | 5 +++- tests/test_validators.py | 54 ++++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index c030abdba..ab3616149 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -140,7 +140,10 @@ class UniqueTogetherValidator: queryset = self.exclude_current_instance(attrs, queryset) # Ignore validation if any field is None - if None not in attrs.values() and queryset.exists(): + checked_values = [ + value for field, value in attrs.items() if field in self.fields + ] + if None not in checked_values and queryset.exists(): field_names = ', '.join(self.fields) raise ValidationError(self.message.format(field_names=field_names)) diff --git a/tests/test_validators.py b/tests/test_validators.py index 185febf83..c4c60b7fe 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -83,11 +83,37 @@ class UniquenessTogetherModel(models.Model): unique_together = ('race_name', 'position') +class NullUniquenessTogetherModel(models.Model): + """ + Used to ensure that null values are not included when checking + unique_together constraints. + + Ignoring items which have a null in any of the validated fields is the same + behavior that database backends will use when they have the + unique_together constraint added. + + Example case: a null position could indicate a non-finisher in the race, + there could be many non-finishers in a race, but all non-NULL + values *should* be unique against the given `race_name`. + """ + date_of_birth = models.DateField(null=True) # Not part of the uniqueness constraint + race_name = models.CharField(max_length=100) + position = models.IntegerField(null=True) + + class Meta: + unique_together = ('race_name', 'position') + + class UniquenessTogetherSerializer(serializers.ModelSerializer): class Meta: model = UniquenessTogetherModel +class NullUniquenessTogetherSerializer(serializers.ModelSerializer): + class Meta: + model = NullUniquenessTogetherModel + + class TestUniquenessTogetherValidation(TestCase): def setUp(self): self.instance = UniquenessTogetherModel.objects.create( @@ -183,15 +209,33 @@ class TestUniquenessTogetherValidation(TestCase): assert repr(serializer) == expected def test_ignore_validation_for_null_fields(self): - UniquenessTogetherModel.objects.create( - race_name=None, + # None values that are on fields which are part of the uniqueness + # constraint cause the instance to ignore uniqueness validation. + NullUniquenessTogetherModel.objects.create( + date_of_birth=datetime.date(2000, 1, 1), + race_name='Paris Marathon', position=None ) - data = {'race_name': None, 'position': None} - serializer = UniquenessTogetherSerializer(data=data) - + data = { + 'date': datetime.date(2000, 1, 1), + 'race_name': 'Paris Marathon', + 'position': None + } + serializer = NullUniquenessTogetherSerializer(data=data) assert serializer.is_valid() + def test_do_not_ignore_validation_for_null_fields(self): + # None values that are not on fields part of the uniqueness constraint + # do not cause the instance to skip validation. + NullUniquenessTogetherModel.objects.create( + date_of_birth=datetime.date(2000, 1, 1), + race_name='Paris Marathon', + position=1 + ) + data = {'date': None, 'race_name': 'Paris Marathon', 'position': 1} + serializer = NullUniquenessTogetherSerializer(data=data) + assert not serializer.is_valid() + # Tests for `UniqueForDateValidator` # ---------------------------------- From aa7ed316d842c06d7eb6907d4481d72c747991d7 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Thu, 19 Feb 2015 18:09:04 +0300 Subject: [PATCH 03/28] Return UniquenessTogetherModel to previous state --- tests/test_validators.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index c4c60b7fe..127ec6f8b 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -76,8 +76,8 @@ class TestUniquenessValidation(TestCase): # ----------------------------------- class UniquenessTogetherModel(models.Model): - race_name = models.CharField(max_length=100, null=True) - position = models.IntegerField(null=True) + race_name = models.CharField(max_length=100) + position = models.IntegerField() class Meta: unique_together = ('race_name', 'position') @@ -134,8 +134,8 @@ class TestUniquenessTogetherValidation(TestCase): expected = dedent(""" UniquenessTogetherSerializer(): id = IntegerField(label='ID', read_only=True) - race_name = CharField(allow_null=True, max_length=100, required=True) - position = IntegerField(allow_null=True, required=True) + race_name = CharField(max_length=100, required=True) + position = IntegerField(required=True) class Meta: validators = [] """) @@ -204,7 +204,7 @@ class TestUniquenessTogetherValidation(TestCase): expected = dedent(""" ExcludedFieldSerializer(): id = IntegerField(label='ID', read_only=True) - race_name = CharField(allow_null=True, max_length=100, required=False) + race_name = CharField(max_length=100) """) assert repr(serializer) == expected From c0916c2859468f4888d688217baca73747fd3bf7 Mon Sep 17 00:00:00 2001 From: aRkadeFR Date: Fri, 20 Feb 2015 15:59:10 +0100 Subject: [PATCH 04/28] Documentation test fix double word --- docs/api-guide/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index d9a1696dd..1b96b325e 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -14,7 +14,7 @@ Extends [Django's existing `RequestFactory` class][requestfactory]. ## Creating test requests -The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. +The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. from rest_framework.test import APIRequestFactory From bb8690cfb3208440c35a5c35eb65562f7e1729cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 20 Feb 2015 11:43:12 -0400 Subject: [PATCH 05/28] Disable select field if no choices available --- .../rest_framework/horizontal/select_multiple.html | 9 +++++++-- .../templates/rest_framework/inline/select_multiple.html | 9 +++++++-- .../rest_framework/vertical/select_multiple.html | 9 +++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/rest_framework/templates/rest_framework/horizontal/select_multiple.html b/rest_framework/templates/rest_framework/horizontal/select_multiple.html index 01c251fb0..0735f2809 100644 --- a/rest_framework/templates/rest_framework/horizontal/select_multiple.html +++ b/rest_framework/templates/rest_framework/horizontal/select_multiple.html @@ -1,11 +1,16 @@ +{% load i18n %} +{% trans "No items to select." as no_items %} +
{% if field.label %} {% endif %}
- {% for key, text in field.choices.items %} - + + {% empty %} + {% endfor %} {% if field.errors %} diff --git a/rest_framework/templates/rest_framework/inline/select_multiple.html b/rest_framework/templates/rest_framework/inline/select_multiple.html index feddf7abd..5a8b2494b 100644 --- a/rest_framework/templates/rest_framework/inline/select_multiple.html +++ b/rest_framework/templates/rest_framework/inline/select_multiple.html @@ -1,10 +1,15 @@ +{% load i18n %} +{% trans "No items to select." as no_items %} +
{% if field.label %} {% endif %} - {% for key, text in field.choices.items %} - + + {% empty %} + {% endfor %}
diff --git a/rest_framework/templates/rest_framework/vertical/select_multiple.html b/rest_framework/templates/rest_framework/vertical/select_multiple.html index 54839294a..81b25c2a3 100644 --- a/rest_framework/templates/rest_framework/vertical/select_multiple.html +++ b/rest_framework/templates/rest_framework/vertical/select_multiple.html @@ -1,10 +1,15 @@ +{% load i18n %} +{% trans "No items to select." as no_items %} +
{% if field.label %} {% endif %} - {% for key, text in field.choices.items %} - + + {% empty %} + {% endfor %} {% if field.errors %} From 7345830c88615839891f12fd4ed6abee99bb1468 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Fri, 20 Feb 2015 20:12:39 +0100 Subject: [PATCH 06/28] Check if sessions are enabled before calling logout. Closes #2545. --- rest_framework/test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 4f4b7c201..a83d082ab 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -209,7 +209,8 @@ class APIClient(APIRequestFactory, DjangoClient): self.handler._force_user = None self.handler._force_token = None - return super(APIClient, self).logout() + if self.session: + super(APIClient, self).logout() class APITransactionTestCase(testcases.TransactionTestCase): From f29b657798d3f2223275fb33ca95fab2209fc229 Mon Sep 17 00:00:00 2001 From: ludbek Date: Sat, 21 Feb 2015 07:52:56 +0545 Subject: [PATCH 07/28] updated outdated link at testing.md#APIClient --- docs/api-guide/testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index d9a1696dd..9dc3f2bf1 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -115,7 +115,7 @@ Extends [Django's existing `Client` class][client]. ## Making requests -The `APIClient` class supports the same request interface as `APIRequestFactory`. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example: +The `APIClient` class supports the same request interface as Django's standard `Client` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example: from rest_framework.test import APIClient @@ -269,6 +269,6 @@ For example, to add support for using `format='html'` in test requests, you migh } [cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper -[client]: https://docs.djangoproject.com/en/dev/topics/testing/overview/#module-django.test.client +[client]: https://docs.djangoproject.com/en/dev/topics/testing/tools/#the-test-client [requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory [configuration]: #configuration From bdc64d4e7370575a70a167dc2ae5d159610ce184 Mon Sep 17 00:00:00 2001 From: Yannick PEROUX Date: Wed, 25 Feb 2015 11:54:11 +0100 Subject: [PATCH 08/28] Fix removal of url_path on @detail_route and @list_route. Fix # #2583 SimpleRouter.get_routes was popping out the url_path kwarg from list_route and detail_route decorators. This was causing troubles when the route was re-used, for example if the viewset was inherited. --- rest_framework/routers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 6a4184e20..081654b8c 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -171,9 +171,9 @@ class SimpleRouter(BaseRouter): # Dynamic detail routes (@detail_route decorator) for httpmethods, methodname in detail_routes: method_kwargs = getattr(viewset, methodname).kwargs - url_path = method_kwargs.pop("url_path", None) or methodname initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) + url_path = initkwargs.pop("url_path", None) or methodname ret.append(Route( url=replace_methodname(route.url, url_path), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), @@ -184,9 +184,9 @@ class SimpleRouter(BaseRouter): # Dynamic list routes (@list_route decorator) for httpmethods, methodname in list_routes: method_kwargs = getattr(viewset, methodname).kwargs - url_path = method_kwargs.pop("url_path", None) or methodname initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) + url_path = initkwargs.pop("url_path", None) or methodname ret.append(Route( url=replace_methodname(route.url, url_path), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), From 9cafdd1854ccd5215b7a188c5896fb498a59d725 Mon Sep 17 00:00:00 2001 From: Yannick PEROUX Date: Tue, 24 Feb 2015 17:14:53 +0100 Subject: [PATCH 09/28] Add a test for #2583 fix --- tests/test_routers.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_routers.py b/tests/test_routers.py index 948c69bbf..08c58ec70 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -302,12 +302,16 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet): return Response({'method': 'link2'}) +class SubDynamicListAndDetailViewSet(DynamicListAndDetailViewSet): + pass + + class TestDynamicListAndDetailRouter(TestCase): def setUp(self): self.router = SimpleRouter() - def test_list_and_detail_route_decorators(self): - routes = self.router.get_routes(DynamicListAndDetailViewSet) + def _test_list_and_detail_route_decorators(self, viewset): + routes = self.router.get_routes(viewset) decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))] MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path') @@ -336,3 +340,9 @@ class TestDynamicListAndDetailRouter(TestCase): else: method_map = 'get' self.assertEqual(route.mapping[method_map], method_name) + + def test_list_and_detail_route_decorators(self): + self._test_list_and_detail_route_decorators(DynamicListAndDetailViewSet) + + def test_inherited_list_and_detail_route_decorators(self): + self._test_list_and_detail_route_decorators(SubDynamicListAndDetailViewSet) From 940cf2e2e004f913d3cc260fa2b490d33a163b51 Mon Sep 17 00:00:00 2001 From: Yannick PEROUX Date: Wed, 25 Feb 2015 13:29:07 +0100 Subject: [PATCH 10/28] Remove duplicated code in routers.SimpleRouter --- rest_framework/routers.py | 40 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 081654b8c..b1e39ff7d 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -165,34 +165,30 @@ class SimpleRouter(BaseRouter): else: list_routes.append((httpmethods, methodname)) + def _get_dynamic_routes(route, dynamic_routes): + ret = [] + for httpmethods, methodname in dynamic_routes: + method_kwargs = getattr(viewset, methodname).kwargs + initkwargs = route.initkwargs.copy() + initkwargs.update(method_kwargs) + url_path = initkwargs.pop("url_path", None) or methodname + ret.append(Route( + url=replace_methodname(route.url, url_path), + mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), + name=replace_methodname(route.name, url_path), + initkwargs=initkwargs, + )) + + return ret + ret = [] for route in self.routes: if isinstance(route, DynamicDetailRoute): # Dynamic detail routes (@detail_route decorator) - for httpmethods, methodname in detail_routes: - method_kwargs = getattr(viewset, methodname).kwargs - initkwargs = route.initkwargs.copy() - initkwargs.update(method_kwargs) - url_path = initkwargs.pop("url_path", None) or methodname - ret.append(Route( - url=replace_methodname(route.url, url_path), - mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, url_path), - initkwargs=initkwargs, - )) + ret += _get_dynamic_routes(route, detail_routes) elif isinstance(route, DynamicListRoute): # Dynamic list routes (@list_route decorator) - for httpmethods, methodname in list_routes: - method_kwargs = getattr(viewset, methodname).kwargs - initkwargs = route.initkwargs.copy() - initkwargs.update(method_kwargs) - url_path = initkwargs.pop("url_path", None) or methodname - ret.append(Route( - url=replace_methodname(route.url, url_path), - mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, url_path), - initkwargs=initkwargs, - )) + ret += _get_dynamic_routes(route, list_routes) else: # Standard route ret.append(route) From 71619a02c58df8ee56533daa70e5cc5ece278cf7 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 25 Feb 2015 17:58:54 +0100 Subject: [PATCH 11/28] Update third-party-resources.md --- docs/topics/third-party-resources.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index e26e3a2fa..3125601ba 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -188,6 +188,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [hawkrest][hawkrest] - Provides Hawk HTTP Authorization. * [djangorestframework-httpsignature][djangorestframework-httpsignature] - Provides an easy to use HTTP Signature Authentication mechanism. * [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. +* [django-rest-auth][django-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. ### Permissions From e51dc1855c2e0b2c079d5e248e58afea5bc016f7 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 25 Feb 2015 18:51:20 +0100 Subject: [PATCH 12/28] Update authentication.md --- docs/api-guide/authentication.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 4b8110bd6..fe1be7bf0 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -353,6 +353,10 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a [Djoser][djoser] library provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. The package works with a custom user model and it uses token based authentication. This is a ready to use REST implementation of Django authentication system. +## django-rest-auth + +[Django-rest-auth][django-rest-auth] library provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. By having these API endpoints, your client apps such as AngularJS, iOS, Android, and others can communicate to your Django backend site independently via REST APIs for user management. + [cite]: http://jacobian.org/writing/rest-worst-practices/ [http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 @@ -392,3 +396,4 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a [mohawk]: http://mohawk.readthedocs.org/en/latest/ [mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05 [djoser]: https://github.com/sunscrapers/djoser +[django-rest-auth]: https://github.com/Tivix/django-rest-auth From b92d6df66a761de697557cf66168a30db167e043 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 25 Feb 2015 18:53:33 +0100 Subject: [PATCH 13/28] Update third-party-resources.md --- docs/topics/third-party-resources.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index 3125601ba..2f46e1fc4 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -325,3 +325,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-rest-framework-and-angularjs-video]: http://www.youtube.com/watch?v=q8frbgtj020 [web-api-performance-profiling-django-rest-framework]: http://dabapps.com/blog/api-performance-profiling-django-rest-framework/ [api-development-with-django-and-django-rest-framework]: https://bnotions.com/api-development-with-django-and-django-rest-framework/ +[django-rest-auth]: https://github.com/Tivix/django-rest-auth/ From 86c5fa240131fe20121db707b0324a32967987ab Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Wed, 25 Feb 2015 16:20:45 +0100 Subject: [PATCH 14/28] Force-evaluate querysets (see #2602) --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9475e119b..2eef6eeb5 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -13,6 +13,7 @@ response content is handled by parsers and renderers. from __future__ import unicode_literals from django.db import models from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField +from django.db.models import query from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import postgres_fields, unicode_to_repr from rest_framework.utils import model_meta @@ -562,7 +563,7 @@ class ListSerializer(BaseSerializer): """ # Dealing with nested relationships, data can be a Manager, # so, first get a queryset from the Manager if needed - iterable = data.all() if isinstance(data, models.Manager) else data + iterable = data.all() if isinstance(data, (models.Manager, query.QuerySet)) else data return [ self.child.to_representation(item) for item in iterable ] From 03818ed004cbe77459663f92a21691cfb7d9f010 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 12:48:34 +0000 Subject: [PATCH 15/28] Pagination tweaks and docs --- docs/api-guide/pagination.md | 164 ++++++++++++++++++++++++++++++++--- rest_framework/pagination.py | 6 +- 2 files changed, 157 insertions(+), 13 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index bae579a6d..697ba38d5 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -32,14 +32,14 @@ You can also set the pagination class on an individual view by using the `pagina If you want to modify particular aspects of the pagination style, you'll want to override one of the pagination classes, and set the attributes that you want to change. class LargeResultsSetPagination(PageNumberPagination): - paginate_by = 1000 - paginate_by_param = 'page_size' - max_paginate_by = 10000 + page_size = 1000 + page_size_query_param = 'page_size' + max_page_size = 10000 class StandardResultsSetPagination(PageNumberPagination): - paginate_by = 100 - paginate_by_param = 'page_size' - max_paginate_by = 1000 + page_size = 100 + page_size_query_param = 'page_size' + max_page_size = 1000 You can then apply your new style to a view using the `.pagination_class` attribute: @@ -59,15 +59,141 @@ Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. ## PageNumberPagination -**TODO** +This pagination style accepts a single number page number in the request query parameters. + +**Request**: + + GET https://api.example.org/accounts/?page=4 + +**Response**: + + HTTP 200 OK + { + "count": 1023 + "next": "https://api.example.org/accounts/?page=5", + "previous": "https://api.example.org/accounts/?page=3", + "results": [ + … + ] + } + +#### Setup + +To enable the `PageNumberPagination` style globally, use the following configuration, modifying the `DEFAULT_PAGE_SIZE` as desired: + + REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'DEFAULT_PAGE_SIZE': 100 + } + +On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `PageNumberPagination` on a per-view basis. + +#### Configuration + +The `PageNumberPagination` class includes a number of attributes that may be overridden to modify the pagination style. + +To set these attributes you should override the `PageNumberPagination` class, and then enable your custom pagination class as above. + +* `page_size` - A numeric value indicating the page size. If set, this overrides the `DEFAULT_PAGE_SIZE` setting. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key. +* `page_query_param` - A string value indicating the name of the query parameter to use for the pagination control. +* `page_size_query_param` - If set, this is a string value indicating the name of a query parameter that allows the client to set the page size on a per-request basis. Defaults to `None`, indicating that the client may not control the requested page size. +* `max_page_size` - If set, this is a numeric value indicating the maximum allowable requested page size. This attribute is only valid if `page_size_query_param` is also set. +* `last_page_strings` - A list or tuple of string values indicating values that may be used with the `page_query_param` to request the final page in the set. Defaults to `('last',)` +* `template` - The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/numbers.html"`. + +--- ## LimitOffsetPagination -**TODO** +This pagination style mirrors the syntax used when looking up multiple database records. The client includes both a "limit" and an +"offset" query parameter. The limit indicates the maximum number of items to return, and is equivalent to the `page_size` in other styles. The offset indicates the starting position of the query in relation to the complete set of unpaginated items. + +**Request**: + + GET https://api.example.org/accounts/?limit=100&offset=400 + +**Response**: + + HTTP 200 OK + { + "count": 1023 + "next": "https://api.example.org/accounts/?limit=100&offset=500", + "previous": "https://api.example.org/accounts/?limit=100&offset=300", + "results": [ + … + ] + } + +#### Setup + +To enable the `PageNumberPagination` style globally, use the following configuration: + + REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination' + } + +Optionally, you may also set a `DEFAULT_PAGE_SIZE` key. If the `DEFAULT_PAGE_SIZE` parameter is also used then the `limit` query parameter will be optional, and may be omitted by the client. + +On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `LimitOffsetPagination` on a per-view basis. + +#### Configuration + +The `LimitOffsetPagination` class includes a number of attributes that may be overridden to modify the pagination style. + +To set these attributes you should override the `LimitOffsetPagination` class, and then enable your custom pagination class as above. + +* `default_limit` - A numeric value indicating the limit to use if one is not provided by the client in a query parameter. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key. +* `limit_query_param` - A string value indicating the name of the "limit" query parameter. Defaults to `'limit'`. +* `offset_query_param` - A string value indicating the name of the "offset" query parameter. Defaults to `'offset'`. +* `max_limit` - If set this is a numeric value indicating the maximum allowable limit that may be requested by the client. Defaults to `None`. +* `template` - The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/numbers.html"`. + +--- ## CursorPagination -**TODO** +The cursor-based pagination presents an opaque "cursor" indicator that the client may use to page through the result set. This pagination style only presents forward and reverse controls, and does not allow the client to navigate to arbitrary positions. + +Cursor based pagination requires that there is a unique, unchanging ordering of items in the result set. This ordering might typically be a creation timestamp on the records, as this presents a consistent ordering to paginate against. + +Cursor based pagination is more complex than other schemes. It also requires that the result set presents a fixed ordering, and does not allow the client to arbitrarily index into the result set. However it does provide the following benefits: + +* Provides a consistent pagination view. When used properly `CursorPagination` ensures that the client will never see the same item twice when paging through records. +* Supports usage with very large datasets. With extremely large datasets pagination using offset-based pagination styles may become inefficient or unusable. Cursor based pagination schemes instead have fixed-time properties, and do not slow down as the dataset size increases. + +#### Details and limitations + +This implementation of cursor pagination uses a smart "position plus offset" style that allows it to properly support not-strictly-unique values as the ordering. + +It should be noted that using non-unique values the ordering does introduce the possibility of paging artifacts, where pagination consistency is no longer 100% guaranteed. + +**TODO**: Notes on `None`. + +The implementation also supports both forward and reverse pagination, which is often not supported in other implementations. + +For more technical details on the implementation we use for cursor pagination, the ["Building cursors for the Disqus API"][disqus-cursor-api] blog post gives a good overview of the basic approach. + +#### Setup + +To enable the `CursorPagination` style globally, use the following configuration, modifying the `DEFAULT_PAGE_SIZE` as desired: + + REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination', + 'DEFAULT_PAGE_SIZE': 100 + } + +On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `CursorPagination` on a per-view basis. + +#### Configuration + +The `CursorPagination` class includes a number of attributes that may be overridden to modify the pagination style. + +To set these attributes you should override the `CursorPagination` class, and then enable your custom pagination class as above. + +* `page_size` = A numeric value indicating the page size. If set, this overrides the `DEFAULT_PAGE_SIZE` setting. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key. +* `cursor_query_param` = A string value indicating the name of the "cursor" query parameter. Defaults to `'cursor'`. +* `ordering` = This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: `ordering = 'created'`. Any filters on the view which define a `get_ordering` will override this attribute. Defaults to `None`. +* `template` = The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/previous_and_next.html"`. --- @@ -108,7 +234,7 @@ To have your custom pagination class be used by default, use the `DEFAULT_PAGINA REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.LinkHeaderPagination', - 'PAGINATE_BY': 10 + 'DEFAULT_PAGE_SIZE': 10 } API responses for list endpoints will now include a `Link` header, instead of including the pagination links as part of the body of the response, for example: @@ -123,8 +249,25 @@ API responses for list endpoints will now include a `Link` header, instead of in # HTML pagination controls +By default using the pagination classes will cause HTML pagination controls to be displayed in the browsable API. There are two built-in display styles. The `PageNumberPagination` and `LimitOffsetPagination` classes display a list of page numbers with previous and next controls. The `CursorPagination` class displays a simpler style that only displays a previous and next control. + ## Customizing the controls +You can override the templates that render the HTML pagination controls. The two built-in styles are: + +* `rest_framework/pagination/numbers.html` +* `rest_framework/pagination/previous_and_next.html` + +Providing a template with either of these paths in a global template directory will override the default rendering for the relevant pagination classes. + +Alternatively you can disable HTML pagination controls completely by subclassing on of the existing classes, setting `template = None` as an attribute on the class. You'll then need to configure your `DEFAULT_PAGINATION_CLASS` settings key to use your custom class as the default pagination style. + +#### Low-level API + +The low-level API for determining if a pagination class should display the controls or not is exposed as a `display_page_controls` attribute on the pagination instance. Custom pagination classes should be set to `True` in the `paginate_queryset` method if they require the HTML pagination controls to be displayed. + +The `.to_html()` and `.get_html_context()` methods may also be overridden in a custom pagination class in order to further customize how the controls are rendered. + --- # Third party packages @@ -140,3 +283,4 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` [link-header]: ../img/link-header-pagination.png [drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ [paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin +[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/ \ No newline at end of file diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 496500ba5..809858737 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -259,7 +259,7 @@ class PageNumberPagination(BasePagination): ) raise NotFound(msg) - if paginator.count > 1: + if paginator.count > 1 and self.template is not None: # The browsable API should display pagination controls. self.display_page_controls = True @@ -347,7 +347,7 @@ class LimitOffsetPagination(BasePagination): self.offset = self.get_offset(request) self.count = _get_count(queryset) self.request = request - if self.count > self.limit: + if self.count > self.limit and self.template is not None: self.display_page_controls = True return queryset[self.offset:self.offset + self.limit] @@ -518,7 +518,7 @@ class CursorPagination(BasePagination): # Display page controls in the browsable API if there is more # than one page. - if self.has_previous or self.has_next: + if (self.has_previous or self.has_next) and self.template is not None: self.display_page_controls = True return self.page From e72428214c33b889f61ac83c7f39030b4c66317d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 12:53:24 +0000 Subject: [PATCH 16/28] Formally upgrade suport to Django 1.8-beta --- README.md | 2 +- docs/index.md | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index eec809779..045cdbc46 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) -* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-alpha) +* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-beta) # Installation diff --git a/docs/index.md b/docs/index.md index 23781419f..91766a0b8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ Some reasons you might want to use REST framework: REST framework requires the following: * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) -* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7) +* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-beta) The following packages are optional: diff --git a/tox.ini b/tox.ini index b96b4939b..f626268c8 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ deps = django15: Django==1.5.6 # Should track minimum supported django16: Django==1.6.3 # Should track minimum supported django17: Django==1.7.2 # Should track maximum supported - django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/ + django18alpha: https://www.djangoproject.com/download/1.8b1/tarball/ -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 8f988466a594f9c0b6a7e6a2ed76c0b27a7f1895 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 13:20:26 +0000 Subject: [PATCH 17/28] Docs on exception handler context. Closes #2604. --- docs/api-guide/exceptions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 56811ec33..3e4b3e8be 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -47,7 +47,7 @@ Any example validation error might look like this: You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects. This allows you to control the style of error responses used by your API. -The function must take a single argument, which is the exception to be handled, and should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. +The function must take a pair of arguments, this first is the exception to be handled, and the second is a dictionary containing any extra context such as the view currently being handled. The exception handler function should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. For example, you might want to ensure that all error responses include the HTTP status code in the body of the response, like so: @@ -72,6 +72,8 @@ In order to alter the style of the response, you could write the following custo return response +The context argument is not used by the default handler, but can be useful if the exception handler needs further information such as the view currently being handled, which can be accessed as `context['view']`. + The exception handler must also be configured in your settings, using the `EXCEPTION_HANDLER` setting key. For example: REST_FRAMEWORK = { From b3956bc591e7bd2c0d1460cdbc2731a372df25a5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 13:23:05 +0000 Subject: [PATCH 18/28] Upgrade testing env name to django18beta --- .travis.yml | 8 ++++---- tox.ini | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f9297853..3eb89dc4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,10 +21,10 @@ env: - TOX_ENV=py26-django15 - TOX_ENV=py27-django14 - TOX_ENV=py26-django14 - - TOX_ENV=py34-django18alpha - - TOX_ENV=py33-django18alpha - - TOX_ENV=py32-django18alpha - - TOX_ENV=py27-django18alpha + - TOX_ENV=py34-django18beta + - TOX_ENV=py33-django18beta + - TOX_ENV=py32-django18beta + - TOX_ENV=py27-django18beta install: - pip install tox diff --git a/tox.ini b/tox.ini index f626268c8..c986250c5 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py27-{flake8,docs}, {py26,py27}-django14, {py26,py27,py32,py33,py34}-django{15,16}, - {py27,py32,py33,py34}-django{17,18alpha} + {py27,py32,py33,py34}-django{17,18beta} [testenv] commands = ./runtests.py --fast @@ -14,7 +14,7 @@ deps = django15: Django==1.5.6 # Should track minimum supported django16: Django==1.6.3 # Should track minimum supported django17: Django==1.7.2 # Should track maximum supported - django18alpha: https://www.djangoproject.com/download/1.8b1/tarball/ + django18beta: https://www.djangoproject.com/download/1.8b1/tarball/ -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 4b745eef3a452b79bac0fc2e7703aa0ade6836fb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 13:25:14 +0000 Subject: [PATCH 19/28] Update test for more graceful 1.8 handling of malformed filename encodings --- tests/test_parsers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 8816065ab..a9f32a65e 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -101,9 +101,10 @@ class TestFileUploadParser(TestCase): self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt') filename = parser.get_filename(self.stream, None, self.parser_context) - # Malformed. Either None or 'fallback.txt' will be acceptable. + + # Malformed. Either None, 'ÀĥƦ.txt' or 'fallback.txt' will be acceptable. # See also https://code.djangoproject.com/ticket/24209 - self.assertIn(filename, ('fallback.txt', None)) + self.assertIn(filename, ('fallback.txt', 'ÀĥƦ.txt', None)) def __replace_content_disposition(self, disposition): self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition From 1b398a20decbf6e10173d280bc4fccd86a94b629 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 13:41:25 +0000 Subject: [PATCH 20/28] Who care what we do when it's totally malformed? Not me. --- tests/test_parsers.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_parsers.py b/tests/test_parsers.py index a9f32a65e..fe6aec196 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -99,12 +99,5 @@ class TestFileUploadParser(TestCase): filename = parser.get_filename(self.stream, None, self.parser_context) self.assertEqual(filename, 'ÀĥƦ.txt') - self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt') - filename = parser.get_filename(self.stream, None, self.parser_context) - - # Malformed. Either None, 'ÀĥƦ.txt' or 'fallback.txt' will be acceptable. - # See also https://code.djangoproject.com/ticket/24209 - self.assertIn(filename, ('fallback.txt', 'ÀĥƦ.txt', None)) - def __replace_content_disposition(self, disposition): self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition From 16ffe5e31f80058389139fe5dae5184cc22319a6 Mon Sep 17 00:00:00 2001 From: Evan Heidtmann Date: Thu, 26 Feb 2015 08:34:14 -0800 Subject: [PATCH 21/28] Add tests for callable attributes raising exceptions --- tests/test_fields.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index ab3418bd6..7f5f81029 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -93,6 +93,31 @@ class TestSource: "same as the field name. Remove the `source` keyword argument." ) + def test_callable_source(self): + class ExampleSerializer(serializers.Serializer): + example_field = serializers.CharField(source='example_callable') + + class ExampleInstance(object): + def example_callable(self): + return 'example callable value' + + serializer = ExampleSerializer(ExampleInstance()) + assert serializer.data['example_field'] == 'example callable value' + + def test_callable_source_raises(self): + class ExampleSerializer(serializers.Serializer): + example_field = serializers.CharField(source='example_callable', read_only=True) + + class ExampleInstance(object): + def example_callable(self): + raise AttributeError('method call failed') + + with pytest.raises(ValueError) as exc_info: + serializer = ExampleSerializer(ExampleInstance()) + serializer.data.items() + + assert 'method call failed' in str(exc_info.value) + class TestReadOnly: def setup(self): From bdb73d558891192c96368d5ca2266327302dba54 Mon Sep 17 00:00:00 2001 From: Evan Heidtmann Date: Thu, 26 Feb 2015 09:00:51 -0800 Subject: [PATCH 22/28] Avoid swallowing exceptions thrown in callable attributes --- rest_framework/fields.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a5348922a..01e7c78c8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -71,7 +71,11 @@ def get_attribute(instance, attrs): except ObjectDoesNotExist: return None if is_simple_callable(instance): - instance = instance() + try: + instance = instance() + except (AttributeError, KeyError) as exc: + raise ValueError('Exception raised in callable attribute "{0}"; original exception was: {1}'.format(attr, exc)) + return instance From e6b06c34c1ee526b65c92b9071c47be2ddc668c4 Mon Sep 17 00:00:00 2001 From: Evan Heidtmann Date: Thu, 26 Feb 2015 09:20:17 -0800 Subject: [PATCH 23/28] Add explanation for this exception mutation --- rest_framework/fields.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 01e7c78c8..f2791a13f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -74,6 +74,9 @@ def get_attribute(instance, attrs): try: instance = instance() except (AttributeError, KeyError) as exc: + # If we raised an Attribute or KeyError here it'd get treated + # as an omitted field in `Field.get_attribute()`. Instead we + # raise a ValueError to ensure the exception is not masked. raise ValueError('Exception raised in callable attribute "{0}"; original exception was: {1}'.format(attr, exc)) return instance From 32c885c2a0ddd296b17198cbcce27f539bf39456 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Fri, 27 Feb 2015 15:22:19 +0000 Subject: [PATCH 24/28] Ensure validators are new-style classes on python2 --- rest_framework/validators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index ab3616149..6ae80b897 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -13,7 +13,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.utils.representation import smart_repr -class UniqueValidator: +class UniqueValidator(object): """ Validator that corresponds to `unique=True` on a model field. @@ -67,7 +67,7 @@ class UniqueValidator: )) -class UniqueTogetherValidator: +class UniqueTogetherValidator(object): """ Validator that corresponds to `unique_together = (...)` on a model class. @@ -155,7 +155,7 @@ class UniqueTogetherValidator: )) -class BaseUniqueForValidator: +class BaseUniqueForValidator(object): message = None missing_message = _('This field is required.') From 9c359181d7e897e796bd38f0b16e6ddd5ae70d86 Mon Sep 17 00:00:00 2001 From: aRkadeFR Date: Fri, 27 Feb 2015 17:38:28 +0100 Subject: [PATCH 25/28] update for `that the` instead of `that` --- docs/api-guide/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 1b96b325e..ed8bbd1d0 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -14,7 +14,7 @@ Extends [Django's existing `RequestFactory` class][requestfactory]. ## Creating test requests -The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. +The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means that the standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. from rest_framework.test import APIRequestFactory From 9098856d46f2bf1a5f191b48b5e7b7e07add4dc7 Mon Sep 17 00:00:00 2001 From: Janusz Harkot Date: Fri, 27 Feb 2015 19:46:36 +0100 Subject: [PATCH 26/28] fix DictKey initial value --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c0f93816a..13301f31b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1191,7 +1191,7 @@ class ListField(Field): class DictField(Field): child = _UnvalidatedField() - initial = [] + initial = {} default_error_messages = { 'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".') } From 78e8b1b0108bb8338aa578ea2f4a0237d4edd1d4 Mon Sep 17 00:00:00 2001 From: Kevin Wood Date: Fri, 27 Feb 2015 22:14:15 -0800 Subject: [PATCH 27/28] Updated CreateOnlyDefault to call set_context on its default (if callable) --- rest_framework/fields.py | 2 ++ tests/test_fields.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 13301f31b..c327f11bc 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -114,6 +114,8 @@ class CreateOnlyDefault: def set_context(self, serializer_field): self.is_update = serializer_field.parent.instance is not None + if callable(self.default) and hasattr(self.default, 'set_context'): + self.default.set_context(serializer_field) def __call__(self): if self.is_update: diff --git a/tests/test_fields.py b/tests/test_fields.py index 7f5f81029..2ffffd55a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -317,6 +317,22 @@ class TestCreateOnlyDefault: 'text': 'example', } + def test_create_only_default_callable_sets_context(self): + """ CreateOnlyDefault instances with a callable default should set_context on the callable if possible """ + class TestCallableDefault: + def set_context(self, serializer_field): + self.field = serializer_field + + def __call__(self): + return "success" if hasattr(self, 'field') else "failure" + + class TestSerializer(serializers.Serializer): + context_set = serializers.CharField(default=serializers.CreateOnlyDefault(TestCallableDefault())) + + serializer = TestSerializer(data={}) + assert serializer.is_valid() + assert serializer.validated_data['context_set'] == 'success' + # Tests for field input and output values. # ---------------------------------------- From b582d52afbad81c56edad8ea3b6d2ac2d352b87e Mon Sep 17 00:00:00 2001 From: Kevin Wood Date: Sat, 28 Feb 2015 13:06:47 -0800 Subject: [PATCH 28/28] Fix docstring formatting --- tests/test_fields.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 2ffffd55a..1aa528da6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -318,7 +318,10 @@ class TestCreateOnlyDefault: } def test_create_only_default_callable_sets_context(self): - """ CreateOnlyDefault instances with a callable default should set_context on the callable if possible """ + """ + CreateOnlyDefault instances with a callable default should set_context + on the callable if possible + """ class TestCallableDefault: def set_context(self, serializer_field): self.field = serializer_field