From 3f70a0a52f2fbbb2d815e5a47d358427e82c12a5 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 3 Mar 2025 18:52:00 +0000 Subject: [PATCH] Add fallback for blank string representations in audit log, and add tests for creating/editing invalid draft snippets --- wagtail/models/audit_log.py | 2 +- wagtail/snippets/tests/test_snippets.py | 117 ++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/wagtail/models/audit_log.py b/wagtail/models/audit_log.py index 8258fd9b5c..d96197b4b4 100644 --- a/wagtail/models/audit_log.py +++ b/wagtail/models/audit_log.py @@ -97,7 +97,7 @@ class BaseLogEntryManager(models.Manager): return LogEntryQuerySet(self.model, using=self._db) def get_instance_title(self, instance): - return str(instance) + return str(instance) or f"{instance.__class__.__name__} object ({instance.pk})" def log_action(self, instance, action, **kwargs): """ diff --git a/wagtail/snippets/tests/test_snippets.py b/wagtail/snippets/tests/test_snippets.py index 0fdd451e96..7ed9e7fc3b 100644 --- a/wagtail/snippets/tests/test_snippets.py +++ b/wagtail/snippets/tests/test_snippets.py @@ -1325,6 +1325,45 @@ class TestCreateDraftStateSnippet(WagtailTestUtils, TestCase): # The revision content should contain the data self.assertEqual(snippet.latest_revision.content["text"], "Draft-enabled Foo") + # A log entry should be created + log_entry = ModelLogEntry.objects.for_instance(snippet).get( + action="wagtail.create" + ) + self.assertEqual(log_entry.revision, snippet.latest_revision) + self.assertEqual(log_entry.label, "Draft-enabled Foo") + + def test_create_skips_validation_when_saving_draft(self): + response = self.post(post_data={"text": ""}) + snippet = DraftStateModel.objects.get(text="") + + self.assertRedirects( + response, + reverse( + "wagtailsnippets_tests_draftstatemodel:edit", args=[quote(snippet.pk)] + ), + ) + + self.assertFalse(snippet.live) + + # A log entry should be created (with a fallback label) + log_entry = ModelLogEntry.objects.for_instance(snippet).get( + action="wagtail.create" + ) + self.assertEqual(log_entry.revision, snippet.latest_revision) + self.assertEqual(log_entry.label, f"DraftStateModel object ({snippet.pk})") + + def test_create_will_not_publish_invalid_snippet(self): + response = self.post( + post_data={"text": "", "action-publish": "Publish"}, + ) + self.assertEqual(response.status_code, 200) + self.assertContains( + response, "The draft state model could not be created due to errors." + ) + + snippets = DraftStateModel.objects.filter(text="") + self.assertEqual(snippets.count(), 0) + def test_publish(self): # Connect a mock signal handler to published signal mock_handler = mock.MagicMock() @@ -2238,6 +2277,84 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView): # The revision content should contain the new data self.assertEqual(latest_revision.content["text"], "Draft-enabled Bar") + # A log entry should be created + log_entry = ( + ModelLogEntry.objects.for_instance(self.test_snippet) + .filter(action="wagtail.edit") + .order_by("-timestamp") + .first() + ) + self.assertEqual(log_entry.revision, self.test_snippet.latest_revision) + self.assertEqual(log_entry.label, "Draft-enabled Bar") + + def test_skip_validation_on_save_draft(self): + response = self.post(post_data={"text": ""}) + self.test_snippet.refresh_from_db() + revisions = Revision.objects.for_instance(self.test_snippet) + latest_revision = self.test_snippet.latest_revision + + self.assertRedirects(response, self.get_edit_url()) + + # The instance should be updated, since it is still a draft + self.assertEqual(self.test_snippet.text, "") + + # The instance should be a draft + self.assertFalse(self.test_snippet.live) + self.assertTrue(self.test_snippet.has_unpublished_changes) + self.assertIsNone(self.test_snippet.first_published_at) + self.assertIsNone(self.test_snippet.last_published_at) + self.assertIsNone(self.test_snippet.live_revision) + + # The revision should be created and set as latest_revision + self.assertEqual(revisions.count(), 1) + self.assertEqual(latest_revision, revisions.first()) + + # The revision content should contain the new data + self.assertEqual(latest_revision.content["text"], "") + + # A log entry should be created (with a fallback label) + log_entry = ( + ModelLogEntry.objects.for_instance(self.test_snippet) + .filter(action="wagtail.edit") + .order_by("-timestamp") + .first() + ) + self.assertEqual(log_entry.revision, self.test_snippet.latest_revision) + self.assertEqual( + log_entry.label, + f"DraftStateCustomPrimaryKeyModel object ({self.test_snippet.pk})", + ) + + def test_cannot_publish_invalid(self): + # Connect a mock signal handler to published signal + mock_handler = mock.MagicMock() + published.connect(mock_handler) + + try: + response = self.post( + post_data={ + "text": "", + "action-publish": "action-publish", + } + ) + + self.test_snippet.refresh_from_db() + + self.assertEqual(response.status_code, 200) + self.assertContains( + response, + "The draft state custom primary key model could not be saved due to errors.", + ) + + # The instance should be unchanged + self.assertEqual(self.test_snippet.text, "Draft-enabled Foo") + self.assertFalse(self.test_snippet.live) + + # The published signal should not have been fired + self.assertEqual(mock_handler.call_count, 0) + finally: + published.disconnect(mock_handler) + def test_publish(self): # Connect a mock signal handler to published signal mock_handler = mock.MagicMock()