From 63c56cda133a85a158502891c40465bc0331d3d9 Mon Sep 17 00:00:00 2001 From: Annabelle Wiegart Date: Sun, 18 Jan 2026 20:03:28 +0100 Subject: 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 --- django/conf/__init__.py | 13 +++++++++++++ django/conf/global_settings.py | 3 +++ django/contrib/admin/options.py | 5 ++++- django/db/models/fields/__init__.py | 15 ++++++++++++--- django/db/models/fields/reverse_related.py | 6 ++++-- django/db/models/utils.py | 12 ++++++++++++ django/forms/fields.py | 3 ++- django/forms/models.py | 6 ++++-- 8 files changed, 54 insertions(+), 9 deletions(-) (limited to 'django') 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