diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 422438539..eac481bef 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -116,6 +116,17 @@ class CustomField(models.Model): def __str__(self): return self.label or self.name.replace('_', ' ').capitalize() + def remove_stale_data(self, content_types): + """ + Delete custom field data which is no longer relevant (either because the CustomField is + no longer assigned to a model, or because it has been deleted). + """ + for ct in content_types: + model = ct.model_class() + for obj in model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False}): + del(obj.custom_field_data[self.name]) + obj.save() + def clean(self): # Choices can be set only on selection fields if self.choices and self.type != CustomFieldTypeChoices.TYPE_SELECT: diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index e10c41d34..d4e187b5c 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -3,12 +3,14 @@ from datetime import timedelta from cacheops.signals import cache_invalidated, cache_read from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.db.models.signals import m2m_changed, pre_delete from django.utils import timezone from django_prometheus.models import model_deletes, model_inserts, model_updates from prometheus_client import Counter from .choices import ObjectChangeActionChoices -from .models import ObjectChange +from .models import CustomField, ObjectChange from .webhooks import enqueue_webhooks @@ -71,6 +73,29 @@ def _handle_deleted_object(request, sender, instance, **kwargs): model_deletes.labels(instance._meta.model_name).inc() +# +# Custom fields +# + +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. + """ + if action == 'post_remove': + instance.remove_stale_data(ContentType.objects.filter(pk__in=pk_set)) + + +def handle_cf_deleted(instance, **kwargs): + """ + Handle the cleanup of old custom field data when a CustomField is deleted. + """ + instance.remove_stale_data(instance.obj_type.all()) + + +m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.obj_type.through) +pre_delete.connect(handle_cf_deleted, sender=CustomField) + + # # Caching #