2
0
Эх сурвалжийг харах

Fixes #5652: Update object data when renaming a custom field

jeremystretch 4 жил өмнө
parent
commit
92df40a6a0

+ 1 - 0
docs/release-notes/version-2.10.md

@@ -12,6 +12,7 @@
 ### Bug Fixes
 ### Bug Fixes
 
 
 * [#5419](https://github.com/netbox-community/netbox/issues/5419) - Update parent device/VM when deleting a primary IP
 * [#5419](https://github.com/netbox-community/netbox/issues/5419) - Update parent device/VM when deleting a primary IP
+* [#5652](https://github.com/netbox-community/netbox/issues/5652) - Update object data when renaming a custom field
 * [#6056](https://github.com/netbox-community/netbox/issues/6056) - Optimize change log cleanup
 * [#6056](https://github.com/netbox-community/netbox/issues/6056) - Optimize change log cleanup
 * [#6144](https://github.com/netbox-community/netbox/issues/6144) - Fix MAC address field display in VM interfaces search form
 * [#6144](https://github.com/netbox-community/netbox/issues/6144) - Fix MAC address field display in VM interfaces search form
 * [#6152](https://github.com/netbox-community/netbox/issues/6152) - Fix custom field filtering for cables, virtual chassis
 * [#6152](https://github.com/netbox-community/netbox/issues/6152) - Fix custom field filtering for cables, virtual chassis

+ 18 - 0
netbox/extras/models/customfields.py

@@ -162,6 +162,24 @@ class CustomField(models.Model):
     def __str__(self):
     def __str__(self):
         return self.label or self.name.replace('_', ' ').capitalize()
         return self.label or self.name.replace('_', ' ').capitalize()
 
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # 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):
+        """
+        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 remove_stale_data(self, content_types):
     def remove_stale_data(self, content_types):
         """
         """
         Delete custom field data which is no longer relevant (either because the CustomField is
         Delete custom field data which is no longer relevant (either because the CustomField is

+ 10 - 1
netbox/extras/signals.py

@@ -5,7 +5,7 @@ from cacheops.signals import cache_invalidated, cache_read
 from django.conf import settings
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.db import DEFAULT_DB_ALIAS
 from django.db import DEFAULT_DB_ALIAS
-from django.db.models.signals import m2m_changed, pre_delete
+from django.db.models.signals import m2m_changed, post_save, pre_delete
 from django.utils import timezone
 from django.utils import timezone
 from django_prometheus.models import model_deletes, model_inserts, model_updates
 from django_prometheus.models import model_deletes, model_inserts, model_updates
 from prometheus_client import Counter
 from prometheus_client import Counter
@@ -86,6 +86,14 @@ def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):
         instance.remove_stale_data(ContentType.objects.filter(pk__in=pk_set))
         instance.remove_stale_data(ContentType.objects.filter(pk__in=pk_set))
 
 
 
 
+def handle_cf_renamed(instance, created, **kwargs):
+    """
+    Handle the renaming of custom field data on objects when a CustomField is renamed.
+    """
+    if not created and instance.name != instance._name:
+        instance.rename_object_data(old_name=instance._name, new_name=instance.name)
+
+
 def handle_cf_deleted(instance, **kwargs):
 def handle_cf_deleted(instance, **kwargs):
     """
     """
     Handle the cleanup of old custom field data when a CustomField is deleted.
     Handle the cleanup of old custom field data when a CustomField is deleted.
@@ -94,6 +102,7 @@ def handle_cf_deleted(instance, **kwargs):
 
 
 
 
 m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)
 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)
 pre_delete.connect(handle_cf_deleted, sender=CustomField)
 
 
 
 

+ 27 - 0
netbox/extras/tests/test_customfields.py

@@ -91,6 +91,33 @@ class CustomFieldTest(TestCase):
         # Delete the custom field
         # Delete the custom field
         cf.delete()
         cf.delete()
 
 
+    def test_rename_customfield(self):
+        obj_type = ContentType.objects.get_for_model(Site)
+        FIELD_DATA = 'abc'
+
+        # Create a custom field
+        cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
+        cf.save()
+        cf.content_types.set([obj_type])
+
+        # Assign custom field data to an object
+        site = Site.objects.create(
+            name='Site 1',
+            slug='site-1',
+            custom_field_data={'field1': FIELD_DATA}
+        )
+        site.refresh_from_db()
+        self.assertEqual(site.custom_field_data['field1'], FIELD_DATA)
+
+        # Rename the custom field
+        cf.name = 'field2'
+        cf.save()
+
+        # Check that custom field data on the object has been updated
+        site.refresh_from_db()
+        self.assertNotIn('field1', site.custom_field_data)
+        self.assertEqual(site.custom_field_data['field2'], FIELD_DATA)
+
 
 
 class CustomFieldManagerTest(TestCase):
 class CustomFieldManagerTest(TestCase):