Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
28 changes: 27 additions & 1 deletion netbox/netbox/views/generic/object_views.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
#
Expand All @@ -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):
Expand All @@ -343,13 +367,15 @@ 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),
})

return render(request, self.template_name, {
'object': obj,
'form': form,
'return_url': self.get_return_url(request, obj),
'dependent_objects': dependent_objects,
**self.get_extra_context(request, obj),
})

Expand Down
34 changes: 34 additions & 0 deletions netbox/templates/htmx/delete_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,40 @@ <h5 class="modal-title">{% trans "Confirm Deletion" %}</h5>
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">
Expand Down