1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +00:00

Fixed #35870 -- Replaced blank select choice with accessible text.

Thanks James Scholes for providing feedback on accessible options, Thibaud Colas for running screen reader tests, Carlton Gibson for helping get started.
This commit is contained in:
Marijke Luttekes 2024-12-17 00:22:56 +01:00
parent 3ee4c6a27a
commit b3505f38f6
No known key found for this signature in database
GPG Key ID: B8079A753CA344D1
21 changed files with 120 additions and 64 deletions

View File

@ -223,6 +223,9 @@ FORM_RENDERER = "django.forms.renderers.DjangoTemplates"
# Set to True to assume "https" during the Django 5.x release cycle. # Set to True to assume "https" during the Django 5.x release cycle.
FORMS_URLFIELD_ASSUME_HTTPS = False FORMS_URLFIELD_ASSUME_HTTPS = False
# ToDo: Documentation string
FORMS_DEFAULT_BLANK_CHOICE_LABEL = gettext_noop("- Select an option -")
# Default email address to use for various automated correspondence from # Default email address to use for various automated correspondence from
# the site managers. # the site managers.
DEFAULT_FROM_EMAIL = "webmaster@localhost" DEFAULT_FROM_EMAIL = "webmaster@localhost"

View File

@ -1087,11 +1087,13 @@ class ModelAdmin(BaseModelAdmin):
actions = self._filter_actions_by_permissions(request, self._get_base_actions()) actions = self._filter_actions_by_permissions(request, self._get_base_actions())
return {name: (func, name, desc) for func, name, desc in actions} return {name: (func, name, desc) for func, name, desc in actions}
def get_action_choices(self, request, default_choices=models.BLANK_CHOICE_DASH): def get_action_choices(self, request, default_choices=None):
""" """
Return a list of choices for use in a form object. Each choice is a Return a list of choices for use in a form object. Each choice is a
tuple (name, description). tuple (name, description).
""" """
if default_choices is None:
default_choices = [("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL)]
choices = [] + default_choices choices = [] + default_choices
for func, name, description in self.get_actions(request).values(): for func, name, description in self.get_actions(request).values():
choice = (name, description % model_format_dict(self.opts)) choice = (name, description % model_format_dict(self.opts))

View File

@ -82,6 +82,7 @@ class NOT_PROVIDED:
# The values to use for "blank" in SelectFields. Will be appended to the start # The values to use for "blank" in SelectFields. Will be appended to the start
# of most "choices" lists. # of most "choices" lists.
# ToDo: write deprecation notice per Trac issue #35870.
BLANK_CHOICE_DASH = [("", "---------")] BLANK_CHOICE_DASH = [("", "---------")]
@ -1056,7 +1057,7 @@ class Field(RegisterLookupMixin):
def get_choices( def get_choices(
self, self,
include_blank=True, include_blank=True,
blank_choice=BLANK_CHOICE_DASH, blank_choice=None,
limit_choices_to=None, limit_choices_to=None,
ordering=(), ordering=(),
): ):
@ -1064,6 +1065,8 @@ class Field(RegisterLookupMixin):
Return choices with a default blank choices included, for use Return choices with a default blank choices included, for use
as <select> choices for this field. as <select> choices for this field.
""" """
if blank_choice is None:
blank_choice = [("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL)]
if self.choices is not None: if self.choices is not None:
if include_blank: if include_blank:
return BlankChoiceIterator(self.choices, blank_choice) return BlankChoiceIterator(self.choices, blank_choice)

View File

@ -11,12 +11,12 @@ they're the closest concept currently available.
import warnings import warnings
from django.conf import settings
from django.core import exceptions from django.core import exceptions
from django.utils.deprecation import RemovedInDjango60Warning from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.hashable import make_hashable from django.utils.hashable import make_hashable
from . import BLANK_CHOICE_DASH
from .mixins import FieldCacheMixin from .mixins import FieldCacheMixin
@ -175,7 +175,7 @@ class ForeignObjectRel(FieldCacheMixin):
def get_choices( def get_choices(
self, self,
include_blank=True, include_blank=True,
blank_choice=BLANK_CHOICE_DASH, blank_choice=None,
limit_choices_to=None, limit_choices_to=None,
ordering=(), ordering=(),
): ):
@ -186,6 +186,8 @@ class ForeignObjectRel(FieldCacheMixin):
Analog of django.db.models.fields.Field.get_choices(), provided Analog of django.db.models.fields.Field.get_choices(), provided
initially for utilization by RelatedFieldListFilter. initially for utilization by RelatedFieldListFilter.
""" """
if blank_choice is None:
blank_choice = [("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL)]
limit_choices_to = limit_choices_to or self.limit_choices_to limit_choices_to = limit_choices_to or self.limit_choices_to
qs = self.related_model._default_manager.complex_filter(limit_choices_to) qs = self.related_model._default_manager.complex_filter(limit_choices_to)
if ordering: if ordering:

View File

@ -1217,7 +1217,7 @@ class FilePathField(ChoiceField):
if self.required: if self.required:
self.choices = [] self.choices = []
else: else:
self.choices = [("", "---------")] self.choices = [("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL)]
if self.match is not None: if self.match is not None:
self.match_re = re.compile(self.match) self.match_re = re.compile(self.match)

View File

@ -5,6 +5,7 @@ and database field objects.
from itertools import chain from itertools import chain
from django.conf import settings
from django.core.exceptions import ( from django.core.exceptions import (
NON_FIELD_ERRORS, NON_FIELD_ERRORS,
FieldError, FieldError,
@ -1456,7 +1457,7 @@ class ModelChoiceField(ChoiceField):
self, self,
queryset, queryset,
*, *,
empty_label="---------", empty_label="",
required=True, required=True,
widget=None, widget=None,
label=None, label=None,
@ -1483,6 +1484,8 @@ class ModelChoiceField(ChoiceField):
): ):
self.empty_label = None self.empty_label = None
else: else:
if empty_label == "":
empty_label = settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL
self.empty_label = empty_label self.empty_label = empty_label
self.queryset = queryset self.queryset = queryset
self.limit_choices_to = limit_choices_to # limit the queryset later. self.limit_choices_to = limit_choices_to # limit the queryset later.

View File

@ -1376,7 +1376,7 @@ generating choices. See :ref:`iterating-relationship-choices` for details.
By default the ``<select>`` widget used by ``ModelChoiceField`` will have an By default the ``<select>`` widget used by ``ModelChoiceField`` will have an
empty choice at the top of the list. You can change the text of this empty choice at the top of the list. You can change the text of this
label (which is ``"---------"`` by default) with the ``empty_label`` label (which is ``"- Select an option -"`` by default) with the ``empty_label``
attribute, or you can disable the empty label entirely by setting attribute, or you can disable the empty label entirely by setting
``empty_label`` to ``None``:: ``empty_label`` to ``None``::

View File

@ -234,7 +234,7 @@ documentation.
.. _field-choices-blank-label: .. _field-choices-blank-label:
Unless :attr:`blank=False<Field.blank>` is set on the field along with a Unless :attr:`blank=False<Field.blank>` is set on the field along with a
:attr:`~Field.default` then a label containing ``"---------"`` will be rendered :attr:`~Field.default` then a label containing ``"- Select an option -"`` will be rendered
with the select box. To override this behavior, add a tuple to ``choices`` with the select box. To override this behavior, add a tuple to ``choices``
containing ``None``; e.g. ``(None, 'Your String For Display')``. containing ``None``; e.g. ``(None, 'Your String For Display')``.
Alternatively, you can use an empty string instead of ``None`` where this makes Alternatively, you can use an empty string instead of ``None`` where this makes

View File

@ -1697,6 +1697,16 @@ Set this transitional setting to ``True`` to opt into using ``"https"`` as the
new default value of :attr:`URLField.assume_scheme new default value of :attr:`URLField.assume_scheme
<django.forms.URLField.assume_scheme>` during the Django 5.x release cycle. <django.forms.URLField.assume_scheme>` during the Django 5.x release cycle.
.. setting:: FORMS_DEFAULT_BLANK_CHOICE_LABEL
``FORMS_DEFAULT_BLANK_CHOICE_LABEL``
------------------------------------
Default: ``'- Select an option -'`` (Translated)
The default label for the blank choice option used by ``<select>`` elements in forms.
This value can be a regular or a translatable string, and can be overriden individually per form.
.. setting:: FORMAT_MODULE_PATH .. setting:: FORMAT_MODULE_PATH
``FORMAT_MODULE_PATH`` ``FORMAT_MODULE_PATH``

View File

@ -294,7 +294,7 @@ class AdminActionsTest(TestCase):
self.assertContains( self.assertContains(
response, response,
"""<label>Action: <select name="action" required> """<label>Action: <select name="action" required>
<option value="" selected>---------</option> <option value="" selected>- Select an option -</option>
<option value="delete_selected">Delete selected external <option value="delete_selected">Delete selected external
subscribers</option> subscribers</option>
<option value="redirect_to">Redirect to (Awesome action)</option> <option value="redirect_to">Redirect to (Awesome action)</option>

View File

@ -110,7 +110,7 @@ class SeleniumTests(AdminSeleniumTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
fk_dropdown.get_attribute("innerHTML"), fk_dropdown.get_attribute("innerHTML"),
f""" f"""
<option value="" selected="">---------</option> <option value="" selected="">- Select an option -</option>
<option value="{id_value}" selected>{interesting_name}</option> <option value="{id_value}" selected>{interesting_name}</option>
""", """,
) )
@ -155,7 +155,7 @@ class SeleniumTests(AdminSeleniumTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
fk_dropdown.get_attribute("innerHTML"), fk_dropdown.get_attribute("innerHTML"),
f""" f"""
<option value="" selected>---------</option> <option value="" selected>- Select an option -</option>
<option value="{id_value}">{name}</option> <option value="{id_value}">{name}</option>
""", """,
) )

View File

@ -7,6 +7,7 @@ from unittest import mock
from urllib.parse import parse_qsl, urljoin, urlsplit from urllib.parse import parse_qsl, urljoin, urlsplit
from django import forms from django import forms
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.admin import AdminSite, ModelAdmin from django.contrib.admin import AdminSite, ModelAdmin
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
@ -6450,7 +6451,9 @@ class SeleniumTests(AdminSeleniumTestCase):
self.selenium.switch_to.window(self.selenium.window_handles[0]) self.selenium.switch_to.window(self.selenium.window_handles[0])
select = Select(self.selenium.find_element(By.ID, "id_parent")) select = Select(self.selenium.find_element(By.ID, "id_parent"))
self.assertEqual(ParentWithUUIDPK.objects.count(), 0) self.assertEqual(ParentWithUUIDPK.objects.count(), 0)
self.assertEqual(select.first_selected_option.text, "---------") self.assertEqual(
select.first_selected_option.text, settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL
)
self.assertEqual(select.first_selected_option.get_attribute("value"), "") self.assertEqual(select.first_selected_option.get_attribute("value"), "")
def test_inline_with_popup_cancel_delete(self): def test_inline_with_popup_cancel_delete(self):
@ -6696,7 +6699,7 @@ class SeleniumTests(AdminSeleniumTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
_get_HTML_inside_element_by_id(born_country_select_id), _get_HTML_inside_element_by_id(born_country_select_id),
""" """
<option value="" selected="">---------</option> <option value="" selected="">- Select an option -</option>
<option value="1" selected="">Argentina</option> <option value="1" selected="">Argentina</option>
""", """,
) )
@ -6715,7 +6718,7 @@ class SeleniumTests(AdminSeleniumTestCase):
# limit_choices_to. # limit_choices_to.
self.assertHTMLEqual( self.assertHTMLEqual(
_get_HTML_inside_element_by_id(favorite_country_to_vacation_select_id), _get_HTML_inside_element_by_id(favorite_country_to_vacation_select_id),
'<option value="" selected="">---------</option>', '<option value="" selected="">- Select an option -</option>',
) )
# Add new Country from the living_country select. # Add new Country from the living_country select.
@ -6734,7 +6737,7 @@ class SeleniumTests(AdminSeleniumTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
_get_HTML_inside_element_by_id(born_country_select_id), _get_HTML_inside_element_by_id(born_country_select_id),
""" """
<option value="" selected="">---------</option> <option value="" selected="">- Select an option -</option>
<option value="1" selected="">Argentina</option> <option value="1" selected="">Argentina</option>
<option value="2">Spain</option> <option value="2">Spain</option>
""", """,
@ -6756,7 +6759,7 @@ class SeleniumTests(AdminSeleniumTestCase):
# limit_choices_to. # limit_choices_to.
self.assertHTMLEqual( self.assertHTMLEqual(
_get_HTML_inside_element_by_id(favorite_country_to_vacation_select_id), _get_HTML_inside_element_by_id(favorite_country_to_vacation_select_id),
'<option value="" selected="">---------</option>', '<option value="" selected="">- Select an option -</option>',
) )
# Edit second Country created from living_country select. # Edit second Country created from living_country select.
@ -6776,7 +6779,7 @@ class SeleniumTests(AdminSeleniumTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
_get_HTML_inside_element_by_id(born_country_select_id), _get_HTML_inside_element_by_id(born_country_select_id),
""" """
<option value="" selected="">---------</option> <option value="" selected="">- Select an option -</option>
<option value="1" selected="">Argentina</option> <option value="1" selected="">Argentina</option>
<option value="2">Italy</option> <option value="2">Italy</option>
""", """,
@ -6796,7 +6799,7 @@ class SeleniumTests(AdminSeleniumTestCase):
# favorite_country_to_vacation field has no options. # favorite_country_to_vacation field has no options.
self.assertHTMLEqual( self.assertHTMLEqual(
_get_HTML_inside_element_by_id(favorite_country_to_vacation_select_id), _get_HTML_inside_element_by_id(favorite_country_to_vacation_select_id),
'<option value="" selected="">---------</option>', '<option value="" selected="">- Select an option -</option>',
) )
# Add a new Asian country. # Add a new Asian country.

View File

@ -1811,7 +1811,7 @@ class RelatedFieldWidgetSeleniumTests(AdminWidgetSeleniumTestCase):
# Chrome and Safari don't update related object links when selecting # Chrome and Safari don't update related object links when selecting
# the same option as previously submitted. As a consequence, the # the same option as previously submitted. As a consequence, the
# "pencil" and "eye" buttons remain disable, so select "---------" # "pencil" and "eye" buttons remain disable, so select "- Select an option -"
# first. # first.
select = Select(self.selenium.find_element(By.ID, "id_user")) select = Select(self.selenium.find_element(By.ID, "id_user"))
select.select_by_index(0) select.select_by_index(0)

View File

@ -1,5 +1,6 @@
import decimal import decimal
from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms import TypedChoiceField from django.forms import TypedChoiceField
from django.test import SimpleTestCase from django.test import SimpleTestCase
@ -59,7 +60,11 @@ class TypedChoiceFieldTest(SimpleTestCase):
self.assertFalse(f.has_changed("1", "1")) self.assertFalse(f.has_changed("1", "1"))
f = TypedChoiceField( f = TypedChoiceField(
choices=[("", "---------"), ("a", "a"), ("b", "b")], choices=[
("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
("a", "a"),
("b", "b"),
],
coerce=str, coerce=str,
required=False, required=False,
initial=None, initial=None,

View File

@ -1,12 +1,13 @@
import datetime import datetime
from django.conf import settings
from django.forms import ChoiceField, Form, MultiWidget, RadioSelect, TextInput from django.forms import ChoiceField, Form, MultiWidget, RadioSelect, TextInput
from django.test import override_settings from django.test import override_settings
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from .test_choicewidget import ChoiceWidgetTest from .test_choicewidget import ChoiceWidgetTest
BLANK_CHOICE_DASH = (("", "------"),) BLANK_CHOICE = (("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),)
class RadioSelectTest(ChoiceWidgetTest): class RadioSelectTest(ChoiceWidgetTest):
@ -16,7 +17,9 @@ class RadioSelectTest(ChoiceWidgetTest):
html = """ html = """
<div> <div>
<div> <div>
<label><input type="radio" name="beatle" value="">------</label> <label>
<input type="radio" name="beatle" value="">- Select an option -
</label>
</div> </div>
<div> <div>
<label><input checked type="radio" name="beatle" value="J">John</label> <label><input checked type="radio" name="beatle" value="J">John</label>
@ -32,7 +35,7 @@ class RadioSelectTest(ChoiceWidgetTest):
</div> </div>
</div> </div>
""" """
beatles_with_blank = BLANK_CHOICE_DASH + self.beatles beatles_with_blank = BLANK_CHOICE + self.beatles
for choices in (beatles_with_blank, dict(beatles_with_blank)): for choices in (beatles_with_blank, dict(beatles_with_blank)):
with self.subTest(choices): with self.subTest(choices):
self.check_html(self.widget(choices=choices), "beatle", "J", html=html) self.check_html(self.widget(choices=choices), "beatle", "J", html=html)
@ -83,11 +86,13 @@ class RadioSelectTest(ChoiceWidgetTest):
""" """
If value is None, none of the options are selected. If value is None, none of the options are selected.
""" """
choices = BLANK_CHOICE_DASH + self.beatles choices = BLANK_CHOICE + self.beatles
html = """ html = """
<div> <div>
<div> <div>
<label><input checked type="radio" name="beatle" value="">------</label> <label>
<input checked type="radio" name="beatle" value="">- Select an option -
</label>
</div> </div>
<div> <div>
<label><input type="radio" name="beatle" value="J">John</label> <label><input type="radio" name="beatle" value="J">John</label>
@ -463,11 +468,11 @@ class RadioSelectTest(ChoiceWidgetTest):
def test_render_as_subwidget(self): def test_render_as_subwidget(self):
"""A RadioSelect as a subwidget of MultiWidget.""" """A RadioSelect as a subwidget of MultiWidget."""
choices = BLANK_CHOICE_DASH + self.beatles choices = BLANK_CHOICE + self.beatles
html = """ html = """
<div> <div>
<div><label> <div><label>
<input type="radio" name="beatle_0" value="">------</label> <input type="radio" name="beatle_0" value="">- Select an option -</label>
</div> </div>
<div><label> <div><label>
<input checked type="radio" name="beatle_0" value="J">John</label> <input checked type="radio" name="beatle_0" value="J">John</label>

View File

@ -1,4 +1,5 @@
from django import forms from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError, models, transaction from django.db import IntegrityError, models, transaction
from django.test import SimpleTestCase, TestCase from django.test import SimpleTestCase, TestCase
@ -48,7 +49,10 @@ class BooleanFieldTests(TestCase):
""" """
choices = [(1, "Si"), (2, "No")] choices = [(1, "Si"), (2, "No")]
f = models.BooleanField(choices=choices) f = models.BooleanField(choices=choices)
self.assertEqual(f.formfield().choices, [("", "---------")] + choices) self.assertEqual(
f.formfield().choices,
[("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL)] + choices,
)
def test_nullbooleanfield_formfield(self): def test_nullbooleanfield_formfield(self):
f = models.BooleanField(null=True) f = models.BooleanField(null=True)

View File

@ -1,6 +1,7 @@
import pickle import pickle
from django import forms from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.test import SimpleTestCase, TestCase from django.test import SimpleTestCase, TestCase
@ -354,7 +355,10 @@ class GetChoicesTests(SimpleTestCase):
def test_lazy_strings_not_evaluated(self): def test_lazy_strings_not_evaluated(self):
lazy_func = lazy(lambda x: 0 / 0, int) # raises ZeroDivisionError if evaluated. lazy_func = lazy(lambda x: 0 / 0, int) # raises ZeroDivisionError if evaluated.
f = models.CharField(choices=[(lazy_func("group"), [("a", "A"), ("b", "B")])]) f = models.CharField(choices=[(lazy_func("group"), [("a", "A"), ("b", "B")])])
self.assertEqual(f.get_choices(include_blank=True)[0], ("", "---------")) self.assertEqual(
f.get_choices(include_blank=True)[0],
("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
)
class GetChoicesOrderingTests(TestCase): class GetChoicesOrderingTests(TestCase):

View File

@ -1,6 +1,7 @@
import datetime import datetime
from django import forms from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms.models import ModelChoiceIterator, ModelChoiceIteratorValue from django.forms.models import ModelChoiceIterator, ModelChoiceIteratorValue
from django.forms.widgets import CheckboxSelectMultiple from django.forms.widgets import CheckboxSelectMultiple
@ -24,7 +25,7 @@ class ModelChoiceFieldTests(TestCase):
self.assertEqual( self.assertEqual(
list(f.choices), list(f.choices),
[ [
("", "---------"), ("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
(self.c1.pk, "Entertainment"), (self.c1.pk, "Entertainment"),
(self.c2.pk, "A test"), (self.c2.pk, "A test"),
(self.c3.pk, "Third"), (self.c3.pk, "Third"),
@ -102,7 +103,7 @@ class ModelChoiceFieldTests(TestCase):
self.assertEqual( self.assertEqual(
list(f.choices), list(f.choices),
[ [
("", "---------"), ("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
(self.c1.pk, "Entertainment"), (self.c1.pk, "Entertainment"),
(self.c2.pk, "A test"), (self.c2.pk, "A test"),
], ],
@ -118,7 +119,7 @@ class ModelChoiceFieldTests(TestCase):
self.assertEqual( self.assertEqual(
list(gen_two), list(gen_two),
[ [
("", "---------"), ("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
(self.c1.pk, "Entertainment"), (self.c1.pk, "Entertainment"),
(self.c2.pk, "A test"), (self.c2.pk, "A test"),
], ],
@ -130,7 +131,7 @@ class ModelChoiceFieldTests(TestCase):
self.assertEqual( self.assertEqual(
list(f.choices), list(f.choices),
[ [
("", "---------"), ("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
(self.c1.pk, "category Entertainment"), (self.c1.pk, "category Entertainment"),
(self.c2.pk, "category A test"), (self.c2.pk, "category A test"),
(self.c3.pk, "category Third"), (self.c3.pk, "category Third"),
@ -143,7 +144,7 @@ class ModelChoiceFieldTests(TestCase):
self.assertEqual( self.assertEqual(
list(f.choices), list(f.choices),
[ [
("", "---------"), ("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
(self.c1.pk, "Entertainment"), (self.c1.pk, "Entertainment"),
(self.c2.pk, "A test"), (self.c2.pk, "A test"),
(self.c3.pk, "Third"), (self.c3.pk, "Third"),
@ -154,7 +155,7 @@ class ModelChoiceFieldTests(TestCase):
self.assertEqual( self.assertEqual(
list(f.choices), list(f.choices),
[ [
("", "---------"), ("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
(self.c1.pk, "Entertainment"), (self.c1.pk, "Entertainment"),
(self.c2.pk, "A test"), (self.c2.pk, "A test"),
(self.c3.pk, "Third"), (self.c3.pk, "Third"),
@ -174,6 +175,7 @@ class ModelChoiceFieldTests(TestCase):
self.assertIs(bool(f.choices), True) self.assertIs(bool(f.choices), True)
def test_choices_radio_blank(self): def test_choices_radio_blank(self):
blank_choice = [("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL)]
choices = [ choices = [
(self.c1.pk, "Entertainment"), (self.c1.pk, "Entertainment"),
(self.c2.pk, "A test"), (self.c2.pk, "A test"),
@ -190,7 +192,7 @@ class ModelChoiceFieldTests(TestCase):
) )
self.assertEqual( self.assertEqual(
list(f.choices), list(f.choices),
[("", "---------")] + choices if blank else choices, (blank_choice + choices if blank else choices),
) )
def test_deepcopies_widget(self): def test_deepcopies_widget(self):
@ -425,7 +427,7 @@ class ModelChoiceFieldTests(TestCase):
self.assertCountEqual( self.assertCountEqual(
list(f.choices), list(f.choices),
[ [
("", "---------"), ("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
(self.c1.pk, "Entertainment"), (self.c1.pk, "Entertainment"),
(self.c2.pk, "A test"), (self.c2.pk, "A test"),
(self.c3.pk, "Third"), (self.c3.pk, "Third"),

View File

@ -5,6 +5,7 @@ from decimal import Decimal
from unittest import mock, skipUnless from unittest import mock, skipUnless
from django import forms from django import forms
from django.conf import settings
from django.core.exceptions import ( from django.core.exceptions import (
NON_FIELD_ERRORS, NON_FIELD_ERRORS,
FieldError, FieldError,
@ -341,7 +342,7 @@ class ModelFormBaseTest(TestCase):
self.assertEqual( self.assertEqual(
list(form.fields["author"].choices), list(form.fields["author"].choices),
[ [
("", "---------"), ("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
(writer.pk, "Joe Doe"), (writer.pk, "Joe Doe"),
], ],
) )
@ -1534,7 +1535,7 @@ class ModelFormBasicTests(TestCase):
<li>Slug: <input type="text" name="slug" maxlength="50" required></li> <li>Slug: <input type="text" name="slug" maxlength="50" required></li>
<li>Pub date: <input type="text" name="pub_date" required></li> <li>Pub date: <input type="text" name="pub_date" required></li>
<li>Writer: <select name="writer" required> <li>Writer: <select name="writer" required>
<option value="" selected>---------</option> <option value="" selected>- Select an option -</option>
<option value="%s">Bob Woodward</option> <option value="%s">Bob Woodward</option>
<option value="%s">Mike Royko</option> <option value="%s">Mike Royko</option>
</select></li> </select></li>
@ -1546,7 +1547,7 @@ class ModelFormBasicTests(TestCase):
<option value="%s">Third test</option> <option value="%s">Third test</option>
</select></li> </select></li>
<li>Status: <select name="status"> <li>Status: <select name="status">
<option value="" selected>---------</option> <option value="" selected>- Select an option -</option>
<option value="1">Draft</option> <option value="1">Draft</option>
<option value="2">Pending</option> <option value="2">Pending</option>
<option value="3">Live</option> <option value="3">Live</option>
@ -1588,7 +1589,7 @@ class ModelFormBasicTests(TestCase):
<li>Pub date: <li>Pub date:
<input type="text" name="pub_date" value="1988-01-04" required></li> <input type="text" name="pub_date" value="1988-01-04" required></li>
<li>Writer: <select name="writer" required> <li>Writer: <select name="writer" required>
<option value="">---------</option> <option value="">- Select an option -</option>
<option value="%s">Bob Woodward</option> <option value="%s">Bob Woodward</option>
<option value="%s" selected>Mike Royko</option> <option value="%s" selected>Mike Royko</option>
</select></li> </select></li>
@ -1600,7 +1601,7 @@ class ModelFormBasicTests(TestCase):
<option value="%s">Third test</option> <option value="%s">Third test</option>
</select></li> </select></li>
<li>Status: <select name="status"> <li>Status: <select name="status">
<option value="" selected>---------</option> <option value="" selected>- Select an option -</option>
<option value="1">Draft</option> <option value="1">Draft</option>
<option value="2">Pending</option> <option value="2">Pending</option>
<option value="3">Live</option> <option value="3">Live</option>
@ -1733,7 +1734,7 @@ class ModelFormBasicTests(TestCase):
</div> </div>
<div>Writer: <div>Writer:
<select name="writer" required> <select name="writer" required>
<option value="" selected>---------</option> <option value="" selected>- Select an option -</option>
<option value="%s">Bob Woodward</option> <option value="%s">Bob Woodward</option>
<option value="%s">Mike Royko</option> <option value="%s">Mike Royko</option>
</select> </select>
@ -1750,7 +1751,7 @@ class ModelFormBasicTests(TestCase):
</div> </div>
<div>Status: <div>Status:
<select name="status"> <select name="status">
<option value="" selected>---------</option> <option value="" selected>- Select an option -</option>
<option value="1">Draft</option><option value="2">Pending</option> <option value="1">Draft</option><option value="2">Pending</option>
<option value="3">Live</option> <option value="3">Live</option>
</select> </select>
@ -1783,7 +1784,7 @@ class ModelFormBasicTests(TestCase):
<li>Pub date: <li>Pub date:
<input type="text" name="pub_date" value="1988-01-04" required></li> <input type="text" name="pub_date" value="1988-01-04" required></li>
<li>Writer: <select name="writer" required> <li>Writer: <select name="writer" required>
<option value="">---------</option> <option value="">- Select an option -</option>
<option value="%s">Bob Woodward</option> <option value="%s">Bob Woodward</option>
<option value="%s" selected>Mike Royko</option> <option value="%s" selected>Mike Royko</option>
</select></li> </select></li>
@ -1795,7 +1796,7 @@ class ModelFormBasicTests(TestCase):
<option value="%s">Third test</option> <option value="%s">Third test</option>
</select></li> </select></li>
<li>Status: <select name="status"> <li>Status: <select name="status">
<option value="" selected>---------</option> <option value="" selected>- Select an option -</option>
<option value="1">Draft</option> <option value="1">Draft</option>
<option value="2">Pending</option> <option value="2">Pending</option>
<option value="3">Live</option> <option value="3">Live</option>
@ -1957,7 +1958,7 @@ class ModelFormBasicTests(TestCase):
'<li>Slug: <input type="text" name="slug" maxlength="50" required></li>' '<li>Slug: <input type="text" name="slug" maxlength="50" required></li>'
'<li>Pub date: <input type="text" name="pub_date" required></li>' '<li>Pub date: <input type="text" name="pub_date" required></li>'
'<li>Writer: <select name="writer" required>' '<li>Writer: <select name="writer" required>'
'<option value="" selected>---------</option>' '<option value="" selected>- Select an option -</option>'
'<option value="%s">Bob Woodward</option>' '<option value="%s">Bob Woodward</option>'
'<option value="%s">Mike Royko</option>' '<option value="%s">Mike Royko</option>'
"</select></li>" "</select></li>"
@ -1969,7 +1970,7 @@ class ModelFormBasicTests(TestCase):
'<option value="%s">Third test</option>' '<option value="%s">Third test</option>'
"</select> </li>" "</select> </li>"
'<li>Status: <select name="status">' '<li>Status: <select name="status">'
'<option value="" selected>---------</option>' '<option value="" selected>- Select an option -</option>'
'<option value="1">Draft</option>' '<option value="1">Draft</option>'
'<option value="2">Pending</option>' '<option value="2">Pending</option>'
'<option value="3">Live</option>' '<option value="3">Live</option>'
@ -1986,7 +1987,7 @@ class ModelFormBasicTests(TestCase):
'<li>Slug: <input type="text" name="slug" maxlength="50" required></li>' '<li>Slug: <input type="text" name="slug" maxlength="50" required></li>'
'<li>Pub date: <input type="text" name="pub_date" required></li>' '<li>Pub date: <input type="text" name="pub_date" required></li>'
'<li>Writer: <select name="writer" required>' '<li>Writer: <select name="writer" required>'
'<option value="" selected>---------</option>' '<option value="" selected>- Select an option -</option>'
'<option value="%s">Bob Woodward</option>' '<option value="%s">Bob Woodward</option>'
'<option value="%s">Carl Bernstein</option>' '<option value="%s">Carl Bernstein</option>'
'<option value="%s">Mike Royko</option>' '<option value="%s">Mike Royko</option>'
@ -2000,7 +2001,7 @@ class ModelFormBasicTests(TestCase):
'<option value="%s">Fourth</option>' '<option value="%s">Fourth</option>'
"</select></li>" "</select></li>"
'<li>Status: <select name="status">' '<li>Status: <select name="status">'
'<option value="" selected>---------</option>' '<option value="" selected>- Select an option -</option>'
'<option value="1">Draft</option>' '<option value="1">Draft</option>'
'<option value="2">Pending</option>' '<option value="2">Pending</option>'
'<option value="3">Live</option>' '<option value="3">Live</option>'
@ -2044,7 +2045,8 @@ class ModelFormBasicTests(TestCase):
self.assertEqual(call_count, 0) self.assertEqual(call_count, 0)
self.assertEqual( self.assertEqual(
form.fields["animal"].choices, form.fields["animal"].choices,
models.BLANK_CHOICE_DASH + [("LION", "Lion"), ("ZEBRA", "Zebra")], [("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL)]
+ [("LION", "Lion"), ("ZEBRA", "Zebra")],
) )
self.assertEqual(call_count, 1) self.assertEqual(call_count, 1)
@ -2410,7 +2412,7 @@ class ModelOneToOneFieldTests(TestCase):
""" """
<p><label for="id_writer">Writer:</label> <p><label for="id_writer">Writer:</label>
<select name="writer" id="id_writer" required> <select name="writer" id="id_writer" required>
<option value="" selected>---------</option> <option value="" selected>- Select an option -</option>
<option value="%s">Bob Woodward</option> <option value="%s">Bob Woodward</option>
<option value="%s">Mike Royko</option> <option value="%s">Mike Royko</option>
</select></p> </select></p>
@ -2437,7 +2439,7 @@ class ModelOneToOneFieldTests(TestCase):
""" """
<p><label for="id_writer">Writer:</label> <p><label for="id_writer">Writer:</label>
<select name="writer" id="id_writer" required> <select name="writer" id="id_writer" required>
<option value="">---------</option> <option value="">- Select an option -</option>
<option value="%s" selected>Bob Woodward</option> <option value="%s" selected>Bob Woodward</option>
<option value="%s">Mike Royko</option> <option value="%s">Mike Royko</option>
</select></p> </select></p>
@ -2726,7 +2728,8 @@ class FileAndImageFieldTests(TestCase):
form = FPForm() form = FPForm()
self.assertEqual( self.assertEqual(
[name for _, name in form["path"].field.choices], ["---------", "models.py"] [name for _, name in form["path"].field.choices],
[settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL, "models.py"],
) )
@skipUnless(test_images, "Pillow not installed") @skipUnless(test_images, "Pillow not installed")
@ -3055,7 +3058,7 @@ class OtherModelFormTests(TestCase):
self.assertEqual( self.assertEqual(
tuple(field.choices), tuple(field.choices),
( (
("", "---------"), ("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
(multicolor_item.pk, "blue, red"), (multicolor_item.pk, "blue, red"),
(red_item.pk, "red"), (red_item.pk, "red"),
), ),
@ -3069,14 +3072,19 @@ class OtherModelFormTests(TestCase):
field = forms.ModelChoiceField(Inventory.objects.all(), to_field_name="barcode") field = forms.ModelChoiceField(Inventory.objects.all(), to_field_name="barcode")
self.assertEqual( self.assertEqual(
tuple(field.choices), tuple(field.choices),
(("", "---------"), (86, "Apple"), (87, "Core"), (22, "Pear")), (
("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL),
(86, "Apple"),
(87, "Core"),
(22, "Pear"),
),
) )
form = InventoryForm(instance=core) form = InventoryForm(instance=core)
self.assertHTMLEqual( self.assertHTMLEqual(
str(form["parent"]), str(form["parent"]),
"""<select name="parent" id="id_parent"> """<select name="parent" id="id_parent">
<option value="">---------</option> <option value="">- Select an option -</option>
<option value="86" selected>Apple</option> <option value="86" selected>Apple</option>
<option value="87">Core</option> <option value="87">Core</option>
<option value="22">Pear</option> <option value="22">Pear</option>

View File

@ -1267,7 +1267,7 @@ class ModelFormsetTest(TestCase):
formset.forms[0].as_p(), formset.forms[0].as_p(),
'<p><label for="id_form-0-owner">Owner:</label>' '<p><label for="id_form-0-owner">Owner:</label>'
'<select name="form-0-owner" id="id_form-0-owner">' '<select name="form-0-owner" id="id_form-0-owner">'
'<option value="" selected>---------</option>' '<option value="" selected>- Select an option -</option>'
'<option value="%d">Joe Perry at Giordanos</option>' '<option value="%d">Joe Perry at Giordanos</option>'
'<option value="%d">Jack Berry at Giordanos</option>' '<option value="%d">Jack Berry at Giordanos</option>'
"</select></p>" "</select></p>"

View File

@ -1,6 +1,7 @@
from datetime import date from datetime import date
from django import forms from django import forms
from django.conf import settings
from django.contrib.admin.models import ADDITION, CHANGE, DELETION, LogEntry from django.contrib.admin.models import ADDITION, CHANGE, DELETION, LogEntry
from django.contrib.admin.options import ( from django.contrib.admin.options import (
HORIZONTAL, HORIZONTAL,
@ -664,7 +665,7 @@ class ModelAdminTests(TestCase):
'<div class="related-widget-wrapper" data-model-ref="band">' '<div class="related-widget-wrapper" data-model-ref="band">'
'<select data-context="available-source" ' '<select data-context="available-source" '
'name="main_band" id="id_main_band" required>' 'name="main_band" id="id_main_band" required>'
'<option value="" selected>---------</option>' '<option value="" selected>- Select an option -</option>'
'<option value="%d">The Beatles</option>' '<option value="%d">The Beatles</option>'
'<option value="%d">The Doors</option>' '<option value="%d">The Doors</option>'
"</select></div>" % (band2.id, self.band.id), "</select></div>" % (band2.id, self.band.id),
@ -688,7 +689,7 @@ class ModelAdminTests(TestCase):
'<div class="related-widget-wrapper" data-model-ref="band">' '<div class="related-widget-wrapper" data-model-ref="band">'
'<select data-context="available-source" ' '<select data-context="available-source" '
'name="main_band" id="id_main_band" required>' 'name="main_band" id="id_main_band" required>'
'<option value="" selected>---------</option>' '<option value="" selected>- Select an option -</option>'
'<option value="%d">The Doors</option>' '<option value="%d">The Doors</option>'
"</select></div>" % self.band.id, "</select></div>" % self.band.id,
) )
@ -736,29 +737,30 @@ class ModelAdminTests(TestCase):
# ForeignKey widgets in the admin are wrapped with RelatedFieldWidgetWrapper so # ForeignKey widgets in the admin are wrapped with RelatedFieldWidgetWrapper so
# they need to be handled properly when type checking. For Select fields, all of # they need to be handled properly when type checking. For Select fields, all of
# the choices lists have a first entry of dashes. # the choices lists have a first entry of dashes.
blank_option = ("", settings.FORMS_DEFAULT_BLANK_CHOICE_LABEL)
cma = ModelAdmin(Concert, self.site) cma = ModelAdmin(Concert, self.site)
cmafa = cma.get_form(request) cmafa = cma.get_form(request)
self.assertEqual(type(cmafa.base_fields["main_band"].widget.widget), Select) self.assertEqual(type(cmafa.base_fields["main_band"].widget.widget), Select)
self.assertEqual( self.assertEqual(
list(cmafa.base_fields["main_band"].widget.choices), list(cmafa.base_fields["main_band"].widget.choices),
[("", "---------"), (self.band.id, "The Doors")], [blank_option, (self.band.id, "The Doors")],
) )
self.assertEqual(type(cmafa.base_fields["opening_band"].widget.widget), Select) self.assertEqual(type(cmafa.base_fields["opening_band"].widget.widget), Select)
self.assertEqual( self.assertEqual(
list(cmafa.base_fields["opening_band"].widget.choices), list(cmafa.base_fields["opening_band"].widget.choices),
[("", "---------"), (self.band.id, "The Doors")], [blank_option, (self.band.id, "The Doors")],
) )
self.assertEqual(type(cmafa.base_fields["day"].widget), Select) self.assertEqual(type(cmafa.base_fields["day"].widget), Select)
self.assertEqual( self.assertEqual(
list(cmafa.base_fields["day"].widget.choices), list(cmafa.base_fields["day"].widget.choices),
[("", "---------"), (1, "Fri"), (2, "Sat")], [blank_option, (1, "Fri"), (2, "Sat")],
) )
self.assertEqual(type(cmafa.base_fields["transport"].widget), Select) self.assertEqual(type(cmafa.base_fields["transport"].widget), Select)
self.assertEqual( self.assertEqual(
list(cmafa.base_fields["transport"].widget.choices), list(cmafa.base_fields["transport"].widget.choices),
[("", "---------"), (1, "Plane"), (2, "Train"), (3, "Bus")], [blank_option, (1, "Plane"), (2, "Train"), (3, "Bus")],
) )
def test_foreign_key_as_radio_field(self): def test_foreign_key_as_radio_field(self):