Merge branch 'master' into 725-blank-choice-dash

Conflicts:
	rest_framework/tests/fields.py
This commit is contained in:
Pablo Recio 2013-05-18 12:51:40 +02:00
commit 53a8004401
11 changed files with 291 additions and 44 deletions

View File

@ -7,9 +7,9 @@ python:
- "3.3" - "3.3"
env: env:
- DJANGO="django==1.5 --use-mirrors" - DJANGO="django==1.5.1 --use-mirrors"
- DJANGO="django==1.4.3 --use-mirrors" - DJANGO="django==1.4.5 --use-mirrors"
- DJANGO="django==1.3.5 --use-mirrors" - DJANGO="django==1.3.7 --use-mirrors"
install: install:
- pip install $DJANGO - pip install $DJANGO
@ -18,7 +18,7 @@ install:
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi"
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi"
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6a1 --use-mirrors; fi" - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi"
- export PYTHONPATH=. - export PYTHONPATH=.
script: script:
@ -27,10 +27,11 @@ script:
matrix: matrix:
exclude: exclude:
- python: "3.2" - python: "3.2"
env: DJANGO="django==1.4.3 --use-mirrors" env: DJANGO="django==1.4.5 --use-mirrors"
- python: "3.2" - python: "3.2"
env: DJANGO="django==1.3.5 --use-mirrors" env: DJANGO="django==1.3.7 --use-mirrors"
- python: "3.3" - python: "3.3"
env: DJANGO="django==1.4.3 --use-mirrors" env: DJANGO="django==1.4.5 --use-mirrors"
- python: "3.3" - python: "3.3"
env: DJANGO="django==1.3.5 --use-mirrors" env: DJANGO="django==1.3.7 --use-mirrors"

View File

@ -20,6 +20,7 @@ from django import forms
from django.forms import widgets from django.forms import widgets
from django.utils.encoding import is_protected_type from django.utils.encoding import is_protected_type
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict
from rest_framework import ISO_8601 from rest_framework import ISO_8601
from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time
@ -171,7 +172,11 @@ class Field(object):
elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)): elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):
return [self.to_native(item) for item in value] return [self.to_native(item) for item in value]
elif isinstance(value, dict): elif isinstance(value, dict):
return dict(map(self.to_native, (k, v)) for k, v in value.items()) # Make sure we preserve field ordering, if it exists
ret = SortedDict()
for key, val in value.items():
ret[key] = self.to_native(val)
return ret
return smart_text(value) return smart_text(value)
def attributes(self): def attributes(self):

View File

@ -221,12 +221,20 @@ class PrimaryKeyRelatedField(RelatedField):
def field_to_native(self, obj, field_name): def field_to_native(self, obj, field_name):
if self.many: if self.many:
# To-many relationship # To-many relationship
try:
queryset = None
if not self.source:
# Prefer obj.serializable_value for performance reasons # Prefer obj.serializable_value for performance reasons
queryset = obj.serializable_value(self.source or field_name) try:
except AttributeError: queryset = obj.serializable_value(field_name)
except AttributeError:
pass
if queryset is None:
# RelatedManager (reverse relationship) # RelatedManager (reverse relationship)
queryset = getattr(obj, self.source or field_name) source = self.source or field_name
queryset = obj
for component in source.split('.'):
queryset = get_component(queryset, component)
# Forward relationship # Forward relationship
return [self.to_native(item.pk) for item in queryset.all()] return [self.to_native(item.pk) for item in queryset.all()]

View File

@ -336,7 +336,7 @@ class BrowsableAPIRenderer(BaseRenderer):
return # Cannot use form overloading return # Cannot use form overloading
try: try:
view.check_permissions(clone_request(request, method)) view.check_permissions(request)
except exceptions.APIException: except exceptions.APIException:
return False # Doesn't have permissions return False # Doesn't have permissions
return True return True
@ -372,6 +372,30 @@ class BrowsableAPIRenderer(BaseRenderer):
return fields return fields
def _get_form(self, view, method, request):
# We need to impersonate a request with the correct method,
# so that eg. any dynamic get_serializer_class methods return the
# correct form for each method.
restore = view.request
request = clone_request(request, method)
view.request = request
try:
return self.get_form(view, method, request)
finally:
view.request = restore
def _get_raw_data_form(self, view, method, request, media_types):
# We need to impersonate a request with the correct method,
# so that eg. any dynamic get_serializer_class methods return the
# correct form for each method.
restore = view.request
request = clone_request(request, method)
view.request = request
try:
return self.get_raw_data_form(view, method, request, media_types)
finally:
view.request = restore
def get_form(self, view, method, request): def get_form(self, view, method, request):
""" """
Get a form, possibly bound to either the input or output data. Get a form, possibly bound to either the input or output data.
@ -465,15 +489,15 @@ class BrowsableAPIRenderer(BaseRenderer):
renderer = self.get_default_renderer(view) renderer = self.get_default_renderer(view)
content = self.get_content(renderer, data, accepted_media_type, renderer_context) content = self.get_content(renderer, data, accepted_media_type, renderer_context)
put_form = self.get_form(view, 'PUT', request) put_form = self._get_form(view, 'PUT', request)
post_form = self.get_form(view, 'POST', request) post_form = self._get_form(view, 'POST', request)
patch_form = self.get_form(view, 'PATCH', request) patch_form = self._get_form(view, 'PATCH', request)
delete_form = self.get_form(view, 'DELETE', request) delete_form = self._get_form(view, 'DELETE', request)
options_form = self.get_form(view, 'OPTIONS', request) options_form = self._get_form(view, 'OPTIONS', request)
raw_data_put_form = self.get_raw_data_form(view, 'PUT', request, media_types) raw_data_put_form = self._get_raw_data_form(view, 'PUT', request, media_types)
raw_data_post_form = self.get_raw_data_form(view, 'POST', request, media_types) raw_data_post_form = self._get_raw_data_form(view, 'POST', request, media_types)
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request, media_types) raw_data_patch_form = self._get_raw_data_form(view, 'PATCH', request, media_types)
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
name = self.get_name(view) name = self.get_name(view)

View File

@ -4,6 +4,8 @@ DEBUG = True
TEMPLATE_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG
DEBUG_PROPAGATE_EXCEPTIONS = True DEBUG_PROPAGATE_EXCEPTIONS = True
ALLOWED_HOSTS = ['*']
ADMINS = ( ADMINS = (
# ('Your Name', 'your_email@domain.com'), # ('Your Name', 'your_email@domain.com'),
) )

View File

@ -2,13 +2,12 @@
General serializer field tests. General serializer field tests.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.datastructures import SortedDict
import datetime import datetime
from decimal import Decimal from decimal import Decimal
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.core import validators from django.core import validators
from rest_framework import serializers from rest_framework import serializers
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
@ -63,6 +62,20 @@ class BasicFieldTests(TestCase):
serializer = CharPrimaryKeyModelSerializer() serializer = CharPrimaryKeyModelSerializer()
self.assertEqual(serializer.fields['id'].read_only, False) self.assertEqual(serializer.fields['id'].read_only, False)
def test_dict_field_ordering(self):
"""
Field should preserve dictionary ordering, if it exists.
See: https://github.com/tomchristie/django-rest-framework/issues/832
"""
ret = SortedDict()
ret['c'] = 1
ret['b'] = 1
ret['a'] = 1
ret['z'] = 1
field = serializers.Field()
keys = list(field.to_native(ret).keys())
self.assertEqual(keys, ['c', 'b', 'a', 'z'])
class DateFieldTest(TestCase): class DateFieldTest(TestCase):
""" """

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.db import models from django.db import models
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.test import TestCase from django.test import TestCase
from rest_framework import generics, serializers, status from rest_framework import generics, renderers, serializers, status
from rest_framework.tests.utils import RequestFactory from rest_framework.tests.utils import RequestFactory
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
from rest_framework.compat import six from rest_framework.compat import six
@ -476,3 +476,35 @@ class TestFilterBackendAppliedToViews(TestCase):
response = instance_view(request, pk=1).render() response = instance_view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foo'}) self.assertEqual(response.data, {'id': 1, 'text': 'foo'})
class TwoFieldModel(models.Model):
field_a = models.CharField(max_length=100)
field_b = models.CharField(max_length=100)
class DynamicSerializerView(generics.ListCreateAPIView):
model = TwoFieldModel
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
def get_serializer_class(self):
if self.request.method == 'POST':
class DynamicSerializer(serializers.ModelSerializer):
class Meta:
model = TwoFieldModel
fields = ('field_b',)
return DynamicSerializer
return super(DynamicSerializerView, self).get_serializer_class()
class TestFilterBackendAppliedToViews(TestCase):
def test_dynamic_serializer_form_in_browsable_api(self):
"""
GET requests to ListCreateAPIView should return filtered list.
"""
view = DynamicSerializerView.as_view()
request = factory.get('/')
response = view(request).render()
self.assertContains(response, 'field_b')
self.assertNotContains(response, 'field_a')

View File

@ -5,6 +5,7 @@ from __future__ import unicode_literals
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import BlogPost
class NullModel(models.Model): class NullModel(models.Model):
@ -33,7 +34,7 @@ class FieldTests(TestCase):
self.assertRaises(serializers.ValidationError, field.from_native, []) self.assertRaises(serializers.ValidationError, field.from_native, [])
class TestManyRelateMixin(TestCase): class TestManyRelatedMixin(TestCase):
def test_missing_many_to_many_related_field(self): def test_missing_many_to_many_related_field(self):
''' '''
Regression test for #632 Regression test for #632
@ -45,3 +46,55 @@ class TestManyRelateMixin(TestCase):
into = {} into = {}
field.field_from_native({}, None, 'field_name', into) field.field_from_native({}, None, 'field_name', into)
self.assertEqual(into['field_name'], []) self.assertEqual(into['field_name'], [])
# Regression tests for #694 (`source` attribute on related fields)
class RelatedFieldSourceTests(TestCase):
def test_related_manager_source(self):
"""
Relational fields should be able to use manager-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.RelatedField(many=True, source='get_blogposts_manager')
class ClassWithManagerMethod(object):
def get_blogposts_manager(self):
return BlogPost.objects
obj = ClassWithManagerMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['BlogPost object'])
def test_related_queryset_source(self):
"""
Relational fields should be able to use queryset-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.RelatedField(many=True, source='get_blogposts_queryset')
class ClassWithQuerysetMethod(object):
def get_blogposts_queryset(self):
return BlogPost.objects.all()
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['BlogPost object'])
def test_dotted_source(self):
"""
Source argument should support dotted.source notation.
"""
BlogPost.objects.create(title='blah')
field = serializers.RelatedField(many=True, source='a.b.c')
class ClassWithQuerysetMethod(object):
a = {
'b': {
'c': BlogPost.objects.all()
}
}
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['BlogPost object'])

View File

@ -4,6 +4,7 @@ from django.test.client import RequestFactory
from rest_framework import serializers from rest_framework import serializers
from rest_framework.compat import patterns, url from rest_framework.compat import patterns, url
from rest_framework.tests.models import ( from rest_framework.tests.models import (
BlogPost,
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
) )
@ -16,6 +17,7 @@ def dummy_view(request, pk):
pass pass
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'), url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'), url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'), url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
@ -451,3 +453,72 @@ class HyperlinkedNullableOneToOneTests(TestCase):
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None}, {'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
# Regression tests for #694 (`source` attribute on related fields)
class HyperlinkedRelatedFieldSourceTests(TestCase):
urls = 'rest_framework.tests.relations_hyperlink'
def test_related_manager_source(self):
"""
Relational fields should be able to use manager-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.HyperlinkedRelatedField(
many=True,
source='get_blogposts_manager',
view_name='dummy-url',
)
field.context = {'request': request}
class ClassWithManagerMethod(object):
def get_blogposts_manager(self):
return BlogPost.objects
obj = ClassWithManagerMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
def test_related_queryset_source(self):
"""
Relational fields should be able to use queryset-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.HyperlinkedRelatedField(
many=True,
source='get_blogposts_queryset',
view_name='dummy-url',
)
field.context = {'request': request}
class ClassWithQuerysetMethod(object):
def get_blogposts_queryset(self):
return BlogPost.objects.all()
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
def test_dotted_source(self):
"""
Source argument should support dotted.source notation.
"""
BlogPost.objects.create(title='blah')
field = serializers.HyperlinkedRelatedField(
many=True,
source='a.b.c',
view_name='dummy-url',
)
field.context = {'request': request}
class ClassWithQuerysetMethod(object):
a = {
'b': {
'c': BlogPost.objects.all()
}
}
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['http://testserver/dummyurl/1/'])

View File

@ -1,7 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource from rest_framework.tests.models import (
BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,
)
from rest_framework.compat import six from rest_framework.compat import six
@ -421,3 +424,55 @@ class PKNullableOneToOneTests(TestCase):
{'id': 2, 'name': 'target-2', 'nullable_source': 1}, {'id': 2, 'name': 'target-2', 'nullable_source': 1},
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
# Regression tests for #694 (`source` attribute on related fields)
class PrimaryKeyRelatedFieldSourceTests(TestCase):
def test_related_manager_source(self):
"""
Relational fields should be able to use manager-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager')
class ClassWithManagerMethod(object):
def get_blogposts_manager(self):
return BlogPost.objects
obj = ClassWithManagerMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, [1])
def test_related_queryset_source(self):
"""
Relational fields should be able to use queryset-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset')
class ClassWithQuerysetMethod(object):
def get_blogposts_queryset(self):
return BlogPost.objects.all()
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, [1])
def test_dotted_source(self):
"""
Source argument should support dotted.source notation.
"""
BlogPost.objects.create(title='blah')
field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c')
class ClassWithQuerysetMethod(object):
a = {
'b': {
'c': BlogPost.objects.all()
}
}
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, [1])

View File

@ -872,23 +872,6 @@ class RelatedTraversalTest(TestCase):
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
def test_queryset_nested_traversal(self):
"""
Relational fields should be able to use methods as their source.
"""
BlogPost.objects.create(title='blah')
class QuerysetMethodSerializer(serializers.Serializer):
blogposts = serializers.RelatedField(many=True, source='get_all_blogposts')
class ClassWithQuerysetMethod(object):
def get_all_blogposts(self):
return BlogPost.objects
obj = ClassWithQuerysetMethod()
serializer = QuerysetMethodSerializer(obj)
self.assertEqual(serializer.data, {'blogposts': ['BlogPost object']})
class SerializerMethodFieldTests(TestCase): class SerializerMethodFieldTests(TestCase):
def setUp(self): def setUp(self):