Code

Ticket #6870: 6870.pre_delete.r13248.diff

File 6870.pre_delete.r13248.diff, 8.3 KB (added by cgieringer, 4 years ago)

Removed "".format to make Python 2.4 compliant

Line 
1diff --git a/django/db/models/base.py b/django/db/models/base.py
2index 6304e00..3a4d5c2 100644
3--- a/django/db/models/base.py
4+++ b/django/db/models/base.py
5@@ -544,7 +544,7 @@ class Model(object):
6 
7     save_base.alters_data = True
8 
9-    def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
10+    def _collect_sub_objects(self, seen_objs, parent=None, nullable=False, pre_signal=None):
11         """
12         Recursively populates seen_objs with all objects related to this
13         object.
14@@ -553,6 +553,8 @@ class Model(object):
15             [(model_class, {pk_val: obj, pk_val: obj, ...}),
16              (model_class, {pk_val: obj, pk_val: obj, ...}), ...]
17         """
18+        if not pre_signal is None and not self.__class__._meta.auto_created:
19+            pre_signal.send(sender=self.__class__, instance=self)
20         pk_val = self._get_pk_val()
21         if seen_objs.add(self.__class__, pk_val, self,
22                          type(parent), parent, nullable):
23@@ -566,7 +568,7 @@ class Model(object):
24                 except ObjectDoesNotExist:
25                     pass
26                 else:
27-                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
28+                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null, pre_signal)
29             else:
30                 # To make sure we can access all elements, we can't use the
31                 # normal manager on the related object. So we work directly
32@@ -584,7 +586,7 @@ class Model(object):
33                         continue
34                 delete_qs = rel_descriptor.delete_manager(self).all()
35                 for sub_obj in delete_qs:
36-                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
37+                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null, pre_signal)
38 
39         for related in self._meta.get_all_related_many_to_many_objects():
40             if related.field.rel.through:
41@@ -594,7 +596,7 @@ class Model(object):
42                 nullable = opts.get_field(reverse_field_name).null
43                 filters = {reverse_field_name: self}
44                 for sub_obj in related.field.rel.through._base_manager.using(db).filter(**filters):
45-                    sub_obj._collect_sub_objects(seen_objs, self, nullable)
46+                    sub_obj._collect_sub_objects(seen_objs, self, nullable, pre_signal)
47 
48         for f in self._meta.many_to_many:
49             if f.rel.through:
50@@ -604,13 +606,13 @@ class Model(object):
51                 nullable = opts.get_field(field_name).null
52                 filters = {field_name: self}
53                 for sub_obj in f.rel.through._base_manager.using(db).filter(**filters):
54-                    sub_obj._collect_sub_objects(seen_objs, self, nullable)
55+                    sub_obj._collect_sub_objects(seen_objs, self, nullable, pre_signal)
56             else:
57                 # m2m-ish but with no through table? GenericRelation: cascade delete
58                 for sub_obj in f.value_from_object(self).all():
59                     # Generic relations not enforced by db constraints, thus we can set
60                     # nullable=True, order does not matter
61-                    sub_obj._collect_sub_objects(seen_objs, self, True)
62+                    sub_obj._collect_sub_objects(seen_objs, self, True, pre_signal)
63 
64         # Handle any ancestors (for the model-inheritance case). We do this by
65         # traversing to the most remote parent classes -- those with no parents
66@@ -625,7 +627,7 @@ class Model(object):
67                 continue
68             # At this point, parent_obj is base class (no ancestor models). So
69             # delete it and all its descendents.
70-            parent_obj._collect_sub_objects(seen_objs)
71+            parent_obj._collect_sub_objects(seen_objs, pre_signal=pre_signal)
72 
73     def delete(self, using=None):
74         using = using or router.db_for_write(self.__class__, instance=self)
75@@ -633,7 +635,7 @@ class Model(object):
76 
77         # Find all the objects than need to be deleted.
78         seen_objs = CollectedObjects()
79-        self._collect_sub_objects(seen_objs)
80+        self._collect_sub_objects(seen_objs, pre_signal=signals.pre_delete)
81 
82         # Actually delete the objects.
83         delete_objects(seen_objs, using)
84diff --git a/django/db/models/query.py b/django/db/models/query.py
85index d9fbd9b..b9947b7 100644
86--- a/django/db/models/query.py
87+++ b/django/db/models/query.py
88@@ -438,7 +438,7 @@ class QuerySet(object):
89             # need to maintain the query cache on del_query (see #12328)
90             seen_objs = CollectedObjects(seen_objs)
91             for i, obj in izip(xrange(CHUNK_SIZE), del_itr):
92-                obj._collect_sub_objects(seen_objs)
93+                obj._collect_sub_objects(seen_objs, pre_signal=signals.pre_delete)
94 
95             if not seen_objs:
96                 break
97@@ -1308,11 +1308,6 @@ def delete_objects(seen_objs, using):
98             items.sort()
99             obj_pairs[cls] = items
100 
101-            # Pre-notify all instances to be deleted.
102-            for pk_val, instance in items:
103-                if not cls._meta.auto_created:
104-                    signals.pre_delete.send(sender=cls, instance=instance)
105-
106             pk_list = [pk for pk,instance in items]
107 
108             update_query = sql.UpdateQuery(cls)
109diff --git a/tests/modeltests/signals/models.py b/tests/modeltests/signals/models.py
110index ea8137f..936c476 100644
111--- a/tests/modeltests/signals/models.py
112+++ b/tests/modeltests/signals/models.py
113@@ -11,6 +11,18 @@ class Person(models.Model):
114     def __unicode__(self):
115         return u"%s %s" % (self.first_name, self.last_name)
116 
117+class BlogPost(models.Model):
118+    author = models.ForeignKey(Person, related_name="posts", null=True)
119+    post = models.TextField()
120+   
121+    def __unicode__(self):
122+        max_len = 128
123+        if len(self.post) > max_len:
124+            post_unicode = self.post[:max_len-len('...')] + '...'
125+        else:
126+            post_unicode = self.post
127+        return u"%s: %s" % (unicode(self.author), post_unicode)
128+
129 def pre_save_test(signal, sender, instance, **kwargs):
130     print 'pre_save signal,', instance
131     if kwargs.get('raw'):
132diff --git a/tests/modeltests/signals/tests.py b/tests/modeltests/signals/tests.py
133index 329636c..2a8f748 100644
134--- a/tests/modeltests/signals/tests.py
135+++ b/tests/modeltests/signals/tests.py
136@@ -1,6 +1,6 @@
137 from django.db.models import signals
138 from django.test import TestCase
139-from modeltests.signals.models import Person
140+from modeltests.signals.models import Person, BlogPost
141 
142 class MyReceiver(object):
143     def __init__(self, param):
144@@ -11,6 +11,25 @@ class MyReceiver(object):
145         self._run = True
146         signal.disconnect(receiver=self, sender=sender)
147 
148+def clear_nullable_related(signal, sender, instance, **kwargs):
149+    """
150+    Clears any nullable foreign key fields on related objects.
151+    This simulates ON DELETE SET NULL behavior manually.
152+    """
153+    model = instance #didactic rename
154+    for related in model._meta.get_all_related_objects():
155+        if related.field.null:
156+            accessor = related.get_accessor_name()
157+            try:
158+                related_set = getattr(model, accessor)
159+            except: # some version of DoesNotExist
160+                pass
161+            else:
162+                related_set.clear()
163+
164+def is_deleted(model):
165+    return model.__class__.objects.filter(pk=model.pk).count() == 0
166+
167 class SignalTests(TestCase):
168     def test_disconnect_in_dispatch(self):
169         """
170@@ -25,4 +44,16 @@ class SignalTests(TestCase):
171         self.failUnless(a._run)
172         self.failUnless(b._run)
173         self.assertEqual(signals.post_save.receivers, [])
174-       
175+
176+    def test_pre_delete_sent_before_related_objects_cached(self):
177+        """
178+        Test that the pre_delete signal is sent for a model before its related
179+        objects are cached for deletion.  The most common usage for this is to
180+        allow the listener to clear related models so that they are not
181+        cascade deleted.
182+        """
183+        signals.pre_delete.connect(sender=Person, receiver=clear_nullable_related)
184+        person = Person.objects.create(first_name='Zeddicus', last_name='Zorander')
185+        blog_post = BlogPost.objects.create(author=person, post="The life and times of an ornery wizard.")
186+        person.delete()
187+        self.assertTrue(not is_deleted(blog_post))