aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/authentication.py6
-rw-r--r--rest_framework/authtoken/models.py8
-rw-r--r--rest_framework/compat.py22
-rw-r--r--rest_framework/fields.py2
-rw-r--r--rest_framework/permissions.py22
-rw-r--r--rest_framework/routers.py14
-rw-r--r--rest_framework/serializers.py9
-rw-r--r--rest_framework/tests/models.py1
-rw-r--r--rest_framework/tests/test_authentication.py26
-rw-r--r--rest_framework/tests/test_serializer.py97
-rw-r--r--rest_framework/tests/test_validation.py21
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):