Source code for safedelete.admin

from __future__ import unicode_literals

import django
from django.contrib import admin, messages
from django.contrib.admin import helpers
from django.contrib.admin.models import CHANGE, LogEntry
from django.contrib.admin.utils import model_ngettext
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db.models import F
from django.template.response import TemplateResponse
from django.utils.encoding import force_str
from django.utils.html import conditional_escape, format_html
from django.utils.translation import gettext_lazy as _
from pkg_resources import parse_version

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

# Django 3.0 compatibility
try:
    from django.utils.six import text_type  # type: ignore
except ImportError:
    text_type = str


def highlight_deleted(obj):
    """
        Display in red lines when object is deleted.
    """
    obj_str = conditional_escape(text_type(obj))
    if not getattr(obj, FIELD_NAME, False):
        return obj_str
    else:
        return format_html('<span class="deleted">{0}</span>', obj_str)


highlight_deleted.short_description = _("Name")  # type: ignore


class SafeDeleteAdminFilter(admin.SimpleListFilter):
    """
        Filters objects by whether or not they have been deleted
    """
    title = _("Deleted")
    parameter_name = FIELD_NAME

    def lookups(self, request, model_admin):
        lookups = (
            (self.parameter_name, _('All (Including Deleted)')),
            (self.parameter_name + "_only", _('Deleted Only')),
        )
        return lookups

    def queryset(self, request, queryset):
        parameter_is_null = True
        if self.value() == self.parameter_name:
            return queryset
        elif self.value() == self.parameter_name + "_only":
            parameter_is_null = False
        return queryset.filter(**{self.parameter_name + '__isnull': parameter_is_null})


[docs]class SafeDeleteAdmin(admin.ModelAdmin): """ An abstract ModelAdmin which will include deleted objects in its listing. :Example: >>> from safedelete.admin import SafeDeleteAdmin, SafeDeleteAdminFilter, highlight_deleted >>> class ContactAdmin(SafeDeleteAdmin): ... list_display = (highlight_deleted, "highlight_deleted_field", "first_name", "last_name", "email") + SafeDeleteAdmin.list_display ... list_filter = ("last_name", SafeDeleteAdminFilter,) + SafeDeleteAdmin.list_filter ... ... field_to_highlight = "id" ... ... 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', 'hard_delete_soft_deleted') class Meta: abstract = True class Media: css = { 'all': ('safedelete/admin.css',), } def queryset(self, request): # Deprecated in latest Django versions return self.get_queryset(request) def get_queryset(self, request): try: queryset = self.model.all_objects.all() except Exception: queryset = self.model._default_manager.all() if self.field_to_highlight: queryset = queryset.annotate(_highlighted_field=F(self.field_to_highlight)) ordering = self.get_ordering(request) if ordering: queryset = queryset.order_by(*ordering) return queryset def log_undeletion(self, request, obj, object_repr): """ Log that an object will be undeleted. The default implementation creates an admin LogEntry object. """ LogEntry.objects.log_action( user_id=request.user.pk, content_type_id=ContentType.objects.get_for_model(self.model).pk, object_id=obj.pk, object_repr=object_repr, action_flag=CHANGE ) def undelete_selected(self, request, queryset): """ Admin action to undelete objects in bulk with confirmation. """ if not self.has_delete_permission(request): raise PermissionDenied assert hasattr(queryset, 'undelete') # Remove not deleted item from queryset queryset = queryset.filter(**{FIELD_NAME + '__isnull': False}) # Undeletion confirmed if request.POST.get('post'): requested = queryset.count() if requested: for obj in queryset: obj_display = force_str(obj) self.log_undeletion(request, obj, obj_display) changed = queryset.undelete()[0] if changed < requested: self.message_user( request, _("Successfully undeleted %(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 undeleted %(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(queryset) == 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 queryset] context = { 'title': title, 'objects_name': objects_name, 'queryset': queryset, "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.undelete_selected_confirmation_template, context, current_app=self.admin_site.name, ) else: return TemplateResponse( request, 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, } 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) except TypeError: raise ValueError("Must set field_to_highlight to your field's name (as a string)") field_str = conditional_escape(text_type(field_str)) if not getattr(obj, FIELD_NAME, False): return field_str else: return format_html('<span class="deleted">{0}</span>', field_str) field_to_highlight = None highlight_deleted_field.short_description = _("Override this name (see docs)") # type: ignore 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