Merge commit '95a670de41a246777bc1e448dca8cc576b7b86ea' into BrowsableAPIRenderer

Conflicts:
	rest_framework/renderers.py - manually resolved conflict
This commit is contained in:
Marko Tibold 2012-10-22 20:09:36 +02:00
commit d1e05ea8d4
6 changed files with 175 additions and 29 deletions

View File

@ -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`.
---

View File

@ -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,6 +172,9 @@ class WritableField(Field):
try:
native = data[field_name]
except KeyError:
if self.default is not None:
native = self.default
else:
if self.required:
raise ValidationError(self.error_messages['required'])
return
@ -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'

View File

@ -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):

View File

@ -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.

View File

@ -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)

View File

@ -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)