Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 93 additions & 1 deletion rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
parse_date, parse_datetime, parse_duration, parse_time
)
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.ipv6 import clean_ipv6_address
from django.utils.translation import ugettext_lazy as _
Expand Down Expand Up @@ -262,6 +262,7 @@ class SkipField(Exception):
class Field(object):
_creation_counter = 0

type = 'field'
default_error_messages = {
'required': _('This field is required.'),
'null': _('This field may not be null.')
Expand Down Expand Up @@ -597,10 +598,23 @@ def __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...

class BooleanField(Field):
type = 'boolean'
default_error_messages = {
'invalid': _('"{input}" is not a valid boolean.')
}
Expand Down Expand Up @@ -632,6 +646,7 @@ def to_representation(self, value):


class NullBooleanField(Field):
type = 'boolean'
default_error_messages = {
'invalid': _('"{input}" is not a valid boolean.')
}
Expand Down Expand Up @@ -667,6 +682,7 @@ def to_representation(self, value):
# String types...

class CharField(Field):
type = 'string'
default_error_messages = {
'blank': _('This field may not be blank.'),
'max_length': _('Ensure this field has no more than {max_length} characters.'),
Expand Down Expand Up @@ -704,8 +720,17 @@ def to_internal_value(self, data):
def to_representation(self, 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):
type = 'email'
default_error_messages = {
'invalid': _('Enter a valid email address.')
}
Expand All @@ -717,6 +742,7 @@ def __init__(self, **kwargs):


class RegexField(CharField):
type = 'regex'
default_error_messages = {
'invalid': _('This value does not match the required pattern.')
}
Expand All @@ -728,6 +754,7 @@ def __init__(self, regex, **kwargs):


class SlugField(CharField):
type = 'slug'
default_error_messages = {
'invalid': _('Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.')
}
Expand All @@ -740,6 +767,7 @@ def __init__(self, **kwargs):


class URLField(CharField):
type = 'url'
default_error_messages = {
'invalid': _('Enter a valid URL.')
}
Expand Down Expand Up @@ -814,6 +842,7 @@ def to_internal_value(self, data):
# Number types...

class IntegerField(Field):
type = 'integer'
default_error_messages = {
'invalid': _('A valid integer is required.'),
'max_value': _('Ensure this value is less than or equal to {max_value}.'),
Expand Down Expand Up @@ -847,8 +876,17 @@ def to_internal_value(self, data):
def to_representation(self, 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):
type = 'float'
default_error_messages = {
'invalid': _('A valid number is required.'),
'max_value': _('Ensure this value is less than or equal to {max_value}.'),
Expand Down Expand Up @@ -880,8 +918,17 @@ def to_internal_value(self, data):
def to_representation(self, 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):
type = 'decimal'
default_error_messages = {
'invalid': _('A valid number is required.'),
'max_value': _('Ensure this value is less than or equal to {max_value}.'),
Expand Down Expand Up @@ -998,10 +1045,19 @@ def quantize(self, value):
decimal.Decimal('.1') ** self.decimal_places,
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...

class DateTimeField(Field):
type = 'datetime'
default_error_messages = {
'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
'date': _('Expected a datetime but got a date.'),
Expand Down Expand Up @@ -1080,6 +1136,7 @@ def to_representation(self, value):


class DateField(Field):
type = 'date'
default_error_messages = {
'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'),
'datetime': _('Expected a date but got a datetime.'),
Expand Down Expand Up @@ -1149,6 +1206,7 @@ def to_representation(self, value):


class TimeField(Field):
type = 'time'
default_error_messages = {
'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'),
}
Expand Down Expand Up @@ -1232,6 +1290,7 @@ def to_representation(self, value):
# Choice types...

class ChoiceField(Field):
type = 'choice'
default_error_messages = {
'invalid_choice': _('"{input}" is not a valid choice.')
}
Expand Down Expand Up @@ -1279,8 +1338,21 @@ def iter_options(self):
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):
type = 'multiple choice'
default_error_messages = {
'invalid_choice': _('"{input}" is not a valid choice.'),
'not_a_list': _('Expected a list of items but got type "{input_type}".'),
Expand Down Expand Up @@ -1339,6 +1411,7 @@ def __init__(self, path, match=None, recursive=False, allow_files=True,
# File types...

class FileField(Field):
type = 'file upload'
default_error_messages = {
'required': _('No file was submitted.'),
'invalid': _('The submitted data was not a file. Check the encoding type on the form.'),
Expand Down Expand Up @@ -1388,8 +1461,15 @@ def to_representation(self, value):
return url
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):
type = 'image upload'
default_error_messages = {
'invalid_image': _(
'Upload a valid image. The file you uploaded was either not an image or a corrupted image.'
Expand Down Expand Up @@ -1427,6 +1507,7 @@ def to_representation(self, value):


class ListField(Field):
type = 'list'
child = _UnvalidatedField()
initial = []
default_error_messages = {
Expand Down Expand Up @@ -1479,8 +1560,14 @@ def to_representation(self, 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):
type = 'nested object'
child = _UnvalidatedField()
initial = {}
default_error_messages = {
Expand Down Expand Up @@ -1528,6 +1615,11 @@ def to_representation(self, value):
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):
default_error_messages = {
Expand Down
40 changes: 2 additions & 38 deletions rest_framework/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,47 +103,11 @@ def get_serializer_info(self, serializer):
Given an instance of a serializer, return a dictionary of metadata
about its fields.
"""
if hasattr(serializer, 'child'):
# 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()
])
return serializer.get_metadata()

def get_field_info(self, field):
"""
Given an instance of a serializer field, return a dictionary
of metadata about it.
"""
field_info = OrderedDict()
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
return field.get_metadata()
14 changes: 13 additions & 1 deletion rest_framework/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.db.models import Manager
from django.db.models.query import QuerySet
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.translation import ugettext_lazy as _

Expand Down Expand Up @@ -186,6 +186,18 @@ def iter_options(self):
def display_value(self, 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):
"""
Expand Down
22 changes: 22 additions & 0 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ def get_validation_error_detail(exc):

@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
type = 'nested object'
default_error_messages = {
'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
}
Expand Down Expand Up @@ -512,6 +513,17 @@ def errors(self):
ret = super(Serializer, self).errors
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,
# but that's probably better than obfuscating the call hierarchy.
Expand Down Expand Up @@ -685,6 +697,16 @@ def errors(self):
return ReturnDict(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
# --------------------------------------------
Expand Down
Loading