mirror of
https://github.com/django/django.git
synced 2025-03-06 15:32:33 +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:
parent
0e3a6c5fec
commit
5997fdc921
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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`).
|
||||||
|
@ -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
|
||||||
|
@ -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 %}"
|
||||||
|
@ -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))
|
||||||
|
@ -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",
|
||||||
|
@ -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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user