summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorAnnabelle Wiegart <annabelle.wiegart@proton.me>2026-01-18 20:03:28 +0100
committerJacob Walls <jacobtylerwalls@gmail.com>2026-04-22 17:06:29 -0400
commit63c56cda133a85a158502891c40465bc0331d3d9 (patch)
tree04380903d14307b71416b2e048ce4be8361cf0df /django
parentdc467fdc3b5744cec71fab876c23a14013e2510b (diff)
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>
Diffstat (limited to 'django')
-rw-r--r--django/conf/__init__.py13
-rw-r--r--django/conf/global_settings.py3
-rw-r--r--django/contrib/admin/options.py5
-rw-r--r--django/db/models/fields/__init__.py15
-rw-r--r--django/db/models/fields/reverse_related.py6
-rw-r--r--django/db/models/utils.py12
-rw-r--r--django/forms/fields.py3
-rw-r--r--django/forms/models.py6
8 files changed, 54 insertions, 9 deletions
diff --git a/django/conf/__init__.py b/django/conf/__init__.py
index c7ae36aba0..25f5ffa305 100644
--- a/django/conf/__init__.py
+++ b/django/conf/__init__.py
@@ -16,12 +16,19 @@ from pathlib import Path
import django
from django.conf import global_settings
from django.core.exceptions import ImproperlyConfigured
+from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes
from django.utils.functional import LazyObject, empty
ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
DEFAULT_STORAGE_ALIAS = "default"
STATICFILES_STORAGE_ALIAS = "staticfiles"
+USE_BLANK_CHOICE_DASH_DEPRECATED_MSG = (
+ "The USE_BLANK_CHOICE_DASH setting is deprecated. If you wish to define "
+ "your own default blank choice label, override "
+ "django.db.models.fields.BLANK_CHOICE_LABEL in your app's ready() method."
+)
+
class SettingsReference(str):
"""
@@ -226,6 +233,12 @@ class UserSettingsHolder:
def __setattr__(self, name, value):
self._deleted.discard(name)
+ if name == "USE_BLANK_CHOICE_DASH":
+ warnings.warn(
+ USE_BLANK_CHOICE_DASH_DEPRECATED_MSG,
+ RemovedInDjango70Warning,
+ skip_file_prefixes=django_file_prefixes(),
+ )
super().__setattr__(name, value)
def __delattr__(self, name):
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 72c376dd78..b2d07cffba 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -218,6 +218,9 @@ TEMPLATES = []
# Default form rendering class.
FORM_RENDERER = "django.forms.renderers.DjangoTemplates"
+# RemovedInDjango70Warning: This setting allows to revert back to the old
+# blank choice label in Django 6.1.
+USE_BLANK_CHOICE_DASH = False
# Default email address to use for various automated correspondence from
# the site managers.
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index bb091e4c52..71d4a2d55c 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -43,6 +43,7 @@ from django.core.exceptions import (
from django.core.paginator import Paginator
from django.db import models, router, transaction
from django.db.models.constants import LOOKUP_SEP
+from django.db.models.utils import get_blank_choice_label
from django.forms.formsets import DELETION_FIELD_NAME, all_valid
from django.forms.models import (
BaseInlineFormSet,
@@ -1049,11 +1050,13 @@ class ModelAdmin(BaseModelAdmin):
actions = self._filter_actions_by_permissions(request, self._get_base_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
tuple (name, description).
"""
+ if default_choices is None:
+ default_choices = [("", get_blank_choice_label())]
choices = [*default_choices]
for func, name, description in self.get_actions(request).values():
choice = (name, description % model_format_dict(self.opts))
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index e248b70ba3..a7ec41bf75 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -15,6 +15,7 @@ from django.core import checks, exceptions, validators
from django.db import connection, connections, router
from django.db.models.constants import LOOKUP_SEP
from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin
+from django.db.models.utils import get_blank_choice_label
from django.db.utils import NotSupportedError
from django.utils import timezone
from django.utils.choices import (
@@ -39,7 +40,9 @@ from django.utils.translation import gettext_lazy as _
__all__ = [
"AutoField",
+ # RemovedInDjango70Warning
"BLANK_CHOICE_DASH",
+ "BLANK_CHOICE_LABEL",
"BigAutoField",
"BigIntegerField",
"BinaryField",
@@ -81,9 +84,13 @@ class NOT_PROVIDED:
pass
-# The values to use for "blank" in SelectFields. Will be appended to the start
-# of most "choices" lists.
+# RemovedInDjango70Warning: From Django 6.1, the values to use for "blank"
+# in SelectFields will be defined by the below BLANK_CHOICE_LABEL constant.
+# Will be appended to the start of most "choices" lists.
+# BLANK_CHOICE_DASH is still available as a constant in Django 6.1.
BLANK_CHOICE_DASH = [("", "---------")]
+# This allows any app's ready() method to overwrite BLANK_CHOICE_LABEL.
+BLANK_CHOICE_LABEL = _("- Select an option -")
def _load_field(app_label, model_name, field_name):
@@ -1088,7 +1095,7 @@ class Field(RegisterLookupMixin):
def get_choices(
self,
include_blank=True,
- blank_choice=BLANK_CHOICE_DASH,
+ blank_choice=None,
limit_choices_to=None,
ordering=(),
):
@@ -1096,6 +1103,8 @@ class Field(RegisterLookupMixin):
Return choices with a default blank choices included, for use
as <select> choices for this field.
"""
+ if blank_choice is None:
+ blank_choice = [("", get_blank_choice_label())]
if self.choices is not None:
if include_blank:
return BlankChoiceIterator(self.choices, blank_choice)
diff --git a/django/db/models/fields/reverse_related.py b/django/db/models/fields/reverse_related.py
index e6c2525115..df50ba436f 100644
--- a/django/db/models/fields/reverse_related.py
+++ b/django/db/models/fields/reverse_related.py
@@ -13,7 +13,7 @@ from django.core import exceptions
from django.utils.functional import cached_property
from django.utils.hashable import make_hashable
-from . import BLANK_CHOICE_DASH
+from ..utils import get_blank_choice_label
from .mixins import FieldCacheMixin
@@ -172,7 +172,7 @@ class ForeignObjectRel(FieldCacheMixin):
def get_choices(
self,
include_blank=True,
- blank_choice=BLANK_CHOICE_DASH,
+ blank_choice=None,
limit_choices_to=None,
ordering=(),
):
@@ -183,6 +183,8 @@ class ForeignObjectRel(FieldCacheMixin):
Analog of django.db.models.fields.Field.get_choices(), provided
initially for utilization by RelatedFieldListFilter.
"""
+ if blank_choice is None:
+ blank_choice = [("", get_blank_choice_label())]
limit_choices_to = limit_choices_to or self.limit_choices_to
qs = self.related_model._default_manager.complex_filter(limit_choices_to)
if ordering:
diff --git a/django/db/models/utils.py b/django/db/models/utils.py
index c6cb5ef165..7f38bd7afb 100644
--- a/django/db/models/utils.py
+++ b/django/db/models/utils.py
@@ -67,3 +67,15 @@ class AltersData:
break
super().__init_subclass__(**kwargs)
+
+
+# RemovedInDjango70Warning: At the end of the deprecation, remove this function
+# and use .fields.BLANK_CHOICE_LABEL directly instead.
+def get_blank_choice_label():
+ from django.conf import settings
+
+ from .fields import BLANK_CHOICE_DASH, BLANK_CHOICE_LABEL
+
+ if settings.USE_BLANK_CHOICE_DASH:
+ return BLANK_CHOICE_DASH[0][1]
+ return BLANK_CHOICE_LABEL
diff --git a/django/forms/fields.py b/django/forms/fields.py
index 8aad2d48b8..26640ed7d3 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -15,6 +15,7 @@ from io import BytesIO
from django.core import validators
from django.core.exceptions import ValidationError
+from django.db.models.utils import get_blank_choice_label
from django.forms.boundfield import BoundField
from django.forms.utils import from_current_timezone, to_current_timezone
from django.forms.widgets import (
@@ -1200,7 +1201,7 @@ class FilePathField(ChoiceField):
if self.required:
self.choices = []
else:
- self.choices = [("", "---------")]
+ self.choices = [("", get_blank_choice_label())]
if self.match is not None:
self.match_re = re.compile(self.match)
diff --git a/django/forms/models.py b/django/forms/models.py
index a53f119995..9686baa6f2 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -12,7 +12,7 @@ from django.core.exceptions import (
ValidationError,
)
from django.core.validators import ProhibitNullCharactersValidator
-from django.db.models.utils import AltersData
+from django.db.models.utils import AltersData, get_blank_choice_label
from django.forms.fields import ChoiceField, Field
from django.forms.forms import BaseForm, DeclarativeFieldsMetaclass
from django.forms.formsets import BaseFormSet, formset_factory
@@ -1481,7 +1481,7 @@ class ModelChoiceField(ChoiceField):
self,
queryset,
*,
- empty_label="---------",
+ empty_label="",
required=True,
widget=None,
label=None,
@@ -1508,6 +1508,8 @@ class ModelChoiceField(ChoiceField):
):
self.empty_label = None
else:
+ if empty_label == "":
+ empty_label = get_blank_choice_label()
self.empty_label = empty_label
self.queryset = queryset
self.limit_choices_to = limit_choices_to # limit the queryset later.