mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 17:47:04 +03:00
Merge branch 'html-form-rendering'
This commit is contained in:
commit
c3e370b168
|
@ -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.'),
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
12
rest_framework/templates/rest_framework/raw_data_form.html
Normal file
12
rest_framework/templates/rest_framework/raw_data_form.html
Normal file
|
@ -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 %}
|
|
@ -159,8 +159,7 @@ class BasicTests(TestCase):
|
|||
expected = {
|
||||
'email': '',
|
||||
'content': '',
|
||||
'created': None,
|
||||
'sub_comment': ''
|
||||
'created': None
|
||||
}
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
|
15
rest_framework/tests/test_serializer_empty.py
Normal file
15
rest_framework/tests/test_serializer_empty.py
Normal file
|
@ -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})
|
Loading…
Reference in New Issue
Block a user