From 9b0258fef4131461a7c82c60c99e5259df9c7b27 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 16 Aug 2021 14:38:06 -0400 Subject: [PATCH] Fixes #6686: Force assignment of null custom field values to objects --- docs/release-notes/version-2.11.md | 1 + netbox/extras/models/customfields.py | 27 +++++++++++++++++------- netbox/extras/signals.py | 11 +++++++++- netbox/extras/tests/test_customfields.py | 10 +++++++-- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 1379a9de1..a3061d866 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -10,6 +10,7 @@ ### Bug Fixes * [#5968](https://github.com/netbox-community/netbox/issues/5968) - Model forms should save empty custom field values as null +* [#6686](https://github.com/netbox-community/netbox/issues/6686) - Force assignment of null custom field values to objects --- diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index a433a3f81..089621f92 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -120,17 +120,16 @@ class CustomField(BigIDModel): # Cache instance's original name so we can check later whether it has changed self._name = self.name - def rename_object_data(self, old_name, new_name): + def populate_initial_data(self, content_types): """ - Called when a CustomField has been renamed. Updates all assigned object data. + Populate initial custom field data upon either a) the creation of a new CustomField, or + b) the assignment of an existing CustomField to new object types. """ - for ct in self.content_types.all(): + for ct in content_types: model = ct.model_class() - params = {f'custom_field_data__{old_name}__isnull': False} - instances = model.objects.filter(**params) - for instance in instances: - instance.custom_field_data[new_name] = instance.custom_field_data.pop(old_name) - model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100) + for obj in model.objects.exclude(**{f'custom_field_data__contains': self.name}): + obj.custom_field_data[self.name] = self.default + obj.save() def remove_stale_data(self, content_types): """ @@ -143,6 +142,18 @@ class CustomField(BigIDModel): del(obj.custom_field_data[self.name]) obj.save() + def rename_object_data(self, old_name, new_name): + """ + Called when a CustomField has been renamed. Updates all assigned object data. + """ + for ct in self.content_types.all(): + model = ct.model_class() + params = {f'custom_field_data__{old_name}__isnull': False} + instances = model.objects.filter(**params) + for instance in instances: + instance.custom_field_data[new_name] = instance.custom_field_data.pop(old_name) + model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100) + def clean(self): super().clean() diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index 2fc292294..e68b19a89 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -108,6 +108,14 @@ def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs): # Custom fields # +def handle_cf_added_obj_types(instance, action, pk_set, **kwargs): + """ + Handle the population of default/null values when a CustomField is added to one or more ContentTypes. + """ + if action == 'post_add': + instance.populate_initial_data(ContentType.objects.filter(pk__in=pk_set)) + + def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs): """ Handle the cleanup of old custom field data when a CustomField is removed from one or more ContentTypes. @@ -131,9 +139,10 @@ def handle_cf_deleted(instance, **kwargs): instance.remove_stale_data(instance.content_types.all()) -m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through) post_save.connect(handle_cf_renamed, sender=CustomField) pre_delete.connect(handle_cf_deleted, sender=CustomField) +m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.content_types.through) +m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through) # diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index c14424ba6..b1bf10be6 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -42,8 +42,11 @@ class CustomFieldTest(TestCase): cf.save() cf.content_types.set([obj_type]) - # Assign a value to the first Site + # Check that the field has a null initial value site = Site.objects.first() + self.assertIsNone(site.custom_field_data[cf.name]) + + # Assign a value to the first Site site.custom_field_data[cf.name] = data['field_value'] site.save() @@ -73,8 +76,11 @@ class CustomFieldTest(TestCase): cf.save() cf.content_types.set([obj_type]) - # Assign a value to the first Site + # Check that the field has a null initial value site = Site.objects.first() + self.assertIsNone(site.custom_field_data[cf.name]) + + # Assign a value to the first Site site.custom_field_data[cf.name] = 'Option A' site.save()