summaryrefslogtreecommitdiff
path: root/django
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 /django
parent51cde873d9fc8e4540f4efecbd39cfe8e770be38 (diff)
Fixed #15667 -- Added template-based widget rendering.
Thanks Carl Meyer and Tim Graham for contributing to the patch.
Diffstat (limited to 'django')
-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
77 files changed, 739 insertions, 565 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)