Skip to content
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

Open
davidn opened this issue Oct 22, 2013 · 16 comments · Fixed by #195 · May be fixed by #1169
Open

View deleted instances from admin page #72

davidn opened this issue Oct 22, 2013 · 16 comments · Fixed by #195 · May be fixed by #1169

Comments

@davidn
Copy link

davidn commented Oct 22, 2013

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.

@treyhunner
Copy link
Member

History for all instances (including deleted ones) can be queried on the model class like this:

Choice.history.filter(history_type='-')  # Deleted choices

@treyhunner
Copy link
Member

I agree that this might be a useful feature for the admin interface.

Pull requests welcome.

@matheusjardimb
Copy link

+1 !

@macro1
Copy link
Collaborator

macro1 commented Dec 5, 2015

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.

@macro1 macro1 reopened this Dec 5, 2015
@Visgean
Copy link

Visgean commented Jun 13, 2017

Any progress on this?

@treyhunner
Copy link
Member

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. 😄

@Visgean
Copy link

Visgean commented Jun 14, 2017

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 :).

@peterretief
Copy link

The revert_url() method allows the admin to see deleted instances

@marco-silva0000
Copy link

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

@marco-silva0000
Copy link

marco-silva0000 commented Mar 2, 2021

The above doesn't quite work because the list_display urls will still point to /model/<id>/ and not /model/<id>/history.
Any pointers on how to get that to work?

If this were to be implemented on this lib, some logic could be added on the SimpleHistoryAdmin class to change the normal url.

Edit:
Hacked my way into a crude solution, not sure if this is the best way to do it, but it works.
add this on your ModelAdmin.

    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:
There was an issue with the above hack where it would still show you the history route so I made a change to it that seems to be working now:

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

@ijharulislam
Copy link

HI
Any update on this issue?

@rhaver
Copy link

rhaver commented Apr 5, 2022

Collating the snippets by @marco-silva0000, you can define a new admin type SimpleHistoryWithDeletedAdmin. Any model admin class that derives from it includes the filter that lists deleted objects.

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)
    ]

@vidz1979
Copy link

Works like a charm, @rhaver ! Hope this solution can be shipped out of the box!

@duebbert
Copy link

@rhaver's solution didn't work for me in Django 4.0.4. I think the ChangeList.apply_select_related() method might hav changed.

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)
        ]

@jurrian
Copy link

jurrian commented Apr 24, 2023

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 url_for_result() to refer to the instance history instead.

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:

image

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:

{# file: templates/admin/change_list.html #}

{% extends 'admin/change_list.html' %}
{% load i18n admin_urls %}

{% block object-tools-items %}
  {% if cl.pk_attname != 'history_id' %}
  <li>
    {% url opts|admin_urlname:'changelist_history' as history_url %}
    <a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
  </li>
  {% endif %}
  {{ block.super }}
{% endblock %}

mjsir911 pushed a commit to mjsir911/django-simple-history that referenced this issue Apr 27, 2023
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).
@mjsir911 mjsir911 linked a pull request Apr 27, 2023 that will close this issue
11 tasks
mjsir911 pushed a commit to mjsir911/django-simple-history that referenced this issue Apr 27, 2023
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).
mjsir911 pushed a commit to mjsir911/django-simple-history that referenced this issue Jul 28, 2023
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).
mjsir911 pushed a commit to mjsir911/django-simple-history that referenced this issue Jul 28, 2023
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).
mjsir911 pushed a commit to terrapower/django-simple-history that referenced this issue Aug 18, 2023
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).
@THAN05
Copy link

THAN05 commented Dec 1, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment