diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index b0e355eeb6..e72ab014b8 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -11,6 +11,7 @@ try:
from urllib.parse import urljoin
except ImportError: # Python 2
from urlparse import urljoin
+import warnings
from django.conf import settings
from django.forms.util import flatatt, to_current_timezone
@@ -585,14 +586,16 @@ class SelectMultiple(Select):
@python_2_unicode_compatible
-class RadioInput(SubWidget):
+class ChoiceInput(SubWidget):
"""
- An object used by RadioFieldRenderer that represents a single
- .
+ An object used by ChoiceFieldRenderer that represents a single
+ .
"""
+ input_type = None # Subclasses must define this
def __init__(self, name, value, attrs, choice, index):
- self.name, self.value = name, value
+ self.name = name
+ self.value = value
self.attrs = attrs
self.choice_value = force_text(choice[0])
self.choice_label = force_text(choice[1])
@@ -609,8 +612,7 @@ class RadioInput(SubWidget):
label_for = format_html(' for="{0}_{1}"', self.attrs['id'], self.index)
else:
label_for = ''
- choice_label = force_text(self.choice_label)
- return format_html('{1} {2} ', label_for, self.tag(), choice_label)
+ return format_html('{1} {2} ', label_for, self.tag(), self.choice_label)
def is_checked(self):
return self.value == self.choice_value
@@ -618,34 +620,69 @@ class RadioInput(SubWidget):
def tag(self):
if 'id' in self.attrs:
self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
- final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value)
+ final_attrs = dict(self.attrs, type=self.input_type, name=self.name, value=self.choice_value)
if self.is_checked():
final_attrs['checked'] = 'checked'
return format_html(' ', flatatt(final_attrs))
+
+class RadioChoiceInput(ChoiceInput):
+ input_type = 'radio'
+
+ def __init__(self, *args, **kwargs):
+ super(RadioChoiceInput, self).__init__(*args, **kwargs)
+ self.value = force_text(self.value)
+
+
+class RadioInput(RadioChoiceInput):
+ def __init__(self, *args, **kwargs):
+ msg = "RadioInput has been deprecated. Use RadioChoiceInput instead."
+ warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
+ super(RadioInput, self).__init__(*args, **kwargs)
+
+
+class CheckboxChoiceInput(ChoiceInput):
+ 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
+
+
@python_2_unicode_compatible
-class RadioFieldRenderer(object):
+class ChoiceFieldRenderer(object):
"""
An object used by RadioSelect to enable customization of radio widgets.
"""
+ choice_input_class = None
+
def __init__(self, name, value, attrs, choices):
- self.name, self.value, self.attrs = name, value, attrs
+ self.name = name
+ self.value = value
+ self.attrs = attrs
self.choices = choices
def __iter__(self):
for i, choice in enumerate(self.choices):
- yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
+ yield self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, i)
def __getitem__(self, idx):
choice = self.choices[idx] # Let the IndexError propogate
- return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
+ return self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, idx)
def __str__(self):
return self.render()
def render(self):
- """Outputs a
for this set of radio fields."""
+ """
+ Outputs a for this set of choice fields.
+ If an id was given to the field, it is applied to the (each
+ item in the list will get an id of `$id_$i`).
+ """
id_ = self.attrs.get('id', None)
start_tag = format_html('', id_) if id_ else ''
output = [start_tag]
@@ -654,15 +691,25 @@ class RadioFieldRenderer(object):
output.append(' ')
return mark_safe('\n'.join(output))
-class RadioSelect(Select):
- renderer = RadioFieldRenderer
+
+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(RadioSelect, self).__init__(*args, **kwargs)
+ super(RendererMixin, self).__init__(*args, **kwargs)
def subwidgets(self, name, value, attrs=None, choices=()):
for widget in self.get_renderer(name, value, attrs, choices):
@@ -670,56 +717,35 @@ class RadioSelect(Select):
def get_renderer(self, name, value, attrs=None, choices=()):
"""Returns an instance of the renderer."""
- if value is None: value = ''
- str_value = force_text(value) # Normalize to string.
+ if value is None:
+ value = self._empty_value
final_attrs = self.build_attrs(attrs)
choices = list(chain(self.choices, choices))
- return self.renderer(name, str_value, final_attrs, choices)
+ return self.renderer(name, value, final_attrs, choices)
def render(self, name, value, attrs=None, choices=()):
return self.get_renderer(name, value, attrs, choices).render()
def id_for_label(self, id_):
- # RadioSelect is represented by multiple fields,
- # each of which has a distinct ID. The IDs are made distinct by a "_X"
- # suffix, where X is the zero-based index of the radio field. Thus,
- # the label for a RadioSelect should reference the first one ('_0').
+ # Widgets using this RendererMixin are made of a collection of
+ # subwidgets, each with their own , and distinct ID.
+ # The IDs are made distinct by y "_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 CheckboxSelectMultiple(SelectMultiple):
- def render(self, name, value, attrs=None, choices=()):
- if value is None: value = []
- final_attrs = self.build_attrs(attrs, name=name)
- id_ = final_attrs.get('id', None)
- start_tag = format_html('', id_) if id_ else ''
- output = [start_tag]
- # Normalize to strings
- str_values = set([force_text(v) for v in value])
- for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
- # If an ID attribute was given, add a numeric index as a suffix,
- # so that the checkboxes don't all have the same ID attribute.
- if id_:
- final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
- label_for = format_html(' for="{0}_{1}"', id_, i)
- else:
- label_for = ''
- cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
- option_value = force_text(option_value)
- rendered_cb = cb.render(name, option_value)
- option_label = force_text(option_label)
- output.append(format_html('{1} {2} ',
- label_for, rendered_cb, option_label))
- output.append(' ')
- return mark_safe('\n'.join(output))
+class RadioSelect(RendererMixin, Select):
+ renderer = RadioFieldRenderer
+ _empty_value = ''
+
+
+class CheckboxSelectMultiple(RendererMixin, SelectMultiple):
+ renderer = CheckboxFieldRenderer
+ _empty_value = []
- def id_for_label(self, id_):
- # See the comment for RadioSelect.id_for_label()
- if id_:
- id_ += '_0'
- return id_
class MultiWidget(Widget):
"""
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index bf1a323489..1533e25dc8 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -372,6 +372,9 @@ these changes.
- ``django.db.transaction.is_managed()``
- ``django.db.transaction.managed()``
+* ``django.forms.widgets.RadioInput`` will be removed in favor of
+ ``django.forms.widgets.RadioChoiceInput``.
+
2.0
---
diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt
index 836389125a..514e8b3dc0 100644
--- a/docs/ref/forms/widgets.txt
+++ b/docs/ref/forms/widgets.txt
@@ -658,6 +658,9 @@ the widget.
The outer ```` container will now receive the ``id`` attribute defined on
the widget.
+Like :class:`RadioSelect`, you can now loop over the individual checkboxes making
+up the lists. See the documentation of :class:`RadioSelect` for more details.
+
.. _file-upload-widgets:
File upload widgets
diff --git a/tests/forms_tests/tests/test_widgets.py b/tests/forms_tests/tests/test_widgets.py
index 3c782713f3..4664553aa7 100644
--- a/tests/forms_tests/tests/test_widgets.py
+++ b/tests/forms_tests/tests/test_widgets.py
@@ -15,7 +15,7 @@ from django.utils import six
from django.utils.translation import activate, deactivate
from django.test import TestCase
from django.test.utils import override_settings
-from django.utils.encoding import python_2_unicode_compatible
+from django.utils.encoding import python_2_unicode_compatible, force_text
from ..models import Article
@@ -656,7 +656,7 @@ beatle J R Ringo False""")
George
Ringo """)
- # A RadioFieldRenderer object also allows index access to individual RadioInput
+ # A RadioFieldRenderer object also allows index access to individual RadioChoiceInput
w = RadioSelect()
r = w.get_renderer('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
self.assertHTMLEqual(str(r[1]), ' Paul ')
@@ -665,11 +665,8 @@ beatle J R Ringo False""")
self.assertFalse(r[1].is_checked())
self.assertEqual((r[1].name, r[1].value, r[1].choice_value, r[1].choice_label), ('beatle', 'J', 'P', 'Paul'))
- try:
+ with self.assertRaises(IndexError):
r[10]
- self.fail("This offset should not exist.")
- except IndexError:
- pass
# Choices are escaped correctly
w = RadioSelect()
@@ -817,6 +814,25 @@ beatle J R Ringo False""")
C
""")
+ w = CheckboxSelectMultiple()
+ r = w.get_renderer('abc', 'b', choices=[(c, c.upper()) for c in 'abc'])
+ # You can iterate over the CheckboxFieldRenderer to get individual elements
+ expected = [
+ ' A ',
+ ' B ',
+ ' C ',
+ ]
+ for output, expected in zip(r, expected):
+ self.assertHTMLEqual(force_text(output), expected)
+
+ # You can access individual elements
+ self.assertHTMLEqual(force_text(r[1]),
+ ' B ')
+
+ # Out-of-range errors are propagated
+ with self.assertRaises(IndexError):
+ r[42]
+
def test_multi(self):
class MyMultiWidget(MultiWidget):
def decompress(self, value):