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