diff options
| -rw-r--r-- | docs/tutorial/quickstart.md | 8 | ||||
| -rw-r--r-- | rest_framework/compat.py | 16 | ||||
| -rw-r--r-- | rest_framework/fields.py | 28 | ||||
| -rw-r--r-- | rest_framework/relations.py | 20 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 10 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 35 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/fields/horizontal/fieldset.html | 5 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/fields/horizontal/list_fieldset.html | 13 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/fields/inline/fieldset.html | 5 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/fields/vertical/fieldset.html | 5 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/fields/vertical/list_fieldset.html | 7 | ||||
| -rw-r--r-- | tests/test_bound_fields.py | 69 |
12 files changed, 194 insertions, 27 deletions
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 813e9872..c2dc4bea 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -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 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> diff --git a/tests/test_bound_fields.py b/tests/test_bound_fields.py new file mode 100644 index 00000000..469437e4 --- /dev/null +++ b/tests/test_bound_fields.py @@ -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' |
