First pass at HTML form rendering

This commit is contained in:
Tom Christie 2014-10-01 19:44:46 +01:00
parent c630a12e26
commit c171fa21ac
34 changed files with 325 additions and 16 deletions

View File

@ -13,17 +13,18 @@ import django
from django import forms from django import forms
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
from django.template import RequestContext, loader, Template from django.template import Context, RequestContext, loader, Template
from django.test.client import encode_multipart from django.test.client import encode_multipart
from django.utils import six from django.utils import six
from django.utils.xmlutils import SimplerXMLGenerator from django.utils.xmlutils import SimplerXMLGenerator
from rest_framework import exceptions, serializers, status, VERSION
from rest_framework.compat import StringIO, smart_text, yaml from rest_framework.compat import StringIO, smart_text, yaml
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.request import is_form_media_type, override_method from rest_framework.request import is_form_media_type, override_method
from rest_framework.utils import encoders from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework import exceptions, status, VERSION from rest_framework.utils.field_mapping import ClassLookupDict
def zero_as_none(value): def zero_as_none(value):
@ -341,6 +342,42 @@ class HTMLFormRenderer(BaseRenderer):
template = 'rest_framework/form.html' template = 'rest_framework/form.html'
charset = 'utf-8' charset = 'utf-8'
field_templates = ClassLookupDict({
serializers.Field: {
'default': 'input.html'
},
serializers.BooleanField: {
'default': 'checkbox.html'
},
serializers.CharField: {
'default': 'input.html',
'textarea': 'textarea.html'
},
serializers.ChoiceField: {
'default': 'select.html',
'radio': 'select_radio.html'
},
serializers.MultipleChoiceField: {
'default': 'select_multiple.html',
'checkbox': 'select_checkbox.html'
}
})
def render_field(self, field, value, errors, layout=None):
layout = layout or 'vertical'
style_type = field.style.get('type', 'default')
if style_type == 'textarea' and layout == 'inline':
style_type = 'default'
base = self.field_templates[field][style_type]
template_name = 'rest_framework/fields/' + layout + '/' + base
template = loader.get_template(template_name)
context = Context({
'field': field,
'value': value,
'errors': errors
})
return template.render(context)
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
""" """
Render serializer data and return an HTML form, as a string. Render serializer data and return an HTML form, as a string.
@ -349,7 +386,11 @@ class HTMLFormRenderer(BaseRenderer):
request = renderer_context['request'] request = renderer_context['request']
template = loader.get_template(self.template) template = loader.get_template(self.template)
context = RequestContext(request, {'form': data}) context = RequestContext(request, {
'form': data,
'layout': getattr(getattr(data, 'Meta', None), 'layout', 'vertical'),
'renderer': self
})
return template.render(context) return template.render(context)

View File

@ -302,6 +302,8 @@ class Serializer(BaseSerializer):
def __iter__(self): def __iter__(self):
errors = self.errors if hasattr(self, '_errors') else {} errors = self.errors if hasattr(self, '_errors') else {}
for field in self.fields.values(): for field in self.fields.values():
if field.read_only:
continue
value = self.data.get(field.field_name) if self.data else None value = self.data.get(field.field_name) if self.data else None
error = errors.get(field.field_name) error = errors.get(field.field_name)
yield FieldResult(field, value, error) yield FieldResult(field, value, error)

View File

@ -0,0 +1 @@
name="{{ field.field_name }}" {% if field.style.placeholder %}placeholder="{{ field.style.placeholder }}"{% endif %} {% if field.style.rows %}rows="{{ field.style.rows }}"{% endif %}

View File

@ -0,0 +1,10 @@
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label>
<input type="checkbox" name="{{ field.field_name }}" value="true" {% if value %}checked{% endif %}>
{% if field.label %}{{ field.label }}{% endif %}
</label>
</div>
</div>
</div>

View File

@ -0,0 +1,10 @@
<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 value.field_items.values() %}
{{ renderer.render_field(field_item, layout=layout) }}
{% endfor %}
</fieldset>

View File

@ -0,0 +1,7 @@
<div class="form-group">
{% include "rest_framework/fields/horizontal/label.html" %}
<div class="col-sm-10">
<input type="{{ input_type }}" class="form-control" {% include "rest_framework/fields/attrs.html" %} {% if value %}value="{{ value }}"{% endif %}>
{% if field.help_text %}<p class="help-block">{{ field.help_text }}</p>{% endif %}
</div>
</div>

View File

@ -0,0 +1 @@
{% if field.label %}<label class="col-sm-2 control-label {% if field.style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>{% endif %}

View File

@ -0,0 +1,10 @@
<div class="form-group">
{% include "rest_framework/fields/horizontal/label.html" %}
<div class="col-sm-10">
<select class="form-control" name="{{ field.field_name }}">
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
</div>
</div>

View File

@ -0,0 +1,22 @@
<div class="form-group">
{% include "rest_framework/fields/horizontal/label.html" %}
<div class="col-sm-10">
{% if field.style.inline %}
{% for key, text in field.choices.items %}
<label class="checkbox-inline">
<input type="checkbox" name="{{ field.field_name }}" value="{{ key }}" {% if key in value %}checked{% endif %}>
{{ text }}
</label>
{% endfor %}
{% else %}
{% for key, text in field.choices.items %}
<div class="checkbox">
<label>
<input type="checkbox" name="{{ field.field_name }}" value="{{ key }}" {% if key in value %}checked{% endif %}>
{{ text }}
</label>
</div>
{% endfor %}
{% endif %}
</div>
</div>

View File

@ -0,0 +1,10 @@
<div class="form-group">
{% include "rest_framework/fields/horizontal/label.html" %}
<div class="col-sm-10">
<select multiple class="form-control" name="{{ field.field_name }}">
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key in value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
</div>
</div>

View File

@ -0,0 +1,22 @@
<div class="form-group">
{% include "rest_framework/fields/horizontal/label.html" %}
<div class="col-sm-10">
{% if field.style.inline %}
{% for key, text in field.choices.items %}
<label class="radio-inline">
<input type="radio" name="{{ field.field_name }}" value="{{ key }}" {% if key == value %}checked{% endif %}>
{{ text }}
</label>
{% endfor %}
{% else %}
{% for key, text in field.choices.items %}
<div class="radio">
<label>
<input type="radio" name="{{ field.field_name }}" value="{{ key }}" {% if key == value %}checked{% endif %}>
{{ text }}
</label>
</div>
{% endfor %}
{% endif %}
</div>
</div>

View File

@ -0,0 +1,7 @@
<div class="form-group">
{% include "rest_framework/fields/horizontal/label.html" %}
<div class="col-sm-10">
<textarea class="form-control" {% include "rest_framework/fields/attrs.html" %}>{% if value %}{{ value }}{% endif %}</textarea>
{% if field.help_text %}<p class="help-block">{{ field.help_text }}</p>{% endif %}
</div>
</div>

View File

@ -0,0 +1,6 @@
<div class="checkbox">
<label>
<input type="checkbox" name="{{ field.field_name }}" value="true" {% if value %}checked{% endif %}>
{% if field.label %}{{ field.label }}{% endif %}
</label>
</div>

View File

@ -0,0 +1,3 @@
{% for field_item in value.field_items.values() %}
{{ renderer.render_field(field_item, layout=layout) }}
{% endfor %}

View File

@ -0,0 +1,4 @@
<div class="form-group">
{% include "rest_framework/fields/inline/label.html" %}
<input type="{{ input_type }}" class="form-control" {% include "rest_framework/fields/attrs.html" %} {% if value %}value="{{ value }}"{% endif %}>
</div>

View File

@ -0,0 +1 @@
{% if field.label %}<label class="sr-only">{{ field.label }}</label>{% endif %}

View File

@ -0,0 +1,8 @@
<div class="form-group">
{% include "rest_framework/fields/inline/label.html" %}
<select class="form-control" name="{{ field.field_name }}">
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
</div>

View File

@ -0,0 +1,11 @@
<div class="form-group">
{% include "rest_framework/fields/inline/label.html" %}
{% for key, text in field.choices.items %}
<div class="checkbox">
<label>
<input type="checkbox" name="{{ rest_framework/field.field_name }}" value="{{ key }}" {% if key in value %}checked{% endif %}>
{{ text }}
</label>
</div>
{% endfor %}
</div>

View File

@ -0,0 +1,8 @@
<div class="form-group">
{% include "rest_framework/fields/inline/label.html" %}
<select multiple class="form-control" name="{{ field.field_name }}">
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key in value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
</div>

View File

@ -0,0 +1,11 @@
<div class="form-group">
{% include "rest_framework/fields/inline/label.html" %}
{% for key, text in field.choices.items %}
<div class="radio">
<label>
<input type="radio" name="{{ field.field_name }}" value="{{ key }}" {% if key == value %}checked{% endif %}>
{{ text }}
</label>
</div>
{% endfor %}
</div>

View File

@ -0,0 +1,4 @@
<div class="form-group">
{% include "rest_framework/fields/inline/label.html" %}
<textarea class="form-control" {% include "rest_framework/fields/attrs.html" %}>{% if value %}{{ value }}{% endif %}</textarea>
</div>

View File

@ -0,0 +1,6 @@
<div class="checkbox">
<label>
<input type="checkbox" name="{{ field.field_name }}" value="true" {% if value %}checked{% endif %}>
{% if field.label %}{{ field.label }}{% endif %}
</label>
</div>

View File

@ -0,0 +1,6 @@
<fieldset>
{% if field.label %}<legend {% if field.style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
{% for field_item in value.field_items.values() %}
{{ renderer.render_field(field_item, layout=layout) }}
{% endfor %}
</fieldset>

View File

@ -0,0 +1,5 @@
<div class="form-group">
{% include "rest_framework/fields/vertical/label.html" %}
<input type="{{ input_type }}" class="form-control" {% include "rest_framework/fields/attrs.html" %} {% if value %}value="{{ value }}"{% endif %}>
{% if field.help_text %}<p class="help-block">{{ field.help_text }}</p>{% endif %}
</div>

View File

@ -0,0 +1 @@
{% if field.label %}<label {% if field.style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>{% endif %}

View File

@ -0,0 +1,8 @@
<div class="form-group">
{% include "rest_framework/fields/vertical/label.html" %}
<select class="form-control" name="{{ field.field_name }}">
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
</div>

View File

@ -0,0 +1,22 @@
<div class="form-group">
{% include "rest_framework/fields/vertical/label.html" %}
{% if field.style.inline %}
<div>
{% for key, text in field.choices.items %}
<label class="checkbox-inline">
<input type="checkbox" name="{{ field.field_name }}" value="{{ key }}" {% if key in value %}checked{% endif %}>
{{ text }}
</label>
{% endfor %}
</div>
{% else %}
{% for key, text in field.choices.items %}
<div class="checkbox">
<label>
<input type="checkbox" name="{{ field.field_name }}" value="{{ key }}" {% if key in value %}checked{% endif %}>
{{ text }}
</label>
</div>
{% endfor %}
{% endif %}
</div>

View File

@ -0,0 +1,8 @@
<div class="form-group">
{% include "rest_framework/fields/vertical/label.html" %}
<select multiple class="form-control" name="{{ field.field_name }}">
{% for key, text in field.choices.items() %}
<option value="{{ key }}" {% if key in value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
</div>

View File

@ -0,0 +1,22 @@
<div class="form-group">
{% include "rest_framework/fields/vertical/label.html" %}
{% if field.style.inline %}
<div>
{% for key, text in field.choices.items %}
<label class="radio-inline">
<input type="radio" name="{{ field.field_name }}" value="{{ key }}" {% if key|string==value|string %}checked{% endif %}>
{{ text }}
</label>
{% endfor %}
</div>
{% else %}
{% for key, text in field.choices.items %}
<div class="radio">
<label>
<input type="radio" name="{{ field.field_name }}" value="{{ key }}" {% if key|string==value|string %}checked{% endif %}>
{{ text }}
</label>
</div>
{% endfor %}
{% endif %}
</div>

View File

@ -0,0 +1,5 @@
<div class="form-group">
{% include "rest_framework/fields/vertical/label.html" %}
<textarea class="form-control" {% include "rest_framework/fields/attrs.html" %}>{% if value %}{{ value }}{% endif %}</textarea>
{% if field.help_text %}<p class="help-block">{{ field.help_text }}</p>{% endif %}
</div>

View File

@ -1,15 +1,31 @@
<html>
<head>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<h1>User update</h1>
<div class="well">
{% load rest_framework %} {% load rest_framework %}
{% csrf_token %} <form {% if layout == "inline" %}class="form-inline"{% elif layout == "horizontal" %}class="form-horizontal"{% endif %} role="form" action="" method="POST">
{{ form.non_field_errors }} {% csrf_token %}
{% for field in form.fields.values %} {% for field, value, errors in form %}
{% if not field.read_only %} {% render_field field value errors layout=layout renderer=renderer %}
<div class="control-group {% if field.errors %}error{% endif %}"> {% endfor %}
{{ field.label_tag|add_class:"control-label" }} <!-- form.non_field_errors -->
<div class="controls"> {% if layout == "horizontal" %}
{{ field.widget_html }} <div class="form-group">
{% if field.help_text %}<span class="help-block">{{ field.help_text }}</span>{% endif %} <div class="col-sm-offset-2 col-sm-10">
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} <button type="submit" class="btn btn-default">Submit</button>
</div> </div>
</div> </div>
{% else %}
<button type="submit" class="btn btn-default">Submit</button>
{% endif %} {% endif %}
{% endfor %} </form>
</div>
</div></body>
</html>

View File

@ -31,6 +31,14 @@ class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
# And the template tags themselves... # And the template tags themselves...
# @register.simple_tag
# def render_field(field, value, errors, renderer):
# return renderer.render_field(field, value, errors)
@register.simple_tag
def render_field(field, value, errors, layout=None, renderer=None):
return renderer.render_field(field, value, errors, layout)
@register.simple_tag @register.simple_tag
def optional_login(request): def optional_login(request):
""" """

View File

@ -79,6 +79,9 @@ def get_field_kwargs(field_name, model_field):
kwargs['choices'] = model_field.flatchoices kwargs['choices'] = model_field.flatchoices
return kwargs return kwargs
if isinstance(model_field, models.TextField):
kwargs['style'] = {'type': 'textarea'}
if model_field.null and not isinstance(model_field, models.NullBooleanField): if model_field.null and not isinstance(model_field, models.NullBooleanField):
kwargs['allow_null'] = True kwargs['allow_null'] = True

View File

@ -95,7 +95,7 @@ class TestRegularFieldMappings(TestCase):
positive_small_integer_field = IntegerField() positive_small_integer_field = IntegerField()
slug_field = SlugField(max_length=100) slug_field = SlugField(max_length=100)
small_integer_field = IntegerField() small_integer_field = IntegerField()
text_field = CharField() text_field = CharField(style={'type': 'textarea'})
time_field = TimeField() time_field = TimeField()
url_field = URLField(max_length=100) url_field = URLField(max_length=100)
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>) custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)