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 = [] TEMPLATES = []
# Default form rendering class.
FORM_RENDERER = 'django.forms.renderers.DjangoTemplates'
# Default email address to use for various automated correspondence from # Default email address to use for various automated correspondence from
# the site managers. # the site managers.
DEFAULT_FROM_EMAIL = 'webmaster@localhost' 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 import forms
from django.db.models.deletion import CASCADE 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 import reverse
from django.urls.exceptions import NoReverseMatch from django.urls.exceptions import NoReverseMatch
from django.utils import six from django.utils import six
from django.utils.encoding import force_text 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.safestring import mark_safe
from django.utils.text import Truncator from django.utils.text import Truncator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -37,17 +34,14 @@ class FilteredSelectMultiple(forms.SelectMultiple):
self.is_stacked = is_stacked self.is_stacked = is_stacked
super(FilteredSelectMultiple, self).__init__(attrs, choices) super(FilteredSelectMultiple, self).__init__(attrs, choices)
def render(self, name, value, attrs=None): def get_context(self, name, value, attrs=None):
if attrs is None: context = super(FilteredSelectMultiple, self).get_context(name, value, attrs)
attrs = {} context['widget']['attrs']['class'] = 'selectfilter'
attrs['class'] = 'selectfilter'
if self.is_stacked: if self.is_stacked:
attrs['class'] += 'stacked' context['widget']['attrs']['class'] += 'stacked'
context['widget']['attrs']['data-field-name'] = self.verbose_name
attrs['data-field-name'] = self.verbose_name context['widget']['attrs']['data-is-stacked'] = int(self.is_stacked)
attrs['data-is-stacked'] = int(self.is_stacked) return context
output = super(FilteredSelectMultiple, self).render(name, value, attrs)
return mark_safe(output)
class AdminDateWidget(forms.DateInput): class AdminDateWidget(forms.DateInput):
@ -80,38 +74,27 @@ class AdminSplitDateTime(forms.SplitDateTimeWidget):
""" """
A SplitDateTime Widget that has some admin-specific styling. A SplitDateTime Widget that has some admin-specific styling.
""" """
template_name = 'admin/widgets/split_datetime.html'
def __init__(self, attrs=None): def __init__(self, attrs=None):
widgets = [AdminDateWidget, AdminTimeWidget] widgets = [AdminDateWidget, AdminTimeWidget]
# Note that we're calling MultiWidget, not SplitDateTimeWidget, because # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
# we want to define widgets. # we want to define widgets.
forms.MultiWidget.__init__(self, widgets, attrs) forms.MultiWidget.__init__(self, widgets, attrs)
def format_output(self, rendered_widgets): def get_context(self, name, value, attrs):
return format_html('<p class="datetime">{} {}<br />{} {}</p>', context = super(AdminSplitDateTime, self).get_context(name, value, attrs)
_('Date:'), rendered_widgets[0], context['date_label'] = _('Date:')
_('Time:'), rendered_widgets[1]) context['time_label'] = _('Time:')
return context
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)))
class AdminRadioSelect(forms.RadioSelect): class AdminRadioSelect(forms.RadioSelect):
renderer = AdminRadioFieldRenderer template_name = 'admin/widgets/radio.html'
class AdminFileWidget(forms.ClearableFileInput): class AdminFileWidget(forms.ClearableFileInput):
template_with_initial = ( template_name = 'admin/widgets/clearable_file_input.html'
'<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
)
def url_params_from_lookup_dict(lookups): 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 A Widget for displaying ForeignKeys in the "raw_id" interface rather than
in a <select> box. in a <select> box.
""" """
template_name = 'admin/widgets/foreign_key_raw_id.html'
def __init__(self, rel, admin_site, attrs=None, using=None): def __init__(self, rel, admin_site, attrs=None, using=None):
self.rel = rel self.rel = rel
self.admin_site = admin_site self.admin_site = admin_site
self.db = using self.db = using
super(ForeignKeyRawIdWidget, self).__init__(attrs) 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 rel_to = self.rel.model
if attrs is None:
attrs = {}
extra = []
if rel_to in self.admin_site._registry: if rel_to in self.admin_site._registry:
# The related object is registered with the same AdminSite # The related object is registered with the same AdminSite
related_url = reverse( related_url = reverse(
@ -164,21 +147,16 @@ class ForeignKeyRawIdWidget(forms.TextInput):
params = self.url_parameters() params = self.url_parameters()
if params: if params:
url = '?' + '&amp;'.join('%s=%s' % (k, v) for k, v in params.items()) related_url += '?' + '&amp;'.join(
else: '%s=%s' % (k, v) for k, v in params.items(),
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 context['related_url'] = mark_safe(related_url)
if value: context['link_title'] = _('Lookup')
output.append(self.label_for_value(value)) # The JavaScript code looks for this class.
return mark_safe(''.join(output)) 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): def base_url_parameters(self):
limit_choices_to = self.rel.limit_choices_to 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}) params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
return params return params
def label_for_value(self, value): def label_and_url_for_value(self, value):
key = self.rel.get_related_field().name key = self.rel.get_related_field().name
try: try:
obj = self.rel.model._default_manager.using(self.db).get(**{key: value}) obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
except (ValueError, self.rel.model.DoesNotExist): except (ValueError, self.rel.model.DoesNotExist):
return '' return '', ''
label = '&nbsp;<strong>{}</strong>'
text = Truncator(obj).words(14, truncate='...')
try: try:
change_url = reverse( url = reverse(
'%s:%s_%s_change' % ( '%s:%s_%s_change' % (
self.admin_site.name, self.admin_site.name,
obj._meta.app_label, obj._meta.app_label,
@ -211,11 +187,9 @@ class ForeignKeyRawIdWidget(forms.TextInput):
args=(obj.pk,) args=(obj.pk,)
) )
except NoReverseMatch: except NoReverseMatch:
pass # Admin not registered for target model. url = '' # Admin not registered for target model.
else:
text = format_html('<a href="{}">{}</a>', change_url, text)
return format_html(label, text) return Truncator(obj).words(14, truncate='...'), url
class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
@ -223,36 +197,36 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
A Widget for displaying ManyToMany ids in the "raw_id" interface rather than A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
in a <select multiple> box. in a <select multiple> box.
""" """
def render(self, name, value, attrs=None): template_name = 'admin/widgets/many_to_many_raw_id.html'
if attrs is None:
attrs = {} 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: if self.rel.model in self.admin_site._registry:
# The related object is registered with the same AdminSite # The related object is registered with the same AdminSite
attrs['class'] = 'vManyToManyRawIdAdminField' context['widget']['attrs']['class'] = 'vManyToManyRawIdAdminField'
if value: return context
value = ','.join(force_text(v) for v in value)
else:
value = ''
return super(ManyToManyRawIdWidget, self).render(name, value, attrs)
def url_parameters(self): def url_parameters(self):
return self.base_url_parameters() return self.base_url_parameters()
def label_for_value(self, value): def label_and_url_for_value(self, value):
return '' return '', ''
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
value = data.get(name) value = data.get(name)
if value: if value:
return value.split(',') return value.split(',')
def format_value(self, value):
return ','.join(force_text(v) for v in value) if value else ''
class RelatedFieldWidgetWrapper(forms.Widget): class RelatedFieldWidgetWrapper(forms.Widget):
""" """
This class is a wrapper to a given widget to add the add icon for the This class is a wrapper to a given widget to add the add icon for the
admin interface. 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, def __init__(self, widget, rel, admin_site, can_add_related=None,
can_change_related=False, can_delete_related=False): can_change_related=False, can_delete_related=False):
@ -294,21 +268,19 @@ class RelatedFieldWidgetWrapper(forms.Widget):
return reverse("admin:%s_%s_%s" % (info + (action,)), return reverse("admin:%s_%s_%s" % (info + (action,)),
current_app=self.admin_site.name, args=args) 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 from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
rel_opts = self.rel.model._meta rel_opts = self.rel.model._meta
info = (rel_opts.app_label, rel_opts.model_name) info = (rel_opts.app_label, rel_opts.model_name)
self.widget.choices = self.choices
url_params = '&'.join("%s=%s" % param for param in [ url_params = '&'.join("%s=%s" % param for param in [
(TO_FIELD_VAR, self.rel.get_related_field().name), (TO_FIELD_VAR, self.rel.get_related_field().name),
(IS_POPUP_VAR, 1), (IS_POPUP_VAR, 1),
]) ])
context = { context['url_params'] = url_params
'widget': self.widget.render(name, value, *args, **kwargs), context['model'] = rel_opts.verbose_name
'name': name,
'url_params': url_params,
'model': rel_opts.verbose_name,
}
if self.can_change_related: if self.can_change_related:
change_related_template_url = self.get_related_url(info, 'change', '__fk__') change_related_template_url = self.get_related_url(info, 'change', '__fk__')
context.update( context.update(
@ -327,12 +299,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
can_delete_related=True, can_delete_related=True,
delete_related_template_url=delete_related_template_url, delete_related_template_url=delete_related_template_url,
) )
return mark_safe(render_to_string(self.template, context)) return 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
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
return self.widget.value_from_datadict(data, files, name) return self.widget.value_from_datadict(data, files, name)
@ -366,23 +333,24 @@ class AdminEmailInputWidget(forms.EmailInput):
class AdminURLFieldWidget(forms.URLInput): class AdminURLFieldWidget(forms.URLInput):
template_name = 'admin/widgets/url.html'
def __init__(self, attrs=None): def __init__(self, attrs=None):
final_attrs = {'class': 'vURLField'} final_attrs = {'class': 'vURLField'}
if attrs is not None: if attrs is not None:
final_attrs.update(attrs) final_attrs.update(attrs)
super(AdminURLFieldWidget, self).__init__(attrs=final_attrs) super(AdminURLFieldWidget, self).__init__(attrs=final_attrs)
def render(self, name, value, attrs=None): def get_context(self, name, value, attrs):
html = super(AdminURLFieldWidget, self).render(name, value, attrs) context = super(AdminURLFieldWidget, self).get_context(name, value, attrs)
if value: context['current_label'] = _('Currently:')
value = force_text(self.format_value(value)) context['change_label'] = _('Change:')
final_attrs = {'href': smart_urlquote(value)} context['widget']['href'] = smart_urlquote(context['widget']['value'])
html = format_html( return context
'<p class="url">{} <a{}>{}</a><br />{} {}</p>',
_('Currently:'), flatatt(final_attrs), value, def format_value(self, value):
_('Change:'), html value = super(AdminURLFieldWidget, self).format_value(value)
) return force_text(value)
return html
class AdminIntegerFieldWidget(forms.NumberInput): 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.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.forms.utils import flatatt
from django.template import loader from django.template import loader
from django.utils.encoding import force_bytes 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.http import urlsafe_base64_encode
from django.utils.safestring import mark_safe
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.translation import ugettext, ugettext_lazy as _
@ -26,26 +23,23 @@ UserModel = get_user_model()
class ReadOnlyPasswordHashWidget(forms.Widget): class ReadOnlyPasswordHashWidget(forms.Widget):
def render(self, name, value, attrs): template_name = 'auth/widgets/read_only_password_hash.html'
encoded = value
final_attrs = self.build_attrs(attrs)
if not encoded or encoded.startswith(UNUSABLE_PASSWORD_PREFIX): def get_context(self, name, value, attrs):
summary = mark_safe("<strong>%s</strong>" % ugettext("No password set.")) 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: else:
try: try:
hasher = identify_hasher(encoded) hasher = identify_hasher(value)
except ValueError: except ValueError:
summary = mark_safe("<strong>%s</strong>" % ugettext( summary.append({'label': ugettext("Invalid password format or unknown hashing algorithm.")})
"Invalid password format or unknown hashing algorithm."
))
else: else:
summary = format_html_join( for key, value_ in hasher.safe_summary(value).items():
'', '<strong>{}</strong>: {} ', summary.append({'label': ugettext(key), 'value': value_})
((ugettext(key), value) for key, value in hasher.safe_summary(encoded).items()) context['summary'] = summary
) return context
return format_html("<div{}>{}</div>", flatatt(final_attrs), summary)
class ReadOnlyPasswordHashField(forms.Field): 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' collection_type = 'None'
class OLMap(self.widget): class OLMap(self.widget):
template = self.map_template template_name = self.map_template
geom_type = db_field.geom_type geom_type = db_field.geom_type
wms_options = '' wms_options = ''

View File

@ -3,7 +3,6 @@ import logging
from django.contrib.gis.gdal import GDALException from django.contrib.gis.gdal import GDALException
from django.contrib.gis.geos import GEOSException, GEOSGeometry from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.forms.widgets import Textarea from django.forms.widgets import Textarea
from django.template import loader
from django.utils import six, translation from django.utils import six, translation
# Creating a template context that contains Django settings # 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. 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. # Update the template parameters with any attributes passed in.
if attrs: if attrs:
self.params.update(attrs) self.params.update(attrs)
@ -77,7 +76,7 @@ class OpenLayersWidget(Textarea):
self.params['wkt'] = wkt self.params['wkt'] = wkt
self.params.update(geo_context) self.params.update(geo_context)
return loader.render_to_string(self.template, self.params) return self.params
def map_options(self): def map_options(self):
"Builds the map options hash for the OpenLayers template." "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 import gdal
from django.contrib.gis.geos import GEOSException, GEOSGeometry from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.forms.widgets import Widget from django.forms.widgets import Widget
from django.template import loader
from django.utils import six, translation from django.utils import six, translation
logger = logging.getLogger('django.contrib.gis') logger = logging.getLogger('django.contrib.gis')
@ -43,7 +42,7 @@ class BaseGeometryWidget(Widget):
logger.error("Error creating geometry from value '%s' (%s)", value, err) logger.error("Error creating geometry from value '%s' (%s)", value, err)
return None 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 # If a string reaches here (via a validation error on another
# field) then just reconstruct the Geometry. # field) then just reconstruct the Geometry.
if value and isinstance(value, six.string_types): if value and isinstance(value, six.string_types):
@ -62,16 +61,19 @@ class BaseGeometryWidget(Widget):
value.srid, self.map_srid, err value.srid, self.map_srid, err
) )
context = self.build_attrs( if attrs is None:
attrs, attrs = {}
context = self.build_attrs(self.attrs, dict(
name=name, name=name,
module='geodjango_%s' % name.replace('-', '_'), # JS-safe module='geodjango_%s' % name.replace('-', '_'), # JS-safe
serialized=self.serialize(value), serialized=self.serialize(value),
geom_type=gdal.OGRGeomType(self.attrs['geom_type']), geom_type=gdal.OGRGeomType(self.attrs['geom_type']),
STATIC_URL=settings.STATIC_URL, STATIC_URL=settings.STATIC_URL,
LANGUAGE_BIDI=translation.get_language_bidi(), LANGUAGE_BIDI=translation.get_language_bidi(),
) **attrs
return loader.render_to_string(self.template_name, context) ))
return context
class OpenLayersWidget(BaseGeometryWidget): class OpenLayersWidget(BaseGeometryWidget):

View File

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

View File

@ -1,13 +1,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
import warnings
from django.forms.utils import flatatt, pretty_name from django.forms.utils import flatatt, pretty_name
from django.forms.widgets import Textarea, TextInput from django.forms.widgets import Textarea, TextInput
from django.utils import six 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.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html, html_safe 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.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ 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 id_ = self.field.widget.attrs.get('id') or self.auto_id
attrs = {'id': id_} if id_ else {} attrs = {'id': id_} if id_ else {}
attrs = self.build_widget_attrs(attrs) 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): def __iter__(self):
return iter(self.subwidgets) return iter(self.subwidgets)
@ -97,7 +103,23 @@ class BoundField(object):
name = self.html_name name = self.html_name
else: else:
name = self.html_initial_name 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): def as_text(self, attrs=None, **kwargs):
""" """
@ -230,3 +252,45 @@ class BoundField(object):
if self.field.disabled: if self.field.disabled:
attrs['disabled'] = True attrs['disabled'] = True
return attrs 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.safestring import mark_safe
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from .renderers import get_default_renderer
__all__ = ('BaseForm', 'Form') __all__ = ('BaseForm', 'Form')
@ -65,13 +67,14 @@ class BaseForm(object):
# class is different than Form. See the comments by the Form class for more # 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* # information. Any improvements to the form API should be made to *this*
# class, not to the Form class. # class, not to the Form class.
default_renderer = None
field_order = None field_order = None
prefix = None prefix = None
use_required_attribute = True use_required_attribute = True
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=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.is_bound = data is not None or files is not None
self.data = data or {} self.data = data or {}
self.files = files or {} self.files = files or {}
@ -97,6 +100,17 @@ class BaseForm(object):
if use_required_attribute is not None: if use_required_attribute is not None:
self.use_required_attribute = use_required_attribute 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): def order_fields(self, field_order):
""" """
Rearranges the fields according to 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 django.utils.module_loading import import_string
from .base import BaseEngine from .base import BaseEngine
from .utils import csrf_input_lazy, csrf_token_lazy
class Jinja2(BaseEngine): class Jinja2(BaseEngine):
@ -70,6 +69,7 @@ class Template(object):
) )
def render(self, context=None, request=None): def render(self, context=None, request=None):
from .utils import csrf_input_lazy, csrf_token_lazy
if context is None: if context is None:
context = {} context = {}
if request is not None: if request is not None:

View File

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

View File

@ -51,6 +51,9 @@ details on these changes.
* Support for regular expression groups with ``iLmsu#`` in ``url()`` will be * Support for regular expression groups with ``iLmsu#`` in ``url()`` will be
removed. removed.
* Support for ``Widget.render()`` methods without the ``renderer`` argument
will be removed.
.. _deprecation-removed-in-2.0: .. _deprecation-removed-in-2.0:
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 ``use_required_attribute=False`` to avoid incorrect browser validation when
adding and deleting forms from a formset. 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 Notes on field ordering
----------------------- -----------------------

View File

@ -12,5 +12,6 @@ Detailed form API reference. For introductory material, see the
fields fields
models models
formsets formsets
renderers
widgets widgets
validation 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 In older versions, this method is a private API named
``_format_value()``. The old name will work until Django 2.0. ``_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_) .. method:: id_for_label(self, id_)
Returns the HTML ID attribute of this widget for use by a ``<label>``, 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 return an ID value that corresponds to the first ID in the widget's
tags. 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 Renders a widget to HTML using the given renderer. If ``renderer`` is
implemented by the subclass, otherwise ``NotImplementedError`` will be ``None``, the renderer from the :setting:`FORM_RENDERER` setting is
raised. used.
The 'value' given is not guaranteed to be valid input, therefore .. versionchanged:: 1.11
subclass implementations should program defensively.
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) .. method:: value_from_datadict(data, files, name)
@ -360,40 +384,21 @@ foundation for custom widgets.
with the opposite responsibility - to combine cleaned values of with the opposite responsibility - to combine cleaned values of
all member fields into one. 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 In addition to the ``'widget'`` key described in
subclasses of :class:`~Widget` because it has to figure out how to :meth:`Widget.get_context`, ``MultiValueWidget`` adds a
split a single value for display in multiple widgets. ``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``. .. code-block:: html+django
* A single value (e.g., a string) that is the "compressed" representation
of a ``list`` of values.
If ``value`` is a list, the output of :meth:`~MultiWidget.render` will {% for subwidget in widget.subwidgets %}
be a concatenation of rendered child widgets. If ``value`` is not a {% include widget.template_name with widget=subwidget %}
list, it will first be processed by the method {% endfor %}
: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.
Here's an example widget which subclasses :class:`MultiWidget` to display 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 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 [value.day, value.month, value.year]
return [None, None, None] return [None, None, None]
def format_output(self, rendered_widgets):
return ''.join(rendered_widgets)
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
datelist = [ datelist = [
widget.value_from_datadict(data, files, name + '_%s' % i) 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 The constructor creates several :class:`Select` widgets in a tuple. The
``super`` class uses this tuple to setup the widget. ``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 The required method :meth:`~MultiWidget.decompress` breaks up a
``datetime.date`` value into the day, month, and year values corresponding ``datetime.date`` value into the day, month, and year values corresponding
to each widget. Note how the method handles the case where ``value`` is 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 .. class:: TextInput
Text input: ``<input type="text" ...>`` * ``input_type``: ``'text'``
* ``template_name``: ``'django/forms/widgets/text.html'``
* Renders as: ``<input type="text" ...>``
``NumberInput`` ``NumberInput``
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
.. class:: 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 Beware that not all browsers support entering localized numbers in
``number`` input types. Django itself avoids using them for fields having ``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 .. class:: EmailInput
Text input: ``<input type="email" ...>`` * ``input_type``: ``'email'``
* ``template_name``: ``'django/forms/widgets/email.html'``
* Renders as: ``<input type="email" ...>``
``URLInput`` ``URLInput``
~~~~~~~~~~~~ ~~~~~~~~~~~~
.. class:: URLInput .. class:: URLInput
Text input: ``<input type="url" ...>`` * ``input_type``: ``'url'``
* ``template_name``: ``'django/forms/widgets/url.html'``
* Renders as: ``<input type="url" ...>``
``PasswordInput`` ``PasswordInput``
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
.. class:: 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: Takes one optional argument:
@ -531,7 +538,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: HiddenInput .. 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 Note that there also is a :class:`MultipleHiddenInput` widget that
encapsulates a set of hidden input elements. encapsulates a set of hidden input elements.
@ -541,7 +550,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: DateInput .. 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: 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 .. 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: 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 .. 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: 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 .. class:: Textarea
Text area: ``<textarea>...</textarea>`` * ``template_name``: ``'django/forms/widgets/textarea.html'``
* Renders as: ``<textarea>...</textarea>``
.. _selector-widgets: .. _selector-widgets:
@ -610,7 +626,9 @@ Selector and checkbox widgets
.. class:: CheckboxInput .. 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: Takes one optional argument:
@ -624,7 +642,8 @@ Selector and checkbox widgets
.. class:: Select .. class:: Select
Select widget: ``<select><option ...>...</select>`` * ``template_name``: ``'django/forms/widgets/select.html'``
* Renders as: ``<select><option ...>...</select>``
.. attribute:: Select.choices .. attribute:: Select.choices
@ -637,6 +656,8 @@ Selector and checkbox widgets
.. class:: NullBooleanSelect .. class:: NullBooleanSelect
* ``template_name``: ``'django/forms/widgets/select.html'``
Select widget with options 'Unknown', 'Yes' and 'No' Select widget with options 'Unknown', 'Yes' and 'No'
``SelectMultiple`` ``SelectMultiple``
@ -644,6 +665,8 @@ Selector and checkbox widgets
.. class:: SelectMultiple .. class:: SelectMultiple
* ``template_name``: ``'django/forms/widgets/select.html'``
Similar to :class:`Select`, but allows multiple selection: Similar to :class:`Select`, but allows multiple selection:
``<select multiple='multiple'>...</select>`` ``<select multiple='multiple'>...</select>``
@ -652,6 +675,8 @@ Selector and checkbox widgets
.. class:: RadioSelect .. class:: RadioSelect
* ``template_name``: ``'django/forms/widgets/radio.html'``
Similar to :class:`Select`, but rendered as a list of radio buttons within Similar to :class:`Select`, but rendered as a list of radio buttons within
``<li>`` tags: ``<li>`` tags:
@ -744,6 +769,8 @@ Selector and checkbox widgets
.. class:: CheckboxSelectMultiple .. class:: CheckboxSelectMultiple
* ``template_name``: ``'django/forms/widgets/checkbox_select.html'``
Similar to :class:`SelectMultiple`, but rendered as a list of check Similar to :class:`SelectMultiple`, but rendered as a list of check
buttons: buttons:
@ -776,14 +803,16 @@ File upload widgets
.. class:: FileInput .. class:: FileInput
File upload input: ``<input type='file' ...>`` * ``template_name``: ``'django/forms/widgets/file.html'``
* Renders as: ``<input type='file' ...>``
``ClearableFileInput`` ``ClearableFileInput``
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
.. class:: ClearableFileInput .. class:: ClearableFileInput
File upload input: ``<input type='file' ...>``, with an additional checkbox * ``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 input to clear the field's value, if the field is not required and has
initial data. initial data.
@ -797,7 +826,8 @@ Composite widgets
.. class:: MultipleHiddenInput .. 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 A widget that handles multiple hidden widgets for fields that have a list
of values. of values.
@ -813,6 +843,8 @@ Composite widgets
.. class:: SplitDateTimeWidget .. class:: SplitDateTimeWidget
* ``template_name``: ``'django/forms/widgets/splitdatetime.html'``
Wrapper (using :class:`MultiWidget`) around two widgets: :class:`DateInput` Wrapper (using :class:`MultiWidget`) around two widgets: :class:`DateInput`
for the date, and :class:`TimeInput` for the time. Must be used with for the date, and :class:`TimeInput` for the time. Must be used with
:class:`SplitDateTimeField` rather than :class:`DateTimeField`. :class:`SplitDateTimeField` rather than :class:`DateTimeField`.
@ -832,6 +864,8 @@ Composite widgets
.. class:: SplitHiddenDateTimeWidget .. class:: SplitHiddenDateTimeWidget
* ``template_name``: ``'django/forms/widgets/splithiddendatetime.html'``
Similar to :class:`SplitDateTimeWidget`, but uses :class:`HiddenInput` for Similar to :class:`SplitDateTimeWidget`, but uses :class:`HiddenInput` for
both date and time. both date and time.
@ -840,6 +874,8 @@ Composite widgets
.. class:: SelectDateWidget .. class:: SelectDateWidget
* ``template_name``: ``'django/forms/widgets/select_date.html'``
Wrapper around three :class:`~django.forms.Select` widgets: one each for Wrapper around three :class:`~django.forms.Select` widgets: one each for
month, day, and year. 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. 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 .. setting:: FORMAT_MODULE_PATH
``FORMAT_MODULE_PATH`` ``FORMAT_MODULE_PATH``
@ -3351,6 +3363,10 @@ File uploads
* :setting:`MEDIA_ROOT` * :setting:`MEDIA_ROOT`
* :setting:`MEDIA_URL` * :setting:`MEDIA_URL`
Forms
-----
* :setting:`FORM_RENDERER`
Globalization (``i18n``/``l10n``) Globalization (``i18n``/``l10n``)
--------------------------------- ---------------------------------
* :setting:`DATE_FORMAT` * :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 :class:`~django.contrib.postgres.indexes.GinIndex`. It also allows defining the
order (ASC/DESC) for the columns of the index. 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 Minor features
-------------- --------------
@ -551,6 +560,21 @@ inside help text.
Read-only fields are wrapped in ``<div class="readonly">...</div>`` instead of 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. ``<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 Miscellaneous
------------- -------------
@ -754,3 +778,7 @@ Miscellaneous
entries for search engines, for example. An alternative solution could be to entries for search engines, for example. An alternative solution could be to
create a :data:`~django.conf.urls.handler404` that looks for uppercase create a :data:`~django.conf.urls.handler404` that looks for uppercase
characters in the URL and redirects to a lowercase equivalent. 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 reloader
removetags removetags
renderer renderer
renderers
repo repo
reportable reportable
reprojection reprojection

View File

@ -184,9 +184,11 @@ class TestInline(TestDataMixin, TestCase):
SomeChildModel.objects.create(name='c', position='1', parent=parent) SomeChildModel.objects.create(name='c', position='1', parent=parent)
response = self.client.get(reverse('admin:admin_inlines_someparentmodel_change', args=(parent.pk,))) response = self.client.get(reverse('admin:admin_inlines_someparentmodel_change', args=(parent.pk,)))
self.assertNotContains(response, '<td class="field-position">') self.assertNotContains(response, '<td class="field-position">')
self.assertContains(response, ( self.assertInHTML(
'<input id="id_somechildmodel_set-1-position" ' '<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): def test_non_related_name_inline(self):
""" """
@ -273,12 +275,12 @@ class TestInline(TestDataMixin, TestCase):
'name="binarytree_set-TOTAL_FORMS" type="hidden" value="2" />' 'name="binarytree_set-TOTAL_FORMS" type="hidden" value="2" />'
) )
response = self.client.get(reverse('admin:admin_inlines_binarytree_add')) response = self.client.get(reverse('admin:admin_inlines_binarytree_add'))
self.assertContains(response, max_forms_input % 3) self.assertInHTML(max_forms_input % 3, response.rendered_content)
self.assertContains(response, total_forms_hidden) self.assertInHTML(total_forms_hidden, response.rendered_content)
response = self.client.get(reverse('admin:admin_inlines_binarytree_change', args=(bt_head.id,))) response = self.client.get(reverse('admin:admin_inlines_binarytree_change', args=(bt_head.id,)))
self.assertContains(response, max_forms_input % 2) self.assertInHTML(max_forms_input % 2, response.rendered_content)
self.assertContains(response, total_forms_hidden) self.assertInHTML(total_forms_hidden, response.rendered_content)
def test_min_num(self): def test_min_num(self):
""" """
@ -302,8 +304,8 @@ class TestInline(TestDataMixin, TestCase):
request = self.factory.get(reverse('admin:admin_inlines_binarytree_add')) request = self.factory.get(reverse('admin:admin_inlines_binarytree_add'))
request.user = User(username='super', is_superuser=True) request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request) response = modeladmin.changeform_view(request)
self.assertContains(response, min_forms) self.assertInHTML(min_forms, response.rendered_content)
self.assertContains(response, total_forms) self.assertInHTML(total_forms, response.rendered_content)
def test_custom_min_num(self): def test_custom_min_num(self):
bt_head = BinaryTree.objects.create(name="Tree Head") 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 = self.factory.get(reverse('admin:admin_inlines_binarytree_add'))
request.user = User(username='super', is_superuser=True) request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request) response = modeladmin.changeform_view(request)
self.assertContains(response, min_forms % 2) self.assertInHTML(min_forms % 2, response.rendered_content)
self.assertContains(response, total_forms % 5) self.assertInHTML(total_forms % 5, response.rendered_content)
request = self.factory.get(reverse('admin:admin_inlines_binarytree_change', args=(bt_head.id,))) request = self.factory.get(reverse('admin:admin_inlines_binarytree_change', args=(bt_head.id,)))
request.user = User(username='super', is_superuser=True) request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request, object_id=str(bt_head.id)) response = modeladmin.changeform_view(request, object_id=str(bt_head.id))
self.assertContains(response, min_forms % 5) self.assertInHTML(min_forms % 5, response.rendered_content)
self.assertContains(response, total_forms % 8) self.assertInHTML(total_forms % 8, response.rendered_content)
def test_inline_nonauto_noneditable_pk(self): def test_inline_nonauto_noneditable_pk(self):
response = self.client.get(reverse('admin:admin_inlines_author_add')) 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, response, 'inline_admin_formset', 0, None,
['Children must share a family name with their parents in this contrived test case'] ['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): with self.assertRaisesMessage(AssertionError, msg):
self.assertFormsetError(response, 'inline_admin_formset', None, None, ['Error']) self.assertFormsetError(response, 'inline_admin_formset', None, None, ['Error'])

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
import gettext import gettext
import os import os
import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from importlib import import_module from importlib import import_module
@ -354,34 +355,53 @@ class AdminURLWidgetTest(SimpleTestCase):
) )
def test_render_quoting(self): 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() w = widgets.AdminURLFieldWidget()
output = w.render('test', 'http://example.com/<sometag>some text</sometag>')
self.assertEqual( self.assertEqual(
w.render('test', 'http://example.com/<sometag>some text</sometag>'), HREF_RE.search(output).groups()[0],
'<p class="url">Currently: ' 'http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E',
'<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>'
) )
self.assertEqual( self.assertEqual(
w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>'), TEXT_RE.search(output).groups()[0],
'<p class="url">Currently: ' 'http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;',
'<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>'
) )
self.assertEqual( self.assertEqual(
w.render('test', 'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"'), VALUE_RE.search(output).groups()[0],
'<p class="url">Currently: ' 'http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;',
'<a href="http://www.example.com/%C3%A4%22%3E%3Cscript%3Ealert(%22XSS!%22)%3C/script%3E%22">' )
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;' 'http://www.example.com/%C3%A4&quot;&gt;&lt;script&gt;'
'alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;</a><br />' 'alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;'
'Change: <input class="vURLField" name="test" type="url" ' )
'value="http://www.example.com/%C3%A4&quot;&gt;&lt;script&gt;' self.assertEqual(
'alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;" /></p>' 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/forms.py', 'forms.py'),
('/django/forms/formsets.py', 'formsets.py'), ('/django/forms/formsets.py', 'formsets.py'),
('/django/forms/models.py', 'models.py'), ('/django/forms/models.py', 'models.py'),
('/django/forms/renderers.py', 'renderers.py'),
('/django/forms/utils.py', 'utils.py'), ('/django/forms/utils.py', 'utils.py'),
('/django/forms/widgets.py', 'widgets.py') ('/django/forms/widgets.py', 'widgets.py')
] ]
@ -62,6 +63,7 @@ class FilePathFieldTest(SimpleTestCase):
('/django/forms/forms.py', 'forms.py'), ('/django/forms/forms.py', 'forms.py'),
('/django/forms/formsets.py', 'formsets.py'), ('/django/forms/formsets.py', 'formsets.py'),
('/django/forms/models.py', 'models.py'), ('/django/forms/models.py', 'models.py'),
('/django/forms/renderers.py', 'renderers.py'),
('/django/forms/utils.py', 'utils.py'), ('/django/forms/utils.py', 'utils.py'),
('/django/forms/widgets.py', 'widgets.py') ('/django/forms/widgets.py', 'widgets.py')
] ]
@ -83,6 +85,7 @@ class FilePathFieldTest(SimpleTestCase):
('/django/forms/forms.py', 'forms.py'), ('/django/forms/forms.py', 'forms.py'),
('/django/forms/formsets.py', 'formsets.py'), ('/django/forms/formsets.py', 'formsets.py'),
('/django/forms/models.py', 'models.py'), ('/django/forms/models.py', 'models.py'),
('/django/forms/renderers.py', 'renderers.py'),
('/django/forms/utils.py', 'utils.py'), ('/django/forms/utils.py', 'utils.py'),
('/django/forms/widgets.py', 'widgets.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, SplitDateTimeField, SplitHiddenDateTimeWidget, Textarea, TextInput,
TimeField, ValidationError, forms, TimeField, ValidationError, forms,
) )
from django.forms.renderers import DjangoTemplates, get_default_renderer
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.http import QueryDict from django.http import QueryDict
from django.template import Context, Template 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>""" <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): def test_form_with_noniterable_boundfield(self):
# You can iterate over any BoundField, not just those with widget=RadioSelect. # You can iterate over any BoundField, not just those with widget=RadioSelect.
class BeatleForm(Form): class BeatleForm(Form):
@ -1993,8 +2038,9 @@ Password: <input type="password" name="password" required /></li>
doesn't lose it's safe string status (#22950). doesn't lose it's safe string status (#22950).
""" """
class CustomWidget(TextInput): class CustomWidget(TextInput):
def render(self, name, value, attrs=None): def render(self, name, value, attrs=None, choices=None,
return format_html(str('<input{} required />'), ' id=custom') renderer=None, extra_context=None):
return format_html(str('<input{} />'), ' id=custom')
class SampleForm(Form): class SampleForm(Form):
name = CharField(widget=CustomWidget) name = CharField(widget=CustomWidget)
@ -3573,3 +3619,46 @@ Good luck picking a username that doesn&#39;t already exist.</p>
f = DataForm({'data': 'xyzzy'}) f = DataForm({'data': 'xyzzy'})
self.assertTrue(f.is_valid()) self.assertTrue(f.is_valid())
self.assertEqual(f.cleaned_data, {'data': 'xyzzy'}) 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 __future__ import unicode_literals
from django.contrib.admin.tests import AdminSeleniumTestCase from django.contrib.admin.tests import AdminSeleniumTestCase
from django.forms import ( from django.test import override_settings
CheckboxSelectMultiple, ClearableFileInput, RadioSelect, TextInput,
)
from django.forms.widgets import (
ChoiceFieldRenderer, ChoiceInput, RadioFieldRenderer,
)
from django.test import SimpleTestCase, override_settings
from django.urls import reverse 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 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') @override_settings(ROOT_URLCONF='forms_tests.urls')
class LiveWidgetTests(AdminSeleniumTestCase): 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.get(self.live_server_url + reverse('article_form', args=[article.pk]))
self.selenium.find_element_by_id('submit').submit() self.selenium.find_element_by_id('submit').submit()
article = Article.objects.get(pk=article.pk) article = Article.objects.get(pk=article.pk)
# Should be "\nTst\n" after #19251 is fixed
self.assertEqual(article.content, "\r\nTst\r\n") 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 from django.test import SimpleTestCase
try:
import jinja2
except ImportError:
jinja2 = None
class WidgetTest(SimpleTestCase): class WidgetTest(SimpleTestCase):
beatles = (('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')) 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): 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) self.assertHTMLEqual(output, html)

View File

@ -221,6 +221,68 @@ class SelectTest(WidgetTest):
</select>""" </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): def test_deepcopy(self):
""" """
__deepcopy__() should copy all attributes properly (#25085). __deepcopy__() should copy all attributes properly (#25085).

View File

@ -1651,13 +1651,6 @@ class ModelChoiceFieldTests(TestCase):
with self.assertNumQueries(1): with self.assertNumQueries(1):
template.render(Context({'field': field})) 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): def test_disabled_modelchoicefield(self):
class ModelChoiceForm(forms.ModelForm): class ModelChoiceForm(forms.ModelForm):
author = forms.ModelChoiceField(Author.objects.all(), disabled=True) author = forms.ModelChoiceField(Author.objects.all(), disabled=True)
@ -2115,7 +2108,7 @@ class FileAndImageFieldTests(TestCase):
doc = Document.objects.create() doc = Document.objects.create()
form = DocumentForm(instance=doc) form = DocumentForm(instance=doc)
self.assertEqual( self.assertHTMLEqual(
str(form['myfile']), str(form['myfile']),
'<input id="id_myfile" name="myfile" type="file" />' '<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.', 'The GeoManager class is deprecated.',
RemovedInDjango20Warning RemovedInDjango20Warning
) )
warnings.filterwarnings(
'ignore',
'django.forms.extras is deprecated.',
RemovedInDjango20Warning
)
# Load all the ALWAYS_INSTALLED_APPS. # Load all the ALWAYS_INSTALLED_APPS.
django.setup() django.setup()