Quellcode durchsuchen

Closes #13690: List all objects to be deleted (#14089)

* show objects that would be deleted by cascade

* some items were not showing (eg ips on devices)

* dont include the item being deleted in the list of related items

* Revert "dont include the item being deleted in the list of related items"

This reverts commit 298a7860b20c2fd90e887c66c4f196460097e71e.

* cleanup

- migrate code to use collector directly instead of the NestedObjects wrapper from admin.utils

- adjust object names and text output

* requested adjustments

* remove comma from end of list

* linting

* refactor, add accordion

* migrate to defaultdict, use title for capitalisation of accordian titles

* Misc cleanup

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Jamie (Bear) Murphy vor 2 Jahren
Ursprung
Commit
f6338abf14
2 geänderte Dateien mit 61 neuen und 1 gelöschten Zeilen
  1. 27 1
      netbox/netbox/views/generic/object_views.py
  2. 34 0
      netbox/templates/htmx/delete_form.html

+ 27 - 1
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 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
         """
         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 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
                 '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 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
             'object': obj,
             'form': form,
             'return_url': self.get_return_url(request, obj),
+            'dependent_objects': dependent_objects,
             **self.get_extra_context(request, obj),
         })
 

+ 34 - 0
netbox/templates/htmx/delete_form.html

@@ -12,6 +12,40 @@
         Are you sure you want to <strong class="text-danger">delete</strong> {{ object_type }} <strong>{{ object }}</strong>?
       {% endblocktrans %}
     </p>
+    {% if dependent_objects %}
+      <p>
+        {% trans "The following objects will be deleted as a result of this action." %}
+      </p>
+      <div class="accordion" id="deleteAccordion">
+        {% for model, instances in dependent_objects.items %}
+          <div class="accordion-item">
+            <h2 class="accordion-header" id="deleteheading{{ forloop.counter }}">
+              <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ forloop.counter }}" aria-expanded="false" aria-controls="collapse{{ forloop.counter }}">
+                {% with object_count=instances|length %}
+                  {{ object_count }}
+                  {% if object_count == 1 %}
+                    {{ model|meta:"verbose_name" }}
+                  {% else %}
+                    {{ model|meta:"verbose_name_plural" }}
+                  {% endif %}
+                {% endwith %}
+              </button>
+            </h2>
+            <div id="collapse{{ forloop.counter }}" class="accordion-collapse collapse" aria-labelledby="deleteheading{{ forloop.counter }}" data-bs-parent="#deleteAccordion">
+              <div class="accordion-body p-0">
+                <div class="list-group list-group-flush">
+                  {% for instance in instances %}
+                    {% with url=instance.get_absolute_url %}
+                      <a {% if url %}href="{{ url }}" {% endif %}class="list-group-item list-group-item-action">{{ instance }}</a>
+                    {% endwith %}
+                  {% endfor %}
+                </div>
+              </div>
+            </div>
+          </div>
+        {% endfor %}
+      </div>
+    {% endif %}
     {% render_form form %}
   </div>
   <div class="modal-footer">