diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 77bd33dbf..1379a9de1 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -7,6 +7,10 @@ * [#6748](https://github.com/netbox-community/netbox/issues/6748) - Add site group filter to devices list * [#6872](https://github.com/netbox-community/netbox/issues/6872) - Add table configuration button to child prefixes view +### Bug Fixes + +* [#5968](https://github.com/netbox-community/netbox/issues/5968) - Model forms should save empty custom field values as null + --- ## v2.11.11 (2021-08-12) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index ab1c5aded..25fc7813d 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -77,7 +77,11 @@ class CustomFieldModelForm(forms.ModelForm): # Save custom field data on instance for cf_name in self.custom_fields: - self.instance.custom_field_data[cf_name[3:]] = self.cleaned_data.get(cf_name) + key = cf_name[3:] # Strip "cf_" from field name + value = self.cleaned_data.get(cf_name) + empty_values = self.fields[cf_name].empty_values + # Convert "empty" values to null + self.instance.custom_field_data[key] = value if value not in empty_values else None return super().clean() diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py new file mode 100644 index 000000000..cb0a9c081 --- /dev/null +++ b/netbox/extras/tests/test_forms.py @@ -0,0 +1,53 @@ +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from dcim.forms import SiteForm +from dcim.models import Site +from extras.choices import CustomFieldTypeChoices +from extras.models import CustomField + + +class CustomFieldModelFormTest(TestCase): + + @classmethod + def setUpTestData(cls): + obj_type = ContentType.objects.get_for_model(Site) + CHOICES = ('A', 'B', 'C') + + cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT) + cf_text.content_types.set([obj_type]) + + cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER) + cf_integer.content_types.set([obj_type]) + + cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN) + cf_boolean.content_types.set([obj_type]) + + cf_date = CustomField.objects.create(name='date', type=CustomFieldTypeChoices.TYPE_DATE) + cf_date.content_types.set([obj_type]) + + cf_url = CustomField.objects.create(name='url', type=CustomFieldTypeChoices.TYPE_URL) + cf_url.content_types.set([obj_type]) + + cf_select = CustomField.objects.create(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=CHOICES) + cf_select.content_types.set([obj_type]) + + cf_multiselect = CustomField.objects.create(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, + choices=CHOICES) + cf_multiselect.content_types.set([obj_type]) + + def test_empty_values(self): + """ + Test that empty custom field values are stored as null + """ + form = SiteForm({ + 'name': 'Site 1', + 'slug': 'site-1', + 'status': 'active', + }) + self.assertTrue(form.is_valid()) + instance = form.save() + + for field_type, _ in CustomFieldTypeChoices.CHOICES: + self.assertIn(field_type, instance.custom_field_data) + self.assertIsNone(instance.custom_field_data[field_type])