Merge remote-tracking branch 'reference/master' into bugfix/1936

This commit is contained in:
Xavier Ordoquy 2014-11-14 02:11:40 +01:00
commit b523524d8e
20 changed files with 168 additions and 354 deletions

View File

@ -1,6 +1,7 @@
# Django REST framework
[![build-status-image]][travis]
[![pypi-version]][pypi]
**Awesome web-browseable Web APIs.**
@ -181,6 +182,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[pypi-version]: https://pypip.in/version/djangorestframework/badge.svg
[pypi]: https://pypi.python.org/pypi/djangorestframework
[twitter]: https://twitter.com/_tomchristie
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X

View File

@ -506,9 +506,9 @@ We now use the following:
REST framework now has more explicit and clear control over validating empty values for fields.
Previously the meaning of the `required=False` keyword argument was underspecified. In practice its use meant that a field could either be not included in the input, or it could be included, but be `None`.
Previously the meaning of the `required=False` keyword argument was underspecified. In practice its use meant that a field could either be not included in the input, or it could be included, but be `None` or the empty string.
We now have a better separation, with separate `required` and `allow_none` arguments.
We now have a better separation, with separate `required`, `allow_none` and `allow_blank` arguments.
The following set of arguments are used to control validation of empty values:
@ -519,7 +519,7 @@ The following set of arguments are used to control validation of empty values:
Typically you'll want to use `required=False` if the corresponding model field has a default value, and additionally set either `allow_none=True` or `allow_blank=True` if required.
The `default` argument is there if you need it, but you'll more typically want defaults to be set on model fields, rather than serializer fields.
The `default` argument is also available and always implies that the field is not required to be in the input. It is unnecessary to use the `required` argument when a default is specified, and doing so will result in an error.
#### Coercing output types.

View File

@ -10,9 +10,17 @@ from django.utils.translation import ugettext_lazy as _
class PKOnlyObject(object):
"""
This is a mock object, used for when we only need the pk of the object
instance, but still want to return an object with a .pk attribute,
in order to keep the same interface as a regular model instance.
"""
def __init__(self, pk):
self.pk = pk
# We assume that 'validators' are intended for the child serializer,
# rather than the parent serializer.
MANY_RELATION_KWARGS = (
'read_only', 'write_only', 'required', 'default', 'initial', 'source',
'label', 'help_text', 'style', 'error_messages'
@ -34,15 +42,19 @@ class RelatedField(Field):
def __new__(cls, *args, **kwargs):
# We override this method in order to automagically create
# `ManyRelation` classes instead when `many=True` is set.
# `ManyRelatedField` classes instead when `many=True` is set.
if kwargs.pop('many', False):
list_kwargs = {'child_relation': cls(*args, **kwargs)}
for key in kwargs.keys():
if key in MANY_RELATION_KWARGS:
list_kwargs[key] = kwargs[key]
return ManyRelation(**list_kwargs)
return cls.many_init(*args, **kwargs)
return super(RelatedField, cls).__new__(cls, *args, **kwargs)
@classmethod
def many_init(cls, *args, **kwargs):
list_kwargs = {'child_relation': cls(*args, **kwargs)}
for key in kwargs.keys():
if key in MANY_RELATION_KWARGS:
list_kwargs[key] = kwargs[key]
return ManyRelatedField(**list_kwargs)
def run_validation(self, data=empty):
# We force empty strings to None values for relational fields.
if data == '':
@ -286,12 +298,12 @@ class SlugRelatedField(RelatedField):
return getattr(obj, self.slug_field)
class ManyRelation(Field):
class ManyRelatedField(Field):
"""
Relationships with `many=True` transparently get coerced into instead being
a ManyRelation with a child relationship.
a ManyRelatedField with a child relationship.
The `ManyRelation` class is responsible for handling iterating through
The `ManyRelatedField` class is responsible for handling iterating through
the values and passing each one to the child relationship.
You shouldn't need to be using this class directly yourself.
@ -302,7 +314,7 @@ class ManyRelation(Field):
def __init__(self, child_relation=None, *args, **kwargs):
self.child_relation = child_relation
assert child_relation is not None, '`child_relation` is a required argument.'
super(ManyRelation, self).__init__(*args, **kwargs)
super(ManyRelatedField, self).__init__(*args, **kwargs)
self.child_relation.bind(field_name='', parent=self)
def get_value(self, dictionary):

View File

@ -383,7 +383,10 @@ class HTMLFormRenderer(BaseRenderer):
serializers.MultipleChoiceField: {
'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
},
serializers.ManyRelation: {
serializers.RelatedField: {
'base_template': 'select.html', # Also valid: 'radio.html'
},
serializers.ManyRelatedField: {
'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
},
serializers.Serializer: {

View File

@ -46,6 +46,9 @@ import warnings
from rest_framework.relations import * # NOQA
from rest_framework.fields import * # NOQA
# We assume that 'validators' are intended for the child serializer,
# rather than the parent serializer.
LIST_SERIALIZER_KWARGS = (
'read_only', 'write_only', 'required', 'default', 'initial', 'source',
'label', 'help_text', 'style', 'error_messages',
@ -73,13 +76,25 @@ class BaseSerializer(Field):
# We override this method in order to automagically create
# `ListSerializer` classes instead when `many=True` is set.
if kwargs.pop('many', False):
list_kwargs = {'child': cls(*args, **kwargs)}
for key in kwargs.keys():
if key in LIST_SERIALIZER_KWARGS:
list_kwargs[key] = kwargs[key]
return ListSerializer(*args, **list_kwargs)
return cls.many_init(*args, **kwargs)
return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
@classmethod
def many_init(cls, *args, **kwargs):
"""
This method implements the creation of a `ListSerializer` parent
class when `many=True` is used. You can customize it if you need to
control which keyword arguments are passed to the parent, and
which are passed to the child.
"""
child_serializer = cls(*args, **kwargs)
list_kwargs = {'child': child_serializer}
list_kwargs.update(dict([
(key, value) for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS
]))
return ListSerializer(*args, **list_kwargs)
def to_internal_value(self, data):
raise NotImplementedError('`to_internal_value()` must be implemented.')
@ -230,18 +245,18 @@ class Serializer(BaseSerializer):
def get_initial(self):
if self._initial_data is not None:
return ReturnDict([
return OrderedDict([
(field_name, field.get_value(self._initial_data))
for field_name, field in self.fields.items()
if field.get_value(self._initial_data) is not empty
and not field.read_only
], serializer=self)
])
return ReturnDict([
return OrderedDict([
(field.field_name, field.get_initial())
for field in self.fields.values()
if not field.read_only
], serializer=self)
])
def get_value(self, dictionary):
# We override the default field access in order to support
@ -304,8 +319,8 @@ class Serializer(BaseSerializer):
"""
Dict of native values <- Dict of primitive datatypes.
"""
ret = {}
errors = ReturnDict(serializer=self)
ret = OrderedDict()
errors = OrderedDict()
fields = [
field for field in self.fields.values()
if (not field.read_only) or (field.default is not empty)
@ -334,7 +349,7 @@ class Serializer(BaseSerializer):
"""
Object instance -> Dict of primitive datatypes.
"""
ret = ReturnDict(serializer=self)
ret = OrderedDict()
fields = [field for field in self.fields.values() if not field.write_only]
for field in fields:
@ -373,6 +388,19 @@ class Serializer(BaseSerializer):
return NestedBoundField(field, value, error)
return BoundField(field, value, error)
# Include a backlink to the serializer class on return objects.
# Allows renderers such as HTMLFormRenderer to get the full field info.
@property
def data(self):
ret = super(Serializer, self).data
return ReturnDict(ret, serializer=self)
@property
def errors(self):
ret = super(Serializer, self).errors
return ReturnDict(ret, serializer=self)
# There's some replication of `ListField` here,
# but that's probably better than obfuscating the call hierarchy.
@ -395,7 +423,7 @@ class ListSerializer(BaseSerializer):
def get_initial(self):
if self._initial_data is not None:
return self.to_representation(self._initial_data)
return ReturnList(serializer=self)
return []
def get_value(self, dictionary):
"""
@ -423,7 +451,7 @@ class ListSerializer(BaseSerializer):
})
ret = []
errors = ReturnList(serializer=self)
errors = []
for item in data:
try:
@ -444,37 +472,64 @@ class ListSerializer(BaseSerializer):
List of object instances -> List of dicts of primitive datatypes.
"""
iterable = data.all() if (hasattr(data, 'all')) else data
return ReturnList(
[self.child.to_representation(item) for item in iterable],
serializer=self
return [
self.child.to_representation(item) for item in iterable
]
def update(self, instance, validated_data):
raise NotImplementedError(
"Serializers with many=True do not support multiple update by "
"default, only multiple create. For updates it is unclear how to "
"deal with insertions and deletions. If you need to support "
"multiple update, use a `ListSerializer` class and override "
"`.update()` so you can specify the behavior exactly."
)
def create(self, validated_data):
return [
self.child.create(attrs) for attrs in validated_data
]
def save(self, **kwargs):
"""
Save and return a list of object instances.
"""
assert self.instance is None, (
"Serializers do not support multiple update by default, only "
"multiple create. For updates it is unclear how to deal with "
"insertions and deletions. If you need to support multiple update, "
"use a `ListSerializer` class and override `.save()` so you can "
"specify the behavior exactly."
)
validated_data = [
dict(list(attrs.items()) + list(kwargs.items()))
for attrs in self.validated_data
]
self.instance = [
self.child.create(attrs) for attrs in validated_data
]
if self.instance is not None:
self.instance = self.update(self.instance, validated_data)
assert self.instance is not None, (
'`update()` did not return an object instance.'
)
else:
self.instance = self.create(validated_data)
assert self.instance is not None, (
'`create()` did not return an object instance.'
)
return self.instance
def __repr__(self):
return representation.list_repr(self, indent=1)
# Include a backlink to the serializer class on return objects.
# Allows renderers such as HTMLFormRenderer to get the full field info.
@property
def data(self):
ret = super(ListSerializer, self).data
return ReturnList(ret, serializer=self)
@property
def errors(self):
ret = super(ListSerializer, self).errors
if isinstance(ret, dict):
return ReturnDict(ret, serializer=self)
return ReturnList(ret, serializer=self)
# ModelSerializer & HyperlinkedModelSerializer
# --------------------------------------------

View File

@ -88,7 +88,7 @@ def get_field_kwargs(field_name, model_field):
kwargs['read_only'] = True
return kwargs
if model_field.has_default():
if model_field.has_default() or model_field.blank or model_field.null:
kwargs['required'] = False
if model_field.flatchoices:
@ -215,7 +215,7 @@ def get_relation_kwargs(field_name, relation_info):
# If this field is read-only, then return early.
# No further keyword arguments are valid.
return kwargs
if model_field.has_default():
if model_field.has_default() or model_field.null:
kwargs['required'] = False
if model_field.null:
kwargs['allow_null'] = True

View File

@ -1,8 +0,0 @@
from django.db import models
from tests.users.models import User
class Account(models.Model):
owner = models.ForeignKey(User, related_name='accounts_owned')
admins = models.ManyToManyField(User, blank=True, null=True, related_name='accounts_administered')

View File

@ -1,11 +0,0 @@
from rest_framework import serializers
from tests.accounts.models import Account
from tests.users.serializers import UserSerializer
class AccountSerializer(serializers.ModelSerializer):
admins = UserSerializer(many=True)
class Meta:
model = Account

View File

@ -33,9 +33,6 @@ def pytest_configure():
'rest_framework',
'rest_framework.authtoken',
'tests',
'tests.accounts',
'tests.records',
'tests.users',
),
PASSWORD_HASHERS=(
'django.contrib.auth.hashers.SHA1PasswordHasher',

View File

@ -1,33 +0,0 @@
# From test_validation...
class TestPreSaveValidationExclusions(TestCase):
def test_pre_save_validation_exclusions(self):
"""
Somewhat weird test case to ensure that we don't perform model
validation on read only fields.
"""
obj = ValidationModel.objects.create(blank_validated_field='')
request = factory.put('/', {}, format='json')
view = UpdateValidationModel().as_view()
response = view(request, pk=obj.pk).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
# From test_permissions...
class ModelPermissionsIntegrationTests(TestCase):
def setUp(...):
...
def test_has_put_as_create_permissions(self):
# User only has update permissions - should be able to update an entity.
request = factory.put('/1', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# But if PUTing to a new entity, permission should be denied.
request = factory.put('/2', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='2')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

View File

@ -1,6 +0,0 @@
from django.db import models
class Record(models.Model):
account = models.ForeignKey('accounts.Account', blank=True, null=True)
owner = models.ForeignKey('users.User', blank=True, null=True)

View File

@ -1,165 +0,0 @@
# Django settings for testproject project.
DEBUG = True
TEMPLATE_DEBUG = DEBUG
DEBUG_PROPAGATE_EXCEPTIONS = True
ALLOWED_HOSTS = ['*']
ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)
MANAGERS = ADMINS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'sqlite.db', # Or path to database file if using sqlite3.
'USER': '', # Not used with sqlite3.
'PASSWORD': '', # Not used with sqlite3.
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3.
}
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# On Unix systems, a value of None will cause Django to use the same
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'Europe/London'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-uk'
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale
USE_L10N = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = ''
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = ''
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
)
ROOT_URLCONF = 'tests.urls'
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'tests',
'tests.accounts',
'tests.records',
'tests.users',
)
# OAuth is optional and won't work if there is no oauth_provider & oauth2
try:
import oauth_provider # NOQA
import oauth2 # NOQA
except ImportError:
pass
else:
INSTALLED_APPS += (
'oauth_provider',
)
try:
import provider # NOQA
except ImportError:
pass
else:
INSTALLED_APPS += (
'provider',
'provider.oauth2',
)
# guardian is optional
try:
import guardian # NOQA
except ImportError:
pass
else:
ANONYMOUS_USER_ID = -1
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # default
'guardian.backends.ObjectPermissionBackend',
)
INSTALLED_APPS += (
'guardian',
)
STATIC_URL = '/static/'
PASSWORD_HASHERS = (
'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher',
'django.contrib.auth.hashers.CryptPasswordHasher',
)
AUTH_USER_MODEL = 'auth.User'
import django
if django.VERSION < (1, 3):
INSTALLED_APPS += ('staticfiles',)
# If we're running on the Jenkins server we want to archive the coverage reports as XML.
import os
if os.environ.get('HUDSON_URL', None):
TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
TEST_OUTPUT_VERBOSE = True
TEST_OUTPUT_DESCRIPTIONS = True
TEST_OUTPUT_DIR = 'xmlrunner'

View File

@ -90,7 +90,7 @@ class TestRegularFieldMappings(TestCase):
email_field = EmailField(max_length=100)
float_field = FloatField()
integer_field = IntegerField()
null_boolean_field = NullBooleanField()
null_boolean_field = NullBooleanField(required=False)
positive_integer_field = IntegerField()
positive_small_integer_field = IntegerField()
slug_field = SlugField(max_length=100)
@ -112,8 +112,8 @@ class TestRegularFieldMappings(TestCase):
id = IntegerField(label='ID', read_only=True)
value_limit_field = IntegerField(max_value=10, min_value=1)
length_limit_field = CharField(max_length=12, min_length=3)
blank_field = CharField(allow_blank=True, max_length=10)
null_field = IntegerField(allow_null=True)
blank_field = CharField(allow_blank=True, max_length=10, required=False)
null_field = IntegerField(allow_null=True, required=False)
default_field = IntegerField(required=False)
descriptive_field = IntegerField(help_text='Some help text', label='A label')
choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')])

View File

@ -95,59 +95,59 @@ class ModelPermissionsIntegrationTests(TestCase):
response = instance_view(request, pk=1)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
# def test_options_permitted(self):
# request = factory.options(
# '/',
# HTTP_AUTHORIZATION=self.permitted_credentials
# )
# response = root_view(request, pk='1')
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertIn('actions', response.data)
# self.assertEqual(list(response.data['actions'].keys()), ['POST'])
def test_options_permitted(self):
request = factory.options(
'/',
HTTP_AUTHORIZATION=self.permitted_credentials
)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['POST'])
# request = factory.options(
# '/1',
# HTTP_AUTHORIZATION=self.permitted_credentials
# )
# response = instance_view(request, pk='1')
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertIn('actions', response.data)
# self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
request = factory.options(
'/1',
HTTP_AUTHORIZATION=self.permitted_credentials
)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
# def test_options_disallowed(self):
# request = factory.options(
# '/',
# HTTP_AUTHORIZATION=self.disallowed_credentials
# )
# response = root_view(request, pk='1')
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertNotIn('actions', response.data)
def test_options_disallowed(self):
request = factory.options(
'/',
HTTP_AUTHORIZATION=self.disallowed_credentials
)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
# request = factory.options(
# '/1',
# HTTP_AUTHORIZATION=self.disallowed_credentials
# )
# response = instance_view(request, pk='1')
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertNotIn('actions', response.data)
request = factory.options(
'/1',
HTTP_AUTHORIZATION=self.disallowed_credentials
)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
# def test_options_updateonly(self):
# request = factory.options(
# '/',
# HTTP_AUTHORIZATION=self.updateonly_credentials
# )
# response = root_view(request, pk='1')
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertNotIn('actions', response.data)
def test_options_updateonly(self):
request = factory.options(
'/',
HTTP_AUTHORIZATION=self.updateonly_credentials
)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
# request = factory.options(
# '/1',
# HTTP_AUTHORIZATION=self.updateonly_credentials
# )
# response = instance_view(request, pk='1')
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertIn('actions', response.data)
# self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
request = factory.options(
'/1',
HTTP_AUTHORIZATION=self.updateonly_credentials
)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
class BasicPermModel(models.Model):

View File

@ -1,19 +0,0 @@
# from django.test import TestCase
# from rest_framework import serializers
# from tests.accounts.serializers import AccountSerializer
# class ImportingModelSerializerTests(TestCase):
# """
# In some situations like, GH #1225, it is possible, especially in
# testing, to import a serializer who's related models have not yet
# been resolved by Django. `AccountSerializer` is an example of such
# a serializer (imported at the top of this file).
# """
# def test_import_model_serializer(self):
# """
# The serializer at the top of this file should have been
# imported successfully, and we should be able to instantiate it.
# """
# self.assertIsInstance(AccountSerializer(), serializers.ModelSerializer)

View File

@ -1,6 +0,0 @@
from django.db import models
class User(models.Model):
account = models.ForeignKey('accounts.Account', blank=True, null=True, related_name='users')
active_record = models.ForeignKey('records.Record', blank=True, null=True)

View File

@ -1,8 +0,0 @@
from rest_framework import serializers
from tests.users.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User