| 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$
|
|---|