Allowed fields to specify their own metadata.

This commit is contained in:
Simon Charette 2016-03-29 17:31:09 -04:00
parent 79abb8147d
commit 6bbb9165a5
4 changed files with 130 additions and 40 deletions

View File

@ -24,7 +24,7 @@ from django.utils.dateparse import (
parse_date, parse_datetime, parse_duration, parse_time parse_date, parse_datetime, parse_duration, parse_time
) )
from django.utils.duration import duration_string from django.utils.duration import duration_string
from django.utils.encoding import is_protected_type, smart_text from django.utils.encoding import force_text, is_protected_type, smart_text
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.ipv6 import clean_ipv6_address from django.utils.ipv6 import clean_ipv6_address
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -262,6 +262,7 @@ MISSING_ERROR_MESSAGE = (
class Field(object): class Field(object):
_creation_counter = 0 _creation_counter = 0
type = 'field'
default_error_messages = { default_error_messages = {
'required': _('This field is required.'), 'required': _('This field is required.'),
'null': _('This field may not be null.') 'null': _('This field may not be null.')
@ -597,10 +598,23 @@ class Field(object):
""" """
return unicode_to_repr(representation.field_repr(self)) return unicode_to_repr(representation.field_repr(self))
def get_metadata(self):
metadata = OrderedDict([
('type', self.type),
('required', self.required),
('read_only', self.read_only),
])
for attr in ['label', 'help_text']:
value = getattr(self, attr)
if value is not None and value != '':
metadata[attr] = force_text(value, strings_only=True)
return metadata
# Boolean types... # Boolean types...
class BooleanField(Field): class BooleanField(Field):
type = 'boolean'
default_error_messages = { default_error_messages = {
'invalid': _('"{input}" is not a valid boolean.') 'invalid': _('"{input}" is not a valid boolean.')
} }
@ -632,6 +646,7 @@ class BooleanField(Field):
class NullBooleanField(Field): class NullBooleanField(Field):
type = 'boolean'
default_error_messages = { default_error_messages = {
'invalid': _('"{input}" is not a valid boolean.') 'invalid': _('"{input}" is not a valid boolean.')
} }
@ -667,6 +682,7 @@ class NullBooleanField(Field):
# String types... # String types...
class CharField(Field): class CharField(Field):
type = 'string'
default_error_messages = { default_error_messages = {
'blank': _('This field may not be blank.'), 'blank': _('This field may not be blank.'),
'max_length': _('Ensure this field has no more than {max_length} characters.'), 'max_length': _('Ensure this field has no more than {max_length} characters.'),
@ -704,8 +720,17 @@ class CharField(Field):
def to_representation(self, value): def to_representation(self, value):
return six.text_type(value) return six.text_type(value)
def get_metadata(self):
metadata = super(CharField, self).get_metadata()
for attr in ['min_length', 'max_length']:
value = getattr(self, attr)
if value is not None:
metadata[attr] = value
return metadata
class EmailField(CharField): class EmailField(CharField):
type = 'email'
default_error_messages = { default_error_messages = {
'invalid': _('Enter a valid email address.') 'invalid': _('Enter a valid email address.')
} }
@ -717,6 +742,7 @@ class EmailField(CharField):
class RegexField(CharField): class RegexField(CharField):
type = 'regex'
default_error_messages = { default_error_messages = {
'invalid': _('This value does not match the required pattern.') 'invalid': _('This value does not match the required pattern.')
} }
@ -728,6 +754,7 @@ class RegexField(CharField):
class SlugField(CharField): class SlugField(CharField):
type = 'slug'
default_error_messages = { default_error_messages = {
'invalid': _('Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.') 'invalid': _('Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.')
} }
@ -740,6 +767,7 @@ class SlugField(CharField):
class URLField(CharField): class URLField(CharField):
type = 'url'
default_error_messages = { default_error_messages = {
'invalid': _('Enter a valid URL.') 'invalid': _('Enter a valid URL.')
} }
@ -814,6 +842,7 @@ class IPAddressField(CharField):
# Number types... # Number types...
class IntegerField(Field): class IntegerField(Field):
type = 'integer'
default_error_messages = { default_error_messages = {
'invalid': _('A valid integer is required.'), 'invalid': _('A valid integer is required.'),
'max_value': _('Ensure this value is less than or equal to {max_value}.'), 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
@ -847,8 +876,17 @@ class IntegerField(Field):
def to_representation(self, value): def to_representation(self, value):
return int(value) return int(value)
def get_metadata(self):
metadata = super(IntegerField, self).get_metadata()
for attr in ['max_value', 'min_value']:
value = getattr(self, attr)
if value is not None:
metadata[attr] = value
return metadata
class FloatField(Field): class FloatField(Field):
type = 'float'
default_error_messages = { default_error_messages = {
'invalid': _('A valid number is required.'), 'invalid': _('A valid number is required.'),
'max_value': _('Ensure this value is less than or equal to {max_value}.'), 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
@ -880,8 +918,17 @@ class FloatField(Field):
def to_representation(self, value): def to_representation(self, value):
return float(value) return float(value)
def get_metadata(self):
metadata = super(FloatField, self).get_metadata()
for attr in ['max_value', 'min_value']:
value = getattr(self, attr)
if value is not None:
metadata[attr] = value
return metadata
class DecimalField(Field): class DecimalField(Field):
type = 'decimal'
default_error_messages = { default_error_messages = {
'invalid': _('A valid number is required.'), 'invalid': _('A valid number is required.'),
'max_value': _('Ensure this value is less than or equal to {max_value}.'), 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
@ -998,10 +1045,19 @@ class DecimalField(Field):
decimal.Decimal('.1') ** self.decimal_places, decimal.Decimal('.1') ** self.decimal_places,
context=context) context=context)
def get_metadata(self):
metadata = super(DecimalField, self).get_metadata()
for attr in ['max_value', 'min_value']:
value = getattr(self, attr)
if value is not None:
metadata[attr] = value
return metadata
# Date & time fields... # Date & time fields...
class DateTimeField(Field): class DateTimeField(Field):
type = 'datetime'
default_error_messages = { default_error_messages = {
'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'), 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
'date': _('Expected a datetime but got a date.'), 'date': _('Expected a datetime but got a date.'),
@ -1080,6 +1136,7 @@ class DateTimeField(Field):
class DateField(Field): class DateField(Field):
type = 'date'
default_error_messages = { default_error_messages = {
'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'), 'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'),
'datetime': _('Expected a date but got a datetime.'), 'datetime': _('Expected a date but got a datetime.'),
@ -1149,6 +1206,7 @@ class DateField(Field):
class TimeField(Field): class TimeField(Field):
type = 'time'
default_error_messages = { default_error_messages = {
'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'), 'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'),
} }
@ -1232,6 +1290,7 @@ class DurationField(Field):
# Choice types... # Choice types...
class ChoiceField(Field): class ChoiceField(Field):
type = 'choice'
default_error_messages = { default_error_messages = {
'invalid_choice': _('"{input}" is not a valid choice.') 'invalid_choice': _('"{input}" is not a valid choice.')
} }
@ -1279,8 +1338,21 @@ class ChoiceField(Field):
cutoff_text=self.html_cutoff_text cutoff_text=self.html_cutoff_text
) )
def get_metadata(self):
metadata = super(ChoiceField, self).get_metadata()
if not self.read_only:
metadata['choices'] = [
{
'value': choice_value,
'display_name': force_text(choice_name, strings_only=True)
}
for choice_value, choice_name in self.choices.items()
]
return metadata
class MultipleChoiceField(ChoiceField): class MultipleChoiceField(ChoiceField):
type = 'multiple choice'
default_error_messages = { default_error_messages = {
'invalid_choice': _('"{input}" is not a valid choice.'), 'invalid_choice': _('"{input}" is not a valid choice.'),
'not_a_list': _('Expected a list of items but got type "{input_type}".'), 'not_a_list': _('Expected a list of items but got type "{input_type}".'),
@ -1339,6 +1411,7 @@ class FilePathField(ChoiceField):
# File types... # File types...
class FileField(Field): class FileField(Field):
type = 'file upload'
default_error_messages = { default_error_messages = {
'required': _('No file was submitted.'), 'required': _('No file was submitted.'),
'invalid': _('The submitted data was not a file. Check the encoding type on the form.'), 'invalid': _('The submitted data was not a file. Check the encoding type on the form.'),
@ -1388,8 +1461,15 @@ class FileField(Field):
return url return url
return value.name return value.name
def get_metadata(self):
metadata = super(FileField, self).get_metadata()
if self.max_length is not None:
metadata['max_length'] = self.max_length
return metadata
class ImageField(FileField): class ImageField(FileField):
type = 'image upload'
default_error_messages = { default_error_messages = {
'invalid_image': _( 'invalid_image': _(
'Upload a valid image. The file you uploaded was either not an image or a corrupted image.' 'Upload a valid image. The file you uploaded was either not an image or a corrupted image.'
@ -1427,6 +1507,7 @@ class _UnvalidatedField(Field):
class ListField(Field): class ListField(Field):
type = 'list'
child = _UnvalidatedField() child = _UnvalidatedField()
initial = [] initial = []
default_error_messages = { default_error_messages = {
@ -1479,8 +1560,14 @@ class ListField(Field):
""" """
return [self.child.to_representation(item) for item in data] return [self.child.to_representation(item) for item in data]
def get_metadata(self):
metadata = super(ListField, self).get_metadata()
metadata['child'] = self.child.get_metadata()
return metadata
class DictField(Field): class DictField(Field):
type = 'nested object'
child = _UnvalidatedField() child = _UnvalidatedField()
initial = {} initial = {}
default_error_messages = { default_error_messages = {
@ -1528,6 +1615,11 @@ class DictField(Field):
for key, val in value.items() for key, val in value.items()
} }
def get_metadata(self):
metadata = super(DictField, self).get_metadata()
metadata['child'] = self.child.get_metadata()
return metadata
class JSONField(Field): class JSONField(Field):
default_error_messages = { default_error_messages = {

View File

@ -103,47 +103,11 @@ class SimpleMetadata(BaseMetadata):
Given an instance of a serializer, return a dictionary of metadata Given an instance of a serializer, return a dictionary of metadata
about its fields. about its fields.
""" """
if hasattr(serializer, 'child'): return serializer.get_metadata()
# If this is a `ListSerializer` then we want to examine the
# underlying child serializer instance instead.
serializer = serializer.child
return OrderedDict([
(field_name, self.get_field_info(field))
for field_name, field in serializer.fields.items()
])
def get_field_info(self, field): def get_field_info(self, field):
""" """
Given an instance of a serializer field, return a dictionary Given an instance of a serializer field, return a dictionary
of metadata about it. of metadata about it.
""" """
field_info = OrderedDict() return field.get_metadata()
field_info['type'] = self.label_lookup[field]
field_info['required'] = getattr(field, 'required', False)
attrs = [
'read_only', 'label', 'help_text',
'min_length', 'max_length',
'min_value', 'max_value'
]
for attr in attrs:
value = getattr(field, attr, None)
if value is not None and value != '':
field_info[attr] = force_text(value, strings_only=True)
if getattr(field, 'child', None):
field_info['child'] = self.get_field_info(field.child)
elif getattr(field, 'fields', None):
field_info['children'] = self.get_serializer_info(field)
if not field_info.get('read_only') and hasattr(field, 'choices'):
field_info['choices'] = [
{
'value': choice_value,
'display_name': force_text(choice_name, strings_only=True)
}
for choice_value, choice_name in field.choices.items()
]
return field_info

View File

@ -10,7 +10,7 @@ from django.core.urlresolvers import (
from django.db.models import Manager from django.db.models import Manager
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.utils import six from django.utils import six
from django.utils.encoding import smart_text from django.utils.encoding import force_text, smart_text
from django.utils.six.moves.urllib import parse as urlparse from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -186,6 +186,18 @@ class RelatedField(Field):
def display_value(self, instance): def display_value(self, instance):
return six.text_type(instance) return six.text_type(instance)
def get_metadata(self):
metadata = super(RelatedField, self).get_metadata()
if not self.read_only:
metadata['choices'] = [
{
'value': choice_value,
'display_name': force_text(choice_name, strings_only=True)
}
for choice_value, choice_name in self.choices.items()
]
return metadata
class StringRelatedField(RelatedField): class StringRelatedField(RelatedField):
""" """

View File

@ -322,6 +322,7 @@ def get_validation_error_detail(exc):
@six.add_metaclass(SerializerMetaclass) @six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer): class Serializer(BaseSerializer):
type = 'nested object'
default_error_messages = { default_error_messages = {
'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.') 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
} }
@ -512,6 +513,17 @@ class Serializer(BaseSerializer):
ret = super(Serializer, self).errors ret = super(Serializer, self).errors
return ReturnDict(ret, serializer=self) return ReturnDict(ret, serializer=self)
def get_metadata(self):
fields_metadata = OrderedDict([
(field_name, field.get_metadata())
for field_name, field in self.fields.items()
])
if self.root is self:
return fields_metadata
metadata = super(Serializer, self).get_metadata()
metadata['children'] = fields_metadata
return metadata
# There's some replication of `ListField` here, # There's some replication of `ListField` here,
# but that's probably better than obfuscating the call hierarchy. # but that's probably better than obfuscating the call hierarchy.
@ -685,6 +697,16 @@ class ListSerializer(BaseSerializer):
return ReturnDict(ret, serializer=self) return ReturnDict(ret, serializer=self)
return ReturnList(ret, serializer=self) return ReturnList(ret, serializer=self)
def get_metadata(self):
if self.root is self:
return OrderedDict([
(field_name, field.get_metadata())
for field_name, field in self.child.fields.items()
])
metadata = super(ListSerializer, self).get_metadata()
metadata['child'] = self.child.get_metadata()
return metadata
# ModelSerializer & HyperlinkedModelSerializer # ModelSerializer & HyperlinkedModelSerializer
# -------------------------------------------- # --------------------------------------------