From b52c73008a9d67e9ddbb841872dc15cdd3d6ee01 Mon Sep 17 00:00:00 2001 From: Preston Timmons Date: Tue, 27 Dec 2016 17:00:56 -0500 Subject: [PATCH] Fixed #15667 -- Added template-based widget rendering. Thanks Carl Meyer and Tim Graham for contributing to the patch. --- django/conf/global_settings.py | 3 + .../admin/widgets/clearable_file_input.html | 6 + .../admin/widgets/foreign_key_raw_id.html | 1 + .../admin/widgets/many_to_many_raw_id.html | 1 + .../admin/templates/admin/widgets/radio.html | 1 + .../admin/widgets/related_widget_wrapper.html | 27 + .../admin/widgets/split_datetime.html | 4 + .../admin/templates/admin/widgets/url.html | 1 + django/contrib/admin/widgets.py | 166 ++-- django/contrib/auth/forms.py | 30 +- .../auth/widgets/read_only_password_hash.html | 3 + django/contrib/gis/admin/options.py | 2 +- django/contrib/gis/admin/widgets.py | 5 +- django/contrib/gis/forms/widgets.py | 14 +- django/contrib/postgres/forms/array.py | 4 +- django/forms/boundfield.py | 68 +- django/forms/forms.py | 16 +- .../jinja2/django/forms/widgets/attrs.html | 1 + .../jinja2/django/forms/widgets/checkbox.html | 1 + .../django/forms/widgets/checkbox_option.html | 1 + .../django/forms/widgets/checkbox_select.html | 1 + .../forms/widgets/clearable_file_input.html | 5 + .../jinja2/django/forms/widgets/date.html | 1 + .../jinja2/django/forms/widgets/datetime.html | 1 + .../jinja2/django/forms/widgets/email.html | 1 + .../jinja2/django/forms/widgets/file.html | 1 + .../jinja2/django/forms/widgets/hidden.html | 1 + .../jinja2/django/forms/widgets/input.html | 1 + .../django/forms/widgets/input_option.html | 1 + .../django/forms/widgets/multiple_hidden.html | 1 + .../django/forms/widgets/multiple_input.html | 5 + .../django/forms/widgets/multiwidget.html | 1 + .../jinja2/django/forms/widgets/number.html | 1 + .../jinja2/django/forms/widgets/password.html | 1 + .../jinja2/django/forms/widgets/radio.html | 1 + .../django/forms/widgets/radio_option.html | 1 + .../jinja2/django/forms/widgets/select.html | 5 + .../django/forms/widgets/select_date.html | 1 + .../django/forms/widgets/select_option.html | 1 + .../django/forms/widgets/splitdatetime.html | 1 + .../forms/widgets/splithiddendatetime.html | 1 + .../jinja2/django/forms/widgets/text.html | 1 + .../jinja2/django/forms/widgets/textarea.html | 2 + .../jinja2/django/forms/widgets/time.html | 1 + .../jinja2/django/forms/widgets/url.html | 1 + django/forms/renderers.py | 71 ++ .../templates/django/forms/widgets/attrs.html | 1 + .../django/forms/widgets/checkbox.html | 1 + .../django/forms/widgets/checkbox_option.html | 1 + .../django/forms/widgets/checkbox_select.html | 1 + .../forms/widgets/clearable_file_input.html | 5 + .../templates/django/forms/widgets/date.html | 1 + .../django/forms/widgets/datetime.html | 1 + .../templates/django/forms/widgets/email.html | 1 + .../templates/django/forms/widgets/file.html | 1 + .../django/forms/widgets/hidden.html | 1 + .../templates/django/forms/widgets/input.html | 1 + .../django/forms/widgets/input_option.html | 1 + .../django/forms/widgets/multiple_hidden.html | 1 + .../django/forms/widgets/multiple_input.html | 5 + .../django/forms/widgets/multiwidget.html | 1 + .../django/forms/widgets/number.html | 1 + .../django/forms/widgets/password.html | 1 + .../templates/django/forms/widgets/radio.html | 1 + .../django/forms/widgets/radio_option.html | 1 + .../django/forms/widgets/select.html | 5 + .../django/forms/widgets/select_date.html | 1 + .../django/forms/widgets/select_option.html | 1 + .../django/forms/widgets/splitdatetime.html | 1 + .../forms/widgets/splithiddendatetime.html | 1 + .../templates/django/forms/widgets/text.html | 1 + .../django/forms/widgets/textarea.html | 2 + .../templates/django/forms/widgets/time.html | 1 + .../templates/django/forms/widgets/url.html | 1 + django/forms/widgets.py | 803 ++++++++---------- django/template/backends/jinja2.py | 2 +- django/test/signals.py | 2 + docs/internals/deprecation.txt | 3 + docs/ref/forms/api.txt | 23 + docs/ref/forms/index.txt | 1 + docs/ref/forms/renderers.txt | 131 +++ docs/ref/forms/widgets.txt | 156 ++-- docs/ref/settings.txt | 16 + docs/releases/1.11.txt | 28 + docs/spelling_wordlist | 1 + tests/admin_inlines/tests.py | 26 +- tests/admin_views/tests.py | 2 +- tests/admin_widgets/tests.py | 62 +- .../field_tests/test_filepathfield.py | 3 + .../jinja2/forms_tests/custom_widget.html | 1 + .../templates/forms_tests/custom_widget.html | 1 + tests/forms_tests/tests/test_forms.py | 93 +- tests/forms_tests/tests/test_renderers.py | 52 ++ tests/forms_tests/tests/test_widgets.py | 201 +---- tests/forms_tests/widget_tests/base.py | 20 +- tests/forms_tests/widget_tests/test_select.py | 62 ++ tests/model_forms/tests.py | 9 +- tests/runtests.py | 5 + 98 files changed, 1334 insertions(+), 874 deletions(-) create mode 100644 django/contrib/admin/templates/admin/widgets/clearable_file_input.html create mode 100644 django/contrib/admin/templates/admin/widgets/foreign_key_raw_id.html create mode 100644 django/contrib/admin/templates/admin/widgets/many_to_many_raw_id.html create mode 100644 django/contrib/admin/templates/admin/widgets/radio.html create mode 100644 django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html create mode 100644 django/contrib/admin/templates/admin/widgets/split_datetime.html create mode 100644 django/contrib/admin/templates/admin/widgets/url.html create mode 100644 django/contrib/auth/templates/auth/widgets/read_only_password_hash.html create mode 100644 django/forms/jinja2/django/forms/widgets/attrs.html create mode 100644 django/forms/jinja2/django/forms/widgets/checkbox.html create mode 100644 django/forms/jinja2/django/forms/widgets/checkbox_option.html create mode 100644 django/forms/jinja2/django/forms/widgets/checkbox_select.html create mode 100644 django/forms/jinja2/django/forms/widgets/clearable_file_input.html create mode 100644 django/forms/jinja2/django/forms/widgets/date.html create mode 100644 django/forms/jinja2/django/forms/widgets/datetime.html create mode 100644 django/forms/jinja2/django/forms/widgets/email.html create mode 100644 django/forms/jinja2/django/forms/widgets/file.html create mode 100644 django/forms/jinja2/django/forms/widgets/hidden.html create mode 100644 django/forms/jinja2/django/forms/widgets/input.html create mode 100644 django/forms/jinja2/django/forms/widgets/input_option.html create mode 100644 django/forms/jinja2/django/forms/widgets/multiple_hidden.html create mode 100644 django/forms/jinja2/django/forms/widgets/multiple_input.html create mode 100644 django/forms/jinja2/django/forms/widgets/multiwidget.html create mode 100644 django/forms/jinja2/django/forms/widgets/number.html create mode 100644 django/forms/jinja2/django/forms/widgets/password.html create mode 100644 django/forms/jinja2/django/forms/widgets/radio.html create mode 100644 django/forms/jinja2/django/forms/widgets/radio_option.html create mode 100644 django/forms/jinja2/django/forms/widgets/select.html create mode 100644 django/forms/jinja2/django/forms/widgets/select_date.html create mode 100644 django/forms/jinja2/django/forms/widgets/select_option.html create mode 100644 django/forms/jinja2/django/forms/widgets/splitdatetime.html create mode 100644 django/forms/jinja2/django/forms/widgets/splithiddendatetime.html create mode 100644 django/forms/jinja2/django/forms/widgets/text.html create mode 100644 django/forms/jinja2/django/forms/widgets/textarea.html create mode 100644 django/forms/jinja2/django/forms/widgets/time.html create mode 100644 django/forms/jinja2/django/forms/widgets/url.html create mode 100644 django/forms/renderers.py create mode 100644 django/forms/templates/django/forms/widgets/attrs.html create mode 100644 django/forms/templates/django/forms/widgets/checkbox.html create mode 100644 django/forms/templates/django/forms/widgets/checkbox_option.html create mode 100644 django/forms/templates/django/forms/widgets/checkbox_select.html create mode 100644 django/forms/templates/django/forms/widgets/clearable_file_input.html create mode 100644 django/forms/templates/django/forms/widgets/date.html create mode 100644 django/forms/templates/django/forms/widgets/datetime.html create mode 100644 django/forms/templates/django/forms/widgets/email.html create mode 100644 django/forms/templates/django/forms/widgets/file.html create mode 100644 django/forms/templates/django/forms/widgets/hidden.html create mode 100644 django/forms/templates/django/forms/widgets/input.html create mode 100644 django/forms/templates/django/forms/widgets/input_option.html create mode 100644 django/forms/templates/django/forms/widgets/multiple_hidden.html create mode 100644 django/forms/templates/django/forms/widgets/multiple_input.html create mode 100644 django/forms/templates/django/forms/widgets/multiwidget.html create mode 100644 django/forms/templates/django/forms/widgets/number.html create mode 100644 django/forms/templates/django/forms/widgets/password.html create mode 100644 django/forms/templates/django/forms/widgets/radio.html create mode 100644 django/forms/templates/django/forms/widgets/radio_option.html create mode 100644 django/forms/templates/django/forms/widgets/select.html create mode 100644 django/forms/templates/django/forms/widgets/select_date.html create mode 100644 django/forms/templates/django/forms/widgets/select_option.html create mode 100644 django/forms/templates/django/forms/widgets/splitdatetime.html create mode 100644 django/forms/templates/django/forms/widgets/splithiddendatetime.html create mode 100644 django/forms/templates/django/forms/widgets/text.html create mode 100644 django/forms/templates/django/forms/widgets/textarea.html create mode 100644 django/forms/templates/django/forms/widgets/time.html create mode 100644 django/forms/templates/django/forms/widgets/url.html create mode 100644 docs/ref/forms/renderers.txt create mode 100644 tests/forms_tests/jinja2/forms_tests/custom_widget.html create mode 100644 tests/forms_tests/templates/forms_tests/custom_widget.html create mode 100644 tests/forms_tests/tests/test_renderers.py 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 %}

{{ initial_text }}: {{ widget.value }}{% if not widget.required %} + + +{% endif %}
+{{ input_text }}:{% endif %} +{% if is_initial %}

{% 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 %}{% endif %}{% if link_label %} {% if link_url %}{% endif %}{{ link_label }}{% if link_url %}{% endif %}{% 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 %} + 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 @@ +

+ {{ date_label }} {% with widget=widget.subwidgets.0 %}{% include widget.template_name %}{% endwith %}
+ {{ time_label }} {% with widget=widget.subwidgets.1 %}{% include widget.template_name %}{% endwith %} +

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 %}

{{ current_label }} {{ widget.value }}
{{ change_label }} {% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.value %}

{% 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('

{} {}
{} {}

', - _('Date:'), rendered_widgets[0], - _('Time:'), rendered_widgets[1]) - - -class AdminRadioFieldRenderer(RadioFieldRenderer): - def render(self): - """Outputs a ', - flatatt(self.attrs), - format_html_join('\n', '
  • {}
  • ', - ((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 = ( - '

    %s

    ' % forms.ClearableFileInput.template_with_initial - ) - template_with_clear = ( - '%s' % 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 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( - '

    {} {}
    {} {}

    ', - _('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("%s" % 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("%s" % ugettext( - "Invalid password format or unknown hashing algorithm." - )) + summary.append({'label': ugettext("Invalid password format or unknown hashing algorithm.")}) else: - summary = format_html_join( - '', '{}: {} ', - ((ugettext(key), value) for key, value in hasher.safe_summary(encoded).items()) - ) - - return format_html("{}", 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 %} +{{ entry.label }}{% 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 %} + + {% 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 }}: {{ widget.value }}{% if not widget.required %} + +{% endif %}
    +{{ input_text }}:{% endif %} + 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 @@ + 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 %}{% endif %}{% include "django/forms/widgets/input.html" %}{% if wrap_label %} {{ widget.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 %}{% for group, options, index in widget.optgroups %}{% if group %} +
  • {{ group }}{% endif %}{% for widget in options %} +
  • {% include widget.template_name %}
  • {% endfor %}{% if group %} + {% endif %}{% endfor %} + 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 @@ + 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 @@ + 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 @@ + 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 }}: {{ widget.value }}{% if not widget.required %} + +{% endif %}
    +{{ input_text }}:{% endif %} + 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 @@ + 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 %}{% endif %}{% include "django/forms/widgets/input.html" %}{% if wrap_label %} {{ widget.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 %}{% for group, options, index in widget.optgroups %}{% if group %} +
  • {{ group }}{% endif %}{% for option in options %} +
  • {% include option.template_name with widget=option %}
  • {% endfor %}{% if group %} + {% endif %}{% endfor %} +{% 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 @@ + 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 @@ + 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 @@ + 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 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): """ - yield SubWidget(self, name, value, attrs, choices) + Return a value as it should appear when rendered in a template. + """ + if value is None: + value = '' + if self.is_localized: + return formats.localize_input(value) + return force_text(value) - def render(self, name, value, attrs=None): + 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, 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 build_attrs(self, extra_attrs=None, **kwargs): + 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, 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 widgets (except type='checkbox' and - type='radio', which are special). + Base class for all widgets. """ input_type = None # Subclasses must define this. - - def format_value(self, value): - if self.is_localized: - return formats.localize_input(value) - return value - - 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('', flatatt(final_attrs)) - - -class TextInput(Input): - input_type = 'text' + template_name = 'django/forms/widgets/input.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) + super(Input, self).__init__(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 NumberInput(TextInput): +class TextInput(Input): + input_type = 'text' + template_name = 'django/forms/widgets/text.html' + + +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 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('', 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: %(initial)s ' - '%(clear_template)s
    %(input_text)s: %(input)s' - ) - - template_with_clear = '%(clear)s ' + 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), - } - - def render(self, name, value, attrs=None): - substitutions = { - 'initial_text': self.initial_text, - 'input_text': self.input_text, - 'clear_template': '', - '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 value - return mark_safe(template % substitutions) + 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, + 'initial_text': self.initial_text, + 'clear_checkbox_label': self.clear_checkbox_label, + }) + 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('\r\n{}', 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('', 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('', flatatt(final_attrs))] - options = self.render_options([value]) - if options: - output.append(options) - output.append('') - 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, 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 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('', force_text(option_value))) - for option in option_label: - output.append(self.render_option(selected_choices, *option)) - output.append('') + 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: - output.append(self.render_option(selected_choices, option_value, option_label)) - return '\n'.join(output) + option_value = force_text(option_value) + + if isinstance(option_label, (list, tuple)): + 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: + 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('') - 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 = 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_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('', flatatt(final_attrs)) - - @property - def id_for_label(self): - return self.attrs.get('id', '') - - -class RadioChoiceInput(ChoiceInput): +class RadioSelect(ChoiceWidget): input_type = 'radio' - - def __init__(self, *args, **kwargs): - super(RadioChoiceInput, self).__init__(*args, **kwargs) - self.value = force_text(self.value) + template_name = 'django/forms/widgets/radio.html' + option_template_name = 'django/forms/widgets/radio_option.html' -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 = '{content}' - inner_html = '
  • {choice_value}{sub_widgets}
  • ' - - 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
      for this set of choice fields. - If an id was given to the field, it is applied to the
        (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