Skip to content

Commit 843ae60

Browse files
committed
Merge branch 'issue-192-expose-fields-for-options' of git://github.com/grimborg/django-rest-framework into issue-192-expose-fields-for-options
Conflicts: rest_framework/tests/fields.py
2 parents 7f1cc82 + fecadac commit 843ae60

File tree

3 files changed

+169
-2
lines changed

3 files changed

+169
-2
lines changed

rest_framework/fields.py

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,44 @@
2323
from django.utils.datastructures import SortedDict
2424

2525
from rest_framework import ISO_8601
26-
from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time
26+
from rest_framework.compat import (timezone, parse_date, parse_datetime,
27+
parse_time)
2728
from rest_framework.compat import BytesIO
2829
from rest_framework.compat import six
2930
from rest_framework.compat import smart_text
3031
from rest_framework.settings import api_settings
3132

3233

34+
HUMANIZED_FIELD_TYPES = {
35+
'BooleanField': u'Boolean',
36+
'CharField': u'Single Character',
37+
'ChoiceField': u'Single Choice',
38+
'ComboField': u'Single Choice',
39+
'DateField': u'Date',
40+
'DateTimeField': u'Date and Time',
41+
'DecimalField': u'Decimal',
42+
'EmailField': u'Email',
43+
'Field': u'Field',
44+
'FileField': u'File',
45+
'FilePathField': u'File Path',
46+
'FloatField': u'Float',
47+
'GenericIPAddressField': u'Generic IP Address',
48+
'IPAddressField': u'IP Address',
49+
'ImageField': u'Image',
50+
'IntegerField': u'Integer',
51+
'MultiValueField': u'Multiple Value',
52+
'MultipleChoiceField': u'Multiple Choice',
53+
'NullBooleanField': u'Nullable Boolean',
54+
'RegexField': u'Regular Expression',
55+
'SlugField': u'Slug',
56+
'SplitDateTimeField': u'Split Date and Time',
57+
'TimeField': u'Time',
58+
'TypedChoiceField': u'Typed Single Choice',
59+
'TypedMultipleChoiceField': u'Typed Multiple Choice',
60+
'URLField': u'URL',
61+
}
62+
63+
3364
def is_simple_callable(obj):
3465
"""
3566
True if the object is a callable that takes no arguments.
@@ -62,7 +93,8 @@ def get_component(obj, attr_name):
6293

6394

6495
def readable_datetime_formats(formats):
65-
format = ', '.join(formats).replace(ISO_8601, 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
96+
format = ', '.join(formats).replace(ISO_8601,
97+
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
6698
return humanize_strptime(format)
6799

68100

@@ -71,6 +103,61 @@ def readable_date_formats(formats):
71103
return humanize_strptime(format)
72104

73105

106+
def humanize_field_type(field_type):
107+
"""Return a human-readable name for a field type.
108+
109+
:param field_type: Either a field type class (for example
110+
django.forms.fields.DateTimeField), or the name of a field type
111+
(for example "DateTimeField").
112+
113+
:return: unicode
114+
115+
"""
116+
if isinstance(field_type, basestring):
117+
field_type_name = field_type
118+
else:
119+
field_type_name = field_type.__name__
120+
try:
121+
return HUMANIZED_FIELD_TYPES[field_type_name]
122+
except KeyError:
123+
humanized = re.sub('([a-z0-9])([A-Z])', r'\1 \2', field_type_name)
124+
return humanized.capitalize()
125+
126+
127+
def humanize_field(field):
128+
"""Return a human-readable description of a field.
129+
130+
:param field: A Django field.
131+
132+
:return: A dictionary of the form {type: type name, required: bool,
133+
label: field label: read_only: bool,
134+
help_text: optional help text}
135+
136+
"""
137+
humanized = {
138+
'type': humanize_field_type(field.__class__),
139+
'required': getattr(field, 'required', False),
140+
'label': field.label,
141+
}
142+
optional_attrs = ['read_only', 'help_text']
143+
for attr in optional_attrs:
144+
if hasattr(field, attr):
145+
humanized[attr] = getattr(field, attr)
146+
return humanized
147+
148+
149+
def humanize_form_fields(form):
150+
"""Return a humanized description of all the fields in a form.
151+
152+
:param form: A Django form.
153+
:return: A dictionary of {field_label: humanized description}
154+
155+
"""
156+
fields = SortedDict([(name, humanize_field(field))
157+
for name, field in form.fields.iteritems()])
158+
return fields
159+
160+
74161
def readable_time_formats(formats):
75162
format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
76163
return humanize_strptime(format)

rest_framework/tests/fields.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
from __future__ import unicode_literals
55
from django.utils.datastructures import SortedDict
66
import datetime
7+
from rest_framework.fields import (humanize_field, humanize_field_type,
8+
humanize_form_fields)
9+
from django import forms
710
from decimal import Decimal
811
from django.db import models
912
from django.test import TestCase
1013
from django.core import validators
1114
from rest_framework import serializers
1215
from rest_framework.serializers import Serializer
1316
from rest_framework.tests.models import RESTFrameworkModel
17+
from rest_framework.fields import Field
18+
from collections import namedtuple
19+
from uuid import uuid4
1420

1521

1622
class TimestampedModel(models.Model):
@@ -809,3 +815,75 @@ class Meta:
809815
serializer = URLFieldSerializer(data={})
810816
self.assertEqual(serializer.is_valid(), True)
811817
self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20)
818+
819+
820+
class HumanizedFieldType(TestCase):
821+
def test_standard_type_classes(self):
822+
for field_type_name in forms.fields.__all__:
823+
field_type = getattr(forms.fields, field_type_name)
824+
humanized = humanize_field_type(field_type)
825+
self.assert_valid_name(humanized)
826+
827+
def test_standard_type_names(self):
828+
for field_type_name in forms.fields.__all__:
829+
humanized = humanize_field_type(field_type_name)
830+
self.assert_valid_name(humanized)
831+
832+
def test_custom_type_name(self):
833+
humanized = humanize_field_type('SomeCustomType')
834+
self.assertEquals(humanized, u'Some custom type')
835+
836+
def test_custom_type(self):
837+
custom_type = namedtuple('SomeCustomType', [])
838+
humanized = humanize_field_type(custom_type)
839+
self.assertEquals(humanized, u'Some custom type')
840+
841+
def assert_valid_name(self, humanized):
842+
"""A humanized field name is valid if it's a non-empty
843+
unicode.
844+
845+
"""
846+
self.assertIsInstance(humanized, unicode)
847+
self.assertTrue(humanized)
848+
849+
850+
class HumanizedField(TestCase):
851+
def setUp(self):
852+
self.required_field = Field()
853+
self.required_field.label = uuid4().hex
854+
self.required_field.required = True
855+
856+
self.optional_field = Field()
857+
self.optional_field.label = uuid4().hex
858+
self.optional_field.required = False
859+
860+
def test_required(self):
861+
self.assertEqual(humanize_field(self.required_field)['required'], True)
862+
863+
def test_optional(self):
864+
self.assertEqual(humanize_field(self.optional_field)['required'],
865+
False)
866+
867+
def test_label(self):
868+
for field in (self.required_field, self.optional_field):
869+
self.assertEqual(humanize_field(field)['label'], field.label)
870+
871+
872+
class Form(forms.Form):
873+
field1 = forms.CharField(max_length=3, label='field one')
874+
field2 = forms.CharField(label='field two')
875+
876+
877+
class HumanizedSerializer(TestCase):
878+
def setUp(self):
879+
self.serializer = TimestampedModelSerializer()
880+
881+
def test_humanized(self):
882+
humanized = humanize_form_fields(Form())
883+
self.assertEqual(humanized, {
884+
'field1': {
885+
u'help_text': u'', u'required': True,
886+
u'type': u'Single Character', u'label': 'field one'},
887+
'field2': {
888+
u'help_text': u'', u'required': True,
889+
u'type': u'Single Character', u'label': 'field two'}})

rest_framework/views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def _generate_action_metadata(self, request):
8484
if serializer is not None:
8585
field_name_types = {}
8686
for name, field in serializer.fields.iteritems():
87+
from rest_framework.fields import humanize_field
88+
humanize_field(field)
8789
field_name_types[name] = field.__class__.__name__
8890

8991
actions[method] = field_name_types

0 commit comments

Comments
 (0)