Skip to content

Commit 63c56cd

Browse files
annalaurawMHLut
authored andcommitted
Fixed #35870 -- Made blank choice label in forms more accessible.
Added new constant django.db.models.fields.BLANK_CHOICE_LABEL for an accessible and translatable blank choice label in forms. Deprecated django.db.models.fields.BLANK_CHOICE_DASH constant. Added the immediately deprecated transitional setting USE_BLANK_CHOICE_DASH. Co-Authored-By: Marijke Luttekes <mail@marijkeluttekes.dev>
1 parent dc467fd commit 63c56cd

31 files changed

Lines changed: 276 additions & 79 deletions

File tree

django/conf/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@
1616
import django
1717
from django.conf import global_settings
1818
from django.core.exceptions import ImproperlyConfigured
19+
from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes
1920
from django.utils.functional import LazyObject, empty
2021

2122
ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
2223
DEFAULT_STORAGE_ALIAS = "default"
2324
STATICFILES_STORAGE_ALIAS = "staticfiles"
2425

26+
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG = (
27+
"The USE_BLANK_CHOICE_DASH setting is deprecated. If you wish to define "
28+
"your own default blank choice label, override "
29+
"django.db.models.fields.BLANK_CHOICE_LABEL in your app's ready() method."
30+
)
31+
2532

2633
class SettingsReference(str):
2734
"""
@@ -226,6 +233,12 @@ def __getattr__(self, name):
226233

227234
def __setattr__(self, name, value):
228235
self._deleted.discard(name)
236+
if name == "USE_BLANK_CHOICE_DASH":
237+
warnings.warn(
238+
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG,
239+
RemovedInDjango70Warning,
240+
skip_file_prefixes=django_file_prefixes(),
241+
)
229242
super().__setattr__(name, value)
230243

231244
def __delattr__(self, name):

django/conf/global_settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ def gettext_noop(s):
218218

219219
# Default form rendering class.
220220
FORM_RENDERER = "django.forms.renderers.DjangoTemplates"
221+
# RemovedInDjango70Warning: This setting allows to revert back to the old
222+
# blank choice label in Django 6.1.
223+
USE_BLANK_CHOICE_DASH = False
221224

222225
# Default email address to use for various automated correspondence from
223226
# the site managers.

django/contrib/admin/options.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from django.core.paginator import Paginator
4444
from django.db import models, router, transaction
4545
from django.db.models.constants import LOOKUP_SEP
46+
from django.db.models.utils import get_blank_choice_label
4647
from django.forms.formsets import DELETION_FIELD_NAME, all_valid
4748
from django.forms.models import (
4849
BaseInlineFormSet,
@@ -1049,11 +1050,13 @@ def get_actions(self, request):
10491050
actions = self._filter_actions_by_permissions(request, self._get_base_actions())
10501051
return {name: (func, name, desc) for func, name, desc in actions}
10511052

1052-
def get_action_choices(self, request, default_choices=models.BLANK_CHOICE_DASH):
1053+
def get_action_choices(self, request, default_choices=None):
10531054
"""
10541055
Return a list of choices for use in a form object. Each choice is a
10551056
tuple (name, description).
10561057
"""
1058+
if default_choices is None:
1059+
default_choices = [("", get_blank_choice_label())]
10571060
choices = [*default_choices]
10581061
for func, name, description in self.get_actions(request).values():
10591062
choice = (name, description % model_format_dict(self.opts))

django/db/models/fields/__init__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from django.db import connection, connections, router
1616
from django.db.models.constants import LOOKUP_SEP
1717
from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin
18+
from django.db.models.utils import get_blank_choice_label
1819
from django.db.utils import NotSupportedError
1920
from django.utils import timezone
2021
from django.utils.choices import (
@@ -39,7 +40,9 @@
3940

4041
__all__ = [
4142
"AutoField",
43+
# RemovedInDjango70Warning
4244
"BLANK_CHOICE_DASH",
45+
"BLANK_CHOICE_LABEL",
4346
"BigAutoField",
4447
"BigIntegerField",
4548
"BinaryField",
@@ -81,9 +84,13 @@ class NOT_PROVIDED:
8184
pass
8285

8386

84-
# The values to use for "blank" in SelectFields. Will be appended to the start
85-
# of most "choices" lists.
87+
# RemovedInDjango70Warning: From Django 6.1, the values to use for "blank"
88+
# in SelectFields will be defined by the below BLANK_CHOICE_LABEL constant.
89+
# Will be appended to the start of most "choices" lists.
90+
# BLANK_CHOICE_DASH is still available as a constant in Django 6.1.
8691
BLANK_CHOICE_DASH = [("", "---------")]
92+
# This allows any app's ready() method to overwrite BLANK_CHOICE_LABEL.
93+
BLANK_CHOICE_LABEL = _("- Select an option -")
8794

8895

8996
def _load_field(app_label, model_name, field_name):
@@ -1088,14 +1095,16 @@ def _db_default_expression(self):
10881095
def get_choices(
10891096
self,
10901097
include_blank=True,
1091-
blank_choice=BLANK_CHOICE_DASH,
1098+
blank_choice=None,
10921099
limit_choices_to=None,
10931100
ordering=(),
10941101
):
10951102
"""
10961103
Return choices with a default blank choices included, for use
10971104
as <select> choices for this field.
10981105
"""
1106+
if blank_choice is None:
1107+
blank_choice = [("", get_blank_choice_label())]
10991108
if self.choices is not None:
11001109
if include_blank:
11011110
return BlankChoiceIterator(self.choices, blank_choice)

django/db/models/fields/reverse_related.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from django.utils.functional import cached_property
1414
from django.utils.hashable import make_hashable
1515

16-
from . import BLANK_CHOICE_DASH
16+
from ..utils import get_blank_choice_label
1717
from .mixins import FieldCacheMixin
1818

1919

@@ -172,7 +172,7 @@ def __getstate__(self):
172172
def get_choices(
173173
self,
174174
include_blank=True,
175-
blank_choice=BLANK_CHOICE_DASH,
175+
blank_choice=None,
176176
limit_choices_to=None,
177177
ordering=(),
178178
):
@@ -183,6 +183,8 @@ def get_choices(
183183
Analog of django.db.models.fields.Field.get_choices(), provided
184184
initially for utilization by RelatedFieldListFilter.
185185
"""
186+
if blank_choice is None:
187+
blank_choice = [("", get_blank_choice_label())]
186188
limit_choices_to = limit_choices_to or self.limit_choices_to
187189
qs = self.related_model._default_manager.complex_filter(limit_choices_to)
188190
if ordering:

django/db/models/utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,15 @@ def __init_subclass__(cls, **kwargs):
6767
break
6868

6969
super().__init_subclass__(**kwargs)
70+
71+
72+
# RemovedInDjango70Warning: At the end of the deprecation, remove this function
73+
# and use .fields.BLANK_CHOICE_LABEL directly instead.
74+
def get_blank_choice_label():
75+
from django.conf import settings
76+
77+
from .fields import BLANK_CHOICE_DASH, BLANK_CHOICE_LABEL
78+
79+
if settings.USE_BLANK_CHOICE_DASH:
80+
return BLANK_CHOICE_DASH[0][1]
81+
return BLANK_CHOICE_LABEL

django/forms/fields.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from django.core import validators
1717
from django.core.exceptions import ValidationError
18+
from django.db.models.utils import get_blank_choice_label
1819
from django.forms.boundfield import BoundField
1920
from django.forms.utils import from_current_timezone, to_current_timezone
2021
from django.forms.widgets import (
@@ -1200,7 +1201,7 @@ def __init__(
12001201
if self.required:
12011202
self.choices = []
12021203
else:
1203-
self.choices = [("", "---------")]
1204+
self.choices = [("", get_blank_choice_label())]
12041205

12051206
if self.match is not None:
12061207
self.match_re = re.compile(self.match)

django/forms/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
ValidationError,
1313
)
1414
from django.core.validators import ProhibitNullCharactersValidator
15-
from django.db.models.utils import AltersData
15+
from django.db.models.utils import AltersData, get_blank_choice_label
1616
from django.forms.fields import ChoiceField, Field
1717
from django.forms.forms import BaseForm, DeclarativeFieldsMetaclass
1818
from django.forms.formsets import BaseFormSet, formset_factory
@@ -1481,7 +1481,7 @@ def __init__(
14811481
self,
14821482
queryset,
14831483
*,
1484-
empty_label="---------",
1484+
empty_label="",
14851485
required=True,
14861486
widget=None,
14871487
label=None,
@@ -1508,6 +1508,8 @@ def __init__(
15081508
):
15091509
self.empty_label = None
15101510
else:
1511+
if empty_label == "":
1512+
empty_label = get_blank_choice_label()
15111513
self.empty_label = empty_label
15121514
self.queryset = queryset
15131515
self.limit_choices_to = limit_choices_to # limit the queryset later.

docs/internals/deprecation.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ details on these changes.
6464
:class:`~django.db.models.JSONNull` to query for a JSON ``null`` value
6565
instead.
6666

67+
* The ``django.db.models.fields.BLANK_CHOICE_DASH`` constant will be removed.
68+
69+
* The ``USE_BLANK_CHOICE_DASH`` transitional setting will be removed.
70+
6771
* The ``Field.get_placeholder_sql`` shim over the deprecated
6872
``get_placeholder`` method will be removed.
6973

docs/intro/_images/admin09.png

4.15 KB
Loading

0 commit comments

Comments
 (0)