Repository: netbox-community/netbox

Issue:
when deleteing an object in webui. list the related objects that would also be deleted
### NetBox version

v3.6.0

### Feature type

Change to existing functionality

### Proposed functionality

when choosing delete on an object (eg device for this example). the popup that "asks are you sure you want to delete device xxx" should be extended to show the list of related objects that will also be deleted (interfaces, cables, etc)

### Use case

some users are not aware that deleting an object will remove some other related objects.
showing the user what additional objects will deleted would be a fantastic safety feature.

### Database changes

n/a

### External dependencies

n/a


Discussion:
related slack convo: https://netdev-community.slack.com/archives/C01P0FRSXRV/p1693924009439029
another option to consider - the delete modal popup could require typing the “name” of the object to enable the delete button on the modal. (I’m personally not a fan of that method, but it is a fairly standard way of helping to stop erroneous deletions in webapps)
It looks like something similar was proposed in #8551, but never got an owner. I see this as also related to #6015, which is still a problem in our organization, as the current behavior is unexpected for our engineers.
> another option to consider - the delete modal popup could require typing the “name” of the object to enable the delete button on the modal. (I’m personally not a fan of that method, but it is a fairly standard way of helping to stop erroneous deletions in webapps)

I don't like it because it doesn't address the core problem, which is making sure a user understands that deleting a device will also potentially delete all related interfaces and cables.

However, one thing that occurrs to me, instead of implementing this in core NetBox, could a similar effect be acheived with a custom validator? The custom validator could for example reject any deletions of any devices that have cables connected to them, requiring the user to disconnect all cables in NetBox before deleting the object. This is similar to how some network vendors set up their equipment, making it impossible to delete configuration object that have other dependent objects.
> instead of implementing this in core NetBox, could a similar effect be acheived with a custom validator?

I just checked and no, unfortunately the custom validator only triggers when editing an object, it doesn't trigger on object deletion.

Maybe an alternative way to implement this could be to extend the concept of custom validators to also call a method on them prior to deletion?

Resolving patch:
diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py
--- a/netbox/netbox/views/generic/object_views.py
+++ b/netbox/netbox/views/generic/object_views.py
@@ -1,9 +1,11 @@
 import logging
+from collections import defaultdict
 from copy import deepcopy
 
 from django.contrib import messages
-from django.db import transaction
+from django.db import router, transaction
 from django.db.models import ProtectedError, RestrictedError
+from django.db.models.deletion import Collector
 from django.shortcuts import redirect, render
 from django.urls import reverse
 from django.utils.html import escape
@@ -320,6 +322,27 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'delete')
 
+    def _get_dependent_objects(self, obj):
+        """
+        Returns a dictionary mapping of dependent objects (organized by model) which will be deleted as a result of
+        deleting the requested object.
+
+        Args:
+            obj: The object to return dependent objects for
+        """
+        using = router.db_for_write(obj._meta.model)
+        collector = Collector(using=using)
+        collector.collect([obj])
+
+        # Compile a mapping of models to instances
+        dependent_objects = defaultdict(list)
+        for model, instance in collector.instances_with_model():
+            # Omit the root object
+            if instance != obj:
+                dependent_objects[model].append(instance)
+
+        return dict(dependent_objects)
+
     #
     # Request handlers
     #
@@ -333,6 +356,7 @@ def get(self, request, *args, **kwargs):
         """
         obj = self.get_object(**kwargs)
         form = ConfirmationForm(initial=request.GET)
+        dependent_objects = self._get_dependent_objects(obj)
 
         # If this is an HTMX request, return only the rendered deletion form as modal content
         if is_htmx(request):
@@ -343,6 +367,7 @@ def get(self, request, *args, **kwargs):
                 'object_type': self.queryset.model._meta.verbose_name,
                 'form': form,
                 'form_url': form_url,
+                'dependent_objects': dependent_objects,
                 **self.get_extra_context(request, obj),
             })
 
@@ -350,6 +375,7 @@ def get(self, request, *args, **kwargs):
             'object': obj,
             'form': form,
             'return_url': self.get_return_url(request, obj),
+            'dependent_objects': dependent_objects,
             **self.get_extra_context(request, obj),
         })
 


Test patch: