Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #4592: Make CheckboxSelectMultiple more like RadioSelect. #1011

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
130 changes: 79 additions & 51 deletions django/forms/widgets.py
Expand Up @@ -11,6 +11,7 @@
from urllib.parse import urljoin from urllib.parse import urljoin
except ImportError: # Python 2 except ImportError: # Python 2
from urlparse import urljoin from urlparse import urljoin
import warnings


from django.conf import settings from django.conf import settings
from django.forms.util import flatatt, to_current_timezone from django.forms.util import flatatt, to_current_timezone
Expand Down Expand Up @@ -584,15 +585,18 @@ def value_from_datadict(self, data, files, name):
return data.get(name, None) return data.get(name, None)





@python_2_unicode_compatible @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='radio'>. <input type='$input_type'>.
""" """
input_type = None # Subclasses must define this


def __init__(self, name, value, attrs, choice, index): def __init__(self, name, value, attrs, choice, index):
self.name, self.value = name, value self.name = name
self.value = value
self.attrs = attrs self.attrs = attrs
self.choice_value = force_text(choice[0]) self.choice_value = force_text(choice[0])
self.choice_label = force_text(choice[1]) self.choice_label = force_text(choice[1])
Expand All @@ -618,34 +622,69 @@ def is_checked(self):
def tag(self): def tag(self):
if 'id' in self.attrs: if 'id' in self.attrs:
self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index) 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(): if self.is_checked():
final_attrs['checked'] = 'checked' final_attrs['checked'] = 'checked'
return format_html('<input{0} />', flatatt(final_attrs)) return format_html('<input{0} />', 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 @python_2_unicode_compatible
class RadioFieldRenderer(object): class ChoiceFieldRenderer(object):
""" """
An object used by RadioSelect to enable customization of radio widgets. An object used by RadioSelect to enable customization of radio widgets.
""" """


choice_input_class = None

def __init__(self, name, value, attrs, choices): 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 self.choices = choices


def __iter__(self): def __iter__(self):
for i, choice in enumerate(self.choices): 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): def __getitem__(self, idx):
choice = self.choices[idx] # Let the IndexError propogate 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): def __str__(self):
return self.render() return self.render()


def render(self): def render(self):
"""Outputs a <ul> for this set of radio fields.""" """
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', None) id_ = self.attrs.get('id', None)
start_tag = format_html('<ul id="{0}">', id_) if id_ else '<ul>' start_tag = format_html('<ul id="{0}">', id_) if id_ else '<ul>'
output = [start_tag] output = [start_tag]
Expand All @@ -654,72 +693,61 @@ def render(self):
output.append('</ul>') output.append('</ul>')
return mark_safe('\n'.join(output)) 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 # subclassed must define this
_empty_value = None


def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Override the default renderer if we were passed one. # Override the default renderer if we were passed one.
renderer = kwargs.pop('renderer', None) renderer = kwargs.pop('renderer', None)
if renderer: if renderer:
self.renderer = renderer self.renderer = renderer
super(RadioSelect, self).__init__(*args, **kwargs) super(RendererMixin, self).__init__(*args, **kwargs)


def subwidgets(self, name, value, attrs=None, choices=()): def subwidgets(self, name, value, attrs=None, choices=()):
for widget in self.get_renderer(name, value, attrs, choices): for widget in self.get_renderer(name, value, attrs, choices):
yield widget yield widget


def get_renderer(self, name, value, attrs=None, choices=()): def get_renderer(self, name, value, attrs=None, choices=()):
"""Returns an instance of the renderer.""" """Returns an instance of the renderer."""
if value is None: value = '' if value is None:
str_value = force_text(value) # Normalize to string. value = self._empty_value
final_attrs = self.build_attrs(attrs) final_attrs = self.build_attrs(attrs)
choices = list(chain(self.choices, choices)) 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=()): def render(self, name, value, attrs=None, choices=()):
return self.get_renderer(name, value, attrs, choices).render() return self.get_renderer(name, value, attrs, choices).render()


def id_for_label(self, id_): def id_for_label(self, id_):
# RadioSelect is represented by multiple <input type="radio"> fields, # Widgets using this RendererMixin are made of a collection of
# each of which has a distinct ID. The IDs are made distinct by a "_X" # subwidgets, each with their own <label>, and distinct ID.
# suffix, where X is the zero-based index of the radio field. Thus, # The IDs are made distinct by y "_X" suffix, where X is the zero-based
# the label for a RadioSelect should reference the first one ('_0'). # index of the choice field. Thus, the label for the main widget should
# reference the first subwidget, hence the "_0" suffix.
if id_: if id_:
id_ += '_0' id_ += '_0'
return id_ 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('<ul id="{0}">', id_) if id_ else '<ul>'
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('<li><label{0}>{1} {2}</label></li>',
label_for, rendered_cb, option_label))
output.append('</ul>')
return mark_safe('\n'.join(output))


def id_for_label(self, id_): class RadioSelect(RendererMixin, Select):
# See the comment for RadioSelect.id_for_label() renderer = RadioFieldRenderer
if id_: _empty_value = ''
id_ += '_0'
return id_
class CheckboxSelectMultiple(RendererMixin, SelectMultiple):
renderer = CheckboxFieldRenderer
_empty_value = []



class MultiWidget(Widget): class MultiWidget(Widget):
""" """
Expand Down
3 changes: 3 additions & 0 deletions docs/internals/deprecation.txt
Expand Up @@ -372,6 +372,9 @@ these changes.
- ``django.db.transaction.is_managed()`` - ``django.db.transaction.is_managed()``
- ``django.db.transaction.managed()`` - ``django.db.transaction.managed()``


* ``django.forms.widgets.RadioInput`` will be removed in favor of
``django.forms.widgets.RadioChoiceInput``.

2.0 2.0
--- ---


Expand Down
5 changes: 5 additions & 0 deletions docs/ref/forms/widgets.txt
Expand Up @@ -658,6 +658,11 @@ the widget.
The outer ``<ul>`` container will now receive the ``id`` attribute defined on The outer ``<ul>`` container will now receive the ``id`` attribute defined on
the widget. the widget.


.. versionadded:: 1.6

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:


File upload widgets File upload widgets
Expand Down
28 changes: 22 additions & 6 deletions tests/forms_tests/tests/test_widgets.py
Expand Up @@ -15,7 +15,7 @@
from django.utils.translation import activate, deactivate from django.utils.translation import activate, deactivate
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings 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 from ..models import Article


Expand Down Expand Up @@ -656,7 +656,7 @@ class CustomRadioSelect(RadioSelect):
<label><input checked="checked" type="radio" name="beatle" value="G" /> George</label><br /> <label><input checked="checked" type="radio" name="beatle" value="G" /> George</label><br />
<label><input type="radio" name="beatle" value="R" /> Ringo</label>""") <label><input type="radio" name="beatle" value="R" /> Ringo</label>""")


# A RadioFieldRenderer object also allows index access to individual RadioInput # A RadioFieldRenderer object also allows index access to individual RadioChoiceInput
w = RadioSelect() w = RadioSelect()
r = w.get_renderer('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))) r = w.get_renderer('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
self.assertHTMLEqual(str(r[1]), '<label><input type="radio" name="beatle" value="P" /> Paul</label>') self.assertHTMLEqual(str(r[1]), '<label><input type="radio" name="beatle" value="P" /> Paul</label>')
Expand All @@ -665,11 +665,8 @@ class CustomRadioSelect(RadioSelect):
self.assertFalse(r[1].is_checked()) 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')) 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] r[10]
self.fail("This offset should not exist.")
except IndexError:
pass


# Choices are escaped correctly # Choices are escaped correctly
w = RadioSelect() w = RadioSelect()
Expand Down Expand Up @@ -817,6 +814,25 @@ def get_choices():
<li><label for="abc_2"><input checked="checked" type="checkbox" name="letters" value="c" id="abc_2" /> C</label></li> <li><label for="abc_2"><input checked="checked" type="checkbox" name="letters" value="c" id="abc_2" /> C</label></li>
</ul>""") </ul>""")


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 = [
'<label><input type="checkbox" name="abc" value="a" /> A</label>',
'<label><input checked="checked" type="checkbox" name="abc" value="b" /> B</label>',
'<label><input type="checkbox" name="abc" value="c" /> C</label>',
]
for output, expected in zip(r, expected):
self.assertHTMLEqual(force_text(output), expected)

# You can access individual elements
self.assertHTMLEqual(force_text(r[1]), '<label><input checked="checked" type="checkbox" name="abc" value="b" /> B</label>')

# Out-of-range errors are propagated
with self.assertRaises(IndexError):
r[42]


def test_multi(self): def test_multi(self):
class MyMultiWidget(MultiWidget): class MyMultiWidget(MultiWidget):
def decompress(self, value): def decompress(self, value):
Expand Down