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
30 changes: 28 additions & 2 deletions netbox/netbox/views/generic/object_views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
from copy import deepcopy
from collections import defaultdict

from django.contrib import messages
from django.db import transaction
from django.db import transaction, router
from django.db.models import ProtectedError
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 All @@ -15,7 +17,7 @@
from utilities.forms import ConfirmationForm, restrict_form_fields
from utilities.htmx import is_htmx
from utilities.permissions import get_permission_for_model
from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fields
from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fields, title
from utilities.views import GetReturnURLMixin
from .base import BaseObjectView
from .mixins import ActionsMixin, TableMixin
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