Fix RadioSelect showing blank option for required foreign keys

Prevent ModelForm RadioSelect widgets from displaying a blank "-------" option when the foreign key has blank=False. This ensures the form only shows valid choices and avoids user confusion.

Closes # (if applicable)
This commit is contained in:
utkarsh.arya@zomato.com 2025-11-15 23:00:16 +00:00
parent 335c9c94ac
commit 271666ad4a
2 changed files with 169 additions and 1 deletions

View file

@ -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

View file

@ -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...')