Fix audit log display of deleted models

The audit log report currently raises an unhandled exception if it
tries to display log entries for model instances where the model was
deleted after the log entry was made. This commit fixes that bug.

If the model was deleted via a migration but the content type still
exists in the database (a "stale content type" in Django parlance),
the report will now display a capitalized version of the content type's
"name" field, for example "Homepage" for the HomePage model.

If the content type has also been deleted (likely via Django's
remove_stale_contenttypes management command), the report will now
display the phrase "Unknown content type".

Fixes issue 8699.
pull/10132/head
Andy Chosak 2023-02-21 10:27:37 -05:00 zatwierdzone przez Matt Westcott
rodzic 93077eaccd
commit 8d1835f55c
4 zmienionych plików z 64 dodań i 3 usunięć

Wyświetl plik

@ -4,6 +4,7 @@ from io import BytesIO
from django.conf import settings
from django.conf.locale import LANG_INFO
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse
@ -11,7 +12,7 @@ from django.utils import timezone, translation
from openpyxl import load_workbook
from wagtail.admin.views.mixins import ExcelDateFormatter
from wagtail.models import Page, PageLogEntry
from wagtail.models import ModelLogEntry, Page, PageLogEntry
from wagtail.test.utils import WagtailTestUtils
@ -279,6 +280,34 @@ class TestFilteredLogEntriesView(WagtailTestUtils, TestCase):
],
)
def test_log_entry_with_stale_content_type(self):
stale_content_type = ContentType.objects.create(
app_label="fake_app", model="deleted model"
)
ModelLogEntry.objects.create(
object_id=123,
content_type=stale_content_type,
label="This instance's model was deleted, but its content type was not",
action="wagtail.create",
timestamp=timezone.now(),
)
response = self.get()
self.assertContains(response, "Deleted model")
def test_log_entry_with_null_content_type(self):
ModelLogEntry.objects.create(
object_id=123,
content_type=None,
label="This instance's model was deleted, and so was its content type",
action="wagtail.create",
timestamp=timezone.now(),
)
response = self.get()
self.assertContains(response, "Unknown content type")
@override_settings(
USE_L10N=True,

Wyświetl plik

@ -19,6 +19,7 @@ from django.test import RequestFactory
from django.utils.encoding import force_str
from django.utils.text import capfirst, slugify
from django.utils.translation import check_for_language, get_supported_language_variant
from django.utils.translation import gettext_lazy as _
if TYPE_CHECKING:
from wagtail.models import Site
@ -156,6 +157,9 @@ def get_content_type_label(content_type):
Return a human-readable label for a content type object, suitable for display in the admin
in place of the default 'wagtailcore | page' representation
"""
if content_type is None:
return _("Unknown content type")
model = content_type.model_class()
if model:
return str(capfirst(model._meta.verbose_name))

Wyświetl plik

@ -65,8 +65,19 @@ class LogEntryQuerySet(models.QuerySet):
{}
) # lookup of (content_type_id, stringified_object_id) to instance
for content_type_id, object_ids in ids_by_content_type.items():
model = ContentType.objects.get_for_id(content_type_id).model_class()
model_instances = model.objects.in_bulk(object_ids)
try:
content_type = ContentType.objects.get_for_id(content_type_id)
model = content_type.model_class()
except ContentType.DoesNotExist:
model = None
if model:
model_instances = model.objects.in_bulk(object_ids)
else:
# The model class for the logged instance no longer exists,
# so we have no instance to return. Return None instead.
model_instances = {object_id: None for object_id in object_ids}
for object_id, instance in model_instances.items():
instances_by_id[(content_type_id, str(object_id))] = instance

Wyświetl plik

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*
import pickle
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
from django.test import SimpleTestCase, TestCase, override_settings
from django.utils.text import slugify
@ -14,6 +15,7 @@ from wagtail.coreutils import (
cautious_slugify,
find_available_slug,
get_content_languages,
get_content_type_label,
get_dummy_request,
get_supported_content_language_variant,
multigetattr,
@ -295,6 +297,21 @@ class TestGetContentLanguages(TestCase):
)
def TestGetContentTypeLabel(TestCase):
def test_none(self):
self.assertEqual(get_content_type_label(None), "Unknown content type")
def test_valid_content_type(self):
page_content_type = ContentType.objects.get_for_model(Page)
self.assertEqual(get_content_type_label(page_content_type), "Page")
def test_stale_content_type(self):
stale_content_type = ContentType.objects.create(
app_label="fake_app", model="deleted model"
)
self.assertEqual(get_content_type_label(stale_content_type), "Deleted model")
@override_settings(
USE_I18N=True,
WAGTAIL_I18N_ENABLED=True,