diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 34ed758f7..dba9fd9f0 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -130,6 +130,34 @@ 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 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 + +# for key in translated_data.keys(): +# if key in mapping: + else: # Data can be None so translated_data is too + translated_data = None + + return translated_data + + def _get_declared_fields(bases, attrs): """ Create a list of serializer field instances from the passed in 'attrs', @@ -166,6 +194,7 @@ class SerializerOptions(object): self.depth = getattr(meta, 'depth', 0) self.fields = getattr(meta, 'fields', ()) self.exclude = getattr(meta, 'exclude', ()) + self.convert_fields = getattr(meta, 'convert_fields', False) class BaseSerializer(WritableField): @@ -287,27 +316,14 @@ class BaseSerializer(WritableField): self._errors['non_field_errors'] = ['Invalid data'] return None - # Handle translation of serialized fields into non serailzed fields - if data is not None: - translated_data = copy.deepcopy(data) - field_name_map = self.get_field_name_map() - for key in translated_data.keys(): - if key in field_name_map: - newkey = field_name_map.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 + 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(): field.initialize(parent=self, field_name=field_name) try: - field.field_from_native(translated_data, files, field_name, reverted_data) + field.field_from_native(data, files, field_name, reverted_data) except ValidationError as err: self._errors[field_name] = list(err.messages) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 6c8f2342b..7646facb7 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -175,3 +175,8 @@ class FilterableItem(models.Model): text = models.CharField(max_length=100) decimal = models.DecimalField(max_digits=4, decimal_places=2) date = models.DateField() + + +class ModelWithUnderscoreFields(RESTFrameworkModel): + char_field = models.CharField(max_length=100) + number_field = models.IntegerField() diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 3ee2b38a7..c4851da09 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -7,9 +7,12 @@ from django.utils import unittest from django.utils.datastructures import MultiValueDict from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers, fields, relations -from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, - BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel, - ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel) +from rest_framework.tests.models import ( + HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, + BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, + DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel, + Photo, RESTFrameworkModel, ModelWithUnderscoreFields + ) from rest_framework.tests.models import BasicModelSerializer import datetime import pickle @@ -176,6 +179,16 @@ class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer): 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): def setUp(self): self.comment = Comment( @@ -331,6 +344,23 @@ class BasicTests(TestCase): exclusions = serializer.get_validation_exclusions() 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): """ @@ -836,6 +866,22 @@ class ManyToManyTests(TestCase): self.assertEqual(instance.pk, 2) 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): def setUp(self):