Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion netbox/utilities/forms/fields/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand All @@ -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):
Expand Down
33 changes: 33 additions & 0 deletions netbox/utilities/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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'))