diff options
| author | Annabelle Wiegart <annabelle.wiegart@proton.me> | 2026-01-18 20:03:28 +0100 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-04-22 17:06:29 -0400 |
| commit | 63c56cda133a85a158502891c40465bc0331d3d9 (patch) | |
| tree | 04380903d14307b71416b2e048ce4be8361cf0df /django | |
| parent | dc467fdc3b5744cec71fab876c23a14013e2510b (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__.py | 13 | ||||
| -rw-r--r-- | django/conf/global_settings.py | 3 | ||||
| -rw-r--r-- | django/contrib/admin/options.py | 5 | ||||
| -rw-r--r-- | django/db/models/fields/__init__.py | 15 | ||||
| -rw-r--r-- | django/db/models/fields/reverse_related.py | 6 | ||||
| -rw-r--r-- | django/db/models/utils.py | 12 | ||||
| -rw-r--r-- | django/forms/fields.py | 3 | ||||
| -rw-r--r-- | django/forms/models.py | 6 |
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. |
