From 94fe03779b8e193a4cfd67be28ab9276e36f4179 Mon Sep 17 00:00:00 2001 From: Rodolfo Carvalho Date: Wed, 5 Mar 2014 17:01:54 +0100 Subject: [PATCH 01/38] Fix typo --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 10256d479..c95b0593f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -758,7 +758,7 @@ class ModelSerializer(Serializer): ret[accessor_name] = field - # Add the `read_only` flag to any fields that have bee specified + # Add the `read_only` flag to any fields that have been specified # in the `read_only_fields` option for field_name in self.opts.read_only_fields: assert field_name not in self.base_fields.keys(), ( From e0682e9298092721c0d3eb358ce4be8039e7ccf6 Mon Sep 17 00:00:00 2001 From: Eric Buehl Date: Wed, 5 Mar 2014 17:15:52 +0000 Subject: [PATCH 02/38] don't implicitly import provider.oauth2 --- rest_framework/authentication.py | 4 ++-- rest_framework/compat.py | 13 ++----------- rest_framework/permissions.py | 7 +++---- rest_framework/tests/test_authentication.py | 12 ++++++------ 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index e491ce5f9..b0e88d88b 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -326,11 +326,11 @@ class OAuth2Authentication(BaseAuthentication): """ try: - token = oauth2_provider.models.AccessToken.objects.select_related('user') + token = oauth2_provider.oauth2.models.AccessToken.objects.select_related('user') # provider_now switches to timezone aware datetime when # the oauth2_provider version supports to it. token = token.get(token=access_token, expires__gt=provider_now()) - except oauth2_provider.models.AccessToken.DoesNotExist: + except oauth2_provider.oauth2.models.AccessToken.DoesNotExist: raise exceptions.AuthenticationFailed('Invalid token') user = token.user diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 3089b7fbb..f60a180df 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -550,13 +550,8 @@ except (ImportError, ImproperlyConfigured): # OAuth 2 support is optional try: - import provider.oauth2 as oauth2_provider - from provider.oauth2 import models as oauth2_provider_models - from provider.oauth2 import forms as oauth2_provider_forms - from provider import scope as oauth2_provider_scope - from provider import constants as oauth2_constants - from provider import __version__ as provider_version - if provider_version in ('0.2.3', '0.2.4'): + import provider as oauth2_provider + if oauth2_provider.__version__ in ('0.2.3', '0.2.4'): # 0.2.3 and 0.2.4 are supported version that do not support # timezone aware datetimes import datetime @@ -566,10 +561,6 @@ try: from django.utils.timezone import now as provider_now except ImportError: oauth2_provider = None - oauth2_provider_models = None - oauth2_provider_forms = None - oauth2_provider_scope = None - oauth2_constants = None provider_now = None # Handle lazy strings diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index f24a51235..6460056af 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -8,8 +8,7 @@ import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] from django.http import Http404 -from rest_framework.compat import (get_model_name, oauth2_provider_scope, - oauth2_constants) +from rest_framework.compat import (get_model_name, oauth2_provider) class BasePermission(object): @@ -219,8 +218,8 @@ class TokenHasReadWriteScope(BasePermission): if hasattr(token, 'resource'): # OAuth 1 return read_only or not request.auth.resource.is_readonly elif hasattr(token, 'scope'): # OAuth 2 - required = oauth2_constants.READ if read_only else oauth2_constants.WRITE - return oauth2_provider_scope.check(required, request.auth.scope) + required = oauth2_provider.constants.READ if read_only else oauth2_provider.constants.WRITE + return oauth2_provider.scope.check(required, request.auth.scope) assert False, ('TokenHasReadWriteScope requires either the' '`OAuthAuthentication` or `OAuth2Authentication` authentication ' diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index f072b81b7..90383eefd 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -19,7 +19,7 @@ from rest_framework.authentication import ( ) from rest_framework.authtoken.models import Token from rest_framework.compat import patterns, url, include -from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope +from rest_framework.compat import oauth2_provider from rest_framework.compat import oauth, oauth_provider from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView @@ -488,7 +488,7 @@ class OAuth2Tests(TestCase): self.ACCESS_TOKEN = "access_token" self.REFRESH_TOKEN = "refresh_token" - self.oauth2_client = oauth2_provider_models.Client.objects.create( + self.oauth2_client = oauth2_provider.oauth2.models.Client.objects.create( client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, redirect_uri='', @@ -497,12 +497,12 @@ class OAuth2Tests(TestCase): user=None, ) - self.access_token = oauth2_provider_models.AccessToken.objects.create( + self.access_token = oauth2_provider.oauth2.models.AccessToken.objects.create( token=self.ACCESS_TOKEN, client=self.oauth2_client, user=self.user, ) - self.refresh_token = oauth2_provider_models.RefreshToken.objects.create( + self.refresh_token = oauth2_provider.oauth2.models.RefreshToken.objects.create( user=self.user, access_token=self.access_token, client=self.oauth2_client @@ -581,7 +581,7 @@ class OAuth2Tests(TestCase): def test_post_form_with_invalid_scope_failing_auth(self): """Ensure POSTing with a readonly scope instead of a write scope fails""" read_only_access_token = self.access_token - read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read'] + read_only_access_token.scope = oauth2_provider.scope.SCOPE_NAME_DICT['read'] read_only_access_token.save() auth = self._create_authorization_header(token=read_only_access_token.token) response = self.csrf_client.get('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth) @@ -593,7 +593,7 @@ class OAuth2Tests(TestCase): def test_post_form_with_valid_scope_passing_auth(self): """Ensure POSTing with a write scope succeed""" read_write_access_token = self.access_token - read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write'] + read_write_access_token.scope = oauth2_provider.scope.SCOPE_NAME_DICT['write'] read_write_access_token.save() auth = self._create_authorization_header(token=read_write_access_token.token) response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth) From c1148241eee3df1139f9855ee3220c82f60726d5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Mar 2014 09:01:05 +0000 Subject: [PATCH 03/38] Version 2.3.13 --- docs/topics/release-notes.md | 11 +++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 14503148c..0010f6878 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,17 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### 2.3.13 + +**Date**: 6th March 2014 + +* Django 1.7 Support. +* Fix `default` argument when used with serializer relation fields. +* Display the media type of the content that is being displayed in the browsable API, rather than 'text/html'. +* Bugfix for `urlize` template failure when URL regex is matched, but value does not `urlparse`. +* Use `urandom` for token generation. +* Only use `Vary: Accept` when more than one renderer exists. + ### 2.3.12 **Date**: 15th January 2014 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 6759680b7..d6689a871 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _ """ __title__ = 'Django REST framework' -__version__ = '2.3.12' +__version__ = '2.3.13' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2013 Tom Christie' From ef94861c2d31592c3760a0c0758beb084f452c03 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Mar 2014 09:25:18 +0000 Subject: [PATCH 04/38] It's 2014 now, dontchaknow --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index d6689a871..2d76b55d5 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -11,7 +11,7 @@ __title__ = 'Django REST framework' __version__ = '2.3.13' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' -__copyright__ = 'Copyright 2011-2013 Tom Christie' +__copyright__ = 'Copyright 2011-2014 Tom Christie' # Version synonym VERSION = __version__ From 9e291879d1705dea18131fc66be31e422afa1e62 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 6 Mar 2014 15:24:07 +0100 Subject: [PATCH 05/38] Added an optional unique field to Album and checked that duplicates are detected. --- rest_framework/tests/models.py | 2 +- rest_framework/tests/test_serializer.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index bf9883123..6c8f2342b 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -103,7 +103,7 @@ class BlogPostComment(RESTFrameworkModel): class Album(RESTFrameworkModel): title = models.CharField(max_length=100, unique=True) - + ref = models.CharField(max_length=10, unique=True, null=True, blank=True) class Photo(RESTFrameworkModel): description = models.TextField() diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 198c269f0..17ef191a1 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -611,12 +611,15 @@ class ModelValidationTests(TestCase): """ Just check if serializers.ModelSerializer handles unique checks via .full_clean() """ - serializer = AlbumsSerializer(data={'title': 'a'}) + serializer = AlbumsSerializer(data={'title': 'a', 'ref': '1'}) serializer.is_valid() serializer.save() second_serializer = AlbumsSerializer(data={'title': 'a'}) self.assertFalse(second_serializer.is_valid()) - self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']}) + self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.'],}) + third_serializer = AlbumsSerializer(data={'title': 'b', 'ref': '1'}) + self.assertFalse(third_serializer.is_valid()) + self.assertEqual(third_serializer.errors, {'ref': ['Album with this Ref already exists.'],}) def test_foreign_key_is_null_with_partial(self): """ From de899824b8352912d2a0d2fa030b8e5a053a3ae5 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 6 Mar 2014 16:43:30 +0100 Subject: [PATCH 06/38] Forgot to add the ref field to the field list. --- rest_framework/tests/test_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 17ef191a1..56714d1e3 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -161,7 +161,7 @@ class AlbumsSerializer(serializers.ModelSerializer): class Meta: model = Album - fields = ['title'] # lists are also valid options + fields = ['title', 'ref'] # lists are also valid options class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer): From caf4d36cb3484313a36453a229bfc002a075f811 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 6 Mar 2014 21:17:41 +0100 Subject: [PATCH 07/38] More complex test case. --- rest_framework/tests/test_serializer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 56714d1e3..441630113 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -617,9 +617,9 @@ class ModelValidationTests(TestCase): second_serializer = AlbumsSerializer(data={'title': 'a'}) self.assertFalse(second_serializer.is_valid()) self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.'],}) - third_serializer = AlbumsSerializer(data={'title': 'b', 'ref': '1'}) + third_serializer = AlbumsSerializer(data=[{'title': 'b', 'ref': '1'}, {'title': 'c'}]) self.assertFalse(third_serializer.is_valid()) - self.assertEqual(third_serializer.errors, {'ref': ['Album with this Ref already exists.'],}) + self.assertEqual(third_serializer.errors, [{'ref': ['Album with this Ref already exists.']}, {}]) def test_foreign_key_is_null_with_partial(self): """ From 51e6982397cc032d6b3fd66f452713d448eb9084 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 6 Mar 2014 21:18:37 +0100 Subject: [PATCH 08/38] Fixed the validation for optional fields that have a value. --- rest_framework/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c95b0593f..5c726dfcd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -881,7 +881,7 @@ class ModelSerializer(Serializer): except KeyError: return ModelField(model_field=model_field, **kwargs) - def get_validation_exclusions(self): + def get_validation_exclusions(self, instance=None): """ Return a list of field names to exclude from model validation. """ @@ -893,7 +893,7 @@ class ModelSerializer(Serializer): field_name = field.source or field_name if field_name in exclusions \ and not field.read_only \ - and field.required \ + and (field.required or hasattr(instance, field_name)) \ and not isinstance(field, Serializer): exclusions.remove(field_name) return exclusions @@ -908,7 +908,7 @@ class ModelSerializer(Serializer): the full_clean validation checking. """ try: - instance.full_clean(exclude=self.get_validation_exclusions()) + instance.full_clean(exclude=self.get_validation_exclusions(instance)) except ValidationError as err: self._errors = err.message_dict return None From 34887ed75625a58d00c986b3ea5526877f4724b2 Mon Sep 17 00:00:00 2001 From: Eric Buehl Date: Thu, 6 Mar 2014 20:19:21 +0000 Subject: [PATCH 09/38] it's safe to import scope and constants --- rest_framework/compat.py | 4 ++++ rest_framework/permissions.py | 7 ++++--- rest_framework/tests/test_authentication.py | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index f60a180df..d155f5542 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -551,6 +551,8 @@ except (ImportError, ImproperlyConfigured): # OAuth 2 support is optional try: import provider as oauth2_provider + from provider import scope as oauth2_provider_scope + from provider import constants as oauth2_constants if oauth2_provider.__version__ in ('0.2.3', '0.2.4'): # 0.2.3 and 0.2.4 are supported version that do not support # timezone aware datetimes @@ -561,6 +563,8 @@ try: from django.utils.timezone import now as provider_now except ImportError: oauth2_provider = None + oauth2_provider_scope = None + oauth2_constants = None provider_now = None # Handle lazy strings diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 6460056af..f24a51235 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -8,7 +8,8 @@ import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] from django.http import Http404 -from rest_framework.compat import (get_model_name, oauth2_provider) +from rest_framework.compat import (get_model_name, oauth2_provider_scope, + oauth2_constants) class BasePermission(object): @@ -218,8 +219,8 @@ class TokenHasReadWriteScope(BasePermission): if hasattr(token, 'resource'): # OAuth 1 return read_only or not request.auth.resource.is_readonly elif hasattr(token, 'scope'): # OAuth 2 - required = oauth2_provider.constants.READ if read_only else oauth2_provider.constants.WRITE - return oauth2_provider.scope.check(required, request.auth.scope) + required = oauth2_constants.READ if read_only else oauth2_constants.WRITE + return oauth2_provider_scope.check(required, request.auth.scope) assert False, ('TokenHasReadWriteScope requires either the' '`OAuthAuthentication` or `OAuth2Authentication` authentication ' diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index 90383eefd..8caeb0812 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -19,7 +19,7 @@ from rest_framework.authentication import ( ) from rest_framework.authtoken.models import Token from rest_framework.compat import patterns, url, include -from rest_framework.compat import oauth2_provider +from rest_framework.compat import oauth2_provider, oauth2_provider_scope from rest_framework.compat import oauth, oauth_provider from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView @@ -581,7 +581,7 @@ class OAuth2Tests(TestCase): def test_post_form_with_invalid_scope_failing_auth(self): """Ensure POSTing with a readonly scope instead of a write scope fails""" read_only_access_token = self.access_token - read_only_access_token.scope = oauth2_provider.scope.SCOPE_NAME_DICT['read'] + read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read'] read_only_access_token.save() auth = self._create_authorization_header(token=read_only_access_token.token) response = self.csrf_client.get('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth) @@ -593,7 +593,7 @@ class OAuth2Tests(TestCase): def test_post_form_with_valid_scope_passing_auth(self): """Ensure POSTing with a write scope succeed""" read_write_access_token = self.access_token - read_write_access_token.scope = oauth2_provider.scope.SCOPE_NAME_DICT['write'] + read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write'] read_write_access_token.save() auth = self._create_authorization_header(token=read_write_access_token.token) response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth) From 2353878951b0607a95d539c27c362d0353c53119 Mon Sep 17 00:00:00 2001 From: Peter Inglesby Date: Thu, 6 Mar 2014 21:39:44 +0000 Subject: [PATCH 10/38] Add SEARCH_PARAM and ORDERING_PARAM to settings Fixes #1434 --- docs/api-guide/filtering.md | 6 +++- docs/api-guide/settings.md | 12 ++++++++ rest_framework/filters.py | 7 +++-- rest_framework/settings.py | 4 +++ rest_framework/tests/test_filters.py | 42 +++++++++++++++++++++++++++- rest_framework/tests/utils.py | 24 ++++++++++++++++ 6 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 rest_framework/tests/utils.py diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 07420d842..d6c4b1c1b 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -264,13 +264,17 @@ For example: search_fields = ('=username', '=email') +By default, the search parameter is named `'search`', but this may be overridden with the `SEARCH_PARAM` setting. + For more details, see the [Django documentation][search-django-admin]. --- ## OrderingFilter -The `OrderingFilter` class supports simple query parameter controlled ordering of results. To specify the result order, set a query parameter named `'ordering'` to the required field name. For example: +The `OrderingFilter` class supports simple query parameter controlled ordering of results. By default, the query parameter is named `'ordering'`, but this may by overridden with the `ORDERING_PARAM` setting. + +For example, to order users by username: http://example.com/api/users?ordering=username diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 5aee52aa1..c979019f8 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -158,6 +158,18 @@ A client request like the following would return a paginated list of up to 100 i Default: `None` +### SEARCH_PARAM + +The name of a query paramater, which can be used to specify the search term used by `SearchFilter`. + +Default: `search` + +#### ORDERING_PARAM + +The name of a query paramater, which can be used to specify the ordering of results returned by `OrderingFilter`. + +Default: `ordering` + --- ## Authentication settings diff --git a/rest_framework/filters.py b/rest_framework/filters.py index de91caedc..96d15eb9d 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.db import models from rest_framework.compat import django_filters, six, guardian, get_model_name +from rest_framework.settings import api_settings from functools import reduce import operator @@ -69,7 +70,8 @@ class DjangoFilterBackend(BaseFilterBackend): class SearchFilter(BaseFilterBackend): - search_param = 'search' # The URL query parameter used for the search. + # The URL query parameter used for the search. + search_param = api_settings.SEARCH_PARAM def get_search_terms(self, request): """ @@ -107,7 +109,8 @@ class SearchFilter(BaseFilterBackend): class OrderingFilter(BaseFilterBackend): - ordering_param = 'ordering' # The URL query parameter used for the ordering. + # The URL query parameter used for the ordering. + ordering_param = api_settings.ORDERING_PARAM ordering_fields = None def get_ordering(self, request): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index ce171d6d4..38753c968 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -69,6 +69,10 @@ DEFAULTS = { 'PAGINATE_BY_PARAM': None, 'MAX_PAGINATE_BY': None, + # Filtering + 'SEARCH_PARAM': 'search', + 'ORDERING_PARAM': 'ordering', + # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index dd5d8e428..23226bbcf 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -7,9 +7,11 @@ from django.test import TestCase from django.utils import unittest from rest_framework import generics, serializers, status, filters from rest_framework.compat import django_filters, patterns, url +from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel from .models import FilterableItem +from .utils import temporary_setting factory = APIRequestFactory() @@ -363,6 +365,24 @@ class SearchFilterTests(TestCase): ] ) + def test_search_with_nonstandard_search_param(self): + with temporary_setting('SEARCH_PARAM', 'query', module=filters): + class SearchListView(generics.ListAPIView): + model = SearchFilterModel + filter_backends = (filters.SearchFilter,) + search_fields = ('title', 'text') + + view = SearchListView.as_view() + request = factory.get('/', {'query': 'b'}) + response = view(request) + self.assertEqual( + response.data, + [ + {'id': 1, 'title': 'z', 'text': 'abc'}, + {'id': 2, 'title': 'zz', 'text': 'bcd'} + ] + ) + class OrdringFilterModel(models.Model): title = models.CharField(max_length=20) @@ -520,6 +540,26 @@ class OrderingFilterTests(TestCase): ] ) + def test_ordering_with_nonstandard_ordering_param(self): + with temporary_setting('ORDERING_PARAM', 'order', filters): + class OrderingListView(generics.ListAPIView): + model = OrdringFilterModel + filter_backends = (filters.OrderingFilter,) + ordering = ('title',) + ordering_fields = ('text',) + + view = OrderingListView.as_view() + request = factory.get('/', {'order': 'text'}) + response = view(request) + self.assertEqual( + response.data, + [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + ] + ) + class SensitiveOrderingFilterModel(models.Model): username = models.CharField(max_length=20) @@ -618,4 +658,4 @@ class SensitiveOrderingFilterTests(TestCase): {'id': 2, username_field: 'userB'}, # PassC {'id': 3, username_field: 'userC'}, # PassA ] - ) \ No newline at end of file + ) diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py new file mode 100644 index 000000000..ee157824b --- /dev/null +++ b/rest_framework/tests/utils.py @@ -0,0 +1,24 @@ +from contextlib import contextmanager +from rest_framework.settings import api_settings + + +@contextmanager +def temporary_setting(setting, value, module=None): + """ + Temporarily change value of setting for test. + + Optionally reload given module, useful when module uses value of setting on + import. + """ + original_value = getattr(api_settings, setting) + setattr(api_settings, setting, value) + + if module is not None: + reload(module) + + yield + + setattr(api_settings, setting, original_value) + + if module is not None: + reload(module) From 29f5ce7aeb57abde3924527f63bb761e0c2342d3 Mon Sep 17 00:00:00 2001 From: Peter Inglesby Date: Thu, 6 Mar 2014 23:51:02 +0000 Subject: [PATCH 11/38] Use six to reload module --- rest_framework/tests/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py index ee157824b..a8f2eb0b0 100644 --- a/rest_framework/tests/utils.py +++ b/rest_framework/tests/utils.py @@ -1,4 +1,5 @@ from contextlib import contextmanager +from rest_framework.compat import six from rest_framework.settings import api_settings @@ -14,11 +15,11 @@ def temporary_setting(setting, value, module=None): setattr(api_settings, setting, value) if module is not None: - reload(module) + six.moves.reload_module(module) yield setattr(api_settings, setting, original_value) if module is not None: - reload(module) + six.moves.reload_module(module) From 4001cd74ed46dd8bf56518e2e8f64e6152d4d480 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Mar 2014 14:15:56 +0000 Subject: [PATCH 12/38] Pin pillow to 2.3.0 --- .travis.yml | 2 +- optionals.txt | 1 + tox.ini | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c165d8d5a..f6b4753d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ env: install: - pip install $DJANGO - - pip install defusedxml==0.3 Pillow + - pip install defusedxml==0.3 Pillow==2.3.0 - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi" diff --git a/optionals.txt b/optionals.txt index 96f4b2f44..262e76443 100644 --- a/optionals.txt +++ b/optionals.txt @@ -5,3 +5,4 @@ django-filter>=0.5.4 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 +Pillow==2.3.0 diff --git a/tox.ini b/tox.ini index 77766d20b..d25e3140a 100644 --- a/tox.ini +++ b/tox.ini @@ -10,12 +10,14 @@ basepython = python3.3 deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 + Pillow==2.3.0 [testenv:py3.2-django1.6] basepython = python3.2 deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 + Pillow==2.3.0 [testenv:py2.7-django1.6] basepython = python2.7 @@ -26,6 +28,7 @@ deps = Django==1.6 oauth2==1.5.211 django-oauth2-provider==0.2.4 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.6-django1.6] basepython = python2.6 @@ -36,18 +39,21 @@ deps = Django==1.6 oauth2==1.5.211 django-oauth2-provider==0.2.4 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py3.3-django1.5] basepython = python3.3 deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 + Pillow==2.3.0 [testenv:py3.2-django1.5] basepython = python3.2 deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 + Pillow==2.3.0 [testenv:py2.7-django1.5] basepython = python2.7 @@ -58,6 +64,7 @@ deps = django==1.5.5 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.6-django1.5] basepython = python2.6 @@ -68,6 +75,7 @@ deps = django==1.5.5 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.7-django1.4] basepython = python2.7 @@ -78,6 +86,7 @@ deps = django==1.4.10 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.6-django1.4] basepython = python2.6 @@ -88,6 +97,7 @@ deps = django==1.4.10 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.7-django1.3] basepython = python2.7 @@ -98,6 +108,7 @@ deps = django==1.3.5 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.6-django1.3] basepython = python2.6 From 3fa95132d8fced3e45f5175912672163cb71933b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Mar 2014 14:16:14 +0000 Subject: [PATCH 13/38] Don't barf if PIL is not installed. --- rest_framework/tests/test_serializer.py | 42 +++++++++++++++---------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 441630113..85a899c53 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from django.db import models from django.db.models.fields import BLANK_CHOICE_DASH from django.test import TestCase +from django.utils import unittest from django.utils.datastructures import MultiValueDict from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers, fields, relations @@ -12,26 +13,31 @@ from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, Acti from rest_framework.tests.models import BasicModelSerializer import datetime import pickle +try: + import PIL +except: + PIL = None -class AMOAFModel(RESTFrameworkModel): - char_field = models.CharField(max_length=1024, blank=True) - comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True) - decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True) - email_field = models.EmailField(max_length=1024, blank=True) - file_field = models.FileField(upload_to='test', max_length=1024, blank=True) - image_field = models.ImageField(upload_to='test', max_length=1024, blank=True) - slug_field = models.SlugField(max_length=1024, blank=True) - url_field = models.URLField(max_length=1024, blank=True) +if PIL is not None: + class AMOAFModel(RESTFrameworkModel): + char_field = models.CharField(max_length=1024, blank=True) + comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True) + decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True) + email_field = models.EmailField(max_length=1024, blank=True) + file_field = models.FileField(upload_to='test', max_length=1024, blank=True) + image_field = models.ImageField(upload_to='test', max_length=1024, blank=True) + slug_field = models.SlugField(max_length=1024, blank=True) + url_field = models.URLField(max_length=1024, blank=True) -class DVOAFModel(RESTFrameworkModel): - positive_integer_field = models.PositiveIntegerField(blank=True) - positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) - email_field = models.EmailField(blank=True) - file_field = models.FileField(upload_to='test', blank=True) - image_field = models.ImageField(upload_to='test', blank=True) - slug_field = models.SlugField(blank=True) - url_field = models.URLField(blank=True) + class DVOAFModel(RESTFrameworkModel): + positive_integer_field = models.PositiveIntegerField(blank=True) + positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) + email_field = models.EmailField(blank=True) + file_field = models.FileField(upload_to='test', blank=True) + image_field = models.ImageField(upload_to='test', blank=True) + slug_field = models.SlugField(blank=True) + url_field = models.URLField(blank=True) class SubComment(object): @@ -1568,6 +1574,7 @@ class ManyFieldHelpTextTest(TestCase): self.assertEqual('Some help text.', rel_field.help_text) +@unittest.skipUnless(PIL is not None, 'PIL is not installed') class AttributeMappingOnAutogeneratedFieldsTests(TestCase): def setUp(self): @@ -1640,6 +1647,7 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase): self.field_test('url_field') +@unittest.skipUnless(PIL is not None, 'PIL is not installed') class DefaultValuesOnAutogeneratedFieldsTests(TestCase): def setUp(self): From 6cf19fa4ef1942c84546322aedd371ba87c0ed5f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Mar 2014 14:16:31 +0000 Subject: [PATCH 14/38] Add Django 1.7 to tox --- tox.ini | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d25e3140a..47fdf67ff 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,35 @@ [tox] downloadcache = {toxworkdir}/cache/ -envlist = py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3 +envlist = py3.3-django1.7,py3.2-django1.7,py2.7-django1.7,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3 [testenv] commands = {envpython} rest_framework/runtests/runtests.py +[testenv:py3.3-django1.7] +basepython = python3.3 +deps = https://www.djangoproject.com/download/1.7a2/tarball/ + django-filter==0.6a1 + defusedxml==0.3 + Pillow==2.3.0 + +[testenv:py3.2-django1.7] +basepython = python3.2 +deps = https://www.djangoproject.com/download/1.7a2/tarball/ + django-filter==0.6a1 + defusedxml==0.3 + Pillow==2.3.0 + +[testenv:py2.7-django1.7] +basepython = python2.7 +deps = https://www.djangoproject.com/download/1.7a2/tarball/ + django-filter==0.6a1 + defusedxml==0.3 + django-oauth-plus==2.2.1 + oauth2==1.5.211 + django-oauth2-provider==0.2.4 + django-guardian==1.1.1 + Pillow==2.3.0 + [testenv:py3.3-django1.6] basepython = python3.3 deps = Django==1.6 @@ -119,3 +144,4 @@ deps = django==1.3.5 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 From 0e677e9dd178ae7a0250829729a666b54f4eac61 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 7 Mar 2014 16:11:51 +0100 Subject: [PATCH 15/38] Reintroduced url arguments in the urls for the tests. --- rest_framework/test.py | 4 ++++ rest_framework/tests/test_testing.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/rest_framework/test.py b/rest_framework/test.py index 75cb4d0b9..df5a5b3b3 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -76,6 +76,10 @@ class APIRequestFactory(DjangoRequestFactory): r = { 'QUERY_STRING': urlencode(data or {}, doseq=True), } + # Fix to support old behavior where you have the arguments in the url + # See #1461 + if not data and '?' in path: + r['QUERY_STRING'] = path.split('?')[1] r.update(extra) return self.generic('GET', path, **r) diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index 71bd8b552..a55d4b225 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -152,3 +152,13 @@ class TestAPIRequestFactory(TestCase): simple_png.name = 'test.png' factory = APIRequestFactory() factory.post('/', data={'image': simple_png}) + + def test_request_factory_url_arguments(self): + """ + This is a non regression test against #1461 + """ + factory = APIRequestFactory() + request = factory.get('/view/?demo=test') + self.assertEqual(dict(request.GET), {'demo': ['test']}) + request = factory.get('/view/', {'demo': 'test'}) + self.assertEqual(dict(request.GET), {'demo': ['test']}) From c779dce3e4bba8fc453e0abe51f6fb5b2f005721 Mon Sep 17 00:00:00 2001 From: Steven Cummings Date: Sun, 16 Mar 2014 18:55:21 -0500 Subject: [PATCH 16/38] Serializer fields section for 3rd-party packages * Add new section to serializer fields page where we can list and link 3rd-party packages that provide more field types * Add an entry for drf-compound-fields --- docs/api-guide/fields.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 93f992e66..89606798c 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -2,7 +2,7 @@ # Serializer fields -> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it — normalizing it to a consistent format. +> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it — normalizing it to a consistent format. > > — [Django documentation][cite] @@ -47,7 +47,7 @@ Defaults to `True`. ### `default` -If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all. +If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all. May be set to a function or other callable, in which case the value will be evaluated each time it is used. @@ -92,7 +92,7 @@ For example, using the following model. name = models.CharField(max_length=100) created = models.DateTimeField(auto_now_add=True) payment_expiry = models.DateTimeField() - + def has_expired(self): return now() > self.payment_expiry @@ -102,7 +102,7 @@ A serializer definition that looked like this: class AccountSerializer(serializers.HyperlinkedModelSerializer): expired = serializers.Field(source='has_expired') - + class Meta: model = Account fields = ('url', 'owner', 'name', 'expired') @@ -112,7 +112,7 @@ Would produce output similar to: { 'url': 'http://example.com/api/accounts/3/', 'owner': 'http://example.com/api/users/12/', - 'name': 'FooCorp business account', + 'name': 'FooCorp business account', 'expired': True } @@ -224,7 +224,7 @@ In the case of JSON this means the default datetime representation uses the [ECM **Signature:** `DateTimeField(format=None, input_formats=None)` -* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer. +* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer. * `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. DateTime format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`) @@ -284,7 +284,7 @@ Corresponds to `django.forms.fields.FileField`. **Signature:** `FileField(max_length=None, allow_empty_file=False)` - `max_length` designates the maximum length for the file name. - + - `allow_empty_file` designates if empty files are allowed. ## ImageField @@ -329,12 +329,12 @@ Let's look at an example of serializing a class that represents an RGB color val """ def to_native(self, obj): return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue) - + def from_native(self, data): data = data.strip('rgb(').rstrip(')') red, green, blue = [int(col) for col in data.split(',')] return Color(red, green, blue) - + By default field values are treated as mapping to an attribute on the object. If you need to customize how the field value is accessed and set you need to override `.field_to_native()` and/or `.field_from_native()`. @@ -347,6 +347,14 @@ As an example, let's create a field that can be used represent the class name of """ return obj.__class__ +# More fields from 3rd-party packages + +## [drf-compound-fields](http://drf-compound-fields.readthedocs.org) + +Provides "compound" serializer fields, such as lists of simple values, which can be described by +other fields rather than serializers with the `many=True` option. Also provided are fields for +typed dictionaries and values that can be either a specific type or a list of items of that type. + [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 From dddbff59319bdefbf235f8a37af5b6ac20c4fec1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 17 Mar 2014 08:33:18 +0000 Subject: [PATCH 17/38] Tweak DRF compound fields docs --- docs/api-guide/fields.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 89606798c..b3d5b55a2 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -347,16 +347,15 @@ As an example, let's create a field that can be used represent the class name of """ return obj.__class__ -# More fields from 3rd-party packages +# Third party packages -## [drf-compound-fields](http://drf-compound-fields.readthedocs.org) +## DRF Compound Fields -Provides "compound" serializer fields, such as lists of simple values, which can be described by -other fields rather than serializers with the `many=True` option. Also provided are fields for -typed dictionaries and values that can be either a specific type or a list of items of that type. +The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type. [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior [iso8601]: http://www.w3.org/TR/NOTE-datetime +[drf-compound-fields]: http://drf-compound-fields.readthedocs.org From abe14c06f78de3b1ab20ed73f0ee5691f5224f94 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 17 Mar 2014 08:36:13 +0000 Subject: [PATCH 18/38] Minor docs tweak --- docs/api-guide/fields.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b3d5b55a2..67fa65d2d 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -349,6 +349,8 @@ As an example, let's create a field that can be used represent the class name of # Third party packages +The following third party packages are also available. + ## DRF Compound Fields The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type. From 1909472aa27907190467b81a10fc4ee496bb8889 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Thu, 13 Mar 2014 23:53:53 +0100 Subject: [PATCH 19/38] authentication: allow all transport modes of access token in OAuth2Authentication RFC6750 describe three transport modes for access tokens when accessing a protected resource: - Auhthorization header with the Bearer authentication type - form-encoded body parameter - URI query parameter This patch add support for last two transport modes. --- rest_framework/authentication.py | 12 ++++++++-- rest_framework/tests/test_authentication.py | 26 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index b0e88d88b..da9ca510e 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -6,6 +6,7 @@ import base64 from django.contrib.auth import authenticate from django.core.exceptions import ImproperlyConfigured +from django.conf import settings from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import CsrfViewMiddleware from rest_framework.compat import oauth, oauth_provider, oauth_provider_store @@ -291,6 +292,7 @@ class OAuth2Authentication(BaseAuthentication): OAuth 2 authentication backend using `django-oauth2-provider` """ www_authenticate_realm = 'api' + allow_query_params_token = settings.DEBUG def __init__(self, *args, **kwargs): super(OAuth2Authentication, self).__init__(*args, **kwargs) @@ -308,7 +310,13 @@ class OAuth2Authentication(BaseAuthentication): auth = get_authorization_header(request).split() - if not auth or auth[0].lower() != b'bearer': + if auth and auth[0].lower() == b'bearer': + access_token = auth[1] + elif 'access_token' in request.POST: + access_token = request.POST['access_token'] + elif 'access_token' in request.GET and self.allow_query_params_token: + access_token = request.GET['access_token'] + else: return None if len(auth) == 1: @@ -318,7 +326,7 @@ class OAuth2Authentication(BaseAuthentication): msg = 'Invalid bearer header. Token string should not contain spaces.' raise exceptions.AuthenticationFailed(msg) - return self.authenticate_credentials(request, auth[1]) + return self.authenticate_credentials(request, access_token) def authenticate_credentials(self, request, access_token): """ diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index 8caeb0812..c37d2a512 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User from django.http import HttpResponse from django.test import TestCase from django.utils import unittest +from django.utils.http import urlencode from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions from rest_framework import permissions @@ -53,10 +54,14 @@ urlpatterns = patterns('', permission_classes=[permissions.TokenHasReadWriteScope])) ) +class OAuth2AuthenticationDebug(OAuth2Authentication): + allow_query_params_token = True + if oauth2_provider is not None: urlpatterns += patterns('', url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')), url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])), + url(r'^oauth2-test-debug/$', MockView.as_view(authentication_classes=[OAuth2AuthenticationDebug])), url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication], permission_classes=[permissions.TokenHasReadWriteScope])), ) @@ -545,6 +550,27 @@ class OAuth2Tests(TestCase): response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_post_form_passing_auth_url_transport(self): + """Ensure GETing form over OAuth with correct client credentials in form data succeed""" + response = self.csrf_client.post('/oauth2-test/', + data={'access_token': self.access_token.token}) + self.assertEqual(response.status_code, 200) + + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_get_form_passing_auth_url_transport(self): + """Ensure GETing form over OAuth with correct client credentials in query succeed when DEBUG is True""" + query = urlencode({'access_token': self.access_token.token}) + response = self.csrf_client.get('/oauth2-test-debug/?%s' % query) + self.assertEqual(response.status_code, 200) + + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_get_form_failing_auth_url_transport(self): + """Ensure GETing form over OAuth with correct client credentials in query fails when DEBUG is False""" + query = urlencode({'access_token': self.access_token.token}) + response = self.csrf_client.get('/oauth2-test/?%s' % query) + self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') def test_post_form_passing_auth(self): """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF""" From 5c87db96c54aeb8ee62213b3ab2a054546d9756c Mon Sep 17 00:00:00 2001 From: elmkarami Date: Wed, 19 Mar 2014 15:41:25 +0000 Subject: [PATCH 20/38] Update serializers.py Prevent iterating over a string that is supposed to be an iterable <==> Prevent read_only_fields = ('some_string) --- rest_framework/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 10256d479..62ef5eedc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -757,6 +757,9 @@ class ModelSerializer(Serializer): field.read_only = True ret[accessor_name] = field + + #Ensure that 'read_only_fields is an iterable + assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' # Add the `read_only` flag to any fields that have bee specified # in the `read_only_fields` option @@ -771,7 +774,10 @@ class ModelSerializer(Serializer): "on serializer '%s'." % (field_name, self.__class__.__name__)) ret[field_name].read_only = True - + + # Ensure that 'write_only_fields' is an iterabe + assert isinstance(self.opts.write_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' + for field_name in self.opts.write_only_fields: assert field_name not in self.base_fields.keys(), ( "field '%s' on serializer '%s' specified in " From 03f96988baa6b7fa3a94fd49a5a1631f92b19b4a Mon Sep 17 00:00:00 2001 From: elmkarami Date: Wed, 19 Mar 2014 17:11:44 +0000 Subject: [PATCH 21/38] Update serializers.py Prevent iterating over a string that is supposed to be an iterable <==> Prevent read_only_fields = ('some_string) --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 62ef5eedc..03db418dc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -758,7 +758,7 @@ class ModelSerializer(Serializer): ret[accessor_name] = field - #Ensure that 'read_only_fields is an iterable + # Ensure that 'read_only_fields' is an iterable assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' # Add the `read_only` flag to any fields that have bee specified @@ -775,7 +775,7 @@ class ModelSerializer(Serializer): (field_name, self.__class__.__name__)) ret[field_name].read_only = True - # Ensure that 'write_only_fields' is an iterabe + # Ensure that 'write_only_fields' is an iterable assert isinstance(self.opts.write_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' for field_name in self.opts.write_only_fields: From 499d3cb8f0cb2f8327050e4fe775ee4bdf288285 Mon Sep 17 00:00:00 2001 From: elmkarami Date: Wed, 19 Mar 2014 17:23:15 +0000 Subject: [PATCH 22/38] Update serializers.py --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 03db418dc..4cb2d81c8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -776,7 +776,7 @@ class ModelSerializer(Serializer): ret[field_name].read_only = True # Ensure that 'write_only_fields' is an iterable - assert isinstance(self.opts.write_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' + assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' for field_name in self.opts.write_only_fields: assert field_name not in self.base_fields.keys(), ( From 19c03f4a60f339397b8ed03c9e6f20b3c604ffc3 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Thu, 20 Mar 2014 01:49:30 +0400 Subject: [PATCH 23/38] Added test writable star source Uses nested serializer with parent object --- rest_framework/tests/test_serializer.py | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 85a899c53..b78ceaa66 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -508,6 +508,32 @@ class ValidationTests(TestCase): ) self.assertEqual(serializer.is_valid(), True) + def test_writable_star_source_on_nested_serializer_with_parent_object(self): + class Serializer(serializers.Serializer): + title = serializers.WritableField(source='title') + + class AlbumSerializer(serializers.ModelSerializer): + nested = Serializer(source='*') + + class Meta: + model = Album + fields = ('nested',) + + class PhotoSerializer(serializers.ModelSerializer): + album = AlbumSerializer(source='album') + + class Meta: + model = Photo + fields = ('album', ) + + photo = Photo(album=Album()) + + data = {'album': {'nested': {'title': 'test'}}} + + serializer = PhotoSerializer(photo, data=data) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data, data) + def test_writable_star_source_with_inner_source_fields(self): """ Tests that a serializer with source="*" correctly expands the From c3aa10e589cb524dc3bb39a4fccee8238763d25a Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Thu, 20 Mar 2014 01:50:40 +0400 Subject: [PATCH 24/38] Moved get component from object after test source is star --- rest_framework/serializers.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5c726dfcd..cc0e027f7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -438,25 +438,26 @@ class BaseSerializer(WritableField): raise ValidationError(self.error_messages['required']) return - # Set the serializer object if it exists - obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None - - # If we have a model manager or similar object then we need - # to iterate through each instance. - if (self.many and - not hasattr(obj, '__iter__') and - is_simple_callable(getattr(obj, 'all', None))): - obj = obj.all() - - if self.source == '*': - if value: - reverted_data = self.restore_fields(value, {}) - if not self._errors: - into.update(reverted_data) else: if value in (None, ''): into[(self.source or field_name)] = None else: + # Set the serializer object if it exists + obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None + + # If we have a model manager or similar object then we need + # to iterate through each instance. + if (self.many and + not hasattr(obj, '__iter__') and + is_simple_callable(getattr(obj, 'all', None))): + obj = obj.all() + + if self.source == '*': + if value: + reverted_data = self.restore_fields(value, {}) + if not self._errors: + into.update(reverted_data) + kwargs = { 'instance': obj, 'data': value, From e8167f96e6c1a112e76b647ac32164be931b09a8 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Thu, 20 Mar 2014 08:53:41 +0400 Subject: [PATCH 25/38] Fixed copy-paste --- rest_framework/serializers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index cc0e027f7..01606e9c0 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -438,6 +438,11 @@ class BaseSerializer(WritableField): raise ValidationError(self.error_messages['required']) return + if self.source == '*': + if value: + reverted_data = self.restore_fields(value, {}) + if not self._errors: + into.update(reverted_data) else: if value in (None, ''): into[(self.source or field_name)] = None @@ -452,12 +457,6 @@ class BaseSerializer(WritableField): is_simple_callable(getattr(obj, 'all', None))): obj = obj.all() - if self.source == '*': - if value: - reverted_data = self.restore_fields(value, {}) - if not self._errors: - into.update(reverted_data) - kwargs = { 'instance': obj, 'data': value, From f5fc6937ece8c2bc70088979cc19c2c0a660c7a0 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Thu, 20 Mar 2014 20:27:07 +0400 Subject: [PATCH 26/38] Change serializer name for removing confusion --- rest_framework/tests/test_serializer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index b78ceaa66..3ee2b38a7 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -509,11 +509,11 @@ class ValidationTests(TestCase): self.assertEqual(serializer.is_valid(), True) def test_writable_star_source_on_nested_serializer_with_parent_object(self): - class Serializer(serializers.Serializer): + class TitleSerializer(serializers.Serializer): title = serializers.WritableField(source='title') class AlbumSerializer(serializers.ModelSerializer): - nested = Serializer(source='*') + nested = TitleSerializer(source='*') class Meta: model = Album From b04cd570504df0415d5f52b6e5cee490f9219cf2 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 21 Mar 2014 16:37:27 +0100 Subject: [PATCH 27/38] Bumped tests against Django from 1.7a2 to 1.7b1 --- .travis.yml | 6 +++--- tox.ini | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f6b4753d8..60b48cbaf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - "3.3" env: - - DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/" + - DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/" - DJANGO="django==1.6.2" - DJANGO="django==1.5.5" - DJANGO="django==1.4.10" @@ -23,7 +23,7 @@ install: - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi" - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7a2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" + - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7b1/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - export PYTHONPATH=. script: @@ -32,7 +32,7 @@ script: matrix: exclude: - python: "2.6" - env: DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/" + env: DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/" - python: "3.2" env: DJANGO="django==1.4.10" - python: "3.2" diff --git a/tox.ini b/tox.ini index 47fdf67ff..eac17ede5 100644 --- a/tox.ini +++ b/tox.ini @@ -7,22 +7,22 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py3.3-django1.7] basepython = python3.3 -deps = https://www.djangoproject.com/download/1.7a2/tarball/ django-filter==0.6a1 +deps = https://www.djangoproject.com/download/1.7b1/tarball/ defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.7] basepython = python3.2 -deps = https://www.djangoproject.com/download/1.7a2/tarball/ django-filter==0.6a1 +deps = https://www.djangoproject.com/download/1.7b1/tarball/ defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.7] basepython = python2.7 -deps = https://www.djangoproject.com/download/1.7a2/tarball/ django-filter==0.6a1 +deps = https://www.djangoproject.com/download/1.7b1/tarball/ defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 From 3b71be725a727be802eb2e43d4d155b734320023 Mon Sep 17 00:00:00 2001 From: Daniel Kontsek Date: Sat, 22 Mar 2014 10:22:08 +0100 Subject: [PATCH 28/38] Fixed encoding parameter in QueryDict --- rest_framework/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index ca70b49e7..40467c03d 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -346,7 +346,7 @@ class Request(object): media_type = self.content_type if stream is None or media_type is None: - empty_data = QueryDict('', self._request._encoding) + empty_data = QueryDict('', encoding=self._request._encoding) empty_files = MultiValueDict() return (empty_data, empty_files) @@ -362,7 +362,7 @@ class Request(object): # re-raise. Ensures we don't simply repeat the error when # attempting to render the browsable renderer response, or when # logging the request or similar. - self._data = QueryDict('', self._request._encoding) + self._data = QueryDict('', encoding=self._request._encoding) self._files = MultiValueDict() raise From 2a27674a7971a60050d4530a0852a864b2065adb Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 23 Mar 2014 15:39:57 +0100 Subject: [PATCH 29/38] Aligned the django-filter version with travis builds. --- tox.ini | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tox.ini b/tox.ini index eac17ede5..855ab0ceb 100644 --- a/tox.ini +++ b/tox.ini @@ -7,22 +7,22 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py3.3-django1.7] basepython = python3.3 - django-filter==0.6a1 deps = https://www.djangoproject.com/download/1.7b1/tarball/ + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.7] basepython = python3.2 - django-filter==0.6a1 deps = https://www.djangoproject.com/download/1.7b1/tarball/ + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.7] basepython = python2.7 - django-filter==0.6a1 deps = https://www.djangoproject.com/download/1.7b1/tarball/ + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -33,21 +33,21 @@ deps = https://www.djangoproject.com/download/1.7b1/tarball/ [testenv:py3.3-django1.6] basepython = python3.3 deps = Django==1.6 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.6] basepython = python3.2 deps = Django==1.6 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.6] basepython = python2.7 deps = Django==1.6 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -58,7 +58,7 @@ deps = Django==1.6 [testenv:py2.6-django1.6] basepython = python2.6 deps = Django==1.6 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -69,21 +69,21 @@ deps = Django==1.6 [testenv:py3.3-django1.5] basepython = python3.3 deps = django==1.5.5 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.5] basepython = python3.2 deps = django==1.5.5 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.5] basepython = python2.7 deps = django==1.5.5 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -94,7 +94,7 @@ deps = django==1.5.5 [testenv:py2.6-django1.5] basepython = python2.6 deps = django==1.5.5 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -105,7 +105,7 @@ deps = django==1.5.5 [testenv:py2.7-django1.4] basepython = python2.7 deps = django==1.4.10 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -116,7 +116,7 @@ deps = django==1.4.10 [testenv:py2.6-django1.4] basepython = python2.6 deps = django==1.4.10 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 From 3560796bbff33917df9f8e6885328467c7f809f9 Mon Sep 17 00:00:00 2001 From: Ravi Kotecha Date: Mon, 31 Mar 2014 11:38:26 +0100 Subject: [PATCH 30/38] add regression tests for field Validators pep8 and add issue no fix formatting for python 2.6 and strings for python 3.2 --- rest_framework/tests/test_validation.py | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index 124c874d7..31549df85 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from django.core.validators import MaxValueValidator from django.db import models from django.test import TestCase from rest_framework import generics, serializers, status @@ -102,3 +103,46 @@ class TestAvoidValidation(TestCase): self.assertFalse(serializer.is_valid()) self.assertDictEqual(serializer.errors, {'non_field_errors': ['Invalid data']}) + + +# regression tests for issue: 1493 + +class ValidationMaxValueValidatorModel(models.Model): + number_value = models.PositiveIntegerField(validators=[MaxValueValidator(100)]) + + +class ValidationMaxValueValidatorModelSerializer(serializers.ModelSerializer): + class Meta: + model = ValidationMaxValueValidatorModel + + +class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView): + model = ValidationMaxValueValidatorModel + serializer_class = ValidationMaxValueValidatorModelSerializer + + +class TestMaxValueValidatorValidation(TestCase): + + def test_max_value_validation_serializer_success(self): + serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 99}) + self.assertTrue(serializer.is_valid()) + + def test_max_value_validation_serializer_fails(self): + serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 101}) + self.assertFalse(serializer.is_valid()) + self.assertDictEqual({'number_value': ['Ensure this value is less than or equal to 100.']}, serializer.errors) + + def test_max_value_validation_success(self): + obj = ValidationMaxValueValidatorModel.objects.create(number_value=100) + request = factory.patch('/{0}'.format(obj.pk), {'number_value': 98}, format='json') + view = UpdateMaxValueValidationModel().as_view() + response = view(request, pk=obj.pk).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_max_value_validation_fail(self): + obj = ValidationMaxValueValidatorModel.objects.create(number_value=100) + request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json') + view = UpdateMaxValueValidationModel().as_view() + response = view(request, pk=obj.pk).render() + self.assertEqual(response.content, '{"number_value": ["Ensure this value is less than or equal to 100."]}') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) From 591cf8a48c6e5ce37d205c4b7e418fb7d2c31b0f Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 31 Mar 2014 13:17:31 +0200 Subject: [PATCH 31/38] Content is a binary string. --- rest_framework/tests/test_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index 31549df85..e13e4078c 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -144,5 +144,5 @@ class TestMaxValueValidatorValidation(TestCase): request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json') view = UpdateMaxValueValidationModel().as_view() response = view(request, pk=obj.pk).render() - self.assertEqual(response.content, '{"number_value": ["Ensure this value is less than or equal to 100."]}') + self.assertEqual(response.content, b'{"number_value": ["Ensure this value is less than or equal to 100."]}') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) From 6322feb32dceb7be67b2117686f0a7570a615294 Mon Sep 17 00:00:00 2001 From: jacobg Date: Fri, 4 Apr 2014 10:22:02 -0400 Subject: [PATCH 32/38] add a __str__ implementation to APIException Add a __str__ implementation to rest_framework.exceptions.APIException. This helps for logging raised exceptions. Thanks. --- rest_framework/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 0ac5866ef..5f774a9f3 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -20,6 +20,8 @@ class APIException(Exception): def __init__(self, detail=None): self.detail = detail or self.default_detail + def __str__(self): + return self.detail class ParseError(APIException): status_code = status.HTTP_400_BAD_REQUEST From e45e52a255c0dfbecfc5048697534ffbe0e2648e Mon Sep 17 00:00:00 2001 From: Dmitry Mukhin Date: Mon, 7 Apr 2014 20:39:45 +0400 Subject: [PATCH 33/38] replace page with page_size to avoide confusion --- docs/topics/release-notes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 0010f6878..2bc8b2d6a 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -112,11 +112,11 @@ You can determine your currently installed version using `pip freeze`: * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. -* Bugfix: Clients setting `page=0` now simply returns the default page size, instead of disabling pagination. [*] +* Bugfix: Clients setting `page_size=0` now simply returns the default page size, instead of disabling pagination. [*] --- -[*] Note that the change in `page=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior. +[*] Note that the change in `page_size=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior. class DisablePaginationMixin(object): def get_paginate_by(self, queryset=None): From 2a1571b3bf36ff153af68401f7aefa0620f80807 Mon Sep 17 00:00:00 2001 From: Mauro de Carvalho Date: Mon, 7 Apr 2014 18:27:59 -0300 Subject: [PATCH 34/38] Fixed comment. --- 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 68b956822..946a59545 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -164,7 +164,7 @@ class Field(object): Called to set up a field prior to field_to_native or field_from_native. parent - The parent serializer. - model_field - The model field this field corresponds to, if one exists. + field_name - The name of the field being initialized. """ self.parent = parent self.root = parent.root or parent From 3234a5dd6b0c090dd25a716e7b1c2567d8fee89b Mon Sep 17 00:00:00 2001 From: Craig Date: Tue, 8 Apr 2014 22:56:07 -0400 Subject: [PATCH 35/38] Fix python syntax in filtering docs --- docs/api-guide/filtering.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index d6c4b1c1b..6a8a267b2 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -24,7 +24,7 @@ For example: from myapp.serializers import PurchaseSerializer from rest_framework import generics - class PurchaseList(generics.ListAPIView) + class PurchaseList(generics.ListAPIView): serializer_class = PurchaseSerializer def get_queryset(self): @@ -46,7 +46,7 @@ For example if your URL config contained an entry like this: You could then write a view that returned a purchase queryset filtered by the username portion of the URL: - class PurchaseList(generics.ListAPIView) + class PurchaseList(generics.ListAPIView): serializer_class = PurchaseSerializer def get_queryset(self): @@ -63,7 +63,7 @@ A final example of filtering the initial queryset would be to determine the init We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL: - class PurchaseList(generics.ListAPIView) + class PurchaseList(generics.ListAPIView): serializer_class = PurchaseSerializer def get_queryset(self): From c1ac65edce1bcfff4c87df3bb9c4df14fe8e9d6c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 9 Apr 2014 15:51:00 +0200 Subject: [PATCH 36/38] Adds test that blank option is added when required=False on RelatedFields --- rest_framework/relations.py | 2 ++ rest_framework/tests/test_relations.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 308545ce9..3463954dc 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -59,6 +59,8 @@ class RelatedField(WritableField): super(RelatedField, self).__init__(*args, **kwargs) if not self.required: + # Accessed in ModelChoiceIterator django/forms/models.py:1034 + # If set adds empty choice. self.empty_label = BLANK_CHOICE_DASH[0][1] self.queryset = queryset diff --git a/rest_framework/tests/test_relations.py b/rest_framework/tests/test_relations.py index f52e0e1e5..c421096ab 100644 --- a/rest_framework/tests/test_relations.py +++ b/rest_framework/tests/test_relations.py @@ -118,3 +118,25 @@ class RelatedFieldSourceTests(TestCase): (serializers.ModelSerializer,), attrs) with self.assertRaises(AttributeError): TestSerializer(data={'name': 'foo'}) + + +class RelatedFieldChoicesTests(TestCase): + """ + Tests for #1408 "Web browseable API doesn't have blank option on drop down list box" + https://github.com/tomchristie/django-rest-framework/issues/1408 + """ + def test_blank_option_is_added_to_choice_if_required_equals_false(self): + """ + + """ + post = BlogPost(title="Checking blank option is added") + post.save() + + queryset = BlogPost.objects.all() + field = serializers.RelatedField(required=False, queryset=queryset) + + choice_count = BlogPost.objects.count() + widget_count = len(field.widget.choices) + + self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added') + From a73498d7974b15a25902fbdd1024742b95a166d4 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 9 Apr 2014 19:54:13 +0200 Subject: [PATCH 37/38] Skip new test for Django < 1.6 --- rest_framework/tests/test_relations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/test_relations.py b/rest_framework/tests/test_relations.py index c421096ab..37ac826b2 100644 --- a/rest_framework/tests/test_relations.py +++ b/rest_framework/tests/test_relations.py @@ -2,8 +2,10 @@ General tests for relational fields. """ from __future__ import unicode_literals +from django import get_version from django.db import models from django.test import TestCase +from django.utils import unittest from rest_framework import serializers from rest_framework.tests.models import BlogPost @@ -119,7 +121,7 @@ class RelatedFieldSourceTests(TestCase): with self.assertRaises(AttributeError): TestSerializer(data={'name': 'foo'}) - +@unittest.skipIf(get_version() < '1.6.0', 'Upstream behaviour changed in v1.6') class RelatedFieldChoicesTests(TestCase): """ Tests for #1408 "Web browseable API doesn't have blank option on drop down list box" From 613df5c6501f715c0775229f34fcba9f4291c05d Mon Sep 17 00:00:00 2001 From: Ian Leith Date: Fri, 11 Apr 2014 05:49:49 +0100 Subject: [PATCH 38/38] Fix dict_keys equality test for python 3. --- rest_framework/utils/mediatypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index c09c29338..92f99efd2 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -74,7 +74,7 @@ class _MediaType(object): return 0 elif self.sub_type == '*': return 1 - elif not self.params or self.params.keys() == ['q']: + elif not self.params or list(self.params.keys()) == ['q']: return 2 return 3