mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-16 11:12:21 +03:00
Merge latest changes from master.
This commit is contained in:
commit
0b84c5a0ac
17
.travis.yml
17
.travis.yml
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,9 @@ The following people have helped make REST framework great.
|
||||||
* Marlon Bailey - [avinash240]
|
* Marlon Bailey - [avinash240]
|
||||||
* James Summerfield - [jsummerfield]
|
* James Summerfield - [jsummerfield]
|
||||||
* Andy Freeland - [rouge8]
|
* Andy Freeland - [rouge8]
|
||||||
|
* Craig de Stigter - [craigds]
|
||||||
|
* Pablo Recio - [pyriku]
|
||||||
|
* Brian Zambrano - [brianz]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -284,3 +287,6 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||||
[avinash240]: https://github.com/avinash240
|
[avinash240]: https://github.com/avinash240
|
||||||
[jsummerfield]: https://github.com/jsummerfield
|
[jsummerfield]: https://github.com/jsummerfield
|
||||||
[rouge8]: https://github.com/rouge8
|
[rouge8]: https://github.com/rouge8
|
||||||
|
[craigds]: https://github.com/craigds
|
||||||
|
[pyriku]: https://github.com/pyriku
|
||||||
|
[brianz]: https://github.com/brianz
|
||||||
|
|
|
@ -15,10 +15,12 @@ import warnings
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django import forms
|
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
|
||||||
|
@ -50,7 +52,7 @@ def get_component(obj, attr_name):
|
||||||
return that attribute on the object.
|
return that attribute on the object.
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
val = obj[attr_name]
|
val = obj.get(attr_name)
|
||||||
else:
|
else:
|
||||||
val = getattr(obj, attr_name)
|
val = getattr(obj, attr_name)
|
||||||
|
|
||||||
|
@ -170,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):
|
||||||
|
@ -402,6 +408,8 @@ class ChoiceField(WritableField):
|
||||||
def __init__(self, choices=(), *args, **kwargs):
|
def __init__(self, choices=(), *args, **kwargs):
|
||||||
super(ChoiceField, self).__init__(*args, **kwargs)
|
super(ChoiceField, self).__init__(*args, **kwargs)
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
|
if not self.required:
|
||||||
|
self.choices = BLANK_CHOICE_DASH + self.choices
|
||||||
|
|
||||||
def _get_choices(self):
|
def _get_choices(self):
|
||||||
return self._choices
|
return self._choices
|
||||||
|
|
|
@ -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:
|
||||||
|
queryset = obj.serializable_value(field_name)
|
||||||
except AttributeError:
|
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()]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -722,15 +722,14 @@ class ModelSerializer(Serializer):
|
||||||
Creates a default instance of a basic non-relational field.
|
Creates a default instance of a basic non-relational field.
|
||||||
"""
|
"""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
has_default = model_field.has_default()
|
|
||||||
|
|
||||||
if model_field.null or model_field.blank or has_default:
|
if model_field.null or model_field.blank:
|
||||||
kwargs['required'] = False
|
kwargs['required'] = False
|
||||||
|
|
||||||
if isinstance(model_field, models.AutoField) or not model_field.editable:
|
if isinstance(model_field, models.AutoField) or not model_field.editable:
|
||||||
kwargs['read_only'] = True
|
kwargs['read_only'] = True
|
||||||
|
|
||||||
if has_default:
|
if model_field.has_default():
|
||||||
kwargs['default'] = model_field.get_default()
|
kwargs['default'] = model_field.get_default()
|
||||||
|
|
||||||
if issubclass(model_field.__class__, models.TextField):
|
if issubclass(model_field.__class__, models.TextField):
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -646,3 +659,29 @@ class DecimalFieldTest(TestCase):
|
||||||
|
|
||||||
self.assertFalse(s.is_valid())
|
self.assertFalse(s.is_valid())
|
||||||
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
|
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceFieldTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for the ChoiceField options generator
|
||||||
|
"""
|
||||||
|
|
||||||
|
SAMPLE_CHOICES = [
|
||||||
|
('red', 'Red'),
|
||||||
|
('green', 'Green'),
|
||||||
|
('blue', 'Blue'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_choices_required(self):
|
||||||
|
"""
|
||||||
|
Make sure proper choices are rendered if field is required
|
||||||
|
"""
|
||||||
|
f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES)
|
||||||
|
self.assertEqual(f.choices, self.SAMPLE_CHOICES)
|
||||||
|
|
||||||
|
def test_choices_not_required(self):
|
||||||
|
"""
|
||||||
|
Make sure proper choices (plus blank) are rendered if the field isn't required
|
||||||
|
"""
|
||||||
|
f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
|
||||||
|
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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/'])
|
||||||
|
|
|
@ -2,7 +2,10 @@ 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 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -484,3 +487,56 @@ class PKManyToManyThroughTests(TestCase):
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(obj.name, 'target-2')
|
self.assertEqual(obj.name, 'target-2')
|
||||||
self.assertEqual(obj.sources.count(), 0)
|
self.assertEqual(obj.sources.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
# 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])
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.utils.datastructures import MultiValueDict
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -43,6 +45,17 @@ class CommentSerializer(serializers.Serializer):
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class NamesSerializer(serializers.Serializer):
|
||||||
|
first = serializers.CharField()
|
||||||
|
last = serializers.CharField(required=False, default='')
|
||||||
|
initials = serializers.CharField(required=False, default='')
|
||||||
|
|
||||||
|
|
||||||
|
class PersonIdentifierSerializer(serializers.Serializer):
|
||||||
|
ssn = serializers.CharField()
|
||||||
|
names = NamesSerializer(source='names', required=False)
|
||||||
|
|
||||||
|
|
||||||
class BookSerializer(serializers.ModelSerializer):
|
class BookSerializer(serializers.ModelSerializer):
|
||||||
isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
|
isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
|
||||||
|
|
||||||
|
@ -153,6 +166,42 @@ class BasicTests(TestCase):
|
||||||
self.assertFalse(serializer.object is expected)
|
self.assertFalse(serializer.object is expected)
|
||||||
self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
|
self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
|
||||||
|
|
||||||
|
def test_create_nested(self):
|
||||||
|
"""Test a serializer with nested data."""
|
||||||
|
names = {'first': 'John', 'last': 'Doe', 'initials': 'jd'}
|
||||||
|
data = {'ssn': '1234567890', 'names': names}
|
||||||
|
serializer = PersonIdentifierSerializer(data=data)
|
||||||
|
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, data)
|
||||||
|
self.assertFalse(serializer.object is data)
|
||||||
|
self.assertEqual(serializer.data['names'], names)
|
||||||
|
|
||||||
|
def test_create_partial_nested(self):
|
||||||
|
"""Test a serializer with nested data which has missing fields."""
|
||||||
|
names = {'first': 'John'}
|
||||||
|
data = {'ssn': '1234567890', 'names': names}
|
||||||
|
serializer = PersonIdentifierSerializer(data=data)
|
||||||
|
|
||||||
|
expected_names = {'first': 'John', 'last': '', 'initials': ''}
|
||||||
|
data['names'] = expected_names
|
||||||
|
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, data)
|
||||||
|
self.assertFalse(serializer.object is expected_names)
|
||||||
|
self.assertEqual(serializer.data['names'], expected_names)
|
||||||
|
|
||||||
|
def test_null_nested(self):
|
||||||
|
"""Test a serializer with a nonexistent nested field"""
|
||||||
|
data = {'ssn': '1234567890'}
|
||||||
|
serializer = PersonIdentifierSerializer(data=data)
|
||||||
|
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, data)
|
||||||
|
self.assertFalse(serializer.object is data)
|
||||||
|
expected = {'ssn': '1234567890', 'names': None}
|
||||||
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
serializer = CommentSerializer(self.comment, data=self.data)
|
serializer = CommentSerializer(self.comment, data=self.data)
|
||||||
expected = self.comment
|
expected = self.comment
|
||||||
|
@ -871,23 +920,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):
|
||||||
|
@ -1018,6 +1050,73 @@ class SerializerPickleTests(TestCase):
|
||||||
repr(pickle.loads(pickle.dumps(data, 0)))
|
repr(pickle.loads(pickle.dumps(data, 0)))
|
||||||
|
|
||||||
|
|
||||||
|
# test for issue #725
|
||||||
|
class SeveralChoicesModel(models.Model):
|
||||||
|
color = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')],
|
||||||
|
blank=False
|
||||||
|
)
|
||||||
|
drink = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')],
|
||||||
|
blank=False,
|
||||||
|
default='beer'
|
||||||
|
)
|
||||||
|
os = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')],
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
music_genre = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')],
|
||||||
|
blank=True,
|
||||||
|
default='metal'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerChoiceFields(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(SerializerChoiceFields, self).setUp()
|
||||||
|
|
||||||
|
class SeveralChoicesSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = SeveralChoicesModel
|
||||||
|
fields = ('color', 'drink', 'os', 'music_genre')
|
||||||
|
|
||||||
|
self.several_choices_serializer = SeveralChoicesSerializer
|
||||||
|
|
||||||
|
def test_choices_blank_false_not_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['color'].choices,
|
||||||
|
[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_choices_blank_false_with_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['drink'].choices,
|
||||||
|
[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_choices_blank_true_not_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['os'].choices,
|
||||||
|
BLANK_CHOICE_DASH + [('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_choices_blank_true_with_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['music_genre'].choices,
|
||||||
|
BLANK_CHOICE_DASH + [('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DepthTest(TestCase):
|
class DepthTest(TestCase):
|
||||||
def test_implicit_nesting(self):
|
def test_implicit_nesting(self):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user