mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-18 12:30:58 +03:00
Merge commit '95a670de41a246777bc1e448dca8cc576b7b86ea' into BrowsableAPIRenderer
Conflicts: rest_framework/renderers.py - manually resolved conflict
This commit is contained in:
commit
d1e05ea8d4
|
@ -73,34 +73,52 @@ These fields represent basic datatypes, and support both reading and writing val
|
||||||
|
|
||||||
## BooleanField
|
## BooleanField
|
||||||
|
|
||||||
A Boolean representation, corresponds to `django.db.models.fields.BooleanField`.
|
A Boolean representation.
|
||||||
|
|
||||||
|
Corresponds to `django.db.models.fields.BooleanField`.
|
||||||
|
|
||||||
## CharField
|
## 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`.
|
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
|
## 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
|
## DateField
|
||||||
|
|
||||||
A date representation. Corresponds to `django.db.models.fields.DateField`
|
A date representation.
|
||||||
|
|
||||||
|
Corresponds to `django.db.models.fields.DateField`
|
||||||
|
|
||||||
## DateTimeField
|
## 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
|
## 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
|
## FloatField
|
||||||
|
|
||||||
A floating point representation. Corresponds to `django.db.models.fields.FloatField`.
|
A floating point representation.
|
||||||
|
|
||||||
|
Corresponds to `django.db.models.fields.FloatField`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.core import validators
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.urlresolvers import resolve
|
from django.core.urlresolvers import resolve
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.forms import widgets
|
||||||
from django.utils.encoding import is_protected_type, smart_unicode
|
from django.utils.encoding import is_protected_type, smart_unicode
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
@ -107,10 +108,15 @@ class WritableField(Field):
|
||||||
'required': _('This field is required.'),
|
'required': _('This field is required.'),
|
||||||
'invalid': _('Invalid value.'),
|
'invalid': _('Invalid value.'),
|
||||||
}
|
}
|
||||||
|
widget = widgets.TextInput
|
||||||
|
default = None
|
||||||
|
|
||||||
def __init__(self, source=None, readonly=False, required=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)
|
super(WritableField, self).__init__(source=source)
|
||||||
|
|
||||||
self.readonly = readonly
|
self.readonly = readonly
|
||||||
if required is None:
|
if required is None:
|
||||||
self.required = not(readonly)
|
self.required = not(readonly)
|
||||||
|
@ -125,6 +131,13 @@ class WritableField(Field):
|
||||||
self.error_messages = messages
|
self.error_messages = messages
|
||||||
|
|
||||||
self.validators = self.default_validators + validators
|
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):
|
def validate(self, value):
|
||||||
if value in validators.EMPTY_VALUES and self.required:
|
if value in validators.EMPTY_VALUES and self.required:
|
||||||
|
@ -159,9 +172,12 @@ class WritableField(Field):
|
||||||
try:
|
try:
|
||||||
native = data[field_name]
|
native = data[field_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if self.required:
|
if self.default is not None:
|
||||||
raise ValidationError(self.error_messages['required'])
|
native = self.default
|
||||||
return
|
else:
|
||||||
|
if self.required:
|
||||||
|
raise ValidationError(self.error_messages['required'])
|
||||||
|
return
|
||||||
|
|
||||||
value = self.from_native(native)
|
value = self.from_native(native)
|
||||||
if self.source == '*':
|
if self.source == '*':
|
||||||
|
@ -399,20 +415,23 @@ class HyperlinkedIdentityField(Field):
|
||||||
|
|
||||||
class BooleanField(WritableField):
|
class BooleanField(WritableField):
|
||||||
type_name = 'BooleanField'
|
type_name = 'BooleanField'
|
||||||
|
widget = widgets.CheckboxInput
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'invalid': _(u"'%s' value must be either True or False."),
|
'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):
|
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'):
|
if value in ('t', 'True', '1'):
|
||||||
return True
|
return True
|
||||||
if value in ('f', 'False', '0'):
|
if value in ('f', 'False', '0'):
|
||||||
return False
|
return False
|
||||||
raise ValidationError(self.error_messages['invalid'] % value)
|
return bool(value)
|
||||||
|
|
||||||
|
|
||||||
class CharField(WritableField):
|
class CharField(WritableField):
|
||||||
|
@ -432,6 +451,52 @@ class CharField(WritableField):
|
||||||
return smart_unicode(value)
|
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):
|
class EmailField(CharField):
|
||||||
type_name = 'EmailField'
|
type_name = 'EmailField'
|
||||||
|
|
||||||
|
|
|
@ -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.
|
REST framework also provides an HTML renderer the renders the browseable API.
|
||||||
"""
|
"""
|
||||||
|
import copy
|
||||||
import string
|
import string
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.http.multipartparser import parse_header
|
from django.http.multipartparser import parse_header
|
||||||
|
@ -260,13 +261,32 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
kwargs['required'] = v.required
|
||||||
|
|
||||||
if getattr(v, 'queryset', None):
|
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:
|
try:
|
||||||
fields[k] = field_mapping[v.__class__](**kwargs)
|
fields[k] = field_mapping[v.__class__](**kwargs)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
fields[k] = forms.CharField()
|
fields[k] = forms.CharField(**kwargs)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_form(self, view, method, request):
|
def get_form(self, view, method, request):
|
||||||
|
|
|
@ -247,6 +247,19 @@ class BaseSerializer(Field):
|
||||||
if not self._errors:
|
if not self._errors:
|
||||||
return self.restore_object(attrs, instance=getattr(self, 'object', None))
|
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
|
@property
|
||||||
def errors(self):
|
def errors(self):
|
||||||
"""
|
"""
|
||||||
|
@ -295,16 +308,6 @@ class ModelSerializer(Serializer):
|
||||||
"""
|
"""
|
||||||
_options_class = ModelSerializerOptions
|
_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):
|
def default_fields(self, serialize, obj=None, data=None, nested=False):
|
||||||
"""
|
"""
|
||||||
Return all the fields that should be serialized for the model.
|
Return all the fields that should be serialized for the model.
|
||||||
|
|
|
@ -92,6 +92,17 @@ class Comment(RESTFrameworkModel):
|
||||||
content = models.CharField(max_length=200)
|
content = models.CharField(max_length=200)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
class ActionItem(RESTFrameworkModel):
|
class ActionItem(RESTFrameworkModel):
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
done = models.BooleanField(default=False)
|
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)
|
||||||
|
|
|
@ -302,3 +302,32 @@ class CallableDefaultValueTests(TestCase):
|
||||||
self.assertEquals(len(self.objects.all()), 1)
|
self.assertEquals(len(self.objects.all()), 1)
|
||||||
self.assertEquals(instance.pk, 1)
|
self.assertEquals(instance.pk, 1)
|
||||||
self.assertEquals(instance.text, 'overridden')
|
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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user