How to display Django Admin Log on a detail page using an inline

Django admin site is a very useful feature, it allows you to add (though quite simplistic) CRUD interface. Django admin also logs every admin action, so you have some accountability out of the box. However, accessing these records from the UI is not optimal.

Recently in one project, I needed to display what actions were taken by the admins, so I wanted to display all logs for a given object on the detail view. Weirdly there is no official way to do this, so here is my solution (bear in mind that it is quite hackish).

from django.contrib.admin.checks import BaseModelAdminChecks
from django.contrib.admin.models import LogEntry
from django.contrib.contenttypes.admin import GenericStackedInline

class ModelAdminLog(GenericStackedInline):
    model = LogEntry

    # All fields are read-only, obviously
    readonly_fields = fields = ["action_time", "user", "change_message"]
    # No extra fields so noone can add new logs
    extra = 0
    # No one can delete logs
    can_delete = False

    # This is a hackity hack! See below
    checks_class = BaseModelAdminChecks

    def change_message(self, obj):
        return obj.get_change_message()

@admin.register(models.MyModel)
class MyModelAdminAdmin(admin.ModelAdmin):
    # ...

    inlines = [ModelAdminLog]

Magic is in the ModelAdminLog class, this is an inline model admin which displays all actions taken by admin on an object.

Django has a quite interesting mechanism that allows generic relations <https://docs.djangoproject.com/en/2.1/ref/contrib/contenttypes/#generic-relations>`__, that is a relation where an object can have relation with any registered model. I generally dislike these as I prefer the safety of database-enforced foreign keys, but they are very useful in cases like ``AdminLog.

Generic foreign keys need three attributes on a model:

  • object_id --- id of a referenced row;
  • content_type --- foreign key to the ContentType table, that defines to which model current row relates to;
  • content_object --- a python object that exposes ForeignKey interface using the above two fields.

GenericStackedInline is an inline model admin, that uses GenericForeignKey, Django AdminLog model however only uses object_id and content_type but does not define actual GenericForeignKey field. However GenericStackedInline does not require actual GenericForeignKey as it directly uses object_id and content_type. So I just needed to disable admin checks by re-setting them to base check class here: checks_class = BaseModelAdminChecks.

That's it.