Repository: encode/django-rest-framework

Issue:
Add DictField (Which should support 1.8's HStore field)
We should include a `MappingField` or `DictField` and automatically map to this from Django 1.8's `HStoreField`.



Discussion:

Resolving patch:
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -58,6 +58,13 @@ def total_seconds(timedelta):
     from django.http import HttpResponse as HttpResponseBase
 
 
+# contrib.postgres only supported from 1.8 onwards.
+try:
+    from django.contrib.postgres import fields as postgres_fields
+except ImportError:
+    postgres_fields = None
+
+
 # request only provides `resolver_match` from 1.5 onwards.
 def get_resolver_match(request):
     try:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1132,8 +1132,21 @@ def to_internal_value(self, data):
 
 # Composite field types...
 
+class _UnvalidatedField(Field):
+    def __init__(self, *args, **kwargs):
+        super(_UnvalidatedField, self).__init__(*args, **kwargs)
+        self.allow_blank = True
+        self.allow_null = True
+
+    def to_internal_value(self, data):
+        return data
+
+    def to_representation(self, value):
+        return value
+
+
 class ListField(Field):
-    child = None
+    child = _UnvalidatedField()
     initial = []
     default_error_messages = {
         'not_a_list': _('Expected a list of items but got type `{input_type}`')
@@ -1141,7 +1154,6 @@ class ListField(Field):
 
     def __init__(self, *args, **kwargs):
         self.child = kwargs.pop('child', copy.deepcopy(self.child))
-        assert self.child is not None, '`child` is a required argument.'
         assert not inspect.isclass(self.child), '`child` has not been instantiated.'
         super(ListField, self).__init__(*args, **kwargs)
         self.child.bind(field_name='', parent=self)
@@ -1170,6 +1182,49 @@ def to_representation(self, data):
         return [self.child.to_representation(item) for item in data]
 
 
+class DictField(Field):
+    child = _UnvalidatedField()
+    initial = []
+    default_error_messages = {
+        'not_a_dict': _('Expected a dictionary of items but got type `{input_type}`')
+    }
+
+    def __init__(self, *args, **kwargs):
+        self.child = kwargs.pop('child', copy.deepcopy(self.child))
+        assert not inspect.isclass(self.child), '`child` has not been instantiated.'
+        super(DictField, self).__init__(*args, **kwargs)
+        self.child.bind(field_name='', parent=self)
+
+    def get_value(self, dictionary):
+        # We override the default field access in order to support
+        # lists in HTML forms.
+        if html.is_html_input(dictionary):
+            return html.parse_html_list(dictionary, prefix=self.field_name)
+        return dictionary.get(self.field_name, empty)
+
+    def to_internal_value(self, data):
+        """
+        Dicts of native values <- Dicts of primitive datatypes.
+        """
+        if html.is_html_input(data):
+            data = html.parse_html_dict(data)
+        if not isinstance(data, dict):
+            self.fail('not_a_dict', input_type=type(data).__name__)
+        return dict([
+            (six.text_type(key), self.child.run_validation(value))
+            for key, value in data.items()
+        ])
+
+    def to_representation(self, value):
+        """
+        List of object instances -> List of dicts of primitive datatypes.
+        """
+        return dict([
+            (six.text_type(key), self.child.to_representation(val))
+            for key, val in value.items()
+        ])
+
+
 # Miscellaneous field types...
 
 class ReadOnlyField(Field):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -14,7 +14,7 @@
 from django.db import models
 from django.db.models.fields import FieldDoesNotExist, Field as DjangoField
 from django.utils.translation import ugettext_lazy as _
-from rest_framework.compat import unicode_to_repr
+from rest_framework.compat import postgres_fields, unicode_to_repr
 from rest_framework.utils import model_meta
 from rest_framework.utils.field_mapping import (
     get_url_kwargs, get_field_kwargs,
@@ -1137,6 +1137,12 @@ class Meta:
 if hasattr(models, 'UUIDField'):
     ModelSerializer._field_mapping[models.UUIDField] = UUIDField
 
+if postgres_fields:
+    class CharMappingField(DictField):
+        child = CharField()
+
+    ModelSerializer._field_mapping[postgres_fields.HStoreField] = CharMappingField
+
 
 class HyperlinkedModelSerializer(ModelSerializer):
     """


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1047,7 +1047,7 @@ class TestValidImageField(FieldValues):
 
 class TestListField(FieldValues):
     """
-    Values for `ListField`.
+    Values for `ListField` with IntegerField as child.
     """
     valid_inputs = [
         ([1, 2, 3], [1, 2, 3]),
@@ -1064,6 +1064,55 @@ class TestListField(FieldValues):
     field = serializers.ListField(child=serializers.IntegerField())
 
 
+class TestUnvalidatedListField(FieldValues):
+    """
+    Values for `ListField` with no `child` argument.
+    """
+    valid_inputs = [
+        ([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]),
+    ]
+    invalid_inputs = [
+        ('not a list', ['Expected a list of items but got type `str`']),
+    ]
+    outputs = [
+        ([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]),
+    ]
+    field = serializers.ListField()
+
+
+class TestDictField(FieldValues):
+    """
+    Values for `ListField` with CharField as child.
+    """
+    valid_inputs = [
+        ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
+    ]
+    invalid_inputs = [
+        ({'a': 1, 'b': None}, ['This field may not be null.']),
+        ('not a dict', ['Expected a dictionary of items but got type `str`']),
+    ]
+    outputs = [
+        ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
+    ]
+    field = serializers.DictField(child=serializers.CharField())
+
+
+class TestUnvalidatedDictField(FieldValues):
+    """
+    Values for `ListField` with no `child` argument.
+    """
+    valid_inputs = [
+        ({'a': 1, 'b': [4, 5, 6], 1: 123}, {'a': 1, 'b': [4, 5, 6], '1': 123}),
+    ]
+    invalid_inputs = [
+        ('not a dict', ['Expected a dictionary of items but got type `str`']),
+    ]
+    outputs = [
+        ({'a': 1, 'b': [4, 5, 6]}, {'a': 1, 'b': [4, 5, 6]}),
+    ]
+    field = serializers.DictField()
+
+
 # Tests for FieldField.
 # ---------------------