Ticket #7539: 7539.on_delete.r14291.diff
File 7539.on_delete.r14291.diff, 50.7 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, source=field.rel.to, source_attr=field.name, nullable=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, source_attr=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 109 new_objs = self.add(objs, source, nullable) 110 if not new_objs: 111 return 112 model = new_objs[0].__class__ 113 114 # Recusively collect parent models, but not their related objects. 115 for parent_model, ptr in model._meta.parents.iteritems(): 116 if ptr: 117 parent_objs = [getattr(obj, ptr.name) for obj in new_objs] 118 self.collect(parent_objs, source=model, source_attr=ptr.rel.related_name, collect_related=False) 119 120 if collect_related: 121 for related in model._meta.get_all_related_objects(): 122 field = related.field 123 if field.rel.is_hidden(): 124 self.add_batch(related.model, field, new_objs) 125 else: 126 sub_objs = related.model._base_manager.using(using).filter(**{"%s__in" % field.name: new_objs}) 127 if not sub_objs: 128 continue 129 field.rel.on_delete(self, field, sub_objs) 130 131 # FIXME: support for generic relations should not require special handling 132 for field in model._meta.many_to_many: 133 if not field.rel.through: 134 # m2m-ish but with no through table? GenericRelation: cascade delete 135 for obj in new_objs: 136 self.collect(field.value_from_object(obj).all(), source=model, source_attr=field.rel.related_name, nullable=True, using=using) 137 138 def instances_with_model(self): 139 for model, instances in self.data.iteritems(): 140 for obj in instances: 141 yield model, obj 142 143 def sort(self): 144 sorted_models = [] 145 models = self.data.keys() 146 while len(sorted_models) < len(models): 147 found = False 148 for model in models: 149 if model in sorted_models: 150 continue 151 dependencies = self.dependencies.get(model) 152 if not dependencies or not dependencies.difference(sorted_models): 153 sorted_models.append(model) 154 found = True 155 if not found: 156 return 157 self.data = SortedDict([(model, self.data[model]) for model in sorted_models]) 158 159 @force_managed 160 def delete(self, using=None): 161 # sort instance collections 162 for instances in self.data.itervalues(): 163 instances.sort(key=lambda obj: obj.pk) 164 165 # if possible, bring the models in an order suitable for databases that don't support transactions 166 # or cannot defer contraint checks until the end of a transaction. 167 self.sort() 168 169 # send pre_delete signals 170 for model, obj in self.instances_with_model(): 171 if not model._meta.auto_created: 172 signals.pre_delete.send(sender=model, instance=obj, using=using) 173 174 # update fields 175 for model, instances_for_fieldvalues in self.field_updates.iteritems(): 176 query = sql.UpdateQuery(model) 177 for (field, value), instances in instances_for_fieldvalues.iteritems(): 178 query.update_batch([obj.pk for obj in instances], {field.name: value}, using) 179 180 # reverse instance collections 181 for instances in self.data.itervalues(): 182 instances.reverse() 183 184 # delete batches 185 for model, batches in self.batches.iteritems(): 186 query = sql.DeleteQuery(model) 187 for field, instances in batches.iteritems(): 188 query.delete_batch([obj.pk for obj in instances], using, field) 189 190 # delete instances 191 for model, instances in self.data.iteritems(): 192 query = sql.DeleteQuery(model) 193 pk_list = [obj.pk for obj in instances] 194 #query.delete_generic_relation_hack(pk_list, using) 195 query.delete_batch(pk_list, using) 196 197 # send post_delete signals 198 for model, obj in self.instances_with_model(): 199 if not model._meta.auto_created: 200 signals.post_delete.send(sender=model, instance=obj, using=using) 201 202 # update collected instances 203 for model, instances_for_fieldvalues in self.field_updates.iteritems(): 204 for (field, value), instances in instances_for_fieldvalues.iteritems(): 205 for obj in instances: 206 setattr(obj, field.attname, value) 207 for model, instances in self.data.iteritems(): 208 for instance in instances: 209 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.db.models.related import RelatedObject 3 4 from django.forms.forms import pretty_name 4 5 from django.utils import formats … … 104 105 method uses this function also from a change_list view. 105 106 This will not be used if we can reverse the URL. 106 107 """ 108 # FIXME: This code is broken. The old version depends on Model._collect_sub_objects() and contained a TODO comment. 107 109 collector = NestedObjects() 108 for obj in objs: 109 # TODO using a private model API! 110 obj._collect_sub_objects(collector) 111 110 collector.collect(objs) 112 111 perms_needed = set() 113 114 112 to_delete = collector.nested(_format_callback, 115 113 user=user, 116 114 admin_site=admin_site, … … 120 118 return to_delete, perms_needed 121 119 122 120 123 class NestedObjects(object): 124 """ 125 A directed acyclic graph collection that exposes the add() API 126 expected by Model._collect_sub_objects and can present its data as 127 a nested list of objects. 128 129 """ 121 class NestedObjects(Collector): 130 122 def __init__(self): 131 # Use object keys of the form (model, pk) because actual model 132 # objects may not be unique 123 super(NestedObjects, self).__init__() 124 self.edges = {} # {from_instance: [to_instances]} 125 126 def add_edge(self, source, target): 127 self.edges.setdefault(source, []).append(target) 128 129 def collect(self, objs, source_attr=None, **kwargs): 130 for obj in objs: 131 if source_attr: 132 self.add_edge(getattr(obj, source_attr), obj) 133 else: 134 self.add_edge(None, obj) 135 return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs) 133 136 134 # maps object key to list of child keys 135 self.children = SortedDict() 136 137 # maps object key to parent key 138 self.parents = SortedDict() 139 140 # maps object key to actual object 141 self.seen = SortedDict() 142 143 def add(self, model, pk, obj, 144 parent_model=None, parent_obj=None, nullable=False): 145 """ 146 Add item ``obj`` to the graph. Returns True (and does nothing) 147 if the item has been seen already. 148 149 The ``parent_obj`` argument must already exist in the graph; if 150 not, it's ignored (but ``obj`` is still added with no 151 parent). In any case, Model._collect_sub_objects (for whom 152 this API exists) will never pass a parent that hasn't already 153 been added itself. 154 155 These restrictions in combination ensure the graph will remain 156 acyclic (but can have multiple roots). 157 158 ``model``, ``pk``, and ``parent_model`` arguments are ignored 159 in favor of the appropriate lookups on ``obj`` and 160 ``parent_obj``; unlike CollectedObjects, we can't maintain 161 independence from the knowledge that we're operating on model 162 instances, and we don't want to allow for inconsistency. 163 164 ``nullable`` arg is ignored: it doesn't affect how the tree of 165 collected objects should be nested for display. 166 """ 167 model, pk = type(obj), obj._get_pk_val() 168 169 # auto-created M2M models don't interest us 170 if model._meta.auto_created: 171 return True 172 173 key = model, pk 174 175 if key in self.seen: 176 return True 177 self.seen.setdefault(key, obj) 178 179 if parent_obj is not None: 180 parent_model, parent_pk = (type(parent_obj), 181 parent_obj._get_pk_val()) 182 parent_key = (parent_model, parent_pk) 183 if parent_key in self.seen: 184 self.children.setdefault(parent_key, list()).append(key) 185 self.parents.setdefault(key, parent_key) 186 187 def _nested(self, key, format_callback=None, **kwargs): 188 obj = self.seen[key] 137 def _nested(self, obj, seen, format_callback, kwargs): 138 if obj in seen: 139 return [] 140 seen.add(obj) 141 children = [] 142 for child in self.edges.get(obj, ()): 143 children.extend(self._nested(child, seen, format_callback, kwargs)) 189 144 if format_callback: 190 145 ret = [format_callback(obj, **kwargs)] 191 146 else: 192 147 ret = [obj] 193 194 children = []195 for child in self.children.get(key, ()):196 children.extend(self._nested(child, format_callback, **kwargs))197 148 if children: 198 149 ret.append(children) 199 200 150 return ret 201 151 202 152 def nested(self, format_callback=None, **kwargs): … … 206 156 Passes **kwargs back to the format_callback as kwargs. 207 157 208 158 """ 159 seen = set() 209 160 roots = [] 210 for key in self.seen.keys(): 211 if key not in self.parents: 212 roots.extend(self._nested(key, format_callback, **kwargs)) 161 for root in self.edges.get(None, ()): 162 roots.extend(self._nested(root, seen, format_callback, kwargs)) 213 163 return roots 214 164 215 165 -
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 -
tests/regressiontests/admin_util/tests.py
26 26 def _check(self, target): 27 27 self.assertEquals(self.n.nested(lambda obj: obj.num), target) 28 28 29 def _add(self, obj, parent=None): 30 # don't bother providing the extra args that NestedObjects ignores 31 self.n.add(None, None, obj, None, parent) 29 def _connect(self, i, j): 30 self.objs[i].parent = self.objs[j] 31 self.objs[i].save() 32 33 def _collect(self, *indices): 34 self.n.collect([self.objs[i] for i in indices]) 32 35 33 36 def test_unrelated_roots(self): 34 self._add(self.objs[0]) 35 self._add(self.objs[1]) 36 self._add(self.objs[2], self.objs[1]) 37 37 self._connect(2, 1) 38 self._collect(0) 39 self._collect(1) 38 40 self._check([0, 1, [2]]) 39 41 40 42 def test_siblings(self): 41 self._add(self.objs[0]) 42 self._add(self.objs[1], self.objs[0]) 43 self._add(self.objs[2], self.objs[0]) 44 43 self._connect(1, 0) 44 self._connect(2, 0) 45 self._collect(0) 45 46 self._check([0, [1, 2]]) 46 47 47 def test_duplicate_instances(self):48 self._add(self.objs[0])49 self._add(self.objs[1])50 dupe = Count.objects.get(num=1)51 self._add(dupe, self.objs[0])52 53 self._check([0, 1])54 55 48 def test_non_added_parent(self): 56 self._ add(self.objs[0], self.objs[1])57 49 self._connect(0, 1) 50 self._collect(0) 58 51 self._check([0]) 59 52 60 53 def test_cyclic(self): 61 self._add(self.objs[0], self.objs[2]) 62 self._add(self.objs[1], self.objs[0]) 63 self._add(self.objs[2], self.objs[1]) 64 self._add(self.objs[0], self.objs[2]) 65 54 self._connect(0, 2) 55 self._connect(1, 0) 56 self._connect(2, 1) 57 self._collect(0) 66 58 self._check([0, [1, [2]]]) 67 59 68 69 60 class UtilTests(unittest.TestCase): 70 61 def test_values_from_lookup_field(self): 71 62 """ -
tests/regressiontests/admin_util/models.py
18 18 19 19 class Count(models.Model): 20 20 num = models.PositiveSmallIntegerField() 21 parent = models.ForeignKey('self', null=True) 21 22 23 def __unicode__(self): 24 return unicode(self.num) 25 22 26 class Event(models.Model): 23 27 date = models.DateTimeField(auto_now_add=True) 24 28