aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2015-01-23 16:27:23 +0000
committerTom Christie2015-01-23 16:27:23 +0000
commit35f6a8246299d31ecce4f791f9527bf34cebe6e2 (patch)
treee68af328d0e280d188e5de10b2feb1cf522fd961
parent889a07f5563a0f970639a0958c0dcbc26e82919f (diff)
downloaddjango-rest-framework-35f6a8246299d31ecce4f791f9527bf34cebe6e2.tar.bz2
Added DictField and support for HStoreField.
-rw-r--r--docs/api-guide/fields.md19
-rw-r--r--rest_framework/compat.py7
-rw-r--r--rest_framework/fields.py59
-rw-r--r--rest_framework/serializers.py8
-rw-r--r--tests/test_fields.py51
5 files changed, 139 insertions, 5 deletions
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 64ec902b..1c78a42b 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -380,7 +380,7 @@ A field class that validates a list of objects.
**Signature**: `ListField(child)`
-- `child` - A field instance that should be used for validating the objects in the list.
+- `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.
For example, to validate a list of integers you might use something like the following:
@@ -395,6 +395,23 @@ The `ListField` class also supports a declarative style that allows you to write
We can now reuse our custom `StringListField` class throughout our application, without having to provide a `child` argument to it.
+## DictField
+
+A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values.
+
+**Signature**: `DictField(child)`
+
+- `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.
+
+For example, to create a field that validates a mapping of strings to strings, you would write something like this:
+
+ document = DictField(child=CharField())
+
+You can also use the declarative style, as with `ListField`. For example:
+
+ class DocumentField(DictField):
+ child = CharField()
+
---
# Miscellaneous fields
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 766afaec..36413394 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -58,6 +58,13 @@ except ImportError:
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
index 5e3f7ce4..71a9f193 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1132,8 +1132,21 @@ class ImageField(FileField):
# 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 @@ class ListField(Field):
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
index dca612ca..42d1e370 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -14,7 +14,7 @@ from __future__ import unicode_literals
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 ModelSerializer(Serializer):
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):
"""
diff --git a/tests/test_fields.py b/tests/test_fields.py
index a46cc205..6744cf64 100644
--- 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.
# ---------------------