aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorTom Christie2014-10-09 15:11:19 +0100
committerTom Christie2014-10-09 15:11:19 +0100
commit5d247a65c89594a7ab5ce2333612f23eadc6828d (patch)
treed9e67e3a84a588747cd6e39356151149cf73b376 /rest_framework
parentbabdc78e61ac915fa4a01bdfb04e11a32dbf5d79 (diff)
downloaddjango-rest-framework-5d247a65c89594a7ab5ce2333612f23eadc6828d.tar.bz2
First pass on nested serializers in HTML
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/compat.py16
-rw-r--r--rest_framework/fields.py28
-rw-r--r--rest_framework/relations.py20
-rw-r--r--rest_framework/renderers.py10
-rw-r--r--rest_framework/serializers.py35
-rw-r--r--rest_framework/templates/rest_framework/fields/horizontal/fieldset.html5
-rw-r--r--rest_framework/templates/rest_framework/fields/horizontal/list_fieldset.html13
-rw-r--r--rest_framework/templates/rest_framework/fields/inline/fieldset.html5
-rw-r--r--rest_framework/templates/rest_framework/fields/vertical/fieldset.html5
-rw-r--r--rest_framework/templates/rest_framework/fields/vertical/list_fieldset.html7
10 files changed, 120 insertions, 24 deletions
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index e4e69580..4ab23a4d 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -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
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index b371c7d0..7053acee 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -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__)
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index c1e5aa18..268b95cf 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -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
])
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 931dd434..4fb36060 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -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)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 9fcbcba7..1c006990 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -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,15 +366,22 @@ class Serializer(BaseSerializer):
def validate(self, attrs):
return attrs
+ 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):
- 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)
+ yield self[field.field_name]
- def __repr__(self):
- return representation.serializer_repr(self, indent=1)
+ 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,
@@ -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
)
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/fieldset.html b/rest_framework/templates/rest_framework/fields/horizontal/fieldset.html
index 843a56b2..ff93c6ba 100644
--- a/rest_framework/templates/rest_framework/fields/horizontal/fieldset.html
+++ b/rest_framework/templates/rest_framework/fields/horizontal/fieldset.html
@@ -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>
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/list_fieldset.html b/rest_framework/templates/rest_framework/fields/horizontal/list_fieldset.html
new file mode 100644
index 00000000..68c75d4f
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/horizontal/list_fieldset.html
@@ -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>
diff --git a/rest_framework/templates/rest_framework/fields/inline/fieldset.html b/rest_framework/templates/rest_framework/fields/inline/fieldset.html
index 380d4627..ba9f1835 100644
--- a/rest_framework/templates/rest_framework/fields/inline/fieldset.html
+++ b/rest_framework/templates/rest_framework/fields/inline/fieldset.html
@@ -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 %}
diff --git a/rest_framework/templates/rest_framework/fields/vertical/fieldset.html b/rest_framework/templates/rest_framework/fields/vertical/fieldset.html
index 8708916b..248fe904 100644
--- a/rest_framework/templates/rest_framework/fields/vertical/fieldset.html
+++ b/rest_framework/templates/rest_framework/fields/vertical/fieldset.html
@@ -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>
diff --git a/rest_framework/templates/rest_framework/fields/vertical/list_fieldset.html b/rest_framework/templates/rest_framework/fields/vertical/list_fieldset.html
new file mode 100644
index 00000000..6b99a834
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/vertical/list_fieldset.html
@@ -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>