Skip to content

Commit 2709de1

Browse files
Ryan P Kilbycarltongibson
Ryan P Kilby
authored andcommitted
Add HStoreField, postgres fields tests (#5654)
* Test postgres field mapping * Add HStoreField * Ensure 'HStoreField' child is a 'CharField' * Add HStoreField docs
1 parent d3f3c3d commit 2709de1

File tree

6 files changed

+118
-8
lines changed

6 files changed

+118
-8
lines changed

docs/api-guide/fields.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,16 @@ You can also use the declarative style, as with `ListField`. For example:
473473
class DocumentField(DictField):
474474
child = CharField()
475475

476+
## HStoreField
477+
478+
A preconfigured `DictField` that is compatible with Django's postgres `HStoreField`.
479+
480+
**Signature**: `HStoreField(child=<A_FIELD_INSTANCE>)`
481+
482+
- `child` - A field instance that is used for validating the values in the dictionary. The default child field accepts both empty strings and null values.
483+
484+
Note that the child field **must** be an instance of `CharField`, as the hstore extension stores values as strings.
485+
476486
## JSONField
477487

478488
A field class that validates that the incoming data structure consists of valid JSON primitives. In its alternate binary mode, it will represent and validate JSON-encoded binary strings.

requirements/requirements-optionals.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Optional packages which may be used with REST framework.
22
pytz==2017.2
3+
psycopg2==2.7.3
34
markdown==2.6.4
45
django-guardian==1.4.9
56
django-filter==1.1.0

rest_framework/fields.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,6 +1711,17 @@ def run_child_validation(self, data):
17111711
raise ValidationError(errors)
17121712

17131713

1714+
class HStoreField(DictField):
1715+
child = CharField(allow_blank=True, allow_null=True)
1716+
1717+
def __init__(self, *args, **kwargs):
1718+
super(HStoreField, self).__init__(*args, **kwargs)
1719+
assert isinstance(self.child, CharField), (
1720+
"The `child` argument must be an instance of `CharField`, "
1721+
"as the hstore extension stores values as strings."
1722+
)
1723+
1724+
17141725
class JSONField(Field):
17151726
default_error_messages = {
17161727
'invalid': _('Value must be valid JSON.')

rest_framework/serializers.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
from rest_framework.fields import ( # NOQA # isort:skip
5555
BooleanField, CharField, ChoiceField, DateField, DateTimeField, DecimalField,
5656
DictField, DurationField, EmailField, Field, FileField, FilePathField, FloatField,
57-
HiddenField, IPAddressField, ImageField, IntegerField, JSONField, ListField,
58-
ModelField, MultipleChoiceField, NullBooleanField, ReadOnlyField, RegexField,
59-
SerializerMethodField, SlugField, TimeField, URLField, UUIDField,
57+
HiddenField, HStoreField, IPAddressField, ImageField, IntegerField, JSONField,
58+
ListField, ModelField, MultipleChoiceField, NullBooleanField, ReadOnlyField,
59+
RegexField, SerializerMethodField, SlugField, TimeField, URLField, UUIDField,
6060
)
6161
from rest_framework.relations import ( # NOQA # isort:skip
6262
HyperlinkedIdentityField, HyperlinkedRelatedField, ManyRelatedField,
@@ -1541,10 +1541,7 @@ def get_unique_for_date_validators(self):
15411541
ModelSerializer.serializer_field_mapping[models.IPAddressField] = IPAddressField
15421542

15431543
if postgres_fields:
1544-
class CharMappingField(DictField):
1545-
child = CharField(allow_blank=True)
1546-
1547-
ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField
1544+
ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = HStoreField
15481545
ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField
15491546
ModelSerializer.serializer_field_mapping[postgres_fields.JSONField] = JSONField
15501547

tests/test_fields.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,6 +1933,49 @@ class TestUnvalidatedDictField(FieldValues):
19331933
field = serializers.DictField()
19341934

19351935

1936+
class TestHStoreField(FieldValues):
1937+
"""
1938+
Values for `ListField` with CharField as child.
1939+
"""
1940+
valid_inputs = [
1941+
({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
1942+
({'a': 1, 'b': None}, {'a': '1', 'b': None}),
1943+
]
1944+
invalid_inputs = [
1945+
('not a dict', ['Expected a dictionary of items but got type "str".']),
1946+
]
1947+
outputs = [
1948+
({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
1949+
]
1950+
field = serializers.HStoreField()
1951+
1952+
def test_child_is_charfield(self):
1953+
with pytest.raises(AssertionError) as exc_info:
1954+
serializers.HStoreField(child=serializers.IntegerField())
1955+
1956+
assert str(exc_info.value) == (
1957+
"The `child` argument must be an instance of `CharField`, "
1958+
"as the hstore extension stores values as strings."
1959+
)
1960+
1961+
def test_no_source_on_child(self):
1962+
with pytest.raises(AssertionError) as exc_info:
1963+
serializers.HStoreField(child=serializers.CharField(source='other'))
1964+
1965+
assert str(exc_info.value) == (
1966+
"The `source` argument is not meaningful when applied to a `child=` field. "
1967+
"Remove `source=` from the field declaration."
1968+
)
1969+
1970+
def test_allow_null(self):
1971+
"""
1972+
If `allow_null=True` then `None` is a valid input.
1973+
"""
1974+
field = serializers.HStoreField(allow_null=True)
1975+
output = field.run_validation(None)
1976+
assert output is None
1977+
1978+
19361979
class TestJSONField(FieldValues):
19371980
"""
19381981
Values for `JSONField`.

tests/test_model_serializer.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from django.utils import six
2222

2323
from rest_framework import serializers
24-
from rest_framework.compat import unicode_repr
24+
from rest_framework.compat import postgres_fields, unicode_repr
2525

2626

2727
def dedent(blocktext):
@@ -379,6 +379,54 @@ class Meta:
379379
'{0}'.format(s.errors))
380380

381381

382+
@pytest.mark.skipUnless(postgres_fields, 'postgres is required')
383+
class TestPosgresFieldsMapping(TestCase):
384+
def test_hstore_field(self):
385+
class HStoreFieldModel(models.Model):
386+
hstore_field = postgres_fields.HStoreField()
387+
388+
class TestSerializer(serializers.ModelSerializer):
389+
class Meta:
390+
model = HStoreFieldModel
391+
fields = ['hstore_field']
392+
393+
expected = dedent("""
394+
TestSerializer():
395+
hstore_field = HStoreField()
396+
""")
397+
self.assertEqual(unicode_repr(TestSerializer()), expected)
398+
399+
def test_array_field(self):
400+
class ArrayFieldModel(models.Model):
401+
array_field = postgres_fields.ArrayField(base_field=models.CharField())
402+
403+
class TestSerializer(serializers.ModelSerializer):
404+
class Meta:
405+
model = ArrayFieldModel
406+
fields = ['array_field']
407+
408+
expected = dedent("""
409+
TestSerializer():
410+
array_field = ListField(child=CharField(label='Array field', validators=[<django.core.validators.MaxLengthValidator object>]))
411+
""")
412+
self.assertEqual(unicode_repr(TestSerializer()), expected)
413+
414+
def test_json_field(self):
415+
class JSONFieldModel(models.Model):
416+
json_field = postgres_fields.JSONField()
417+
418+
class TestSerializer(serializers.ModelSerializer):
419+
class Meta:
420+
model = JSONFieldModel
421+
fields = ['json_field']
422+
423+
expected = dedent("""
424+
TestSerializer():
425+
json_field = JSONField(style={'base_template': 'textarea.html'})
426+
""")
427+
self.assertEqual(unicode_repr(TestSerializer()), expected)
428+
429+
382430
# Tests for relational field mappings.
383431
# ------------------------------------
384432

0 commit comments

Comments
 (0)