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 298a7860b2.

* 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>
This commit is contained in:
Jamie (Bear) Murphy 2023-11-01 19:13:45 +00:00 committed by GitHub
parent 944008d475
commit f6338abf14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 61 additions and 1 deletions

View File

@ -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),
})

View File

@ -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">