Skip to content

Commit fb44a6a

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents c76e8d7 + ae2736c commit fb44a6a

File tree

11 files changed

+63
-22
lines changed

11 files changed

+63
-22
lines changed

django/contrib/admin/models.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@
2424
class LogEntryManager(models.Manager):
2525
use_in_migrations = True
2626

27-
def log_actions(
28-
self, user_id, queryset, action_flag, change_message="", *, single_object=False
29-
):
27+
def log_actions(self, user_id, queryset, action_flag, change_message=""):
3028
if isinstance(change_message, list):
3129
change_message = json.dumps(change_message)
3230

@@ -44,7 +42,7 @@ def log_actions(
4442
for obj in queryset
4543
]
4644

47-
if single_object and log_entry_list:
45+
if len(log_entry_list) == 1:
4846
instance = log_entry_list[0]
4947
instance.save()
5048
return instance

django/contrib/admin/options.py

-2
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,6 @@ def log_addition(self, request, obj, message):
946946
queryset=[obj],
947947
action_flag=ADDITION,
948948
change_message=message,
949-
single_object=True,
950949
)
951950

952951
def log_change(self, request, obj, message):
@@ -962,7 +961,6 @@ def log_change(self, request, obj, message):
962961
queryset=[obj],
963962
action_flag=CHANGE,
964963
change_message=message,
965-
single_object=True,
966964
)
967965

968966
def log_deletions(self, request, queryset):

docs/ref/contrib/admin/index.txt

+7
Original file line numberDiff line numberDiff line change
@@ -2614,6 +2614,13 @@ we can do this with inline admin models. Suppose we have the following models::
26142614
date_joined = models.DateField()
26152615
invite_reason = models.CharField(max_length=64)
26162616

2617+
class Meta:
2618+
constraints = [
2619+
models.UniqueConstraint(
2620+
fields=["person", "group"], name="unique_person_group"
2621+
)
2622+
]
2623+
26172624
The first step in displaying this intermediate model in the admin is to
26182625
define an inline class for the ``Membership`` model::
26192626

docs/ref/models/fields.txt

+3-8
Original file line numberDiff line numberDiff line change
@@ -2028,13 +2028,6 @@ that control how the relationship functions.
20282028
:ref:`extra data with a many-to-many relationship
20292029
<intermediary-manytomany>`.
20302030

2031-
.. note::
2032-
2033-
If you don't want multiple associations between the same instances, add
2034-
a :class:`~django.db.models.UniqueConstraint` including the from and to
2035-
fields. Django's automatically generated many-to-many tables include
2036-
such a constraint.
2037-
20382031
.. note::
20392032

20402033
Recursive relationships using an intermediary model can't determine the
@@ -2045,7 +2038,9 @@ that control how the relationship functions.
20452038

20462039
If you don't specify an explicit ``through`` model, there is still an
20472040
implicit ``through`` model class you can use to directly access the table
2048-
created to hold the association. It has three fields to link the models.
2041+
created to hold the association. It has three fields to link the models, a
2042+
primary key and two foreign keys. There is a unique constraint on the two
2043+
foreign keys.
20492044

20502045
If the source and target models differ, the following fields are
20512046
generated:

docs/releases/5.1.7.txt

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

docs/releases/5.1.txt

+5
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,11 @@ Miscellaneous
401401
* The minimum supported version of ``asgiref`` is increased from 3.7.0 to
402402
3.8.1.
403403

404+
* To improve performance, the ``delete_selected`` admin action now uses
405+
``QuerySet.bulk_create()`` when creating multiple ``LogEntry`` objects. As a
406+
result, ``pre_save`` and ``post_save`` signals for ``LogEntry`` are not sent
407+
when multiple objects are deleted via this admin action.
408+
404409
.. _deprecated-features-5.1:
405410

406411
Features deprecated in 5.1

docs/topics/db/models.txt

+12
Original file line numberDiff line numberDiff line change
@@ -503,10 +503,22 @@ something like this::
503503
date_joined = models.DateField()
504504
invite_reason = models.CharField(max_length=64)
505505

506+
class Meta:
507+
constraints = [
508+
models.UniqueConstraint(
509+
fields=["person", "group"], name="unique_person_group"
510+
)
511+
]
512+
506513
When you set up the intermediary model, you explicitly specify foreign
507514
keys to the models that are involved in the many-to-many relationship. This
508515
explicit declaration defines how the two models are related.
509516

517+
If you don't want multiple associations between the same instances, add a
518+
:class:`~django.db.models.UniqueConstraint` including the ``from`` and ``to``
519+
fields. Django's automatically generated many-to-many tables include such a
520+
constraint.
521+
510522
There are a few restrictions on the intermediate model:
511523

512524
* Your intermediate model must contain one - and *only* one - foreign key

tests/admin_changelist/tests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1845,7 +1845,7 @@ def test_no_user(self):
18451845
"""{% get_admin_log %} works without specifying a user."""
18461846
user = User(username="jondoe", password="secret", email="[email protected]")
18471847
user.save()
1848-
LogEntry.objects.log_actions(user.pk, [user], 1, single_object=True)
1848+
LogEntry.objects.log_actions(user.pk, [user], 1)
18491849
context = Context({"log_entries": LogEntry.objects.all()})
18501850
t = Template(
18511851
"{% load log %}"

tests/admin_utils/test_logentry.py

+29-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.contrib.admin.utils import quote
66
from django.contrib.auth.models import User
77
from django.contrib.contenttypes.models import ContentType
8+
from django.db.models.signals import post_save, pre_save
89
from django.test import TestCase, override_settings
910
from django.urls import reverse
1011
from django.utils import translation
@@ -41,11 +42,23 @@ def setUpTestData(cls):
4142
[cls.a1],
4243
CHANGE,
4344
change_message="Changed something",
44-
single_object=True,
4545
)
4646

4747
def setUp(self):
4848
self.client.force_login(self.user)
49+
self.signals = []
50+
51+
pre_save.connect(self.pre_save_listener, sender=LogEntry)
52+
self.addCleanup(pre_save.disconnect, self.pre_save_listener, sender=LogEntry)
53+
54+
post_save.connect(self.post_save_listener, sender=LogEntry)
55+
self.addCleanup(post_save.disconnect, self.post_save_listener, sender=LogEntry)
56+
57+
def pre_save_listener(self, instance, **kwargs):
58+
self.signals.append(("pre_save", instance))
59+
60+
def post_save_listener(self, instance, created, **kwargs):
61+
self.signals.append(("post_save", instance, created))
4962

5063
def test_logentry_save(self):
5164
"""
@@ -271,6 +284,7 @@ def test_log_actions(self):
271284
for obj in queryset
272285
]
273286
self.assertSequenceEqual(logs, expected_log_values)
287+
self.assertEqual(self.signals, [])
274288

275289
def test_recentactions_without_content_type(self):
276290
"""
@@ -314,6 +328,8 @@ def test_proxy_model_content_type_is_used_for_log_entries(self):
314328
"created_1": "00:00",
315329
}
316330
changelist_url = reverse("admin:admin_utils_articleproxy_changelist")
331+
expected_signals = []
332+
self.assertEqual(self.signals, expected_signals)
317333

318334
# add
319335
proxy_add_url = reverse("admin:admin_utils_articleproxy_add")
@@ -322,6 +338,10 @@ def test_proxy_model_content_type_is_used_for_log_entries(self):
322338
proxy_addition_log = LogEntry.objects.latest("id")
323339
self.assertEqual(proxy_addition_log.action_flag, ADDITION)
324340
self.assertEqual(proxy_addition_log.content_type, proxy_content_type)
341+
expected_signals.extend(
342+
[("pre_save", proxy_addition_log), ("post_save", proxy_addition_log, True)]
343+
)
344+
self.assertEqual(self.signals, expected_signals)
325345

326346
# change
327347
article_id = proxy_addition_log.object_id
@@ -334,6 +354,10 @@ def test_proxy_model_content_type_is_used_for_log_entries(self):
334354
proxy_change_log = LogEntry.objects.latest("id")
335355
self.assertEqual(proxy_change_log.action_flag, CHANGE)
336356
self.assertEqual(proxy_change_log.content_type, proxy_content_type)
357+
expected_signals.extend(
358+
[("pre_save", proxy_change_log), ("post_save", proxy_change_log, True)]
359+
)
360+
self.assertEqual(self.signals, expected_signals)
337361

338362
# delete
339363
proxy_delete_url = reverse(
@@ -344,6 +368,10 @@ def test_proxy_model_content_type_is_used_for_log_entries(self):
344368
proxy_delete_log = LogEntry.objects.latest("id")
345369
self.assertEqual(proxy_delete_log.action_flag, DELETION)
346370
self.assertEqual(proxy_delete_log.content_type, proxy_content_type)
371+
expected_signals.extend(
372+
[("pre_save", proxy_delete_log), ("post_save", proxy_delete_log, True)]
373+
)
374+
self.assertEqual(self.signals, expected_signals)
347375

348376
def test_action_flag_choices(self):
349377
tests = ((1, "Addition"), (2, "Change"), (3, "Deletion"))
@@ -358,15 +386,13 @@ def test_hook_get_log_entries(self):
358386
[self.a1],
359387
CHANGE,
360388
change_message="Article changed message",
361-
single_object=True,
362389
)
363390
c1 = Car.objects.create()
364391
LogEntry.objects.log_actions(
365392
self.user.pk,
366393
[c1],
367394
ADDITION,
368395
change_message="Car created message",
369-
single_object=True,
370396
)
371397
exp_str_article = escape(str(self.a1))
372398
exp_str_car = escape(str(c1))

tests/admin_views/test_history_view.py

-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ def setUp(self):
6666
[self.superuser],
6767
CHANGE,
6868
change_message=f"Changed something {i}",
69-
single_object=True,
7069
)
7170
self.admin_login(
7271
username="super",

tests/admin_views/tests.py

-3
Original file line numberDiff line numberDiff line change
@@ -3884,21 +3884,18 @@ def setUpTestData(cls):
38843884
[cls.m1],
38853885
2,
38863886
change_message="Changed something",
3887-
single_object=True,
38883887
)
38893888
LogEntry.objects.log_actions(
38903889
user_pk,
38913890
[cls.m1],
38923891
1,
38933892
change_message="Added something",
3894-
single_object=True,
38953893
)
38963894
LogEntry.objects.log_actions(
38973895
user_pk,
38983896
[cls.m1],
38993897
3,
39003898
change_message="Deleted something",
3901-
single_object=True,
39023899
)
39033900

39043901
def setUp(self):

0 commit comments

Comments
 (0)