From 27b68bcadf1ab2e9f7fd223aed42db352ccdc62d Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 12 Mar 2025 11:05:50 +0000 Subject: [PATCH] Fixed #36234 -- Restored single_object argument to LogEntry.objects.log_actions(). Thank you Adam Johnson for the report and fix. Thank you Sarah Boyce for your spot on analysis. Regression in c09bceef68e5abb79accedd12dade16aa6577a09, which is partially reverted in this branch. Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> --- django/contrib/admin/models.py | 8 +++-- django/contrib/admin/options.py | 2 ++ docs/releases/5.1.8.txt | 4 ++- tests/admin_utils/test_logentry.py | 49 +++++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py index 453e65cf01..57122dac35 100644 --- a/django/contrib/admin/models.py +++ b/django/contrib/admin/models.py @@ -24,7 +24,9 @@ ACTION_FLAG_CHOICES = [ class LogEntryManager(models.Manager): use_in_migrations = True - def log_actions(self, user_id, queryset, action_flag, change_message=""): + def log_actions( + self, user_id, queryset, action_flag, change_message="", *, single_object=False + ): if isinstance(change_message, list): change_message = json.dumps(change_message) @@ -45,7 +47,9 @@ class LogEntryManager(models.Manager): if len(log_entry_list) == 1: instance = log_entry_list[0] instance.save() - return instance + if single_object: + return instance + return [instance] return self.model.objects.bulk_create(log_entry_list) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 090b12151a..3c2cf9d130 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -946,6 +946,7 @@ class ModelAdmin(BaseModelAdmin): queryset=[obj], action_flag=ADDITION, change_message=message, + single_object=True, ) def log_change(self, request, obj, message): @@ -961,6 +962,7 @@ class ModelAdmin(BaseModelAdmin): queryset=[obj], action_flag=CHANGE, change_message=message, + single_object=True, ) def log_deletions(self, request, queryset): diff --git a/docs/releases/5.1.8.txt b/docs/releases/5.1.8.txt index fd96acd0cb..e5143904a2 100644 --- a/docs/releases/5.1.8.txt +++ b/docs/releases/5.1.8.txt @@ -9,4 +9,6 @@ Django 5.1.8 fixes several bugs in 5.1.7. Bugfixes ======== -* ... +* Fixed a regression in Django 5.1.7 where the removal of the ``single_object`` + parameter unintentionally altered the signature and return type of + ``LogEntryManager.log_actions()`` (:ticket:`36234`). diff --git a/tests/admin_utils/test_logentry.py b/tests/admin_utils/test_logentry.py index 7ba43c4ba0..a800de0fad 100644 --- a/tests/admin_utils/test_logentry.py +++ b/tests/admin_utils/test_logentry.py @@ -254,12 +254,13 @@ class LogEntryTests(TestCase): content_type = ContentType.objects.get_for_model(self.a1) self.assertEqual(len(queryset), 3) with self.assertNumQueries(1): - LogEntry.objects.log_actions( + result = LogEntry.objects.log_actions( self.user.pk, queryset, DELETION, change_message=msg, ) + self.assertEqual(len(result), len(queryset)) logs = ( LogEntry.objects.filter(action_flag=DELETION) .order_by("id") @@ -283,9 +284,55 @@ class LogEntryTests(TestCase): ) for obj in queryset ] + result_logs = [ + ( + entry.user_id, + entry.content_type_id, + str(entry.object_id), + entry.object_repr, + entry.action_flag, + entry.change_message, + ) + for entry in result + ] + self.assertSequenceEqual(logs, result_logs) self.assertSequenceEqual(logs, expected_log_values) self.assertEqual(self.signals, []) + def test_log_actions_single_object_param(self): + queryset = Article.objects.filter(pk=self.a1.pk) + msg = "Deleted Something" + content_type = ContentType.objects.get_for_model(self.a1) + self.assertEqual(len(queryset), 1) + for single_object in (True, False): + self.signals = [] + with self.subTest(single_object=single_object), self.assertNumQueries(1): + result = LogEntry.objects.log_actions( + self.user.pk, + queryset, + DELETION, + change_message=msg, + single_object=single_object, + ) + if single_object: + self.assertIsInstance(result, LogEntry) + entry = result + else: + self.assertIsInstance(result, list) + self.assertEqual(len(result), 1) + entry = result[0] + self.assertEqual(entry.user_id, self.user.pk) + self.assertEqual(entry.content_type_id, content_type.id) + self.assertEqual(str(entry.object_id), str(self.a1.pk)) + self.assertEqual(entry.object_repr, str(self.a1)) + self.assertEqual(entry.action_flag, DELETION) + self.assertEqual(entry.change_message, msg) + expected_signals = [ + ("pre_save", entry), + ("post_save", entry, True), + ] + self.assertEqual(self.signals, expected_signals) + def test_recentactions_without_content_type(self): """ If a LogEntry is missing content_type it will not display it in span