diff --git a/safedelete/admin.py b/safedelete/admin.py index 19946c0..ec32051 100644 --- a/safedelete/admin.py +++ b/safedelete/admin.py @@ -16,6 +16,7 @@ from .config import FIELD_NAME from .utils import related_objects +from .models import HARD_DELETE # Django 3.0 compatibility try: @@ -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 @@ -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) @@ -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 diff --git a/safedelete/templates/safedelete/hard_delete_selected_confirmation.html b/safedelete/templates/safedelete/hard_delete_selected_confirmation.html new file mode 100644 index 0000000..fa1b756 --- /dev/null +++ b/safedelete/templates/safedelete/hard_delete_selected_confirmation.html @@ -0,0 +1,23 @@ +{% extends "admin/delete_selected_confirmation.html" %} +{% load i18n l10n %} + +{% block content %} +

{% blocktrans %}Are you sure you want to hard delete the selected {{ objects_name }}?{% endblocktrans %}

+ +
{% csrf_token %} +
+ {% for obj in queryset %} + + {% endfor %} + +

{% blocktrans %}Related objects{% endblocktrans %}

+ {% for related in related_list %} + + {% endfor %} + + + + +
+
+{% endblock %} \ No newline at end of file diff --git a/safedelete/tests/test_admin.py b/safedelete/tests/test_admin.py index a0ee5c2..b80ae60 100644 --- a/safedelete/tests/test_admin.py +++ b/safedelete/tests/test_admin.py @@ -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)