1
0
mirror of https://github.com/django/django.git synced 2025-03-06 07:22:32 +00:00

[5.2.x] Fixed #36217 -- Restored pre_save/post_save signal emission via LogEntry.save() for single-object deletion in the admin.

Regression in 40b3975e7d3e1464a733c69171ad7d38f8814280.

Thanks smiling-watermelon for the report.

Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>

Backport of c09bceef68e5abb79accedd12dade16aa6577a09 from main.
This commit is contained in:
antoliny0919 2025-02-28 12:17:17 +01:00 committed by Sarah Boyce
parent 0e3a6c5fec
commit 5997fdc921
8 changed files with 41 additions and 14 deletions

View File

@ -51,9 +51,7 @@ class LogEntryManager(models.Manager):
change_message=change_message, change_message=change_message,
) )
def log_actions( def log_actions(self, user_id, queryset, action_flag, change_message=""):
self, user_id, queryset, action_flag, change_message="", *, single_object=False
):
# RemovedInDjango60Warning. # RemovedInDjango60Warning.
if type(self).log_action != LogEntryManager.log_action: if type(self).log_action != LogEntryManager.log_action:
warnings.warn( warnings.warn(
@ -93,7 +91,7 @@ class LogEntryManager(models.Manager):
for obj in queryset for obj in queryset
] ]
if single_object and log_entry_list: if len(log_entry_list) == 1:
instance = log_entry_list[0] instance = log_entry_list[0]
instance.save() instance.save()
return instance return instance

View File

@ -955,7 +955,6 @@ class ModelAdmin(BaseModelAdmin):
queryset=[obj], queryset=[obj],
action_flag=ADDITION, action_flag=ADDITION,
change_message=message, change_message=message,
single_object=True,
) )
def log_change(self, request, obj, message): def log_change(self, request, obj, message):
@ -971,7 +970,6 @@ class ModelAdmin(BaseModelAdmin):
queryset=[obj], queryset=[obj],
action_flag=CHANGE, action_flag=CHANGE,
change_message=message, change_message=message,
single_object=True,
) )
def log_deletion(self, request, obj, object_repr): def log_deletion(self, request, obj, object_repr):

View File

@ -22,3 +22,7 @@ Bugfixes
of ``ManyToManyField`` related managers would always return ``0`` and of ``ManyToManyField`` related managers would always return ``0`` and
``False`` when the intermediary model back references used ``to_field`` ``False`` when the intermediary model back references used ``to_field``
(:ticket:`36197`). (:ticket:`36197`).
* Fixed a regression in Django 5.1 where the ``pre_save`` and ``post_save``
signals for ``LogEntry`` were not sent when deleting a single object in the
admin (:ticket:`36217`).

View File

@ -401,6 +401,11 @@ Miscellaneous
* The minimum supported version of ``asgiref`` is increased from 3.7.0 to * The minimum supported version of ``asgiref`` is increased from 3.7.0 to
3.8.1. 3.8.1.
* To improve performance, the ``delete_selected`` admin action now uses
``QuerySet.bulk_create()`` when creating multiple ``LogEntry`` objects. As a
result, ``pre_save`` and ``post_save`` signals for ``LogEntry`` are not sent
when multiple objects are deleted via this admin action.
.. _deprecated-features-5.1: .. _deprecated-features-5.1:
Features deprecated in 5.1 Features deprecated in 5.1

View File

@ -1845,7 +1845,7 @@ class GetAdminLogTests(TestCase):
"""{% get_admin_log %} works without specifying a user.""" """{% get_admin_log %} works without specifying a user."""
user = User(username="jondoe", password="secret", email="super@example.com") user = User(username="jondoe", password="secret", email="super@example.com")
user.save() user.save()
LogEntry.objects.log_actions(user.pk, [user], 1, single_object=True) LogEntry.objects.log_actions(user.pk, [user], 1)
context = Context({"log_entries": LogEntry.objects.all()}) context = Context({"log_entries": LogEntry.objects.all()})
t = Template( t = Template(
"{% load log %}" "{% load log %}"

View File

@ -5,6 +5,7 @@ from django.contrib.admin.models import ADDITION, CHANGE, DELETION, LogEntry
from django.contrib.admin.utils import quote from django.contrib.admin.utils import quote
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import post_save, pre_save
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.urls import reverse from django.urls import reverse
from django.utils import translation from django.utils import translation
@ -42,11 +43,23 @@ class LogEntryTests(TestCase):
[cls.a1], [cls.a1],
CHANGE, CHANGE,
change_message="Changed something", change_message="Changed something",
single_object=True,
) )
def setUp(self): def setUp(self):
self.client.force_login(self.user) self.client.force_login(self.user)
self.signals = []
pre_save.connect(self.pre_save_listener, sender=LogEntry)
self.addCleanup(pre_save.disconnect, self.pre_save_listener, sender=LogEntry)
post_save.connect(self.post_save_listener, sender=LogEntry)
self.addCleanup(post_save.disconnect, self.post_save_listener, sender=LogEntry)
def pre_save_listener(self, instance, **kwargs):
self.signals.append(("pre_save", instance))
def post_save_listener(self, instance, created, **kwargs):
self.signals.append(("post_save", instance, created))
def test_logentry_save(self): def test_logentry_save(self):
""" """
@ -288,6 +301,7 @@ class LogEntryTests(TestCase):
for obj in queryset for obj in queryset
] ]
self.assertSequenceEqual(logs, expected_log_values) self.assertSequenceEqual(logs, expected_log_values)
self.assertEqual(self.signals, [])
# RemovedInDjango60Warning. # RemovedInDjango60Warning.
def test_log_action_fallback(self): def test_log_action_fallback(self):
@ -371,6 +385,8 @@ class LogEntryTests(TestCase):
"created_1": "00:00", "created_1": "00:00",
} }
changelist_url = reverse("admin:admin_utils_articleproxy_changelist") changelist_url = reverse("admin:admin_utils_articleproxy_changelist")
expected_signals = []
self.assertEqual(self.signals, expected_signals)
# add # add
proxy_add_url = reverse("admin:admin_utils_articleproxy_add") proxy_add_url = reverse("admin:admin_utils_articleproxy_add")
@ -379,6 +395,10 @@ class LogEntryTests(TestCase):
proxy_addition_log = LogEntry.objects.latest("id") proxy_addition_log = LogEntry.objects.latest("id")
self.assertEqual(proxy_addition_log.action_flag, ADDITION) self.assertEqual(proxy_addition_log.action_flag, ADDITION)
self.assertEqual(proxy_addition_log.content_type, proxy_content_type) self.assertEqual(proxy_addition_log.content_type, proxy_content_type)
expected_signals.extend(
[("pre_save", proxy_addition_log), ("post_save", proxy_addition_log, True)]
)
self.assertEqual(self.signals, expected_signals)
# change # change
article_id = proxy_addition_log.object_id article_id = proxy_addition_log.object_id
@ -391,6 +411,10 @@ class LogEntryTests(TestCase):
proxy_change_log = LogEntry.objects.latest("id") proxy_change_log = LogEntry.objects.latest("id")
self.assertEqual(proxy_change_log.action_flag, CHANGE) self.assertEqual(proxy_change_log.action_flag, CHANGE)
self.assertEqual(proxy_change_log.content_type, proxy_content_type) self.assertEqual(proxy_change_log.content_type, proxy_content_type)
expected_signals.extend(
[("pre_save", proxy_change_log), ("post_save", proxy_change_log, True)]
)
self.assertEqual(self.signals, expected_signals)
# delete # delete
proxy_delete_url = reverse( proxy_delete_url = reverse(
@ -401,6 +425,10 @@ class LogEntryTests(TestCase):
proxy_delete_log = LogEntry.objects.latest("id") proxy_delete_log = LogEntry.objects.latest("id")
self.assertEqual(proxy_delete_log.action_flag, DELETION) self.assertEqual(proxy_delete_log.action_flag, DELETION)
self.assertEqual(proxy_delete_log.content_type, proxy_content_type) self.assertEqual(proxy_delete_log.content_type, proxy_content_type)
expected_signals.extend(
[("pre_save", proxy_delete_log), ("post_save", proxy_delete_log, True)]
)
self.assertEqual(self.signals, expected_signals)
def test_action_flag_choices(self): def test_action_flag_choices(self):
tests = ((1, "Addition"), (2, "Change"), (3, "Deletion")) tests = ((1, "Addition"), (2, "Change"), (3, "Deletion"))
@ -415,7 +443,6 @@ class LogEntryTests(TestCase):
[self.a1], [self.a1],
CHANGE, CHANGE,
change_message="Article changed message", change_message="Article changed message",
single_object=True,
) )
c1 = Car.objects.create() c1 = Car.objects.create()
LogEntry.objects.log_actions( LogEntry.objects.log_actions(
@ -423,7 +450,6 @@ class LogEntryTests(TestCase):
[c1], [c1],
ADDITION, ADDITION,
change_message="Car created message", change_message="Car created message",
single_object=True,
) )
exp_str_article = escape(str(self.a1)) exp_str_article = escape(str(self.a1))
exp_str_car = escape(str(c1)) exp_str_car = escape(str(c1))

View File

@ -66,7 +66,6 @@ class SeleniumTests(AdminSeleniumTestCase):
[self.superuser], [self.superuser],
CHANGE, CHANGE,
change_message=f"Changed something {i}", change_message=f"Changed something {i}",
single_object=True,
) )
self.admin_login( self.admin_login(
username="super", username="super",

View File

@ -3881,21 +3881,18 @@ class AdminViewStringPrimaryKeyTest(TestCase):
[cls.m1], [cls.m1],
2, 2,
change_message="Changed something", change_message="Changed something",
single_object=True,
) )
LogEntry.objects.log_actions( LogEntry.objects.log_actions(
user_pk, user_pk,
[cls.m1], [cls.m1],
1, 1,
change_message="Added something", change_message="Added something",
single_object=True,
) )
LogEntry.objects.log_actions( LogEntry.objects.log_actions(
user_pk, user_pk,
[cls.m1], [cls.m1],
3, 3,
change_message="Deleted something", change_message="Deleted something",
single_object=True,
) )
def setUp(self): def setUp(self):