From a8a86f1d99ed48f5da26e0b773f05b5ddfa2ea6b Mon Sep 17 00:00:00 2001 From: Sean Lee <81959287+Xalph555@users.noreply.github.com> Date: Sun, 30 Jul 2023 17:18:51 +1000 Subject: [PATCH 1/3] Add hard_delete_soft_deleted action to SafeDeleteAdmin --- safedelete/admin.py | 78 ++++++++++++++++++- .../hard_delete_selected_confirmation.html | 23 ++++++ safedelete/tests/test_admin.py | 20 +++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 safedelete/templates/safedelete/hard_delete_selected_confirmation.html diff --git a/safedelete/admin.py b/safedelete/admin.py index 19946c0..26193ca 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,12 @@ 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 @@ -193,6 +196,78 @@ def undelete_selected(self, request, queryset): self.undelete_selected_confirmation_template, 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, + } + + return TemplateResponse( + request, + self.hard_delete_selected_confirmation_template, + context, + ) def highlight_deleted_field(self, obj): try: @@ -211,3 +286,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") 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..daa8f2b 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) \ No newline at end of file From 47c349ac9ed21a1e22f0d3342ba457988f11ae23 Mon Sep 17 00:00:00 2001 From: Sean Lee <81959287+Xalph555@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:19:56 +1000 Subject: [PATCH 2/3] Fix Flake8 formatting errors --- safedelete/admin.py | 3 +-- safedelete/tests/test_admin.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/safedelete/admin.py b/safedelete/admin.py index 26193ca..9258050 100644 --- a/safedelete/admin.py +++ b/safedelete/admin.py @@ -79,7 +79,6 @@ class SafeDeleteAdmin(admin.ModelAdmin): """ 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,) @@ -196,7 +195,7 @@ def undelete_selected(self, request, queryset): self.undelete_selected_confirmation_template, context, ) - + def hard_delete_soft_deleted(self, request, queryset): """Admin action to hard delete soft deleted records""" diff --git a/safedelete/tests/test_admin.py b/safedelete/tests/test_admin.py index daa8f2b..b80ae60 100644 --- a/safedelete/tests/test_admin.py +++ b/safedelete/tests/test_admin.py @@ -177,4 +177,4 @@ def test_admin_hard_delete_soft_deleted_action(self): }) with self.assertRaises(Category.DoesNotExist): - Category.objects.get(pk=self.categories[1].pk) \ No newline at end of file + Category.objects.get(pk=self.categories[1].pk) From 52a78678d074226956b3c16977c5cacf0df76ef6 Mon Sep 17 00:00:00 2001 From: Sean Lee <81959287+Xalph555@users.noreply.github.com> Date: Tue, 29 Aug 2023 20:36:52 +1000 Subject: [PATCH 3/3] Update admin.py --- safedelete/admin.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/safedelete/admin.py b/safedelete/admin.py index 9258050..ec32051 100644 --- a/safedelete/admin.py +++ b/safedelete/admin.py @@ -262,11 +262,19 @@ def hard_delete_soft_deleted(self, request, queryset): "related_list": related_list, } - return TemplateResponse( - request, - self.hard_delete_selected_confirmation_template, - context, - ) + 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: @@ -285,4 +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") + hard_delete_soft_deleted.short_description = _("Hard delete selected soft deleted %(verbose_name_plural)s") # type: ignore