浏览代码

Fixed #13251 -- Made pre/post_delete signals dispatch the origin.

mgaligniana 3 年之前
父节点
当前提交
fa235004dd

+ 1 - 1
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()
 

+ 1 - 1
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():

+ 1 - 1
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()
 

+ 7 - 3
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

+ 1 - 1
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()
 

+ 12 - 0
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``
 ---------------
 

+ 3 - 1
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
 ~~~~~~~~~

+ 5 - 0
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()

+ 81 - 9
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 = []