Skip to content

Instantly share code, notes, and snippets.

Last active April 24, 2023 12:59
Show Gist options
  • Save jurrian/a932b5064255602dcc6055f6475316ea to your computer and use it in GitHub Desktop.
Save jurrian/a932b5064255602dcc6055f6475316ea to your computer and use it in GitHub Desktop.
Django Simple History for list pages
{# 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' %}
{% url opts|admin_urlname:'changelist_history' as history_url %}
<a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
{% endif %}
{{ block.super }}
{% endblock %}
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=[])
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
# 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 = [
return history_urls + urls
Copy link

jurrian commented Apr 24, 2023

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