diff options
| author | Tom Christie | 2013-10-10 17:33:39 +0100 |
|---|---|---|
| committer | Tom Christie | 2013-10-10 17:33:39 +0100 |
| commit | c3e370b168de03b963b6afcffce6bd52e1d4ab7c (patch) | |
| tree | 112a90214d1f6cac4490ab42c4a80178e28aaaa7 /rest_framework | |
| parent | afc9e9e03868634c548178e6730a0f9964f398c0 (diff) | |
| parent | 9e29c6389529210978d58cee78e437b901f9daa2 (diff) | |
| download | django-rest-framework-c3e370b168de03b963b6afcffce6bd52e1d4ab7c.tar.bz2 | |
Merge branch 'html-form-rendering'
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/fields.py | 24 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 78 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 23 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/base.html | 4 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/form.html | 12 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/raw_data_form.html | 12 | ||||
| -rw-r--r-- | rest_framework/tests/test_serializer.py | 3 | ||||
| -rw-r--r-- | rest_framework/tests/test_serializer_empty.py | 15 |
8 files changed, 97 insertions, 74 deletions
diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 0c3817b5..e23fc001 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -123,6 +123,7 @@ class Field(object): use_files = False form_field_class = forms.CharField type_label = 'field' + widget = None def __init__(self, source=None, label=None, help_text=None): self.parent = None @@ -134,9 +135,29 @@ class Field(object): if label is not None: self.label = smart_text(label) + else: + self.label = None if help_text is not None: self.help_text = strip_multiple_choice_msg(smart_text(help_text)) + else: + self.help_text = None + + self._errors = [] + self._value = None + self._name = None + + @property + def errors(self): + return self._errors + + def widget_html(self): + if not self.widget: + return '' + return self.widget.render(self._name, self._value) + + def label_tag(self): + return '<label for="%s">%s:</label>' % (self._name, self.label) def initialize(self, parent, field_name): """ @@ -757,6 +778,7 @@ class IntegerField(WritableField): type_name = 'IntegerField' type_label = 'integer' form_field_class = forms.IntegerField + empty = 0 default_error_messages = { 'invalid': _('Enter a whole number.'), @@ -788,6 +810,7 @@ class FloatField(WritableField): type_name = 'FloatField' type_label = 'float' form_field_class = forms.FloatField + empty = 0 default_error_messages = { 'invalid': _("'%s' value must be a float."), @@ -808,6 +831,7 @@ class DecimalField(WritableField): type_name = 'DecimalField' type_label = 'decimal' form_field_class = forms.DecimalField + empty = Decimal('0') default_error_messages = { 'invalid': _('Enter a number.'), diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a27160d4..fe4f43d4 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -336,71 +336,15 @@ class HTMLFormRenderer(BaseRenderer): template = 'rest_framework/form.html' charset = 'utf-8' - def data_to_form_fields(self, data): - fields = {} - for key, val in data.fields.items(): - if getattr(val, 'read_only', True): - # Don't include read-only fields. - continue - - if getattr(val, 'fields', None): - # Nested data not supported by HTML forms. - continue - - kwargs = {} - kwargs['required'] = val.required - - #if getattr(v, 'queryset', None): - # kwargs['queryset'] = v.queryset - - if getattr(val, 'choices', None) is not None: - kwargs['choices'] = val.choices - - if getattr(val, 'regex', None) is not None: - kwargs['regex'] = val.regex - - if getattr(val, 'widget', None): - widget = copy.deepcopy(val.widget) - kwargs['widget'] = widget - - if getattr(val, 'default', None) is not None: - kwargs['initial'] = val.default - - if getattr(val, 'label', None) is not None: - kwargs['label'] = val.label - - if getattr(val, 'help_text', None) is not None: - kwargs['help_text'] = val.help_text - - fields[key] = val.form_field_class(**kwargs) - - return fields - def render(self, data, accepted_media_type=None, renderer_context=None): """ Render serializer data and return an HTML form, as a string. """ - # The HTMLFormRenderer currently uses something of a hack to render - # the content, by translating each of the serializer fields into - # an html form field, creating a dynamic form using those fields, - # and then rendering that form. - - # This isn't strictly neccessary, as we could render the serilizer - # fields to HTML directly. The implementation is historical and will - # likely change at some point. - - self.renderer_context = renderer_context or {} - request = self.renderer_context['request'] - - # Creating an on the fly form see: - # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python - fields = self.data_to_form_fields(data) - DynamicForm = type(str('DynamicForm'), (forms.Form,), fields) - data = None if data.empty else data + renderer_context = renderer_context or {} + request = renderer_context['request'] template = loader.get_template(self.template) - context = RequestContext(request, {'form': DynamicForm(data)}) - + context = RequestContext(request, {'form': data}) return template.render(context) @@ -475,6 +419,13 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence of the View having an associated form then return None. """ + if request.method == method: + data = request.DATA + files = request.FILES + else: + data = None + files = None + with override_method(view, request, method) as request: obj = getattr(view, 'object', None) if not self.show_form_for_method(view, method, request, obj): @@ -487,9 +438,10 @@ class BrowsableAPIRenderer(BaseRenderer): or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)): return - serializer = view.get_serializer(instance=obj) - + serializer = view.get_serializer(instance=obj, data=data, files=files) + serializer.is_valid() data = serializer.data + form_renderer = self.form_renderer_class() return form_renderer.render(data, self.accepted_media_type, self.renderer_context) @@ -581,6 +533,7 @@ class BrowsableAPIRenderer(BaseRenderer): renderer = self.get_default_renderer(view) + raw_data_post_form = self.get_raw_data_form(view, 'POST', request) raw_data_put_form = self.get_raw_data_form(view, 'PUT', request) raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request) raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form @@ -599,12 +552,11 @@ class BrowsableAPIRenderer(BaseRenderer): 'put_form': self.get_rendered_html_form(view, 'PUT', request), 'post_form': self.get_rendered_html_form(view, 'POST', request), - 'patch_form': self.get_rendered_html_form(view, 'PATCH', request), 'delete_form': self.get_rendered_html_form(view, 'DELETE', request), 'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), 'raw_data_put_form': raw_data_put_form, - 'raw_data_post_form': self.get_raw_data_form(view, 'POST', request), + 'raw_data_post_form': raw_data_post_form, 'raw_data_patch_form': raw_data_patch_form, 'raw_data_put_or_patch_form': raw_data_put_or_patch_form, diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8d2e0feb..8e945688 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -32,6 +32,13 @@ from rest_framework.relations import * from rest_framework.fields import * +def pretty_name(name): + """Converts 'first_name' to 'First name'""" + if not name: + return '' + return name.replace('_', ' ').capitalize() + + class RelationsList(list): _deleted = [] @@ -301,14 +308,16 @@ class BaseSerializer(WritableField): """ ret = self._dict_class() ret.fields = self._dict_class() - ret.empty = obj is None for field_name, field in self.fields.items(): + if field.read_only and obj is None: + continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) ret[key] = value - ret.fields[key] = field + ret.fields[key] = self.augment_field(field, field_name, key, value) + return ret def from_native(self, data, files): @@ -316,6 +325,7 @@ class BaseSerializer(WritableField): Deserialize primitives -> objects. """ self._errors = {} + if data is not None or files is not None: attrs = self.restore_fields(data, files) if attrs is not None: @@ -326,6 +336,15 @@ class BaseSerializer(WritableField): if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) + def augment_field(self, field, field_name, key, value): + # This horrible stuff is to manage serializers rendering to HTML + field._errors = self._errors.get(key) if self._errors else None + field._name = field_name + field._value = self.init_data.get(key) if self._errors and self.init_data else value + if not field.label: + field.label = pretty_name(key) + return field + def field_to_native(self, obj, field_name): """ Override default so that the serializer can be used as a nested field diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 2776d550..33be36db 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -151,7 +151,7 @@ {% with form=raw_data_post_form %} <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> <fieldset> - {% include "rest_framework/form.html" %} + {% include "rest_framework/raw_data_form.html" %} <div class="form-actions"> <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button> </div> @@ -188,7 +188,7 @@ {% with form=raw_data_put_or_patch_form %} <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> <fieldset> - {% include "rest_framework/form.html" %} + {% include "rest_framework/raw_data_form.html" %} <div class="form-actions"> {% if raw_data_put_form %} <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> diff --git a/rest_framework/templates/rest_framework/form.html b/rest_framework/templates/rest_framework/form.html index b27f652e..b1e148df 100644 --- a/rest_framework/templates/rest_framework/form.html +++ b/rest_framework/templates/rest_framework/form.html @@ -1,13 +1,15 @@ {% load rest_framework %} {% csrf_token %} {{ form.non_field_errors }} -{% for field in form %} - <div class="control-group"> <!--{% if field.errors %}error{% endif %}--> +{% for field in form.fields.values %} + {% if not field.read_only %} + <div class="control-group {% if field.errors %}error{% endif %}"> {{ field.label_tag|add_class:"control-label" }} <div class="controls"> - {{ field }} - <span class="help-block">{{ field.help_text }}</span> - <!--{{ field.errors|add_class:"help-block" }}--> + {{ field.widget_html }} + {% if field.help_text %}<span class="help-block">{{ field.help_text }}</span>{% endif %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} </div> </div> + {% endif %} {% endfor %} diff --git a/rest_framework/templates/rest_framework/raw_data_form.html b/rest_framework/templates/rest_framework/raw_data_form.html new file mode 100644 index 00000000..075279f7 --- /dev/null +++ b/rest_framework/templates/rest_framework/raw_data_form.html @@ -0,0 +1,12 @@ +{% load rest_framework %} +{% csrf_token %} +{{ form.non_field_errors }} +{% for field in form %} + <div class="control-group"> + {{ field.label_tag|add_class:"control-label" }} + <div class="controls"> + {{ field }} + <span class="help-block">{{ field.help_text }}</span> + </div> + </div> +{% endfor %} diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 8d246b01..d4e5a93f 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -159,8 +159,7 @@ class BasicTests(TestCase): expected = { 'email': '', 'content': '', - 'created': None, - 'sub_comment': '' + 'created': None } self.assertEqual(serializer.data, expected) diff --git a/rest_framework/tests/test_serializer_empty.py b/rest_framework/tests/test_serializer_empty.py new file mode 100644 index 00000000..30cff361 --- /dev/null +++ b/rest_framework/tests/test_serializer_empty.py @@ -0,0 +1,15 @@ +from django.test import TestCase +from rest_framework import serializers + + +class EmptySerializerTestCase(TestCase): + def test_empty_serializer(self): + class FooBarSerializer(serializers.Serializer): + foo = serializers.IntegerField() + bar = serializers.SerializerMethodField('get_bar') + + def get_bar(self, obj): + return 'bar' + + serializer = FooBarSerializer() + self.assertEquals(serializer.data, {'foo': 0}) |
