Browse Source

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>
Adam Johnson 1 week ago
parent
commit
27b68bcadf

+ 6 - 2
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)
 

+ 2 - 0
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):

+ 3 - 1
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`).

+ 48 - 1
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