1 | MJG-MBP:django_on_delete_patch mjg$ svn diff
|
---|
2 | Index: django/db/models/base.py
|
---|
3 | ===================================================================
|
---|
4 | --- django/db/models/base.py (revision 11620)
|
---|
5 | +++ django/db/models/base.py (working copy)
|
---|
6 | @@ -13,10 +13,11 @@
|
---|
7 | from django.db.models.fields import AutoField, FieldDoesNotExist
|
---|
8 | from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
|
---|
9 | from django.db.models.query import delete_objects, Q
|
---|
10 | -from django.db.models.query_utils import CollectedObjects, DeferredAttribute
|
---|
11 | +from django.db.models.query_utils import CollectedFields, CollectedObjects, DeferredAttribute
|
---|
12 | from django.db.models.options import Options
|
---|
13 | -from django.db import connection, transaction, DatabaseError
|
---|
14 | +from django.db import connection, transaction, DatabaseError, IntegrityError
|
---|
15 | from django.db.models import signals
|
---|
16 | +from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT
|
---|
17 | from django.db.models.loading import register_models, get_model
|
---|
18 | from django.utils.functional import curry
|
---|
19 | from django.utils.encoding import smart_str, force_unicode, smart_unicode
|
---|
20 | @@ -507,7 +508,7 @@
|
---|
21 |
|
---|
22 | save_base.alters_data = True
|
---|
23 |
|
---|
24 | - def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
|
---|
25 | + def _collect_sub_objects(self, seen_objs, fields_to_set, parent=None, nullable=False):
|
---|
26 | """
|
---|
27 | Recursively populates seen_objs with all objects related to this
|
---|
28 | object.
|
---|
29 | @@ -519,16 +520,65 @@
|
---|
30 | pk_val = self._get_pk_val()
|
---|
31 | if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
|
---|
32 | return
|
---|
33 | +
|
---|
34 | + def _handle_sub_obj(related, sub_obj):
|
---|
35 | + on_delete = related.field.rel.on_delete
|
---|
36 | + if on_delete is None:
|
---|
37 | + #If no explicit on_delete option is specified, use the old
|
---|
38 | + #django behavior as the default: SET_NULL if the foreign
|
---|
39 | + #key is nullable, otherwise CASCADE.
|
---|
40 | + if related.field.null:
|
---|
41 | + on_delete = SET_NULL
|
---|
42 | + else:
|
---|
43 | + on_delete = CASCADE
|
---|
44 | +
|
---|
45 | + if on_delete == CASCADE:
|
---|
46 | + sub_obj._collect_sub_objects(seen_objs, fields_to_set, self.__class__)
|
---|
47 | + elif on_delete == PROTECT:
|
---|
48 | + msg = '[Django] Cannot delete a parent object: a foreign key constraint fails (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
|
---|
49 | + sub_obj.__class__,
|
---|
50 | + sub_obj._get_pk_val(),
|
---|
51 | + self.__class__,
|
---|
52 | + pk_val,
|
---|
53 | + )
|
---|
54 | + raise IntegrityError(msg)
|
---|
55 | + elif on_delete == SET_NULL:
|
---|
56 | + if not related.field.null:
|
---|
57 | + msg = '[Django] Cannot delete a parent object: foreign key constraint on_delete=SET_NULL is specified for a non-nullable foreign key (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
|
---|
58 | + sub_obj.__class__,
|
---|
59 | + sub_obj._get_pk_val(),
|
---|
60 | + self.__class__,
|
---|
61 | + pk_val,
|
---|
62 | + )
|
---|
63 | + raise IntegrityError(msg)
|
---|
64 | + fields_to_set.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name, None)
|
---|
65 | + elif on_delete == SET_DEFAULT:
|
---|
66 | + if not related.field.has_default():
|
---|
67 | + msg = '[Django] Cannot delete a parent object: foreign key constraint on_delete=SET_DEFAULT is specified for a foreign key with no default value (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
|
---|
68 | + sub_obj.__class__,
|
---|
69 | + sub_obj._get_pk_val(),
|
---|
70 | + self.__class__,
|
---|
71 | + pk_val,
|
---|
72 | + )
|
---|
73 | + raise IntegrityError(msg)
|
---|
74 | + fields_to_set.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name, related.field.get_default())
|
---|
75 | + else:
|
---|
76 | + raise AttributeError('Unexpected value for on_delete')
|
---|
77 |
|
---|
78 | for related in self._meta.get_all_related_objects():
|
---|
79 | rel_opts_name = related.get_accessor_name()
|
---|
80 | if isinstance(related.field.rel, OneToOneRel):
|
---|
81 | try:
|
---|
82 | + # delattr(self, rel_opts_name) #Delete first to clear any stale cache
|
---|
83 | + #TODO: the above line is a bit of a hack
|
---|
84 | + #It's one way (not a very good one) to work around stale cache data causing
|
---|
85 | + #spurious RESTRICT errors, etc; it would be better to prevent the cache from
|
---|
86 | + #becoming stale in the first place.
|
---|
87 | sub_obj = getattr(self, rel_opts_name)
|
---|
88 | except ObjectDoesNotExist:
|
---|
89 | pass
|
---|
90 | else:
|
---|
91 | - sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
|
---|
92 | + _handle_sub_obj(related, sub_obj)
|
---|
93 | else:
|
---|
94 | # To make sure we can access all elements, we can't use the
|
---|
95 | # normal manager on the related object. So we work directly
|
---|
96 | @@ -541,7 +591,7 @@
|
---|
97 | raise AssertionError("Should never get here.")
|
---|
98 | delete_qs = rel_descriptor.delete_manager(self).all()
|
---|
99 | for sub_obj in delete_qs:
|
---|
100 | - sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
|
---|
101 | + _handle_sub_obj(related, sub_obj)
|
---|
102 |
|
---|
103 | # Handle any ancestors (for the model-inheritance case). We do this by
|
---|
104 | # traversing to the most remote parent classes -- those with no parents
|
---|
105 | @@ -556,18 +606,18 @@
|
---|
106 | continue
|
---|
107 | # At this point, parent_obj is base class (no ancestor models). So
|
---|
108 | # delete it and all its descendents.
|
---|
109 | - parent_obj._collect_sub_objects(seen_objs)
|
---|
110 | + parent_obj._collect_sub_objects(seen_objs, fields_to_set)
|
---|
111 |
|
---|
112 | def delete(self):
|
---|
113 | 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)
|
---|
114 |
|
---|
115 | # Find all the objects than need to be deleted.
|
---|
116 | seen_objs = CollectedObjects()
|
---|
117 | - self._collect_sub_objects(seen_objs)
|
---|
118 | + fields_to_set = CollectedFields()
|
---|
119 | + self._collect_sub_objects(seen_objs, fields_to_set)
|
---|
120 |
|
---|
121 | # Actually delete the objects.
|
---|
122 | - delete_objects(seen_objs)
|
---|
123 | -
|
---|
124 | + delete_objects(seen_objs, fields_to_set)
|
---|
125 | delete.alters_data = True
|
---|
126 |
|
---|
127 | def _get_FIELD_display(self, field):
|
---|
128 | Index: django/db/models/fields/related.py
|
---|
129 | ===================================================================
|
---|
130 | --- django/db/models/fields/related.py (revision 11620)
|
---|
131 | +++ django/db/models/fields/related.py (working copy)
|
---|
132 | @@ -20,6 +20,16 @@
|
---|
133 |
|
---|
134 | pending_lookups = {}
|
---|
135 |
|
---|
136 | +class CASCADE(object):
|
---|
137 | + pass
|
---|
138 | +class PROTECT(object):
|
---|
139 | + pass
|
---|
140 | +class SET_NULL(object):
|
---|
141 | + pass
|
---|
142 | +class SET_DEFAULT(object):
|
---|
143 | + pass
|
---|
144 | +ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, PROTECT, SET_NULL, SET_DEFAULT])
|
---|
145 | +
|
---|
146 | def add_lazy_relation(cls, field, relation, operation):
|
---|
147 | """
|
---|
148 | Adds a lookup on ``cls`` when a related field is defined using a string,
|
---|
149 | @@ -218,6 +228,16 @@
|
---|
150 | # object you just set.
|
---|
151 | setattr(instance, self.cache_name, value)
|
---|
152 | setattr(value, self.related.field.get_cache_name(), instance)
|
---|
153 | +
|
---|
154 | + #TODO: the following function is a bit of a hack
|
---|
155 | + #It's one way (not a very good one) to work around stale cache data causing
|
---|
156 | + #spurious RESTRICT errors, etc; it would be better to prevent the cache from
|
---|
157 | + #becoming stale in the first place.
|
---|
158 | + # def __delete__(self, instance):
|
---|
159 | + # try:
|
---|
160 | + # return delattr(instance, self.cache_name)
|
---|
161 | + # except AttributeError:
|
---|
162 | + # pass
|
---|
163 |
|
---|
164 | class ReverseSingleRelatedObjectDescriptor(object):
|
---|
165 | # This class provides the functionality that makes the related-object
|
---|
166 | @@ -628,7 +648,8 @@
|
---|
167 |
|
---|
168 | class ManyToOneRel(object):
|
---|
169 | def __init__(self, to, field_name, related_name=None,
|
---|
170 | - limit_choices_to=None, lookup_overrides=None, parent_link=False):
|
---|
171 | + limit_choices_to=None, lookup_overrides=None, parent_link=False,
|
---|
172 | + on_delete=None):
|
---|
173 | try:
|
---|
174 | to._meta
|
---|
175 | except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
|
---|
176 | @@ -641,6 +662,7 @@
|
---|
177 | self.lookup_overrides = lookup_overrides or {}
|
---|
178 | self.multiple = True
|
---|
179 | self.parent_link = parent_link
|
---|
180 | + self.on_delete = on_delete
|
---|
181 |
|
---|
182 | def get_related_field(self):
|
---|
183 | """
|
---|
184 | @@ -655,10 +677,12 @@
|
---|
185 |
|
---|
186 | class OneToOneRel(ManyToOneRel):
|
---|
187 | def __init__(self, to, field_name, related_name=None,
|
---|
188 | - limit_choices_to=None, lookup_overrides=None, parent_link=False):
|
---|
189 | + limit_choices_to=None, lookup_overrides=None, parent_link=False,
|
---|
190 | + on_delete=None):
|
---|
191 | super(OneToOneRel, self).__init__(to, field_name,
|
---|
192 | related_name=related_name, limit_choices_to=limit_choices_to,
|
---|
193 | - lookup_overrides=lookup_overrides, parent_link=parent_link)
|
---|
194 | + lookup_overrides=lookup_overrides, parent_link=parent_link,
|
---|
195 | + on_delete=on_delete)
|
---|
196 | self.multiple = False
|
---|
197 |
|
---|
198 | class ManyToManyRel(object):
|
---|
199 | @@ -697,7 +721,8 @@
|
---|
200 | related_name=kwargs.pop('related_name', None),
|
---|
201 | limit_choices_to=kwargs.pop('limit_choices_to', None),
|
---|
202 | lookup_overrides=kwargs.pop('lookup_overrides', None),
|
---|
203 | - parent_link=kwargs.pop('parent_link', False))
|
---|
204 | + parent_link=kwargs.pop('parent_link', False),
|
---|
205 | + on_delete=kwargs.pop('on_delete', None))
|
---|
206 | Field.__init__(self, **kwargs)
|
---|
207 |
|
---|
208 | self.db_index = True
|
---|
209 | @@ -742,6 +767,16 @@
|
---|
210 | target = self.rel.to._meta.db_table
|
---|
211 | cls._meta.duplicate_targets[self.column] = (target, "o2m")
|
---|
212 |
|
---|
213 | + on_delete = self.rel.on_delete
|
---|
214 | + if on_delete not in ALLOWED_ON_DELETE_ACTION_TYPES:
|
---|
215 | + raise ValueError("Invalid value 'on_delete=%s' specified for %s %s.%s." % (on_delete, type(self).__name__, cls.__name__, name))
|
---|
216 | + if on_delete == SET_NULL and not self.null:
|
---|
217 | + specification = "'on_delete=SET_NULL'"
|
---|
218 | + raise ValueError("%s specified for %s '%s.%s', but the field is not nullable." % (specification, type(self).__name__, cls.__name__, name))
|
---|
219 | + if on_delete == SET_DEFAULT and not self.has_default():
|
---|
220 | + specification = "'on_delete=SET_DEFAULT'"
|
---|
221 | + raise ValueError("%s specified for %s '%s.%s', but the field has no default value." % (specification, type(self).__name__, cls.__name__, name))
|
---|
222 | +
|
---|
223 | def contribute_to_related_class(self, cls, related):
|
---|
224 | setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
|
---|
225 |
|
---|
226 | Index: django/db/models/__init__.py
|
---|
227 | ===================================================================
|
---|
228 | --- django/db/models/__init__.py (revision 11620)
|
---|
229 | +++ django/db/models/__init__.py (working copy)
|
---|
230 | @@ -11,6 +11,7 @@
|
---|
231 | from django.db.models.fields.subclassing import SubfieldBase
|
---|
232 | from django.db.models.fields.files import FileField, ImageField
|
---|
233 | from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
|
---|
234 | +from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT
|
---|
235 | from django.db.models import signals
|
---|
236 |
|
---|
237 | # Admin stages.
|
---|
238 | Index: django/db/models/query.py
|
---|
239 | ===================================================================
|
---|
240 | --- django/db/models/query.py (revision 11620)
|
---|
241 | +++ django/db/models/query.py (working copy)
|
---|
242 | @@ -11,8 +11,9 @@
|
---|
243 |
|
---|
244 | from django.db import connection, transaction, IntegrityError
|
---|
245 | from django.db.models.aggregates import Aggregate
|
---|
246 | +from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
|
---|
247 | from django.db.models.fields import DateField
|
---|
248 | -from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory
|
---|
249 | +from django.db.models.query_utils import Q, select_related_descend, CollectedFields, CollectedObjects, CyclicDependency, deferred_class_factory
|
---|
250 | from django.db.models import signals, sql
|
---|
251 |
|
---|
252 |
|
---|
253 | @@ -391,12 +392,13 @@
|
---|
254 | # Collect all the objects to be deleted in this chunk, and all the
|
---|
255 | # objects that are related to the objects that are to be deleted.
|
---|
256 | seen_objs = CollectedObjects(seen_objs)
|
---|
257 | + fields_to_set = CollectedFields()
|
---|
258 | for object in del_query[:CHUNK_SIZE]:
|
---|
259 | - object._collect_sub_objects(seen_objs)
|
---|
260 | + object._collect_sub_objects(seen_objs, fields_to_set)
|
---|
261 |
|
---|
262 | if not seen_objs:
|
---|
263 | break
|
---|
264 | - delete_objects(seen_objs)
|
---|
265 | + delete_objects(seen_objs, fields_to_set)
|
---|
266 |
|
---|
267 | # Clear the result cache, in case this QuerySet gets reused.
|
---|
268 | self._result_cache = None
|
---|
269 | @@ -1002,7 +1004,7 @@
|
---|
270 | setattr(obj, f.get_cache_name(), rel_obj)
|
---|
271 | return obj, index_end
|
---|
272 |
|
---|
273 | -def delete_objects(seen_objs):
|
---|
274 | +def delete_objects(seen_objs, fields_to_set):
|
---|
275 | """
|
---|
276 | Iterate through a list of seen classes, and remove any instances that are
|
---|
277 | referred to.
|
---|
278 | @@ -1023,6 +1025,19 @@
|
---|
279 |
|
---|
280 | obj_pairs = {}
|
---|
281 | try:
|
---|
282 | + for cls, cls_dct in fields_to_set.iteritems():
|
---|
283 | + #TODO: batch these, similar to UpdateQuery.clear_related?
|
---|
284 | + #(Note that it may be harder to do here because the default value
|
---|
285 | + #for a given field may be different for each instance,
|
---|
286 | + #while UpdateQuery.clear_related always uses the value None).
|
---|
287 | + query = sql.UpdateQuery(cls, connection)
|
---|
288 | + for instance, field_names_and_values in cls_dct.itervalues():
|
---|
289 | + query.where = query.where_class()
|
---|
290 | + pk = query.model._meta.pk
|
---|
291 | + query.where.add((sql.where.Constraint(None, pk.column, pk), 'exact', instance.pk), sql.where.AND)
|
---|
292 | + query.add_update_values(field_names_and_values)
|
---|
293 | + query.execute_sql()
|
---|
294 | +
|
---|
295 | for cls in ordered_classes:
|
---|
296 | items = seen_objs[cls].items()
|
---|
297 | items.sort()
|
---|
298 | @@ -1032,33 +1047,29 @@
|
---|
299 | for pk_val, instance in items:
|
---|
300 | signals.pre_delete.send(sender=cls, instance=instance)
|
---|
301 |
|
---|
302 | + # Handle related GenericRelation and ManyToManyField instances
|
---|
303 | pk_list = [pk for pk,instance in items]
|
---|
304 | del_query = sql.DeleteQuery(cls, connection)
|
---|
305 | del_query.delete_batch_related(pk_list)
|
---|
306 |
|
---|
307 | - update_query = sql.UpdateQuery(cls, connection)
|
---|
308 | - for field, model in cls._meta.get_fields_with_model():
|
---|
309 | - if (field.rel and field.null and field.rel.to in seen_objs and
|
---|
310 | - filter(lambda f: f.column == field.rel.get_related_field().column,
|
---|
311 | - field.rel.to._meta.fields)):
|
---|
312 | - if model:
|
---|
313 | - sql.UpdateQuery(model, connection).clear_related(field,
|
---|
314 | - pk_list)
|
---|
315 | - else:
|
---|
316 | - update_query.clear_related(field, pk_list)
|
---|
317 | -
|
---|
318 | - # Now delete the actual data.
|
---|
319 | for cls in ordered_classes:
|
---|
320 | items = obj_pairs[cls]
|
---|
321 | items.reverse()
|
---|
322 | -
|
---|
323 | pk_list = [pk for pk,instance in items]
|
---|
324 | del_query = sql.DeleteQuery(cls, connection)
|
---|
325 | del_query.delete_batch(pk_list)
|
---|
326 |
|
---|
327 | - # Last cleanup; set NULLs where there once was a reference to the
|
---|
328 | - # object, NULL the primary key of the found objects, and perform
|
---|
329 | - # post-notification.
|
---|
330 | + #Last cleanup; set NULLs and default values where there once was a
|
---|
331 | + #reference to the object, NULL the primary key of the found objects,
|
---|
332 | + #and perform post-notification.
|
---|
333 | + for cls, cls_dct in fields_to_set.iteritems():
|
---|
334 | + for instance, field_names_and_values in cls_dct.itervalues():
|
---|
335 | + for field_name, field_value in field_names_and_values.iteritems():
|
---|
336 | + field = cls._meta.get_field_by_name(field_name)[0]
|
---|
337 | + setattr(instance, field.attname, field_value)
|
---|
338 | + for cls in ordered_classes:
|
---|
339 | + items = obj_pairs[cls]
|
---|
340 | + items.reverse()
|
---|
341 | for pk_val, instance in items:
|
---|
342 | for field in cls._meta.fields:
|
---|
343 | if field.rel and field.null and field.rel.to in seen_objs:
|
---|
344 | Index: django/db/models/query_utils.py
|
---|
345 | ===================================================================
|
---|
346 | --- django/db/models/query_utils.py (revision 11620)
|
---|
347 | +++ django/db/models/query_utils.py (working copy)
|
---|
348 | @@ -124,6 +124,56 @@
|
---|
349 | """
|
---|
350 | return self.data.keys()
|
---|
351 |
|
---|
352 | +class CollectedFields(object):
|
---|
353 | + """
|
---|
354 | + A container that stores the model object and field name
|
---|
355 | + for fields that need to be set to enforce on_delete=SET_NULL
|
---|
356 | + and on_delete=SET_DEFAULT ForeigKey constraints.
|
---|
357 | + """
|
---|
358 | +
|
---|
359 | + def __init__(self):
|
---|
360 | + self.data = {}
|
---|
361 | +
|
---|
362 | + def add(self, model, pk, obj, field_name, field_value):
|
---|
363 | + """
|
---|
364 | + Adds an item.
|
---|
365 | + model is the class of the object being added,
|
---|
366 | + pk is the primary key, obj is the object itself,
|
---|
367 | + field_name is the name of the field to be set,
|
---|
368 | + field_value is the value it needs to be set to.
|
---|
369 | + """
|
---|
370 | + d = self.data.setdefault(model, SortedDict())
|
---|
371 | + obj, field_names_and_values = d.setdefault(pk, (obj, dict()))
|
---|
372 | + assert field_name not in field_names_and_values or field_names_and_values[field_name] == field_value
|
---|
373 | + field_names_and_values[field_name] = field_value
|
---|
374 | +
|
---|
375 | + def __contains__(self, key):
|
---|
376 | + return self.data.__contains__(key)
|
---|
377 | +
|
---|
378 | + def __getitem__(self, key):
|
---|
379 | + return self.data[key]
|
---|
380 | +
|
---|
381 | + def __nonzero__(self):
|
---|
382 | + return bool(self.data)
|
---|
383 | +
|
---|
384 | + def iteritems(self):
|
---|
385 | + return self.data.iteritems()
|
---|
386 | +
|
---|
387 | + def iterkeys(self):
|
---|
388 | + return self.data.iterkeys()
|
---|
389 | +
|
---|
390 | + def itervalues(self):
|
---|
391 | + return self.data.itervalues()
|
---|
392 | +
|
---|
393 | + def items(self):
|
---|
394 | + return self.data.items()
|
---|
395 | +
|
---|
396 | + def keys(self):
|
---|
397 | + return self.data.keys()
|
---|
398 | +
|
---|
399 | + def values(self):
|
---|
400 | + return self.data.values()
|
---|
401 | +
|
---|
402 | class QueryWrapper(object):
|
---|
403 | """
|
---|
404 | A type that indicates the contents are an SQL fragment and the associate
|
---|
405 | Index: tests/modeltests/delete/models.py
|
---|
406 | ===================================================================
|
---|
407 | --- tests/modeltests/delete/models.py (revision 11620)
|
---|
408 | +++ tests/modeltests/delete/models.py (working copy)
|
---|
409 | @@ -46,7 +46,7 @@
|
---|
410 |
|
---|
411 | ## First, test the CollectedObjects data structure directly
|
---|
412 |
|
---|
413 | ->>> from django.db.models.query import CollectedObjects
|
---|
414 | +>>> from django.db.models.query_utils import CollectedFields, CollectedObjects
|
---|
415 |
|
---|
416 | >>> g = CollectedObjects()
|
---|
417 | >>> g.add("key1", 1, "item1", None)
|
---|
418 | @@ -112,10 +112,12 @@
|
---|
419 | >>> d1 = D(c=c1, a=a1)
|
---|
420 | >>> d1.save()
|
---|
421 |
|
---|
422 | ->>> o = CollectedObjects()
|
---|
423 | ->>> a1._collect_sub_objects(o)
|
---|
424 | +>>> o, f = CollectedObjects(), CollectedFields()
|
---|
425 | +>>> a1._collect_sub_objects(o, f)
|
---|
426 | >>> o.keys()
|
---|
427 | [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
|
---|
428 | +>>> f.keys()
|
---|
429 | +[]
|
---|
430 | >>> a1.delete()
|
---|
431 |
|
---|
432 | # Same again with a known bad order
|
---|
433 | @@ -131,10 +133,12 @@
|
---|
434 | >>> d2 = D(c=c2, a=a2)
|
---|
435 | >>> d2.save()
|
---|
436 |
|
---|
437 | ->>> o = CollectedObjects()
|
---|
438 | ->>> a2._collect_sub_objects(o)
|
---|
439 | +>>> o, f = CollectedObjects(), CollectedFields()
|
---|
440 | +>>> a2._collect_sub_objects(o, f)
|
---|
441 | >>> o.keys()
|
---|
442 | [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
|
---|
443 | +>>> f.keys()
|
---|
444 | +[]
|
---|
445 | >>> a2.delete()
|
---|
446 |
|
---|
447 | ### Tests for models E,F - nullable related fields ###
|
---|
448 | @@ -163,21 +167,14 @@
|
---|
449 | # Since E.f is nullable, we should delete F first (after nulling out
|
---|
450 | # the E.f field), then E.
|
---|
451 |
|
---|
452 | ->>> o = CollectedObjects()
|
---|
453 | ->>> e1._collect_sub_objects(o)
|
---|
454 | +>>> o, f = CollectedObjects(), CollectedFields()
|
---|
455 | +>>> e1._collect_sub_objects(o, f)
|
---|
456 | >>> o.keys()
|
---|
457 | [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
|
---|
458 | +>>> f.keys()
|
---|
459 | +[<class 'modeltests.delete.models.E'>]
|
---|
460 |
|
---|
461 | -# temporarily replace the UpdateQuery class to verify that E.f is actually nulled out first
|
---|
462 | ->>> import django.db.models.sql
|
---|
463 | ->>> class LoggingUpdateQuery(django.db.models.sql.UpdateQuery):
|
---|
464 | -... def clear_related(self, related_field, pk_list):
|
---|
465 | -... print "CLEARING FIELD",related_field.name
|
---|
466 | -... return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list)
|
---|
467 | ->>> original_class = django.db.models.sql.UpdateQuery
|
---|
468 | ->>> django.db.models.sql.UpdateQuery = LoggingUpdateQuery
|
---|
469 | >>> e1.delete()
|
---|
470 | -CLEARING FIELD f
|
---|
471 |
|
---|
472 | >>> e2 = E()
|
---|
473 | >>> e2.save()
|
---|
474 | @@ -188,15 +185,13 @@
|
---|
475 |
|
---|
476 | # Same deal as before, though we are starting from the other object.
|
---|
477 |
|
---|
478 | ->>> o = CollectedObjects()
|
---|
479 | ->>> f2._collect_sub_objects(o)
|
---|
480 | +>>> o, f = CollectedObjects(), CollectedFields()
|
---|
481 | +>>> f2._collect_sub_objects(o, f)
|
---|
482 | >>> o.keys()
|
---|
483 | -[<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
|
---|
484 | +[<class 'modeltests.delete.models.F'>]
|
---|
485 | +>>> f.keys()
|
---|
486 | +[<class 'modeltests.delete.models.E'>]
|
---|
487 |
|
---|
488 | >>> f2.delete()
|
---|
489 | -CLEARING FIELD f
|
---|
490 | -
|
---|
491 | -# Put this back to normal
|
---|
492 | ->>> django.db.models.sql.UpdateQuery = original_class
|
---|
493 | """
|
---|
494 | }
|
---|
495 | MJG-MBP:django_on_delete_patch mjg$
|
---|