diff --git a/django/db/models/base.py b/django/db/models/base.py
index 6304e00..3a4d5c2 100644
a
|
b
|
class Model(object):
|
544 | 544 | |
545 | 545 | save_base.alters_data = True |
546 | 546 | |
547 | | def _collect_sub_objects(self, seen_objs, parent=None, nullable=False): |
| 547 | def _collect_sub_objects(self, seen_objs, parent=None, nullable=False, pre_signal=None): |
548 | 548 | """ |
549 | 549 | Recursively populates seen_objs with all objects related to this |
550 | 550 | object. |
… |
… |
class Model(object):
|
553 | 553 | [(model_class, {pk_val: obj, pk_val: obj, ...}), |
554 | 554 | (model_class, {pk_val: obj, pk_val: obj, ...}), ...] |
555 | 555 | """ |
| 556 | if not pre_signal is None and not self.__class__._meta.auto_created: |
| 557 | pre_signal.send(sender=self.__class__, instance=self) |
556 | 558 | pk_val = self._get_pk_val() |
557 | 559 | if seen_objs.add(self.__class__, pk_val, self, |
558 | 560 | type(parent), parent, nullable): |
… |
… |
class Model(object):
|
566 | 568 | except ObjectDoesNotExist: |
567 | 569 | pass |
568 | 570 | else: |
569 | | sub_obj._collect_sub_objects(seen_objs, self, related.field.null) |
| 571 | sub_obj._collect_sub_objects(seen_objs, self, related.field.null, pre_signal) |
570 | 572 | else: |
571 | 573 | # To make sure we can access all elements, we can't use the |
572 | 574 | # normal manager on the related object. So we work directly |
… |
… |
class Model(object):
|
584 | 586 | continue |
585 | 587 | delete_qs = rel_descriptor.delete_manager(self).all() |
586 | 588 | for sub_obj in delete_qs: |
587 | | sub_obj._collect_sub_objects(seen_objs, self, related.field.null) |
| 589 | sub_obj._collect_sub_objects(seen_objs, self, related.field.null, pre_signal) |
588 | 590 | |
589 | 591 | for related in self._meta.get_all_related_many_to_many_objects(): |
590 | 592 | if related.field.rel.through: |
… |
… |
class Model(object):
|
594 | 596 | nullable = opts.get_field(reverse_field_name).null |
595 | 597 | filters = {reverse_field_name: self} |
596 | 598 | for sub_obj in related.field.rel.through._base_manager.using(db).filter(**filters): |
597 | | sub_obj._collect_sub_objects(seen_objs, self, nullable) |
| 599 | sub_obj._collect_sub_objects(seen_objs, self, nullable, pre_signal) |
598 | 600 | |
599 | 601 | for f in self._meta.many_to_many: |
600 | 602 | if f.rel.through: |
… |
… |
class Model(object):
|
604 | 606 | nullable = opts.get_field(field_name).null |
605 | 607 | filters = {field_name: self} |
606 | 608 | for sub_obj in f.rel.through._base_manager.using(db).filter(**filters): |
607 | | sub_obj._collect_sub_objects(seen_objs, self, nullable) |
| 609 | sub_obj._collect_sub_objects(seen_objs, self, nullable, pre_signal) |
608 | 610 | else: |
609 | 611 | # m2m-ish but with no through table? GenericRelation: cascade delete |
610 | 612 | for sub_obj in f.value_from_object(self).all(): |
611 | 613 | # Generic relations not enforced by db constraints, thus we can set |
612 | 614 | # nullable=True, order does not matter |
613 | | sub_obj._collect_sub_objects(seen_objs, self, True) |
| 615 | sub_obj._collect_sub_objects(seen_objs, self, True, pre_signal) |
614 | 616 | |
615 | 617 | # Handle any ancestors (for the model-inheritance case). We do this by |
616 | 618 | # traversing to the most remote parent classes -- those with no parents |
… |
… |
class Model(object):
|
625 | 627 | continue |
626 | 628 | # At this point, parent_obj is base class (no ancestor models). So |
627 | 629 | # delete it and all its descendents. |
628 | | parent_obj._collect_sub_objects(seen_objs) |
| 630 | parent_obj._collect_sub_objects(seen_objs, pre_signal=pre_signal) |
629 | 631 | |
630 | 632 | def delete(self, using=None): |
631 | 633 | using = using or router.db_for_write(self.__class__, instance=self) |
… |
… |
class Model(object):
|
633 | 635 | |
634 | 636 | # Find all the objects than need to be deleted. |
635 | 637 | seen_objs = CollectedObjects() |
636 | | self._collect_sub_objects(seen_objs) |
| 638 | self._collect_sub_objects(seen_objs, pre_signal=signals.pre_delete) |
637 | 639 | |
638 | 640 | # Actually delete the objects. |
639 | 641 | delete_objects(seen_objs, using) |
diff --git a/django/db/models/query.py b/django/db/models/query.py
index d9fbd9b..b9947b7 100644
a
|
b
|
class QuerySet(object):
|
438 | 438 | # need to maintain the query cache on del_query (see #12328) |
439 | 439 | seen_objs = CollectedObjects(seen_objs) |
440 | 440 | for i, obj in izip(xrange(CHUNK_SIZE), del_itr): |
441 | | obj._collect_sub_objects(seen_objs) |
| 441 | obj._collect_sub_objects(seen_objs, pre_signal=signals.pre_delete) |
442 | 442 | |
443 | 443 | if not seen_objs: |
444 | 444 | break |
… |
… |
def delete_objects(seen_objs, using):
|
1308 | 1308 | items.sort() |
1309 | 1309 | obj_pairs[cls] = items |
1310 | 1310 | |
1311 | | # Pre-notify all instances to be deleted. |
1312 | | for pk_val, instance in items: |
1313 | | if not cls._meta.auto_created: |
1314 | | signals.pre_delete.send(sender=cls, instance=instance) |
1315 | | |
1316 | 1311 | pk_list = [pk for pk,instance in items] |
1317 | 1312 | |
1318 | 1313 | update_query = sql.UpdateQuery(cls) |
diff --git a/tests/modeltests/signals/models.py b/tests/modeltests/signals/models.py
index ea8137f..936c476 100644
a
|
b
|
class Person(models.Model):
|
11 | 11 | def __unicode__(self): |
12 | 12 | return u"%s %s" % (self.first_name, self.last_name) |
13 | 13 | |
| 14 | class BlogPost(models.Model): |
| 15 | author = models.ForeignKey(Person, related_name="posts", null=True) |
| 16 | post = models.TextField() |
| 17 | |
| 18 | def __unicode__(self): |
| 19 | max_len = 128 |
| 20 | if len(self.post) > max_len: |
| 21 | post_unicode = self.post[:max_len-len('...')] + '...' |
| 22 | else: |
| 23 | post_unicode = self.post |
| 24 | return u"%s: %s" % (unicode(self.author), post_unicode) |
| 25 | |
14 | 26 | def pre_save_test(signal, sender, instance, **kwargs): |
15 | 27 | print 'pre_save signal,', instance |
16 | 28 | if kwargs.get('raw'): |
diff --git a/tests/modeltests/signals/tests.py b/tests/modeltests/signals/tests.py
index 329636c..2a8f748 100644
a
|
b
|
|
1 | 1 | from django.db.models import signals |
2 | 2 | from django.test import TestCase |
3 | | from modeltests.signals.models import Person |
| 3 | from modeltests.signals.models import Person, BlogPost |
4 | 4 | |
5 | 5 | class MyReceiver(object): |
6 | 6 | def __init__(self, param): |
… |
… |
class MyReceiver(object):
|
11 | 11 | self._run = True |
12 | 12 | signal.disconnect(receiver=self, sender=sender) |
13 | 13 | |
| 14 | def clear_nullable_related(signal, sender, instance, **kwargs): |
| 15 | """ |
| 16 | Clears any nullable foreign key fields on related objects. |
| 17 | This simulates ON DELETE SET NULL behavior manually. |
| 18 | """ |
| 19 | model = instance #didactic rename |
| 20 | for related in model._meta.get_all_related_objects(): |
| 21 | if related.field.null: |
| 22 | accessor = related.get_accessor_name() |
| 23 | try: |
| 24 | related_set = getattr(model, accessor) |
| 25 | except: # some version of DoesNotExist |
| 26 | pass |
| 27 | else: |
| 28 | related_set.clear() |
| 29 | |
| 30 | def is_deleted(model): |
| 31 | return model.__class__.objects.filter(pk=model.pk).count() == 0 |
| 32 | |
14 | 33 | class SignalTests(TestCase): |
15 | 34 | def test_disconnect_in_dispatch(self): |
16 | 35 | """ |
… |
… |
class SignalTests(TestCase):
|
25 | 44 | self.failUnless(a._run) |
26 | 45 | self.failUnless(b._run) |
27 | 46 | self.assertEqual(signals.post_save.receivers, []) |
28 | | |
| 47 | |
| 48 | def test_pre_delete_sent_before_related_objects_cached(self): |
| 49 | """ |
| 50 | Test that the pre_delete signal is sent for a model before its related |
| 51 | objects are cached for deletion. The most common usage for this is to |
| 52 | allow the listener to clear related models so that they are not |
| 53 | cascade deleted. |
| 54 | """ |
| 55 | signals.pre_delete.connect(sender=Person, receiver=clear_nullable_related) |
| 56 | person = Person.objects.create(first_name='Zeddicus', last_name='Zorander') |
| 57 | blog_post = BlogPost.objects.create(author=person, post="The life and times of an ornery wizard.") |
| 58 | person.delete() |
| 59 | self.assertTrue(not is_deleted(blog_post)) |