Track versions of Wagtail snippets for an automatic audit trail to find the bad guys.

There are trade-offs with everything (or how I stopped worrying and learned to love Wagtail CMS)

Wagtail is a CMS framework built on top of Django that takes away some of tedium of creating a CMS from scratch. It has a nice extendable interface for editors to interact with, page reversion tracking, a spiffy admin UI, easy uploading of images, and lots of other goodies.

I’ll be honest – coming from Django where I have a pretty good grasp of how things work (and where to figure things out if I need to) was a hard transition to Wagtail. It definitely took a little time to understand its philosophy around content.

But, at this point, I have come to appreciate all of the benefits that Wagtail provides… and work around some of the pieces that I like less. One thing that took me a while to understand is that Wagtail uses the term snippet to refer to Django models that aren’t explicitly a Page. Coming from Django, I am used to defining a data model and tying models together with many-to-many or foreign key relationships. Snippets are where to store those pieces of information that could be shared with many different Page models. However, snippets feel like they are not completely integrated into the Wagtail experience, and they lose a lot of the nice functionality that comes “for free” with a page model – one glaring omission is the lack of revisions for snippets. There are a few open issues for this in Wagtail (notably https://github.com/wagtail/wagtail/issues/4541 and https://github.com/wagtail/wagtail/issues/2270).

Django audits in one easy payment of $0

One package that I have really appreciated in the past with a normal Django application is django-reversion which integrates tightly into the Django admin and automatically saves versions of an object as an audit trail. Wagtail provides something very similar for Page models, but snippets eren’t invited to the party and feel very left out.

¡Ay, caramba! Automatic auditing for Wagtail snippets!

All is well in Wagtail-land, though! With a little perserverance, Wagtail snippets can also get automatic revisions and it isn’t too hard.

Setup django-reversion

First, install django-reversion with the normal installation steps. Unfortunately, Wagtail won’t tie into the same automatic magic that django-reversion uses for the Django admin, but it has a stellar API you can call explicitly.

Register snippet models with django-reversion

For snippet models, you will need to add the @reversion.register() decorator above the @register_snippet in models.py.

from django.db import models
import reversion
from wagtail.core.models import ClusterableModel, Orderable
from wagtail.snippets.models import register_snippet


@reversion.register()
@register_snippet
class Book(Orderable, ClusterableModel):
    title = models.CharField(max_length=255)

Override the Wagtail views

Next, you will need to override the Wagtail views for the snippets you want to audit in wagtail_hooks.py.

from reversion.revisions import add_to_revision, create_revision, set_comment, set_user
from wagtail.contrib.modeladmin.options import ModelAdmin
from wagtail.contrib.modeladmin.views import CreateView, EditView

from .models import Book


class RevisionEditView(EditView):
    def form_valid(self, form, *args, **kwargs):
        form_valid_return = super().form_valid(form, *args, **kwargs)

        with create_revision():
            set_comment(self.get_success_message(self.instance))
            add_to_revision(self.instance)
            set_user(self.request.user)

        return form_valid_return


class RevisionCreateView(CreateView):
    def form_valid(self, form, *args, **kwargs):
        form_valid_return = super().form_valid(form, *args, **kwargs)

        # Call form.save() explicitly to get access to the instance
        instance = form.save()

        with create_revision():
            set_comment(self.get_success_message(instance))
            add_to_revision(instance)
            set_user(self.request.user)

        return form_valid_return


class BookAdmin(ModelAdmin):
    model = Book
    edit_view_class = RevisionEditView
    create_view_class = RevisionCreateView

Happy trails

Overriding the ModelAdmin classes by setting the edit_view_class and create_view_class settings and django-revision is the real magic here. But, be sure to note the weirdness in the CreateView to get an instance that is usable. In testing it doesn’t create multiple instances of the model, however, I would not be surprised if there are duplicate database calls because of the implementation.

Tested with the following package versions