From 90712fa865dbee929d9ee01bee788ff6957246d4 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 30 Oct 2025 15:34:27 -0700 Subject: [PATCH] 20645 CSVChoiceField use default if blank --- netbox/utilities/forms/fields/csv.py | 22 ++++++++++++++++++- netbox/utilities/tests/test_forms.py | 33 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/forms/fields/csv.py b/netbox/utilities/forms/fields/csv.py index 45428521c06..eab2773c1c2 100644 --- a/netbox/utilities/forms/fields/csv.py +++ b/netbox/utilities/forms/fields/csv.py @@ -18,6 +18,20 @@ ) +class CSVSelectWidget(forms.Select): + """ + Custom Select widget for CSV imports that treats blank values as omitted. + This allows model defaults to be applied when a CSV field is present but empty. + """ + def value_omitted_from_data(self, data, files, name): + # Check if value is omitted using parent behavior + if super().value_omitted_from_data(data, files, name): + return True + # Treat blank/empty strings as omitted to allow model defaults + value = data.get(name) + return value == '' or value is None + + class CSVChoicesMixin: STATIC_CHOICES = True @@ -29,8 +43,9 @@ def __init__(self, *, choices=(), **kwargs): class CSVChoiceField(CSVChoicesMixin, forms.ChoiceField): """ A CSV field which accepts a single selection value. + Treats blank CSV values as omitted to allow model defaults. """ - pass + widget = CSVSelectWidget class CSVMultipleChoiceField(CSVChoicesMixin, forms.MultipleChoiceField): @@ -46,7 +61,12 @@ def to_python(self, value): class CSVTypedChoiceField(forms.TypedChoiceField): + """ + A CSV field for typed choice values. + Treats blank CSV values as omitted to allow model defaults. + """ STATIC_CHOICES = True + widget = CSVSelectWidget class CSVModelChoiceField(forms.ModelChoiceField): diff --git a/netbox/utilities/tests/test_forms.py b/netbox/utilities/tests/test_forms.py index 8ec1404d5f8..cc3ebbb71cd 100644 --- a/netbox/utilities/tests/test_forms.py +++ b/netbox/utilities/tests/test_forms.py @@ -4,6 +4,7 @@ from dcim.models import Site from netbox.choices import ImportFormatChoices from utilities.forms.bulk_import import BulkImportForm +from utilities.forms.fields.csv import CSVSelectWidget from utilities.forms.forms import BulkRenameForm from utilities.forms.utils import get_field_value, expand_alphanumeric_pattern, expand_ipaddress_pattern @@ -448,3 +449,35 @@ def test_bound_null_with_initial(self): get_field_value(form, 'site'), None ) + + +class CSVSelectWidgetTest(TestCase): + """ + Validate that CSVSelectWidget treats blank values as omitted. + This allows model defaults to be applied when CSV fields are present but empty. + Related to issue #20645. + """ + + def test_blank_value_treated_as_omitted(self): + """Test that blank string values are treated as omitted""" + widget = CSVSelectWidget() + data = {'test_field': ''} + self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field')) + + def test_none_value_treated_as_omitted(self): + """Test that None values are treated as omitted""" + widget = CSVSelectWidget() + data = {'test_field': None} + self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field')) + + def test_missing_field_treated_as_omitted(self): + """Test that missing fields are treated as omitted""" + widget = CSVSelectWidget() + data = {} + self.assertTrue(widget.value_omitted_from_data(data, {}, 'test_field')) + + def test_valid_value_not_omitted(self): + """Test that valid values are not treated as omitted""" + widget = CSVSelectWidget() + data = {'test_field': 'valid_value'} + self.assertFalse(widget.value_omitted_from_data(data, {}, 'test_field'))