aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/api-guide/fields.md34
-rw-r--r--rest_framework/fields.py83
-rw-r--r--rest_framework/renderers.py24
-rw-r--r--rest_framework/serializers.py23
-rw-r--r--rest_framework/tests/models.py11
-rw-r--r--rest_framework/tests/serializer.py29
6 files changed, 175 insertions, 29 deletions
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index b3cf186c..189ed76f 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -73,34 +73,52 @@ These fields represent basic datatypes, and support both reading and writing val
## BooleanField
-A Boolean representation, corresponds to `django.db.models.fields.BooleanField`.
+A Boolean representation.
+
+Corresponds to `django.db.models.fields.BooleanField`.
## CharField
-A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`, corresponds to `django.db.models.fields.CharField`
+A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`.
+
+Corresponds to `django.db.models.fields.CharField`
or `django.db.models.fields.TextField`.
-**Signature:** `CharField([max_length=<Integer>[, min_length=<Integer>]])`
+**Signature:** `CharField(max_length=None, min_length=None)`
+
+## ChoiceField
+
+A field that can accept on of a limited set of choices.
## EmailField
-A text representation, validates the text to be a valid e-mail address. Corresponds to `django.db.models.fields.EmailField`
+A text representation, validates the text to be a valid e-mail address.
+
+Corresponds to `django.db.models.fields.EmailField`
## DateField
-A date representation. Corresponds to `django.db.models.fields.DateField`
+A date representation.
+
+Corresponds to `django.db.models.fields.DateField`
## DateTimeField
-A date and time representation. Corresponds to `django.db.models.fields.DateTimeField`
+A date and time representation.
+
+Corresponds to `django.db.models.fields.DateTimeField`
## IntegerField
-An integer representation. Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.SmallIntegerField`, `django.db.models.fields.PositiveIntegerField` and `django.db.models.fields.PositiveSmallIntegerField`
+An integer representation.
+
+Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.SmallIntegerField`, `django.db.models.fields.PositiveIntegerField` and `django.db.models.fields.PositiveSmallIntegerField`
## FloatField
-A floating point representation. Corresponds to `django.db.models.fields.FloatField`.
+A floating point representation.
+
+Corresponds to `django.db.models.fields.FloatField`.
---
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 162eed26..f610d6aa 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -7,6 +7,7 @@ from django.core import validators
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve
from django.conf import settings
+from django.forms import widgets
from django.utils.encoding import is_protected_type, smart_unicode
from django.utils.translation import ugettext_lazy as _
from rest_framework.reverse import reverse
@@ -107,10 +108,15 @@ class WritableField(Field):
'required': _('This field is required.'),
'invalid': _('Invalid value.'),
}
+ widget = widgets.TextInput
+ default = None
def __init__(self, source=None, readonly=False, required=None,
- validators=[], error_messages=None):
+ validators=[], error_messages=None, widget=None,
+ default=None):
+
super(WritableField, self).__init__(source=source)
+
self.readonly = readonly
if required is None:
self.required = not(readonly)
@@ -125,6 +131,13 @@ class WritableField(Field):
self.error_messages = messages
self.validators = self.default_validators + validators
+ self.default = default or self.default
+
+ # Widgets are ony used for HTML forms.
+ widget = widget or self.widget
+ if isinstance(widget, type):
+ widget = widget()
+ self.widget = widget
def validate(self, value):
if value in validators.EMPTY_VALUES and self.required:
@@ -159,9 +172,12 @@ class WritableField(Field):
try:
native = data[field_name]
except KeyError:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- return
+ if self.default is not None:
+ native = self.default
+ else:
+ if self.required:
+ raise ValidationError(self.error_messages['required'])
+ return
value = self.from_native(native)
if self.source == '*':
@@ -399,20 +415,23 @@ class HyperlinkedIdentityField(Field):
class BooleanField(WritableField):
type_name = 'BooleanField'
+ widget = widgets.CheckboxInput
default_error_messages = {
'invalid': _(u"'%s' value must be either True or False."),
}
+ empty = False
+
+ # Note: we set default to `False` in order to fill in missing value not
+ # supplied by html form. TODO: Fix so that only html form input gets
+ # this behavior.
+ default = False
def from_native(self, value):
- if value in (True, False):
- # if value is 1 or 0 than it's equal to True or False, but we want
- # to return a true bool for semantic reasons.
- return bool(value)
if value in ('t', 'True', '1'):
return True
if value in ('f', 'False', '0'):
return False
- raise ValidationError(self.error_messages['invalid'] % value)
+ return bool(value)
class CharField(WritableField):
@@ -432,6 +451,52 @@ class CharField(WritableField):
return smart_unicode(value)
+class ChoiceField(WritableField):
+ type_name = 'ChoiceField'
+ widget = widgets.Select
+ default_error_messages = {
+ 'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
+ }
+
+ def __init__(self, choices=(), *args, **kwargs):
+ super(ChoiceField, self).__init__(*args, **kwargs)
+ self.choices = choices
+
+ def _get_choices(self):
+ return self._choices
+
+ def _set_choices(self, value):
+ # Setting choices also sets the choices on the widget.
+ # choices can be any iterable, but we call list() on it because
+ # it will be consumed more than once.
+ self._choices = self.widget.choices = list(value)
+
+ choices = property(_get_choices, _set_choices)
+
+ def validate(self, value):
+ """
+ Validates that the input is in self.choices.
+ """
+ super(ChoiceField, self).validate(value)
+ if value and not self.valid_value(value):
+ raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
+
+ def valid_value(self, value):
+ """
+ Check to see if the provided value is a valid choice.
+ """
+ for k, v in self.choices:
+ if isinstance(v, (list, tuple)):
+ # This is an optgroup, so look inside the group for options
+ for k2, v2 in v:
+ if value == smart_unicode(k2):
+ return True
+ else:
+ if value == smart_unicode(k):
+ return True
+ return False
+
+
class EmailField(CharField):
type_name = 'EmailField'
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 43175861..c64fb517 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -6,6 +6,7 @@ on the response, such as JSON encoded data or HTML output.
REST framework also provides an HTML renderer the renders the browseable API.
"""
+import copy
import string
from django import forms
from django.http.multipartparser import parse_header
@@ -260,13 +261,32 @@ class BrowsableAPIRenderer(BaseRenderer):
continue
kwargs = {}
+ kwargs['required'] = v.required
+
if getattr(v, 'queryset', None):
- kwargs['queryset'] = getattr(v, 'queryset', None)
+ kwargs['queryset'] = v.queryset
+
+ if getattr(v, 'widget', None):
+ widget = copy.deepcopy(v.widget)
+ # If choices have friendly readable names,
+ # then add in the identities too
+ if getattr(widget, 'choices', None):
+ choices = widget.choices
+ if any([ident != desc for (ident, desc) in choices]):
+ choices = [(ident, "%s (%s)" % (desc, ident))
+ for (ident, desc) in choices]
+ widget.choices = choices
+ kwargs['widget'] = widget
+
+ if getattr(v, 'default', None) is not None:
+ kwargs['initial'] = v.default
+
+ kwargs['label'] = k
try:
fields[k] = field_mapping[v.__class__](**kwargs)
except KeyError:
- fields[k] = forms.CharField()
+ fields[k] = forms.CharField(**kwargs)
return fields
def get_form(self, view, method, request):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 6724bbdf..221cbf2f 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -247,6 +247,19 @@ class BaseSerializer(Field):
if not self._errors:
return self.restore_object(attrs, instance=getattr(self, 'object', None))
+ def field_to_native(self, obj, field_name):
+ """
+ Override default so that we can apply ModelSerializer as a nested
+ field to relationships.
+ """
+ obj = getattr(obj, self.source or field_name)
+
+ # If the object has an "all" method, assume it's a relationship
+ if is_simple_callable(getattr(obj, 'all', None)):
+ return [self.to_native(item) for item in obj.all()]
+
+ return self.to_native(obj)
+
@property
def errors(self):
"""
@@ -295,16 +308,6 @@ class ModelSerializer(Serializer):
"""
_options_class = ModelSerializerOptions
- def field_to_native(self, obj, field_name):
- """
- Override default so that we can apply ModelSerializer as a nested
- field to relationships.
- """
- obj = getattr(obj, self.source or field_name)
- if obj.__class__.__name__ in ('RelatedManager', 'ManyRelatedManager'):
- return [self.to_native(item) for item in obj.all()]
- return self.to_native(obj)
-
def default_fields(self, serialize, obj=None, data=None, nested=False):
"""
Return all the fields that should be serialized for the model.
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index 75dab2f7..8e721737 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -92,6 +92,17 @@ class Comment(RESTFrameworkModel):
content = models.CharField(max_length=200)
created = models.DateTimeField(auto_now_add=True)
+
class ActionItem(RESTFrameworkModel):
title = models.CharField(max_length=200)
done = models.BooleanField(default=False)
+
+
+# Models for reverse relations
+class BlogPost(RESTFrameworkModel):
+ title = models.CharField(max_length=100)
+
+
+class BlogPostComment(RESTFrameworkModel):
+ text = models.TextField()
+ blog_post = models.ForeignKey(BlogPost)
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index bd1f07da..2dfc04e1 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -302,3 +302,32 @@ class CallableDefaultValueTests(TestCase):
self.assertEquals(len(self.objects.all()), 1)
self.assertEquals(instance.pk, 1)
self.assertEquals(instance.text, 'overridden')
+
+
+class ManyRelatedTests(TestCase):
+ def setUp(self):
+
+ class BlogPostCommentSerializer(serializers.Serializer):
+ text = serializers.CharField()
+
+ class BlogPostSerializer(serializers.Serializer):
+ title = serializers.CharField()
+ comments = BlogPostCommentSerializer(source='blogpostcomment_set')
+
+ self.serializer_class = BlogPostSerializer
+
+ def test_reverse_relations(self):
+ post = BlogPost.objects.create(title="Test blog post")
+ post.blogpostcomment_set.create(text="I hate this blog post")
+ post.blogpostcomment_set.create(text="I love this blog post")
+
+ serializer = self.serializer_class(instance=post)
+ expected = {
+ 'title': 'Test blog post',
+ 'comments': [
+ {'text': 'I hate this blog post'},
+ {'text': 'I love this blog post'}
+ ]
+ }
+
+ self.assertEqual(serializer.data, expected)