mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-03 20:10:10 +03:00
Merge f9dd11df72
into 1d404874b3
This commit is contained in:
commit
e6dd318f9e
|
@ -15,6 +15,7 @@ import copy
|
||||||
import datetime
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
import types
|
import types
|
||||||
|
import warnings
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.contrib.contenttypes.generic import GenericForeignKey
|
from django.contrib.contenttypes.generic import GenericForeignKey
|
||||||
from django.core.paginator import Page
|
from django.core.paginator import Page
|
||||||
|
@ -131,6 +132,31 @@ def _is_protected_type(obj):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_fields(data, mapping):
|
||||||
|
"""
|
||||||
|
Takes data as dict or none and str->str map of keys and
|
||||||
|
returns None or new dictionary with converted keys
|
||||||
|
"""
|
||||||
|
# Handle translation of serialized fields into non serailzed fields
|
||||||
|
if data is not None:
|
||||||
|
translated_data = copy.deepcopy(data)
|
||||||
|
for key in mapping.keys():
|
||||||
|
if key not in translated_data:
|
||||||
|
continue
|
||||||
|
newkey = mapping.get(key)
|
||||||
|
try: # MultiValueDict
|
||||||
|
value = translated_data.getlist(key)
|
||||||
|
del translated_data[key]
|
||||||
|
translated_data.setlist(newkey, value)
|
||||||
|
except AttributeError:
|
||||||
|
value = translated_data.pop(key)
|
||||||
|
translated_data[newkey] = value
|
||||||
|
else: # Data can be None so translated_data is too
|
||||||
|
translated_data = None
|
||||||
|
|
||||||
|
return translated_data
|
||||||
|
|
||||||
|
|
||||||
def _get_declared_fields(bases, attrs):
|
def _get_declared_fields(bases, attrs):
|
||||||
"""
|
"""
|
||||||
Create a list of serializer field instances from the passed in 'attrs',
|
Create a list of serializer field instances from the passed in 'attrs',
|
||||||
|
@ -167,6 +193,7 @@ class SerializerOptions(object):
|
||||||
self.depth = getattr(meta, 'depth', 0)
|
self.depth = getattr(meta, 'depth', 0)
|
||||||
self.fields = getattr(meta, 'fields', ())
|
self.fields = getattr(meta, 'fields', ())
|
||||||
self.exclude = getattr(meta, 'exclude', ())
|
self.exclude = getattr(meta, 'exclude', ())
|
||||||
|
self.convert_fields = getattr(meta, 'convert_fields', False)
|
||||||
|
|
||||||
|
|
||||||
class BaseSerializer(WritableField):
|
class BaseSerializer(WritableField):
|
||||||
|
@ -265,6 +292,20 @@ class BaseSerializer(WritableField):
|
||||||
"""
|
"""
|
||||||
return field_name
|
return field_name
|
||||||
|
|
||||||
|
def get_field_name_map(self):
|
||||||
|
"""
|
||||||
|
Return a map of serialized->python field names
|
||||||
|
"""
|
||||||
|
ret = SortedDict()
|
||||||
|
for name, value in list(self.fields.items()):
|
||||||
|
key = self.get_field_key(name)
|
||||||
|
if key in ret:
|
||||||
|
warnings.warn("Duplicate key found in fields. This can happen if `get_field_key`"
|
||||||
|
" can return the same string for two different inputs! Ensure your keys are unique"
|
||||||
|
" after running them all through `get_field_key`", Warning, stacklevel=3)
|
||||||
|
ret[key] = name
|
||||||
|
return ret
|
||||||
|
|
||||||
def restore_fields(self, data, files):
|
def restore_fields(self, data, files):
|
||||||
"""
|
"""
|
||||||
Core of deserialization, together with `restore_object`.
|
Core of deserialization, together with `restore_object`.
|
||||||
|
@ -276,6 +317,10 @@ class BaseSerializer(WritableField):
|
||||||
self._errors['non_field_errors'] = ['Invalid data']
|
self._errors['non_field_errors'] = ['Invalid data']
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if self.opts.convert_fields:
|
||||||
|
key_map = self.get_field_name_map()
|
||||||
|
data = _convert_fields(data, key_map)
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
for field_name, field in self.fields.items():
|
||||||
field.initialize(parent=self, field_name=field_name)
|
field.initialize(parent=self, field_name=field_name)
|
||||||
try:
|
try:
|
||||||
|
@ -758,9 +803,9 @@ class ModelSerializer(Serializer):
|
||||||
field.read_only = True
|
field.read_only = True
|
||||||
|
|
||||||
ret[accessor_name] = field
|
ret[accessor_name] = field
|
||||||
|
|
||||||
# Ensure that 'read_only_fields' is an iterable
|
# Ensure that 'read_only_fields' is an iterable
|
||||||
assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
|
assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
|
||||||
|
|
||||||
# Add the `read_only` flag to any fields that have been specified
|
# Add the `read_only` flag to any fields that have been specified
|
||||||
# in the `read_only_fields` option
|
# in the `read_only_fields` option
|
||||||
|
@ -775,10 +820,10 @@ class ModelSerializer(Serializer):
|
||||||
"on serializer '%s'." %
|
"on serializer '%s'." %
|
||||||
(field_name, self.__class__.__name__))
|
(field_name, self.__class__.__name__))
|
||||||
ret[field_name].read_only = True
|
ret[field_name].read_only = True
|
||||||
|
|
||||||
# Ensure that 'write_only_fields' is an iterable
|
# Ensure that 'write_only_fields' is an iterable
|
||||||
assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple'
|
assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple'
|
||||||
|
|
||||||
for field_name in self.opts.write_only_fields:
|
for field_name in self.opts.write_only_fields:
|
||||||
assert field_name not in self.base_fields.keys(), (
|
assert field_name not in self.base_fields.keys(), (
|
||||||
"field '%s' on serializer '%s' specified in "
|
"field '%s' on serializer '%s' specified in "
|
||||||
|
@ -789,7 +834,7 @@ class ModelSerializer(Serializer):
|
||||||
"Non-existant field '%s' specified in `write_only_fields` "
|
"Non-existant field '%s' specified in `write_only_fields` "
|
||||||
"on serializer '%s'." %
|
"on serializer '%s'." %
|
||||||
(field_name, self.__class__.__name__))
|
(field_name, self.__class__.__name__))
|
||||||
ret[field_name].write_only = True
|
ret[field_name].write_only = True
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
|
@ -175,3 +175,8 @@ class FilterableItem(models.Model):
|
||||||
text = models.CharField(max_length=100)
|
text = models.CharField(max_length=100)
|
||||||
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
|
|
||||||
|
|
||||||
|
class ModelWithUnderscoreFields(RESTFrameworkModel):
|
||||||
|
char_field = models.CharField(max_length=100)
|
||||||
|
number_field = models.IntegerField()
|
||||||
|
|
|
@ -7,9 +7,12 @@ from django.utils import unittest
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.utils.datastructures import MultiValueDict
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers, fields, relations
|
from rest_framework import serializers, fields, relations
|
||||||
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
|
from rest_framework.tests.models import (
|
||||||
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
|
HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
|
||||||
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
|
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel,
|
||||||
|
DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel,
|
||||||
|
Photo, RESTFrameworkModel, ModelWithUnderscoreFields
|
||||||
|
)
|
||||||
from rest_framework.tests.models import BasicModelSerializer
|
from rest_framework.tests.models import BasicModelSerializer
|
||||||
import datetime
|
import datetime
|
||||||
import pickle
|
import pickle
|
||||||
|
@ -176,6 +179,16 @@ class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
|
||||||
fields = ['some_integer']
|
fields = ['some_integer']
|
||||||
|
|
||||||
|
|
||||||
|
class ModelFieldConversionSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
def get_field_key(self, field_name):
|
||||||
|
return field_name.replace('_','').capitalize()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
convert_fields = True
|
||||||
|
model = ModelWithUnderscoreFields
|
||||||
|
|
||||||
|
|
||||||
class BasicTests(TestCase):
|
class BasicTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.comment = Comment(
|
self.comment = Comment(
|
||||||
|
@ -331,6 +344,23 @@ class BasicTests(TestCase):
|
||||||
exclusions = serializer.get_validation_exclusions()
|
exclusions = serializer.get_validation_exclusions()
|
||||||
self.assertTrue('title' in exclusions, '`title` field was marked `required=False` and should be excluded')
|
self.assertTrue('title' in exclusions, '`title` field was marked `required=False` and should be excluded')
|
||||||
|
|
||||||
|
def test_serialize_with_conversion(self):
|
||||||
|
"""
|
||||||
|
Verify keys get converted from serialized value to deserialized value
|
||||||
|
"""
|
||||||
|
|
||||||
|
underscore = ModelWithUnderscoreFields(char_field='slartibartfast', number_field=42)
|
||||||
|
underscore.save()
|
||||||
|
serializer = ModelFieldConversionSerializer(underscore)
|
||||||
|
serialized = {'Id': 1, 'Numberfield': 42, 'Charfield':'slartibartfast'}
|
||||||
|
self.assertEqual(serialized, serializer.data, "Validate that serialing data with conversion works")
|
||||||
|
|
||||||
|
serializer = ModelFieldConversionSerializer(data=serialized)
|
||||||
|
self.assertTrue(serializer.is_valid(),
|
||||||
|
'Data should get converted from serialized value into deserialized value')
|
||||||
|
self.assertEqual('slartibartfast', serializer.object.char_field)
|
||||||
|
self.assertEqual(42, serializer.object.number_field)
|
||||||
|
|
||||||
|
|
||||||
class DictStyleSerializer(serializers.Serializer):
|
class DictStyleSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
|
@ -836,6 +866,22 @@ class ManyToManyTests(TestCase):
|
||||||
self.assertEqual(instance.pk, 2)
|
self.assertEqual(instance.pk, 2)
|
||||||
self.assertEqual(list(instance.rel.all()), [])
|
self.assertEqual(list(instance.rel.all()), [])
|
||||||
|
|
||||||
|
def test_create_empty_relationship_flat_data_field_convert(self):
|
||||||
|
"""
|
||||||
|
Create an instance of a model with a ManyToMany relationship,
|
||||||
|
containing no items, using a representation that does not support
|
||||||
|
lists (eg form data).
|
||||||
|
"""
|
||||||
|
data = MultiValueDict()
|
||||||
|
data.setlist('rel', [''])
|
||||||
|
self.serializer_class.Meta.convert_fields = True
|
||||||
|
serializer = self.serializer_class(data=data)
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
instance = serializer.save()
|
||||||
|
self.assertEqual(len(ManyToManyModel.objects.all()), 2)
|
||||||
|
self.assertEqual(instance.pk, 2)
|
||||||
|
self.assertEqual(list(instance.rel.all()), [])
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyManyToManyTests(TestCase):
|
class ReadOnlyManyToManyTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user