diff --git a/django/forms/models.py b/django/forms/models.py index dafc11f995..37adda7924 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -13,7 +13,7 @@ from django.forms.forms import BaseForm, DeclarativeFieldsMetaclass from django.forms.formsets import BaseFormSet, formset_factory from django.forms.utils import ErrorList from django.forms.widgets import ( - HiddenInput, MultipleHiddenInput, SelectMultiple, + HiddenInput, MultipleHiddenInput, RadioSelect, SelectMultiple, ) from django.utils.text import capfirst, get_text_list from django.utils.translation import gettext, gettext_lazy as _ @@ -1185,8 +1185,19 @@ class ModelChoiceField(ChoiceField): required=True, widget=None, label=None, initial=None, help_text='', to_field_name=None, limit_choices_to=None, **kwargs): + # Determine whether to include the empty label based on: + # 1. If required and initial is provided, never show empty label + # 2. If required and widget is RadioSelect, never show empty label + # (RadioSelect has a natural unfilled state) + # 3. Otherwise, show the empty label if required and (initial is not None): self.empty_label = None + elif required and isinstance(widget, type) and issubclass(widget, RadioSelect): + # widget is a class (not yet instantiated) + self.empty_label = None + elif required and isinstance(widget, RadioSelect): + # widget is an instance + self.empty_label = None else: self.empty_label = empty_label diff --git a/tests/model_forms/test_radioselect_blank.py b/tests/model_forms/test_radioselect_blank.py new file mode 100644 index 0000000000..cfd3a3e164 --- /dev/null +++ b/tests/model_forms/test_radioselect_blank.py @@ -0,0 +1,157 @@ +""" +Tests for RadioSelect widget with ModelChoiceField to ensure blank options +are not shown when the field is required. +""" +from django import forms +from django.forms.widgets import RadioSelect, Select +from django.test import TestCase + +from .models import Category + + +class RadioSelectBlankOptionTests(TestCase): + """ + Test that RadioSelect widgets for required ModelChoiceField don't show + a blank option, while Select widgets still do. + """ + + @classmethod + def setUpTestData(cls): + cls.c1 = Category.objects.create(name='First', slug='first', url='first') + cls.c2 = Category.objects.create(name='Second', slug='second', url='second') + + def test_radioselect_required_no_blank_option(self): + """ + A required ModelChoiceField with RadioSelect widget should not include + a blank option. + """ + field = forms.ModelChoiceField( + queryset=Category.objects.all(), + widget=RadioSelect, + required=True + ) + # The field should not have an empty_label + self.assertIsNone(field.empty_label) + + # Check choices don't include blank option + choices = list(field.choices) + self.assertEqual(len(choices), 2) + self.assertEqual(choices[0][0], self.c1.pk) + self.assertEqual(choices[1][0], self.c2.pk) + + def test_radioselect_required_no_blank_option_instance(self): + """ + A required ModelChoiceField with an instantiated RadioSelect widget + should not include a blank option. + """ + field = forms.ModelChoiceField( + queryset=Category.objects.all(), + widget=RadioSelect(), + required=True + ) + # The field should not have an empty_label + self.assertIsNone(field.empty_label) + + # Check choices don't include blank option + choices = list(field.choices) + self.assertEqual(len(choices), 2) + self.assertEqual(choices[0][0], self.c1.pk) + self.assertEqual(choices[1][0], self.c2.pk) + + def test_radioselect_not_required_has_blank_option(self): + """ + A non-required ModelChoiceField with RadioSelect widget should still + include a blank option. + """ + field = forms.ModelChoiceField( + queryset=Category.objects.all(), + widget=RadioSelect, + required=False + ) + # The field should have an empty_label + self.assertEqual(field.empty_label, '---------') + + # Check choices include blank option + choices = list(field.choices) + self.assertEqual(len(choices), 3) + self.assertEqual(choices[0][0], '') + self.assertEqual(choices[0][1], '---------') + + def test_select_required_has_blank_option(self): + """ + A required ModelChoiceField with Select widget should still include + a blank option (existing behavior should be preserved). + """ + field = forms.ModelChoiceField( + queryset=Category.objects.all(), + widget=Select, + required=True + ) + # The field should have an empty_label for Select widget + self.assertEqual(field.empty_label, '---------') + + # Check choices include blank option + choices = list(field.choices) + self.assertEqual(len(choices), 3) + self.assertEqual(choices[0][0], '') + self.assertEqual(choices[0][1], '---------') + + def test_select_no_widget_required_has_blank_option(self): + """ + A required ModelChoiceField with no explicit widget (defaults to Select) + should include a blank option (existing behavior should be preserved). + """ + field = forms.ModelChoiceField( + queryset=Category.objects.all(), + required=True + ) + # The field should have an empty_label for default Select widget + self.assertEqual(field.empty_label, '---------') + + # Check choices include blank option + choices = list(field.choices) + self.assertEqual(len(choices), 3) + self.assertEqual(choices[0][0], '') + self.assertEqual(choices[0][1], '---------') + + def test_radioselect_required_with_initial_no_blank_option(self): + """ + A required ModelChoiceField with RadioSelect widget and initial value + should not include a blank option. + """ + field = forms.ModelChoiceField( + queryset=Category.objects.all(), + widget=RadioSelect, + required=True, + initial=self.c1 + ) + # The field should not have an empty_label + self.assertIsNone(field.empty_label) + + # Check choices don't include blank option + choices = list(field.choices) + self.assertEqual(len(choices), 2) + self.assertEqual(choices[0][0], self.c1.pk) + self.assertEqual(choices[1][0], self.c2.pk) + + def test_radioselect_custom_empty_label(self): + """ + A non-required ModelChoiceField with RadioSelect widget and custom + empty_label should use the custom label. + """ + field = forms.ModelChoiceField( + queryset=Category.objects.all(), + widget=RadioSelect, + required=False, + empty_label='Select one...' + ) + # The field should have the custom empty_label + self.assertEqual(field.empty_label, 'Select one...') + + # Check choices include custom blank option + choices = list(field.choices) + self.assertEqual(len(choices), 3) + self.assertEqual(choices[0][0], '') + self.assertEqual(choices[0][1], 'Select one...') + +