Bläddra i källkod

Fixes #6686: Force assignment of null custom field values to objects

jeremystretch 4 år sedan
förälder
incheckning
9b0258fef4

+ 1 - 0
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
 
 ---
 

+ 19 - 8
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()
 

+ 10 - 1
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)
 
 
 #

+ 8 - 2
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()