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

This commit is contained in:
mgaligniana 2021-12-27 23:32:07 -03:00 committed by Mariusz Felisiak
parent f1905db6c0
commit fa235004dd
9 changed files with 112 additions and 17 deletions

View File

@ -116,7 +116,7 @@ def get_deleted_objects(objs, request, admin_site):
return [], {}, set(), [] return [], {}, set(), []
else: else:
using = router.db_for_write(obj._meta.model) using = router.db_for_write(obj._meta.model)
collector = NestedObjects(using=using) collector = NestedObjects(using=using, origin=objs)
collector.collect(objs) collector.collect(objs)
perms_needed = set() perms_needed = set()

View File

@ -51,7 +51,7 @@ class Command(BaseCommand):
ct_info = [] ct_info = []
for ct in to_remove: for ct in to_remove:
ct_info.append(' - Content type for %s.%s' % (ct.app_label, ct.model)) 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]) collector.collect([ct])
for obj_type, objs in collector.data.items(): for obj_type, objs in collector.data.items():

View File

@ -987,7 +987,7 @@ class Model(metaclass=ModelBase):
"to None." % (self._meta.object_name, self._meta.pk.attname) "to None." % (self._meta.object_name, self._meta.pk.attname)
) )
using = using or router.db_for_write(self.__class__, instance=self) 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) collector.collect([self], keep_parents=keep_parents)
return collector.delete() return collector.delete()

View File

@ -76,8 +76,10 @@ def get_candidate_relations_to_delete(opts):
class Collector: class Collector:
def __init__(self, using): def __init__(self, using, origin=None):
self.using = using self.using = using
# A Model or QuerySet object.
self.origin = origin
# Initially, {model: {instances}}, later values become lists. # Initially, {model: {instances}}, later values become lists.
self.data = defaultdict(set) self.data = defaultdict(set)
# {model: {(field, value): {instances}}} # {model: {(field, value): {instances}}}
@ -404,7 +406,8 @@ class Collector:
for model, obj in self.instances_with_model(): for model, obj in self.instances_with_model():
if not model._meta.auto_created: if not model._meta.auto_created:
signals.pre_delete.send( signals.pre_delete.send(
sender=model, instance=obj, using=self.using sender=model, instance=obj, using=self.using,
origin=self.origin,
) )
# fast deletes # fast deletes
@ -435,7 +438,8 @@ class Collector:
if not model._meta.auto_created: if not model._meta.auto_created:
for obj in instances: for obj in instances:
signals.post_delete.send( signals.post_delete.send(
sender=model, instance=obj, using=self.using sender=model, instance=obj, using=self.using,
origin=self.origin,
) )
# update collected instances # update collected instances

View File

@ -753,7 +753,7 @@ class QuerySet:
del_query.query.select_related = False del_query.query.select_related = False
del_query.query.clear_ordering(force=True) 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) collector.collect(del_query)
deleted, _rows_count = collector.delete() deleted, _rows_count = collector.delete()

View File

@ -195,6 +195,12 @@ Arguments sent with this signal:
``using`` ``using``
The database alias being used. 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`` ``post_delete``
--------------- ---------------
@ -219,6 +225,12 @@ Arguments sent with this signal:
``using`` ``using``
The database alias being used. 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`` ``m2m_changed``
--------------- ---------------

View File

@ -249,7 +249,9 @@ Serialization
Signals 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 Templates
~~~~~~~~~ ~~~~~~~~~

View File

@ -30,3 +30,8 @@ class Book(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class Page(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
text = models.TextField()

View File

@ -7,7 +7,7 @@ from django.dispatch import receiver
from django.test import SimpleTestCase, TestCase from django.test import SimpleTestCase, TestCase
from django.test.utils import isolate_apps from django.test.utils import isolate_apps
from .models import Author, Book, Car, Person from .models import Author, Book, Car, Page, Person
class BaseSignalSetup: class BaseSignalSetup:
@ -118,9 +118,9 @@ class SignalTests(BaseSignalSetup, TestCase):
def test_delete_signals(self): def test_delete_signals(self):
data = [] data = []
def pre_delete_handler(signal, sender, instance, **kwargs): def pre_delete_handler(signal, sender, instance, origin, **kwargs):
data.append( data.append(
(instance, sender, instance.id is None) (instance, sender, instance.id is None, origin)
) )
# #8285: signals can be any callable # #8285: signals can be any callable
@ -128,9 +128,9 @@ class SignalTests(BaseSignalSetup, TestCase):
def __init__(self, data): def __init__(self, data):
self.data = data self.data = data
def __call__(self, signal, sender, instance, **kwargs): def __call__(self, signal, sender, instance, origin, **kwargs):
self.data.append( self.data.append(
(instance, sender, instance.id is None) (instance, sender, instance.id is None, origin)
) )
post_delete_handler = PostDeleteHandler(data) post_delete_handler = PostDeleteHandler(data)
@ -140,8 +140,8 @@ class SignalTests(BaseSignalSetup, TestCase):
p1 = Person.objects.create(first_name="John", last_name="Smith") p1 = Person.objects.create(first_name="John", last_name="Smith")
p1.delete() p1.delete()
self.assertEqual(data, [ self.assertEqual(data, [
(p1, Person, False), (p1, Person, False, p1),
(p1, Person, False), (p1, Person, False, p1),
]) ])
data[:] = [] data[:] = []
@ -152,8 +152,8 @@ class SignalTests(BaseSignalSetup, TestCase):
p2.save() p2.save()
p2.delete() p2.delete()
self.assertEqual(data, [ self.assertEqual(data, [
(p2, Person, False), (p2, Person, False, p2),
(p2, Person, False), (p2, Person, False, p2),
]) ])
data[:] = [] data[:] = []
@ -167,6 +167,78 @@ class SignalTests(BaseSignalSetup, TestCase):
signals.pre_delete.disconnect(pre_delete_handler) signals.pre_delete.disconnect(pre_delete_handler)
signals.post_delete.disconnect(post_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): def test_decorators(self):
data = [] data = []