Code

Ticket #10262: delete_cascade_option_with_tests_r9834.diff

File delete_cascade_option_with_tests_r9834.diff, 7.1 KB (added by msaelices, 5 years ago)

With unit tests and with delete_cascade for OneToOneRel

Line 
1Index: tests/modeltests/delete/models.py
2===================================================================
3--- tests/modeltests/delete/models.py   (revisión: 9834)
4+++ tests/modeltests/delete/models.py   (copia de trabajo)
5@@ -40,7 +40,16 @@
6 class F(DefaultRepr, models.Model):
7     e = models.ForeignKey(E, related_name='f_rel')
8 
9+# Standard many to one and one to one relation, with no delete cascade behaviour
10+class G(DefaultRepr, models.Model):
11+    pass
12 
13+class H(DefaultRepr, models.Model):
14+    g = models.ForeignKey(G, null=True, delete_cascade=False)
15+
16+class I(DefaultRepr, models.Model):
17+    h = models.OneToOneField(H, null=True, delete_cascade=False)
18+
19 __test__ = {'API_TESTS': """
20 ### Tests for models A,B,C,D ###
21 
22@@ -186,5 +195,21 @@
23 
24 >>> f2.delete()
25 
26+# Testing non delete cascade behaviour.
27+
28+>>> g1 = G()
29+>>> g1.save()
30+>>> h1 = H(g=g1)
31+>>> h1.save()
32+>>> g1.delete()
33+>>> H.objects.count()
34+1
35+
36+>>> i = I(h=h1)
37+>>> i.save()
38+>>> h1.delete()
39+>>> I.objects.count()
40+1
41+
42 """
43 }
44Index: django/db/models/base.py
45===================================================================
46--- django/db/models/base.py    (revisión: 9834)
47+++ django/db/models/base.py    (copia de trabajo)
48@@ -11,7 +11,7 @@
49 import django.db.models.manager     # Imported to register signal handler.
50 from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
51 from django.db.models.fields import AutoField, FieldDoesNotExist
52-from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
53+from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField, RelatedObject
54 from django.db.models.query import delete_objects, Q, CollectedObjects
55 from django.db.models.options import Options
56 from django.db import connection, transaction, DatabaseError
57@@ -456,6 +456,27 @@
58     def delete(self):
59         assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
60 
61+        # Clear all related objects with no delete cascade relation behaviour
62+        related_objects_updated = False
63+        for field in self._meta.get_all_fields():
64+            if isinstance(field, RelatedObject) and \
65+               isinstance(field.field.rel, ManyToOneRel) and \
66+               not field.field.rel.delete_cascade:
67+                if isinstance(field.field.rel, OneToOneRel):
68+                    try:
69+                        rel_obj = getattr(self, field.get_accessor_name())
70+                        setattr(rel_obj, field.field.name, None)
71+                        rel_obj.save()
72+                    except ObjectDoesNotExist:
73+                        pass # nothing to do
74+                else: # ManyToOneRel
75+                    related_manager = getattr(self, field.get_accessor_name())
76+                    related_manager.clear()
77+                related_objects_updated = True
78+        if related_objects_updated:
79+            # we ensure that refill related object cache
80+            del self._meta._related_objects_cache
81+
82         # Find all the objects than need to be deleted.
83         seen_objs = CollectedObjects()
84         self._collect_sub_objects(seen_objs)
85Index: django/db/models/options.py
86===================================================================
87--- django/db/models/options.py (revisión: 9834)
88+++ django/db/models/options.py (copia de trabajo)
89@@ -283,6 +283,18 @@
90             raise FieldDoesNotExist('%s has no field named %r'
91                     % (self.object_name, name))
92 
93+    def get_all_fields(self):
94+        """
95+        Returns a list of all fields that are possible for this model
96+        (including reverse relation names).
97+        """
98+        try:
99+            cache = self._name_map
100+        except AttributeError:
101+            cache = self.init_name_map()
102+        fields = [ r[0] for r in cache.values() ]
103+        return fields
104+
105     def get_all_field_names(self):
106         """
107         Returns a list of all field names that are possible for this model
108Index: django/db/models/fields/related.py
109===================================================================
110--- django/db/models/fields/related.py  (revisión: 9834)
111+++ django/db/models/fields/related.py  (copia de trabajo)
112@@ -583,8 +583,8 @@
113         manager.add(*value)
114 
115 class ManyToOneRel(object):
116-    def __init__(self, to, field_name, related_name=None,
117-            limit_choices_to=None, lookup_overrides=None, parent_link=False):
118+    def __init__(self, to, field_name, related_name=None, limit_choices_to=None,
119+            lookup_overrides=None, parent_link=False, delete_cascade=True):
120         try:
121             to._meta
122         except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
123@@ -597,6 +597,7 @@
124         self.lookup_overrides = lookup_overrides or {}
125         self.multiple = True
126         self.parent_link = parent_link
127+        self.delete_cascade = delete_cascade
128 
129     def get_related_field(self):
130         """
131@@ -610,11 +611,12 @@
132         return data[0]
133 
134 class OneToOneRel(ManyToOneRel):
135-    def __init__(self, to, field_name, related_name=None,
136-            limit_choices_to=None, lookup_overrides=None, parent_link=False):
137+    def __init__(self, to, field_name, related_name=None, limit_choices_to=None,
138+            lookup_overrides=None, parent_link=False, delete_cascade=True):
139         super(OneToOneRel, self).__init__(to, field_name,
140                 related_name=related_name, limit_choices_to=limit_choices_to,
141-                lookup_overrides=lookup_overrides, parent_link=parent_link)
142+                lookup_overrides=lookup_overrides, parent_link=parent_link,
143+                delete_cascade=delete_cascade)
144         self.multiple = False
145 
146 class ManyToManyRel(object):
147@@ -645,10 +647,13 @@
148             related_name=kwargs.pop('related_name', None),
149             limit_choices_to=kwargs.pop('limit_choices_to', None),
150             lookup_overrides=kwargs.pop('lookup_overrides', None),
151-            parent_link=kwargs.pop('parent_link', False))
152+            parent_link=kwargs.pop('parent_link', False),
153+            delete_cascade=kwargs.pop('delete_cascade', True))
154+
155         Field.__init__(self, **kwargs)
156 
157         self.db_index = True
158+        assert self.null or self.rel.delete_cascade, "%s cannot define a not null relation (null=False) without deleting cascade (delete_cascade=False)" % self.__class__.__name__
159 
160     def get_attname(self):
161         return '%s_id' % self.name
162Index: django/contrib/admin/util.py
163===================================================================
164--- django/contrib/admin/util.py        (revisión: 9834)
165+++ django/contrib/admin/util.py        (copia de trabajo)
166@@ -71,7 +71,11 @@
167         if related.opts in opts_seen:
168             continue
169         opts_seen.append(related.opts)
170+        if not related.field.rel.delete_cascade:
171+            # Avoid deleting objects without a delete cascade relation
172+            continue
173         rel_opts_name = related.get_accessor_name()
174+
175         if isinstance(related.field.rel, models.OneToOneRel):
176             try:
177                 sub_obj = getattr(obj, rel_opts_name)