Permalink
Browse files

Fixed #15667 -- Added template-based widget rendering.

Thanks Carl Meyer and Tim Graham for contributing to the patch.
  • Loading branch information...
1 parent 38cf9ef commit 0b357558ca5b19e7a74c6d31d62cd8ddb3311731 @prestontimmons prestontimmons committed with timgraham Sep 12, 2015
Showing with 1,398 additions and 866 deletions.
  1. +3 −0 django/conf/global_settings.py
  2. +6 −0 django/contrib/admin/templates/admin/widgets/clearable_file_input.html
  3. +1 −0 django/contrib/admin/templates/admin/widgets/foreign_key_raw_id.html
  4. +1 −0 django/contrib/admin/templates/admin/widgets/many_to_many_raw_id.html
  5. +1 −0 django/contrib/admin/templates/admin/widgets/radio.html
  6. +27 −0 django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html
  7. +4 −0 django/contrib/admin/templates/admin/widgets/split_datetime.html
  8. +1 −0 django/contrib/admin/templates/admin/widgets/url.html
  9. +1 −0 django/contrib/admin/tests.py
  10. +77 −100 django/contrib/admin/widgets.py
  11. +13 −18 django/contrib/auth/forms.py
  12. +3 −0 django/contrib/auth/templates/auth/widgets/read_only_password_hash.html
  13. +1 −1 django/contrib/gis/admin/widgets.py
  14. +7 −4 django/contrib/gis/forms/widgets.py
  15. +1 −1 django/contrib/postgres/forms/array.py
  16. +68 −2 django/forms/boundfield.py
  17. +16 −1 django/forms/forms.py
  18. +1 −0 django/forms/jinja2/django/forms/widgets/attrs.html
  19. +1 −0 django/forms/jinja2/django/forms/widgets/checkbox.html
  20. +1 −0 django/forms/jinja2/django/forms/widgets/checkbox_option.html
  21. +1 −0 django/forms/jinja2/django/forms/widgets/checkbox_select.html
  22. +5 −0 django/forms/jinja2/django/forms/widgets/clearable_file_input.html
  23. +1 −0 django/forms/jinja2/django/forms/widgets/date.html
  24. +1 −0 django/forms/jinja2/django/forms/widgets/datetime.html
  25. +1 −0 django/forms/jinja2/django/forms/widgets/email.html
  26. +1 −0 django/forms/jinja2/django/forms/widgets/file.html
  27. +1 −0 django/forms/jinja2/django/forms/widgets/hidden.html
  28. +1 −0 django/forms/jinja2/django/forms/widgets/input.html
  29. +1 −0 django/forms/jinja2/django/forms/widgets/input_option.html
  30. +1 −0 django/forms/jinja2/django/forms/widgets/multiple_hidden.html
  31. +5 −0 django/forms/jinja2/django/forms/widgets/multiple_input.html
  32. +1 −0 django/forms/jinja2/django/forms/widgets/multiwidget.html
  33. +1 −0 django/forms/jinja2/django/forms/widgets/number.html
  34. +1 −0 django/forms/jinja2/django/forms/widgets/password.html
  35. +1 −0 django/forms/jinja2/django/forms/widgets/radio.html
  36. +1 −0 django/forms/jinja2/django/forms/widgets/radio_option.html
  37. +5 −0 django/forms/jinja2/django/forms/widgets/select.html
  38. +1 −0 django/forms/jinja2/django/forms/widgets/select_date.html
  39. +1 −0 django/forms/jinja2/django/forms/widgets/select_option.html
  40. +1 −0 django/forms/jinja2/django/forms/widgets/splitdatetime.html
  41. +1 −0 django/forms/jinja2/django/forms/widgets/splithiddendatetime.html
  42. +1 −0 django/forms/jinja2/django/forms/widgets/text.html
  43. +2 −0 django/forms/jinja2/django/forms/widgets/textarea.html
  44. +1 −0 django/forms/jinja2/django/forms/widgets/time.html
  45. +1 −0 django/forms/jinja2/django/forms/widgets/url.html
  46. +9 −0 django/forms/renderers/__init__.py
  47. +55 −0 django/forms/renderers/templates.py
  48. +1 −0 django/forms/templates/django/forms/widgets/attrs.html
  49. +1 −0 django/forms/templates/django/forms/widgets/checkbox.html
  50. +1 −0 django/forms/templates/django/forms/widgets/checkbox_option.html
  51. +1 −0 django/forms/templates/django/forms/widgets/checkbox_select.html
  52. +5 −0 django/forms/templates/django/forms/widgets/clearable_file_input.html
  53. +1 −0 django/forms/templates/django/forms/widgets/date.html
  54. +1 −0 django/forms/templates/django/forms/widgets/datetime.html
  55. +1 −0 django/forms/templates/django/forms/widgets/email.html
  56. +1 −0 django/forms/templates/django/forms/widgets/file.html
  57. +1 −0 django/forms/templates/django/forms/widgets/hidden.html
  58. +1 −0 django/forms/templates/django/forms/widgets/input.html
  59. +1 −0 django/forms/templates/django/forms/widgets/input_option.html
  60. +1 −0 django/forms/templates/django/forms/widgets/multiple_hidden.html
  61. +5 −0 django/forms/templates/django/forms/widgets/multiple_input.html
  62. +1 −0 django/forms/templates/django/forms/widgets/multiwidget.html
  63. +1 −0 django/forms/templates/django/forms/widgets/number.html
  64. +1 −0 django/forms/templates/django/forms/widgets/password.html
  65. +1 −0 django/forms/templates/django/forms/widgets/radio.html
  66. +1 −0 django/forms/templates/django/forms/widgets/radio_option.html
  67. +5 −0 django/forms/templates/django/forms/widgets/select.html
  68. +1 −0 django/forms/templates/django/forms/widgets/select_date.html
  69. +1 −0 django/forms/templates/django/forms/widgets/select_option.html
  70. +1 −0 django/forms/templates/django/forms/widgets/splitdatetime.html
  71. +1 −0 django/forms/templates/django/forms/widgets/splithiddendatetime.html
  72. +1 −0 django/forms/templates/django/forms/widgets/text.html
  73. +2 −0 django/forms/templates/django/forms/widgets/textarea.html
  74. +1 −0 django/forms/templates/django/forms/widgets/time.html
  75. +1 −0 django/forms/templates/django/forms/widgets/url.html
  76. +390 −435 django/forms/widgets.py
  77. +3 −0 django/test/signals.py
  78. +3 −0 docs/internals/deprecation.txt
  79. +21 −0 docs/ref/forms/api.txt
  80. +1 −0 docs/ref/forms/index.txt
  81. +115 −0 docs/ref/forms/renderers.txt
  82. +96 −60 docs/ref/forms/widgets.txt
  83. +16 −0 docs/ref/settings.txt
  84. +27 −0 docs/releases/1.11.txt
  85. +14 −12 tests/admin_inlines/tests.py
  86. +45 −21 tests/admin_widgets/tests.py
  87. +2 −0 tests/forms_tests/field_tests/test_filepathfield.py
  88. +1 −0 tests/forms_tests/templates/forms_tests/custom_widget.html
  89. +97 −2 tests/forms_tests/tests/test_forms.py
  90. +67 −0 tests/forms_tests/tests/test_renderer.py
  91. +1 −200 tests/forms_tests/tests/test_widgets.py
  92. +48 −1 tests/forms_tests/widget_tests/base.py
  93. +66 −0 tests/forms_tests/widget_tests/test_select.py
  94. +1 −8 tests/model_forms/tests.py
  95. +7 −0 tests/runtests.py
View
3 django/conf/global_settings.py
@@ -213,6 +213,9 @@ def gettext_noop(s):
TEMPLATES = []
+# Default form rendering class.
+FORM_RENDERER = 'django.forms.renderers.templates.TemplateRenderer'
+
# Default email address to use for various automated correspondence from
# the site managers.
DEFAULT_FROM_EMAIL = 'webmaster@localhost'
View
6 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 %}
View
1 django/contrib/admin/templates/admin/widgets/foreign_key_raw_id.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/input.html' %}{% if related_url %}<a href="{{ related_url }}" class="related-lookup" id="lookup_id_{{ widget.name }}" title="{{ link_title }}"></a>{% endif %}{% if link_label %}&nbsp;<strong>{% if link_url %}<a href="{{ link_url }}">{% endif %}{{ link_label }}{% if link_url %}</a>{% endif %}</strong>{% endif %}
View
1 django/contrib/admin/templates/admin/widgets/many_to_many_raw_id.html
@@ -0,0 +1 @@
+{% include 'admin/widgets/foreign_key_raw_id.html' %}
View
1 django/contrib/admin/templates/admin/widgets/radio.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiple_input.html" %}
View
27 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>
View
4 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>
View
1 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 %}
View
1 django/contrib/admin/tests.py
@@ -21,6 +21,7 @@ class AdminSeleniumTestCase(SeleniumTestCase, StaticLiveServerTestCase):
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
+ 'django.forms',
]
def wait_until(self, callback, timeout=10):
View
177 django/contrib/admin/widgets.py
@@ -7,14 +7,11 @@
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 @@ def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
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,18 @@ 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 +148,19 @@ def render(self, name, value, attrs=None):
params = self.url_parameters()
if params:
- url = '?' + '&amp;'.join('%s=%s' % (k, v) for k, v in params.items())
- else:
- url = ''
- if "class" not in attrs:
- attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript code looks for this hook.
- # TODO: "lookup_id_" is hard-coded here. This should instead use
- # the correct API to determine the ID dynamically.
- extra.append(
- '<a href="%s%s" class="related-lookup" id="lookup_id_%s" title="%s"></a>'
- % (related_url, url, name, _('Lookup'))
- )
- output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] + extra
- if value:
- output.append(self.label_for_value(value))
- return mark_safe(''.join(output))
+ related_url += '?' + '&amp;'.join(
+ '%s=%s' % (k, v) for k, v in params.items(),
+ )
+
+ context['related_url'] = mark_safe(related_url)
+ context['link_title'] = _('Lookup')
+ # The JavaScript code looks for this class.
+ context['widget']['attrs'].setdefault('class', 'vForeignKeyRawIdAdminField')
+
+ if context['widget']['value']:
+ context['link_label'], context['link_url'] = self.label_and_url_for_value(value)
+
+ return context
def base_url_parameters(self):
limit_choices_to = self.rel.limit_choices_to
@@ -192,17 +174,15 @@ def url_parameters(self):
params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
return params
- def label_for_value(self, value):
+ def label_and_url_for_value(self, value):
key = self.rel.get_related_field().name
try:
obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
except (ValueError, self.rel.model.DoesNotExist):
- return ''
+ return '', ''
- label = '&nbsp;<strong>{}</strong>'
- text = Truncator(obj).words(14, truncate='...')
try:
- change_url = reverse(
+ url = reverse(
'%s:%s_%s_change' % (
self.admin_site.name,
obj._meta.app_label,
@@ -211,55 +191,55 @@ def label_for_value(self, value):
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):
"""
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):
+ if value:
+ return ','.join(force_text(v) for v in value)
+ return ''
+
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):
self.needs_multipart_form = widget.needs_multipart_form
- self.attrs = widget.attrs
- self.choices = widget.choices
self.widget = widget
+ self.choices = widget.choices
+ self.attrs = widget.attrs
self.rel = rel
# Backwards compatible check for whether a user can add related
# objects.
@@ -294,21 +274,21 @@ def get_related_url(self, info, action, *args):
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 +307,8 @@ def render(self, name, value, *args, **kwargs):
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 +342,24 @@ def __init__(self, attrs=None):
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.TextInput):
View
31 django/contrib/auth/forms.py
@@ -13,37 +13,32 @@
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 _
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):
View
3 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 %}
View
2 django/contrib/gis/admin/widgets.py
@@ -16,7 +16,7 @@ class OpenLayersWidget(Textarea):
"""
Renders an OpenLayers map using the WKT of the geometry.
"""
- def render(self, name, value, attrs=None):
+ def render(self, name, value, attrs=None, renderer=None):
# Update the template parameters with any attributes passed in.
if attrs:
self.params.update(attrs)
View
11 django/contrib/gis/forms/widgets.py
@@ -43,7 +43,7 @@ def deserialize(self, value):
logger.error("Error creating geometry from value '%s' (%s)", value, err)
return None
- def render(self, name, value, attrs=None):
+ def render(self, name, value, attrs=None, renderer=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,15 +62,18 @@ def render(self, name, value, attrs=None):
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(),
- )
+ **attrs
+ ))
return loader.render_to_string(self.template_name, context)
View
2 django/contrib/postgres/forms/array.py
@@ -109,7 +109,7 @@ def id_for_label(self, id_):
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 []
View
70 django/forms/boundfield.py
@@ -1,15 +1,18 @@
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, smart_text,
)
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 _
@@ -51,7 +54,10 @@ def subwidgets(self):
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)
@@ -99,7 +105,24 @@ def as_widget(self, widget=None, attrs=None, only_initial=False):
name = self.html_name
else:
name = self.html_initial_name
- return force_text(widget.render(name, self.value(), attrs=attrs))
+
+ kwargs = {
+ 'name': name,
+ 'value': self.value(),
+ 'attrs': attrs,
+ }
+ if func_supports_parameter(widget.render, 'renderer'):
+ kwargs['renderer'] = self.form.renderer
+ else:
+ warnings.warn(
+ 'Backwards compatibility for widgets without support for the '
+ '`renderer` argument in Widget.render() will be removed in '
+ 'Django 2.0.',
+ RemovedInDjango21Warning, stacklevel=2,
+ )
+
+ html = widget.render(**kwargs)
+ return force_text(html)
def as_text(self, attrs=None, **kwargs):
"""
@@ -232,3 +255,46 @@ def build_widget_attrs(self, attrs, widget=None):
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']
View
17 django/forms/forms.py
@@ -21,6 +21,8 @@
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,18 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
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:
+ if isinstance(self.default_renderer, type):
+ renderer = self.default_renderer()
+ else:
+ renderer = self.default_renderer
+ self.renderer = renderer
+
def order_fields(self, field_order):
"""
Rearranges the fields according to field_order.
View
1 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 %}
View
1 django/forms/jinja2/django/forms/widgets/checkbox.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/jinja2/django/forms/widgets/checkbox_option.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input_option.html" %}
View
1 django/forms/jinja2/django/forms/widgets/checkbox_select.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiple_input.html" %}
View
5 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" %} />
View
1 django/forms/jinja2/django/forms/widgets/date.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/jinja2/django/forms/widgets/datetime.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/jinja2/django/forms/widgets/email.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/jinja2/django/forms/widgets/file.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/jinja2/django/forms/widgets/hidden.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 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" %} />
View
1 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 %}
View
1 django/forms/jinja2/django/forms/widgets/multiple_hidden.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiwidget.html" %}
View
5 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>
View
1 django/forms/jinja2/django/forms/widgets/multiwidget.html
@@ -0,0 +1 @@
+{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}
View
1 django/forms/jinja2/django/forms/widgets/number.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/jinja2/django/forms/widgets/password.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/jinja2/django/forms/widgets/radio.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiple_input.html" %}
View
1 django/forms/jinja2/django/forms/widgets/radio_option.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input_option.html" %}
View
5 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>
View
1 django/forms/jinja2/django/forms/widgets/select_date.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
View
1 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>
View
1 django/forms/jinja2/django/forms/widgets/splitdatetime.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
View
1 django/forms/jinja2/django/forms/widgets/splithiddendatetime.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
View
1 django/forms/jinja2/django/forms/widgets/text.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
2 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>
View
1 django/forms/jinja2/django/forms/widgets/time.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/jinja2/django/forms/widgets/url.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
9 django/forms/renderers/__init__.py
@@ -0,0 +1,9 @@
+from django.conf import settings
+from django.utils import lru_cache
+from django.utils.module_loading import import_string
+
+
+@lru_cache.lru_cache()
+def get_default_renderer():
+ renderer_class = import_string(settings.FORM_RENDERER)
+ return renderer_class()
View
55 django/forms/renderers/templates.py
@@ -0,0 +1,55 @@
+import os
+
+from django import forms
+from django.template import TemplateDoesNotExist
+from django.template.backends.django import DjangoTemplates
+from django.template.loader import get_template
+from django.utils._os import upath
+from django.utils.functional import cached_property
+
+try:
+ import jinja2
+except ImportError:
+ jinja2 = None
+
+ROOT = upath(os.path.dirname(forms.__file__))
+
+
+class StandaloneTemplateRenderer(object):
+ """Render using only the built-in templates."""
+ def get_template(self, template_name):
+ return self.standalone_engine.get_template(template_name)
+
+ def render(self, template_name, context, request=None):
+ template = self.get_template(template_name)
+ return template.render(context, request=request).strip()
+
+ @cached_property
+ def standalone_engine(self):
+ if jinja2:
+ from django.template.backends.jinja2 import Jinja2
+ return Jinja2({
+ 'APP_DIRS': False,
+ 'DIRS': [os.path.join(ROOT, 'jinja2')],
+ 'NAME': 'djangoforms',
+ 'OPTIONS': {},
+ })
+ return DjangoTemplates({
+ 'APP_DIRS': False,
+ 'DIRS': [os.path.join(ROOT, 'templates')],
+ 'NAME': 'djangoforms',
+ 'OPTIONS': {},
+ })
+
+
+class TemplateRenderer(StandaloneTemplateRenderer):
+ """Render first via TEMPLATES, then fall back to built-in templates."""
+ def get_template(self, template_name):
+ try:
+ return get_template(template_name)
+ except TemplateDoesNotExist as e:
+ try:
+ return super(TemplateRenderer, self).get_template(template_name)
+ except TemplateDoesNotExist as e2:
+ e.chain.append(e2)
+ raise TemplateDoesNotExist(template_name, chain=e.chain)
View
1 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 %}
View
1 django/forms/templates/django/forms/widgets/checkbox.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/templates/django/forms/widgets/checkbox_option.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input_option.html" %}
View
1 django/forms/templates/django/forms/widgets/checkbox_select.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiple_input.html" %}
View
5 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" %} />
View
1 django/forms/templates/django/forms/widgets/date.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/templates/django/forms/widgets/datetime.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/templates/django/forms/widgets/email.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/templates/django/forms/widgets/file.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/templates/django/forms/widgets/hidden.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 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" %} />
View
1 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 %}
View
1 django/forms/templates/django/forms/widgets/multiple_hidden.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiwidget.html" %}
View
5 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 %}
View
1 django/forms/templates/django/forms/widgets/multiwidget.html
@@ -0,0 +1 @@
+{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}
View
1 django/forms/templates/django/forms/widgets/number.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/templates/django/forms/widgets/password.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/templates/django/forms/widgets/radio.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/multiple_input.html" %}
View
1 django/forms/templates/django/forms/widgets/radio_option.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input_option.html" %}
View
5 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>
View
1 django/forms/templates/django/forms/widgets/select_date.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
View
1 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>
View
1 django/forms/templates/django/forms/widgets/splitdatetime.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
View
1 django/forms/templates/django/forms/widgets/splithiddendatetime.html
@@ -0,0 +1 @@
+{% include 'django/forms/widgets/multiwidget.html' %}
View
1 django/forms/templates/django/forms/widgets/text.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
2 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>
View
1 django/forms/templates/django/forms/widgets/time.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
1 django/forms/templates/django/forms/widgets/url.html
@@ -0,0 +1 @@
+{% include "django/forms/widgets/input.html" %}
View
825 django/forms/widgets.py
@@ -7,10 +7,11 @@
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 @@
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 @@ def __new__(mcs, name, bases, attrs):
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 @@ def __deepcopy__(self, memo):
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
@@ -254,83 +258,88 @@ def use_required_attribute(self, initial):
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'
-
- def __init__(self, attrs=None):
- if attrs is not None:
- self.input_type = attrs.pop('type', self.input_type)
- super(TextInput, self).__init__(attrs)
+ template_name = 'django/forms/widgets/text.html'
-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):
"""
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:
@@ -339,13 +348,20 @@ def value_from_datadict(self, data, files, name):
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"
@@ -356,16 +372,10 @@ def value_from_datadict(self, data, files, name):
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):
"""
@@ -386,37 +396,26 @@ def is_initial(self, value):
"""
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,
+ if self.is_initial(value):
+ return value
+
+ 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)
@@ -437,19 +436,15 @@ def use_required_attribute(self, initial):
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'}
if attrs:
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 = ''
@@ -465,38 +460,49 @@ def format_value(self, value):
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)
def boolean_check(v):
return not (v is False or v is None or v == '')
-class CheckboxInput(Widget):
- # Don't use model field defaults for fields that aren't in POST data,
- # because checkboxes don't appear in POST data if not checked.
+class CheckboxInput(Input):
dont_use_model_field_default_for_empty_data = True
+ 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'] = 'checked'
+ return super(CheckboxInput, self).get_context(name, value, attrs)
def value_from_datadict(self, data, files, name):
if name not in data:
@@ -511,11 +517,17 @@ def value_from_datadict(self, data, files, name):
return bool(value)
-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': 'checked'}
+ 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.
@@ -528,43 +540,161 @@ def __deepcopy__(self, memo):
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 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="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_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:
+ 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(self, name, value, attrs=None, renderer=None):
+ context = self.get_context(name, value, attrs)
+ return self._render(self.template_name, context, renderer)
+
+ def render_subwidget(self, context, renderer=None):
+ if renderer is None:
+ renderer = get_default_renderer()
+ return format_html(renderer.render(self.option_template_name, context))
+
+ 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)
+
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': 'selected'}
+ 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):
@@ -579,12 +709,11 @@ def __init__(self, attrs=None):
)
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)
@@ -601,17 +730,6 @@ def value_from_datadict(self, data, files, name):
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
@@ -620,190 +738,17 @@ def value_from_datadict(self, data, files, name):
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 =