aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorTom Christie2013-10-10 17:33:39 +0100
committerTom Christie2013-10-10 17:33:39 +0100
commitc3e370b168de03b963b6afcffce6bd52e1d4ab7c (patch)
tree112a90214d1f6cac4490ab42c4a80178e28aaaa7 /rest_framework
parentafc9e9e03868634c548178e6730a0f9964f398c0 (diff)
parent9e29c6389529210978d58cee78e437b901f9daa2 (diff)
downloaddjango-rest-framework-c3e370b168de03b963b6afcffce6bd52e1d4ab7c.tar.bz2
Merge branch 'html-form-rendering'
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/fields.py24
-rw-r--r--rest_framework/renderers.py78
-rw-r--r--rest_framework/serializers.py23
-rw-r--r--rest_framework/templates/rest_framework/base.html4
-rw-r--r--rest_framework/templates/rest_framework/form.html12
-rw-r--r--rest_framework/templates/rest_framework/raw_data_form.html12
-rw-r--r--rest_framework/tests/test_serializer.py3
-rw-r--r--rest_framework/tests/test_serializer_empty.py15
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})