mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-23 15:54:16 +03:00
First pass on nested serializers in HTML
This commit is contained in:
parent
babdc78e61
commit
5d247a65c8
|
@ -26,11 +26,13 @@ Create a new Django project named `tutorial`, then start a new app called `quick
|
|||
|
||||
Now sync your database for the first time:
|
||||
|
||||
python manage.py syncdb
|
||||
python manage.py migrate
|
||||
|
||||
Make sure to create an initial user named `admin` with a password of `password`. We'll authenticate as that user later in our example.
|
||||
We'll also create an initial user named `admin` with a password of `password`. We'll authenticate as that user later in our example.
|
||||
|
||||
Once you've set up a database and got everything synced and ready to go, open up the app's directory and we'll get coding...
|
||||
python manage.py createsuperuser
|
||||
|
||||
Once you've set up a database and initial user created and ready to go, open up the app's directory and we'll get coding...
|
||||
|
||||
## Serializers
|
||||
|
||||
|
|
|
@ -114,12 +114,15 @@ else:
|
|||
|
||||
|
||||
|
||||
# MinValueValidator and MaxValueValidator only accept `message` in 1.8+
|
||||
# MinValueValidator, MaxValueValidator et al. only accept `message` in 1.8+
|
||||
if django.VERSION >= (1, 8):
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.validators import MinLengthValidator, MaxLengthValidator
|
||||
else:
|
||||
from django.core.validators import MinValueValidator as DjangoMinValueValidator
|
||||
from django.core.validators import MaxValueValidator as DjangoMaxValueValidator
|
||||
from django.core.validators import MinLengthValidator as DjangoMinLengthValidator
|
||||
from django.core.validators import MaxLengthValidator as DjangoMaxLengthValidator
|
||||
|
||||
class MinValueValidator(DjangoMinValueValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -131,6 +134,17 @@ else:
|
|||
self.message = kwargs.pop('message', self.message)
|
||||
super(MaxValueValidator, self).__init__(*args, **kwargs)
|
||||
|
||||
class MinLengthValidator(DjangoMinLengthValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = kwargs.pop('message', self.message)
|
||||
super(MinLengthValidator, self).__init__(*args, **kwargs)
|
||||
|
||||
class MaxLengthValidator(DjangoMaxLengthValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = kwargs.pop('message', self.message)
|
||||
super(MaxLengthValidator, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# URLValidator only accepts `message` in 1.6+
|
||||
if django.VERSION >= (1, 6):
|
||||
from django.core.validators import URLValidator
|
||||
|
|
|
@ -8,7 +8,10 @@ from django.utils.dateparse import parse_date, parse_datetime, parse_time
|
|||
from django.utils.encoding import is_protected_type
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import ISO_8601
|
||||
from rest_framework.compat import smart_text, EmailValidator, MinValueValidator, MaxValueValidator, URLValidator
|
||||
from rest_framework.compat import (
|
||||
smart_text, EmailValidator, MinValueValidator, MaxValueValidator,
|
||||
MinLengthValidator, MaxLengthValidator, URLValidator
|
||||
)
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils import html, representation, humanize_datetime
|
||||
import copy
|
||||
|
@ -138,7 +141,7 @@ class Field(object):
|
|||
self.label = label
|
||||
self.help_text = help_text
|
||||
self.style = {} if style is None else style
|
||||
self.validators = validators or self.default_validators[:]
|
||||
self.validators = validators[:] or self.default_validators[:]
|
||||
self.allow_null = allow_null
|
||||
|
||||
# These are set up by `.bind()` when the field is added to a serializer.
|
||||
|
@ -412,16 +415,24 @@ class NullBooleanField(Field):
|
|||
|
||||
class CharField(Field):
|
||||
default_error_messages = {
|
||||
'blank': _('This field may not be blank.')
|
||||
'blank': _('This field may not be blank.'),
|
||||
'max_length': _('Ensure this field has no more than {max_length} characters.'),
|
||||
'min_length': _('Ensure this field has no more than {min_length} characters.')
|
||||
}
|
||||
initial = ''
|
||||
coerce_blank_to_null = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.allow_blank = kwargs.pop('allow_blank', False)
|
||||
self.max_length = kwargs.pop('max_length', None)
|
||||
self.min_length = kwargs.pop('min_length', None)
|
||||
max_length = kwargs.pop('max_length', None)
|
||||
min_length = kwargs.pop('min_length', None)
|
||||
super(CharField, self).__init__(**kwargs)
|
||||
if max_length is not None:
|
||||
message = self.error_messages['max_length'].format(max_length=max_length)
|
||||
self.validators.append(MaxLengthValidator(max_length, message=message))
|
||||
if min_length is not None:
|
||||
message = self.error_messages['min_length'].format(min_length=min_length)
|
||||
self.validators.append(MinLengthValidator(min_length, message=message))
|
||||
|
||||
def run_validation(self, data=empty):
|
||||
# Test for the empty string here so that it does not get validated,
|
||||
|
@ -857,6 +868,13 @@ class MultipleChoiceField(ChoiceField):
|
|||
}
|
||||
default_empty_html = []
|
||||
|
||||
def get_value(self, dictionary):
|
||||
# We override the default field access in order to support
|
||||
# lists in HTML forms.
|
||||
if html.is_html_input(dictionary):
|
||||
return dictionary.getlist(self.field_name)
|
||||
return dictionary.get(self.field_name, empty)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, type('')) or not hasattr(data, '__iter__'):
|
||||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from rest_framework.compat import smart_text, urlparse
|
||||
from rest_framework.fields import empty, Field
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.utils import html
|
||||
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
|
||||
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404
|
||||
from django.db.models.query import QuerySet
|
||||
|
@ -263,6 +264,13 @@ class ManyRelation(Field):
|
|||
super(ManyRelation, self).__init__(*args, **kwargs)
|
||||
self.child_relation.bind(field_name='', parent=self)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
# We override the default field access in order to support
|
||||
# lists in HTML forms.
|
||||
if html.is_html_input(dictionary):
|
||||
return dictionary.getlist(self.field_name)
|
||||
return dictionary.get(self.field_name, empty)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return [
|
||||
self.child_relation.to_internal_value(item)
|
||||
|
@ -278,10 +286,16 @@ class ManyRelation(Field):
|
|||
|
||||
@property
|
||||
def choices(self):
|
||||
queryset = self.child_relation.queryset
|
||||
iterable = queryset.all() if (hasattr(queryset, 'all')) else queryset
|
||||
items_and_representations = [
|
||||
(item, self.child_relation.to_representation(item))
|
||||
for item in iterable
|
||||
]
|
||||
return dict([
|
||||
(
|
||||
str(self.child_relation.to_representation(item)),
|
||||
str(item)
|
||||
str(item_representation),
|
||||
str(item) + ' - ' + str(item_representation)
|
||||
)
|
||||
for item in self.child_relation.queryset.all()
|
||||
for item, item_representation in items_and_representations
|
||||
])
|
||||
|
|
|
@ -364,6 +364,12 @@ class HTMLFormRenderer(BaseRenderer):
|
|||
serializers.ManyRelation: {
|
||||
'default': 'select_multiple.html',
|
||||
'checkbox': 'select_checkbox.html'
|
||||
},
|
||||
serializers.Serializer: {
|
||||
'default': 'fieldset.html'
|
||||
},
|
||||
serializers.ListSerializer: {
|
||||
'default': 'list_fieldset.html'
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -392,7 +398,9 @@ class HTMLFormRenderer(BaseRenderer):
|
|||
template = loader.get_template(template_name)
|
||||
context = Context({
|
||||
'field': field,
|
||||
'input_type': input_type
|
||||
'input_type': input_type,
|
||||
'renderer': self,
|
||||
'layout': layout
|
||||
})
|
||||
|
||||
return template.render(context)
|
||||
|
|
|
@ -166,14 +166,25 @@ class BoundField(object):
|
|||
Returned when iterating over a serializer instance,
|
||||
providing an API similar to Django forms and form fields.
|
||||
"""
|
||||
def __init__(self, field, value, errors):
|
||||
def __init__(self, field, value, errors, prefix=''):
|
||||
self._field = field
|
||||
self.value = value
|
||||
self.errors = errors
|
||||
self.name = prefix + self.field_name
|
||||
|
||||
def __getattr__(self, attr_name):
|
||||
return getattr(self._field, attr_name)
|
||||
|
||||
def __iter__(self):
|
||||
for field in self.fields.values():
|
||||
yield self[field.field_name]
|
||||
|
||||
def __getitem__(self, key):
|
||||
field = self.fields[key]
|
||||
value = self.value.get(key) if self.value else None
|
||||
error = self.errors.get(key) if self.errors else None
|
||||
return BoundField(field, value, error, prefix=self.name + '.')
|
||||
|
||||
@property
|
||||
def _proxy_class(self):
|
||||
return self._field.__class__
|
||||
|
@ -355,16 +366,23 @@ class Serializer(BaseSerializer):
|
|||
def validate(self, attrs):
|
||||
return attrs
|
||||
|
||||
def __iter__(self):
|
||||
errors = self.errors if hasattr(self, '_errors') else {}
|
||||
for field in self.fields.values():
|
||||
value = self.data.get(field.field_name) if self.data else None
|
||||
error = errors.get(field.field_name)
|
||||
yield BoundField(field, value, error)
|
||||
|
||||
def __repr__(self):
|
||||
return representation.serializer_repr(self, indent=1)
|
||||
|
||||
# The following are used for accessing `BoundField` instances on the
|
||||
# serializer, for the purposes of presenting a form-like API onto the
|
||||
# field values and field errors.
|
||||
|
||||
def __iter__(self):
|
||||
for field in self.fields.values():
|
||||
yield self[field.field_name]
|
||||
|
||||
def __getitem__(self, key):
|
||||
field = self.fields[key]
|
||||
value = self.data.get(key)
|
||||
error = self.errors.get(key) if hasattr(self, '_errors') else None
|
||||
return BoundField(field, value, error)
|
||||
|
||||
|
||||
# There's some replication of `ListField` here,
|
||||
# but that's probably better than obfuscating the call hierarchy.
|
||||
|
@ -404,8 +422,9 @@ class ListSerializer(BaseSerializer):
|
|||
"""
|
||||
List of object instances -> List of dicts of primitive datatypes.
|
||||
"""
|
||||
iterable = data.all() if (hasattr(data, 'all')) else data
|
||||
return ReturnList(
|
||||
[self.child.to_representation(item) for item in data],
|
||||
[self.child.to_representation(item) for item in iterable],
|
||||
serializer=self
|
||||
)
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
{% load rest_framework %}
|
||||
<fieldset>
|
||||
{% if field.label %}
|
||||
<div class="form-group" style="border-bottom: 1px solid #e5e5e5">
|
||||
<legend class="control-label col-sm-2 {% if field.style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for field_item in field.value.field_items.values() %}
|
||||
{{ renderer.render_field(field_item, layout=layout) }}
|
||||
{% for nested_field in field %}
|
||||
{% render_field nested_field layout=layout renderer=renderer %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{% load rest_framework %}
|
||||
<fieldset>
|
||||
{% if field.label %}
|
||||
<div class="form-group" style="border-bottom: 1px solid #e5e5e5">
|
||||
<legend class="control-label col-sm-2 {% if field.style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% for child in field.value %}
|
||||
<li>TODO</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</fieldset>
|
|
@ -1,3 +1,4 @@
|
|||
{% for field_item in field.value.field_items.values() %}
|
||||
{{ renderer.render_field(field_item, layout=layout) }}
|
||||
{% load rest_framework %}
|
||||
{% for nested_field in field %}
|
||||
{% render_field nested_field layout=layout renderer=renderer %}
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{% load rest_framework %}
|
||||
<fieldset>
|
||||
{% if field.label %}<legend {% if field.style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
|
||||
{% for field_item in field.value.field_items.values() %}
|
||||
{{ renderer.render_field(field_item, layout=layout) }}
|
||||
{% for nested_field in field %}
|
||||
{% render_field nested_field layout=layout renderer=renderer %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<fieldset>
|
||||
{% if field.label %}<legend {% if field.style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
|
||||
<!-- {% if field.label %}<legend {% if field.style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
|
||||
{% for field_item in field.value.field_items.values() %}
|
||||
{{ renderer.render_field(field_item, layout=layout) }}
|
||||
{% endfor %} -->
|
||||
</fieldset>
|
69
tests/test_bound_fields.py
Normal file
69
tests/test_bound_fields.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class TestSimpleBoundField:
|
||||
def test_empty_bound_field(self):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
text = serializers.CharField(max_length=100)
|
||||
amount = serializers.IntegerField()
|
||||
|
||||
serializer = ExampleSerializer()
|
||||
|
||||
assert serializer['text'].value == ''
|
||||
assert serializer['text'].errors is None
|
||||
assert serializer['text'].name == 'text'
|
||||
assert serializer['amount'].value is None
|
||||
assert serializer['amount'].errors is None
|
||||
assert serializer['amount'].name == 'amount'
|
||||
|
||||
def test_populated_bound_field(self):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
text = serializers.CharField(max_length=100)
|
||||
amount = serializers.IntegerField()
|
||||
|
||||
serializer = ExampleSerializer(data={'text': 'abc', 'amount': 123})
|
||||
|
||||
assert serializer['text'].value == 'abc'
|
||||
assert serializer['text'].errors is None
|
||||
assert serializer['text'].name == 'text'
|
||||
assert serializer['amount'].value is 123
|
||||
assert serializer['amount'].errors is None
|
||||
assert serializer['amount'].name == 'amount'
|
||||
|
||||
def test_error_bound_field(self):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
text = serializers.CharField(max_length=100)
|
||||
amount = serializers.IntegerField()
|
||||
|
||||
serializer = ExampleSerializer(data={'text': 'x' * 1000, 'amount': 123})
|
||||
serializer.is_valid()
|
||||
|
||||
assert serializer['text'].value == 'x' * 1000
|
||||
assert serializer['text'].errors == ['Ensure this field has no more than 100 characters.']
|
||||
assert serializer['text'].name == 'text'
|
||||
assert serializer['amount'].value is 123
|
||||
assert serializer['amount'].errors is None
|
||||
assert serializer['amount'].name == 'amount'
|
||||
|
||||
|
||||
class TestNestedBoundField:
|
||||
def test_nested_empty_bound_field(self):
|
||||
class Nested(serializers.Serializer):
|
||||
more_text = serializers.CharField(max_length=100)
|
||||
amount = serializers.IntegerField()
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
text = serializers.CharField(max_length=100)
|
||||
nested = Nested()
|
||||
|
||||
serializer = ExampleSerializer()
|
||||
|
||||
assert serializer['text'].value == ''
|
||||
assert serializer['text'].errors is None
|
||||
assert serializer['text'].name == 'text'
|
||||
assert serializer['nested']['more_text'].value == ''
|
||||
assert serializer['nested']['more_text'].errors is None
|
||||
assert serializer['nested']['more_text'].name == 'nested.more_text'
|
||||
assert serializer['nested']['amount'].value is None
|
||||
assert serializer['nested']['amount'].errors is None
|
||||
assert serializer['nested']['amount'].name == 'nested.amount'
|
Loading…
Reference in New Issue
Block a user