Fixed #15667 -- Added template-based widget rendering.

Thanks Carl Meyer and Tim Graham for contributing to the patch.
This commit is contained in:
Preston Timmons 2016-12-27 17:00:56 -05:00 committed by Tim Graham
parent 51cde873d9
commit b52c73008a
98 changed files with 1334 additions and 874 deletions

View File

@ -216,6 +216,9 @@ INSTALLED_APPS = []
TEMPLATES = []
# Default form rendering class.
FORM_RENDERER = 'django.forms.renderers.DjangoTemplates'
# Default email address to use for various automated correspondence from
# the site managers.
DEFAULT_FROM_EMAIL = 'webmaster@localhost'

View File

@ -0,0 +1,6 @@
{% if is_initial %}<p class="file-upload">{{ initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
<span class="clearable-file-input">
<input type="checkbox" name="{{ checkbox_name }}" id="{{ checkbox_id }}" />
<label for="{{ checkbox_id }}">{{ clear_checkbox_label }}</label>{% endif %}</span><br />
{{ input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />{% if is_initial %}</p>{% endif %}

View File

@ -0,0 +1 @@
{% include 'django/forms/widgets/input.html' %}{% if related_url %}<a href="{{ related_url }}" class="related-lookup" id="lookup_id_{{ widget.name }}" title="{{ link_title }}"></a>{% endif %}{% if link_label %}&nbsp;<strong>{% if link_url %}<a href="{{ link_url }}">{% endif %}{{ link_label }}{% if link_url %}</a>{% endif %}</strong>{% endif %}

View File

@ -0,0 +1 @@
{% include 'admin/widgets/foreign_key_raw_id.html' %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/multiple_input.html" %}

View File

@ -0,0 +1,27 @@
{% load i18n static %}
<div class="related-widget-wrapper">
{% include widget.template_name %}
{% block links %}
{% if can_change_related %}
<a class="related-widget-wrapper-link change-related" id="change_id_{{ widget.name }}"
data-href-template="{{ change_related_template_url }}?{{ url_params }}"
title="{% blocktrans %}Change selected {{ model }}{% endblocktrans %}">
<img src="{% static 'admin/img/icon-changelink.svg' %}" width="10" height="10" alt="{% trans 'Change' %}"/>
</a>
{% endif %}
{% if can_add_related %}
<a class="related-widget-wrapper-link add-related" id="add_id_{{ widget.name }}"
href="{{ add_related_url }}?{{ url_params }}"
title="{% blocktrans %}Add another {{ model }}{% endblocktrans %}">
<img src="{% static 'admin/img/icon-addlink.svg' %}" width="10" height="10" alt="{% trans 'Add' %}"/>
</a>
{% endif %}
{% if can_delete_related %}
<a class="related-widget-wrapper-link delete-related" id="delete_id_{{ widget.name }}"
data-href-template="{{ delete_related_template_url }}?{{ url_params }}"
title="{% blocktrans %}Delete selected {{ model }}{% endblocktrans %}">
<img src="{% static 'admin/img/icon-deletelink.svg' %}" width="10" height="10" alt="{% trans 'Delete' %}"/>
</a>
{% endif %}
{% endblock %}
</div>

View File

@ -0,0 +1,4 @@
<p class="datetime">
{{ date_label }} {% with widget=widget.subwidgets.0 %}{% include widget.template_name %}{% endwith %}<br />
{{ time_label }} {% with widget=widget.subwidgets.1 %}{% include widget.template_name %}{% endwith %}
</p>

View File

@ -0,0 +1 @@
{% if widget.value %}<p class="url">{{ current_label }} <a href="{{ widget.href }}">{{ widget.value }}</a><br />{{ change_label }} {% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.value %}</p>{% endif %}

View File

@ -7,14 +7,11 @@ import copy
from django import forms
from django.db.models.deletion import CASCADE
from django.forms.utils import flatatt
from django.forms.widgets import RadioFieldRenderer
from django.template.loader import render_to_string
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from django.utils import six
from django.utils.encoding import force_text
from django.utils.html import format_html, format_html_join, smart_urlquote
from django.utils.html import smart_urlquote
from django.utils.safestring import mark_safe
from django.utils.text import Truncator
from django.utils.translation import ugettext as _
@ -37,17 +34,14 @@ class FilteredSelectMultiple(forms.SelectMultiple):
self.is_stacked = is_stacked
super(FilteredSelectMultiple, self).__init__(attrs, choices)
def render(self, name, value, attrs=None):
if attrs is None:
attrs = {}
attrs['class'] = 'selectfilter'
def get_context(self, name, value, attrs=None):
context = super(FilteredSelectMultiple, self).get_context(name, value, attrs)
context['widget']['attrs']['class'] = 'selectfilter'
if self.is_stacked:
attrs['class'] += 'stacked'
attrs['data-field-name'] = self.verbose_name
attrs['data-is-stacked'] = int(self.is_stacked)
output = super(FilteredSelectMultiple, self).render(name, value, attrs)
return mark_safe(output)
context['widget']['attrs']['class'] += 'stacked'
context['widget']['attrs']['data-field-name'] = self.verbose_name
context['widget']['attrs']['data-is-stacked'] = int(self.is_stacked)
return context
class AdminDateWidget(forms.DateInput):
@ -80,38 +74,27 @@ class AdminSplitDateTime(forms.SplitDateTimeWidget):
"""
A SplitDateTime Widget that has some admin-specific styling.
"""
template_name = 'admin/widgets/split_datetime.html'
def __init__(self, attrs=None):
widgets = [AdminDateWidget, AdminTimeWidget]
# Note that we're calling MultiWidget, not SplitDateTimeWidget, because
# we want to define widgets.
forms.MultiWidget.__init__(self, widgets, attrs)
def format_output(self, rendered_widgets):
return format_html('<p class="datetime">{} {}<br />{} {}</p>',
_('Date:'), rendered_widgets[0],
_('Time:'), rendered_widgets[1])
class AdminRadioFieldRenderer(RadioFieldRenderer):
def render(self):
"""Outputs a <ul> for this set of radio fields."""
return format_html('<ul{}>\n{}\n</ul>',
flatatt(self.attrs),
format_html_join('\n', '<li>{}</li>',
((force_text(w),) for w in self)))
def get_context(self, name, value, attrs):
context = super(AdminSplitDateTime, self).get_context(name, value, attrs)
context['date_label'] = _('Date:')
context['time_label'] = _('Time:')
return context
class AdminRadioSelect(forms.RadioSelect):
renderer = AdminRadioFieldRenderer
template_name = 'admin/widgets/radio.html'
class AdminFileWidget(forms.ClearableFileInput):
template_with_initial = (
'<p class="file-upload">%s</p>' % forms.ClearableFileInput.template_with_initial
)
template_with_clear = (
'<span class="clearable-file-input">%s</span>' % forms.ClearableFileInput.template_with_clear
)
template_name = 'admin/widgets/clearable_file_input.html'
def url_params_from_lookup_dict(lookups):
@ -141,17 +124,17 @@ class ForeignKeyRawIdWidget(forms.TextInput):
A Widget for displaying ForeignKeys in the "raw_id" interface rather than
in a <select> box.
"""
template_name = 'admin/widgets/foreign_key_raw_id.html'
def __init__(self, rel, admin_site, attrs=None, using=None):
self.rel = rel
self.admin_site = admin_site
self.db = using
super(ForeignKeyRawIdWidget, self).__init__(attrs)
def render(self, name, value, attrs=None):
def get_context(self, name, value, attrs=None):
context = super(ForeignKeyRawIdWidget, self).get_context(name, value, attrs)
rel_to = self.rel.model
if attrs is None:
attrs = {}
extra = []
if rel_to in self.admin_site._registry:
# The related object is registered with the same AdminSite
related_url = reverse(
@ -164,21 +147,16 @@ class ForeignKeyRawIdWidget(forms.TextInput):
params = self.url_parameters()
if params:
url = '?' + '&amp;'.join('%s=%s' % (k, v) for k, v in params.items())
else:
url = ''
if "class" not in attrs:
attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript code looks for this hook.
# TODO: "lookup_id_" is hard-coded here. This should instead use
# the correct API to determine the ID dynamically.
extra.append(
'<a href="%s%s" class="related-lookup" id="lookup_id_%s" title="%s"></a>'
% (related_url, url, name, _('Lookup'))
)
output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] + extra
if value:
output.append(self.label_for_value(value))
return mark_safe(''.join(output))
related_url += '?' + '&amp;'.join(
'%s=%s' % (k, v) for k, v in params.items(),
)
context['related_url'] = mark_safe(related_url)
context['link_title'] = _('Lookup')
# The JavaScript code looks for this class.
context['widget']['attrs'].setdefault('class', 'vForeignKeyRawIdAdminField')
if context['widget']['value']:
context['link_label'], context['link_url'] = self.label_and_url_for_value(value)
return context
def base_url_parameters(self):
limit_choices_to = self.rel.limit_choices_to
@ -192,17 +170,15 @@ class ForeignKeyRawIdWidget(forms.TextInput):
params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
return params
def label_for_value(self, value):
def label_and_url_for_value(self, value):
key = self.rel.get_related_field().name
try:
obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
except (ValueError, self.rel.model.DoesNotExist):
return ''
return '', ''
label = '&nbsp;<strong>{}</strong>'
text = Truncator(obj).words(14, truncate='...')
try:
change_url = reverse(
url = reverse(
'%s:%s_%s_change' % (
self.admin_site.name,
obj._meta.app_label,
@ -211,11 +187,9 @@ class ForeignKeyRawIdWidget(forms.TextInput):
args=(obj.pk,)
)
except NoReverseMatch:
pass # Admin not registered for target model.
else:
text = format_html('<a href="{}">{}</a>', change_url, text)
url = '' # Admin not registered for target model.
return format_html(label, text)
return Truncator(obj).words(14, truncate='...'), url
class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
@ -223,36 +197,36 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
in a <select multiple> box.
"""
def render(self, name, value, attrs=None):
if attrs is None:
attrs = {}
template_name = 'admin/widgets/many_to_many_raw_id.html'
def get_context(self, name, value, attrs=None):
context = super(ManyToManyRawIdWidget, self).get_context(name, value, attrs)
if self.rel.model in self.admin_site._registry:
# The related object is registered with the same AdminSite
attrs['class'] = 'vManyToManyRawIdAdminField'
if value:
value = ','.join(force_text(v) for v in value)
else:
value = ''
return super(ManyToManyRawIdWidget, self).render(name, value, attrs)
context['widget']['attrs']['class'] = 'vManyToManyRawIdAdminField'
return context
def url_parameters(self):
return self.base_url_parameters()
def label_for_value(self, value):
return ''
def label_and_url_for_value(self, value):
return '', ''
def value_from_datadict(self, data, files, name):
value = data.get(name)
if value:
return value.split(',')
def format_value(self, value):
return ','.join(force_text(v) for v in value) if value else ''
class RelatedFieldWidgetWrapper(forms.Widget):
"""
This class is a wrapper to a given widget to add the add icon for the
admin interface.
"""
template = 'admin/related_widget_wrapper.html'
template_name = 'admin/widgets/related_widget_wrapper.html'
def __init__(self, widget, rel, admin_site, can_add_related=None,
can_change_related=False, can_delete_related=False):
@ -294,21 +268,19 @@ class RelatedFieldWidgetWrapper(forms.Widget):
return reverse("admin:%s_%s_%s" % (info + (action,)),
current_app=self.admin_site.name, args=args)
def render(self, name, value, *args, **kwargs):
def get_context(self, name, value, attrs=None):
with self.widget.override_choices(self.choices):
context = self.widget.get_context(name, value, attrs)
from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
rel_opts = self.rel.model._meta
info = (rel_opts.app_label, rel_opts.model_name)
self.widget.choices = self.choices
url_params = '&'.join("%s=%s" % param for param in [
(TO_FIELD_VAR, self.rel.get_related_field().name),
(IS_POPUP_VAR, 1),
])
context = {
'widget': self.widget.render(name, value, *args, **kwargs),
'name': name,
'url_params': url_params,
'model': rel_opts.verbose_name,
}
context['url_params'] = url_params
context['model'] = rel_opts.verbose_name
if self.can_change_related:
change_related_template_url = self.get_related_url(info, 'change', '__fk__')
context.update(
@ -327,12 +299,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
can_delete_related=True,
delete_related_template_url=delete_related_template_url,
)
return mark_safe(render_to_string(self.template, context))
def build_attrs(self, extra_attrs=None, **kwargs):
"Helper function for building an attribute dictionary."
self.attrs = self.widget.build_attrs(extra_attrs=None, **kwargs)
return self.attrs
return context
def value_from_datadict(self, data, files, name):
return self.widget.value_from_datadict(data, files, name)
@ -366,23 +333,24 @@ class AdminEmailInputWidget(forms.EmailInput):
class AdminURLFieldWidget(forms.URLInput):
template_name = 'admin/widgets/url.html'
def __init__(self, attrs=None):
final_attrs = {'class': 'vURLField'}
if attrs is not None:
final_attrs.update(attrs)
super(AdminURLFieldWidget, self).__init__(attrs=final_attrs)
def render(self, name, value, attrs=None):
html = super(AdminURLFieldWidget, self).render(name, value, attrs)
if value:
value = force_text(self.format_value(value))
final_attrs = {'href': smart_urlquote(value)}
html = format_html(
'<p class="url">{} <a{}>{}</a><br />{} {}</p>',
_('Currently:'), flatatt(final_attrs), value,
_('Change:'), html
)
return html
def get_context(self, name, value, attrs):
context = super(AdminURLFieldWidget, self).get_context(name, value, attrs)
context['current_label'] = _('Currently:')
context['change_label'] = _('Change:')
context['widget']['href'] = smart_urlquote(context['widget']['value'])
return context
def format_value(self, value):
value = super(AdminURLFieldWidget, self).format_value(value)
return force_text(value)
class AdminIntegerFieldWidget(forms.NumberInput):

View File

@ -13,12 +13,9 @@ from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMultiAlternatives
from django.forms.utils import flatatt
from django.template import loader
from django.utils.encoding import force_bytes
from django.utils.html import format_html, format_html_join
from django.utils.http import urlsafe_base64_encode
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext, ugettext_lazy as _
@ -26,26 +23,23 @@ UserModel = get_user_model()
class ReadOnlyPasswordHashWidget(forms.Widget):
def render(self, name, value, attrs):
encoded = value
final_attrs = self.build_attrs(attrs)
template_name = 'auth/widgets/read_only_password_hash.html'
if not encoded or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
def get_context(self, name, value, attrs):
context = super(ReadOnlyPasswordHashWidget, self).get_context(name, value, attrs)
summary = []
if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX):
summary.append({'label': ugettext("No password set.")})
else:
try:
hasher = identify_hasher(encoded)
hasher = identify_hasher(value)
except ValueError:
summary = mark_safe("<strong>%s</strong>" % ugettext(
"Invalid password format or unknown hashing algorithm."
))
summary.append({'label': ugettext("Invalid password format or unknown hashing algorithm.")})
else:
summary = format_html_join(
'', '<strong>{}</strong>: {} ',
((ugettext(key), value) for key, value in hasher.safe_summary(encoded).items())
)
return format_html("<div{}>{}</div>", flatatt(final_attrs), summary)
for key, value_ in hasher.safe_summary(value).items():
summary.append({'label': ugettext(key), 'value': value_})
context['summary'] = summary
return context
class ReadOnlyPasswordHashField(forms.Field):

View File

@ -0,0 +1,3 @@
{% for entry in summary %}
<div{% include 'django/forms/widgets/attrs.html' %}><strong>{{ entry.label }}</strong>{% if entry.value %}: {{ entry.value }}{% endif %}
{% endfor %}

View File

@ -80,7 +80,7 @@ class GeoModelAdmin(ModelAdmin):
collection_type = 'None'
class OLMap(self.widget):
template = self.map_template
template_name = self.map_template
geom_type = db_field.geom_type
wms_options = ''

View File

@ -3,7 +3,6 @@ import logging
from django.contrib.gis.gdal import GDALException
from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.forms.widgets import Textarea
from django.template import loader
from django.utils import six, translation
# Creating a template context that contains Django settings
@ -16,7 +15,7 @@ class OpenLayersWidget(Textarea):
"""
Renders an OpenLayers map using the WKT of the geometry.
"""
def render(self, name, value, attrs=None):
def get_context(self, name, value, attrs=None):
# Update the template parameters with any attributes passed in.
if attrs:
self.params.update(attrs)
@ -77,7 +76,7 @@ class OpenLayersWidget(Textarea):
self.params['wkt'] = wkt
self.params.update(geo_context)
return loader.render_to_string(self.template, self.params)
return self.params
def map_options(self):
"Builds the map options hash for the OpenLayers template."

View File

@ -6,7 +6,6 @@ from django.conf import settings
from django.contrib.gis import gdal
from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.forms.widgets import Widget
from django.template import loader
from django.utils import six, translation
logger = logging.getLogger('django.contrib.gis')
@ -43,7 +42,7 @@ class BaseGeometryWidget(Widget):
logger.error("Error creating geometry from value '%s' (%s)", value, err)
return None
def render(self, name, value, attrs=None):
def get_context(self, name, value, attrs=None):
# If a string reaches here (via a validation error on another
# field) then just reconstruct the Geometry.
if value and isinstance(value, six.string_types):
@ -62,16 +61,19 @@ class BaseGeometryWidget(Widget):
value.srid, self.map_srid, err
)
context = self.build_attrs(
attrs,
if attrs is None:
attrs = {}
context = self.build_attrs(self.attrs, dict(
name=name,
module='geodjango_%s' % name.replace('-', '_'), # JS-safe
serialized=self.serialize(value),
geom_type=gdal.OGRGeomType(self.attrs['geom_type']),
STATIC_URL=settings.STATIC_URL,
LANGUAGE_BIDI=translation.get_language_bidi(),
)
return loader.render_to_string(self.template_name, context)
**attrs
))
return context
class OpenLayersWidget(BaseGeometryWidget):

View File

@ -117,7 +117,7 @@ class SplitArrayWidget(forms.Widget):
id_ += '_0'
return id_
def render(self, name, value, attrs=None):
def render(self, name, value, attrs=None, renderer=None):
if self.is_localized:
self.widget.is_localized = self.is_localized
value = value or []
@ -131,7 +131,7 @@ class SplitArrayWidget(forms.Widget):
widget_value = None
if id_:
final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
output.append(self.widget.render(name + '_%s' % i, widget_value, final_attrs))
output.append(self.widget.render(name + '_%s' % i, widget_value, final_attrs, renderer))
return mark_safe(self.format_output(output))
def format_output(self, rendered_widgets):

View File

@ -1,13 +1,16 @@
from __future__ import unicode_literals
import datetime
import warnings
from django.forms.utils import flatatt, pretty_name
from django.forms.widgets import Textarea, TextInput
from django.utils import six
from django.utils.deprecation import RemovedInDjango21Warning
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html, html_safe
from django.utils.inspect import func_supports_parameter
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
@ -49,7 +52,10 @@ class BoundField(object):
id_ = self.field.widget.attrs.get('id') or self.auto_id
attrs = {'id': id_} if id_ else {}
attrs = self.build_widget_attrs(attrs)
return list(self.field.widget.subwidgets(self.html_name, self.value(), attrs))
return list(
BoundWidget(self.field.widget, widget, self.form.renderer)
for widget in self.field.widget.subwidgets(self.html_name, self.value(), attrs=attrs)
)
def __iter__(self):
return iter(self.subwidgets)
@ -97,7 +103,23 @@ class BoundField(object):
name = self.html_name
else:
name = self.html_initial_name
return force_text(widget.render(name, self.value(), attrs=attrs))
kwargs = {}
if func_supports_parameter(widget.render, 'renderer'):
kwargs['renderer'] = self.form.renderer
else:
warnings.warn(
'Add the `renderer` argument to the render() method of %s. '
'It will be mandatory in Django 2.1.' % widget.__class__,
RemovedInDjango21Warning, stacklevel=2,
)
html = widget.render(
name=name,
value=self.value(),
attrs=attrs,
**kwargs
)
return force_text(html)
def as_text(self, attrs=None, **kwargs):
"""
@ -230,3 +252,45 @@ class BoundField(object):
if self.field.disabled:
attrs['disabled'] = True
return attrs
@html_safe
@python_2_unicode_compatible
class BoundWidget(object):
"""
A container class used for iterating over widgets. This is useful for
widgets that have choices. For example, the following can be used in a
template:
{% for radio in myform.beatles %}
<label for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
<span class="radio">{{ radio.tag }}</span>
</label>
{% endfor %}
"""
def __init__(self, parent_widget, data, renderer):
self.parent_widget = parent_widget
self.data = data
self.renderer = renderer
def __str__(self):
return self.tag(wrap_label=True)
def tag(self, wrap_label=False):
context = {'widget': self.data, 'wrap_label': wrap_label}
return self.parent_widget._render(self.template_name, context, self.renderer)
@property
def template_name(self):
if 'template_name' in self.data:
return self.data['template_name']
return self.parent_widget.template_name
@property
def id_for_label(self):
return 'id_%s_%s' % (self.data['name'], self.data['index'])
@property
def choice_label(self):
return self.data['label']

View File

@ -21,6 +21,8 @@ from django.utils.html import conditional_escape, html_safe
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from .renderers import get_default_renderer
__all__ = ('BaseForm', 'Form')
@ -65,13 +67,14 @@ class BaseForm(object):
# class is different than Form. See the comments by the Form class for more
# information. Any improvements to the form API should be made to *this*
# class, not to the Form class.
default_renderer = None
field_order = None
prefix = None
use_required_attribute = True
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
empty_permitted=False, field_order=None, use_required_attribute=None):
empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None):
self.is_bound = data is not None or files is not None
self.data = data or {}
self.files = files or {}
@ -97,6 +100,17 @@ class BaseForm(object):
if use_required_attribute is not None:
self.use_required_attribute = use_required_attribute
# Initialize form renderer. Use a global default if not specified
# either as an argument or as self.default_renderer.
if renderer is None:
if self.default_renderer is None:
renderer = get_default_renderer()
else:
renderer = self.default_renderer
if isinstance(self.default_renderer, type):
renderer = renderer()
self.renderer = renderer
def order_fields(self, field_order):
"""
Rearranges the fields according to field_order.

View File

@ -0,0 +1 @@
{% for name, value in widget.attrs.items() %} {{ name }}{% if not value is sameas True %}="{{ value }}"{% endif %}{% endfor %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input_option.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/multiple_input.html" %}

View File

@ -0,0 +1,5 @@
{% if is_initial %}{{ initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
<input type="checkbox" name="{{ checkbox_name }}" id="{{ checkbox_id }}" />
<label for="{{ checkbox_id }}">{{ clear_checkbox_label }}</label>{% endif %}<br />
{{ input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None and widget.value != "" %} value="{{ widget.value }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} />

View File

@ -0,0 +1 @@
{% if wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if wrap_label %} {{ widget.label }}</label>{% endif %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/multiwidget.html" %}

View File

@ -0,0 +1,5 @@
{% set id = widget.attrs.id %}<ul{% if id %} id="{{ id }}"{% endif %}>{% for group, options, index in widget.optgroups %}{% if group %}
<li>{{ group }}<ul{% if id %} id="{{ id }}_{{ index }}{% endif %}">{% endif %}{% for widget in options %}
<li>{% include widget.template_name %}</li>{% endfor %}{% if group %}
</ul></li>{% endif %}{% endfor %}
</ul>

View File

@ -0,0 +1 @@
{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/multiple_input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input_option.html" %}

View File

@ -0,0 +1,5 @@
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
<optgroup label="{{ group_name }}">{% endif %}{% for widget in group_choices %}
{% include widget.template_name %}{% endfor %}{% if group_name %}
</optgroup>{% endif %}{% endfor %}
</select>

View File

@ -0,0 +1 @@
{% include 'django/forms/widgets/multiwidget.html' %}

View File

@ -0,0 +1 @@
<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}>{{ widget.label }}</option>

View File

@ -0,0 +1 @@
{% include 'django/forms/widgets/multiwidget.html' %}

View File

@ -0,0 +1 @@
{% include 'django/forms/widgets/multiwidget.html' %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1,2 @@
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
{% if widget.value %}{{ widget.value }}{% endif %}</textarea>

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

71
django/forms/renderers.py Normal file
View File

@ -0,0 +1,71 @@
import os
from django.conf import settings
from django.template.backends.django import DjangoTemplates
from django.template.loader import get_template
from django.utils import lru_cache
from django.utils._os import upath
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
try:
from django.template.backends.jinja2 import Jinja2
except ImportError:
def Jinja2(params):
raise ImportError("jinja2 isn't installed")
ROOT = upath(os.path.dirname(__file__))
@lru_cache.lru_cache()
def get_default_renderer():
renderer_class = import_string(settings.FORM_RENDERER)
return renderer_class()
class BaseRenderer(object):
def get_template(self, template_name):
raise NotImplementedError('subclasses must implement get_template()')
def render(self, template_name, context, request=None):
template = self.get_template(template_name)
return template.render(context, request=request).strip()
class EngineMixin(object):
def get_template(self, template_name):
return self.engine.get_template(template_name)
@cached_property
def engine(self):
return self.backend({
'APP_DIRS': True,
'DIRS': [os.path.join(ROOT, self.backend.app_dirname)],
'NAME': 'djangoforms',
'OPTIONS': {},
})
class DjangoTemplates(EngineMixin, BaseRenderer):
"""
Load Django templates from the built-in widget templates in
django/forms/templates and from apps' 'templates' directory.
"""
backend = DjangoTemplates
class Jinja2(EngineMixin, BaseRenderer):
"""
Load Jinja2 templates from the built-in widget templates in
django/forms/jinja2 and from apps' 'jinja2' directory.
"""
backend = Jinja2
class TemplatesSetting(BaseRenderer):
"""
Load templates using template.loader.get_template() which is configured
based on settings.TEMPLATES.
"""
def get_template(self, template_name):
return get_template(template_name)

View File

@ -0,0 +1 @@
{% for name, value in widget.attrs.items %} {{ name }}{% if not value is True %}="{{ value }}"{% endif %}{% endfor %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input_option.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/multiple_input.html" %}

View File

@ -0,0 +1,5 @@
{% if is_initial %}{{ initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
<input type="checkbox" name="{{ checkbox_name }}" id="{{ checkbox_id }}" />
<label for="{{ checkbox_id }}">{{ clear_checkbox_label }}</label>{% endif %}<br />
{{ input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None and widget.value != "" %} value="{{ widget.value }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} />

View File

@ -0,0 +1 @@
{% if wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if wrap_label %} {{ widget.label }}</label>{% endif %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/multiwidget.html" %}

View File

@ -0,0 +1,5 @@
{% with id=widget.attrs.id %}<ul{% if id %} id="{{ id }}"{% endif %}>{% for group, options, index in widget.optgroups %}{% if group %}
<li>{{ group }}<ul{% if id %} id="{{ id }}_{{ index }}{% endif %}">{% endif %}{% for option in options %}
<li>{% include option.template_name with widget=option %}</li>{% endfor %}{% if group %}
</ul></li>{% endif %}{% endfor %}
</ul>{% endwith %}

View File

@ -0,0 +1 @@
{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/multiple_input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input_option.html" %}

View File

@ -0,0 +1,5 @@
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
<optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
{% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
</optgroup>{% endif %}{% endfor %}
</select>

View File

@ -0,0 +1 @@
{% include 'django/forms/widgets/multiwidget.html' %}

View File

@ -0,0 +1 @@
<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}>{{ widget.label }}</option>

View File

@ -0,0 +1 @@
{% include 'django/forms/widgets/multiwidget.html' %}

View File

@ -0,0 +1 @@
{% include 'django/forms/widgets/multiwidget.html' %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1,2 @@
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
{% if widget.value %}{{ widget.value }}{% endif %}</textarea>

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

View File

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,6 @@ from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from .base import BaseEngine
from .utils import csrf_input_lazy, csrf_token_lazy
class Jinja2(BaseEngine):
@ -70,6 +69,7 @@ class Template(object):
)
def render(self, context=None, request=None):
from .utils import csrf_input_lazy, csrf_token_lazy
if context is None:
context = {}
if request is not None:

View File

@ -97,6 +97,8 @@ def reset_template_engines(**kwargs):
engines._engines = {}
from django.template.engine import Engine
Engine.get_default.cache_clear()
from django.forms.renderers import get_default_renderer
get_default_renderer.cache_clear()
@receiver(setting_changed)

View File

@ -51,6 +51,9 @@ details on these changes.
* Support for regular expression groups with ``iLmsu#`` in ``url()`` will be
removed.
* Support for ``Widget.render()`` methods without the ``renderer`` argument
will be removed.
.. _deprecation-removed-in-2.0:
2.0

View File

@ -720,6 +720,29 @@ When set to ``True`` (the default), required form fields will have the
``use_required_attribute=False`` to avoid incorrect browser validation when
adding and deleting forms from a formset.
Configuring the rendering of a form's widgets
---------------------------------------------
.. attribute:: Form.default_renderer
.. versionadded:: 1.11
Specifies the :doc:`renderer <renderers>` to use for the form. Defaults to
``None`` which means to use the default renderer specified by the
:setting:`FORM_RENDERER` setting.
You can set this as a class attribute when declaring your form or use the
``renderer`` argument to ``Form.__init__()``. For example::
from django import forms
class MyForm(forms.Form):
default_renderer = MyRenderer()
or::
form = MyForm(renderer=MyRenderer())
Notes on field ordering
-----------------------

View File

@ -12,5 +12,6 @@ Detailed form API reference. For introductory material, see the
fields
models
formsets
renderers
widgets
validation

View File

@ -0,0 +1,131 @@
======================
The form rendering API
======================
.. module:: django.forms.renderers
:synopsis: Built-in form renderers.
.. versionadded:: 1.11
In older versions, widgets are rendered using Python. All APIs described
in this document are new.
Django's form widgets are rendered using Django's :doc:`template engines
system </topics/templates>`.
The form rendering process can be customized at several levels:
* Widgets can specify custom template names.
* Forms and widgets can specify custom renderer classes.
* A widget's template can be overridden by a project. (Reusable applications
typically shouldn't override built-in templates because they might conflict
with a project's custom templates.)
.. _low-level-widget-render-api:
The low-level render API
========================
The rendering of form templates is controlled by a customizable renderer class.
A custom renderer can be specified by updating the :setting:`FORM_RENDERER`
setting. It defaults to
``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``.
You can also provide a custom renderer by setting the
:attr:`.Form.default_renderer` attribute or by using the ``renderer`` argument
of :meth:`.Widget.render`.
Use one of the :ref:`built-in template form renderers
<built-in-template-form-renderers>` or implement your own. Custom renderers
must implement a ``render(template_name, context, request=None)`` method. It
should return a rendered templates (as a string) or raise
:exc:`~django.template.TemplateDoesNotExist`.
.. _built-in-template-form-renderers:
Built-in-template form renderers
================================
``DjangoTemplates``
-------------------
.. class:: DjangoTemplates
This renderer uses a standalone
:class:`~django.template.backends.django.DjangoTemplates`
engine (unconnected to what you might have configured in the
:setting:`TEMPLATES` setting). It loads templates first from the built-in form
templates directory in ``django/forms/templates`` and then from the installed
apps' templates directories using the :class:`app_directories
<django.template.loaders.app_directories.Loader>` loader.
If you want to render templates with customizations from your
:setting:`TEMPLATES` setting, such as context processors for example, use the
:class:`TemplatesSetting` renderer.
``Jinja2``
----------
.. class:: Jinja2
This renderer is the same as the :class:`DjangoTemplates` renderer except that
it uses a :class:`~django.template.backends.jinja2.Jinja2` backend. Templates
for the built-in widgets are located in ``django/forms/jinja2`` and installed
apps can provide templates in a ``jinja2`` directory.
To use this backend, all the widgets in your project and its third-party apps
must have Jinja2 templates. Unless you provide your own Jinja2 templates for
widgets that don't have any, you can't use this renderer. For example,
:mod:`django.contrib.admin` doesn't include Jinja2 templates for its widgets
due to their usage of Django template tags.
``TemplatesSetting``
--------------------
.. class:: TemplatesSetting
This renderer gives you complete control of how widget templates are sourced.
It uses :func:`~django.template.loader.get_template` to find widget
templates based on what's configured in the :setting:`TEMPLATES` setting.
Using this renderer along with the built-in widget templates requires either:
#. ``'django.forms'`` in :setting:`INSTALLED_APPS` and at least one engine
with :setting:`APP_DIRS=True <TEMPLATES-APP_DIRS>`.
#. Adding the built-in widgets templates directory (``django/forms/templates``
or ``django/forms/jinja2``) in :setting:`DIRS <TEMPLATES-DIRS>` of one of
your template engines.
Using this renderer requires you to make sure the form templates your project
needs can be located.
Context available in widget templates
=====================================
Widget templates receive a context from :meth:`.Widget.get_context`. By
default, widgets receive a single value in the context, ``widget``. This is a
dictionary that contains values like:
* ``name``
* ``value``
* ``attrs``
* ``is_hidden``
* ``template_name``
Some widgets add further information to the context. For instance, all widgets
that subclass ``Input`` defines ``widget['type']`` and :class:`.MultiWidget`
defines ``widget['subwidgets']`` for looping purposes.
Overriding built-in widget templates
====================================
Each widget has a ``template_name`` attribute with a value such as
``input.html``. Built-in widget templates are stored in the
``django/forms/widgets`` path. You can provide a custom template for
``input.html`` by defining ``django/forms/widgets/input.html``, for example.
See :ref:`built-in widgets` for the name of each widget's template.
If you use the :class:`TemplatesSetting` renderer, overriding widget templates
works the same as overriding any other template in your project. You can't
override built-in widget templates using the other built-in renderers.

View File

@ -241,6 +241,28 @@ foundation for custom widgets.
In older versions, this method is a private API named
``_format_value()``. The old name will work until Django 2.0.
.. method:: get_context(name, value, attrs=None)
.. versionadded:: 1.11
Returns a dictionary of values to use when rendering the widget
template. By default, the dictionary contains a single key,
``'widget'``, which is a dictionary representation of the widget
containing the following keys:
* ``'name'``: The name of the field from the ``name`` argument.
* ``'is_hidden'``: A boolean indicating whether or not this widget is
hidden.
* ``'required'``: A boolean indicating whether or not the field for
this widget is required.
* ``'value'``: The value as returned by :meth:`format_value`.
* ``'attrs'``: HTML attributes to be set on the rendered widget. The
combination of the :attr:`attrs` attribute and the ``attrs`` argument.
* ``'template_name'``: The value of ``self.template_name``.
``Widget`` subclasses can provide custom context values by overriding
this method.
.. method:: id_for_label(self, id_)
Returns the HTML ID attribute of this widget for use by a ``<label>``,
@ -251,14 +273,16 @@ foundation for custom widgets.
return an ID value that corresponds to the first ID in the widget's
tags.
.. method:: render(name, value, attrs=None)
.. method:: render(name, value, attrs=None, renderer=None)
Returns HTML for the widget, as a Unicode string. This method must be
implemented by the subclass, otherwise ``NotImplementedError`` will be
raised.
Renders a widget to HTML using the given renderer. If ``renderer`` is
``None``, the renderer from the :setting:`FORM_RENDERER` setting is
used.
The 'value' given is not guaranteed to be valid input, therefore
subclass implementations should program defensively.
.. versionchanged:: 1.11
The ``renderer`` argument was added. Support for subclasses that
don't accept it will be removed in Django 2.1.
.. method:: value_from_datadict(data, files, name)
@ -360,40 +384,21 @@ foundation for custom widgets.
with the opposite responsibility - to combine cleaned values of
all member fields into one.
Other methods that may be useful to override include:
It provides some custom context:
.. method:: render(name, value, attrs=None)
.. method:: get_context(name, value, attrs=None)
Argument ``value`` is handled differently in this method from the
subclasses of :class:`~Widget` because it has to figure out how to
split a single value for display in multiple widgets.
In addition to the ``'widget'`` key described in
:meth:`Widget.get_context`, ``MultiValueWidget`` adds a
``widget['subwidgets']`` key.
The ``value`` argument used when rendering can be one of two things:
These can be looped over in the widget template:
* A ``list``.
* A single value (e.g., a string) that is the "compressed" representation
of a ``list`` of values.
.. code-block:: html+django
If ``value`` is a list, the output of :meth:`~MultiWidget.render` will
be a concatenation of rendered child widgets. If ``value`` is not a
list, it will first be processed by the method
:meth:`~MultiWidget.decompress()` to create the list and then rendered.
When ``render()`` executes its HTML rendering, each value in the list
is rendered with the corresponding widget -- the first value is
rendered in the first widget, the second value is rendered in the
second widget, etc.
Unlike in the single value widgets, method :meth:`~MultiWidget.render`
need not be implemented in the subclasses.
.. method:: format_output(rendered_widgets)
Given a list of rendered widgets (as strings), returns a Unicode string
representing the HTML for the whole lot.
This hook allows you to format the HTML design of the widgets any way
you'd like.
{% for subwidget in widget.subwidgets %}
{% include widget.template_name with widget=subwidget %}
{% endfor %}
Here's an example widget which subclasses :class:`MultiWidget` to display
a date with the day, month, and year in different select boxes. This widget
@ -421,9 +426,6 @@ foundation for custom widgets.
return [value.day, value.month, value.year]
return [None, None, None]
def format_output(self, rendered_widgets):
return ''.join(rendered_widgets)
def value_from_datadict(self, data, files, name):
datelist = [
widget.value_from_datadict(data, files, name + '_%s' % i)
@ -442,11 +444,6 @@ foundation for custom widgets.
The constructor creates several :class:`Select` widgets in a tuple. The
``super`` class uses this tuple to setup the widget.
The :meth:`~MultiWidget.format_output` method is fairly vanilla here (in
fact, it's the same as what's been implemented as the default for
``MultiWidget``), but the idea is that you could add custom HTML between
the widgets should you wish.
The required method :meth:`~MultiWidget.decompress` breaks up a
``datetime.date`` value into the day, month, and year values corresponding
to each widget. Note how the method handles the case where ``value`` is
@ -485,14 +482,18 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: TextInput
Text input: ``<input type="text" ...>``
* ``input_type``: ``'text'``
* ``template_name``: ``'django/forms/widgets/text.html'``
* Renders as: ``<input type="text" ...>``
``NumberInput``
~~~~~~~~~~~~~~~
.. class:: NumberInput
Text input: ``<input type="number" ...>``
* ``input_type``: ``'number'``
* ``template_name``: ``'django/forms/widgets/number.html'``
* Renders as: ``<input type="number" ...>``
Beware that not all browsers support entering localized numbers in
``number`` input types. Django itself avoids using them for fields having
@ -503,21 +504,27 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: EmailInput
Text input: ``<input type="email" ...>``
* ``input_type``: ``'email'``
* ``template_name``: ``'django/forms/widgets/email.html'``
* Renders as: ``<input type="email" ...>``
``URLInput``
~~~~~~~~~~~~
.. class:: URLInput
Text input: ``<input type="url" ...>``
* ``input_type``: ``'url'``
* ``template_name``: ``'django/forms/widgets/url.html'``
* Renders as: ``<input type="url" ...>``
``PasswordInput``
~~~~~~~~~~~~~~~~~
.. class:: PasswordInput
Password input: ``<input type='password' ...>``
* ``input_type``: ``'password'``
* ``template_name``: ``'django/forms/widgets/password.html'``
* Renders as: ``<input type='password' ...>``
Takes one optional argument:
@ -531,7 +538,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: HiddenInput
Hidden input: ``<input type='hidden' ...>``
* ``input_type``: ``'hidden'``
* ``template_name``: ``'django/forms/widgets/hidden.html'``
* Renders as: ``<input type='hidden' ...>``
Note that there also is a :class:`MultipleHiddenInput` widget that
encapsulates a set of hidden input elements.
@ -541,7 +550,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: DateInput
Date input as a simple text box: ``<input type='text' ...>``
* ``input_type``: ``'text'``
* ``template_name``: ``'django/forms/widgets/date.html'``
* Renders as: ``<input type='text' ...>``
Takes same arguments as :class:`TextInput`, with one more optional argument:
@ -558,7 +569,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: DateTimeInput
Date/time input as a simple text box: ``<input type='text' ...>``
* ``input_type``: ``'text'``
* ``template_name``: ``'django/forms/widgets/datetime.html'``
* Renders as: ``<input type='text' ...>``
Takes same arguments as :class:`TextInput`, with one more optional argument:
@ -579,7 +592,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: TimeInput
Time input as a simple text box: ``<input type='text' ...>``
* ``input_type``: ``'text'``
* ``template_name``: ``'django/forms/widgets/time.html'``
* Renders as: ``<input type='text' ...>``
Takes same arguments as :class:`TextInput`, with one more optional argument:
@ -598,7 +613,8 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: Textarea
Text area: ``<textarea>...</textarea>``
* ``template_name``: ``'django/forms/widgets/textarea.html'``
* Renders as: ``<textarea>...</textarea>``
.. _selector-widgets:
@ -610,7 +626,9 @@ Selector and checkbox widgets
.. class:: CheckboxInput
Checkbox: ``<input type='checkbox' ...>``
* ``input_type``: ``'checkbox'``
* ``template_name``: ``'django/forms/widgets/checkbox.html'``
* Renders as: ``<input type='checkbox' ...>``
Takes one optional argument:
@ -624,7 +642,8 @@ Selector and checkbox widgets
.. class:: Select
Select widget: ``<select><option ...>...</select>``
* ``template_name``: ``'django/forms/widgets/select.html'``
* Renders as: ``<select><option ...>...</select>``
.. attribute:: Select.choices
@ -637,6 +656,8 @@ Selector and checkbox widgets
.. class:: NullBooleanSelect
* ``template_name``: ``'django/forms/widgets/select.html'``
Select widget with options 'Unknown', 'Yes' and 'No'
``SelectMultiple``
@ -644,6 +665,8 @@ Selector and checkbox widgets
.. class:: SelectMultiple
* ``template_name``: ``'django/forms/widgets/select.html'``
Similar to :class:`Select`, but allows multiple selection:
``<select multiple='multiple'>...</select>``
@ -652,6 +675,8 @@ Selector and checkbox widgets
.. class:: RadioSelect
* ``template_name``: ``'django/forms/widgets/radio.html'``
Similar to :class:`Select`, but rendered as a list of radio buttons within
``<li>`` tags:
@ -744,6 +769,8 @@ Selector and checkbox widgets
.. class:: CheckboxSelectMultiple
* ``template_name``: ``'django/forms/widgets/checkbox_select.html'``
Similar to :class:`SelectMultiple`, but rendered as a list of check
buttons:
@ -776,16 +803,18 @@ File upload widgets
.. class:: FileInput
File upload input: ``<input type='file' ...>``
* ``template_name``: ``'django/forms/widgets/file.html'``
* Renders as: ``<input type='file' ...>``
``ClearableFileInput``
~~~~~~~~~~~~~~~~~~~~~~
.. class:: ClearableFileInput
File upload input: ``<input type='file' ...>``, with an additional checkbox
input to clear the field's value, if the field is not required and has
initial data.
* ``template_name``: ``'django/forms/widgets/clearable_file_input.html'``
* Renders as: ``<input type='file' ...>`` with an additional checkbox
input to clear the field's value, if the field is not required and has
initial data.
.. _composite-widgets:
@ -797,7 +826,8 @@ Composite widgets
.. class:: MultipleHiddenInput
Multiple ``<input type='hidden' ...>`` widgets.
* ``template_name``: ``'django/forms/widgets/multiple_hidden.html'``
* Renders as: multiple ``<input type='hidden' ...>`` tags
A widget that handles multiple hidden widgets for fields that have a list
of values.
@ -813,6 +843,8 @@ Composite widgets
.. class:: SplitDateTimeWidget
* ``template_name``: ``'django/forms/widgets/splitdatetime.html'``
Wrapper (using :class:`MultiWidget`) around two widgets: :class:`DateInput`
for the date, and :class:`TimeInput` for the time. Must be used with
:class:`SplitDateTimeField` rather than :class:`DateTimeField`.
@ -832,6 +864,8 @@ Composite widgets
.. class:: SplitHiddenDateTimeWidget
* ``template_name``: ``'django/forms/widgets/splithiddendatetime.html'``
Similar to :class:`SplitDateTimeWidget`, but uses :class:`HiddenInput` for
both date and time.
@ -840,6 +874,8 @@ Composite widgets
.. class:: SelectDateWidget
* ``template_name``: ``'django/forms/widgets/select_date.html'``
Wrapper around three :class:`~django.forms.Select` widgets: one each for
month, day, and year.

View File

@ -1517,6 +1517,18 @@ generate correct URLs when ``SCRIPT_NAME`` is not ``/``.
The setting's use in :func:`django.setup()` was added.
.. setting:: FORM_RENDERER
``FORM_RENDERER``
-----------------
.. versionadded:: 1.11
Default: ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``
The class that renders form widgets. It must implement :ref:`the low-level
render API <low-level-widget-render-api>`.
.. setting:: FORMAT_MODULE_PATH
``FORMAT_MODULE_PATH``
@ -3351,6 +3363,10 @@ File uploads
* :setting:`MEDIA_ROOT`
* :setting:`MEDIA_URL`
Forms
-----
* :setting:`FORM_RENDERER`
Globalization (``i18n``/``l10n``)
---------------------------------
* :setting:`DATE_FORMAT`

View File

@ -61,6 +61,15 @@ It can be subclassed to support different index types, such as
:class:`~django.contrib.postgres.indexes.GinIndex`. It also allows defining the
order (ASC/DESC) for the columns of the index.
Template-based widget rendering
-------------------------------
To ease customizing widgets, form widget rendering is now done using the
template system rather than in Python. See :doc:`/ref/forms/renderers`.
You may need to adjust any custom widgets that you've written for a few
:ref:`backwards incompatible changes <template-widget-incompatibilities-1-11>`.
Minor features
--------------
@ -551,6 +560,21 @@ inside help text.
Read-only fields are wrapped in ``<div class="readonly">...</div>`` instead of
``<p>...</p>`` to allow any kind of HTML as the field's content.
.. _template-widget-incompatibilities-1-11:
Changes due to the introduction of template-based widget rendering
------------------------------------------------------------------
Some undocumented classes in ``django.forms.widgets`` are removed:
* ``SubWidget``
* ``RendererMixin``, ``ChoiceFieldRenderer``, ``RadioFieldRenderer``,
``CheckboxFieldRenderer``
* ``ChoiceInput``, ``RadioChoiceInput``, ``CheckboxChoiceInput``
The ``Widget.format_output()`` method is removed. Use a custom widget template
instead.
Miscellaneous
-------------
@ -754,3 +778,7 @@ Miscellaneous
entries for search engines, for example. An alternative solution could be to
create a :data:`~django.conf.urls.handler404` that looks for uppercase
characters in the URL and redirects to a lowercase equivalent.
* The ``renderer`` argument is added to the :meth:`Widget.render()
<django.forms.Widget.render>` method. Methods that don't accept that argument
will work through a deprecation period.

View File

@ -673,6 +673,7 @@ releasers
reloader
removetags
renderer
renderers
repo
reportable
reprojection

View File

@ -184,9 +184,11 @@ class TestInline(TestDataMixin, TestCase):
SomeChildModel.objects.create(name='c', position='1', parent=parent)
response = self.client.get(reverse('admin:admin_inlines_someparentmodel_change', args=(parent.pk,)))
self.assertNotContains(response, '<td class="field-position">')
self.assertContains(response, (
self.assertInHTML(
'<input id="id_somechildmodel_set-1-position" '
'name="somechildmodel_set-1-position" type="hidden" value="1" />'))
'name="somechildmodel_set-1-position" type="hidden" value="1" />',
response.rendered_content,
)
def test_non_related_name_inline(self):
"""
@ -273,12 +275,12 @@ class TestInline(TestDataMixin, TestCase):
'name="binarytree_set-TOTAL_FORMS" type="hidden" value="2" />'
)
response = self.client.get(reverse('admin:admin_inlines_binarytree_add'))
self.assertContains(response, max_forms_input % 3)
self.assertContains(response, total_forms_hidden)
self.assertInHTML(max_forms_input % 3, response.rendered_content)
self.assertInHTML(total_forms_hidden, response.rendered_content)
response = self.client.get(reverse('admin:admin_inlines_binarytree_change', args=(bt_head.id,)))
self.assertContains(response, max_forms_input % 2)
self.assertContains(response, total_forms_hidden)
self.assertInHTML(max_forms_input % 2, response.rendered_content)
self.assertInHTML(total_forms_hidden, response.rendered_content)
def test_min_num(self):
"""
@ -302,8 +304,8 @@ class TestInline(TestDataMixin, TestCase):
request = self.factory.get(reverse('admin:admin_inlines_binarytree_add'))
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request)
self.assertContains(response, min_forms)
self.assertContains(response, total_forms)
self.assertInHTML(min_forms, response.rendered_content)
self.assertInHTML(total_forms, response.rendered_content)
def test_custom_min_num(self):
bt_head = BinaryTree.objects.create(name="Tree Head")
@ -331,14 +333,14 @@ class TestInline(TestDataMixin, TestCase):
request = self.factory.get(reverse('admin:admin_inlines_binarytree_add'))
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request)
self.assertContains(response, min_forms % 2)
self.assertContains(response, total_forms % 5)
self.assertInHTML(min_forms % 2, response.rendered_content)
self.assertInHTML(total_forms % 5, response.rendered_content)
request = self.factory.get(reverse('admin:admin_inlines_binarytree_change', args=(bt_head.id,)))
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request, object_id=str(bt_head.id))
self.assertContains(response, min_forms % 5)
self.assertContains(response, total_forms % 8)
self.assertInHTML(min_forms % 5, response.rendered_content)
self.assertInHTML(total_forms % 8, response.rendered_content)
def test_inline_nonauto_noneditable_pk(self):
response = self.client.get(reverse('admin:admin_inlines_author_add'))

View File

@ -5980,7 +5980,7 @@ class AdminViewOnSiteTests(TestCase):
response, 'inline_admin_formset', 0, None,
['Children must share a family name with their parents in this contrived test case']
)
msg = "The formset 'inline_admin_formset' in context 4 does not contain any non-form errors."
msg = "The formset 'inline_admin_formset' in context 10 does not contain any non-form errors."
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormsetError(response, 'inline_admin_formset', None, None, ['Error'])

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
import gettext
import os
import re
from datetime import datetime, timedelta
from importlib import import_module
@ -354,34 +355,53 @@ class AdminURLWidgetTest(SimpleTestCase):
)
def test_render_quoting(self):
# WARNING: Don't use assertHTMLEqual in that testcase!
# assertHTMLEqual will get rid of some escapes which are tested here!
"""
WARNING: This test doesn't use assertHTMLEqual since it will get rid
of some escapes which are tested here!
"""
HREF_RE = re.compile('href="([^"]+)"')
VALUE_RE = re.compile('value="([^"]+)"')
TEXT_RE = re.compile('<a[^>]+>([^>]+)</a>')
w = widgets.AdminURLFieldWidget()
output = w.render('test', 'http://example.com/<sometag>some text</sometag>')
self.assertEqual(
w.render('test', 'http://example.com/<sometag>some text</sometag>'),
'<p class="url">Currently: '
'<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">'
'http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />'
'Change: <input class="vURLField" name="test" type="url" '
'value="http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;" /></p>'
HREF_RE.search(output).groups()[0],
'http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E',
)
self.assertEqual(
w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>'),
'<p class="url">Currently: '
'<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">'
'http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />'
'Change: <input class="vURLField" name="test" type="url" '
'value="http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;" /></p>'
TEXT_RE.search(output).groups()[0],
'http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;',
)
self.assertEqual(
w.render('test', 'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"'),
'<p class="url">Currently: '
'<a href="http://www.example.com/%C3%A4%22%3E%3Cscript%3Ealert(%22XSS!%22)%3C/script%3E%22">'
VALUE_RE.search(output).groups()[0],
'http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;',
)
output = w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>')
self.assertEqual(
HREF_RE.search(output).groups()[0],
'http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E',
)
self.assertEqual(
TEXT_RE.search(output).groups()[0],
'http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;',
)
self.assertEqual(
VALUE_RE.search(output).groups()[0],
'http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;',
)
output = w.render('test', 'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"')
self.assertEqual(
HREF_RE.search(output).groups()[0],
'http://www.example.com/%C3%A4%22%3E%3Cscript%3Ealert(%22XSS!%22)%3C/script%3E%22',
)
self.assertEqual(
TEXT_RE.search(output).groups()[0],
'http://www.example.com/%C3%A4&quot;&gt;&lt;script&gt;'
'alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;</a><br />'
'Change: <input class="vURLField" name="test" type="url" '
'value="http://www.example.com/%C3%A4&quot;&gt;&lt;script&gt;'
'alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;" /></p>'
'alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;'
)
self.assertEqual(
VALUE_RE.search(output).groups()[0],
'http://www.example.com/%C3%A4&quot;&gt;&lt;script&gt;alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;',
)

View File

@ -39,6 +39,7 @@ class FilePathFieldTest(SimpleTestCase):
('/django/forms/forms.py', 'forms.py'),
('/django/forms/formsets.py', 'formsets.py'),
('/django/forms/models.py', 'models.py'),
('/django/forms/renderers.py', 'renderers.py'),
('/django/forms/utils.py', 'utils.py'),
('/django/forms/widgets.py', 'widgets.py')
]
@ -62,6 +63,7 @@ class FilePathFieldTest(SimpleTestCase):
('/django/forms/forms.py', 'forms.py'),
('/django/forms/formsets.py', 'formsets.py'),
('/django/forms/models.py', 'models.py'),
('/django/forms/renderers.py', 'renderers.py'),
('/django/forms/utils.py', 'utils.py'),
('/django/forms/widgets.py', 'widgets.py')
]
@ -83,6 +85,7 @@ class FilePathFieldTest(SimpleTestCase):
('/django/forms/forms.py', 'forms.py'),
('/django/forms/formsets.py', 'formsets.py'),
('/django/forms/models.py', 'models.py'),
('/django/forms/renderers.py', 'renderers.py'),
('/django/forms/utils.py', 'utils.py'),
('/django/forms/widgets.py', 'widgets.py')
]

View File

@ -0,0 +1 @@
<input type="text" name="custom">

View File

@ -0,0 +1 @@
<input type="text" name="custom">

View File

@ -17,6 +17,7 @@ from django.forms import (
SplitDateTimeField, SplitHiddenDateTimeWidget, Textarea, TextInput,
TimeField, ValidationError, forms,
)
from django.forms.renderers import DjangoTemplates, get_default_renderer
from django.forms.utils import ErrorList
from django.http import QueryDict
from django.template import Context, Template
@ -678,6 +679,50 @@ Java</label></li>
<div><label><input type="radio" name="name" value="ringo" required /> Ringo</label></div>"""
)
def test_form_with_iterable_boundfield_id(self):
class BeatleForm(Form):
name = ChoiceField(
choices=[('john', 'John'), ('paul', 'Paul'), ('george', 'George'), ('ringo', 'Ringo')],
widget=RadioSelect,
)
fields = list(BeatleForm()['name'])
self.assertEqual(len(fields), 4)
self.assertEqual(fields[0].id_for_label, 'id_name_0')
self.assertEqual(fields[0].choice_label, 'John')
self.assertHTMLEqual(
fields[0].tag(),
'<input type="radio" name="name" value="john" id="id_name_0" required />'
)
self.assertHTMLEqual(
str(fields[0]),
'<label for="id_name_0"><input type="radio" name="name" '
'value="john" id="id_name_0" required /> John</label>'
)
self.assertEqual(fields[1].id_for_label, 'id_name_1')
self.assertEqual(fields[1].choice_label, 'Paul')
self.assertHTMLEqual(
fields[1].tag(),
'<input type="radio" name="name" value="paul" id="id_name_1" required />'
)
self.assertHTMLEqual(
str(fields[1]),
'<label for="id_name_1"><input type="radio" name="name" '
'value="paul" id="id_name_1" required /> Paul</label>'
)
def test_iterable_boundfield_select(self):
class BeatleForm(Form):
name = ChoiceField(choices=[('john', 'John'), ('paul', 'Paul'), ('george', 'George'), ('ringo', 'Ringo')])
fields = list(BeatleForm(auto_id=False)['name'])
self.assertEqual(len(fields), 4)
self.assertEqual(fields[0].id_for_label, 'id_name_0')
self.assertEqual(fields[0].choice_label, 'John')
self.assertHTMLEqual(fields[0].tag(), '<option value="john">John</option>')
self.assertHTMLEqual(str(fields[0]), '<option value="john">John</option>')
def test_form_with_noniterable_boundfield(self):
# You can iterate over any BoundField, not just those with widget=RadioSelect.
class BeatleForm(Form):
@ -1993,8 +2038,9 @@ Password: <input type="password" name="password" required /></li>
doesn't lose it's safe string status (#22950).
"""
class CustomWidget(TextInput):
def render(self, name, value, attrs=None):
return format_html(str('<input{} required />'), ' id=custom')
def render(self, name, value, attrs=None, choices=None,
renderer=None, extra_context=None):
return format_html(str('<input{} />'), ' id=custom')
class SampleForm(Form):
name = CharField(widget=CustomWidget)
@ -3573,3 +3619,46 @@ Good luck picking a username that doesn&#39;t already exist.</p>
f = DataForm({'data': 'xyzzy'})
self.assertTrue(f.is_valid())
self.assertEqual(f.cleaned_data, {'data': 'xyzzy'})
class CustomRenderer(DjangoTemplates):
pass
class RendererTests(SimpleTestCase):
def test_default(self):
form = Form()
self.assertEqual(form.renderer, get_default_renderer())
def test_kwarg_instance(self):
custom = CustomRenderer()
form = Form(renderer=custom)
self.assertEqual(form.renderer, custom)
def test_kwarg_class(self):
custom = CustomRenderer()
form = Form(renderer=custom)
self.assertEqual(form.renderer, custom)
def test_attribute_instance(self):
class CustomForm(Form):
default_renderer = DjangoTemplates()
form = CustomForm()
self.assertEqual(form.renderer, CustomForm.default_renderer)
def test_attribute_class(self):
class CustomForm(Form):
default_renderer = CustomRenderer
form = CustomForm()
self.assertTrue(isinstance(form.renderer, CustomForm.default_renderer))
def test_attribute_override(self):
class CustomForm(Form):
default_renderer = DjangoTemplates()
custom = CustomRenderer()
form = CustomForm(renderer=custom)
self.assertEqual(form.renderer, custom)

View File

@ -0,0 +1,52 @@
import os
import unittest
from django.forms.renderers import (
BaseRenderer, DjangoTemplates, Jinja2, TemplatesSetting,
)
from django.test import SimpleTestCase
from django.utils._os import upath
try:
import jinja2
except ImportError:
jinja2 = None
class SharedTests(object):
expected_widget_dir = 'templates'
def test_installed_apps_template_found(self):
"""Can find a custom template in INSTALLED_APPS."""
renderer = self.renderer()
# Found because forms_tests is .
tpl = renderer.get_template('forms_tests/custom_widget.html')
expected_path = os.path.abspath(
os.path.join(
upath(os.path.dirname(__file__)),
'..',
self.expected_widget_dir + '/forms_tests/custom_widget.html',
)
)
self.assertEqual(tpl.origin.name, expected_path)
class BaseTemplateRendererTests(SimpleTestCase):
def test_get_renderer(self):
with self.assertRaisesMessage(NotImplementedError, 'subclasses must implement get_template()'):
BaseRenderer().get_template('')
class DjangoTemplatesTests(SharedTests, SimpleTestCase):
renderer = DjangoTemplates
@unittest.skipIf(jinja2 is None, 'jinja2 required')
class Jinja2Tests(SharedTests, SimpleTestCase):
renderer = Jinja2
expected_widget_dir = 'jinja2'
class TemplatesSettingTests(SharedTests, SimpleTestCase):
renderer = TemplatesSetting

View File

@ -1,182 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib.admin.tests import AdminSeleniumTestCase
from django.forms import (
CheckboxSelectMultiple, ClearableFileInput, RadioSelect, TextInput,
)
from django.forms.widgets import (
ChoiceFieldRenderer, ChoiceInput, RadioFieldRenderer,
)
from django.test import SimpleTestCase, override_settings
from django.test import override_settings
from django.urls import reverse
from django.utils import six
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.safestring import SafeData
from ..models import Article
class FormsWidgetTests(SimpleTestCase):
def test_radiofieldrenderer(self):
# RadioSelect uses a RadioFieldRenderer to render the individual radio inputs.
# You can manipulate that object directly to customize the way the RadioSelect
# is rendered.
w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
r = w.get_renderer('beatle', 'J')
inp_set1 = []
inp_set2 = []
inp_set3 = []
inp_set4 = []
for inp in r:
inp_set1.append(str(inp))
inp_set2.append('%s<br />' % inp)
inp_set3.append('<p>%s %s</p>' % (inp.tag(), inp.choice_label))
inp_set4.append(
'%s %s %s %s %s' % (
inp.name,
inp.value,
inp.choice_value,
inp.choice_label,
inp.is_checked(),
)
)
self.assertHTMLEqual('\n'.join(inp_set1), """<label><input checked type="radio" name="beatle" value="J" /> John</label>
<label><input type="radio" name="beatle" value="P" /> Paul</label>
<label><input type="radio" name="beatle" value="G" /> George</label>
<label><input type="radio" name="beatle" value="R" /> Ringo</label>""")
self.assertHTMLEqual('\n'.join(inp_set2), """<label><input checked type="radio" name="beatle" value="J" /> John</label><br />
<label><input type="radio" name="beatle" value="P" /> Paul</label><br />
<label><input type="radio" name="beatle" value="G" /> George</label><br />
<label><input type="radio" name="beatle" value="R" /> Ringo</label><br />""")
self.assertHTMLEqual('\n'.join(inp_set3), """<p><input checked type="radio" name="beatle" value="J" /> John</p>
<p><input type="radio" name="beatle" value="P" /> Paul</p>
<p><input type="radio" name="beatle" value="G" /> George</p>
<p><input type="radio" name="beatle" value="R" /> Ringo</p>""")
self.assertHTMLEqual('\n'.join(inp_set4), """beatle J J John True
beatle J P Paul False
beatle J G George False
beatle J R Ringo False""")
# A RadioFieldRenderer object also allows index access to individual RadioChoiceInput
w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
r = w.get_renderer('beatle', 'J')
self.assertHTMLEqual(str(r[1]), '<label><input type="radio" name="beatle" value="P" /> Paul</label>')
self.assertHTMLEqual(
str(r[0]),
'<label><input checked type="radio" name="beatle" value="J" /> John</label>'
)
self.assertTrue(r[0].is_checked())
self.assertFalse(r[1].is_checked())
self.assertEqual((r[1].name, r[1].value, r[1].choice_value, r[1].choice_label), ('beatle', 'J', 'P', 'Paul'))
# These individual widgets can accept extra attributes if manually rendered.
self.assertHTMLEqual(
r[1].render(attrs={'extra': 'value'}),
'<label><input type="radio" extra="value" name="beatle" value="P" /> Paul</label>'
)
with self.assertRaises(IndexError):
r[10]
# You can create your own custom renderers for RadioSelect to use.
class MyRenderer(RadioFieldRenderer):
def render(self):
return '<br />\n'.join(six.text_type(choice) for choice in self)
w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')), renderer=MyRenderer)
self.assertHTMLEqual(
w.render('beatle', 'G'),
"""<label><input type="radio" name="beatle" value="J" /> John</label><br />
<label><input type="radio" name="beatle" value="P" /> Paul</label><br />
<label><input checked type="radio" name="beatle" value="G" /> George</label><br />
<label><input type="radio" name="beatle" value="R" /> Ringo</label>"""
)
# Or you can use custom RadioSelect fields that use your custom renderer.
class CustomRadioSelect(RadioSelect):
renderer = MyRenderer
w = CustomRadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
self.assertHTMLEqual(
w.render('beatle', 'G'),
"""<label><input type="radio" name="beatle" value="J" /> John</label><br />
<label><input type="radio" name="beatle" value="P" /> Paul</label><br />
<label><input checked type="radio" name="beatle" value="G" /> George</label><br />
<label><input type="radio" name="beatle" value="R" /> Ringo</label>"""
)
# You can customize rendering with outer_html/inner_html renderer variables (#22950)
class MyRenderer(RadioFieldRenderer):
# str is just to test some Python 2 issue with bytestrings
outer_html = str('<div{id_attr}>{content}</div>')
inner_html = '<p>{choice_value}{sub_widgets}</p>'
w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')), renderer=MyRenderer)
output = w.render('beatle', 'J', attrs={'id': 'bar'})
self.assertIsInstance(output, SafeData)
self.assertHTMLEqual(
output,
"""<div id="bar">
<p><label for="bar_0"><input checked type="radio" id="bar_0" value="J" name="beatle" /> John</label></p>
<p><label for="bar_1"><input type="radio" id="bar_1" value="P" name="beatle" /> Paul</label></p>
<p><label for="bar_2"><input type="radio" id="bar_2" value="G" name="beatle" /> George</label></p>
<p><label for="bar_3"><input type="radio" id="bar_3" value="R" name="beatle" /> Ringo</label></p>
</div>""")
def test_subwidget(self):
# Each subwidget tag gets a separate ID when the widget has an ID specified
self.assertHTMLEqual(
"\n".join(
c.tag() for c in CheckboxSelectMultiple(
attrs={'id': 'abc'},
choices=zip('abc', 'ABC')
).subwidgets('letters', list('ac'))
),
"""<input checked type="checkbox" name="letters" value="a" id="abc_0" />
<input type="checkbox" name="letters" value="b" id="abc_1" />
<input checked type="checkbox" name="letters" value="c" id="abc_2" />""")
# Each subwidget tag does not get an ID if the widget does not have an ID specified
self.assertHTMLEqual(
"\n".join(c.tag() for c in CheckboxSelectMultiple(
choices=zip('abc', 'ABC'),
).subwidgets('letters', list('ac'))),
"""<input checked type="checkbox" name="letters" value="a" />
<input type="checkbox" name="letters" value="b" />
<input checked type="checkbox" name="letters" value="c" />""")
# The id_for_label property of the subwidget should return the ID that is used on the subwidget's tag
self.assertHTMLEqual(
"\n".join(
'<input type="checkbox" name="letters" value="%s" id="%s" />'
% (c.choice_value, c.id_for_label) for c in CheckboxSelectMultiple(
attrs={'id': 'abc'},
choices=zip('abc', 'ABC'),
).subwidgets('letters', [])
),
"""<input type="checkbox" name="letters" value="a" id="abc_0" />
<input type="checkbox" name="letters" value="b" id="abc_1" />
<input type="checkbox" name="letters" value="c" id="abc_2" />""")
def test_sub_widget_html_safe(self):
widget = TextInput()
subwidget = next(widget.subwidgets('username', 'John Doe'))
self.assertTrue(hasattr(subwidget, '__html__'))
self.assertEqual(force_text(subwidget), subwidget.__html__())
def test_choice_input_html_safe(self):
widget = ChoiceInput('choices', 'CHOICE1', {}, ('CHOICE1', 'first choice'), 0)
self.assertTrue(hasattr(ChoiceInput, '__html__'))
self.assertEqual(force_text(widget), widget.__html__())
def test_choice_field_renderer_html_safe(self):
renderer = ChoiceFieldRenderer('choices', 'CHOICE1', {}, [('CHOICE1', 'first_choice')])
renderer.choice_input_class = lambda *args: args
self.assertTrue(hasattr(ChoiceFieldRenderer, '__html__'))
self.assertEqual(force_text(renderer), renderer.__html__())
@override_settings(ROOT_URLCONF='forms_tests.urls')
class LiveWidgetTests(AdminSeleniumTestCase):
@ -190,33 +20,4 @@ class LiveWidgetTests(AdminSeleniumTestCase):
self.selenium.get(self.live_server_url + reverse('article_form', args=[article.pk]))
self.selenium.find_element_by_id('submit').submit()
article = Article.objects.get(pk=article.pk)
# Should be "\nTst\n" after #19251 is fixed
self.assertEqual(article.content, "\r\nTst\r\n")
@python_2_unicode_compatible
class FakeFieldFile(object):
"""
Quacks like a FieldFile (has a .url and unicode representation), but
doesn't require us to care about storages etc.
"""
url = 'something'
def __str__(self):
return self.url
class ClearableFileInputTests(SimpleTestCase):
def test_render_custom_template(self):
widget = ClearableFileInput()
widget.template_with_initial = (
'%(initial_text)s: <img src="%(initial_url)s" alt="%(initial)s" /> '
'%(clear_template)s<br />%(input_text)s: %(input)s'
)
self.assertHTMLEqual(
widget.render('myfile', FakeFieldFile()),
'Currently: <img src="something" alt="something" /> '
'<input type="checkbox" name="myfile-clear" id="myfile-clear_id" /> '
'<label for="myfile-clear_id">Clear</label><br />Change: <input type="file" name="myfile" />'
)

View File

@ -1,9 +1,27 @@
from django.forms.renderers import DjangoTemplates, Jinja2
from django.test import SimpleTestCase
try:
import jinja2
except ImportError:
jinja2 = None
class WidgetTest(SimpleTestCase):
beatles = (('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))
@classmethod
def setUpClass(cls):
cls.django_renderer = DjangoTemplates()
cls.jinja2_renderer = Jinja2() if jinja2 else None
cls.renderers = [cls.django_renderer] + ([cls.jinja2_renderer] if cls.jinja2_renderer else [])
super(WidgetTest, cls).setUpClass()
def check_html(self, widget, name, value, html='', attrs=None, **kwargs):
output = widget.render(name, value, attrs=attrs, **kwargs)
if self.jinja2_renderer:
output = widget.render(name, value, attrs=attrs, renderer=self.jinja2_renderer, **kwargs)
# Django escapes quotes with '&quot;' while Jinja2 uses '&#34;'.
self.assertHTMLEqual(output.replace('&#34;', '&quot;'), html)
output = widget.render(name, value, attrs=attrs, renderer=self.django_renderer, **kwargs)
self.assertHTMLEqual(output, html)

View File

@ -221,6 +221,68 @@ class SelectTest(WidgetTest):
</select>"""
))
def test_options(self):
options = list(self.widget(choices=self.beatles).options(
'name', ['J'], attrs={'class': 'super'},
))
self.assertEqual(len(options), 4)
self.assertEqual(options[0]['name'], 'name')
self.assertEqual(options[0]['value'], 'J')
self.assertEqual(options[0]['label'], 'John')
self.assertEqual(options[0]['index'], '0')
self.assertEqual(options[0]['selected'], True)
# Template-related attributes
self.assertEqual(options[1]['name'], 'name')
self.assertEqual(options[1]['value'], 'P')
self.assertEqual(options[1]['label'], 'Paul')
self.assertEqual(options[1]['index'], '1')
self.assertEqual(options[1]['selected'], False)
def test_optgroups(self):
choices = [
('Audio', [
('vinyl', 'Vinyl'),
('cd', 'CD'),
]),
('Video', [
('vhs', 'VHS Tape'),
('dvd', 'DVD'),
]),
('unknown', 'Unknown'),
]
groups = list(self.widget(choices=choices).optgroups(
'name', ['vhs'], attrs={'class': 'super'},
))
self.assertEqual(len(groups), 3)
self.assertEqual(groups[0][0], None)
self.assertEqual(groups[0][2], 0)
self.assertEqual(len(groups[0][1]), 1)
options = groups[0][1]
self.assertEqual(options[0]['name'], 'name')
self.assertEqual(options[0]['value'], 'unknown')
self.assertEqual(options[0]['label'], 'Unknown')
self.assertEqual(options[0]['index'], '0')
self.assertEqual(options[0]['selected'], False)
self.assertEqual(groups[1][0], 'Audio')
self.assertEqual(groups[1][2], 1)
self.assertEqual(len(groups[1][1]), 2)
options = groups[1][1]
self.assertEqual(options[0]['name'], 'name')
self.assertEqual(options[0]['value'], 'vinyl')
self.assertEqual(options[0]['label'], 'Vinyl')
self.assertEqual(options[0]['index'], '1_0')
self.assertEqual(options[1]['index'], '1_1')
self.assertEqual(groups[2][0], 'Video')
self.assertEqual(groups[2][2], 2)
self.assertEqual(len(groups[2][1]), 2)
options = groups[2][1]
self.assertEqual(options[0]['name'], 'name')
self.assertEqual(options[0]['value'], 'vhs')
self.assertEqual(options[0]['label'], 'VHS Tape')
self.assertEqual(options[0]['index'], '2_0')
self.assertEqual(options[0]['selected'], True)
self.assertEqual(options[1]['index'], '2_1')
def test_deepcopy(self):
"""
__deepcopy__() should copy all attributes properly (#25085).

View File

@ -1651,13 +1651,6 @@ class ModelChoiceFieldTests(TestCase):
with self.assertNumQueries(1):
template.render(Context({'field': field}))
def test_modelchoicefield_index_renderer(self):
field = forms.ModelChoiceField(Category.objects.all(), widget=forms.RadioSelect)
self.assertEqual(
str(field.widget.get_renderer('foo', [])[0]),
'<label><input name="foo" type="radio" value="" /> ---------</label>'
)
def test_disabled_modelchoicefield(self):
class ModelChoiceForm(forms.ModelForm):
author = forms.ModelChoiceField(Author.objects.all(), disabled=True)
@ -2115,7 +2108,7 @@ class FileAndImageFieldTests(TestCase):
doc = Document.objects.create()
form = DocumentForm(instance=doc)
self.assertEqual(
self.assertHTMLEqual(
str(form['myfile']),
'<input id="id_myfile" name="myfile" type="file" />'
)

View File

@ -169,6 +169,11 @@ def setup(verbosity, test_labels, parallel):
'The GeoManager class is deprecated.',
RemovedInDjango20Warning
)
warnings.filterwarnings(
'ignore',
'django.forms.extras is deprecated.',
RemovedInDjango20Warning
)
# Load all the ALWAYS_INSTALLED_APPS.
django.setup()