Add HStoreField, postgres fields tests (#5654)

* Test postgres field mapping

* Add HStoreField

* Ensure 'HStoreField' child is a 'CharField'

* Add HStoreField docs
This commit is contained in:
Ryan P Kilby 2018-01-15 09:52:30 -05:00 committed by Carlton Gibson
parent d3f3c3d9c1
commit 2709de1310
6 changed files with 118 additions and 8 deletions

View File

@ -473,6 +473,16 @@ You can also use the declarative style, as with `ListField`. For example:
class DocumentField(DictField): class DocumentField(DictField):
child = CharField() child = CharField()
## HStoreField
A preconfigured `DictField` that is compatible with Django's postgres `HStoreField`.
**Signature**: `HStoreField(child=<A_FIELD_INSTANCE>)`
- `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.
Note that the child field **must** be an instance of `CharField`, as the hstore extension stores values as strings.
## JSONField ## JSONField
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. 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.

View File

@ -1,5 +1,6 @@
# Optional packages which may be used with REST framework. # Optional packages which may be used with REST framework.
pytz==2017.2 pytz==2017.2
psycopg2==2.7.3
markdown==2.6.4 markdown==2.6.4
django-guardian==1.4.9 django-guardian==1.4.9
django-filter==1.1.0 django-filter==1.1.0

View File

@ -1711,6 +1711,17 @@ class DictField(Field):
raise ValidationError(errors) raise ValidationError(errors)
class HStoreField(DictField):
child = CharField(allow_blank=True, allow_null=True)
def __init__(self, *args, **kwargs):
super(HStoreField, self).__init__(*args, **kwargs)
assert isinstance(self.child, CharField), (
"The `child` argument must be an instance of `CharField`, "
"as the hstore extension stores values as strings."
)
class JSONField(Field): class JSONField(Field):
default_error_messages = { default_error_messages = {
'invalid': _('Value must be valid JSON.') 'invalid': _('Value must be valid JSON.')

View File

@ -54,9 +54,9 @@ from rest_framework.validators import (
from rest_framework.fields import ( # NOQA # isort:skip from rest_framework.fields import ( # NOQA # isort:skip
BooleanField, CharField, ChoiceField, DateField, DateTimeField, DecimalField, BooleanField, CharField, ChoiceField, DateField, DateTimeField, DecimalField,
DictField, DurationField, EmailField, Field, FileField, FilePathField, FloatField, DictField, DurationField, EmailField, Field, FileField, FilePathField, FloatField,
HiddenField, IPAddressField, ImageField, IntegerField, JSONField, ListField, HiddenField, HStoreField, IPAddressField, ImageField, IntegerField, JSONField,
ModelField, MultipleChoiceField, NullBooleanField, ReadOnlyField, RegexField, ListField, ModelField, MultipleChoiceField, NullBooleanField, ReadOnlyField,
SerializerMethodField, SlugField, TimeField, URLField, UUIDField, RegexField, SerializerMethodField, SlugField, TimeField, URLField, UUIDField,
) )
from rest_framework.relations import ( # NOQA # isort:skip from rest_framework.relations import ( # NOQA # isort:skip
HyperlinkedIdentityField, HyperlinkedRelatedField, ManyRelatedField, HyperlinkedIdentityField, HyperlinkedRelatedField, ManyRelatedField,
@ -1541,10 +1541,7 @@ if hasattr(models, 'IPAddressField'):
ModelSerializer.serializer_field_mapping[models.IPAddressField] = IPAddressField ModelSerializer.serializer_field_mapping[models.IPAddressField] = IPAddressField
if postgres_fields: if postgres_fields:
class CharMappingField(DictField): ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = HStoreField
child = CharField(allow_blank=True)
ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField
ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField
ModelSerializer.serializer_field_mapping[postgres_fields.JSONField] = JSONField ModelSerializer.serializer_field_mapping[postgres_fields.JSONField] = JSONField

View File

@ -1933,6 +1933,49 @@ class TestUnvalidatedDictField(FieldValues):
field = serializers.DictField() field = serializers.DictField()
class TestHStoreField(FieldValues):
"""
Values for `ListField` with CharField as child.
"""
valid_inputs = [
({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
({'a': 1, 'b': None}, {'a': '1', 'b': None}),
]
invalid_inputs = [
('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.HStoreField()
def test_child_is_charfield(self):
with pytest.raises(AssertionError) as exc_info:
serializers.HStoreField(child=serializers.IntegerField())
assert str(exc_info.value) == (
"The `child` argument must be an instance of `CharField`, "
"as the hstore extension stores values as strings."
)
def test_no_source_on_child(self):
with pytest.raises(AssertionError) as exc_info:
serializers.HStoreField(child=serializers.CharField(source='other'))
assert str(exc_info.value) == (
"The `source` argument is not meaningful when applied to a `child=` field. "
"Remove `source=` from the field declaration."
)
def test_allow_null(self):
"""
If `allow_null=True` then `None` is a valid input.
"""
field = serializers.HStoreField(allow_null=True)
output = field.run_validation(None)
assert output is None
class TestJSONField(FieldValues): class TestJSONField(FieldValues):
""" """
Values for `JSONField`. Values for `JSONField`.

View File

@ -21,7 +21,7 @@ from django.test import TestCase
from django.utils import six from django.utils import six
from rest_framework import serializers from rest_framework import serializers
from rest_framework.compat import unicode_repr from rest_framework.compat import postgres_fields, unicode_repr
def dedent(blocktext): def dedent(blocktext):
@ -379,6 +379,54 @@ class TestGenericIPAddressFieldValidation(TestCase):
'{0}'.format(s.errors)) '{0}'.format(s.errors))
@pytest.mark.skipUnless(postgres_fields, 'postgres is required')
class TestPosgresFieldsMapping(TestCase):
def test_hstore_field(self):
class HStoreFieldModel(models.Model):
hstore_field = postgres_fields.HStoreField()
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = HStoreFieldModel
fields = ['hstore_field']
expected = dedent("""
TestSerializer():
hstore_field = HStoreField()
""")
self.assertEqual(unicode_repr(TestSerializer()), expected)
def test_array_field(self):
class ArrayFieldModel(models.Model):
array_field = postgres_fields.ArrayField(base_field=models.CharField())
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = ArrayFieldModel
fields = ['array_field']
expected = dedent("""
TestSerializer():
array_field = ListField(child=CharField(label='Array field', validators=[<django.core.validators.MaxLengthValidator object>]))
""")
self.assertEqual(unicode_repr(TestSerializer()), expected)
def test_json_field(self):
class JSONFieldModel(models.Model):
json_field = postgres_fields.JSONField()
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = JSONFieldModel
fields = ['json_field']
expected = dedent("""
TestSerializer():
json_field = JSONField(style={'base_template': 'textarea.html'})
""")
self.assertEqual(unicode_repr(TestSerializer()), expected)
# Tests for relational field mappings. # Tests for relational field mappings.
# ------------------------------------ # ------------------------------------