Skip to content

Commit 35f6a82

Browse files
committed
Added DictField and support for HStoreField.
1 parent 889a07f commit 35f6a82

File tree

5 files changed

+139
-5
lines changed

5 files changed

+139
-5
lines changed

docs/api-guide/fields.md

+18-1
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ A field class that validates a list of objects.
380380

381381
**Signature**: `ListField(child)`
382382

383-
- `child` - A field instance that should be used for validating the objects in the list.
383+
- `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated.
384384

385385
For example, to validate a list of integers you might use something like the following:
386386

@@ -395,6 +395,23 @@ The `ListField` class also supports a declarative style that allows you to write
395395

396396
We can now reuse our custom `StringListField` class throughout our application, without having to provide a `child` argument to it.
397397

398+
## DictField
399+
400+
A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values.
401+
402+
**Signature**: `DictField(child)`
403+
404+
- `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated.
405+
406+
For example, to create a field that validates a mapping of strings to strings, you would write something like this:
407+
408+
document = DictField(child=CharField())
409+
410+
You can also use the declarative style, as with `ListField`. For example:
411+
412+
class DocumentField(DictField):
413+
child = CharField()
414+
398415
---
399416

400417
# Miscellaneous fields

rest_framework/compat.py

+7
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ def total_seconds(timedelta):
5858
from django.http import HttpResponse as HttpResponseBase
5959

6060

61+
# contrib.postgres only supported from 1.8 onwards.
62+
try:
63+
from django.contrib.postgres import fields as postgres_fields
64+
except ImportError:
65+
postgres_fields = None
66+
67+
6168
# request only provides `resolver_match` from 1.5 onwards.
6269
def get_resolver_match(request):
6370
try:

rest_framework/fields.py

+57-2
Original file line numberDiff line numberDiff line change
@@ -1132,16 +1132,28 @@ def to_internal_value(self, data):
11321132

11331133
# Composite field types...
11341134

1135+
class _UnvalidatedField(Field):
1136+
def __init__(self, *args, **kwargs):
1137+
super(_UnvalidatedField, self).__init__(*args, **kwargs)
1138+
self.allow_blank = True
1139+
self.allow_null = True
1140+
1141+
def to_internal_value(self, data):
1142+
return data
1143+
1144+
def to_representation(self, value):
1145+
return value
1146+
1147+
11351148
class ListField(Field):
1136-
child = None
1149+
child = _UnvalidatedField()
11371150
initial = []
11381151
default_error_messages = {
11391152
'not_a_list': _('Expected a list of items but got type `{input_type}`')
11401153
}
11411154

11421155
def __init__(self, *args, **kwargs):
11431156
self.child = kwargs.pop('child', copy.deepcopy(self.child))
1144-
assert self.child is not None, '`child` is a required argument.'
11451157
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
11461158
super(ListField, self).__init__(*args, **kwargs)
11471159
self.child.bind(field_name='', parent=self)
@@ -1170,6 +1182,49 @@ def to_representation(self, data):
11701182
return [self.child.to_representation(item) for item in data]
11711183

11721184

1185+
class DictField(Field):
1186+
child = _UnvalidatedField()
1187+
initial = []
1188+
default_error_messages = {
1189+
'not_a_dict': _('Expected a dictionary of items but got type `{input_type}`')
1190+
}
1191+
1192+
def __init__(self, *args, **kwargs):
1193+
self.child = kwargs.pop('child', copy.deepcopy(self.child))
1194+
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
1195+
super(DictField, self).__init__(*args, **kwargs)
1196+
self.child.bind(field_name='', parent=self)
1197+
1198+
def get_value(self, dictionary):
1199+
# We override the default field access in order to support
1200+
# lists in HTML forms.
1201+
if html.is_html_input(dictionary):
1202+
return html.parse_html_list(dictionary, prefix=self.field_name)
1203+
return dictionary.get(self.field_name, empty)
1204+
1205+
def to_internal_value(self, data):
1206+
"""
1207+
Dicts of native values <- Dicts of primitive datatypes.
1208+
"""
1209+
if html.is_html_input(data):
1210+
data = html.parse_html_dict(data)
1211+
if not isinstance(data, dict):
1212+
self.fail('not_a_dict', input_type=type(data).__name__)
1213+
return dict([
1214+
(six.text_type(key), self.child.run_validation(value))
1215+
for key, value in data.items()
1216+
])
1217+
1218+
def to_representation(self, value):
1219+
"""
1220+
List of object instances -> List of dicts of primitive datatypes.
1221+
"""
1222+
return dict([
1223+
(six.text_type(key), self.child.to_representation(val))
1224+
for key, val in value.items()
1225+
])
1226+
1227+
11731228
# Miscellaneous field types...
11741229

11751230
class ReadOnlyField(Field):

rest_framework/serializers.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from django.db import models
1515
from django.db.models.fields import FieldDoesNotExist, Field as DjangoField
1616
from django.utils.translation import ugettext_lazy as _
17-
from rest_framework.compat import unicode_to_repr
17+
from rest_framework.compat import postgres_fields, unicode_to_repr
1818
from rest_framework.utils import model_meta
1919
from rest_framework.utils.field_mapping import (
2020
get_url_kwargs, get_field_kwargs,
@@ -1137,6 +1137,12 @@ class Meta:
11371137
if hasattr(models, 'UUIDField'):
11381138
ModelSerializer._field_mapping[models.UUIDField] = UUIDField
11391139

1140+
if postgres_fields:
1141+
class CharMappingField(DictField):
1142+
child = CharField()
1143+
1144+
ModelSerializer._field_mapping[postgres_fields.HStoreField] = CharMappingField
1145+
11401146

11411147
class HyperlinkedModelSerializer(ModelSerializer):
11421148
"""

tests/test_fields.py

+50-1
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,7 @@ class TestValidImageField(FieldValues):
10471047

10481048
class TestListField(FieldValues):
10491049
"""
1050-
Values for `ListField`.
1050+
Values for `ListField` with IntegerField as child.
10511051
"""
10521052
valid_inputs = [
10531053
([1, 2, 3], [1, 2, 3]),
@@ -1064,6 +1064,55 @@ class TestListField(FieldValues):
10641064
field = serializers.ListField(child=serializers.IntegerField())
10651065

10661066

1067+
class TestUnvalidatedListField(FieldValues):
1068+
"""
1069+
Values for `ListField` with no `child` argument.
1070+
"""
1071+
valid_inputs = [
1072+
([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]),
1073+
]
1074+
invalid_inputs = [
1075+
('not a list', ['Expected a list of items but got type `str`']),
1076+
]
1077+
outputs = [
1078+
([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]),
1079+
]
1080+
field = serializers.ListField()
1081+
1082+
1083+
class TestDictField(FieldValues):
1084+
"""
1085+
Values for `ListField` with CharField as child.
1086+
"""
1087+
valid_inputs = [
1088+
({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
1089+
]
1090+
invalid_inputs = [
1091+
({'a': 1, 'b': None}, ['This field may not be null.']),
1092+
('not a dict', ['Expected a dictionary of items but got type `str`']),
1093+
]
1094+
outputs = [
1095+
({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
1096+
]
1097+
field = serializers.DictField(child=serializers.CharField())
1098+
1099+
1100+
class TestUnvalidatedDictField(FieldValues):
1101+
"""
1102+
Values for `ListField` with no `child` argument.
1103+
"""
1104+
valid_inputs = [
1105+
({'a': 1, 'b': [4, 5, 6], 1: 123}, {'a': 1, 'b': [4, 5, 6], '1': 123}),
1106+
]
1107+
invalid_inputs = [
1108+
('not a dict', ['Expected a dictionary of items but got type `str`']),
1109+
]
1110+
outputs = [
1111+
({'a': 1, 'b': [4, 5, 6]}, {'a': 1, 'b': [4, 5, 6]}),
1112+
]
1113+
field = serializers.DictField()
1114+
1115+
10671116
# Tests for FieldField.
10681117
# ---------------------
10691118

0 commit comments

Comments
 (0)