-
-
Notifications
You must be signed in to change notification settings - Fork 480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
View deleted instances from admin page #72
Comments
History for all instances (including deleted ones) can be queried on the model class like this:
|
I agree that this might be a useful feature for the admin interface. Pull requests welcome. |
+1 ! |
Reopening. I can see I closed this issue prematurely as only part was implemented. Currently histories can be seen for deleted instances, but the deleted instances themselves can't be easily queried. |
Any progress on this? |
None that I know of. Pull requests welcome, though I'm not entirely sure which part of this feature is still missing. A roadmap of what should probably be added where would probably very be useful for an ambitious future implementer. 😄 |
There is no link in admin for the list view of the deleted instances. We might need it for our project in future in which case I will definitely try to push into pull request here :). |
The revert_url() method allows the admin to see deleted instances |
As a PoC or a workaround I've created a django filter that will list the deleted entries. note that if you are using any custom methods on the model for the admin display, you need to use a History Base class for this to work(#311). class SimpleHistoryShowDeletedFilter(admin.SimpleListFilter):
title = "Entries"
parameter_name = "entries"
def lookups(self, request, model_admin):
return (
("deleted_only", "Only Deleted"),
)
def queryset(self, request, queryset):
if self.value():
return queryset.model.history.filter(history_type='-').distinct()
return queryset |
The above doesn't quite work because the list_display urls will still point to If this were to be implemented on this lib, some logic could be added on the Edit: def get_changelist(self, request, **kwargs):
def custom_url_for_result(self, result):
pk = getattr(result, self.pk_attname)
from django.urls import reverse
from django.contrib.admin.utils import quote
return reverse('admin:%s_%s_history' % (self.opts.app_label,
self.opts.model_name),
args=(quote(pk),),
current_app=self.model_admin.admin_site.name)
ChangeList = super(YourModelAdmin, self).get_changelist(request, **kwargs)
if request.GET.get('entries', None) == 'deleted_only':
ChangeList.url_for_result = custom_url_for_result
return ChangeList Edit 2: class YourModelAdmin(SimpleHistoryAdmin):
def get_changelist(self, request, **kwargs):
def url_from_result_maker(history=False):
def custom_url_for_result(self, result):
pk = getattr(result, self.pk_attname)
from django.urls import reverse
from django.contrib.admin.utils import quote
route_type = 'history' if history else 'change'
route = f"{self.opts.app_label}_{self.opts.model_name}_{route_type}"
return reverse(f'admin:{route}',
args=(quote(pk),),
current_app=self.model_admin.admin_site.name)
return custom_url_for_result
ChangeList = super(YourModelAdmin, self).get_changelist(request, **kwargs)
if request.GET.get('entries', None) == 'deleted_only':
ChangeList.url_for_result = url_from_result_maker(history=True)
else:
ChangeList.url_for_result = url_from_result_maker(history=False)
return ChangeList |
HI |
Collating the snippets by @marco-silva0000, you can define a new admin type from django.contrib import admin
from django.contrib.admin.utils import quote
from django.urls import reverse
from simple_history.admin import SimpleHistoryAdmin
class SimpleHistoryWithDeletedAdmin(SimpleHistoryAdmin):
class SimpleHistoryShowDeletedFilter(admin.SimpleListFilter):
title = "Entries"
parameter_name = "entries"
def lookups(self, request, model_admin):
return (
("deleted_only", "Only Deleted"),
)
def queryset(self, request, queryset):
if self.value():
return queryset.model.history.filter(history_type='-').distinct()
return queryset
def get_changelist(self, request, **kwargs):
def url_from_result_maker(history=False):
def custom_url_for_result(self, result):
pk = getattr(result, self.pk_attname)
route_type = 'history' if history else 'change'
route = f'{self.opts.app_label}_{self.opts.model_name}_{route_type}'
return reverse(f'admin:{route}',
args=(quote(pk),),
current_app=self.model_admin.admin_site.name)
return custom_url_for_result
changelist = super().get_changelist(request, **kwargs)
if request.GET.get('entries', None) == 'deleted_only':
changelist.url_for_result = url_from_result_maker(history=True)
else:
changelist.url_for_result = url_from_result_maker(history=False)
return changelist
def get_list_filter(self, request):
return [self.SimpleHistoryShowDeletedFilter] + [
f for f in super().get_list_filter(request)
] |
Works like a charm, @rhaver ! Hope this solution can be shipped out of the box! |
@rhaver's solution didn't work for me in Django 4.0.4. I think the Works with slight changes: from django.contrib import admin
from django.contrib.admin.utils import quote
from django.contrib.admin.views.main import ChangeList
from django.urls import reverse
from simple_history.admin import SimpleHistoryAdmin
class SimpleHistoryShowDeletedFilter(admin.SimpleListFilter):
title = "Entries"
parameter_name = "entries"
def lookups(self, request, model_admin):
return (
("deleted_only", "Only Deleted"),
)
def queryset(self, request, queryset):
if self.value():
return queryset.model.history.filter(history_type='-').distinct()
return queryset
class SimpleHistoryChangeList(ChangeList):
def apply_select_related(self, qs):
# Our qs is different if we use the history, so the normal select_related
# won't work and results in an empty QuerySet result.
history = self.params.get("entries", None) == "deleted_only"
if history:
return qs
return super().apply_select_related(qs)
def url_for_result(self, result) -> str:
history = self.params.get("entries", None) == "deleted_only"
route_type = "history" if history else "change"
route = f"{self.opts.app_label}_{self.opts.model_name}_{route_type}"
pk = getattr(result, self.pk_attname)
return reverse(
f"admin:{route}",
args=(quote(pk),),
current_app=self.model_admin.admin_site.name,
)
class SimpleHistoryWithDeletedAdmin(SimpleHistoryAdmin):
def get_changelist(self, request, **kwargs):
return SimpleHistoryChangeList
def get_list_filter(self, request):
# Doing it here will add it to every inherited class. Alternatively,
# add SimpleHistoryShowDeletedFilter to the list_filter and remove the below.
return [SimpleHistoryShowDeletedFilter] + [
f for f in super().get_list_filter(request)
] |
Instead of using filters I found a way to implement it directly in a separate admin page. It will use the normal admin ChangeList but uses the The benefits of using an extra admin page here is that you can apply filters, mixins and addons as you like. Here with DjangoQL package enabled: For more information on how to use, see here. from django.contrib.admin import ModelAdmin
from django.urls import path, reverse
from simple_history.admin import SimpleHistoryAdmin
def url_for_result(result):
"""Urls for history entries should go to the object's history page.
"""
meta = result.history_object._meta # pylint: disable=protected-access
return reverse(f'admin:{meta.app_label}_{meta.model_name}_history', args=[result.id])
class BaseHistoryAdmin(ModelAdmin):
"""Base class for django-simple-history admin classes, can be subclassed to add custom functionality.
"""
actions = None
list_filter = ('history_type',)
list_display = ('history_object', 'history_date', 'history_user', 'history_type', 'history_change_reason')
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
def get_changelist_instance(self, request):
"""Overrule `url_for_result` to point to the object's history page.
This cannot happen on `get_changelist()` because it might be changed by other packages like DjangoQL.
"""
changelist_instance = super().get_changelist_instance(request)
changelist_instance.url_for_result = url_for_result
return changelist_instance
def get_queryset(self, request):
"""Join the history user and prefetch if possible.
Without the `select_related('history_user')`, `ChangeList.apply_select_related()`
will call `select_related()` without parameters which will add a lot of joins.
Also try to prefetch the relation to the history object, so we can reduce some queries.
"""
qs = super().get_queryset(request)
if not isinstance(self.model.history_user, property):
qs = qs.select_related('history_user')
if hasattr(self.model, 'history_relation'):
qs = qs.prefetch_related('history_relation')
return qs
class WMSimpleHistoryAdmin(SimpleHistoryAdmin):
"""Allows to see all history entries on a model using an embedded ModelAdmin.
Adds a button in the changelist to <app_label>/<model_name>/history/
in addition to the already existing object-level history.
"""
history_admin = BaseHistoryAdmin
def get_history_admin_class(self):
"""Returns HistoryAdmin class for model, with `history_admin` as superclass.
Change this to tweak the admin class.
"""
return type(f'{self.model.__name__}HistoryAdmin', (self.history_admin,), {})
def get_urls(self):
"""Register additional "changelist history" to the admin urls.
"""
urls = super().get_urls()
admin_site = self.admin_site
opts = self.model._meta # pylint: disable=protected-access
try:
# pylint: disable=protected-access
model = getattr(self.model, self.model._meta.simple_history_manager_attribute).model
except AttributeError:
# History is not enabled, do nothing, just return urls
return urls
admin_class = self.get_history_admin_class()
model_admin_instance = admin_class(admin_site=admin_site, model=model)
history_urls = [
path(
'history/',
admin_site.admin_view(model_admin_instance.changelist_view),
name=f'{opts.app_label}_{opts.model_name}_changelist_history',
)
]
return history_urls + urls To add a button on the change list pages just before the Add button:
|
Note that if you are using any custom methods on the model for the admin display, you need to use a History Base class for this to work(jazzband#311).
Note that if you are using any custom methods on the model for the admin display, you need to use a History Base class for this to work(jazzband#311).
Note that if you are using any custom methods on the model for the admin display, you need to use a History Base class for this to work(jazzband#311).
Note that if you are using any custom methods on the model for the admin display, you need to use a History Base class for this to work(jazzband#311).
Note that if you are using any custom methods on the model for the admin display, you need to use a History Base class for this to work(jazzband#311).
I stumbled upon a way to view the history of deleted items from the Django admin panel, but you need to know the ID of the deleted item. The workaround starts by clicking on an existing object in the database in the admin pane and then clicking to view its history. Once you're on the history of an existing item, you can change the id in the URL to the deleted item id and you'll be able to view its full history. |
The
SimpleHistoryAdmin
allows the history of a instance that still exists to bee seen. It would be great if there would be a way to view a list of deleted instances, and see their histories.The text was updated successfully, but these errors were encountered: