kopia lustrzana https://github.com/wagtail/wagtail
Rewrite recent edits panel to use audit log
As per https://github.com/wagtail/wagtail/issues/11323#issuecomment-1889520913 Fixes #11323. Thanks to @elhussienalmasri for the initial investigation and proposed fix.pull/11164/head
rodzic
15f7486e67
commit
67f495bb4c
|
@ -56,6 +56,7 @@ Changelog
|
|||
* Fix: Ensure Page querysets support using `alias` and `specific` (Tomasz Knapik)
|
||||
* Fix: Ensure workflow dashboard panels work when the page/snippet is missing (Sage Abdullah)
|
||||
* Fix: Prevent a ValueError with `FormSubmissionsPanel` on Django 5.0 when creating a new form page (Matt Westcott)
|
||||
* Fix: Avoid duplicate entries in "Recent edits" panel when copying pages (Matt Westcott)
|
||||
* Docs: Document, for contributors, the use of translate string literals passed as arguments to tags and filters using `_()` within templates (Chiemezuo Akujobi)
|
||||
* Docs: Document all features for the Documents app in one location (Neeraj Yetheendran)
|
||||
* Docs: Add section to testing docs about creating pages and working with page content (Mariana Bedran Lesche)
|
||||
|
|
|
@ -84,6 +84,7 @@ Thank you to Thibaud Colas and Badr Fourane for their work on this feature.
|
|||
* Ensure `ActionController` explicitly checks for elements that allow select functionality (Nandini Arora)
|
||||
* Prevent a ValueError with `FormSubmissionsPanel` on Django 5.0 when creating a new form page (Matt Westcott)
|
||||
* Add ability to [customise a page's copy form](custom_page_copy_form) including an auto-incrementing slug example (Neeraj Yetheendran)
|
||||
* Avoid duplicate entries in "Recent edits" panel when copying pages (Matt Westcott)
|
||||
|
||||
### Documentation
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for revision, page in last_edits %}
|
||||
{% for last_edited_at, page in last_edits %}
|
||||
<tr>
|
||||
<td class="title" valign="top">
|
||||
<div class="title-wrapper">
|
||||
|
@ -49,7 +49,7 @@
|
|||
<td valign="top">
|
||||
{% include "wagtailadmin/shared/page_status_tag.html" with page=page %}
|
||||
</td>
|
||||
<td valign="top">{% human_readable_date revision.created_at %}</td>
|
||||
<td valign="top">{% human_readable_date last_edited_at %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from freezegun import freeze_time
|
||||
|
||||
from wagtail.admin.views.home import RecentEditsPanel
|
||||
from wagtail.coreutils import get_dummy_request
|
||||
|
@ -84,17 +86,13 @@ class TestRecentEditsPanel(WagtailTestUtils, TestCase):
|
|||
self.assertIn("Your most recent edits", response.content.decode("utf-8"))
|
||||
|
||||
def test_missing_page_record(self):
|
||||
# Ensure that the panel still renders when one of the returned revision records
|
||||
# has no corresponding Page object. It's unclear how this happens, since revisions
|
||||
# are deleted on page deletion, but there are reports of this happening in
|
||||
# https://github.com/wagtail/wagtail/issues/9185
|
||||
|
||||
# edit the revision object to be owned by Alice and have an unrecognised object ID
|
||||
self.revision.user = self.user_alice
|
||||
self.revision.object_id = "999999"
|
||||
self.revision.save()
|
||||
# Ensure that the panel still renders when one of the page IDs returned from querying
|
||||
# PageLogEntry has no corresponding Page object. This can happen if a page is deleted,
|
||||
# because PageLogEntry records are kept on deletion.
|
||||
|
||||
self.login(username="alice", password="password")
|
||||
self.change_something("Alice's edit")
|
||||
self.child_page.delete()
|
||||
response = self.client.get(reverse("wagtailadmin_home"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -102,7 +100,11 @@ class TestRecentEditsPanel(WagtailTestUtils, TestCase):
|
|||
"""Test if the panel actually returns expected pages"""
|
||||
self.login(username="bob", password="password")
|
||||
# change a page
|
||||
self.change_something("Bob's edit")
|
||||
|
||||
edit_timestamp = timezone.now()
|
||||
with freeze_time(edit_timestamp):
|
||||
self.change_something("Bob's edit")
|
||||
|
||||
# set a user to 'mock' a request
|
||||
self.client.user = get_user_model().objects.get(email="bob@example.com")
|
||||
# get the panel to get the last edits
|
||||
|
@ -111,11 +113,36 @@ class TestRecentEditsPanel(WagtailTestUtils, TestCase):
|
|||
|
||||
page = Page.objects.get(pk=self.child_page.id).specific
|
||||
|
||||
# check if the revision is the revision of edited Page
|
||||
self.assertEqual(ctx["last_edits"][0][0].content_object, page)
|
||||
# check if the page in this list is the specific page of this revision
|
||||
# check the timestamp matches the edit
|
||||
self.assertEqual(ctx["last_edits"][0][0], edit_timestamp)
|
||||
# check if the page in this list is the specific page
|
||||
self.assertEqual(ctx["last_edits"][0][1], page)
|
||||
|
||||
def test_copying_does_not_count_as_an_edit(self):
|
||||
self.login(username="bob", password="password")
|
||||
# change a page
|
||||
self.change_something("Bob was ere")
|
||||
|
||||
# copy the page
|
||||
post_data = {
|
||||
"new_title": "Goodbye world!",
|
||||
"new_slug": "goodbye-world",
|
||||
"new_parent_page": str(self.root_page.id),
|
||||
"copy_subpages": False,
|
||||
"alias": False,
|
||||
}
|
||||
self.client.post(
|
||||
reverse("wagtailadmin_pages:copy", args=(self.child_page.id,)), post_data
|
||||
)
|
||||
# check that page has been copied
|
||||
self.assertTrue(Page.objects.get(title="Goodbye world!"))
|
||||
|
||||
response = self.client.get(reverse("wagtailadmin_home"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Your most recent edits")
|
||||
self.assertContains(response, "Bob was ere")
|
||||
self.assertNotContains(response, "Goodbye world!")
|
||||
|
||||
|
||||
class TestRecentEditsQueryCount(WagtailTestUtils, TestCase):
|
||||
fixtures = ["test.json"]
|
||||
|
@ -128,7 +155,7 @@ class TestRecentEditsQueryCount(WagtailTestUtils, TestCase):
|
|||
# an unpredictable number of queries)
|
||||
pages_to_edit = Page.objects.filter(id__in=[4, 5, 6, 9, 12, 13]).specific()
|
||||
for page in pages_to_edit:
|
||||
page.save_revision(user=self.bob)
|
||||
page.save_revision(user=self.bob, log_action=True)
|
||||
|
||||
def test_panel_query_count(self):
|
||||
# fake a request object with bob as the user
|
||||
|
|
|
@ -5,7 +5,6 @@ from typing import Any, Mapping, Union
|
|||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.db import connection
|
||||
from django.db.models import Exists, IntegerField, Max, OuterRef, Q
|
||||
from django.db.models.functions import Cast
|
||||
from django.forms import Media
|
||||
|
@ -21,6 +20,7 @@ from wagtail.admin.ui.components import Component
|
|||
from wagtail.admin.views.generic import WagtailAdminTemplateMixin
|
||||
from wagtail.models import (
|
||||
Page,
|
||||
PageLogEntry,
|
||||
Revision,
|
||||
TaskState,
|
||||
WorkflowState,
|
||||
|
@ -255,44 +255,26 @@ class RecentEditsPanel(Component):
|
|||
|
||||
# Last n edited pages
|
||||
edit_count = getattr(settings, "WAGTAILADMIN_RECENT_EDITS_LIMIT", 5)
|
||||
if connection.vendor == "mysql":
|
||||
# MySQL can't handle the subselect created by the ORM version -
|
||||
# it fails with "This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'"
|
||||
last_edits = Revision.objects.raw(
|
||||
"""
|
||||
SELECT wr.* FROM
|
||||
wagtailcore_revision wr JOIN (
|
||||
SELECT max(created_at) AS max_created_at, object_id FROM
|
||||
wagtailcore_revision WHERE user_id = %s AND base_content_type_id = %s GROUP BY object_id ORDER BY max_created_at DESC LIMIT %s
|
||||
) AS max_rev ON max_rev.max_created_at = wr.created_at ORDER BY wr.created_at DESC
|
||||
""",
|
||||
[
|
||||
User._meta.pk.get_db_prep_value(request.user.pk, connection),
|
||||
get_default_page_content_type().id,
|
||||
edit_count,
|
||||
],
|
||||
)
|
||||
else:
|
||||
last_edits_dates = (
|
||||
Revision.page_revisions.filter(user=request.user)
|
||||
.values("object_id")
|
||||
.annotate(latest_date=Max("created_at"))
|
||||
.order_by("-latest_date")
|
||||
.values("latest_date")[:edit_count]
|
||||
)
|
||||
last_edits = Revision.page_revisions.filter(
|
||||
created_at__in=last_edits_dates
|
||||
).order_by("-created_at")
|
||||
|
||||
# The revision's object_id is a string, so cast it to int first.
|
||||
page_keys = [int(pr.object_id) for pr in last_edits]
|
||||
pages = Page.objects.specific().in_bulk(page_keys)
|
||||
context["last_edits"] = []
|
||||
for revision in last_edits:
|
||||
page = pages.get(int(revision.object_id))
|
||||
# Query the audit log to get a resultset of (page ID, latest edit timestamp)
|
||||
last_edits_dates = (
|
||||
PageLogEntry.objects.filter(user=request.user, action="wagtail.edit")
|
||||
.values("page_id")
|
||||
.annotate(latest_date=Max("timestamp"))
|
||||
.order_by("-latest_date")[:edit_count]
|
||||
)
|
||||
# Retrieve the page objects for those IDs
|
||||
pages_mapping = Page.objects.specific().in_bulk(
|
||||
[log["page_id"] for log in last_edits_dates]
|
||||
)
|
||||
# Compile a list of (latest edit timestamp, page object) tuples
|
||||
last_edits = []
|
||||
for log in last_edits_dates:
|
||||
page = pages_mapping.get(log["page_id"])
|
||||
if page:
|
||||
context["last_edits"].append([revision, page])
|
||||
last_edits.append((log["latest_date"], page))
|
||||
|
||||
context["last_edits"] = last_edits
|
||||
context["request"] = request
|
||||
return context
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue