Ticket #7539: 7539.on_delete.r14218.diff
File 7539.on_delete.r14218.diff, 47.6 KB (added by , 14 years ago) |
---|
-
django/db/models/sql/subqueries.py
26 26 self.where = where 27 27 self.get_compiler(using).execute_sql(None) 28 28 29 def delete_batch(self, pk_list, using ):29 def delete_batch(self, pk_list, using, field=None): 30 30 """ 31 31 Set up and execute delete queries for all the objects in pk_list. 32 32 33 33 More than one physical query may be executed if there are a 34 34 lot of values in pk_list. 35 35 """ 36 if not field: 37 field = self.model._meta.pk 36 38 for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): 37 39 where = self.where_class() 38 field = self.model._meta.pk39 40 where.add((Constraint(None, field.column, field), 'in', 40 41 pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND) 41 42 self.do_query(self.model._meta.db_table, where, using=using) … … 67 68 related_updates=self.related_updates.copy(), **kwargs) 68 69 69 70 70 def clear_related(self, related_field, pk_list, using): 71 """ 72 Set up and execute an update query that clears related entries for the 73 keys in pk_list. 74 75 This is used by the QuerySet.delete_objects() method. 76 """ 71 def update_batch(self, pk_list, values, using): 72 pk_field = self.model._meta.pk 73 self.add_update_values(values) 77 74 for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): 78 75 self.where = self.where_class() 79 f = self.model._meta.pk 80 self.where.add((Constraint(None, f.column, f), 'in', 76 self.where.add((Constraint(None, pk_field.column, pk_field), 'in', 81 77 pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), 82 78 AND) 83 self.values = [(related_field, None, None)]84 79 self.get_compiler(using).execute_sql(None) 85 80 86 81 def add_update_values(self, values): -
django/db/models/base.py
7 7 from django.db.models.fields import AutoField, FieldDoesNotExist 8 8 from django.db.models.fields.related import (OneToOneRel, ManyToOneRel, 9 9 OneToOneField, add_lazy_relation) 10 from django.db.models.query import delete_objects, Q 11 from django.db.models.query_utils import CollectedObjects, DeferredAttribute 10 from django.db.models.query import Q 11 from django.db.models.query_utils import DeferredAttribute 12 from django.db.models.deletion import Collector 12 13 from django.db.models.options import Options 13 14 from django.db import connections, router, transaction, DatabaseError, DEFAULT_DB_ALIAS 14 15 from django.db.models import signals … … 561 562 562 563 save_base.alters_data = True 563 564 564 def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):565 """566 Recursively populates seen_objs with all objects related to this567 object.568 569 When done, seen_objs.items() will be in the format:570 [(model_class, {pk_val: obj, pk_val: obj, ...}),571 (model_class, {pk_val: obj, pk_val: obj, ...}), ...]572 """573 pk_val = self._get_pk_val()574 if seen_objs.add(self.__class__, pk_val, self,575 type(parent), parent, nullable):576 return577 578 for related in self._meta.get_all_related_objects():579 rel_opts_name = related.get_accessor_name()580 if not related.field.rel.multiple:581 try:582 sub_obj = getattr(self, rel_opts_name)583 except ObjectDoesNotExist:584 pass585 else:586 sub_obj._collect_sub_objects(seen_objs, self, related.field.null)587 else:588 # To make sure we can access all elements, we can't use the589 # normal manager on the related object. So we work directly590 # with the descriptor object.591 for cls in self.__class__.mro():592 if rel_opts_name in cls.__dict__:593 rel_descriptor = cls.__dict__[rel_opts_name]594 break595 else:596 # in the case of a hidden fkey just skip it, it'll get597 # processed as an m2m598 if not related.field.rel.is_hidden():599 raise AssertionError("Should never get here.")600 else:601 continue602 delete_qs = rel_descriptor.delete_manager(self).all()603 for sub_obj in delete_qs:604 sub_obj._collect_sub_objects(seen_objs, self, related.field.null)605 606 for related in self._meta.get_all_related_many_to_many_objects():607 if related.field.rel.through:608 db = router.db_for_write(related.field.rel.through.__class__, instance=self)609 opts = related.field.rel.through._meta610 reverse_field_name = related.field.m2m_reverse_field_name()611 nullable = opts.get_field(reverse_field_name).null612 filters = {reverse_field_name: self}613 for sub_obj in related.field.rel.through._base_manager.using(db).filter(**filters):614 sub_obj._collect_sub_objects(seen_objs, self, nullable)615 616 for f in self._meta.many_to_many:617 if f.rel.through:618 db = router.db_for_write(f.rel.through.__class__, instance=self)619 opts = f.rel.through._meta620 field_name = f.m2m_field_name()621 nullable = opts.get_field(field_name).null622 filters = {field_name: self}623 for sub_obj in f.rel.through._base_manager.using(db).filter(**filters):624 sub_obj._collect_sub_objects(seen_objs, self, nullable)625 else:626 # m2m-ish but with no through table? GenericRelation: cascade delete627 for sub_obj in f.value_from_object(self).all():628 # Generic relations not enforced by db constraints, thus we can set629 # nullable=True, order does not matter630 sub_obj._collect_sub_objects(seen_objs, self, True)631 632 # Handle any ancestors (for the model-inheritance case). We do this by633 # traversing to the most remote parent classes -- those with no parents634 # themselves -- and then adding those instances to the collection. That635 # will include all the child instances down to "self".636 parent_stack = [p for p in self._meta.parents.values() if p is not None]637 while parent_stack:638 link = parent_stack.pop()639 parent_obj = getattr(self, link.name)640 if parent_obj._meta.parents:641 parent_stack.extend(parent_obj._meta.parents.values())642 continue643 # At this point, parent_obj is base class (no ancestor models). So644 # delete it and all its descendents.645 parent_obj._collect_sub_objects(seen_objs)646 647 565 def delete(self, using=None): 648 566 using = using or router.db_for_write(self.__class__, instance=self) 649 567 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) 650 568 651 # Find all the objects than need to be deleted.652 seen_objs = CollectedObjects()653 self._collect_sub_objects(seen_objs)569 collector = Collector() 570 collector.collect([self], using=using) 571 collector.delete(using=using) 654 572 655 # Actually delete the objects.656 delete_objects(seen_objs, using)657 658 573 delete.alters_data = True 659 574 660 575 def _get_FIELD_display(self, field): -
django/db/models/options.py
370 370 cache[obj] = parent 371 371 else: 372 372 cache[obj] = model 373 for klass in get_models( ):373 for klass in get_models(include_auto_created=True): 374 374 for f in klass._meta.local_fields: 375 375 if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta: 376 376 cache[RelatedObject(f.rel.to, klass, f)] = None -
django/db/models/fields/related.py
7 7 from django.db.models.related import RelatedObject 8 8 from django.db.models.query import QuerySet 9 9 from django.db.models.query_utils import QueryWrapper 10 from django.db.models.deletion import CASCADE 10 11 from django.utils.encoding import smart_unicode 11 12 from django.utils.translation import ugettext_lazy as _, string_concat, ungettext, ugettext 12 13 from django.utils.functional import curry … … 733 734 manager.add(*value) 734 735 735 736 class ManyToOneRel(object): 736 def __init__(self, to, field_name, related_name=None, 737 limit_choices_to=None, lookup_overrides=None, parent_link=False): 737 def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): 738 738 try: 739 739 to._meta 740 740 except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT … … 744 744 if limit_choices_to is None: 745 745 limit_choices_to = {} 746 746 self.limit_choices_to = limit_choices_to 747 self.lookup_overrides = lookup_overrides or {}748 747 self.multiple = True 749 748 self.parent_link = parent_link 749 self.on_delete = on_delete 750 750 751 751 def is_hidden(self): 752 752 "Should the related object be hidden?" … … 764 764 return data[0] 765 765 766 766 class OneToOneRel(ManyToOneRel): 767 def __init__(self, to, field_name, related_name=None, 768 limit_choices_to=None, lookup_overrides=None, parent_link=False): 767 def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): 769 768 super(OneToOneRel, self).__init__(to, field_name, 770 769 related_name=related_name, limit_choices_to=limit_choices_to, 771 lookup_overrides=lookup_overrides, parent_link=parent_link) 770 parent_link=parent_link, on_delete=on_delete 771 ) 772 772 self.multiple = False 773 773 774 774 class ManyToManyRel(object): … … 820 820 kwargs['rel'] = rel_class(to, to_field, 821 821 related_name=kwargs.pop('related_name', None), 822 822 limit_choices_to=kwargs.pop('limit_choices_to', None), 823 lookup_overrides=kwargs.pop('lookup_overrides', None), 824 parent_link=kwargs.pop('parent_link', False)) 823 parent_link=kwargs.pop('parent_link', False), 824 on_delete=kwargs.pop('on_delete', CASCADE), 825 ) 825 826 Field.__init__(self, **kwargs) 826 827 827 828 def validate(self, value, model_instance): -
django/db/models/__init__.py
11 11 from django.db.models.fields.subclassing import SubfieldBase 12 12 from django.db.models.fields.files import FileField, ImageField 13 13 from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel 14 from django.db.models.deletion import CASCADE, PROTECT, SET, SET_NULL, SET_DEFAULT, DO_NOTHING 14 15 from django.db.models import signals 15 16 16 17 # Admin stages. -
django/db/models/deletion.py
1 from django.utils.datastructures import SortedDict 2 from django.utils.functional import wraps 3 from django.db import connections, transaction, IntegrityError 4 from django.db.models import signals, sql 5 from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE 6 7 def CASCADE(collector, field, sub_objs): 8 collector.collect(sub_objs, field.rel.to, field.null) 9 if field.null: 10 # FIXME: there should be a connection feature indicating whether nullable related fields should be nulled out before deletion 11 collector.add_field_update(field, None, sub_objs) 12 13 def PROTECT(collector, field, sub_objs): 14 msg = "Cannot delete some instances of model '%s' because they are referenced through a protected foreign key: '%s.%s'" % ( 15 field.rel.to.__name__, sub_objs[0].__class__.__name__, field.name 16 ) 17 raise IntegrityError(msg) 18 19 def SET(value): 20 def set_on_delete(collector, field, sub_objs): 21 collector.add_field_update(field, value, sub_objs) 22 return set_on_delete 23 24 def SET_NULL(collector, field, sub_objs): 25 collector.add_field_update(field, None, sub_objs) 26 27 def SET_DEFAULT(collector, field, sub_objs): 28 collector.add_field_update(field, field.get_default(), sub_objs) 29 30 def DO_NOTHING(collector, field, sub_objs): 31 pass 32 33 def force_managed(func): 34 @wraps(func) 35 def decorated(*args, **kwargs): 36 if not transaction.is_managed(): 37 transaction.enter_transaction_management() 38 forced_managed = True 39 else: 40 forced_managed = False 41 try: 42 func(*args, **kwargs) 43 if forced_managed: 44 transaction.commit() 45 else: 46 transaction.commit_unless_managed() 47 finally: 48 if forced_managed: 49 transaction.leave_transaction_management() 50 return decorated 51 52 class Collector(object): 53 def __init__(self): 54 self.data = {} # {model: [instances]} 55 self.batches = {} # {model: {field: set([instances])}} 56 self.field_updates = {} # {model: {(field, value): set([instances])}} 57 self.dependencies = {} # {model: set([models])} 58 59 def add(self, objs, source=None, nullable=False): 60 """ 61 Adds 'objs' to the collection of objects to be deleted. 62 If the call is the result of a cascade, 'source' should be the model that caused it 63 and 'nullable' should be set to True, if the relation can be null. 64 65 Returns a list of all objects that were not already collected. 66 """ 67 if not objs: 68 return [] 69 new_objs = [] 70 model = objs[0].__class__ 71 instances = self.data.setdefault(model, []) 72 for obj in objs: 73 if obj not in instances: 74 new_objs.append(obj) 75 instances.extend(new_objs) 76 # Nullable relationships can be ignored -- they are nulled out before 77 # deleting, and therefore do not affect the order in which objects 78 # have to be deleted. 79 if new_objs and source is not None and not nullable: 80 self.dependencies.setdefault(source, set()).add(model) 81 return new_objs 82 83 def add_batch(self, model, field, objs): 84 """ 85 Schedules a batch delete. Every instance of 'model' that is related to an instance of 'obj' through 'field' will be deleted. 86 """ 87 self.batches.setdefault(model, {}).setdefault(field, set()).update(objs) 88 89 def add_field_update(self, field, value, objs): 90 """ 91 Schedules a field update. 'objs' must be a homogenous iterable collection of model instances (e.g. a QuerySet). 92 """ 93 objs = list(objs) 94 if not objs: 95 return 96 model = objs[0].__class__ 97 self.field_updates.setdefault(model, {}).setdefault((field, value), set()).update(objs) 98 99 def collect(self, objs, source=None, nullable=False, collect_related=True, using=None): 100 """ 101 Adds 'objs' to the collection of objects to be deleted as well as all parent instances. 102 'objs' must be a homogenous iterable collection of model instances (e.g. a QuerySet). 103 If 'collect_related' is True, related objects will be handled by their respective on_delete handler. 104 105 If the call is the result of a cascade, 'source' should be the model that caused it 106 and 'nullable' should be set to True, if the relation can be null. 107 """ 108 new_objs = self.add(objs, source, nullable) 109 if not new_objs: 110 return 111 model = new_objs[0].__class__ 112 113 # Recusively collect parent models, but not their related objects. 114 for parent, ptr in model._meta.parents.items(): 115 if ptr: 116 parent_objs = [getattr(obj, ptr.name) for obj in new_objs] 117 self.collect(parent_objs, model, collect_related=False) 118 119 if collect_related: 120 for related in model._meta.get_all_related_objects(): 121 field = related.field 122 if field.rel.is_hidden(): 123 self.add_batch(related.model, field, new_objs) 124 else: 125 sub_objs = related.model._base_manager.using(using).filter(**{"%s__in" % field.name: new_objs}) 126 if not sub_objs: 127 continue 128 field.rel.on_delete(self, field, sub_objs) 129 130 # FIXME: support for generic relations should not require special handling 131 for field in model._meta.many_to_many: 132 if not field.rel.through: 133 # m2m-ish but with no through table? GenericRelation: cascade delete 134 for obj in new_objs: 135 self.collect(field.value_from_object(obj).all(), model, nullable=True, using=using) 136 137 def instances_with_model(self): 138 for model, instances in self.data.iteritems(): 139 for obj in instances: 140 yield model, obj 141 142 def __iter__(self): 143 for model, obj in self.instances_with_model(): 144 yield obj 145 for model, batches in self.batches.iteritems(): 146 for field, instances in batches.iteritems(): 147 for obj in model._default_manager.filter(**{'%s__in' % field.name: instances}): 148 yield obj 149 150 def __nonzero__(self): 151 return bool(self.data) 152 153 def sort(self): 154 sorted_models = [] 155 models = self.data.keys() 156 while len(sorted_models) < len(models): 157 found = False 158 for model in models: 159 if model in sorted_models: 160 continue 161 dependencies = self.dependencies.get(model) 162 if not dependencies or not dependencies.difference(sorted_models): 163 sorted_models.append(model) 164 found = True 165 if not found: 166 return 167 self.data = SortedDict([(model, self.data[model]) for model in sorted_models]) 168 169 @force_managed 170 def delete(self, using=None): 171 # sort instance collections 172 for instances in self.data.itervalues(): 173 instances.sort(key=lambda obj: obj.pk) 174 175 # if possible, bring the models in an order suitable for databases that don't support transactions 176 # or cannot defer contraint checks until the end of a transaction. 177 self.sort() 178 179 # send pre_delete signals 180 for model, obj in self.instances_with_model(): 181 if not model._meta.auto_created: 182 signals.pre_delete.send(sender=model, instance=obj, using=using) 183 184 # update fields 185 for model, instances_for_fieldvalues in self.field_updates.iteritems(): 186 query = sql.UpdateQuery(model) 187 for (field, value), instances in instances_for_fieldvalues.iteritems(): 188 query.update_batch([obj.pk for obj in instances], {field.name: value}, using) 189 190 # reverse instance collections 191 for instances in self.data.itervalues(): 192 instances.reverse() 193 194 # delete batches 195 for model, batches in self.batches.iteritems(): 196 query = sql.DeleteQuery(model) 197 for field, instances in batches.iteritems(): 198 query.delete_batch([obj.pk for obj in instances], using, field) 199 200 # delete instances 201 for model, instances in self.data.iteritems(): 202 query = sql.DeleteQuery(model) 203 pk_list = [obj.pk for obj in instances] 204 #query.delete_generic_relation_hack(pk_list, using) 205 query.delete_batch(pk_list, using) 206 207 # send post_delete signals 208 for model, obj in self.instances_with_model(): 209 if not model._meta.auto_created: 210 signals.post_delete.send(sender=model, instance=obj, using=using) 211 212 # update collected instances 213 for model, instances_for_fieldvalues in self.field_updates.iteritems(): 214 for (field, value), instances in instances_for_fieldvalues.iteritems(): 215 for obj in instances: 216 setattr(obj, field.attname, value) 217 for model, instances in self.data.iteritems(): 218 for instance in instances: 219 setattr(instance, model._meta.pk.attname, None) -
django/db/models/query.py
7 7 from django.db import connections, router, transaction, IntegrityError 8 8 from django.db.models.aggregates import Aggregate 9 9 from django.db.models.fields import DateField 10 from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory, InvalidQuery 10 from django.db.models.query_utils import Q, select_related_descend, deferred_class_factory, InvalidQuery 11 from django.db.models.deletion import Collector 11 12 from django.db.models import signals, sql 12 13 from django.utils.copycompat import deepcopy 13 14 … … 426 427 del_query.query.select_related = False 427 428 del_query.query.clear_ordering() 428 429 429 # Delete objects in chunks to prevent the list of related objects from 430 # becoming too long. 431 seen_objs = None 432 del_itr = iter(del_query) 433 while 1: 434 # Collect a chunk of objects to be deleted, and then all the 435 # objects that are related to the objects that are to be deleted. 436 # The chunking *isn't* done by slicing the del_query because we 437 # need to maintain the query cache on del_query (see #12328) 438 seen_objs = CollectedObjects(seen_objs) 439 for i, obj in izip(xrange(CHUNK_SIZE), del_itr): 440 obj._collect_sub_objects(seen_objs) 430 collector = Collector() 431 collector.collect(del_query) 432 collector.delete(using=del_query.db) 441 433 442 if not seen_objs:443 break444 delete_objects(seen_objs, del_query.db)445 446 434 # Clear the result cache, in case this QuerySet gets reused. 447 435 self._result_cache = None 448 436 delete.alters_data = True -
django/db/models/query_utils.py
14 14 from django.utils.datastructures import SortedDict 15 15 16 16 17 class CyclicDependency(Exception):18 """19 An error when dealing with a collection of objects that have a cyclic20 dependency, i.e. when deleting multiple objects.21 """22 pass23 24 17 class InvalidQuery(Exception): 25 18 """ 26 19 The query passed to raw isn't a safe query to use with raw. … … 28 21 pass 29 22 30 23 31 class CollectedObjects(object):32 """33 A container that stores keys and lists of values along with remembering the34 parent objects for all the keys.35 36 This is used for the database object deletion routines so that we can37 calculate the 'leaf' objects which should be deleted first.38 39 previously_seen is an optional argument. It must be a CollectedObjects40 instance itself; any previously_seen collected object will be blocked from41 being added to this instance.42 """43 44 def __init__(self, previously_seen=None):45 self.data = {}46 self.children = {}47 if previously_seen:48 self.blocked = previously_seen.blocked49 for cls, seen in previously_seen.data.items():50 self.blocked.setdefault(cls, SortedDict()).update(seen)51 else:52 self.blocked = {}53 54 def add(self, model, pk, obj, parent_model, parent_obj=None, nullable=False):55 """56 Adds an item to the container.57 58 Arguments:59 * model - the class of the object being added.60 * pk - the primary key.61 * obj - the object itself.62 * parent_model - the model of the parent object that this object was63 reached through.64 * parent_obj - the parent object this object was reached65 through (not used here, but needed in the API for use elsewhere)66 * nullable - should be True if this relation is nullable.67 68 Returns True if the item already existed in the structure and69 False otherwise.70 """71 if pk in self.blocked.get(model, {}):72 return True73 74 d = self.data.setdefault(model, SortedDict())75 retval = pk in d76 d[pk] = obj77 # Nullable relationships can be ignored -- they are nulled out before78 # deleting, and therefore do not affect the order in which objects79 # have to be deleted.80 if parent_model is not None and not nullable:81 self.children.setdefault(parent_model, []).append(model)82 return retval83 84 def __contains__(self, key):85 return self.data.__contains__(key)86 87 def __getitem__(self, key):88 return self.data[key]89 90 def __nonzero__(self):91 return bool(self.data)92 93 def iteritems(self):94 for k in self.ordered_keys():95 yield k, self[k]96 97 def items(self):98 return list(self.iteritems())99 100 def keys(self):101 return self.ordered_keys()102 103 def ordered_keys(self):104 """105 Returns the models in the order that they should be dealt with (i.e.106 models with no dependencies first).107 """108 dealt_with = SortedDict()109 # Start with items that have no children110 models = self.data.keys()111 while len(dealt_with) < len(models):112 found = False113 for model in models:114 if model in dealt_with:115 continue116 children = self.children.setdefault(model, [])117 if len([c for c in children if c not in dealt_with]) == 0:118 dealt_with[model] = None119 found = True120 if not found:121 raise CyclicDependency(122 "There is a cyclic dependency of items to be processed.")123 124 return dealt_with.keys()125 126 def unordered_keys(self):127 """128 Fallback for the case where is a cyclic dependency but we don't care.129 """130 return self.data.keys()131 132 24 class QueryWrapper(object): 133 25 """ 134 26 A type that indicates the contents are an SQL fragment and the associate -
django/core/management/validation.py
22 22 from django.db import models, connection 23 23 from django.db.models.loading import get_app_errors 24 24 from django.db.models.fields.related import RelatedObject 25 from django.db.models.deletion import SET_NULL, SET_DEFAULT 25 26 26 27 e = ModelErrorCollection(outfile) 27 28 … … 85 86 # Perform any backend-specific field validation. 86 87 connection.validation.validate_field(e, opts, f) 87 88 89 # Check if the on_delete behavior is sane 90 if f.rel and hasattr(f.rel, 'on_delete'): 91 if f.rel.on_delete == SET_NULL and not f.null: 92 e.add(opts, "'%s' specifies on_delete=SET_NULL, but cannot be null." % f.name) 93 elif f.rel.on_delete == SET_DEFAULT and not f.has_default(): 94 e.add(opts, "'%s' specifies on_delete=SET_DEFAULT, but has no default value." % f.name) 95 88 96 # Check to see if the related field will clash with any existing 89 97 # fields, m2m fields, m2m related objects or related objects 90 98 if f.rel: -
django/contrib/admin/util.py
1 1 from django.db import models 2 from django.db.models.deletion import Collector 2 3 from django.forms.forms import pretty_name 3 4 from django.utils import formats 4 5 from django.utils.html import escape … … 103 104 method uses this function also from a change_list view. 104 105 This will not be used if we can reverse the URL. 105 106 """ 107 # FIXME: This code is broken. The old version depends on Model._collect_sub_objects() and contained a TODO comment. 106 108 collector = NestedObjects() 107 for obj in objs: 108 # TODO using a private model API! 109 obj._collect_sub_objects(collector) 110 109 collector.collect(objs) 111 110 perms_needed = set() 112 113 111 to_delete = collector.nested(_format_callback, 114 112 user=user, 115 113 admin_site=admin_site, … … 119 117 return to_delete, perms_needed 120 118 121 119 122 class NestedObjects(object): 123 """ 124 A directed acyclic graph collection that exposes the add() API 125 expected by Model._collect_sub_objects and can present its data as 126 a nested list of objects. 127 128 """ 129 def __init__(self): 130 # Use object keys of the form (model, pk) because actual model 131 # objects may not be unique 132 133 # maps object key to list of child keys 134 self.children = SortedDict() 135 136 # maps object key to parent key 137 self.parents = SortedDict() 138 139 # maps object key to actual object 140 self.seen = SortedDict() 141 142 def add(self, model, pk, obj, 143 parent_model=None, parent_obj=None, nullable=False): 144 """ 145 Add item ``obj`` to the graph. Returns True (and does nothing) 146 if the item has been seen already. 147 148 The ``parent_obj`` argument must already exist in the graph; if 149 not, it's ignored (but ``obj`` is still added with no 150 parent). In any case, Model._collect_sub_objects (for whom 151 this API exists) will never pass a parent that hasn't already 152 been added itself. 153 154 These restrictions in combination ensure the graph will remain 155 acyclic (but can have multiple roots). 156 157 ``model``, ``pk``, and ``parent_model`` arguments are ignored 158 in favor of the appropriate lookups on ``obj`` and 159 ``parent_obj``; unlike CollectedObjects, we can't maintain 160 independence from the knowledge that we're operating on model 161 instances, and we don't want to allow for inconsistency. 162 163 ``nullable`` arg is ignored: it doesn't affect how the tree of 164 collected objects should be nested for display. 165 """ 166 model, pk = type(obj), obj._get_pk_val() 167 168 # auto-created M2M models don't interest us 169 if model._meta.auto_created: 170 return True 171 172 key = model, pk 173 174 if key in self.seen: 175 return True 176 self.seen.setdefault(key, obj) 177 178 if parent_obj is not None: 179 parent_model, parent_pk = (type(parent_obj), 180 parent_obj._get_pk_val()) 181 parent_key = (parent_model, parent_pk) 182 if parent_key in self.seen: 183 self.children.setdefault(parent_key, list()).append(key) 184 self.parents.setdefault(key, parent_key) 185 186 def _nested(self, key, format_callback=None, **kwargs): 187 obj = self.seen[key] 188 if format_callback: 189 ret = [format_callback(obj, **kwargs)] 190 else: 191 ret = [obj] 192 193 children = [] 194 for child in self.children.get(key, ()): 195 children.extend(self._nested(child, format_callback, **kwargs)) 196 if children: 197 ret.append(children) 198 199 return ret 200 120 class NestedObjects(Collector): 201 121 def nested(self, format_callback=None, **kwargs): 202 """ 203 Return the graph as a nested list. 122 return [format_callback(obj, **kwargs) for obj in self] 204 123 205 Passes **kwargs back to the format_callback as kwargs.206 124 207 """208 roots = []209 for key in self.seen.keys():210 if key not in self.parents:211 roots.extend(self._nested(key, format_callback, **kwargs))212 return roots213 214 215 125 def model_format_dict(obj): 216 126 """ 217 127 Return a `dict` with keys 'verbose_name' and 'verbose_name_plural', -
tests/modeltests/invalid_models/models.py
207 207 tgt = models.ForeignKey(FKTarget, to_field='good') 208 208 209 209 210 class InvalidSetNull(models.Model): 211 fk = models.ForeignKey('self', on_delete=models.SET_NULL) 212 213 class InvalidSetDefault(models.Model): 214 fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT) 215 216 210 217 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer. 211 218 invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer. 212 219 invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer. … … 311 318 invalid_models.uniquem2m: ManyToManyFields cannot be unique. Remove the unique argument on 'unique_people'. 312 319 invalid_models.nonuniquefktarget1: Field 'bad' under model 'FKTarget' must have a unique=True constraint. 313 320 invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have a unique=True constraint. 321 invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null. 322 invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value. 314 323 """ -
tests/modeltests/delete/tests.py
1 1 from django.db.models import sql 2 2 from django.db.models.loading import cache 3 from django.db.models.query import CollectedObjects4 from django.db.models.query_utils import CyclicDependency5 3 from django.test import TestCase 6 4 7 5 from models import A, B, C, D, E, F … … 24 22 self.order_models("a", "b", "c", "d", "e", "f") 25 23 self.clear_rel_obj_caches(A, B, C, D, E, F) 26 24 27 def test_collected_objects(self):28 g = CollectedObjects()29 self.assertFalse(g.add("key1", 1, "item1", None))30 self.assertEqual(g["key1"], {1: "item1"})31 32 self.assertFalse(g.add("key2", 1, "item1", "key1"))33 self.assertFalse(g.add("key2", 2, "item2", "key1"))34 35 self.assertEqual(g["key2"], {1: "item1", 2: "item2"})36 37 self.assertFalse(g.add("key3", 1, "item1", "key1"))38 self.assertTrue(g.add("key3", 1, "item1", "key2"))39 self.assertEqual(g.ordered_keys(), ["key3", "key2", "key1"])40 41 self.assertTrue(g.add("key2", 1, "item1", "key3"))42 self.assertRaises(CyclicDependency, g.ordered_keys)43 44 def test_delete(self):45 ## Second, test the usage of CollectedObjects by Model.delete()46 47 # Due to the way that transactions work in the test harness, doing48 # m.delete() here can work but fail in a real situation, since it may49 # delete all objects, but not in the right order. So we manually check50 # that the order of deletion is correct.51 52 # Also, it is possible that the order is correct 'accidentally', due53 # solely to order of imports etc. To check this, we set the order that54 # 'get_models()' will retrieve to a known 'nice' order, and then try55 # again with a known 'tricky' order. Slightly naughty access to56 # internals here :-)57 58 # If implementation changes, then the tests may need to be simplified:59 # - remove the lines that set the .keyOrder and clear the related60 # object caches61 # - remove the second set of tests (with a2, b2 etc)62 63 a1 = A.objects.create()64 b1 = B.objects.create(a=a1)65 c1 = C.objects.create(b=b1)66 d1 = D.objects.create(c=c1, a=a1)67 68 o = CollectedObjects()69 a1._collect_sub_objects(o)70 self.assertEqual(o.keys(), [D, C, B, A])71 a1.delete()72 73 # Same again with a known bad order74 self.order_models("d", "c", "b", "a")75 self.clear_rel_obj_caches(A, B, C, D)76 77 a2 = A.objects.create()78 b2 = B.objects.create(a=a2)79 c2 = C.objects.create(b=b2)80 d2 = D.objects.create(c=c2, a=a2)81 82 o = CollectedObjects()83 a2._collect_sub_objects(o)84 self.assertEqual(o.keys(), [D, C, B, A])85 a2.delete()86 87 def test_collected_objects_null(self):88 g = CollectedObjects()89 self.assertFalse(g.add("key1", 1, "item1", None))90 self.assertFalse(g.add("key2", 1, "item1", "key1", nullable=True))91 self.assertTrue(g.add("key1", 1, "item1", "key2"))92 self.assertEqual(g.ordered_keys(), ["key1", "key2"])93 94 def test_delete_nullable(self):95 e1 = E.objects.create()96 f1 = F.objects.create(e=e1)97 e1.f = f198 e1.save()99 100 # Since E.f is nullable, we should delete F first (after nulling out101 # the E.f field), then E.102 103 o = CollectedObjects()104 e1._collect_sub_objects(o)105 self.assertEqual(o.keys(), [F, E])106 107 # temporarily replace the UpdateQuery class to verify that E.f is108 # actually nulled out first109 110 logged = []111 class LoggingUpdateQuery(sql.UpdateQuery):112 def clear_related(self, related_field, pk_list, using):113 logged.append(related_field.name)114 return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list, using)115 original = sql.UpdateQuery116 sql.UpdateQuery = LoggingUpdateQuery117 118 e1.delete()119 self.assertEqual(logged, ["f"])120 logged = []121 122 e2 = E.objects.create()123 f2 = F.objects.create(e=e2)124 e2.f = f2125 e2.save()126 127 # Same deal as before, though we are starting from the other object.128 o = CollectedObjects()129 f2._collect_sub_objects(o)130 self.assertEqual(o.keys(), [F, E])131 f2.delete()132 self.assertEqual(logged, ["f"])133 logged = []134 135 sql.UpdateQuery = original -
tests/modeltests/on_delete/__init__.py
1 # 2 No newline at end of file -
tests/modeltests/on_delete/models.py
1 from django.test import TestCase 2 from django.db import models, IntegrityError 3 4 class R(models.Model): 5 is_default = models.BooleanField(default=False) 6 7 def __str__(self): 8 return "%s" % self.pk 9 10 get_default_r = lambda: R.objects.get_or_create(is_default=True)[0] 11 12 class S(models.Model): 13 r = models.ForeignKey(R) 14 15 class T(models.Model): 16 s = models.ForeignKey(S) 17 18 class U(models.Model): 19 t = models.ForeignKey(T) 20 21 22 class A(models.Model): 23 name = models.CharField(max_length=10) 24 25 auto = models.ForeignKey(R, related_name="auto_set") 26 auto_nullable = models.ForeignKey(R, null=True, related_name='auto_nullable_set') 27 setnull = models.ForeignKey(R, on_delete=models.SET_NULL, null=True, related_name='setnull_set') 28 setdefault = models.ForeignKey(R, on_delete=models.SET_DEFAULT, default=get_default_r, related_name='setdefault_set') 29 setdefault_none = models.ForeignKey(R, on_delete=models.SET_DEFAULT, default=None, null=True, related_name='setnull_nullable_set') 30 cascade = models.ForeignKey(R, on_delete=models.CASCADE, related_name='cascade_set') 31 cascade_nullable = models.ForeignKey(R, on_delete=models.CASCADE, null=True, related_name='cascade_nullable_set') 32 protect = models.ForeignKey(R, on_delete=models.PROTECT, null=True) 33 donothing = models.ForeignKey(R, on_delete=models.DO_NOTHING, null=True, related_name='donothing_set') 34 35 def create_a(name): 36 a = A(name=name) 37 for name in ('auto', 'auto_nullable', 'setnull', 'setdefault', 'setdefault_none', 'cascade', 'cascade_nullable', 'protect', 'donothing'): 38 r = R.objects.create() 39 setattr(a, name, r) 40 a.save() 41 return a 42 43 class M(models.Model): 44 m2m = models.ManyToManyField(R, related_name="m_set") 45 m2m_through = models.ManyToManyField(R, through="MR", related_name="m_through_set") 46 m2m_through_null = models.ManyToManyField(R, through="MRNull", related_name="m_through_null_set") 47 48 class MR(models.Model): 49 m = models.ForeignKey(M) 50 r = models.ForeignKey(R) 51 52 class MRNull(models.Model): 53 m = models.ForeignKey(M) 54 r = models.ForeignKey(R, null=True, on_delete=models.SET_NULL) 55 56 class OnDeleteTests(TestCase): 57 def test_basics(self): 58 DEFAULT = get_default_r() 59 60 a = create_a('auto') 61 a.auto.delete() 62 self.failIf(A.objects.filter(name='auto').exists()) 63 64 a = create_a('auto_nullable') 65 a.auto_nullable.delete() 66 self.failIf(A.objects.filter(name='auto_nullable').exists()) 67 68 a = create_a('setnull') 69 a.setnull.delete() 70 a = A.objects.get(pk=a.pk) 71 self.failUnlessEqual(None, a.setnull) 72 73 a = create_a('setdefault') 74 a.setdefault.delete() 75 a = A.objects.get(pk=a.pk) 76 self.failUnlessEqual(DEFAULT, a.setdefault) 77 78 a = create_a('setdefault_none') 79 a.setdefault_none.delete() 80 a = A.objects.get(pk=a.pk) 81 self.failUnlessEqual(None, a.setdefault_none) 82 83 a = create_a('cascade') 84 a.cascade.delete() 85 self.failIf(A.objects.filter(name='cascade').exists()) 86 87 a = create_a('cascade_nullable') 88 a.cascade_nullable.delete() 89 self.failIf(A.objects.filter(name='cascade_nullable').exists()) 90 91 a = create_a('protect') 92 self.assertRaises(IntegrityError, a.protect.delete) 93 94 # Testing DO_NOTHING is a bit harder: It would raise IntegrityError for a normal model, 95 # so we connect to pre_delete and set the fk to a known value. 96 replacement_r = R.objects.create() 97 def check_do_nothing(sender, **kwargs): 98 obj = kwargs['instance'] 99 obj.donothing_set.update(donothing=replacement_r) 100 models.signals.pre_delete.connect(check_do_nothing) 101 a = create_a('do_nothing') 102 a.donothing.delete() 103 a = A.objects.get(pk=a.pk) 104 self.failUnlessEqual(replacement_r, a.donothing) 105 models.signals.pre_delete.disconnect(check_do_nothing) 106 107 A.objects.all().update(protect=None, donothing=None) 108 R.objects.all().delete() 109 self.failIf(A.objects.exists()) 110 111 def test_m2m(self): 112 m = M.objects.create() 113 r = R.objects.create() 114 MR.objects.create(m=m, r=r) 115 r.delete() 116 self.failIf(MR.objects.exists()) 117 118 r = R.objects.create() 119 MR.objects.create(m=m, r=r) 120 m.delete() 121 self.failIf(MR.objects.exists()) 122 123 m = M.objects.create() 124 r = R.objects.create() 125 m.m2m.add(r) 126 r.delete() 127 through = M._meta.get_field('m2m').rel.through 128 self.failIf(through.objects.exists()) 129 130 r = R.objects.create() 131 m.m2m.add(r) 132 m.delete() 133 self.failIf(through.objects.exists()) 134 135 m = M.objects.create() 136 r = R.objects.create() 137 MRNull.objects.create(m=m, r=r) 138 r.delete() 139 self.failIf(not MRNull.objects.exists()) 140 self.failIf(m.m2m_through_null.exists()) 141 142 143 def assert_num_queries(self, num, func, *args, **kwargs): 144 # FIXME: replace with the new builtin method 145 from django.conf import settings 146 from django.db import connection 147 old_debug = settings.DEBUG 148 settings.DEBUG = True 149 query_count = len(connection.queries) 150 func(*args, **kwargs) 151 self.failUnlessEqual(num, len(connection.queries) - query_count) 152 connection.queries = connection.queries[:query_count] 153 settings.DEBUG = old_debug 154 155 def test_bulk(self): 156 from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE 157 s = S.objects.create(r=R.objects.create()) 158 for i in xrange(2*GET_ITERATOR_CHUNK_SIZE): 159 T.objects.create(s=s) 160 # 1 (select related `T` instances) 161 # + 1 (select related `U` instances) 162 # + 2 (delete `T` instances in batches) 163 # + 1 (delete `s`) 164 self.assert_num_queries(5, s.delete) 165 self.failIf(S.objects.exists()) 166 167 def test_instance_update(self): 168 deleted = [] 169 related_setnull_sets = [] 170 def pre_delete(sender, **kwargs): 171 obj = kwargs['instance'] 172 deleted.append(obj) 173 if isinstance(obj, R): 174 related_setnull_sets.append(list(a.pk for a in obj.setnull_set.all())) 175 176 models.signals.pre_delete.connect(pre_delete) 177 a = create_a('update_setnull') 178 a.setnull.delete() 179 180 a = create_a('update_cascade') 181 a.cascade.delete() 182 183 for obj in deleted: 184 self.failUnlessEqual(None, obj.pk) 185 186 for pk_list in related_setnull_sets: 187 for a in A.objects.filter(id__in=pk_list): 188 self.failUnlessEqual(None, a.setnull) 189 190 models.signals.pre_delete.disconnect(pre_delete) 191 192 def test_deletion_order(self): 193 pre_delete_order = [] 194 post_delete_order = [] 195 196 def log_post_delete(sender, **kwargs): 197 pre_delete_order.append((sender, kwargs['instance'].pk)) 198 199 def log_pre_delete(sender, **kwargs): 200 post_delete_order.append((sender, kwargs['instance'].pk)) 201 202 models.signals.post_delete.connect(log_post_delete) 203 models.signals.pre_delete.connect(log_pre_delete) 204 205 r = R.objects.create(pk=1) 206 s1 = S.objects.create(pk=1, r=r) 207 s2 = S.objects.create(pk=2, r=r) 208 t1 = T.objects.create(pk=1, s=s1) 209 t2 = T.objects.create(pk=2, s=s2) 210 r.delete() 211 self.failUnlessEqual(pre_delete_order, [(T, 2), (T, 1), (S, 2), (S, 1), (R, 1)]) 212 self.failUnlessEqual(post_delete_order, [(T, 1), (T, 2), (S, 1), (S, 2), (R, 1)]) 213 214 models.signals.post_delete.disconnect(log_post_delete) 215 models.signals.post_delete.disconnect(log_pre_delete) 216