diff options
| author | Tom Christie | 2013-12-23 09:48:59 +0000 |
|---|---|---|
| committer | Tom Christie | 2013-12-23 09:48:59 +0000 |
| commit | 52686420f4bf866064ee88a15903665f14289394 (patch) | |
| tree | 41ea7b0d4863092f996f63de14e678a1c74a7a3a /rest_framework | |
| parent | 9c41c007afc71c899306bcb02e40bdfc36b09146 (diff) | |
| parent | 83b31e7ea298a8948e9a76c9b971845ea0052b3c (diff) | |
| download | django-rest-framework-52686420f4bf866064ee88a15903665f14289394.tar.bz2 | |
Merge branch 'bennbollay-patch-1' into 2.4.0
Conflicts:
.travis.yml
docs/api-guide/routers.md
rest_framework/compat.py
tox.ini
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/authentication.py | 6 | ||||
| -rw-r--r-- | rest_framework/authtoken/models.py | 8 | ||||
| -rw-r--r-- | rest_framework/compat.py | 22 | ||||
| -rw-r--r-- | rest_framework/fields.py | 2 | ||||
| -rw-r--r-- | rest_framework/permissions.py | 22 | ||||
| -rw-r--r-- | rest_framework/routers.py | 14 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 9 | ||||
| -rw-r--r-- | rest_framework/tests/models.py | 1 | ||||
| -rw-r--r-- | rest_framework/tests/test_authentication.py | 26 | ||||
| -rw-r--r-- | rest_framework/tests/test_serializer.py | 97 | ||||
| -rw-r--r-- | rest_framework/tests/test_validation.py | 21 |
11 files changed, 183 insertions, 45 deletions
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index db5cce40..1f8d37fa 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -9,7 +9,7 @@ from django.core.exceptions import ImproperlyConfigured from django.middleware.csrf import CsrfViewMiddleware from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import oauth, oauth_provider, oauth_provider_store -from rest_framework.compat import oauth2_provider, provider_now +from rest_framework.compat import oauth2_provider, provider_now, check_nonce from rest_framework.authtoken.models import Token @@ -281,7 +281,9 @@ class OAuthAuthentication(BaseAuthentication): """ Checks nonce of request, and return True if valid. """ - return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) + oauth_nonce = oauth_request['oauth_nonce'] + oauth_timestamp = oauth_request['oauth_timestamp'] + return check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp) class OAuth2Authentication(BaseAuthentication): diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 7601f5b7..024f62bf 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -1,11 +1,17 @@ import uuid import hmac from hashlib import sha1 -from rest_framework.compat import AUTH_USER_MODEL from django.conf import settings from django.db import models +# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist. +# Note that we don't perform this code in the compat module due to +# bug report #1297 +# See: https://github.com/tomchristie/django-rest-framework/issues/1297 +AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') + + class Token(models.Model): """ The default authorization token model. diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b4d37ab8..45045c0f 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -6,6 +6,7 @@ versions of django/python, and compatibility wrappers around optional packages. # flake8: noqa from __future__ import unicode_literals import django +import inspect from django.core.exceptions import ImproperlyConfigured from django.conf import settings @@ -99,13 +100,6 @@ def get_concrete_model(model_cls): return model_cls -# Django 1.5 add support for custom auth user model -if django.VERSION >= (1, 5): - AUTH_USER_MODEL = settings.AUTH_USER_MODEL -else: - AUTH_USER_MODEL = 'auth.User' - - # View._allowed_methods only present from 1.5 onwards if django.VERSION >= (1, 5): from django.views.generic import View @@ -201,9 +195,23 @@ except ImportError: try: import oauth_provider from oauth_provider.store import store as oauth_provider_store + + # check_nonce's calling signature in django-oauth-plus changes sometime + # between versions 2.0 and 2.2.1 + def check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp): + check_nonce_args = inspect.getargspec(oauth_provider_store.check_nonce).args + if 'timestamp' in check_nonce_args: + return oauth_provider_store.check_nonce( + request, oauth_request, oauth_nonce, oauth_timestamp + ) + return oauth_provider_store.check_nonce( + request, oauth_request, oauth_nonce + ) + except (ImportError, ImproperlyConfigured): oauth_provider = None oauth_provider_store = None + check_nonce = None # OAuth 2 support is optional diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 65edd0d6..5ee75235 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -423,7 +423,7 @@ class BooleanField(WritableField): def field_from_native(self, data, files, field_name, into): # HTML checkboxes do not explicitly represent unchecked as `False` # we deal with that here... - if isinstance(data, QueryDict): + if isinstance(data, QueryDict) and self.default is None: self.default = False return super(BooleanField, self).field_from_native( diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index d93dba19..c9517138 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -44,9 +44,7 @@ class IsAuthenticated(BasePermission): """ def has_permission(self, request, view): - if request.user and request.user.is_authenticated(): - return True - return False + return request.user and request.user.is_authenticated() class IsAdminUser(BasePermission): @@ -55,9 +53,7 @@ class IsAdminUser(BasePermission): """ def has_permission(self, request, view): - if request.user and request.user.is_staff: - return True - return False + return request.user and request.user.is_staff class IsAuthenticatedOrReadOnly(BasePermission): @@ -66,11 +62,9 @@ class IsAuthenticatedOrReadOnly(BasePermission): """ def has_permission(self, request, view): - if (request.method in SAFE_METHODS or - request.user and - request.user.is_authenticated()): - return True - return False + return (request.method in SAFE_METHODS or + request.user and + request.user.is_authenticated()) class DjangoModelPermissions(BasePermission): @@ -128,11 +122,9 @@ class DjangoModelPermissions(BasePermission): perms = self.get_required_permissions(request.method, model_cls) - if (request.user and + return (request.user and (request.user.is_authenticated() or not self.authenticated_users_only) and - request.user.has_perms(perms)): - return True - return False + request.user.has_perms(perms)) class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 790299cc..740d58f0 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -208,18 +208,24 @@ class SimpleRouter(BaseRouter): bound_methods[method] = action return bound_methods - def get_lookup_regex(self, viewset): + def get_lookup_regex(self, viewset, lookup_prefix=''): """ Given a viewset, return the portion of URL regex that is used to match against a single instance. + + Note that lookup_prefix is not used directly inside REST rest_framework + itself, but is required in order to nicely support nested router + implementations, such as drf-nested-routers. + + https://github.com/alanjds/drf-nested-routers """ if self.trailing_slash: - base_regex = '(?P<{lookup_field}>[^/]+)' + base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)' else: # Don't consume `.json` style suffixes - base_regex = '(?P<{lookup_field}>[^/.]+)' + base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)' lookup_field = getattr(viewset, 'lookup_field', 'pk') - return base_regex.format(lookup_field=lookup_field) + return base_regex.format(lookup_field=lookup_field, lookup_prefix=lookup_prefix) def get_urls(self): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9c27717f..fa935306 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -416,7 +416,9 @@ class BaseSerializer(WritableField): if self.source == '*': if value: - into.update(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 @@ -602,6 +604,7 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, + models.NullBooleanField: BooleanField, models.FileField: FileField, models.ImageField: ImageField, } @@ -695,7 +698,9 @@ class ModelSerializer(Serializer): is_m2m = isinstance(relation.field, models.fields.related.ManyToManyField) - if is_m2m and not relation.field.rel.through._meta.auto_created: + if (is_m2m and + hasattr(relation.field.rel, 'through') and + not relation.field.rel.through._meta.auto_created): has_through_model = True if nested: diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 1598ecd9..32a726c0 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -70,6 +70,7 @@ class Comment(RESTFrameworkModel): class ActionItem(RESTFrameworkModel): title = models.CharField(max_length=200) + started = models.NullBooleanField(default=False) done = models.BooleanField(default=False) info = CustomField(default='---', max_length=12) diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index e9a817c0..fb0bc694 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -249,7 +249,7 @@ class OAuthTests(TestCase): def setUp(self): # these imports are here because oauth is optional and hiding them in try..except block or compat # could obscure problems if something breaks - from oauth_provider.models import Consumer, Resource + from oauth_provider.models import Consumer, Scope from oauth_provider.models import Token as OAuthToken from oauth_provider import consts @@ -269,8 +269,8 @@ class OAuthTests(TestCase): self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, name='example', user=self.user, status=self.consts.ACCEPTED) - self.resource = Resource.objects.create(name="resource name", url="api/") - self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource, + self.scope = Scope.objects.create(name="resource name", url="api/") + self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, scope=self.scope, token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True ) @@ -362,7 +362,8 @@ class OAuthTests(TestCase): def test_post_form_with_urlencoded_parameters(self): """Ensure POSTing with x-www-form-urlencoded auth parameters passes""" params = self._create_authorization_url_parameters() - response = self.csrf_client.post('/oauth/', params) + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') @@ -397,10 +398,10 @@ class OAuthTests(TestCase): @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') @unittest.skipUnless(oauth, 'oauth2 not installed') def test_get_form_with_readonly_resource_passing_auth(self): - """Ensure POSTing with a readonly resource instead of a write scope fails""" + """Ensure POSTing with a readonly scope instead of a write scope fails""" read_only_access_token = self.token - read_only_access_token.resource.is_readonly = True - read_only_access_token.resource.save() + read_only_access_token.scope.is_readonly = True + read_only_access_token.scope.save() params = self._create_authorization_url_parameters() response = self.csrf_client.get('/oauth-with-scope/', params) self.assertEqual(response.status_code, 200) @@ -410,8 +411,8 @@ class OAuthTests(TestCase): def test_post_form_with_readonly_resource_failing_auth(self): """Ensure POSTing with a readonly resource instead of a write scope fails""" read_only_access_token = self.token - read_only_access_token.resource.is_readonly = True - read_only_access_token.resource.save() + read_only_access_token.scope.is_readonly = True + read_only_access_token.scope.save() params = self._create_authorization_url_parameters() response = self.csrf_client.post('/oauth-with-scope/', params) self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) @@ -421,10 +422,11 @@ class OAuthTests(TestCase): def test_post_form_with_write_resource_passing_auth(self): """Ensure POSTing with a write resource succeed""" read_write_access_token = self.token - read_write_access_token.resource.is_readonly = False - read_write_access_token.resource.save() + read_write_access_token.scope.is_readonly = False + read_write_access_token.scope.save() params = self._create_authorization_url_parameters() - response = self.csrf_client.post('/oauth-with-scope/', params) + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth-with-scope/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index e80276e9..86f365de 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -105,6 +105,17 @@ class ModelSerializerWithNestedSerializer(serializers.ModelSerializer): model = Person +class NestedSerializerWithRenamedField(serializers.Serializer): + renamed_info = serializers.Field(source='info') + + +class ModelSerializerWithNestedSerializerWithRenamedField(serializers.ModelSerializer): + nested = NestedSerializerWithRenamedField(source='*') + + class Meta: + model = Person + + class PersonSerializerInvalidReadOnly(serializers.ModelSerializer): """ Testing for #652. @@ -456,6 +467,20 @@ class ValidationTests(TestCase): ) self.assertEqual(serializer.is_valid(), True) + def test_writable_star_source_with_inner_source_fields(self): + """ + Tests that a serializer with source="*" correctly expands the + it's fields into the outer serializer even if they have their + own 'source' parameters. + """ + + serializer = ModelSerializerWithNestedSerializerWithRenamedField(data={ + 'name': 'marko', + 'nested': {'renamed_info': 'hi'}}, + ) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.errors, {}) + class CustomValidationTests(TestCase): class CommentSerializerWithFieldValidator(CommentSerializer): @@ -1743,3 +1768,75 @@ class TestSerializerTransformMethods(TestCase): 'b_renamed': None, } ) + + +class DefaultTrueBooleanModel(models.Model): + cat = models.BooleanField(default=True) + dog = models.BooleanField(default=False) + + +class SerializerDefaultTrueBoolean(TestCase): + + def setUp(self): + super(SerializerDefaultTrueBoolean, self).setUp() + + class DefaultTrueBooleanSerializer(serializers.ModelSerializer): + class Meta: + model = DefaultTrueBooleanModel + fields = ('cat', 'dog') + + self.default_true_boolean_serializer = DefaultTrueBooleanSerializer + + def test_enabled_as_false(self): + serializer = self.default_true_boolean_serializer(data={'cat': False, + 'dog': False}) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], False) + self.assertEqual(serializer.data['dog'], False) + + def test_enabled_as_true(self): + serializer = self.default_true_boolean_serializer(data={'cat': True, + 'dog': True}) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], True) + self.assertEqual(serializer.data['dog'], True) + + def test_enabled_partial(self): + serializer = self.default_true_boolean_serializer(data={'cat': False}, + partial=True) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], False) + self.assertEqual(serializer.data['dog'], False) + + +class BoolenFieldTypeTest(TestCase): + ''' + Ensure the various Boolean based model fields are rendered as the proper + field type + + ''' + + def setUp(self): + ''' + Setup an ActionItemSerializer for BooleanTesting + ''' + data = { + 'title': 'b' * 201, + } + self.serializer = ActionItemSerializer(data=data) + + def test_booleanfield_type(self): + ''' + Test that BooleanField is infered from models.BooleanField + ''' + bfield = self.serializer.get_fields()['done'] + self.assertEqual(type(bfield), fields.BooleanField) + + def test_nullbooleanfield_type(self): + ''' + Test that BooleanField is infered from models.NullBooleanField + + https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8 + ''' + bfield = self.serializer.get_fields()['started'] + self.assertEqual(type(bfield), fields.BooleanField) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index ebfdff9c..124c874d 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -47,12 +47,18 @@ class ShouldValidateModel(models.Model): class ShouldValidateModelSerializer(serializers.ModelSerializer): renamed = serializers.CharField(source='should_validate_field', required=False) + def validate_renamed(self, attrs, source): + value = attrs[source] + if len(value) < 3: + raise serializers.ValidationError('Minimum 3 characters.') + return attrs + class Meta: model = ShouldValidateModel fields = ('renamed',) -class TestPreSaveValidationExclusions(TestCase): +class TestPreSaveValidationExclusionsSerializer(TestCase): def test_renamed_fields_are_model_validated(self): """ Ensure fields with 'source' applied do get still get model validation. @@ -61,6 +67,19 @@ class TestPreSaveValidationExclusions(TestCase): # does not have `blank=True`, so this serializer should not validate. serializer = ShouldValidateModelSerializer(data={'renamed': ''}) self.assertEqual(serializer.is_valid(), False) + self.assertIn('renamed', serializer.errors) + self.assertNotIn('should_validate_field', serializer.errors) + + +class TestCustomValidationMethods(TestCase): + def test_custom_validation_method_is_executed(self): + serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'}) + self.assertFalse(serializer.is_valid()) + self.assertIn('renamed', serializer.errors) + + def test_custom_validation_method_passing(self): + serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'}) + self.assertTrue(serializer.is_valid()) class ValidationSerializer(serializers.Serializer): |
