diff --git a/AUTHORS b/AUTHORS
index ff17d1a418..82938eea88 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -397,6 +397,7 @@ answer newbie questions, and generally made Django that much better:
Jorge Bastida
Jorge Gajon
José Tomás Tocino García
+ Josef Rousek
Joseph Kocherhans
Josh Smeaton
Joshua Ginsberg
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index dd68662d43..4f028d34ad 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -680,6 +680,28 @@ class Select(ChoiceWidget):
context['widget']['attrs']['multiple'] = 'multiple'
return context
+ @staticmethod
+ def _choice_has_empty_value(choice):
+ """Return True if the choice's value is empty string or None."""
+ value, _ = choice
+ return (
+ (isinstance(value, six.string_types) and not bool(value)) or
+ value is None
+ )
+
+ def use_required_attribute(self, initial):
+ """
+ Don't render 'required' if the first has a value, as that's
+ invalid HTML.
+ """
+ use_required_attribute = super(Select, self).use_required_attribute(initial)
+ # 'required' is always okay for .
+ if self.allow_multiple_selected:
+ return use_required_attribute
+
+ first_choice = next(iter(self.choices), None)
+ return use_required_attribute and first_choice is not None and self._choice_has_empty_value(first_choice)
+
class NullBooleanSelect(Select):
"""
diff --git a/tests/forms_tests/field_tests/test_choicefield.py b/tests/forms_tests/field_tests/test_choicefield.py
index 4c70177dac..1d8fe5a3cf 100644
--- a/tests/forms_tests/field_tests/test_choicefield.py
+++ b/tests/forms_tests/field_tests/test_choicefield.py
@@ -82,7 +82,7 @@ class ChoiceFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')], disabled=True)
self.assertWidgetRendersTo(
f,
- 'John '
+ 'John '
'Paul '
)
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index 4079be1235..2b93575bf2 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -502,12 +502,12 @@ class FormsTestCase(SimpleTestCase):
language = ChoiceField(choices=[('P', 'Python'), ('J', 'Java')])
f = FrameworkForm(auto_id=False)
- self.assertHTMLEqual(str(f['language']), """
+ self.assertHTMLEqual(str(f['language']), """
Python
Java
""")
f = FrameworkForm({'name': 'Django', 'language': 'P'}, auto_id=False)
- self.assertHTMLEqual(str(f['language']), """
+ self.assertHTMLEqual(str(f['language']), """
Python
Java
""")
@@ -531,12 +531,12 @@ class FormsTestCase(SimpleTestCase):
language = ChoiceField(choices=[('P', 'Python'), ('J', 'Java')], widget=Select(attrs={'class': 'foo'}))
f = FrameworkForm(auto_id=False)
- self.assertHTMLEqual(str(f['language']), """
+ self.assertHTMLEqual(str(f['language']), """
Python
Java
""")
f = FrameworkForm({'name': 'Django', 'language': 'P'}, auto_id=False)
- self.assertHTMLEqual(str(f['language']), """
+ self.assertHTMLEqual(str(f['language']), """
Python
Java
""")
@@ -552,12 +552,12 @@ class FormsTestCase(SimpleTestCase):
)
f = FrameworkForm(auto_id=False)
- self.assertHTMLEqual(str(f['language']), """
+ self.assertHTMLEqual(str(f['language']), """
Python
Java
""")
f = FrameworkForm({'name': 'Django', 'language': 'P'}, auto_id=False)
- self.assertHTMLEqual(str(f['language']), """
+ self.assertHTMLEqual(str(f['language']), """
Python
Java
""")
@@ -568,10 +568,10 @@ class FormsTestCase(SimpleTestCase):
language = ChoiceField()
f = FrameworkForm(auto_id=False)
- self.assertHTMLEqual(str(f['language']), """
+ self.assertHTMLEqual(str(f['language']), """
""")
f.fields['language'].choices = [('P', 'Python'), ('J', 'Java')]
- self.assertHTMLEqual(str(f['language']), """
+ self.assertHTMLEqual(str(f['language']), """
Python
Java
""")
@@ -2363,37 +2363,37 @@ Password:
is_cool = NullBooleanField()
p = Person({'name': 'Joe'}, auto_id=False)
- self.assertHTMLEqual(str(p['is_cool']), """
+ self.assertHTMLEqual(str(p['is_cool']), """
Unknown
Yes
No
""")
p = Person({'name': 'Joe', 'is_cool': '1'}, auto_id=False)
- self.assertHTMLEqual(str(p['is_cool']), """
+ self.assertHTMLEqual(str(p['is_cool']), """
Unknown
Yes
No
""")
p = Person({'name': 'Joe', 'is_cool': '2'}, auto_id=False)
- self.assertHTMLEqual(str(p['is_cool']), """
+ self.assertHTMLEqual(str(p['is_cool']), """
Unknown
Yes
No
""")
p = Person({'name': 'Joe', 'is_cool': '3'}, auto_id=False)
- self.assertHTMLEqual(str(p['is_cool']), """
+ self.assertHTMLEqual(str(p['is_cool']), """
Unknown
Yes
No
""")
p = Person({'name': 'Joe', 'is_cool': True}, auto_id=False)
- self.assertHTMLEqual(str(p['is_cool']), """
+ self.assertHTMLEqual(str(p['is_cool']), """
Unknown
Yes
No
""")
p = Person({'name': 'Joe', 'is_cool': False}, auto_id=False)
- self.assertHTMLEqual(str(p['is_cool']), """
+ self.assertHTMLEqual(str(p['is_cool']), """
Unknown
Yes
No
@@ -2759,7 +2759,7 @@ Good luck picking a username that doesn't already exist.
"""
Name:
Is cool:
-
+
Unknown
Yes
No
@@ -2775,7 +2775,7 @@ Good luck picking a username that doesn't already exist.
Name:
Is cool:
-
+
Unknown
Yes
No
@@ -2793,7 +2793,7 @@ Good luck picking a username that doesn't already exist.
Is cool:
-
+
Unknown
Yes
No
@@ -3516,7 +3516,7 @@ Good luck picking a username that doesn't already exist.
'F2:
'
'F3:
'
- 'F4: '
+ 'F4: '
'Python '
'Java '
'
',
@@ -3528,7 +3528,7 @@ Good luck picking a username that doesn't already exist.
'F2: '
'F3: '
- 'F4: '
+ 'F4: '
'Python '
'Java '
' ',
@@ -3542,7 +3542,7 @@ Good luck picking a username that doesn't already exist.
'F3: '
' '
- 'F4: '
+ 'F4: '
'Python '
'Java '
' ',
diff --git a/tests/forms_tests/tests/tests.py b/tests/forms_tests/tests/tests.py
index c19d1a66cd..f0007fbe8f 100644
--- a/tests/forms_tests/tests/tests.py
+++ b/tests/forms_tests/tests/tests.py
@@ -109,12 +109,12 @@ class ModelFormCallableModelDefault(TestCase):
ChoiceOptionModel.objects.create(id=3, name='option 3')
self.assertHTMLEqual(
ChoiceFieldForm().as_p(),
- """Choice:
+ """Choice:
ChoiceOption 1
ChoiceOption 2
ChoiceOption 3
-Choice int:
+Choice int:
ChoiceOption 1
ChoiceOption 2
ChoiceOption 3
@@ -145,12 +145,12 @@ class ModelFormCallableModelDefault(TestCase):
'multi_choice': [obj2, obj3],
'multi_choice_int': ChoiceOptionModel.objects.exclude(name="default"),
}).as_p(),
- """Choice:
+ """Choice:
ChoiceOption 1
ChoiceOption 2
ChoiceOption 3
-Choice int:
+Choice int:
ChoiceOption 1
ChoiceOption 2
ChoiceOption 3
diff --git a/tests/forms_tests/widget_tests/test_select.py b/tests/forms_tests/widget_tests/test_select.py
index 4b717b6043..d2660ea787 100644
--- a/tests/forms_tests/widget_tests/test_select.py
+++ b/tests/forms_tests/widget_tests/test_select.py
@@ -294,3 +294,23 @@ class SelectTest(WidgetTest):
self.assertIsNot(widget.choices, obj.choices)
self.assertEqual(widget.attrs, obj.attrs)
self.assertIsNot(widget.attrs, obj.attrs)
+
+ def test_doesnt_render_required_when_impossible_to_select_empty_field(self):
+ widget = self.widget(choices=[('J', 'John'), ('P', 'Paul')])
+ self.assertIs(widget.use_required_attribute(initial=None), False)
+
+ def test_renders_required_when_possible_to_select_empty_field_str(self):
+ widget = self.widget(choices=[('', 'select please'), ('P', 'Paul')])
+ self.assertIs(widget.use_required_attribute(initial=None), True)
+
+ def test_renders_required_when_possible_to_select_empty_field_list(self):
+ widget = self.widget(choices=[['', 'select please'], ['P', 'Paul']])
+ self.assertIs(widget.use_required_attribute(initial=None), True)
+
+ def test_renders_required_when_possible_to_select_empty_field_none(self):
+ widget = self.widget(choices=[(None, 'select please'), ('P', 'Paul')])
+ self.assertIs(widget.use_required_attribute(initial=None), True)
+
+ def test_doesnt_render_required_when_no_choices_are_available(self):
+ widget = self.widget(choices=[])
+ self.assertIs(widget.use_required_attribute(initial=None), False)
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index f03b4433a8..1686629c3a 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -2635,11 +2635,11 @@ class OtherModelFormTests(TestCase):
Date published:
- Mode:
+ Mode:
direct
delayed
- Category:
+ Category:
Games
Comics
Novel