diff options
| author | Preston Timmons <prestontimmons@gmail.com> | 2016-12-27 17:00:56 -0500 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2016-12-27 17:50:10 -0500 |
| commit | b52c73008a9d67e9ddbb841872dc15cdd3d6ee01 (patch) | |
| tree | b58a2d18242db5234b18678116e07e6f6bbc7cb3 /django | |
| parent | 51cde873d9fc8e4540f4efecbd39cfe8e770be38 (diff) | |
Fixed #15667 -- Added template-based widget rendering.
Thanks Carl Meyer and Tim Graham for contributing to the patch.
Diffstat (limited to 'django')
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 %} <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 = '?' + '&'.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 += '?' + '&'.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 = ' <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) |
