summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPreston Timmons <prestontimmons@gmail.com>2016-12-27 17:00:56 -0500
committerTim Graham <timograham@gmail.com>2016-12-27 17:50:10 -0500
commitb52c73008a9d67e9ddbb841872dc15cdd3d6ee01 (patch)
treeb58a2d18242db5234b18678116e07e6f6bbc7cb3
parent51cde873d9fc8e4540f4efecbd39cfe8e770be38 (diff)
Fixed #15667 -- Added template-based widget rendering.
Thanks Carl Meyer and Tim Graham for contributing to the patch.
-rw-r--r--django/conf/global_settings.py3
-rw-r--r--django/contrib/admin/templates/admin/widgets/clearable_file_input.html6
-rw-r--r--django/contrib/admin/templates/admin/widgets/foreign_key_raw_id.html1
-rw-r--r--django/contrib/admin/templates/admin/widgets/many_to_many_raw_id.html1
-rw-r--r--django/contrib/admin/templates/admin/widgets/radio.html1
-rw-r--r--django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html27
-rw-r--r--django/contrib/admin/templates/admin/widgets/split_datetime.html4
-rw-r--r--django/contrib/admin/templates/admin/widgets/url.html1
-rw-r--r--django/contrib/admin/widgets.py166
-rw-r--r--django/contrib/auth/forms.py30
-rw-r--r--django/contrib/auth/templates/auth/widgets/read_only_password_hash.html3
-rw-r--r--django/contrib/gis/admin/options.py2
-rw-r--r--django/contrib/gis/admin/widgets.py5
-rw-r--r--django/contrib/gis/forms/widgets.py14
-rw-r--r--django/contrib/postgres/forms/array.py4
-rw-r--r--django/forms/boundfield.py68
-rw-r--r--django/forms/forms.py16
-rw-r--r--django/forms/jinja2/django/forms/widgets/attrs.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/checkbox.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/checkbox_option.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/checkbox_select.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/clearable_file_input.html5
-rw-r--r--django/forms/jinja2/django/forms/widgets/date.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/datetime.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/email.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/file.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/hidden.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/input.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/input_option.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/multiple_hidden.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/multiple_input.html5
-rw-r--r--django/forms/jinja2/django/forms/widgets/multiwidget.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/number.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/password.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/radio.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/radio_option.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/select.html5
-rw-r--r--django/forms/jinja2/django/forms/widgets/select_date.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/select_option.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/splitdatetime.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/splithiddendatetime.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/text.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/textarea.html2
-rw-r--r--django/forms/jinja2/django/forms/widgets/time.html1
-rw-r--r--django/forms/jinja2/django/forms/widgets/url.html1
-rw-r--r--django/forms/renderers.py71
-rw-r--r--django/forms/templates/django/forms/widgets/attrs.html1
-rw-r--r--django/forms/templates/django/forms/widgets/checkbox.html1
-rw-r--r--django/forms/templates/django/forms/widgets/checkbox_option.html1
-rw-r--r--django/forms/templates/django/forms/widgets/checkbox_select.html1
-rw-r--r--django/forms/templates/django/forms/widgets/clearable_file_input.html5
-rw-r--r--django/forms/templates/django/forms/widgets/date.html1
-rw-r--r--django/forms/templates/django/forms/widgets/datetime.html1
-rw-r--r--django/forms/templates/django/forms/widgets/email.html1
-rw-r--r--django/forms/templates/django/forms/widgets/file.html1
-rw-r--r--django/forms/templates/django/forms/widgets/hidden.html1
-rw-r--r--django/forms/templates/django/forms/widgets/input.html1
-rw-r--r--django/forms/templates/django/forms/widgets/input_option.html1
-rw-r--r--django/forms/templates/django/forms/widgets/multiple_hidden.html1
-rw-r--r--django/forms/templates/django/forms/widgets/multiple_input.html5
-rw-r--r--django/forms/templates/django/forms/widgets/multiwidget.html1
-rw-r--r--django/forms/templates/django/forms/widgets/number.html1
-rw-r--r--django/forms/templates/django/forms/widgets/password.html1
-rw-r--r--django/forms/templates/django/forms/widgets/radio.html1
-rw-r--r--django/forms/templates/django/forms/widgets/radio_option.html1
-rw-r--r--django/forms/templates/django/forms/widgets/select.html5
-rw-r--r--django/forms/templates/django/forms/widgets/select_date.html1
-rw-r--r--django/forms/templates/django/forms/widgets/select_option.html1
-rw-r--r--django/forms/templates/django/forms/widgets/splitdatetime.html1
-rw-r--r--django/forms/templates/django/forms/widgets/splithiddendatetime.html1
-rw-r--r--django/forms/templates/django/forms/widgets/text.html1
-rw-r--r--django/forms/templates/django/forms/widgets/textarea.html2
-rw-r--r--django/forms/templates/django/forms/widgets/time.html1
-rw-r--r--django/forms/templates/django/forms/widgets/url.html1
-rw-r--r--django/forms/widgets.py795
-rw-r--r--django/template/backends/jinja2.py2
-rw-r--r--django/test/signals.py2
-rw-r--r--docs/internals/deprecation.txt3
-rw-r--r--docs/ref/forms/api.txt23
-rw-r--r--docs/ref/forms/index.txt1
-rw-r--r--docs/ref/forms/renderers.txt131
-rw-r--r--docs/ref/forms/widgets.txt156
-rw-r--r--docs/ref/settings.txt16
-rw-r--r--docs/releases/1.11.txt28
-rw-r--r--docs/spelling_wordlist1
-rw-r--r--tests/admin_inlines/tests.py26
-rw-r--r--tests/admin_views/tests.py2
-rw-r--r--tests/admin_widgets/tests.py62
-rw-r--r--tests/forms_tests/field_tests/test_filepathfield.py3
-rw-r--r--tests/forms_tests/jinja2/forms_tests/custom_widget.html1
-rw-r--r--tests/forms_tests/templates/forms_tests/custom_widget.html1
-rw-r--r--tests/forms_tests/tests/test_forms.py93
-rw-r--r--tests/forms_tests/tests/test_renderers.py52
-rw-r--r--tests/forms_tests/tests/test_widgets.py201
-rw-r--r--tests/forms_tests/widget_tests/base.py20
-rw-r--r--tests/forms_tests/widget_tests/test_select.py62
-rw-r--r--tests/model_forms/tests.py9
-rwxr-xr-xtests/runtests.py5
98 files changed, 1330 insertions, 870 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 206d66f15d..f732682b1c 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -216,6 +216,9 @@ INSTALLED_APPS = []
TEMPLATES = []
+# Default form rendering class.
+FORM_RENDERER = 'django.forms.renderers.DjangoTemplates'
+
# Default email address to use for various automated correspondence from
# the site managers.
DEFAULT_FROM_EMAIL = 'webmaster@localhost'
diff --git a/django/contrib/admin/templates/admin/widgets/clearable_file_input.html b/django/contrib/admin/templates/admin/widgets/clearable_file_input.html
new file mode 100644
index 0000000000..327b8ad16a
--- /dev/null
+++ b/django/contrib/admin/templates/admin/widgets/clearable_file_input.html
@@ -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 %}
diff --git a/django/contrib/admin/templates/admin/widgets/foreign_key_raw_id.html b/django/contrib/admin/templates/admin/widgets/foreign_key_raw_id.html
new file mode 100644
index 0000000000..fa641b7b09
--- /dev/null
+++ b/django/contrib/admin/templates/admin/widgets/foreign_key_raw_id.html
@@ -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 %}
diff --git a/django/contrib/admin/templates/admin/widgets/many_to_many_raw_id.html b/django/contrib/admin/templates/admin/widgets/many_to_many_raw_id.html
new file mode 100644
index 0000000000..0dd0331dcb
--- /dev/null
+++ b/django/contrib/admin/templates/admin/widgets/many_to_many_raw_id.html
@@ -0,0 +1 @@
+{% include 'admin/widgets/foreign_key_raw_id.html' %}
diff --git a/django/contrib/admin/templates/admin/widgets/radio.html b/django/contrib/admin/templates/admin/widgets/radio.html
new file mode 100644
index 0000000000..780899af44
--- /dev/null
+++ b/django/contrib/admin/templates/admin/widgets/radio.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiple_input.html" %}
diff --git a/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html b/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html
new file mode 100644
index 0000000000..727d3df793
--- /dev/null
+++ b/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html
@@ -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>
diff --git a/django/contrib/admin/templates/admin/widgets/split_datetime.html b/django/contrib/admin/templates/admin/widgets/split_datetime.html
new file mode 100644
index 0000000000..985f82d0ab
--- /dev/null
+++ b/django/contrib/admin/templates/admin/widgets/split_datetime.html
@@ -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>
diff --git a/django/contrib/admin/templates/admin/widgets/url.html b/django/contrib/admin/templates/admin/widgets/url.html
new file mode 100644
index 0000000000..554a9343fe
--- /dev/null
+++ b/django/contrib/admin/templates/admin/widgets/url.html
@@ -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 %}
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index d110ee46ba..5960f82d91 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -7,14 +7,11 @@ import copy
from django import forms
from django.db.models.deletion import CASCADE
-from django.forms.utils import flatatt
-from django.forms.widgets import RadioFieldRenderer
-from django.template.loader import render_to_string
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from django.utils import six
from django.utils.encoding import force_text
-from django.utils.html import format_html, format_html_join, smart_urlquote
+from django.utils.html import smart_urlquote
from django.utils.safestring import mark_safe
from django.utils.text import Truncator
from django.utils.translation import ugettext as _
@@ -37,17 +34,14 @@ class FilteredSelectMultiple(forms.SelectMultiple):
self.is_stacked = is_stacked
super(FilteredSelectMultiple, self).__init__(attrs, choices)
- def render(self, name, value, attrs=None):
- if attrs is None:
- attrs = {}
- attrs['class'] = 'selectfilter'
+ def get_context(self, name, value, attrs=None):
+ context = super(FilteredSelectMultiple, self).get_context(name, value, attrs)
+ context['widget']['attrs']['class'] = 'selectfilter'
if self.is_stacked:
- attrs['class'] += 'stacked'
-
- attrs['data-field-name'] = self.verbose_name
- attrs['data-is-stacked'] = int(self.is_stacked)
- output = super(FilteredSelectMultiple, self).render(name, value, attrs)
- return mark_safe(output)
+ context['widget']['attrs']['class'] += 'stacked'
+ context['widget']['attrs']['data-field-name'] = self.verbose_name
+ context['widget']['attrs']['data-is-stacked'] = int(self.is_stacked)
+ return context
class AdminDateWidget(forms.DateInput):
@@ -80,38 +74,27 @@ class AdminSplitDateTime(forms.SplitDateTimeWidget):
"""
A SplitDateTime Widget that has some admin-specific styling.
"""
+ template_name = 'admin/widgets/split_datetime.html'
+
def __init__(self, attrs=None):
widgets = [AdminDateWidget, AdminTimeWidget]
# Note that we're calling MultiWidget, not SplitDateTimeWidget, because
# we want to define widgets.
forms.MultiWidget.__init__(self, widgets, attrs)
- def format_output(self, rendered_widgets):
- return format_html('<p class="datetime">{} {}<br />{} {}</p>',
- _('Date:'), rendered_widgets[0],
- _('Time:'), rendered_widgets[1])
-
-
-class AdminRadioFieldRenderer(RadioFieldRenderer):
- def render(self):
- """Outputs a <ul> for this set of radio fields."""
- return format_html('<ul{}>\n{}\n</ul>',
- flatatt(self.attrs),
- format_html_join('\n', '<li>{}</li>',
- ((force_text(w),) for w in self)))
+ def get_context(self, name, value, attrs):
+ context = super(AdminSplitDateTime, self).get_context(name, value, attrs)
+ context['date_label'] = _('Date:')
+ context['time_label'] = _('Time:')
+ return context
class AdminRadioSelect(forms.RadioSelect):
- renderer = AdminRadioFieldRenderer
+ template_name = 'admin/widgets/radio.html'
class AdminFileWidget(forms.ClearableFileInput):
- template_with_initial = (
- '<p class="file-upload">%s</p>' % forms.ClearableFileInput.template_with_initial
- )
- template_with_clear = (
- '<span class="clearable-file-input">%s</span>' % forms.ClearableFileInput.template_with_clear
- )
+ template_name = 'admin/widgets/clearable_file_input.html'
def url_params_from_lookup_dict(lookups):
@@ -141,17 +124,17 @@ class ForeignKeyRawIdWidget(forms.TextInput):
A Widget for displaying ForeignKeys in the "raw_id" interface rather than
in a <select> box.
"""
+ template_name = 'admin/widgets/foreign_key_raw_id.html'
+
def __init__(self, rel, admin_site, attrs=None, using=None):
self.rel = rel
self.admin_site = admin_site
self.db = using
super(ForeignKeyRawIdWidget, self).__init__(attrs)
- def render(self, name, value, attrs=None):
+ def get_context(self, name, value, attrs=None):
+ context = super(ForeignKeyRawIdWidget, self).get_context(name, value, attrs)
rel_to = self.rel.model
- if attrs is None:
- attrs = {}
- extra = []
if rel_to in self.admin_site._registry:
# The related object is registered with the same AdminSite
related_url = reverse(
@@ -164,21 +147,16 @@ class ForeignKeyRawIdWidget(forms.TextInput):
params = self.url_parameters()
if params:
- url = '?' + '&amp;'.join('%s=%s' % (k, v) for k, v in params.items())
- else:
- url = ''
- if "class" not in attrs:
- attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript code looks for this hook.
- # TODO: "lookup_id_" is hard-coded here. This should instead use
- # the correct API to determine the ID dynamically.
- extra.append(
- '<a href="%s%s" class="related-lookup" id="lookup_id_%s" title="%s"></a>'
- % (related_url, url, name, _('Lookup'))
- )
- output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] + extra
- if value:
- output.append(self.label_for_value(value))
- return mark_safe(''.join(output))
+ related_url += '?' + '&amp;'.join(
+ '%s=%s' % (k, v) for k, v in params.items(),
+ )
+ context['related_url'] = mark_safe(related_url)
+ context['link_title'] = _('Lookup')
+ # The JavaScript code looks for this class.
+ context['widget']['attrs'].setdefault('class', 'vForeignKeyRawIdAdminField')
+ if context['widget']['value']:
+ context['link_label'], context['link_url'] = self.label_and_url_for_value(value)
+ return context
def base_url_parameters(self):
limit_choices_to = self.rel.limit_choices_to
@@ -192,17 +170,15 @@ class ForeignKeyRawIdWidget(forms.TextInput):
params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
return params
- def label_for_value(self, value):
+ def label_and_url_for_value(self, value):
key = self.rel.get_related_field().name
try:
obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
except (ValueError, self.rel.model.DoesNotExist):
- return ''
+ return '', ''
- label = '&nbsp;<strong>{}</strong>'
- text = Truncator(obj).words(14, truncate='...')
try:
- change_url = reverse(
+ url = reverse(
'%s:%s_%s_change' % (
self.admin_site.name,
obj._meta.app_label,
@@ -211,11 +187,9 @@ class ForeignKeyRawIdWidget(forms.TextInput):
args=(obj.pk,)
)
except NoReverseMatch:
- pass # Admin not registered for target model.
- else:
- text = format_html('<a href="{}">{}</a>', change_url, text)
+ url = '' # Admin not registered for target model.
- return format_html(label, text)
+ return Truncator(obj).words(14, truncate='...'), url
class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
@@ -223,36 +197,36 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
in a <select multiple> box.
"""
- def render(self, name, value, attrs=None):
- if attrs is None:
- attrs = {}
+ template_name = 'admin/widgets/many_to_many_raw_id.html'
+
+ def get_context(self, name, value, attrs=None):
+ context = super(ManyToManyRawIdWidget, self).get_context(name, value, attrs)
if self.rel.model in self.admin_site._registry:
# The related object is registered with the same AdminSite
- attrs['class'] = 'vManyToManyRawIdAdminField'
- if value:
- value = ','.join(force_text(v) for v in value)
- else:
- value = ''
- return super(ManyToManyRawIdWidget, self).render(name, value, attrs)
+ context['widget']['attrs']['class'] = 'vManyToManyRawIdAdminField'
+ return context
def url_parameters(self):
return self.base_url_parameters()
- def label_for_value(self, value):
- return ''
+ def label_and_url_for_value(self, value):
+ return '', ''
def value_from_datadict(self, data, files, name):
value = data.get(name)
if value:
return value.split(',')
+ def format_value(self, value):
+ return ','.join(force_text(v) for v in value) if value else ''
+
class RelatedFieldWidgetWrapper(forms.Widget):
"""
This class is a wrapper to a given widget to add the add icon for the
admin interface.
"""
- template = 'admin/related_widget_wrapper.html'
+ template_name = 'admin/widgets/related_widget_wrapper.html'
def __init__(self, widget, rel, admin_site, can_add_related=None,
can_change_related=False, can_delete_related=False):
@@ -294,21 +268,19 @@ class RelatedFieldWidgetWrapper(forms.Widget):
return reverse("admin:%s_%s_%s" % (info + (action,)),
current_app=self.admin_site.name, args=args)
- def render(self, name, value, *args, **kwargs):
+ def get_context(self, name, value, attrs=None):
+ with self.widget.override_choices(self.choices):
+ context = self.widget.get_context(name, value, attrs)
+
from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
rel_opts = self.rel.model._meta
info = (rel_opts.app_label, rel_opts.model_name)
- self.widget.choices = self.choices
url_params = '&'.join("%s=%s" % param for param in [
(TO_FIELD_VAR, self.rel.get_related_field().name),
(IS_POPUP_VAR, 1),
])
- context = {
- 'widget': self.widget.render(name, value, *args, **kwargs),
- 'name': name,
- 'url_params': url_params,
- 'model': rel_opts.verbose_name,
- }
+ context['url_params'] = url_params
+ context['model'] = rel_opts.verbose_name
if self.can_change_related:
change_related_template_url = self.get_related_url(info, 'change', '__fk__')
context.update(
@@ -327,12 +299,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
can_delete_related=True,
delete_related_template_url=delete_related_template_url,
)
- return mark_safe(render_to_string(self.template, context))
-
- def build_attrs(self, extra_attrs=None, **kwargs):
- "Helper function for building an attribute dictionary."
- self.attrs = self.widget.build_attrs(extra_attrs=None, **kwargs)
- return self.attrs
+ return context
def value_from_datadict(self, data, files, name):
return self.widget.value_from_datadict(data, files, name)
@@ -366,23 +333,24 @@ class AdminEmailInputWidget(forms.EmailInput):
class AdminURLFieldWidget(forms.URLInput):
+ template_name = 'admin/widgets/url.html'
+
def __init__(self, attrs=None):
final_attrs = {'class': 'vURLField'}
if attrs is not None:
final_attrs.update(attrs)
super(AdminURLFieldWidget, self).__init__(attrs=final_attrs)
- def render(self, name, value, attrs=None):
- html = super(AdminURLFieldWidget, self).render(name, value, attrs)
- if value:
- value = force_text(self.format_value(value))
- final_attrs = {'href': smart_urlquote(value)}
- html = format_html(
- '<p class="url">{} <a{}>{}</a><br />{} {}</p>',
- _('Currently:'), flatatt(final_attrs), value,
- _('Change:'), html
- )
- return html
+ def get_context(self, name, value, attrs):
+ context = super(AdminURLFieldWidget, self).get_context(name, value, attrs)
+ context['current_label'] = _('Currently:')
+ context['change_label'] = _('Change:')
+ context['widget']['href'] = smart_urlquote(context['widget']['value'])
+ return context
+
+ def format_value(self, value):
+ value = super(AdminURLFieldWidget, self).format_value(value)
+ return force_text(value)
class AdminIntegerFieldWidget(forms.NumberInput):
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index a5d0375f58..02250d83da 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -13,12 +13,9 @@ from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMultiAlternatives
-from django.forms.utils import flatatt
from django.template import loader
from django.utils.encoding import force_bytes
-from django.utils.html import format_html, format_html_join
from django.utils.http import urlsafe_base64_encode
-from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext, ugettext_lazy as _
@@ -26,26 +23,23 @@ UserModel = get_user_model()
class ReadOnlyPasswordHashWidget(forms.Widget):
- def render(self, name, value, attrs):
- encoded = value
- final_attrs = self.build_attrs(attrs)
+ template_name = 'auth/widgets/read_only_password_hash.html'
- if not encoded or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
- summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
+ def get_context(self, name, value, attrs):
+ context = super(ReadOnlyPasswordHashWidget, self).get_context(name, value, attrs)
+ summary = []
+ if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX):
+ summary.append({'label': ugettext("No password set.")})
else:
try:
- hasher = identify_hasher(encoded)
+ hasher = identify_hasher(value)
except ValueError:
- summary = mark_safe("<strong>%s</strong>" % ugettext(
- "Invalid password format or unknown hashing algorithm."
- ))
+ summary.append({'label': ugettext("Invalid password format or unknown hashing algorithm.")})
else:
- summary = format_html_join(
- '', '<strong>{}</strong>: {} ',
- ((ugettext(key), value) for key, value in hasher.safe_summary(encoded).items())
- )
-
- return format_html("<div{}>{}</div>", flatatt(final_attrs), summary)
+ for key, value_ in hasher.safe_summary(value).items():
+ summary.append({'label': ugettext(key), 'value': value_})
+ context['summary'] = summary
+ return context
class ReadOnlyPasswordHashField(forms.Field):
diff --git a/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html b/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html
new file mode 100644
index 0000000000..b411298d74
--- /dev/null
+++ b/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html
@@ -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 %}
diff --git a/django/contrib/gis/admin/options.py b/django/contrib/gis/admin/options.py
index 4b99ddf354..4ae61661d3 100644
--- a/django/contrib/gis/admin/options.py
+++ b/django/contrib/gis/admin/options.py
@@ -80,7 +80,7 @@ class GeoModelAdmin(ModelAdmin):
collection_type = 'None'
class OLMap(self.widget):
- template = self.map_template
+ template_name = self.map_template
geom_type = db_field.geom_type
wms_options = ''
diff --git a/django/contrib/gis/admin/widgets.py b/django/contrib/gis/admin/widgets.py
index bf6340d239..014b3ad818 100644
--- a/django/contrib/gis/admin/widgets.py
+++ b/django/contrib/gis/admin/widgets.py
@@ -3,7 +3,6 @@ import logging
from django.contrib.gis.gdal import GDALException
from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.forms.widgets import Textarea
-from django.template import loader
from django.utils import six, translation
# Creating a template context that contains Django settings
@@ -16,7 +15,7 @@ class OpenLayersWidget(Textarea):
"""
Renders an OpenLayers map using the WKT of the geometry.
"""
- def render(self, name, value, attrs=None):
+ def get_context(self, name, value, attrs=None):
# Update the template parameters with any attributes passed in.
if attrs:
self.params.update(attrs)
@@ -77,7 +76,7 @@ class OpenLayersWidget(Textarea):
self.params['wkt'] = wkt
self.params.update(geo_context)
- return loader.render_to_string(self.template, self.params)
+ return self.params
def map_options(self):
"Builds the map options hash for the OpenLayers template."
diff --git a/django/contrib/gis/forms/widgets.py b/django/contrib/gis/forms/widgets.py
index 7b58d5a477..37e58d9b74 100644
--- a/django/contrib/gis/forms/widgets.py
+++ b/django/contrib/gis/forms/widgets.py
@@ -6,7 +6,6 @@ from django.conf import settings
from django.contrib.gis import gdal
from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.forms.widgets import Widget
-from django.template import loader
from django.utils import six, translation
logger = logging.getLogger('django.contrib.gis')
@@ -43,7 +42,7 @@ class BaseGeometryWidget(Widget):
logger.error("Error creating geometry from value '%s' (%s)", value, err)
return None
- def render(self, name, value, attrs=None):
+ def get_context(self, name, value, attrs=None):
# If a string reaches here (via a validation error on another
# field) then just reconstruct the Geometry.
if value and isinstance(value, six.string_types):
@@ -62,16 +61,19 @@ class BaseGeometryWidget(Widget):
value.srid, self.map_srid, err
)
- context = self.build_attrs(
- attrs,
+ if attrs is None:
+ attrs = {}
+
+ context = self.build_attrs(self.attrs, dict(
name=name,
module='geodjango_%s' % name.replace('-', '_'), # JS-safe
serialized=self.serialize(value),
geom_type=gdal.OGRGeomType(self.attrs['geom_type']),
STATIC_URL=settings.STATIC_URL,
LANGUAGE_BIDI=translation.get_language_bidi(),
- )
- return loader.render_to_string(self.template_name, context)
+ **attrs
+ ))
+ return context
class OpenLayersWidget(BaseGeometryWidget):
diff --git a/django/contrib/postgres/forms/array.py b/django/contrib/postgres/forms/array.py
index d22d9081e2..9830c8de48 100644
--- a/django/contrib/postgres/forms/array.py
+++ b/django/contrib/postgres/forms/array.py
@@ -117,7 +117,7 @@ class SplitArrayWidget(forms.Widget):
id_ += '_0'
return id_
- def render(self, name, value, attrs=None):
+ def render(self, name, value, attrs=None, renderer=None):
if self.is_localized:
self.widget.is_localized = self.is_localized
value = value or []
@@ -131,7 +131,7 @@ class SplitArrayWidget(forms.Widget):
widget_value = None
if id_:
final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
- output.append(self.widget.render(name + '_%s' % i, widget_value, final_attrs))
+ output.append(self.widget.render(name + '_%s' % i, widget_value, final_attrs, renderer))
return mark_safe(self.format_output(output))
def format_output(self, rendered_widgets):
diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py
index b6be395a67..a1063b2b05 100644
--- a/django/forms/boundfield.py
+++ b/django/forms/boundfield.py
@@ -1,13 +1,16 @@
from __future__ import unicode_literals
import datetime
+import warnings
from django.forms.utils import flatatt, pretty_name
from django.forms.widgets import Textarea, TextInput
from django.utils import six
+from django.utils.deprecation import RemovedInDjango21Warning
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html, html_safe
+from django.utils.inspect import func_supports_parameter
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
@@ -49,7 +52,10 @@ class BoundField(object):
id_ = self.field.widget.attrs.get('id') or self.auto_id
attrs = {'id': id_} if id_ else {}
attrs = self.build_widget_attrs(attrs)
- return list(self.field.widget.subwidgets(self.html_name, self.value(), attrs))
+ return list(
+ BoundWidget(self.field.widget, widget, self.form.renderer)
+ for widget in self.field.widget.subwidgets(self.html_name, self.value(), attrs=attrs)
+ )
def __iter__(self):
return iter(self.subwidgets)
@@ -97,7 +103,23 @@ class BoundField(object):
name = self.html_name
else:
name = self.html_initial_name
- return force_text(widget.render(name, self.value(), attrs=attrs))
+
+ kwargs = {}
+ if func_supports_parameter(widget.render, 'renderer'):
+ kwargs['renderer'] = self.form.renderer
+ else:
+ warnings.warn(
+ 'Add the `renderer` argument to the render() method of %s. '
+ 'It will be mandatory in Django 2.1.' % widget.__class__,
+ RemovedInDjango21Warning, stacklevel=2,
+ )
+ html = widget.render(
+ name=name,
+ value=self.value(),
+ attrs=attrs,
+ **kwargs
+ )
+ return force_text(html)
def as_text(self, attrs=None, **kwargs):
"""
@@ -230,3 +252,45 @@ class BoundField(object):
if self.field.disabled:
attrs['disabled'] = True
return attrs
+
+
+@html_safe
+@python_2_unicode_compatible
+class BoundWidget(object):
+ """
+ A container class used for iterating over widgets. This is useful for
+ widgets that have choices. For example, the following can be used in a
+ template:
+
+ {% for radio in myform.beatles %}
+ <label for="{{ radio.id_for_label }}">
+ {{ radio.choice_label }}
+ <span class="radio">{{ radio.tag }}</span>
+ </label>
+ {% endfor %}
+ """
+ def __init__(self, parent_widget, data, renderer):
+ self.parent_widget = parent_widget
+ self.data = data
+ self.renderer = renderer
+
+ def __str__(self):
+ return self.tag(wrap_label=True)
+
+ def tag(self, wrap_label=False):
+ context = {'widget': self.data, 'wrap_label': wrap_label}
+ return self.parent_widget._render(self.template_name, context, self.renderer)
+
+ @property
+ def template_name(self):
+ if 'template_name' in self.data:
+ return self.data['template_name']
+ return self.parent_widget.template_name
+
+ @property
+ def id_for_label(self):
+ return 'id_%s_%s' % (self.data['name'], self.data['index'])
+
+ @property
+ def choice_label(self):
+ return self.data['label']
diff --git a/django/forms/forms.py b/django/forms/forms.py
index 17d4598f4c..8044349517 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -21,6 +21,8 @@ from django.utils.html import conditional_escape, html_safe
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
+from .renderers import get_default_renderer
+
__all__ = ('BaseForm', 'Form')
@@ -65,13 +67,14 @@ class BaseForm(object):
# class is different than Form. See the comments by the Form class for more
# information. Any improvements to the form API should be made to *this*
# class, not to the Form class.
+ default_renderer = None
field_order = None
prefix = None
use_required_attribute = True
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
- empty_permitted=False, field_order=None, use_required_attribute=None):
+ empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None):
self.is_bound = data is not None or files is not None
self.data = data or {}
self.files = files or {}
@@ -97,6 +100,17 @@ class BaseForm(object):
if use_required_attribute is not None:
self.use_required_attribute = use_required_attribute
+ # Initialize form renderer. Use a global default if not specified
+ # either as an argument or as self.default_renderer.
+ if renderer is None:
+ if self.default_renderer is None:
+ renderer = get_default_renderer()
+ else:
+ renderer = self.default_renderer
+ if isinstance(self.default_renderer, type):
+ renderer = renderer()
+ self.renderer = renderer
+
def order_fields(self, field_order):
"""
Rearranges the fields according to field_order.
diff --git a/django/forms/jinja2/django/forms/widgets/attrs.html b/django/forms/jinja2/django/forms/widgets/attrs.html
new file mode 100644
index 0000000000..b45d30c449
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/attrs.html
@@ -0,0 +1 @@
+{% for name, value in widget.attrs.items() %} {{ name }}{% if not value is sameas True %}="{{ value }}"{% endif %}{% endfor %}
diff --git a/django/forms/jinja2/django/forms/widgets/checkbox.html b/django/forms/jinja2/django/forms/widgets/checkbox.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/checkbox.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/checkbox_option.html b/django/forms/jinja2/django/forms/widgets/checkbox_option.html
new file mode 100644
index 0000000000..bb9acbafd9
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/checkbox_option.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input_option.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/checkbox_select.html b/django/forms/jinja2/django/forms/widgets/checkbox_select.html
new file mode 100644
index 0000000000..780899af44
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/checkbox_select.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiple_input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/clearable_file_input.html b/django/forms/jinja2/django/forms/widgets/clearable_file_input.html
new file mode 100644
index 0000000000..05f2c2dbe5
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/clearable_file_input.html
@@ -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" %} />
diff --git a/django/forms/jinja2/django/forms/widgets/date.html b/django/forms/jinja2/django/forms/widgets/date.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/date.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/datetime.html b/django/forms/jinja2/django/forms/widgets/datetime.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/datetime.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/email.html b/django/forms/jinja2/django/forms/widgets/email.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/email.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/file.html b/django/forms/jinja2/django/forms/widgets/file.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/file.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/hidden.html b/django/forms/jinja2/django/forms/widgets/hidden.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/hidden.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/input.html b/django/forms/jinja2/django/forms/widgets/input.html
new file mode 100644
index 0000000000..7e70d1953f
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/input.html
@@ -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" %} />
diff --git a/django/forms/jinja2/django/forms/widgets/input_option.html b/django/forms/jinja2/django/forms/widgets/input_option.html
new file mode 100644
index 0000000000..3f7085a4f0
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/input_option.html
@@ -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 %}
diff --git a/django/forms/jinja2/django/forms/widgets/multiple_hidden.html b/django/forms/jinja2/django/forms/widgets/multiple_hidden.html
new file mode 100644
index 0000000000..b9695deb02
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/multiple_hidden.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiwidget.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/multiple_input.html b/django/forms/jinja2/django/forms/widgets/multiple_input.html
new file mode 100644
index 0000000000..be3d449926
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/multiple_input.html
@@ -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>
diff --git a/django/forms/jinja2/django/forms/widgets/multiwidget.html b/django/forms/jinja2/django/forms/widgets/multiwidget.html
new file mode 100644
index 0000000000..0030711182
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/multiwidget.html
@@ -0,0 +1 @@
+{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}
diff --git a/django/forms/jinja2/django/forms/widgets/number.html b/django/forms/jinja2/django/forms/widgets/number.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/number.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/password.html b/django/forms/jinja2/django/forms/widgets/password.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/password.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/radio.html b/django/forms/jinja2/django/forms/widgets/radio.html
new file mode 100644
index 0000000000..780899af44
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/radio.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiple_input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/radio_option.html b/django/forms/jinja2/django/forms/widgets/radio_option.html
new file mode 100644
index 0000000000..bb9acbafd9
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/radio_option.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input_option.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/select.html b/django/forms/jinja2/django/forms/widgets/select.html
new file mode 100644
index 0000000000..ea3bc84113
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/select.html
@@ -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>
diff --git a/django/forms/jinja2/django/forms/widgets/select_date.html b/django/forms/jinja2/django/forms/widgets/select_date.html
new file mode 100644
index 0000000000..32fda82609
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/select_date.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
diff --git a/django/forms/jinja2/django/forms/widgets/select_option.html b/django/forms/jinja2/django/forms/widgets/select_option.html
new file mode 100644
index 0000000000..c6355f69dd
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/select_option.html
@@ -0,0 +1 @@
+<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}>{{ widget.label }}</option>
diff --git a/django/forms/jinja2/django/forms/widgets/splitdatetime.html b/django/forms/jinja2/django/forms/widgets/splitdatetime.html
new file mode 100644
index 0000000000..32fda82609
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/splitdatetime.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
diff --git a/django/forms/jinja2/django/forms/widgets/splithiddendatetime.html b/django/forms/jinja2/django/forms/widgets/splithiddendatetime.html
new file mode 100644
index 0000000000..32fda82609
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/splithiddendatetime.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
diff --git a/django/forms/jinja2/django/forms/widgets/text.html b/django/forms/jinja2/django/forms/widgets/text.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/text.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/textarea.html b/django/forms/jinja2/django/forms/widgets/textarea.html
new file mode 100644
index 0000000000..b86766c894
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/textarea.html
@@ -0,0 +1,2 @@
+<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
+{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
diff --git a/django/forms/jinja2/django/forms/widgets/time.html b/django/forms/jinja2/django/forms/widgets/time.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/time.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/jinja2/django/forms/widgets/url.html b/django/forms/jinja2/django/forms/widgets/url.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/jinja2/django/forms/widgets/url.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/renderers.py b/django/forms/renderers.py
new file mode 100644
index 0000000000..d0b3c3e2db
--- /dev/null
+++ b/django/forms/renderers.py
@@ -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)
diff --git a/django/forms/templates/django/forms/widgets/attrs.html b/django/forms/templates/django/forms/widgets/attrs.html
new file mode 100644
index 0000000000..e673399dbb
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/attrs.html
@@ -0,0 +1 @@
+{% for name, value in widget.attrs.items %} {{ name }}{% if not value is True %}="{{ value }}"{% endif %}{% endfor %}
diff --git a/django/forms/templates/django/forms/widgets/checkbox.html b/django/forms/templates/django/forms/widgets/checkbox.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/checkbox.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/checkbox_option.html b/django/forms/templates/django/forms/widgets/checkbox_option.html
new file mode 100644
index 0000000000..bb9acbafd9
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/checkbox_option.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input_option.html" %}
diff --git a/django/forms/templates/django/forms/widgets/checkbox_select.html b/django/forms/templates/django/forms/widgets/checkbox_select.html
new file mode 100644
index 0000000000..780899af44
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/checkbox_select.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiple_input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/clearable_file_input.html b/django/forms/templates/django/forms/widgets/clearable_file_input.html
new file mode 100644
index 0000000000..05f2c2dbe5
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/clearable_file_input.html
@@ -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" %} />
diff --git a/django/forms/templates/django/forms/widgets/date.html b/django/forms/templates/django/forms/widgets/date.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/date.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/datetime.html b/django/forms/templates/django/forms/widgets/datetime.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/datetime.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/email.html b/django/forms/templates/django/forms/widgets/email.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/email.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/file.html b/django/forms/templates/django/forms/widgets/file.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/file.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/hidden.html b/django/forms/templates/django/forms/widgets/hidden.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/hidden.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/input.html b/django/forms/templates/django/forms/widgets/input.html
new file mode 100644
index 0000000000..7e70d1953f
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/input.html
@@ -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" %} />
diff --git a/django/forms/templates/django/forms/widgets/input_option.html b/django/forms/templates/django/forms/widgets/input_option.html
new file mode 100644
index 0000000000..3f7085a4f0
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/input_option.html
@@ -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 %}
diff --git a/django/forms/templates/django/forms/widgets/multiple_hidden.html b/django/forms/templates/django/forms/widgets/multiple_hidden.html
new file mode 100644
index 0000000000..b9695deb02
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/multiple_hidden.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiwidget.html" %}
diff --git a/django/forms/templates/django/forms/widgets/multiple_input.html b/django/forms/templates/django/forms/widgets/multiple_input.html
new file mode 100644
index 0000000000..60282ff887
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/multiple_input.html
@@ -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 %}
diff --git a/django/forms/templates/django/forms/widgets/multiwidget.html b/django/forms/templates/django/forms/widgets/multiwidget.html
new file mode 100644
index 0000000000..0030711182
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/multiwidget.html
@@ -0,0 +1 @@
+{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}
diff --git a/django/forms/templates/django/forms/widgets/number.html b/django/forms/templates/django/forms/widgets/number.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/number.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/password.html b/django/forms/templates/django/forms/widgets/password.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/password.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/radio.html b/django/forms/templates/django/forms/widgets/radio.html
new file mode 100644
index 0000000000..780899af44
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/radio.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiple_input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/radio_option.html b/django/forms/templates/django/forms/widgets/radio_option.html
new file mode 100644
index 0000000000..bb9acbafd9
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/radio_option.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input_option.html" %}
diff --git a/django/forms/templates/django/forms/widgets/select.html b/django/forms/templates/django/forms/widgets/select.html
new file mode 100644
index 0000000000..4d1f6b057b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/select.html
@@ -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>
diff --git a/django/forms/templates/django/forms/widgets/select_date.html b/django/forms/templates/django/forms/widgets/select_date.html
new file mode 100644
index 0000000000..32fda82609
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/select_date.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
diff --git a/django/forms/templates/django/forms/widgets/select_option.html b/django/forms/templates/django/forms/widgets/select_option.html
new file mode 100644
index 0000000000..c6355f69dd
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/select_option.html
@@ -0,0 +1 @@
+<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}>{{ widget.label }}</option>
diff --git a/django/forms/templates/django/forms/widgets/splitdatetime.html b/django/forms/templates/django/forms/widgets/splitdatetime.html
new file mode 100644
index 0000000000..32fda82609
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/splitdatetime.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
diff --git a/django/forms/templates/django/forms/widgets/splithiddendatetime.html b/django/forms/templates/django/forms/widgets/splithiddendatetime.html
new file mode 100644
index 0000000000..32fda82609
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/splithiddendatetime.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
diff --git a/django/forms/templates/django/forms/widgets/text.html b/django/forms/templates/django/forms/widgets/text.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/text.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/textarea.html b/django/forms/templates/django/forms/widgets/textarea.html
new file mode 100644
index 0000000000..b86766c894
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/textarea.html
@@ -0,0 +1,2 @@
+<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
+{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
diff --git a/django/forms/templates/django/forms/widgets/time.html b/django/forms/templates/django/forms/widgets/time.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/time.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/templates/django/forms/widgets/url.html b/django/forms/templates/django/forms/widgets/url.html
new file mode 100644
index 0000000000..08b1e61c0b
--- /dev/null
+++ b/django/forms/templates/django/forms/widgets/url.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 5c593cca71..dd68662d43 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -7,10 +7,11 @@ from __future__ import unicode_literals
import copy
import datetime
import re
+from contextlib import contextmanager
from itertools import chain
from django.conf import settings
-from django.forms.utils import flatatt, to_current_timezone
+from django.forms.utils import to_current_timezone
from django.templatetags.static import static
from django.utils import datetime_safe, formats, six
from django.utils.dates import MONTHS
@@ -21,11 +22,13 @@ from django.utils.encoding import (
force_str, force_text, python_2_unicode_compatible,
)
from django.utils.formats import get_format
-from django.utils.html import conditional_escape, format_html, html_safe
+from django.utils.html import format_html, html_safe
from django.utils.safestring import mark_safe
from django.utils.six.moves import range
from django.utils.translation import ugettext_lazy
+from .renderers import get_default_renderer
+
__all__ = (
'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'NumberInput',
'EmailInput', 'URLInput', 'PasswordInput', 'HiddenInput',
@@ -157,25 +160,6 @@ class MediaDefiningClass(type):
return new_class
-@html_safe
-@python_2_unicode_compatible
-class SubWidget(object):
- """
- Some widgets are made of multiple HTML elements -- namely, RadioSelect.
- This is a class that represents the "inner" HTML element of a widget.
- """
- def __init__(self, parent_widget, name, value, attrs, choices):
- self.parent_widget = parent_widget
- self.name, self.value = name, value
- self.attrs, self.choices = attrs, choices
-
- def __str__(self):
- args = [self.name, self.value, self.attrs]
- if self.choices:
- args.append(self.choices)
- return self.parent_widget.render(*args)
-
-
class RenameWidgetMethods(MediaDefiningClass, RenameMethodsBase):
renamed_methods = (
('_format_value', 'format_value', RemovedInDjango20Warning),
@@ -204,28 +188,48 @@ class Widget(six.with_metaclass(RenameWidgetMethods)):
def is_hidden(self):
return self.input_type == 'hidden' if hasattr(self, 'input_type') else False
- def subwidgets(self, name, value, attrs=None, choices=()):
- """
- Yields all "subwidgets" of this widget. Used only by RadioSelect to
- allow template access to individual <input type="radio"> buttons.
+ def subwidgets(self, name, value, attrs=None):
+ context = self.get_context(name, value, attrs)
+ yield context['widget']
- Arguments are the same as for render().
+ def format_value(self, value):
+ """
+ Return a value as it should appear when rendered in a template.
"""
- yield SubWidget(self, name, value, attrs, choices)
+ if value is None:
+ value = ''
+ if self.is_localized:
+ return formats.localize_input(value)
+ return force_text(value)
+
+ def get_context(self, name, value, attrs=None):
+ context = {}
+ context['widget'] = {
+ 'name': name,
+ 'is_hidden': self.is_hidden,
+ 'required': self.is_required,
+ 'value': self.format_value(value),
+ 'attrs': self.build_attrs(self.attrs, attrs),
+ 'template_name': self.template_name,
+ }
+ return context
- def render(self, name, value, attrs=None):
+ def render(self, name, value, attrs=None, renderer=None):
"""
Returns this Widget rendered as HTML, as a Unicode string.
-
- The 'value' given is not guaranteed to be valid input, so subclass
- implementations should program defensively.
"""
- raise NotImplementedError('subclasses of Widget must provide a render() method')
+ context = self.get_context(name, value, attrs)
+ return self._render(self.template_name, context, renderer)
+
+ def _render(self, template_name, context, renderer=None):
+ if renderer is None:
+ renderer = get_default_renderer()
+ return mark_safe(renderer.render(template_name, context))
- def build_attrs(self, extra_attrs=None, **kwargs):
+ def build_attrs(self, base_attrs, extra_attrs=None):
"Helper function for building an attribute dictionary."
- attrs = dict(self.attrs, **kwargs)
- if extra_attrs:
+ attrs = base_attrs.copy()
+ if extra_attrs is not None:
attrs.update(extra_attrs)
return attrs
@@ -257,62 +261,59 @@ class Widget(six.with_metaclass(RenameWidgetMethods)):
class Input(Widget):
"""
- Base class for all <input> widgets (except type='checkbox' and
- type='radio', which are special).
+ Base class for all <input> widgets.
"""
input_type = None # Subclasses must define this.
+ template_name = 'django/forms/widgets/input.html'
- def format_value(self, value):
- if self.is_localized:
- return formats.localize_input(value)
- return value
+ def __init__(self, attrs=None):
+ if attrs is not None:
+ self.input_type = attrs.pop('type', self.input_type)
+ super(Input, self).__init__(attrs)
- def render(self, name, value, attrs=None):
- if value is None:
- value = ''
- final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
- if value != '':
- # Only add the 'value' attribute if a value is non-empty.
- final_attrs['value'] = force_text(self.format_value(value))
- return format_html('<input{} />', flatatt(final_attrs))
+ def get_context(self, name, value, attrs=None):
+ context = super(Input, self).get_context(name, value, attrs)
+ context['widget']['type'] = self.input_type
+ return context
class TextInput(Input):
input_type = 'text'
+ template_name = 'django/forms/widgets/text.html'
- def __init__(self, attrs=None):
- if attrs is not None:
- self.input_type = attrs.pop('type', self.input_type)
- super(TextInput, self).__init__(attrs)
-
-class NumberInput(TextInput):
+class NumberInput(Input):
input_type = 'number'
+ template_name = 'django/forms/widgets/number.html'
-class EmailInput(TextInput):
+class EmailInput(Input):
input_type = 'email'
+ template_name = 'django/forms/widgets/email.html'
-class URLInput(TextInput):
+class URLInput(Input):
input_type = 'url'
+ template_name = 'django/forms/widgets/url.html'
-class PasswordInput(TextInput):
+class PasswordInput(Input):
input_type = 'password'
+ template_name = 'django/forms/widgets/password.html'
def __init__(self, attrs=None, render_value=False):
super(PasswordInput, self).__init__(attrs)
self.render_value = render_value
- def render(self, name, value, attrs=None):
+ def get_context(self, name, value, attrs):
if not self.render_value:
value = None
- return super(PasswordInput, self).render(name, value, attrs)
+ return super(PasswordInput, self).get_context(name, value, attrs)
class HiddenInput(Input):
input_type = 'hidden'
+ template_name = 'django/forms/widgets/hidden.html'
class MultipleHiddenInput(HiddenInput):
@@ -320,20 +321,26 @@ class MultipleHiddenInput(HiddenInput):
A widget that handles <input type="hidden"> for fields that have a list
of values.
"""
- def render(self, name, value, attrs=None):
- if value is None:
- value = []
- final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
- id_ = final_attrs.get('id')
- inputs = []
- for i, v in enumerate(value):
- input_attrs = dict(value=force_text(v), **final_attrs)
+ template_name = 'django/forms/widgets/multiple_hidden.html'
+
+ def get_context(self, name, value, attrs=None):
+ context = super(MultipleHiddenInput, self).get_context(name, value, attrs)
+ final_attrs = context['widget']['attrs']
+ id_ = context['widget']['attrs'].get('id')
+
+ subwidgets = []
+ for index, value_ in enumerate(context['widget']['value']):
+ widget_attrs = final_attrs.copy()
if id_:
# An ID attribute was given. Add a numeric index as a suffix
# so that the inputs don't all have the same ID attribute.
- input_attrs['id'] = '%s_%s' % (id_, i)
- inputs.append(format_html('<input{} />', flatatt(input_attrs)))
- return mark_safe('\n'.join(inputs))
+ widget_attrs['id'] = '%s_%s' % (id_, index)
+ widget = HiddenInput()
+ widget.is_required = self.is_required
+ subwidgets.append(widget.get_context(name, value_, widget_attrs)['widget'])
+
+ context['widget']['subwidgets'] = subwidgets
+ return context
def value_from_datadict(self, data, files, name):
try:
@@ -342,13 +349,18 @@ class MultipleHiddenInput(HiddenInput):
getter = data.get
return getter(name)
+ def format_value(self, value):
+ return [] if value is None else value
+
class FileInput(Input):
input_type = 'file'
needs_multipart_form = True
+ template_name = 'django/forms/widgets/file.html'
- def render(self, name, value, attrs=None):
- return super(FileInput, self).render(name, None, attrs=attrs)
+ def format_value(self, value):
+ """File input never renders a value."""
+ return
def value_from_datadict(self, data, files, name):
"File widgets take data from FILES, not POST"
@@ -362,16 +374,10 @@ FILE_INPUT_CONTRADICTION = object()
class ClearableFileInput(FileInput):
+ clear_checkbox_label = ugettext_lazy('Clear')
initial_text = ugettext_lazy('Currently')
input_text = ugettext_lazy('Change')
- clear_checkbox_label = ugettext_lazy('Clear')
-
- template_with_initial = (
- '%(initial_text)s: <a href="%(initial_url)s">%(initial)s</a> '
- '%(clear_template)s<br />%(input_text)s: %(input)s'
- )
-
- template_with_clear = '%(clear)s <label for="%(clear_checkbox_id)s">%(clear_checkbox_label)s</label>'
+ template_name = 'django/forms/widgets/clearable_file_input.html'
def clear_checkbox_name(self, name):
"""
@@ -392,37 +398,26 @@ class ClearableFileInput(FileInput):
"""
return bool(value and getattr(value, 'url', False))
- def get_template_substitution_values(self, value):
+ def format_value(self, value):
"""
- Return value-related substitutions.
+ Return the file object if it has a defined url attribute.
"""
- return {
- 'initial': conditional_escape(value),
- 'initial_url': conditional_escape(value.url),
- }
+ if self.is_initial(value):
+ return value
- def render(self, name, value, attrs=None):
- substitutions = {
- 'initial_text': self.initial_text,
+ def get_context(self, name, value, attrs=None):
+ context = super(ClearableFileInput, self).get_context(name, value, attrs)
+ checkbox_name = self.clear_checkbox_name(name)
+ checkbox_id = self.clear_checkbox_id(checkbox_name)
+ context.update({
+ 'checkbox_name': checkbox_name,
+ 'checkbox_id': checkbox_id,
+ 'is_initial': self.is_initial(value),
'input_text': self.input_text,
- 'clear_template': '',
+ 'initial_text': self.initial_text,
'clear_checkbox_label': self.clear_checkbox_label,
- }
- template = '%(input)s'
- substitutions['input'] = super(ClearableFileInput, self).render(name, value, attrs)
-
- if self.is_initial(value):
- template = self.template_with_initial
- substitutions.update(self.get_template_substitution_values(value))
- if not self.is_required:
- checkbox_name = self.clear_checkbox_name(name)
- checkbox_id = self.clear_checkbox_id(checkbox_name)
- substitutions['clear_checkbox_name'] = conditional_escape(checkbox_name)
- substitutions['clear_checkbox_id'] = conditional_escape(checkbox_id)
- substitutions['clear'] = CheckboxInput().render(checkbox_name, False, attrs={'id': checkbox_id})
- substitutions['clear_template'] = self.template_with_clear % substitutions
-
- return mark_safe(template % substitutions)
+ })
+ return context
def value_from_datadict(self, data, files, name):
upload = super(ClearableFileInput, self).value_from_datadict(data, files, name)
@@ -443,6 +438,8 @@ class ClearableFileInput(FileInput):
class Textarea(Widget):
+ template_name = 'django/forms/widgets/textarea.html'
+
def __init__(self, attrs=None):
# Use slightly better defaults than HTML's 20x2 box
default_attrs = {'cols': '40', 'rows': '10'}
@@ -450,12 +447,6 @@ class Textarea(Widget):
default_attrs.update(attrs)
super(Textarea, self).__init__(default_attrs)
- def render(self, name, value, attrs=None):
- if value is None:
- value = ''
- final_attrs = self.build_attrs(attrs, name=name)
- return format_html('<textarea{}>\r\n{}</textarea>', flatatt(final_attrs), force_text(value))
-
class DateTimeBaseInput(TextInput):
format_key = ''
@@ -471,14 +462,17 @@ class DateTimeBaseInput(TextInput):
class DateInput(DateTimeBaseInput):
format_key = 'DATE_INPUT_FORMATS'
+ template_name = 'django/forms/widgets/date.html'
class DateTimeInput(DateTimeBaseInput):
format_key = 'DATETIME_INPUT_FORMATS'
+ template_name = 'django/forms/widgets/datetime.html'
class TimeInput(DateTimeBaseInput):
format_key = 'TIME_INPUT_FORMATS'
+ template_name = 'django/forms/widgets/time.html'
# Defined at module level so that CheckboxInput is picklable (#17976)
@@ -486,19 +480,28 @@ def boolean_check(v):
return not (v is False or v is None or v == '')
-class CheckboxInput(Widget):
+class CheckboxInput(Input):
+ input_type = 'checkbox'
+ template_name = 'django/forms/widgets/checkbox.html'
+
def __init__(self, attrs=None, check_test=None):
super(CheckboxInput, self).__init__(attrs)
# check_test is a callable that takes a value and returns True
# if the checkbox should be checked for that value.
self.check_test = boolean_check if check_test is None else check_test
- def render(self, name, value, attrs=None):
- final_attrs = self.build_attrs(attrs, type='checkbox', name=name, checked=self.check_test(value))
- if not (value is True or value is False or value is None or value == ''):
- # Only add the 'value' attribute if a value is non-empty.
- final_attrs['value'] = force_text(value)
- return format_html('<input{} />', flatatt(final_attrs))
+ def format_value(self, value):
+ """Only return the 'value' attribute if value isn't empty."""
+ if value is True or value is False or value is None or value == '':
+ return
+ return force_text(value)
+
+ def get_context(self, name, value, attrs=None):
+ if self.check_test(value):
+ if attrs is None:
+ attrs = {}
+ attrs['checked'] = True
+ return super(CheckboxInput, self).get_context(name, value, attrs)
def value_from_datadict(self, data, files, name):
if name not in data:
@@ -518,11 +521,17 @@ class CheckboxInput(Widget):
return False
-class Select(Widget):
+class ChoiceWidget(Widget):
allow_multiple_selected = False
+ input_type = None
+ template_name = None
+ option_template_name = None
+ add_id_index = True
+ checked_attribute = {'checked': True}
+ option_inherits_attrs = True
def __init__(self, attrs=None, choices=()):
- super(Select, self).__init__(attrs)
+ super(ChoiceWidget, self).__init__(attrs)
# choices can be any iterable, but we may need to render this widget
# multiple times. Thus, collapse it into a list so it can be consumed
# more than once.
@@ -535,43 +544,141 @@ class Select(Widget):
memo[id(self)] = obj
return obj
- def render(self, name, value, attrs=None):
- if value is None:
- value = ''
- final_attrs = self.build_attrs(attrs, name=name)
- output = [format_html('<select{}>', flatatt(final_attrs))]
- options = self.render_options([value])
- if options:
- output.append(options)
- output.append('</select>')
- return mark_safe('\n'.join(output))
+ def subwidgets(self, name, value, attrs=None):
+ """
+ Yield all "subwidgets" of this widget. Used to enable iterating
+ options from a BoundField for choice widgets.
+ """
+ value = self.format_value(value)
+ for option in self.options(name, value, attrs):
+ yield option
- def render_option(self, selected_choices, option_value, option_label):
- if option_value is None:
- option_value = ''
- option_value = force_text(option_value)
- if option_value in selected_choices:
- selected_html = mark_safe(' selected')
- if not self.allow_multiple_selected:
- # Only allow for a single selection.
- selected_choices.remove(option_value)
- else:
- selected_html = ''
- return format_html('<option value="{}"{}>{}</option>', option_value, selected_html, force_text(option_label))
+ def render(self, name, value, attrs=None, renderer=None):
+ context = self.get_context(name, value, attrs)
+ return self._render(self.template_name, context, renderer)
+
+ def options(self, name, value, attrs=None):
+ """Yield a flat list of options for this widgets."""
+ for group in self.optgroups(name, value, attrs):
+ for option in group[1]:
+ yield option
+
+ def optgroups(self, name, value, attrs=None):
+ """Return a list of optgroups for this widget."""
+ default = (None, [], 0)
+ groups = [default]
+ has_selected = False
+
+ for option_value, option_label in chain(self.choices):
+ if option_value is None:
+ option_value = ''
+ else:
+ option_value = force_text(option_value)
- def render_options(self, selected_choices):
- # Normalize to strings.
- selected_choices = set(force_text(v) for v in selected_choices)
- output = []
- for option_value, option_label in self.choices:
if isinstance(option_label, (list, tuple)):
- output.append(format_html('<optgroup label="{}">', force_text(option_value)))
- for option in option_label:
- output.append(self.render_option(selected_choices, *option))
- output.append('</optgroup>')
+ index = groups[-1][2] + 1
+ subindex = 0
+ subgroup = []
+ groups.append((option_value, subgroup, index))
+ choices = option_label
+ else:
+ index = len(default[1])
+ subgroup = default[1]
+ subindex = None
+ choices = [(option_value, option_label)]
+
+ for subvalue, sublabel in choices:
+ selected = (
+ subvalue in value and
+ (has_selected is False or self.allow_multiple_selected)
+ )
+ if selected is True and has_selected is False:
+ has_selected = True
+ subgroup.append(self.create_option(
+ name, subvalue, sublabel, selected, index, subindex,
+ attrs=attrs,
+ ))
+ if subindex is not None:
+ subindex += 1
+ return groups
+
+ def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
+ index = str(index) if subindex is None else "%s_%s" % (index, subindex)
+ if attrs is None:
+ attrs = {}
+ option_attrs = self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
+ if selected:
+ option_attrs.update(self.checked_attribute)
+ if 'id' in option_attrs:
+ option_attrs['id'] = self.id_for_label(option_attrs['id'], index)
+ return dict(
+ name=name,
+ value=value,
+ label=label,
+ selected=selected,
+ index=index,
+ attrs=option_attrs,
+ type=self.input_type,
+ template_name=self.option_template_name,
+ )
+
+ def get_context(self, name, value, attrs=None):
+ context = super(ChoiceWidget, self).get_context(name, value, attrs)
+ context['widget']['optgroups'] = self.optgroups(name, context['widget']['value'], attrs)
+ context['wrap_label'] = True
+ return context
+
+ def id_for_label(self, id_, index='0'):
+ """
+ Use an incremented id for each option where the main widget
+ references the zero index.
+ """
+ if id_ and self.add_id_index:
+ id_ = '%s_%s' % (id_, index)
+ return id_
+
+ def value_from_datadict(self, data, files, name):
+ getter = data.get
+ if self.allow_multiple_selected:
+ try:
+ getter = data.getlist
+ except AttributeError:
+ pass
+ return getter(name)
+
+ @contextmanager
+ def override_choices(self, choices):
+ old = self.choices
+ self.choices = choices
+ yield
+ self.choices = old
+
+ def format_value(self, value):
+ """Return selected values as a set."""
+ if not isinstance(value, (tuple, list)):
+ value = [value]
+ values = set()
+ for v in value:
+ if v is None:
+ values.add('')
else:
- output.append(self.render_option(selected_choices, option_value, option_label))
- return '\n'.join(output)
+ values.add(force_text(v))
+ return values
+
+
+class Select(ChoiceWidget):
+ input_type = 'select'
+ template_name = 'django/forms/widgets/select.html'
+ option_template_name = 'django/forms/widgets/select_option.html'
+ add_id_index = False
+ checked_attribute = {'selected': True}
+ option_inherits_attrs = False
+
+ def get_context(self, name, value, attrs=None):
+ context = super(Select, self).get_context(name, value, attrs)
+ if self.allow_multiple_selected:
+ context['widget']['attrs']['multiple'] = 'multiple'
+ return context
class NullBooleanSelect(Select):
@@ -586,12 +693,11 @@ class NullBooleanSelect(Select):
)
super(NullBooleanSelect, self).__init__(attrs, choices)
- def render(self, name, value, attrs=None):
+ def format_value(self, value):
try:
- value = {True: '2', False: '3', '2': '2', '3': '3'}[value]
+ return {True: '2', False: '3', '2': '2', '3': '3'}[value]
except KeyError:
- value = '1'
- return super(NullBooleanSelect, self).render(name, value, attrs)
+ return '1'
def value_from_datadict(self, data, files, name):
value = data.get(name)
@@ -608,17 +714,6 @@ class NullBooleanSelect(Select):
class SelectMultiple(Select):
allow_multiple_selected = True
- def render(self, name, value, attrs=None):
- if value is None:
- value = []
- final_attrs = self.build_attrs(attrs, name=name)
- output = [format_html('<select multiple="multiple"{}>', flatatt(final_attrs))]
- options = self.render_options(value)
- if options:
- output.append(options)
- output.append('</select>')
- return mark_safe('\n'.join(output))
-
def value_from_datadict(self, data, files, name):
try:
getter = data.getlist
@@ -627,190 +722,17 @@ class SelectMultiple(Select):
return getter(name)
-@html_safe
-@python_2_unicode_compatible
-class ChoiceInput(SubWidget):
- """
- An object used by ChoiceFieldRenderer that represents a single
- <input type='$input_type'>.
- """
- input_type = None # Subclasses must define this
-
- def __init__(self, name, value, attrs, choice, index):
- self.name = name
- self.value = value
- self.attrs = attrs
- self.choice_value = force_text(choice[0])
- self.choice_label = force_text(choice[1])
- self.index = index
- if 'id' in self.attrs:
- self.attrs['id'] += "_%d" % self.index
-
- def __str__(self):
- return self.render()
-
- def render(self, name=None, value=None, attrs=None):
- if self.id_for_label:
- label_for = format_html(' for="{}"', self.id_for_label)
- else:
- label_for = ''
- attrs = dict(self.attrs, **attrs) if attrs else self.attrs
- return format_html(
- '<label{}>{} {}</label>', label_for, self.tag(attrs), self.choice_label
- )
-
- def is_checked(self):
- return self.value == self.choice_value
-
- def tag(self, attrs=None):
- attrs = attrs or self.attrs
- final_attrs = dict(
- attrs,
- type=self.input_type,
- name=self.name,
- value=self.choice_value,
- checked=self.is_checked(),
- )
- return format_html('<input{} />', flatatt(final_attrs))
-
- @property
- def id_for_label(self):
- return self.attrs.get('id', '')
-
-
-class RadioChoiceInput(ChoiceInput):
+class RadioSelect(ChoiceWidget):
input_type = 'radio'
+ template_name = 'django/forms/widgets/radio.html'
+ option_template_name = 'django/forms/widgets/radio_option.html'
- def __init__(self, *args, **kwargs):
- super(RadioChoiceInput, self).__init__(*args, **kwargs)
- self.value = force_text(self.value)
-
-class CheckboxChoiceInput(ChoiceInput):
+class CheckboxSelectMultiple(ChoiceWidget):
+ allow_multiple_selected = True
input_type = 'checkbox'
-
- def __init__(self, *args, **kwargs):
- super(CheckboxChoiceInput, self).__init__(*args, **kwargs)
- self.value = set(force_text(v) for v in self.value)
-
- def is_checked(self):
- return self.choice_value in self.value
-
-
-@html_safe
-@python_2_unicode_compatible
-class ChoiceFieldRenderer(object):
- """
- An object used by RadioSelect to enable customization of radio widgets.
- """
-
- choice_input_class = None
- outer_html = '<ul{id_attr}>{content}</ul>'
- inner_html = '<li>{choice_value}{sub_widgets}</li>'
-
- def __init__(self, name, value, attrs, choices):
- self.name = name
- self.value = value
- self.attrs = attrs
- self.choices = choices
-
- def __getitem__(self, idx):
- return list(self)[idx]
-
- def __iter__(self):
- for idx, choice in enumerate(self.choices):
- yield self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, idx)
-
- def __str__(self):
- return self.render()
-
- def render(self):
- """
- Outputs a <ul> for this set of choice fields.
- If an id was given to the field, it is applied to the <ul> (each
- item in the list will get an id of `$id_$i`).
- """
- id_ = self.attrs.get('id')
- output = []
- for i, choice in enumerate(self.choices):
- choice_value, choice_label = choice
- if isinstance(choice_label, (tuple, list)):
- attrs_plus = self.attrs.copy()
- if id_:
- attrs_plus['id'] += '_{}'.format(i)
- sub_ul_renderer = self.__class__(
- name=self.name,
- value=self.value,
- attrs=attrs_plus,
- choices=choice_label,
- )
- sub_ul_renderer.choice_input_class = self.choice_input_class
- output.append(format_html(
- self.inner_html, choice_value=choice_value,
- sub_widgets=sub_ul_renderer.render(),
- ))
- else:
- w = self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, i)
- output.append(format_html(self.inner_html, choice_value=force_text(w), sub_widgets=''))
- return format_html(
- self.outer_html,
- id_attr=format_html(' id="{}"', id_) if id_ else '',
- content=mark_safe('\n'.join(output)),
- )
-
-
-class RadioFieldRenderer(ChoiceFieldRenderer):
- choice_input_class = RadioChoiceInput
-
-
-class CheckboxFieldRenderer(ChoiceFieldRenderer):
- choice_input_class = CheckboxChoiceInput
-
-
-class RendererMixin(object):
- renderer = None # subclasses must define this
- _empty_value = None
-
- def __init__(self, *args, **kwargs):
- # Override the default renderer if we were passed one.
- renderer = kwargs.pop('renderer', None)
- if renderer:
- self.renderer = renderer
- super(RendererMixin, self).__init__(*args, **kwargs)
-
- def subwidgets(self, name, value, attrs=None):
- for widget in self.get_renderer(name, value, attrs):
- yield widget
-
- def get_renderer(self, name, value, attrs=None):
- """Returns an instance of the renderer."""
- if value is None:
- value = self._empty_value
- final_attrs = self.build_attrs(attrs)
- return self.renderer(name, value, final_attrs, self.choices)
-
- def render(self, name, value, attrs=None):
- return self.get_renderer(name, value, attrs).render()
-
- def id_for_label(self, id_):
- # Widgets using this RendererMixin are made of a collection of
- # subwidgets, each with their own <label>, and distinct ID.
- # The IDs are made distinct by a "_X" suffix, where X is the zero-based
- # index of the choice field. Thus, the label for the main widget should
- # reference the first subwidget, hence the "_0" suffix.
- if id_:
- id_ += '_0'
- return id_
-
-
-class RadioSelect(RendererMixin, Select):
- renderer = RadioFieldRenderer
- _empty_value = ''
-
-
-class CheckboxSelectMultiple(RendererMixin, SelectMultiple):
- renderer = CheckboxFieldRenderer
- _empty_value = []
+ template_name = 'django/forms/widgets/checkbox_select.html'
+ option_template_name = 'django/forms/widgets/checkbox_option.html'
def use_required_attribute(self, initial):
# Don't use the 'required' attribute because browser validation would
@@ -822,41 +744,28 @@ class CheckboxSelectMultiple(RendererMixin, SelectMultiple):
# never known if the value is actually omitted.
return False
- def id_for_label(self, id_):
+ def id_for_label(self, id_, index=None):
""""
Don't include for="field_0" in <label> because clicking such a label
would toggle the first checkbox.
"""
- return ''
+ if index is None:
+ return ''
+ return super(CheckboxSelectMultiple, self).id_for_label(id_, index)
class MultiWidget(Widget):
"""
A widget that is composed of multiple widgets.
- Its render() method is different than other widgets', because it has to
- figure out how to split a single value for display in multiple widgets.
- The ``value`` argument can be one of two things:
-
- * A list.
- * A normal value (e.g., a string) that has been "compressed" from
- a list of values.
-
- In the second case -- i.e., if the value is NOT a list -- render() will
- first "decompress" the value into a list before rendering it. It does so by
- calling the decompress() method, which MultiWidget subclasses must
- implement. This method takes a single "compressed" value and returns a
- list.
-
- When render() does 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.
-
- Subclasses may implement format_output(), which takes the list of rendered
- widgets and returns a string of HTML that formats them any way you'd like.
+ In addition to the values added by Widget.get_context(), this widget
+ adds a list of subwidgets to the context as widget['subwidgets'].
+ These can be looped over and rendered like normal widgets.
You'll probably want to use this class with MultiValueField.
"""
+ template_name = 'django/forms/widgets/multiwidget.html'
+
def __init__(self, widgets, attrs=None):
self.widgets = [w() if isinstance(w, type) else w for w in widgets]
super(MultiWidget, self).__init__(attrs)
@@ -865,7 +774,8 @@ class MultiWidget(Widget):
def is_hidden(self):
return all(w.is_hidden for w in self.widgets)
- def render(self, name, value, attrs=None):
+ def get_context(self, name, value, attrs=None):
+ context = super(MultiWidget, self).get_context(name, value, attrs)
if self.is_localized:
for widget in self.widgets:
widget.is_localized = self.is_localized
@@ -873,21 +783,26 @@ class MultiWidget(Widget):
# in self.widgets.
if not isinstance(value, list):
value = self.decompress(value)
- output = []
- final_attrs = self.build_attrs(attrs)
+
+ final_attrs = context['widget']['attrs']
id_ = final_attrs.get('id')
+ subwidgets = []
for i, widget in enumerate(self.widgets):
+ widget_name = '%s_%s' % (name, i)
try:
widget_value = value[i]
except IndexError:
widget_value = None
if id_:
- final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
- output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
- return mark_safe(self.format_output(output))
+ widget_attrs = final_attrs.copy()
+ widget_attrs['id'] = '%s_%s' % (id_, i)
+ else:
+ widget_attrs = final_attrs
+ subwidgets.append(widget.get_context(widget_name, widget_value, widget_attrs)['widget'])
+ context['widget']['subwidgets'] = subwidgets
+ return context
def id_for_label(self, id_):
- # See the comment for RadioSelect.id_for_label()
if id_:
id_ += '_0'
return id_
@@ -901,16 +816,6 @@ class MultiWidget(Widget):
for i, widget in enumerate(self.widgets)
)
- def format_output(self, 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, if
- needed.
- """
- return ''.join(rendered_widgets)
-
def decompress(self, value):
"""
Returns a list of decompressed values for the given compressed value.
@@ -942,6 +847,7 @@ class SplitDateTimeWidget(MultiWidget):
A Widget that splits datetime input into two <input type="text"> boxes.
"""
supports_microseconds = False
+ template_name = 'django/forms/widgets/splitdatetime.html'
def __init__(self, attrs=None, date_format=None, time_format=None):
widgets = (
@@ -961,6 +867,8 @@ class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
"""
A Widget that splits datetime input into two <input type="hidden"> inputs.
"""
+ template_name = 'django/forms/widgets/splithiddendatetime.html'
+
def __init__(self, attrs=None, date_format=None, time_format=None):
super(SplitHiddenDateTimeWidget, self).__init__(attrs, date_format, time_format)
for widget in self.widgets:
@@ -978,8 +886,9 @@ class SelectDateWidget(Widget):
month_field = '%s_month'
day_field = '%s_day'
year_field = '%s_year'
+ template_name = 'django/forms/widgets/select_date.html'
+ input_type = 'select'
select_widget = Select
-
date_re = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$')
def __init__(self, attrs=None, years=None, months=None, empty_label=None):
@@ -1014,6 +923,70 @@ class SelectDateWidget(Widget):
self.month_none_value = self.none_value
self.day_none_value = self.none_value
+ def get_context(self, name, value, attrs=None):
+ context = super(SelectDateWidget, self).get_context(name, value, attrs)
+ date_context = {}
+ year_choices = [(i, i) for i in self.years]
+ if self.is_required is False:
+ year_choices.insert(0, self.year_none_value)
+ year_attrs = context['widget']['attrs'].copy()
+ year_name = self.year_field % name
+ year_attrs['id'] = 'id_%s' % year_name
+ date_context['year'] = self.select_widget(attrs, choices=year_choices).get_context(
+ name=year_name,
+ value=context['widget']['value']['year'],
+ attrs=year_attrs,
+ )
+ month_choices = list(self.months.items())
+ if self.is_required is False:
+ month_choices.insert(0, self.month_none_value)
+ month_attrs = context['widget']['attrs'].copy()
+ month_name = self.month_field % name
+ month_attrs['id'] = 'id_%s' % month_name
+ date_context['month'] = self.select_widget(attrs, choices=month_choices).get_context(
+ name=month_name,
+ value=context['widget']['value']['month'],
+ attrs=month_attrs,
+ )
+ day_choices = [(i, i) for i in range(1, 32)]
+ if self.is_required is False:
+ day_choices.insert(0, self.day_none_value)
+ day_attrs = context['widget']['attrs'].copy()
+ day_name = self.day_field % name
+ day_attrs['id'] = 'id_%s' % day_name
+ date_context['day'] = self.select_widget(attrs, choices=day_choices,).get_context(
+ name=day_name,
+ value=context['widget']['value']['day'],
+ attrs=day_attrs,
+ )
+ subwidgets = []
+ for field in self._parse_date_fmt():
+ subwidgets.append(date_context[field]['widget'])
+ context['widget']['subwidgets'] = subwidgets
+ return context
+
+ def format_value(self, value):
+ """
+ Return a dict containing the year, month, and day of the current value.
+ Use dict instead of a datetime to allow invalid dates such as February
+ 31 to display correctly.
+ """
+ year, month, day = None, None, None
+ if isinstance(value, (datetime.date, datetime.datetime)):
+ year, month, day = value.year, value.month, value.day
+ elif isinstance(value, six.string_types):
+ if settings.USE_L10N:
+ try:
+ input_format = get_format('DATE_INPUT_FORMATS')[0]
+ d = datetime.datetime.strptime(force_str(value), input_format)
+ year, month, day = d.year, d.month, d.day
+ except ValueError:
+ pass
+ match = self.date_re.match(value)
+ if match:
+ year, month, day = [int(val) for val in match.groups()]
+ return {'year': year, 'month': month, 'day': day}
+
@staticmethod
def _parse_date_fmt():
fmt = get_format('DATE_FORMAT')
@@ -1030,36 +1003,6 @@ class SelectDateWidget(Widget):
elif char in 'dj':
yield 'day'
- def render(self, name, value, attrs=None):
- try:
- year_val, month_val, day_val = value.year, value.month, value.day
- except AttributeError:
- year_val = month_val = day_val = None
- if isinstance(value, six.string_types):
- if settings.USE_L10N:
- try:
- input_format = get_format('DATE_INPUT_FORMATS')[0]
- v = datetime.datetime.strptime(force_str(value), input_format)
- year_val, month_val, day_val = v.year, v.month, v.day
- except ValueError:
- pass
- if year_val is None:
- match = self.date_re.match(value)
- if match:
- year_val, month_val, day_val = [int(val) for val in match.groups()]
- html = {}
- choices = [(i, i) for i in self.years]
- html['year'] = self.create_select(name, self.year_field, value, year_val, choices, self.year_none_value)
- choices = list(self.months.items())
- html['month'] = self.create_select(name, self.month_field, value, month_val, choices, self.month_none_value)
- choices = [(i, i) for i in range(1, 32)]
- html['day'] = self.create_select(name, self.day_field, value, day_val, choices, self.day_none_value)
-
- output = []
- for field in self._parse_date_fmt():
- output.append(html[field])
- return mark_safe('\n'.join(output))
-
def id_for_label(self, id_):
for first_select in self._parse_date_fmt():
return '%s_%s' % (id_, first_select)
@@ -1091,15 +1034,3 @@ class SelectDateWidget(Widget):
('{}_{}'.format(name, interval) in data)
for interval in ('year', 'month', 'day')
)
-
- def create_select(self, name, field, value, val, choices, none_value):
- if 'id' in self.attrs:
- id_ = self.attrs['id']
- else:
- id_ = 'id_%s' % name
- if not self.is_required:
- choices.insert(0, none_value)
- local_attrs = self.build_attrs(id=field % id_)
- s = self.select_widget(choices=choices)
- select_html = s.render(field % name, val, local_attrs)
- return select_html
diff --git a/django/template/backends/jinja2.py b/django/template/backends/jinja2.py
index 54f6f6c9d2..cf08858db9 100644
--- a/django/template/backends/jinja2.py
+++ b/django/template/backends/jinja2.py
@@ -12,7 +12,6 @@ from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from .base import BaseEngine
-from .utils import csrf_input_lazy, csrf_token_lazy
class Jinja2(BaseEngine):
@@ -70,6 +69,7 @@ class Template(object):
)
def render(self, context=None, request=None):
+ from .utils import csrf_input_lazy, csrf_token_lazy
if context is None:
context = {}
if request is not None:
diff --git a/django/test/signals.py b/django/test/signals.py
index 6f6d5aec81..7b105eda35 100644
--- a/django/test/signals.py
+++ b/django/test/signals.py
@@ -97,6 +97,8 @@ def reset_template_engines(**kwargs):
engines._engines = {}
from django.template.engine import Engine
Engine.get_default.cache_clear()
+ from django.forms.renderers import get_default_renderer
+ get_default_renderer.cache_clear()
@receiver(setting_changed)
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index c1a1c41cd6..1510ca659b 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -51,6 +51,9 @@ details on these changes.
* Support for regular expression groups with ``iLmsu#`` in ``url()`` will be
removed.
+* Support for ``Widget.render()`` methods without the ``renderer`` argument
+ will be removed.
+
.. _deprecation-removed-in-2.0:
2.0
diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 194c390484..c74cfaed48 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -720,6 +720,29 @@ When set to ``True`` (the default), required form fields will have the
``use_required_attribute=False`` to avoid incorrect browser validation when
adding and deleting forms from a formset.
+Configuring the rendering of a form's widgets
+---------------------------------------------
+
+.. attribute:: Form.default_renderer
+
+.. versionadded:: 1.11
+
+Specifies the :doc:`renderer <renderers>` to use for the form. Defaults to
+``None`` which means to use the default renderer specified by the
+:setting:`FORM_RENDERER` setting.
+
+You can set this as a class attribute when declaring your form or use the
+``renderer`` argument to ``Form.__init__()``. For example::
+
+ from django import forms
+
+ class MyForm(forms.Form):
+ default_renderer = MyRenderer()
+
+or::
+
+ form = MyForm(renderer=MyRenderer())
+
Notes on field ordering
-----------------------
diff --git a/docs/ref/forms/index.txt b/docs/ref/forms/index.txt
index 618c705b92..241f979954 100644
--- a/docs/ref/forms/index.txt
+++ b/docs/ref/forms/index.txt
@@ -12,5 +12,6 @@ Detailed form API reference. For introductory material, see the
fields
models
formsets
+ renderers
widgets
validation
diff --git a/docs/ref/forms/renderers.txt b/docs/ref/forms/renderers.txt
new file mode 100644
index 0000000000..c94b5ef226
--- /dev/null
+++ b/docs/ref/forms/renderers.txt
@@ -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.
diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt
index c6148a5460..f0301c1eee 100644
--- a/docs/ref/forms/widgets.txt
+++ b/docs/ref/forms/widgets.txt
@@ -241,6 +241,28 @@ foundation for custom widgets.
In older versions, this method is a private API named
``_format_value()``. The old name will work until Django 2.0.
+ .. method:: get_context(name, value, attrs=None)
+
+ .. versionadded:: 1.11
+
+ Returns a dictionary of values to use when rendering the widget
+ template. By default, the dictionary contains a single key,
+ ``'widget'``, which is a dictionary representation of the widget
+ containing the following keys:
+
+ * ``'name'``: The name of the field from the ``name`` argument.
+ * ``'is_hidden'``: A boolean indicating whether or not this widget is
+ hidden.
+ * ``'required'``: A boolean indicating whether or not the field for
+ this widget is required.
+ * ``'value'``: The value as returned by :meth:`format_value`.
+ * ``'attrs'``: HTML attributes to be set on the rendered widget. The
+ combination of the :attr:`attrs` attribute and the ``attrs`` argument.
+ * ``'template_name'``: The value of ``self.template_name``.
+
+ ``Widget`` subclasses can provide custom context values by overriding
+ this method.
+
.. method:: id_for_label(self, id_)
Returns the HTML ID attribute of this widget for use by a ``<label>``,
@@ -251,14 +273,16 @@ foundation for custom widgets.
return an ID value that corresponds to the first ID in the widget's
tags.
- .. method:: render(name, value, attrs=None)
+ .. method:: render(name, value, attrs=None, renderer=None)
- Returns HTML for the widget, as a Unicode string. This method must be
- implemented by the subclass, otherwise ``NotImplementedError`` will be
- raised.
+ Renders a widget to HTML using the given renderer. If ``renderer`` is
+ ``None``, the renderer from the :setting:`FORM_RENDERER` setting is
+ used.
- The 'value' given is not guaranteed to be valid input, therefore
- subclass implementations should program defensively.
+ .. versionchanged:: 1.11
+
+ The ``renderer`` argument was added. Support for subclasses that
+ don't accept it will be removed in Django 2.1.
.. method:: value_from_datadict(data, files, name)
@@ -360,40 +384,21 @@ foundation for custom widgets.
with the opposite responsibility - to combine cleaned values of
all member fields into one.
- Other methods that may be useful to override include:
-
- .. method:: render(name, value, attrs=None)
-
- Argument ``value`` is handled differently in this method from the
- subclasses of :class:`~Widget` because it has to figure out how to
- split a single value for display in multiple widgets.
-
- The ``value`` argument used when rendering can be one of two things:
-
- * A ``list``.
- * A single value (e.g., a string) that is the "compressed" representation
- of a ``list`` of values.
+ It provides some custom context:
- If ``value`` is a list, the output of :meth:`~MultiWidget.render` will
- be a concatenation of rendered child widgets. If ``value`` is not a
- list, it will first be processed by the method
- :meth:`~MultiWidget.decompress()` to create the list and then rendered.
+ .. method:: get_context(name, value, attrs=None)
- 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.
+ In addition to the ``'widget'`` key described in
+ :meth:`Widget.get_context`, ``MultiValueWidget`` adds a
+ ``widget['subwidgets']`` key.
- Unlike in the single value widgets, method :meth:`~MultiWidget.render`
- need not be implemented in the subclasses.
+ These can be looped over in the widget template:
- .. method:: format_output(rendered_widgets)
+ .. code-block:: html+django
- Given a list of rendered widgets (as strings), returns a Unicode string
- representing the HTML for the whole lot.
-
- This hook allows you to format the HTML design of the widgets any way
- you'd like.
+ {% for subwidget in widget.subwidgets %}
+ {% include widget.template_name with widget=subwidget %}
+ {% endfor %}
Here's an example widget which subclasses :class:`MultiWidget` to display
a date with the day, month, and year in different select boxes. This widget
@@ -421,9 +426,6 @@ foundation for custom widgets.
return [value.day, value.month, value.year]
return [None, None, None]
- def format_output(self, rendered_widgets):
- return ''.join(rendered_widgets)
-
def value_from_datadict(self, data, files, name):
datelist = [
widget.value_from_datadict(data, files, name + '_%s' % i)
@@ -442,11 +444,6 @@ foundation for custom widgets.
The constructor creates several :class:`Select` widgets in a tuple. The
``super`` class uses this tuple to setup the widget.
- The :meth:`~MultiWidget.format_output` method is fairly vanilla here (in
- fact, it's the same as what's been implemented as the default for
- ``MultiWidget``), but the idea is that you could add custom HTML between
- the widgets should you wish.
-
The required method :meth:`~MultiWidget.decompress` breaks up a
``datetime.date`` value into the day, month, and year values corresponding
to each widget. Note how the method handles the case where ``value`` is
@@ -485,14 +482,18 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: TextInput
- Text input: ``<input type="text" ...>``
+ * ``input_type``: ``'text'``
+ * ``template_name``: ``'django/forms/widgets/text.html'``
+ * Renders as: ``<input type="text" ...>``
``NumberInput``
~~~~~~~~~~~~~~~
.. class:: NumberInput
- Text input: ``<input type="number" ...>``
+ * ``input_type``: ``'number'``
+ * ``template_name``: ``'django/forms/widgets/number.html'``
+ * Renders as: ``<input type="number" ...>``
Beware that not all browsers support entering localized numbers in
``number`` input types. Django itself avoids using them for fields having
@@ -503,21 +504,27 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: EmailInput
- Text input: ``<input type="email" ...>``
+ * ``input_type``: ``'email'``
+ * ``template_name``: ``'django/forms/widgets/email.html'``
+ * Renders as: ``<input type="email" ...>``
``URLInput``
~~~~~~~~~~~~
.. class:: URLInput
- Text input: ``<input type="url" ...>``
+ * ``input_type``: ``'url'``
+ * ``template_name``: ``'django/forms/widgets/url.html'``
+ * Renders as: ``<input type="url" ...>``
``PasswordInput``
~~~~~~~~~~~~~~~~~
.. class:: PasswordInput
- Password input: ``<input type='password' ...>``
+ * ``input_type``: ``'password'``
+ * ``template_name``: ``'django/forms/widgets/password.html'``
+ * Renders as: ``<input type='password' ...>``
Takes one optional argument:
@@ -531,7 +538,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: HiddenInput
- Hidden input: ``<input type='hidden' ...>``
+ * ``input_type``: ``'hidden'``
+ * ``template_name``: ``'django/forms/widgets/hidden.html'``
+ * Renders as: ``<input type='hidden' ...>``
Note that there also is a :class:`MultipleHiddenInput` widget that
encapsulates a set of hidden input elements.
@@ -541,7 +550,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: DateInput
- Date input as a simple text box: ``<input type='text' ...>``
+ * ``input_type``: ``'text'``
+ * ``template_name``: ``'django/forms/widgets/date.html'``
+ * Renders as: ``<input type='text' ...>``
Takes same arguments as :class:`TextInput`, with one more optional argument:
@@ -558,7 +569,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: DateTimeInput
- Date/time input as a simple text box: ``<input type='text' ...>``
+ * ``input_type``: ``'text'``
+ * ``template_name``: ``'django/forms/widgets/datetime.html'``
+ * Renders as: ``<input type='text' ...>``
Takes same arguments as :class:`TextInput`, with one more optional argument:
@@ -579,7 +592,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: TimeInput
- Time input as a simple text box: ``<input type='text' ...>``
+ * ``input_type``: ``'text'``
+ * ``template_name``: ``'django/forms/widgets/time.html'``
+ * Renders as: ``<input type='text' ...>``
Takes same arguments as :class:`TextInput`, with one more optional argument:
@@ -598,7 +613,8 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
.. class:: Textarea
- Text area: ``<textarea>...</textarea>``
+ * ``template_name``: ``'django/forms/widgets/textarea.html'``
+ * Renders as: ``<textarea>...</textarea>``
.. _selector-widgets:
@@ -610,7 +626,9 @@ Selector and checkbox widgets
.. class:: CheckboxInput
- Checkbox: ``<input type='checkbox' ...>``
+ * ``input_type``: ``'checkbox'``
+ * ``template_name``: ``'django/forms/widgets/checkbox.html'``
+ * Renders as: ``<input type='checkbox' ...>``
Takes one optional argument:
@@ -624,7 +642,8 @@ Selector and checkbox widgets
.. class:: Select
- Select widget: ``<select><option ...>...</select>``
+ * ``template_name``: ``'django/forms/widgets/select.html'``
+ * Renders as: ``<select><option ...>...</select>``
.. attribute:: Select.choices
@@ -637,6 +656,8 @@ Selector and checkbox widgets
.. class:: NullBooleanSelect
+ * ``template_name``: ``'django/forms/widgets/select.html'``
+
Select widget with options 'Unknown', 'Yes' and 'No'
``SelectMultiple``
@@ -644,6 +665,8 @@ Selector and checkbox widgets
.. class:: SelectMultiple
+ * ``template_name``: ``'django/forms/widgets/select.html'``
+
Similar to :class:`Select`, but allows multiple selection:
``<select multiple='multiple'>...</select>``
@@ -652,6 +675,8 @@ Selector and checkbox widgets
.. class:: RadioSelect
+ * ``template_name``: ``'django/forms/widgets/radio.html'``
+
Similar to :class:`Select`, but rendered as a list of radio buttons within
``<li>`` tags:
@@ -744,6 +769,8 @@ Selector and checkbox widgets
.. class:: CheckboxSelectMultiple
+ * ``template_name``: ``'django/forms/widgets/checkbox_select.html'``
+
Similar to :class:`SelectMultiple`, but rendered as a list of check
buttons:
@@ -776,16 +803,18 @@ File upload widgets
.. class:: FileInput
- File upload input: ``<input type='file' ...>``
+ * ``template_name``: ``'django/forms/widgets/file.html'``
+ * Renders as: ``<input type='file' ...>``
``ClearableFileInput``
~~~~~~~~~~~~~~~~~~~~~~
.. class:: ClearableFileInput
- File upload input: ``<input type='file' ...>``, with an additional checkbox
- input to clear the field's value, if the field is not required and has
- initial data.
+ * ``template_name``: ``'django/forms/widgets/clearable_file_input.html'``
+ * Renders as: ``<input type='file' ...>`` with an additional checkbox
+ input to clear the field's value, if the field is not required and has
+ initial data.
.. _composite-widgets:
@@ -797,7 +826,8 @@ Composite widgets
.. class:: MultipleHiddenInput
- Multiple ``<input type='hidden' ...>`` widgets.
+ * ``template_name``: ``'django/forms/widgets/multiple_hidden.html'``
+ * Renders as: multiple ``<input type='hidden' ...>`` tags
A widget that handles multiple hidden widgets for fields that have a list
of values.
@@ -813,6 +843,8 @@ Composite widgets
.. class:: SplitDateTimeWidget
+ * ``template_name``: ``'django/forms/widgets/splitdatetime.html'``
+
Wrapper (using :class:`MultiWidget`) around two widgets: :class:`DateInput`
for the date, and :class:`TimeInput` for the time. Must be used with
:class:`SplitDateTimeField` rather than :class:`DateTimeField`.
@@ -832,6 +864,8 @@ Composite widgets
.. class:: SplitHiddenDateTimeWidget
+ * ``template_name``: ``'django/forms/widgets/splithiddendatetime.html'``
+
Similar to :class:`SplitDateTimeWidget`, but uses :class:`HiddenInput` for
both date and time.
@@ -840,6 +874,8 @@ Composite widgets
.. class:: SelectDateWidget
+ * ``template_name``: ``'django/forms/widgets/select_date.html'``
+
Wrapper around three :class:`~django.forms.Select` widgets: one each for
month, day, and year.
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index 1d28c96137..10f60b3b2e 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -1517,6 +1517,18 @@ generate correct URLs when ``SCRIPT_NAME`` is not ``/``.
The setting's use in :func:`django.setup()` was added.
+.. setting:: FORM_RENDERER
+
+``FORM_RENDERER``
+-----------------
+
+.. versionadded:: 1.11
+
+Default: ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``
+
+The class that renders form widgets. It must implement :ref:`the low-level
+render API <low-level-widget-render-api>`.
+
.. setting:: FORMAT_MODULE_PATH
``FORMAT_MODULE_PATH``
@@ -3351,6 +3363,10 @@ File uploads
* :setting:`MEDIA_ROOT`
* :setting:`MEDIA_URL`
+Forms
+-----
+* :setting:`FORM_RENDERER`
+
Globalization (``i18n``/``l10n``)
---------------------------------
* :setting:`DATE_FORMAT`
diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt
index 48849f3b4d..f894c261f9 100644
--- a/docs/releases/1.11.txt
+++ b/docs/releases/1.11.txt
@@ -61,6 +61,15 @@ It can be subclassed to support different index types, such as
:class:`~django.contrib.postgres.indexes.GinIndex`. It also allows defining the
order (ASC/DESC) for the columns of the index.
+Template-based widget rendering
+-------------------------------
+
+To ease customizing widgets, form widget rendering is now done using the
+template system rather than in Python. See :doc:`/ref/forms/renderers`.
+
+You may need to adjust any custom widgets that you've written for a few
+:ref:`backwards incompatible changes <template-widget-incompatibilities-1-11>`.
+
Minor features
--------------
@@ -551,6 +560,21 @@ inside help text.
Read-only fields are wrapped in ``<div class="readonly">...</div>`` instead of
``<p>...</p>`` to allow any kind of HTML as the field's content.
+.. _template-widget-incompatibilities-1-11:
+
+Changes due to the introduction of template-based widget rendering
+------------------------------------------------------------------
+
+Some undocumented classes in ``django.forms.widgets`` are removed:
+
+* ``SubWidget``
+* ``RendererMixin``, ``ChoiceFieldRenderer``, ``RadioFieldRenderer``,
+ ``CheckboxFieldRenderer``
+* ``ChoiceInput``, ``RadioChoiceInput``, ``CheckboxChoiceInput``
+
+The ``Widget.format_output()`` method is removed. Use a custom widget template
+instead.
+
Miscellaneous
-------------
@@ -754,3 +778,7 @@ Miscellaneous
entries for search engines, for example. An alternative solution could be to
create a :data:`~django.conf.urls.handler404` that looks for uppercase
characters in the URL and redirects to a lowercase equivalent.
+
+* The ``renderer`` argument is added to the :meth:`Widget.render()
+ <django.forms.Widget.render>` method. Methods that don't accept that argument
+ will work through a deprecation period.
diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist
index 8405db76dc..ce3c4c23ad 100644
--- a/docs/spelling_wordlist
+++ b/docs/spelling_wordlist
@@ -673,6 +673,7 @@ releasers
reloader
removetags
renderer
+renderers
repo
reportable
reprojection
diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py
index 2716d2abdb..6922a9f82f 100644
--- a/tests/admin_inlines/tests.py
+++ b/tests/admin_inlines/tests.py
@@ -184,9 +184,11 @@ class TestInline(TestDataMixin, TestCase):
SomeChildModel.objects.create(name='c', position='1', parent=parent)
response = self.client.get(reverse('admin:admin_inlines_someparentmodel_change', args=(parent.pk,)))
self.assertNotContains(response, '<td class="field-position">')
- self.assertContains(response, (
+ self.assertInHTML(
'<input id="id_somechildmodel_set-1-position" '
- 'name="somechildmodel_set-1-position" type="hidden" value="1" />'))
+ 'name="somechildmodel_set-1-position" type="hidden" value="1" />',
+ response.rendered_content,
+ )
def test_non_related_name_inline(self):
"""
@@ -273,12 +275,12 @@ class TestInline(TestDataMixin, TestCase):
'name="binarytree_set-TOTAL_FORMS" type="hidden" value="2" />'
)
response = self.client.get(reverse('admin:admin_inlines_binarytree_add'))
- self.assertContains(response, max_forms_input % 3)
- self.assertContains(response, total_forms_hidden)
+ self.assertInHTML(max_forms_input % 3, response.rendered_content)
+ self.assertInHTML(total_forms_hidden, response.rendered_content)
response = self.client.get(reverse('admin:admin_inlines_binarytree_change', args=(bt_head.id,)))
- self.assertContains(response, max_forms_input % 2)
- self.assertContains(response, total_forms_hidden)
+ self.assertInHTML(max_forms_input % 2, response.rendered_content)
+ self.assertInHTML(total_forms_hidden, response.rendered_content)
def test_min_num(self):
"""
@@ -302,8 +304,8 @@ class TestInline(TestDataMixin, TestCase):
request = self.factory.get(reverse('admin:admin_inlines_binarytree_add'))
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request)
- self.assertContains(response, min_forms)
- self.assertContains(response, total_forms)
+ self.assertInHTML(min_forms, response.rendered_content)
+ self.assertInHTML(total_forms, response.rendered_content)
def test_custom_min_num(self):
bt_head = BinaryTree.objects.create(name="Tree Head")
@@ -331,14 +333,14 @@ class TestInline(TestDataMixin, TestCase):
request = self.factory.get(reverse('admin:admin_inlines_binarytree_add'))
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request)
- self.assertContains(response, min_forms % 2)
- self.assertContains(response, total_forms % 5)
+ self.assertInHTML(min_forms % 2, response.rendered_content)
+ self.assertInHTML(total_forms % 5, response.rendered_content)
request = self.factory.get(reverse('admin:admin_inlines_binarytree_change', args=(bt_head.id,)))
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request, object_id=str(bt_head.id))
- self.assertContains(response, min_forms % 5)
- self.assertContains(response, total_forms % 8)
+ self.assertInHTML(min_forms % 5, response.rendered_content)
+ self.assertInHTML(total_forms % 8, response.rendered_content)
def test_inline_nonauto_noneditable_pk(self):
response = self.client.get(reverse('admin:admin_inlines_author_add'))
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index c6894dd2a2..115c854c70 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -5980,7 +5980,7 @@ class AdminViewOnSiteTests(TestCase):
response, 'inline_admin_formset', 0, None,
['Children must share a family name with their parents in this contrived test case']
)
- msg = "The formset 'inline_admin_formset' in context 4 does not contain any non-form errors."
+ msg = "The formset 'inline_admin_formset' in context 10 does not contain any non-form errors."
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormsetError(response, 'inline_admin_formset', None, None, ['Error'])
diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py
index 2d8b88f2cf..b74df4e251 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
import gettext
import os
+import re
from datetime import datetime, timedelta
from importlib import import_module
@@ -354,34 +355,53 @@ class AdminURLWidgetTest(SimpleTestCase):
)
def test_render_quoting(self):
- # WARNING: Don't use assertHTMLEqual in that testcase!
- # assertHTMLEqual will get rid of some escapes which are tested here!
+ """
+ WARNING: This test doesn't use assertHTMLEqual since it will get rid
+ of some escapes which are tested here!
+ """
+ HREF_RE = re.compile('href="([^"]+)"')
+ VALUE_RE = re.compile('value="([^"]+)"')
+ TEXT_RE = re.compile('<a[^>]+>([^>]+)</a>')
w = widgets.AdminURLFieldWidget()
+ output = w.render('test', 'http://example.com/<sometag>some text</sometag>')
+ self.assertEqual(
+ HREF_RE.search(output).groups()[0],
+ 'http://example.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://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(
- w.render('test', 'http://example.com/<sometag>some text</sometag>'),
- '<p class="url">Currently: '
- '<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">'
- 'http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />'
- 'Change: <input class="vURLField" name="test" type="url" '
- 'value="http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;" /></p>'
+ TEXT_RE.search(output).groups()[0],
+ 'http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;',
)
self.assertEqual(
- w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>'),
- '<p class="url">Currently: '
- '<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">'
- 'http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />'
- 'Change: <input class="vURLField" name="test" type="url" '
- 'value="http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;" /></p>'
+ 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(
- w.render('test', 'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"'),
- '<p class="url">Currently: '
- '<a href="http://www.example.com/%C3%A4%22%3E%3Cscript%3Ealert(%22XSS!%22)%3C/script%3E%22">'
+ HREF_RE.search(output).groups()[0],
+ 'http://www.example.com/%C3%A4%22%3E%3Cscript%3Ealert(%22XSS!%22)%3C/script%3E%22',
+ )
+ self.assertEqual(
+ TEXT_RE.search(output).groups()[0],
'http://www.example.com/%C3%A4&quot;&gt;&lt;script&gt;'
- 'alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;</a><br />'
- 'Change: <input class="vURLField" name="test" type="url" '
- 'value="http://www.example.com/%C3%A4&quot;&gt;&lt;script&gt;'
- 'alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;" /></p>'
+ 'alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;'
+ )
+ self.assertEqual(
+ VALUE_RE.search(output).groups()[0],
+ 'http://www.example.com/%C3%A4&quot;&gt;&lt;script&gt;alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;',
)
diff --git a/tests/forms_tests/field_tests/test_filepathfield.py b/tests/forms_tests/field_tests/test_filepathfield.py
index 5c4bd1bbb2..d518e65db1 100644
--- a/tests/forms_tests/field_tests/test_filepathfield.py
+++ b/tests/forms_tests/field_tests/test_filepathfield.py
@@ -39,6 +39,7 @@ class FilePathFieldTest(SimpleTestCase):
('/django/forms/forms.py', 'forms.py'),
('/django/forms/formsets.py', 'formsets.py'),
('/django/forms/models.py', 'models.py'),
+ ('/django/forms/renderers.py', 'renderers.py'),
('/django/forms/utils.py', 'utils.py'),
('/django/forms/widgets.py', 'widgets.py')
]
@@ -62,6 +63,7 @@ class FilePathFieldTest(SimpleTestCase):
('/django/forms/forms.py', 'forms.py'),
('/django/forms/formsets.py', 'formsets.py'),
('/django/forms/models.py', 'models.py'),
+ ('/django/forms/renderers.py', 'renderers.py'),
('/django/forms/utils.py', 'utils.py'),
('/django/forms/widgets.py', 'widgets.py')
]
@@ -83,6 +85,7 @@ class FilePathFieldTest(SimpleTestCase):
('/django/forms/forms.py', 'forms.py'),
('/django/forms/formsets.py', 'formsets.py'),
('/django/forms/models.py', 'models.py'),
+ ('/django/forms/renderers.py', 'renderers.py'),
('/django/forms/utils.py', 'utils.py'),
('/django/forms/widgets.py', 'widgets.py')
]
diff --git a/tests/forms_tests/jinja2/forms_tests/custom_widget.html b/tests/forms_tests/jinja2/forms_tests/custom_widget.html
new file mode 100644
index 0000000000..c6e8abd4ad
--- /dev/null
+++ b/tests/forms_tests/jinja2/forms_tests/custom_widget.html
@@ -0,0 +1 @@
+<input type="text" name="custom">
diff --git a/tests/forms_tests/templates/forms_tests/custom_widget.html b/tests/forms_tests/templates/forms_tests/custom_widget.html
new file mode 100644
index 0000000000..c6e8abd4ad
--- /dev/null
+++ b/tests/forms_tests/templates/forms_tests/custom_widget.html
@@ -0,0 +1 @@
+<input type="text" name="custom">
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index 8e94d29463..4079be1235 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -17,6 +17,7 @@ from django.forms import (
SplitDateTimeField, SplitHiddenDateTimeWidget, Textarea, TextInput,
TimeField, ValidationError, forms,
)
+from django.forms.renderers import DjangoTemplates, get_default_renderer
from django.forms.utils import ErrorList
from django.http import QueryDict
from django.template import Context, Template
@@ -678,6 +679,50 @@ Java</label></li>
<div><label><input type="radio" name="name" value="ringo" required /> Ringo</label></div>"""
)
+ def test_form_with_iterable_boundfield_id(self):
+ class BeatleForm(Form):
+ name = ChoiceField(
+ choices=[('john', 'John'), ('paul', 'Paul'), ('george', 'George'), ('ringo', 'Ringo')],
+ widget=RadioSelect,
+ )
+ fields = list(BeatleForm()['name'])
+ self.assertEqual(len(fields), 4)
+
+ self.assertEqual(fields[0].id_for_label, 'id_name_0')
+ self.assertEqual(fields[0].choice_label, 'John')
+ self.assertHTMLEqual(
+ fields[0].tag(),
+ '<input type="radio" name="name" value="john" id="id_name_0" required />'
+ )
+ self.assertHTMLEqual(
+ str(fields[0]),
+ '<label for="id_name_0"><input type="radio" name="name" '
+ 'value="john" id="id_name_0" required /> John</label>'
+ )
+
+ self.assertEqual(fields[1].id_for_label, 'id_name_1')
+ self.assertEqual(fields[1].choice_label, 'Paul')
+ self.assertHTMLEqual(
+ fields[1].tag(),
+ '<input type="radio" name="name" value="paul" id="id_name_1" required />'
+ )
+ self.assertHTMLEqual(
+ str(fields[1]),
+ '<label for="id_name_1"><input type="radio" name="name" '
+ 'value="paul" id="id_name_1" required /> Paul</label>'
+ )
+
+ def test_iterable_boundfield_select(self):
+ class BeatleForm(Form):
+ name = ChoiceField(choices=[('john', 'John'), ('paul', 'Paul'), ('george', 'George'), ('ringo', 'Ringo')])
+ fields = list(BeatleForm(auto_id=False)['name'])
+ self.assertEqual(len(fields), 4)
+
+ self.assertEqual(fields[0].id_for_label, 'id_name_0')
+ self.assertEqual(fields[0].choice_label, 'John')
+ self.assertHTMLEqual(fields[0].tag(), '<option value="john">John</option>')
+ self.assertHTMLEqual(str(fields[0]), '<option value="john">John</option>')
+
def test_form_with_noniterable_boundfield(self):
# You can iterate over any BoundField, not just those with widget=RadioSelect.
class BeatleForm(Form):
@@ -1993,8 +2038,9 @@ Password: <input type="password" name="password" required /></li>
doesn't lose it's safe string status (#22950).
"""
class CustomWidget(TextInput):
- def render(self, name, value, attrs=None):
- return format_html(str('<input{} required />'), ' id=custom')
+ def render(self, name, value, attrs=None, choices=None,
+ renderer=None, extra_context=None):
+ return format_html(str('<input{} />'), ' id=custom')
class SampleForm(Form):
name = CharField(widget=CustomWidget)
@@ -3573,3 +3619,46 @@ Good luck picking a username that doesn&#39;t already exist.</p>
f = DataForm({'data': 'xyzzy'})
self.assertTrue(f.is_valid())
self.assertEqual(f.cleaned_data, {'data': 'xyzzy'})
+
+
+class CustomRenderer(DjangoTemplates):
+ pass
+
+
+class RendererTests(SimpleTestCase):
+
+ def test_default(self):
+ form = Form()
+ self.assertEqual(form.renderer, get_default_renderer())
+
+ def test_kwarg_instance(self):
+ custom = CustomRenderer()
+ form = Form(renderer=custom)
+ self.assertEqual(form.renderer, custom)
+
+ def test_kwarg_class(self):
+ custom = CustomRenderer()
+ form = Form(renderer=custom)
+ self.assertEqual(form.renderer, custom)
+
+ def test_attribute_instance(self):
+ class CustomForm(Form):
+ default_renderer = DjangoTemplates()
+
+ form = CustomForm()
+ self.assertEqual(form.renderer, CustomForm.default_renderer)
+
+ def test_attribute_class(self):
+ class CustomForm(Form):
+ default_renderer = CustomRenderer
+
+ form = CustomForm()
+ self.assertTrue(isinstance(form.renderer, CustomForm.default_renderer))
+
+ def test_attribute_override(self):
+ class CustomForm(Form):
+ default_renderer = DjangoTemplates()
+
+ custom = CustomRenderer()
+ form = CustomForm(renderer=custom)
+ self.assertEqual(form.renderer, custom)
diff --git a/tests/forms_tests/tests/test_renderers.py b/tests/forms_tests/tests/test_renderers.py
new file mode 100644
index 0000000000..7c37b6fd4e
--- /dev/null
+++ b/tests/forms_tests/tests/test_renderers.py
@@ -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
diff --git a/tests/forms_tests/tests/test_widgets.py b/tests/forms_tests/tests/test_widgets.py
index 7ea2b35266..f67954fd3a 100644
--- a/tests/forms_tests/tests/test_widgets.py
+++ b/tests/forms_tests/tests/test_widgets.py
@@ -1,182 +1,12 @@
-# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib.admin.tests import AdminSeleniumTestCase
-from django.forms import (
- CheckboxSelectMultiple, ClearableFileInput, RadioSelect, TextInput,
-)
-from django.forms.widgets import (
- ChoiceFieldRenderer, ChoiceInput, RadioFieldRenderer,
-)
-from django.test import SimpleTestCase, override_settings
+from django.test import override_settings
from django.urls import reverse
-from django.utils import six
-from django.utils.encoding import force_text, python_2_unicode_compatible
-from django.utils.safestring import SafeData
from ..models import Article
-class FormsWidgetTests(SimpleTestCase):
-
- def test_radiofieldrenderer(self):
- # RadioSelect uses a RadioFieldRenderer to render the individual radio inputs.
- # You can manipulate that object directly to customize the way the RadioSelect
- # is rendered.
- w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
- r = w.get_renderer('beatle', 'J')
- inp_set1 = []
- inp_set2 = []
- inp_set3 = []
- inp_set4 = []
-
- for inp in r:
- inp_set1.append(str(inp))
- inp_set2.append('%s<br />' % inp)
- inp_set3.append('<p>%s %s</p>' % (inp.tag(), inp.choice_label))
- inp_set4.append(
- '%s %s %s %s %s' % (
- inp.name,
- inp.value,
- inp.choice_value,
- inp.choice_label,
- inp.is_checked(),
- )
- )
-
- self.assertHTMLEqual('\n'.join(inp_set1), """<label><input checked type="radio" name="beatle" value="J" /> John</label>
-<label><input type="radio" name="beatle" value="P" /> Paul</label>
-<label><input type="radio" name="beatle" value="G" /> George</label>
-<label><input type="radio" name="beatle" value="R" /> Ringo</label>""")
- self.assertHTMLEqual('\n'.join(inp_set2), """<label><input checked type="radio" name="beatle" value="J" /> John</label><br />
-<label><input type="radio" name="beatle" value="P" /> Paul</label><br />
-<label><input type="radio" name="beatle" value="G" /> George</label><br />
-<label><input type="radio" name="beatle" value="R" /> Ringo</label><br />""")
- self.assertHTMLEqual('\n'.join(inp_set3), """<p><input checked type="radio" name="beatle" value="J" /> John</p>
-<p><input type="radio" name="beatle" value="P" /> Paul</p>
-<p><input type="radio" name="beatle" value="G" /> George</p>
-<p><input type="radio" name="beatle" value="R" /> Ringo</p>""")
- self.assertHTMLEqual('\n'.join(inp_set4), """beatle J J John True
-beatle J P Paul False
-beatle J G George False
-beatle J R Ringo False""")
-
- # A RadioFieldRenderer object also allows index access to individual RadioChoiceInput
- w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
- r = w.get_renderer('beatle', 'J')
- self.assertHTMLEqual(str(r[1]), '<label><input type="radio" name="beatle" value="P" /> Paul</label>')
- self.assertHTMLEqual(
- str(r[0]),
- '<label><input checked type="radio" name="beatle" value="J" /> John</label>'
- )
- self.assertTrue(r[0].is_checked())
- self.assertFalse(r[1].is_checked())
- self.assertEqual((r[1].name, r[1].value, r[1].choice_value, r[1].choice_label), ('beatle', 'J', 'P', 'Paul'))
-
- # These individual widgets can accept extra attributes if manually rendered.
- self.assertHTMLEqual(
- r[1].render(attrs={'extra': 'value'}),
- '<label><input type="radio" extra="value" name="beatle" value="P" /> Paul</label>'
- )
-
- with self.assertRaises(IndexError):
- r[10]
-
- # You can create your own custom renderers for RadioSelect to use.
- class MyRenderer(RadioFieldRenderer):
- def render(self):
- return '<br />\n'.join(six.text_type(choice) for choice in self)
- w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')), renderer=MyRenderer)
- self.assertHTMLEqual(
- w.render('beatle', 'G'),
- """<label><input type="radio" name="beatle" value="J" /> John</label><br />
-<label><input type="radio" name="beatle" value="P" /> Paul</label><br />
-<label><input checked type="radio" name="beatle" value="G" /> George</label><br />
-<label><input type="radio" name="beatle" value="R" /> Ringo</label>"""
- )
-
- # Or you can use custom RadioSelect fields that use your custom renderer.
- class CustomRadioSelect(RadioSelect):
- renderer = MyRenderer
- w = CustomRadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
- self.assertHTMLEqual(
- w.render('beatle', 'G'),
- """<label><input type="radio" name="beatle" value="J" /> John</label><br />
-<label><input type="radio" name="beatle" value="P" /> Paul</label><br />
-<label><input checked type="radio" name="beatle" value="G" /> George</label><br />
-<label><input type="radio" name="beatle" value="R" /> Ringo</label>"""
- )
-
- # You can customize rendering with outer_html/inner_html renderer variables (#22950)
- class MyRenderer(RadioFieldRenderer):
- # str is just to test some Python 2 issue with bytestrings
- outer_html = str('<div{id_attr}>{content}</div>')
- inner_html = '<p>{choice_value}{sub_widgets}</p>'
- w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')), renderer=MyRenderer)
- output = w.render('beatle', 'J', attrs={'id': 'bar'})
- self.assertIsInstance(output, SafeData)
- self.assertHTMLEqual(
- output,
- """<div id="bar">
-<p><label for="bar_0"><input checked type="radio" id="bar_0" value="J" name="beatle" /> John</label></p>
-<p><label for="bar_1"><input type="radio" id="bar_1" value="P" name="beatle" /> Paul</label></p>
-<p><label for="bar_2"><input type="radio" id="bar_2" value="G" name="beatle" /> George</label></p>
-<p><label for="bar_3"><input type="radio" id="bar_3" value="R" name="beatle" /> Ringo</label></p>
-</div>""")
-
- def test_subwidget(self):
- # Each subwidget tag gets a separate ID when the widget has an ID specified
- self.assertHTMLEqual(
- "\n".join(
- c.tag() for c in CheckboxSelectMultiple(
- attrs={'id': 'abc'},
- choices=zip('abc', 'ABC')
- ).subwidgets('letters', list('ac'))
- ),
- """<input checked type="checkbox" name="letters" value="a" id="abc_0" />
-<input type="checkbox" name="letters" value="b" id="abc_1" />
-<input checked type="checkbox" name="letters" value="c" id="abc_2" />""")
-
- # Each subwidget tag does not get an ID if the widget does not have an ID specified
- self.assertHTMLEqual(
- "\n".join(c.tag() for c in CheckboxSelectMultiple(
- choices=zip('abc', 'ABC'),
- ).subwidgets('letters', list('ac'))),
- """<input checked type="checkbox" name="letters" value="a" />
-<input type="checkbox" name="letters" value="b" />
-<input checked type="checkbox" name="letters" value="c" />""")
-
- # The id_for_label property of the subwidget should return the ID that is used on the subwidget's tag
- self.assertHTMLEqual(
- "\n".join(
- '<input type="checkbox" name="letters" value="%s" id="%s" />'
- % (c.choice_value, c.id_for_label) for c in CheckboxSelectMultiple(
- attrs={'id': 'abc'},
- choices=zip('abc', 'ABC'),
- ).subwidgets('letters', [])
- ),
- """<input type="checkbox" name="letters" value="a" id="abc_0" />
-<input type="checkbox" name="letters" value="b" id="abc_1" />
-<input type="checkbox" name="letters" value="c" id="abc_2" />""")
-
- def test_sub_widget_html_safe(self):
- widget = TextInput()
- subwidget = next(widget.subwidgets('username', 'John Doe'))
- self.assertTrue(hasattr(subwidget, '__html__'))
- self.assertEqual(force_text(subwidget), subwidget.__html__())
-
- def test_choice_input_html_safe(self):
- widget = ChoiceInput('choices', 'CHOICE1', {}, ('CHOICE1', 'first choice'), 0)
- self.assertTrue(hasattr(ChoiceInput, '__html__'))
- self.assertEqual(force_text(widget), widget.__html__())
-
- def test_choice_field_renderer_html_safe(self):
- renderer = ChoiceFieldRenderer('choices', 'CHOICE1', {}, [('CHOICE1', 'first_choice')])
- renderer.choice_input_class = lambda *args: args
- self.assertTrue(hasattr(ChoiceFieldRenderer, '__html__'))
- self.assertEqual(force_text(renderer), renderer.__html__())
-
-
@override_settings(ROOT_URLCONF='forms_tests.urls')
class LiveWidgetTests(AdminSeleniumTestCase):
@@ -190,33 +20,4 @@ class LiveWidgetTests(AdminSeleniumTestCase):
self.selenium.get(self.live_server_url + reverse('article_form', args=[article.pk]))
self.selenium.find_element_by_id('submit').submit()
article = Article.objects.get(pk=article.pk)
- # Should be "\nTst\n" after #19251 is fixed
self.assertEqual(article.content, "\r\nTst\r\n")
-
-
-@python_2_unicode_compatible
-class FakeFieldFile(object):
- """
- Quacks like a FieldFile (has a .url and unicode representation), but
- doesn't require us to care about storages etc.
- """
- url = 'something'
-
- def __str__(self):
- return self.url
-
-
-class ClearableFileInputTests(SimpleTestCase):
-
- def test_render_custom_template(self):
- widget = ClearableFileInput()
- widget.template_with_initial = (
- '%(initial_text)s: <img src="%(initial_url)s" alt="%(initial)s" /> '
- '%(clear_template)s<br />%(input_text)s: %(input)s'
- )
- self.assertHTMLEqual(
- widget.render('myfile', FakeFieldFile()),
- 'Currently: <img src="something" alt="something" /> '
- '<input type="checkbox" name="myfile-clear" id="myfile-clear_id" /> '
- '<label for="myfile-clear_id">Clear</label><br />Change: <input type="file" name="myfile" />'
- )
diff --git a/tests/forms_tests/widget_tests/base.py b/tests/forms_tests/widget_tests/base.py
index 3127cb85f2..dbbea25d7a 100644
--- a/tests/forms_tests/widget_tests/base.py
+++ b/tests/forms_tests/widget_tests/base.py
@@ -1,9 +1,27 @@
+from django.forms.renderers import DjangoTemplates, Jinja2
from django.test import SimpleTestCase
+try:
+ import jinja2
+except ImportError:
+ jinja2 = None
+
class WidgetTest(SimpleTestCase):
beatles = (('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))
+ @classmethod
+ def setUpClass(cls):
+ cls.django_renderer = DjangoTemplates()
+ cls.jinja2_renderer = Jinja2() if jinja2 else None
+ cls.renderers = [cls.django_renderer] + ([cls.jinja2_renderer] if cls.jinja2_renderer else [])
+ super(WidgetTest, cls).setUpClass()
+
def check_html(self, widget, name, value, html='', attrs=None, **kwargs):
- output = widget.render(name, value, attrs=attrs, **kwargs)
+ if self.jinja2_renderer:
+ output = widget.render(name, value, attrs=attrs, renderer=self.jinja2_renderer, **kwargs)
+ # Django escapes quotes with '&quot;' while Jinja2 uses '&#34;'.
+ self.assertHTMLEqual(output.replace('&#34;', '&quot;'), html)
+
+ output = widget.render(name, value, attrs=attrs, renderer=self.django_renderer, **kwargs)
self.assertHTMLEqual(output, html)
diff --git a/tests/forms_tests/widget_tests/test_select.py b/tests/forms_tests/widget_tests/test_select.py
index dde84c476f..4b717b6043 100644
--- a/tests/forms_tests/widget_tests/test_select.py
+++ b/tests/forms_tests/widget_tests/test_select.py
@@ -221,6 +221,68 @@ class SelectTest(WidgetTest):
</select>"""
))
+ def test_options(self):
+ options = list(self.widget(choices=self.beatles).options(
+ 'name', ['J'], attrs={'class': 'super'},
+ ))
+ self.assertEqual(len(options), 4)
+ self.assertEqual(options[0]['name'], 'name')
+ self.assertEqual(options[0]['value'], 'J')
+ self.assertEqual(options[0]['label'], 'John')
+ self.assertEqual(options[0]['index'], '0')
+ self.assertEqual(options[0]['selected'], True)
+ # Template-related attributes
+ self.assertEqual(options[1]['name'], 'name')
+ self.assertEqual(options[1]['value'], 'P')
+ self.assertEqual(options[1]['label'], 'Paul')
+ self.assertEqual(options[1]['index'], '1')
+ self.assertEqual(options[1]['selected'], False)
+
+ def test_optgroups(self):
+ choices = [
+ ('Audio', [
+ ('vinyl', 'Vinyl'),
+ ('cd', 'CD'),
+ ]),
+ ('Video', [
+ ('vhs', 'VHS Tape'),
+ ('dvd', 'DVD'),
+ ]),
+ ('unknown', 'Unknown'),
+ ]
+ groups = list(self.widget(choices=choices).optgroups(
+ 'name', ['vhs'], attrs={'class': 'super'},
+ ))
+ self.assertEqual(len(groups), 3)
+ self.assertEqual(groups[0][0], None)
+ self.assertEqual(groups[0][2], 0)
+ self.assertEqual(len(groups[0][1]), 1)
+ options = groups[0][1]
+ self.assertEqual(options[0]['name'], 'name')
+ self.assertEqual(options[0]['value'], 'unknown')
+ self.assertEqual(options[0]['label'], 'Unknown')
+ self.assertEqual(options[0]['index'], '0')
+ self.assertEqual(options[0]['selected'], False)
+ self.assertEqual(groups[1][0], 'Audio')
+ self.assertEqual(groups[1][2], 1)
+ self.assertEqual(len(groups[1][1]), 2)
+ options = groups[1][1]
+ self.assertEqual(options[0]['name'], 'name')
+ self.assertEqual(options[0]['value'], 'vinyl')
+ self.assertEqual(options[0]['label'], 'Vinyl')
+ self.assertEqual(options[0]['index'], '1_0')
+ self.assertEqual(options[1]['index'], '1_1')
+ self.assertEqual(groups[2][0], 'Video')
+ self.assertEqual(groups[2][2], 2)
+ self.assertEqual(len(groups[2][1]), 2)
+ options = groups[2][1]
+ self.assertEqual(options[0]['name'], 'name')
+ self.assertEqual(options[0]['value'], 'vhs')
+ self.assertEqual(options[0]['label'], 'VHS Tape')
+ self.assertEqual(options[0]['index'], '2_0')
+ self.assertEqual(options[0]['selected'], True)
+ self.assertEqual(options[1]['index'], '2_1')
+
def test_deepcopy(self):
"""
__deepcopy__() should copy all attributes properly (#25085).
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index 84aa04b40f..f03b4433a8 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -1651,13 +1651,6 @@ class ModelChoiceFieldTests(TestCase):
with self.assertNumQueries(1):
template.render(Context({'field': field}))
- def test_modelchoicefield_index_renderer(self):
- field = forms.ModelChoiceField(Category.objects.all(), widget=forms.RadioSelect)
- self.assertEqual(
- str(field.widget.get_renderer('foo', [])[0]),
- '<label><input name="foo" type="radio" value="" /> ---------</label>'
- )
-
def test_disabled_modelchoicefield(self):
class ModelChoiceForm(forms.ModelForm):
author = forms.ModelChoiceField(Author.objects.all(), disabled=True)
@@ -2115,7 +2108,7 @@ class FileAndImageFieldTests(TestCase):
doc = Document.objects.create()
form = DocumentForm(instance=doc)
- self.assertEqual(
+ self.assertHTMLEqual(
str(form['myfile']),
'<input id="id_myfile" name="myfile" type="file" />'
)
diff --git a/tests/runtests.py b/tests/runtests.py
index 00b14b808f..0dda1dbee6 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -169,6 +169,11 @@ def setup(verbosity, test_labels, parallel):
'The GeoManager class is deprecated.',
RemovedInDjango20Warning
)
+ warnings.filterwarnings(
+ 'ignore',
+ 'django.forms.extras is deprecated.',
+ RemovedInDjango20Warning
+ )
# Load all the ALWAYS_INSTALLED_APPS.
django.setup()