Skip to content

Commit

Permalink
Closes #13690: List all objects to be deleted (#14089)
Browse files Browse the repository at this point in the history
* 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 298a786.

* 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>
  • Loading branch information
ITJamie and jeremystretch authored Nov 1, 2023
1 parent 944008d commit f6338ab
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 1 deletion.
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

0 comments on commit f6338ab

Please sign in to comment.