diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 9c41edd2c..3755944dc 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -12,6 +12,7 @@ ### Bug Fixes * [#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 * [#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 diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index a69816d21..4f37d4870 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -162,6 +162,24 @@ class CustomField(models.Model): def __str__(self): 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): """ Delete custom field data which is no longer relevant (either because the CustomField is diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index 9eeb4ce45..3556f6fe8 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -5,7 +5,7 @@ from cacheops.signals import cache_invalidated, cache_read from django.conf import settings from django.contrib.contenttypes.models import ContentType 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_prometheus.models import model_deletes, model_inserts, model_updates 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)) +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): """ 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) +post_save.connect(handle_cf_renamed, sender=CustomField) pre_delete.connect(handle_cf_deleted, sender=CustomField) diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 4f7a67676..d1725ac9d 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -91,6 +91,33 @@ class CustomFieldTest(TestCase): # Delete the custom field 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):