summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormgaligniana <marcelogaligniana@gmail.com>2021-12-27 23:32:07 -0300
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-01-11 08:06:18 +0100
commitfa235004dd1423b129befe847bbf207808ca07ad (patch)
treeef9f65f34fbd031007dc2986916ac3927a645698
parentf1905db6c0004a2b6c8b69c55a1f5f9da58ce2eb (diff)
Fixed #13251 -- Made pre/post_delete signals dispatch the origin.
-rw-r--r--django/contrib/admin/utils.py2
-rw-r--r--django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py2
-rw-r--r--django/db/models/base.py2
-rw-r--r--django/db/models/deletion.py10
-rw-r--r--django/db/models/query.py2
-rw-r--r--docs/ref/signals.txt12
-rw-r--r--docs/releases/4.1.txt4
-rw-r--r--tests/signals/models.py5
-rw-r--r--tests/signals/tests.py90
9 files changed, 112 insertions, 17 deletions
diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py
index 42d0d96ea6..4e3d025a56 100644
--- a/django/contrib/admin/utils.py
+++ b/django/contrib/admin/utils.py
@@ -116,7 +116,7 @@ def get_deleted_objects(objs, request, admin_site):
return [], {}, set(), []
else:
using = router.db_for_write(obj._meta.model)
- collector = NestedObjects(using=using)
+ collector = NestedObjects(using=using, origin=objs)
collector.collect(objs)
perms_needed = set()
diff --git a/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py b/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
index e2856bb0f7..5593ecb469 100644
--- a/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
+++ b/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
@@ -51,7 +51,7 @@ class Command(BaseCommand):
ct_info = []
for ct in to_remove:
ct_info.append(' - Content type for %s.%s' % (ct.app_label, ct.model))
- collector = NoFastDeleteCollector(using=using)
+ collector = NoFastDeleteCollector(using=using, origin=ct)
collector.collect([ct])
for obj_type, objs in collector.data.items():
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 0d50dad0c9..793cd936f1 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -987,7 +987,7 @@ class Model(metaclass=ModelBase):
"to None." % (self._meta.object_name, self._meta.pk.attname)
)
using = using or router.db_for_write(self.__class__, instance=self)
- collector = Collector(using=using)
+ collector = Collector(using=using, origin=self)
collector.collect([self], keep_parents=keep_parents)
return collector.delete()
diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py
index d8d8b25990..b99337a309 100644
--- a/django/db/models/deletion.py
+++ b/django/db/models/deletion.py
@@ -76,8 +76,10 @@ def get_candidate_relations_to_delete(opts):
class Collector:
- def __init__(self, using):
+ def __init__(self, using, origin=None):
self.using = using
+ # A Model or QuerySet object.
+ self.origin = origin
# Initially, {model: {instances}}, later values become lists.
self.data = defaultdict(set)
# {model: {(field, value): {instances}}}
@@ -404,7 +406,8 @@ class Collector:
for model, obj in self.instances_with_model():
if not model._meta.auto_created:
signals.pre_delete.send(
- sender=model, instance=obj, using=self.using
+ sender=model, instance=obj, using=self.using,
+ origin=self.origin,
)
# fast deletes
@@ -435,7 +438,8 @@ class Collector:
if not model._meta.auto_created:
for obj in instances:
signals.post_delete.send(
- sender=model, instance=obj, using=self.using
+ sender=model, instance=obj, using=self.using,
+ origin=self.origin,
)
# update collected instances
diff --git a/django/db/models/query.py b/django/db/models/query.py
index fb6639793a..86b1631f67 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -753,7 +753,7 @@ class QuerySet:
del_query.query.select_related = False
del_query.query.clear_ordering(force=True)
- collector = Collector(using=del_query.db)
+ collector = Collector(using=del_query.db, origin=self)
collector.collect(del_query)
deleted, _rows_count = collector.delete()
diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt
index 4eb55c906a..6f0ce0527c 100644
--- a/docs/ref/signals.txt
+++ b/docs/ref/signals.txt
@@ -195,6 +195,12 @@ Arguments sent with this signal:
``using``
The database alias being used.
+``origin``
+ .. versionadded:: 4.1
+
+ The origin of the deletion being the instance of a ``Model`` or
+ ``QuerySet`` class.
+
``post_delete``
---------------
@@ -219,6 +225,12 @@ Arguments sent with this signal:
``using``
The database alias being used.
+``origin``
+ .. versionadded:: 4.1
+
+ The origin of the deletion being the instance of a ``Model`` or
+ ``QuerySet`` class.
+
``m2m_changed``
---------------
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index 8cdec777f9..fa1c05bc80 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -249,7 +249,9 @@ Serialization
Signals
~~~~~~~
-* ...
+* The :data:`~django.db.models.signals.pre_delete` and
+ :data:`~django.db.models.signals.post_delete` signals now dispatch the
+ ``origin`` of the deletion.
Templates
~~~~~~~~~
diff --git a/tests/signals/models.py b/tests/signals/models.py
index ca30cdc51f..b758244749 100644
--- a/tests/signals/models.py
+++ b/tests/signals/models.py
@@ -30,3 +30,8 @@ class Book(models.Model):
def __str__(self):
return self.name
+
+
+class Page(models.Model):
+ book = models.ForeignKey(Book, on_delete=models.CASCADE)
+ text = models.TextField()
diff --git a/tests/signals/tests.py b/tests/signals/tests.py
index c4c692ea3e..b89b7bd91c 100644
--- a/tests/signals/tests.py
+++ b/tests/signals/tests.py
@@ -7,7 +7,7 @@ from django.dispatch import receiver
from django.test import SimpleTestCase, TestCase
from django.test.utils import isolate_apps
-from .models import Author, Book, Car, Person
+from .models import Author, Book, Car, Page, Person
class BaseSignalSetup:
@@ -118,9 +118,9 @@ class SignalTests(BaseSignalSetup, TestCase):
def test_delete_signals(self):
data = []
- def pre_delete_handler(signal, sender, instance, **kwargs):
+ def pre_delete_handler(signal, sender, instance, origin, **kwargs):
data.append(
- (instance, sender, instance.id is None)
+ (instance, sender, instance.id is None, origin)
)
# #8285: signals can be any callable
@@ -128,9 +128,9 @@ class SignalTests(BaseSignalSetup, TestCase):
def __init__(self, data):
self.data = data
- def __call__(self, signal, sender, instance, **kwargs):
+ def __call__(self, signal, sender, instance, origin, **kwargs):
self.data.append(
- (instance, sender, instance.id is None)
+ (instance, sender, instance.id is None, origin)
)
post_delete_handler = PostDeleteHandler(data)
@@ -140,8 +140,8 @@ class SignalTests(BaseSignalSetup, TestCase):
p1 = Person.objects.create(first_name="John", last_name="Smith")
p1.delete()
self.assertEqual(data, [
- (p1, Person, False),
- (p1, Person, False),
+ (p1, Person, False, p1),
+ (p1, Person, False, p1),
])
data[:] = []
@@ -152,8 +152,8 @@ class SignalTests(BaseSignalSetup, TestCase):
p2.save()
p2.delete()
self.assertEqual(data, [
- (p2, Person, False),
- (p2, Person, False),
+ (p2, Person, False, p2),
+ (p2, Person, False, p2),
])
data[:] = []
@@ -167,6 +167,78 @@ class SignalTests(BaseSignalSetup, TestCase):
signals.pre_delete.disconnect(pre_delete_handler)
signals.post_delete.disconnect(post_delete_handler)
+ def test_delete_signals_origin_model(self):
+ data = []
+
+ def pre_delete_handler(signal, sender, instance, origin, **kwargs):
+ data.append((sender, origin))
+
+ def post_delete_handler(signal, sender, instance, origin, **kwargs):
+ data.append((sender, origin))
+
+ person = Person.objects.create(first_name='John', last_name='Smith')
+ book = Book.objects.create(name='Rayuela')
+ Page.objects.create(text='Page 1', book=book)
+ Page.objects.create(text='Page 2', book=book)
+
+ signals.pre_delete.connect(pre_delete_handler, weak=False)
+ signals.post_delete.connect(post_delete_handler, weak=False)
+ try:
+ # Instance deletion.
+ person.delete()
+ self.assertEqual(data, [(Person, person), (Person, person)])
+ data[:] = []
+ # Cascade deletion.
+ book.delete()
+ self.assertEqual(data, [
+ (Page, book),
+ (Page, book),
+ (Book, book),
+ (Page, book),
+ (Page, book),
+ (Book, book),
+ ])
+ finally:
+ signals.pre_delete.disconnect(pre_delete_handler)
+ signals.post_delete.disconnect(post_delete_handler)
+
+ def test_delete_signals_origin_queryset(self):
+ data = []
+
+ def pre_delete_handler(signal, sender, instance, origin, **kwargs):
+ data.append((sender, origin))
+
+ def post_delete_handler(signal, sender, instance, origin, **kwargs):
+ data.append((sender, origin))
+
+ Person.objects.create(first_name='John', last_name='Smith')
+ book = Book.objects.create(name='Rayuela')
+ Page.objects.create(text='Page 1', book=book)
+ Page.objects.create(text='Page 2', book=book)
+
+ signals.pre_delete.connect(pre_delete_handler, weak=False)
+ signals.post_delete.connect(post_delete_handler, weak=False)
+ try:
+ # Queryset deletion.
+ qs = Person.objects.all()
+ qs.delete()
+ self.assertEqual(data, [(Person, qs), (Person, qs)])
+ data[:] = []
+ # Cascade deletion.
+ qs = Book.objects.all()
+ qs.delete()
+ self.assertEqual(data, [
+ (Page, qs),
+ (Page, qs),
+ (Book, qs),
+ (Page, qs),
+ (Page, qs),
+ (Book, qs),
+ ])
+ finally:
+ signals.pre_delete.disconnect(pre_delete_handler)
+ signals.post_delete.disconnect(post_delete_handler)
+
def test_decorators(self):
data = []