diff --git a/wagtail/snippets/templates/wagtailsnippets/snippets/edit.html b/wagtail/snippets/templates/wagtailsnippets/snippets/edit.html
index 2828c3db22..8273deb6ea 100644
--- a/wagtail/snippets/templates/wagtailsnippets/snippets/edit.html
+++ b/wagtail/snippets/templates/wagtailsnippets/snippets/edit.html
@@ -9,6 +9,7 @@
{% trans "Last updated" %}
{% include "wagtailadmin/shared/last_updated.html" with last_updated=latest_log_entry.timestamp time_prefix="at" %}
+ - History
{% endif %}
diff --git a/wagtail/snippets/tests.py b/wagtail/snippets/tests.py
index 5fc47d2243..b151c7d6d4 100644
--- a/wagtail/snippets/tests.py
+++ b/wagtail/snippets/tests.py
@@ -1,8 +1,10 @@
+import datetime
import json
from django.contrib.admin.utils import quote
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, Permission
+from django.contrib.contenttypes.models import ContentType
from django.core import checks
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
@@ -12,6 +14,7 @@ from django.http import HttpRequest, HttpResponse
from django.test import RequestFactory, TestCase
from django.test.utils import override_settings
from django.urls import reverse
+from django.utils.timezone import make_aware
from taggit.models import Tag
from wagtail.admin.admin_url_finder import AdminURLFinder
@@ -19,7 +22,7 @@ from wagtail.admin.edit_handlers import FieldPanel
from wagtail.admin.forms import WagtailAdminModelForm
from wagtail.core import hooks
from wagtail.core.blocks.field_block import FieldBlockAdapter
-from wagtail.core.models import Locale, Page
+from wagtail.core.models import Locale, ModelLogEntry, Page
from wagtail.snippets.action_menu import ActionMenuItem, get_base_snippet_action_menu_items
from wagtail.snippets.blocks import SnippetChooserBlock
from wagtail.snippets.edit_handlers import SnippetChooserPanel
@@ -1054,6 +1057,32 @@ class TestUsedBy(TestCase):
self.assertEqual(type(advert.get_usage()[0]), Page)
+class TestSnippetHistory(TestCase, WagtailTestUtils):
+ fixtures = ['test.json']
+
+ def get(self, params={}):
+ snippet = self.test_snippet
+ args = (snippet._meta.app_label, snippet._meta.model_name, quote(snippet.pk))
+ return self.client.get(reverse('wagtailsnippets:history', args=args), params)
+
+ def setUp(self):
+ self.user = self.login()
+ self.test_snippet = Advert.objects.get(pk=1)
+ ModelLogEntry.objects.create(
+ content_type=ContentType.objects.get_for_model(Advert),
+ label="Test Advert",
+ action='wagtail.create',
+ timestamp=make_aware(datetime.datetime(2021, 9, 30, 10, 1, 0)),
+ object_id='1',
+ )
+
+ def test_simple(self):
+ response = self.get()
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, '
Created | ', html=True)
+ self.assertContains(response, '
')
+
+
class TestSnippetChoose(TestCase, WagtailTestUtils):
fixtures = ['test.json']
diff --git a/wagtail/snippets/urls.py b/wagtail/snippets/urls.py
index bcbefd0a32..6da0b9c1fe 100644
--- a/wagtail/snippets/urls.py
+++ b/wagtail/snippets/urls.py
@@ -21,6 +21,7 @@ urlpatterns = [
path('//multiple/delete/', snippets.delete, name='delete-multiple'),
path('//delete//', snippets.delete, name='delete'),
path('//usage//', snippets.usage, name='usage'),
+ path('//history//', snippets.HistoryView.as_view(), name='history'),
# legacy URLs that could potentially collide if the pk matches one of the reserved names above
# ('add', 'edit' etc) - redirect to the unambiguous version
diff --git a/wagtail/snippets/views/snippets.py b/wagtail/snippets/views/snippets.py
index 34f344c913..f8c2aa3928 100644
--- a/wagtail/snippets/views/snippets.py
+++ b/wagtail/snippets/views/snippets.py
@@ -12,12 +12,14 @@ from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.text import capfirst
from django.utils.translation import gettext as _
-from django.utils.translation import ngettext
+from django.utils.translation import gettext_lazy, ngettext
from django.views.generic import TemplateView
from wagtail.admin import messages
from wagtail.admin.edit_handlers import ObjectList, extract_panel_definitions_from_model_class
from wagtail.admin.forms.search import SearchForm
+from wagtail.admin.ui.tables import Column, DateColumn, UserColumn
+from wagtail.admin.views.generic.models import IndexView
from wagtail.core import hooks
from wagtail.core.log_actions import log
from wagtail.core.log_actions import registry as log_registry
@@ -441,3 +443,32 @@ def redirect_to_delete(request, app_label, model_name, pk):
def redirect_to_usage(request, app_label, model_name, pk):
return redirect('wagtailsnippets:usage', app_label, model_name, pk, permanent=True)
+
+
+class HistoryView(IndexView):
+ template_name = 'wagtailadmin/generic/index.html'
+ page_title = gettext_lazy('Snippet history')
+ header_icon = 'history'
+ paginate_by = 50
+ columns = [
+ Column('message', label=gettext_lazy("Action")),
+ UserColumn('user', blank_display_name='system'),
+ DateColumn('timestamp', label=gettext_lazy("Date")),
+ ]
+
+ def dispatch(self, request, app_label, model_name, pk):
+ self.app_label = app_label
+ self.model_name = model_name
+ self.model = get_snippet_model_from_url_params(app_label, model_name)
+ self.object = get_object_or_404(self.model, pk=unquote(pk))
+
+ return super().dispatch(request)
+
+ def get_page_subtitle(self):
+ return str(self.object)
+
+ def get_index_url(self):
+ return reverse('wagtailsnippets:history', args=(self.app_label, self.model_name, quote(self.object.pk)))
+
+ def get_queryset(self):
+ return log_registry.get_logs_for_instance(self.object).prefetch_related('user__wagtail_userprofile')