Fix on delete cascade entity order

Since [#20708](https://github.com/netbox-community/netbox/pull/20708)
relation with a on delete RESTRICT are not deleted in the proper order.
Then the error `violate not-null constraint` occurs and breaks the
delete cascade feature.
This commit is contained in:
Vincent Simonin 2025-12-08 17:06:13 +01:00
parent 269112a565
commit 605c61ef5b
No known key found for this signature in database

View File

@ -3,7 +3,7 @@ from threading import local
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db.models import CASCADE from django.db.models import CASCADE, RESTRICT
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete
from django.dispatch import receiver, Signal from django.dispatch import receiver, Signal
@ -47,6 +47,7 @@ clear_events = Signal()
# Object types # Object types
# #
@receiver(post_migrate) @receiver(post_migrate)
def update_object_types(sender, **kwargs): def update_object_types(sender, **kwargs):
""" """
@ -133,7 +134,7 @@ def handle_changed_object(sender, instance, **kwargs):
prev_change := ObjectChange.objects.filter( prev_change := ObjectChange.objects.filter(
changed_object_type=ContentType.objects.get_for_model(instance), changed_object_type=ContentType.objects.get_for_model(instance),
changed_object_id=instance.pk, changed_object_id=instance.pk,
request_id=request.id request_id=request.id,
).first() ).first()
): ):
prev_change.postchange_data = objectchange.postchange_data prev_change.postchange_data = objectchange.postchange_data
@ -172,9 +173,7 @@ def handle_deleted_object(sender, instance, **kwargs):
try: try:
run_validators(instance, validators) run_validators(instance, validators)
except ValidationError as e: except ValidationError as e:
raise AbortRequest( raise AbortRequest(_("Deletion is prevented by a protection rule: {message}").format(message=e))
_("Deletion is prevented by a protection rule: {message}").format(message=e)
)
# Get the current request, or bail if not set # Get the current request, or bail if not set
request = current_request.get() request = current_request.get()
@ -221,7 +220,12 @@ def handle_deleted_object(sender, instance, **kwargs):
obj.snapshot() # Ensure the change record includes the "before" state obj.snapshot() # Ensure the change record includes the "before" state
if type(relation) is ManyToManyRel: if type(relation) is ManyToManyRel:
getattr(obj, related_field_name).remove(instance) getattr(obj, related_field_name).remove(instance)
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete is not CASCADE: elif (
type(relation) is ManyToOneRel
and relation.null
and relation.on_delete is not CASCADE
and relation.on_delete is not RESTRICT
):
setattr(obj, related_field_name, None) setattr(obj, related_field_name, None)
obj.save() obj.save()
@ -256,6 +260,7 @@ def clear_events_queue(sender, **kwargs):
# DataSource handlers # DataSource handlers
# #
@receiver(post_save, sender=DataSource) @receiver(post_save, sender=DataSource)
def enqueue_sync_job(instance, created, **kwargs): def enqueue_sync_job(instance, created, **kwargs):
""" """
@ -267,9 +272,10 @@ def enqueue_sync_job(instance, created, **kwargs):
SyncDataSourceJob.enqueue_once(instance, interval=instance.sync_interval) SyncDataSourceJob.enqueue_once(instance, interval=instance.sync_interval)
elif not created: elif not created:
# Delete any previously scheduled recurring jobs for this DataSource # Delete any previously scheduled recurring jobs for this DataSource
for job in SyncDataSourceJob.get_jobs(instance).defer('data').filter( for job in (
interval__isnull=False, SyncDataSourceJob.get_jobs(instance)
status=JobStatusChoices.STATUS_SCHEDULED .defer('data')
.filter(interval__isnull=False, status=JobStatusChoices.STATUS_SCHEDULED)
): ):
# Call delete() per instance to ensure the associated background task is deleted as well # Call delete() per instance to ensure the associated background task is deleted as well
job.delete() job.delete()