Skip to content

Commit

Permalink
Add Hard Delete Action to SafeDeleteAdmin (#236)
Browse files Browse the repository at this point in the history
  • Loading branch information
Xalph555 committed Aug 30, 2023
1 parent 6cdd3e8 commit a0fa764
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 1 deletion.
85 changes: 84 additions & 1 deletion safedelete/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from .config import FIELD_NAME
from .utils import related_objects
from .models import HARD_DELETE

# Django 3.0 compatibility
try:
Expand Down Expand Up @@ -77,10 +78,11 @@ class SafeDeleteAdmin(admin.ModelAdmin):
... ContactAdmin.highlight_deleted_field.short_description = ContactAdmin.field_to_highlight
"""
undelete_selected_confirmation_template = "safedelete/undelete_selected_confirmation.html"
hard_delete_selected_confirmation_template = "safedelete/hard_delete_selected_confirmation.html"

list_display = (FIELD_NAME,)
list_filter = (FIELD_NAME,)
actions = ('undelete_selected',)
actions = ('undelete_selected', 'hard_delete_soft_deleted')

class Meta:
abstract = True
Expand Down Expand Up @@ -194,6 +196,86 @@ def undelete_selected(self, request, queryset):
context,
)

def hard_delete_soft_deleted(self, request, queryset):
"""Admin action to hard delete soft deleted records"""

if not self.has_delete_permission(request):
raise PermissionDenied

# Remove not deleted items from queryset
objects_marked_for_deletion = queryset.filter(
**{FIELD_NAME + "__isnull": False}
)

# Confirmation of hard deletion of selected items
if request.POST.get("post"):
requested = objects_marked_for_deletion.count()
if requested:
changed = objects_marked_for_deletion.delete(force_policy=HARD_DELETE)[
0
]
if changed < requested:
self.message_user(
request,
_(
"Successfully hard deleted %(count_changed)d of the "
"%(count_requested)d selected %(items)s."
)
% {
"count_requested": requested,
"count_changed": changed,
"items": model_ngettext(self.opts, requested),
},
messages.WARNING,
)
else:
self.message_user(
request,
_("Successfully hard deleted %(count)d %(items)s.")
% {
"count": requested,
"items": model_ngettext(self.opts, requested),
},
messages.SUCCESS,
)
# Return None to display the change list page again.
return None

opts = self.model._meta
if len(objects_marked_for_deletion) == 1:
objects_name = force_str(opts.verbose_name)
else:
objects_name = force_str(opts.verbose_name_plural)
title = _("Are you sure?")

related_list = [
list(related_objects(obj)) for obj in objects_marked_for_deletion
]

context = {
"title": title,
"objects_name": objects_name,
"queryset": objects_marked_for_deletion,
"opts": opts,
"app_label": opts.app_label,
"action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
"related_list": related_list,
}

if parse_version(django.get_version()) < parse_version('1.10'):
return TemplateResponse(
request,
self.hard_delete_selected_confirmation_template,
context,
current_app=self.admin_site.name,
)
else:
return TemplateResponse(
request,
self.hard_delete_selected_confirmation_template,
context,
)

def highlight_deleted_field(self, obj):
try:
field_str = getattr(obj, self.field_to_highlight)
Expand All @@ -211,3 +293,4 @@ def highlight_deleted_field(self, obj):
highlight_deleted_field.admin_order_field = "_highlighted_field" # type: ignore

undelete_selected.short_description = _("Undelete selected %(verbose_name_plural)s") # type: ignore
hard_delete_soft_deleted.short_description = _("Hard delete selected soft deleted %(verbose_name_plural)s") # type: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% extends "admin/delete_selected_confirmation.html" %}
{% load i18n l10n %}

{% block content %}
<p>{% blocktrans %}Are you sure you want to hard delete the selected {{ objects_name }}?{% endblocktrans %}</p>
<ul>{{ queryset|unordered_list }}</ul>
<form action="" method="post">{% csrf_token %}
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %}

<p>{% blocktrans %}Related objects{% endblocktrans %}</p>
{% for related in related_list %}
<ul>{{ related | unordered_list }}</ul>
{% endfor %}

<input type="hidden" name="action" value="hard_delete_soft_deleted" />
<input type="hidden" name="post" value="yes" />
<input type="submit" value="{% trans "Yes, I'm sure" %}" />
</div>
</form>
{% endblock %}
20 changes: 20 additions & 0 deletions safedelete/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,23 @@ def test_admin_undelete_action(self):
pk=self.categories[1].pk
)
self.assertFalse(getattr(category, FIELD_NAME))

def test_admin_hard_delete_soft_deleted_action(self):
"""Test objects are hard deleted and action is logged."""
resp = self.client.post('/admin/safedelete/category/', data={
'index': 0,
'action': ['hard_delete_soft_deleted'],
'_selected_action': [self.categories[1].pk],
})
self.assertTemplateUsed(resp, 'safedelete/hard_delete_selected_confirmation.html')
self.assertTrue(getattr(self.categories[1], FIELD_NAME))

resp = self.client.post('/admin/safedelete/category/', data={
'index': 0,
'action': ['hard_delete_soft_deleted'],
'post': True,
'_selected_action': [self.categories[1].pk],
})

with self.assertRaises(Category.DoesNotExist):
Category.objects.get(pk=self.categories[1].pk)

0 comments on commit a0fa764

Please sign in to comment.