diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py
index 4867e72deb..df3de5848e 100644
--- a/django/forms/boundfield.py
+++ b/django/forms/boundfield.py
@@ -51,6 +51,7 @@ 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)
for subwidget in self.field.widget.subwidgets(self.html_name, self.value(), attrs):
yield subwidget
@@ -85,10 +86,7 @@ class BoundField(object):
widget.is_localized = True
attrs = attrs or {}
- if not widget.is_hidden and self.field.required and self.form.use_required_attribute:
- attrs['required'] = True
- if self.field.disabled:
- attrs['disabled'] = True
+ attrs = self.build_widget_attrs(attrs, widget)
auto_id = self.auto_id
if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
if not only_initial:
@@ -227,3 +225,13 @@ class BoundField(object):
widget = self.field.widget
id_ = widget.attrs.get('id') or self.auto_id
return widget.id_for_label(id_)
+
+ def build_widget_attrs(self, attrs, widget=None):
+ if not widget:
+ widget = self.field.widget
+ attrs = dict(attrs) # Copy attrs to avoid modifying the argument.
+ if not widget.is_hidden and self.field.required and self.form.use_required_attribute:
+ attrs['required'] = True
+ if self.field.disabled:
+ attrs['disabled'] = True
+ return attrs
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 604a6a401a..217f37b022 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -787,6 +787,13 @@ class CheckboxSelectMultiple(RendererMixin, SelectMultiple):
renderer = CheckboxFieldRenderer
_empty_value = []
+ def build_attrs(self, extra_attrs=None, **kwargs):
+ attrs = super(CheckboxSelectMultiple, self).build_attrs(extra_attrs, **kwargs)
+ # Remove the 'required' attribute because browser validation would
+ # require all checkboxes to be checked instead of at least one.
+ attrs.pop('required', None)
+ return attrs
+
class MultiWidget(Widget):
"""
diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt
index 6a8cdf3c70..aae92a37c9 100644
--- a/docs/ref/forms/widgets.txt
+++ b/docs/ref/forms/widgets.txt
@@ -714,8 +714,10 @@ Selector and checkbox widgets
The outer ``
`` container receives the ``id`` attribute of the widget,
if defined, or :attr:`BoundField.auto_id` otherwise.
-Like :class:`RadioSelect`, you can now loop over the individual checkboxes making
-up the lists. See the documentation of :class:`RadioSelect` for more details.
+Like :class:`RadioSelect`, you can loop over the individual checkboxes for the
+widget's choices. Unlike :class:`RadioSelect`, the checkboxes won't include the
+``required`` HTML attribute if the field is required because browser validation
+would require all checkboxes to be checked instead of at least one.
When looping over the checkboxes, the ``label`` and ``input`` tags include
``for`` and ``id`` attributes, respectively. Each checkbox has an
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index 2bd49b7361..a9c3c74b69 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -646,9 +646,9 @@ Java
self.assertHTMLEqual(
t.render(Context({'form': f})),
"""
+ Python
"""
+ Java"""
)
def test_form_with_iterable_boundfield(self):
@@ -661,17 +661,17 @@ Java
f = BeatleForm(auto_id=False)
self.assertHTMLEqual(
'\n'.join(str(bf) for bf in f['name']),
- """
-
-
-"""
+ """
+
+
+"""
)
self.assertHTMLEqual(
'\n'.join('
%s
' % bf for bf in f['name']),
- """
-
-
-"""
+ """
+
+
+"""
)
def test_form_with_noniterable_boundfield(self):
@@ -680,7 +680,7 @@ Java
name = CharField()
f = BeatleForm(auto_id=False)
- self.assertHTMLEqual('\n'.join(str(bf) for bf in f['name']), '')
+ self.assertHTMLEqual('\n'.join(str(bf) for bf in f['name']), '')
def test_boundfield_slice(self):
class BeatleForm(Form):
@@ -803,18 +803,18 @@ Java
f = SongForm(auto_id=False)
self.assertHTMLEqual(str(f['composers']), """
-
-
+
+
""")
f = SongForm({'composers': ['J']}, auto_id=False)
self.assertHTMLEqual(str(f['composers']), """
-
-
+
+
""")
f = SongForm({'composers': ['J', 'P']}, auto_id=False)
self.assertHTMLEqual(str(f['composers']), """
-
-
+
+
""")
# Test iterating on individual checkboxes in a template
t = Template('{% for checkbox in form.composers %}