Ticket #7539: 7539.on_delete.r14291.diff

File 7539.on_delete.r14291.diff, 50.7 KB (added by Johannes Dollinger, 14 years ago)
  • django/db/models/sql/subqueries.py

     
    2626        self.where = where
    2727        self.get_compiler(using).execute_sql(None)
    2828
    29     def delete_batch(self, pk_list, using):
     29    def delete_batch(self, pk_list, using, field=None):
    3030        """
    3131        Set up and execute delete queries for all the objects in pk_list.
    3232
    3333        More than one physical query may be executed if there are a
    3434        lot of values in pk_list.
    3535        """
     36        if not field:
     37            field = self.model._meta.pk
    3638        for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
    3739            where = self.where_class()
    38             field = self.model._meta.pk
    3940            where.add((Constraint(None, field.column, field), 'in',
    4041                    pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND)
    4142            self.do_query(self.model._meta.db_table, where, using=using)
     
    6768                related_updates=self.related_updates.copy(), **kwargs)
    6869
    6970
    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)
    7774        for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
    7875            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',
    8177                    pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
    8278                    AND)
    83             self.values = [(related_field, None, None)]
    8479            self.get_compiler(using).execute_sql(None)
    8580
    8681    def add_update_values(self, values):
  • django/db/models/base.py

     
    77from django.db.models.fields import AutoField, FieldDoesNotExist
    88from django.db.models.fields.related import (OneToOneRel, ManyToOneRel,
    99    OneToOneField, add_lazy_relation)
    10 from django.db.models.query import delete_objects, Q
    11 from django.db.models.query_utils import CollectedObjects, DeferredAttribute
     10from django.db.models.query import Q
     11from django.db.models.query_utils import DeferredAttribute
     12from django.db.models.deletion import Collector
    1213from django.db.models.options import Options
    1314from django.db import connections, router, transaction, DatabaseError, DEFAULT_DB_ALIAS
    1415from django.db.models import signals
     
    561562
    562563    save_base.alters_data = True
    563564
    564     def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
    565         """
    566         Recursively populates seen_objs with all objects related to this
    567         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             return
    577 
    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                     pass
    585                 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 the
    589                 # normal manager on the related object. So we work directly
    590                 # 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                         break
    595                 else:
    596                     # in the case of a hidden fkey just skip it, it'll get
    597                     # processed as an m2m
    598                     if not related.field.rel.is_hidden():
    599                         raise AssertionError("Should never get here.")
    600                     else:
    601                         continue
    602                 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._meta
    610                 reverse_field_name = related.field.m2m_reverse_field_name()
    611                 nullable = opts.get_field(reverse_field_name).null
    612                 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._meta
    620                 field_name = f.m2m_field_name()
    621                 nullable = opts.get_field(field_name).null
    622                 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 delete
    627                 for sub_obj in f.value_from_object(self).all():
    628                     # Generic relations not enforced by db constraints, thus we can set
    629                     # nullable=True, order does not matter
    630                     sub_obj._collect_sub_objects(seen_objs, self, True)
    631 
    632         # Handle any ancestors (for the model-inheritance case). We do this by
    633         # traversing to the most remote parent classes -- those with no parents
    634         # themselves -- and then adding those instances to the collection. That
    635         # 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                 continue
    643             # At this point, parent_obj is base class (no ancestor models). So
    644             # delete it and all its descendents.
    645             parent_obj._collect_sub_objects(seen_objs)
    646 
    647565    def delete(self, using=None):
    648566        using = using or router.db_for_write(self.__class__, instance=self)
    649567        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)
    650568
    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)
    654572
    655         # Actually delete the objects.
    656         delete_objects(seen_objs, using)
    657 
    658573    delete.alters_data = True
    659574
    660575    def _get_FIELD_display(self, field):
  • django/db/models/options.py

     
    370370                    cache[obj] = parent
    371371                else:
    372372                    cache[obj] = model
    373         for klass in get_models():
     373        for klass in get_models(include_auto_created=True):
    374374            for f in klass._meta.local_fields:
    375375                if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
    376376                    cache[RelatedObject(f.rel.to, klass, f)] = None
  • django/db/models/fields/related.py

     
    77from django.db.models.related import RelatedObject
    88from django.db.models.query import QuerySet
    99from django.db.models.query_utils import QueryWrapper
     10from django.db.models.deletion import CASCADE
    1011from django.utils.encoding import smart_unicode
    1112from django.utils.translation import ugettext_lazy as _, string_concat, ungettext, ugettext
    1213from django.utils.functional import curry
     
    733734        manager.add(*value)
    734735
    735736class 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):
    738738        try:
    739739            to._meta
    740740        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
     
    744744        if limit_choices_to is None:
    745745            limit_choices_to = {}
    746746        self.limit_choices_to = limit_choices_to
    747         self.lookup_overrides = lookup_overrides or {}
    748747        self.multiple = True
    749748        self.parent_link = parent_link
     749        self.on_delete = on_delete
    750750
    751751    def is_hidden(self):
    752752        "Should the related object be hidden?"
     
    764764        return data[0]
    765765
    766766class 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):
    769768        super(OneToOneRel, self).__init__(to, field_name,
    770769                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        )
    772772        self.multiple = False
    773773
    774774class ManyToManyRel(object):
     
    820820        kwargs['rel'] = rel_class(to, to_field,
    821821            related_name=kwargs.pop('related_name', None),
    822822            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        )
    825826        Field.__init__(self, **kwargs)
    826827
    827828    def validate(self, value, model_instance):
  • django/db/models/__init__.py

     
    1111from django.db.models.fields.subclassing import SubfieldBase
    1212from django.db.models.fields.files import FileField, ImageField
    1313from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
     14from django.db.models.deletion import CASCADE, PROTECT, SET, SET_NULL, SET_DEFAULT, DO_NOTHING
    1415from django.db.models import signals
    1516
    1617# Admin stages.
  • django/db/models/deletion.py

     
     1from django.utils.datastructures import SortedDict
     2from django.utils.functional import wraps
     3from django.db import connections, transaction, IntegrityError
     4from django.db.models import signals, sql
     5from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
     6
     7def 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
     13def 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
     19def 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
     24def SET_NULL(collector, field, sub_objs):
     25    collector.add_field_update(field, None, sub_objs)
     26
     27def SET_DEFAULT(collector, field, sub_objs):
     28    collector.add_field_update(field, field.get_default(), sub_objs)
     29
     30def DO_NOTHING(collector, field, sub_objs):
     31    pass
     32
     33def 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
     52class 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

     
    77from django.db import connections, router, transaction, IntegrityError
    88from django.db.models.aggregates import Aggregate
    99from django.db.models.fields import DateField
    10 from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory, InvalidQuery
     10from django.db.models.query_utils import Q, select_related_descend, deferred_class_factory, InvalidQuery
     11from django.db.models.deletion import Collector
    1112from django.db.models import signals, sql
    1213from django.utils.copycompat import deepcopy
    1314
     
    426427        del_query.query.select_related = False
    427428        del_query.query.clear_ordering()
    428429
    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)
    441433
    442             if not seen_objs:
    443                 break
    444             delete_objects(seen_objs, del_query.db)
    445 
    446434        # Clear the result cache, in case this QuerySet gets reused.
    447435        self._result_cache = None
    448436    delete.alters_data = True
  • django/db/models/query_utils.py

     
    1414from django.utils.datastructures import SortedDict
    1515
    1616
    17 class CyclicDependency(Exception):
    18     """
    19     An error when dealing with a collection of objects that have a cyclic
    20     dependency, i.e. when deleting multiple objects.
    21     """
    22     pass
    23 
    2417class InvalidQuery(Exception):
    2518    """
    2619    The query passed to raw isn't a safe query to use with raw.
     
    2821    pass
    2922
    3023
    31 class CollectedObjects(object):
    32     """
    33     A container that stores keys and lists of values along with remembering the
    34     parent objects for all the keys.
    35 
    36     This is used for the database object deletion routines so that we can
    37     calculate the 'leaf' objects which should be deleted first.
    38 
    39     previously_seen is an optional argument. It must be a CollectedObjects
    40     instance itself; any previously_seen collected object will be blocked from
    41     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.blocked
    49             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 was
    63           reached through.
    64         * parent_obj - the parent object this object was reached
    65           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 and
    69         False otherwise.
    70         """
    71         if pk in self.blocked.get(model, {}):
    72             return True
    73 
    74         d = self.data.setdefault(model, SortedDict())
    75         retval = pk in d
    76         d[pk] = obj
    77         # Nullable relationships can be ignored -- they are nulled out before
    78         # deleting, and therefore do not affect the order in which objects
    79         # have to be deleted.
    80         if parent_model is not None and not nullable:
    81             self.children.setdefault(parent_model, []).append(model)
    82         return retval
    83 
    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 children
    110         models = self.data.keys()
    111         while len(dealt_with) < len(models):
    112             found = False
    113             for model in models:
    114                 if model in dealt_with:
    115                     continue
    116                 children = self.children.setdefault(model, [])
    117                 if len([c for c in children if c not in dealt_with]) == 0:
    118                     dealt_with[model] = None
    119                     found = True
    120             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 
    13224class QueryWrapper(object):
    13325    """
    13426    A type that indicates the contents are an SQL fragment and the associate
  • django/core/management/validation.py

     
    2222    from django.db import models, connection
    2323    from django.db.models.loading import get_app_errors
    2424    from django.db.models.fields.related import RelatedObject
     25    from django.db.models.deletion import SET_NULL, SET_DEFAULT
    2526
    2627    e = ModelErrorCollection(outfile)
    2728
     
    8586            # Perform any backend-specific field validation.
    8687            connection.validation.validate_field(e, opts, f)
    8788
     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
    8896            # Check to see if the related field will clash with any existing
    8997            # fields, m2m fields, m2m related objects or related objects
    9098            if f.rel:
  • django/contrib/admin/util.py

     
    11from django.db import models
     2from django.db.models.deletion import Collector
    23from django.db.models.related import RelatedObject
    34from django.forms.forms import pretty_name
    45from django.utils import formats
     
    104105    method uses this function also from a change_list view.
    105106    This will not be used if we can reverse the URL.
    106107    """
     108    # FIXME: This code is broken. The old version depends on Model._collect_sub_objects() and contained a TODO comment.
    107109    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)
    112111    perms_needed = set()
    113 
    114112    to_delete = collector.nested(_format_callback,
    115113                                 user=user,
    116114                                 admin_site=admin_site,
     
    120118    return to_delete, perms_needed
    121119
    122120
    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     """
     121class NestedObjects(Collector):
    130122    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)
    133136
    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))
    189144        if format_callback:
    190145            ret = [format_callback(obj, **kwargs)]
    191146        else:
    192147            ret = [obj]
    193 
    194         children = []
    195         for child in self.children.get(key, ()):
    196             children.extend(self._nested(child, format_callback, **kwargs))
    197148        if children:
    198149            ret.append(children)
    199 
    200150        return ret
    201151
    202152    def nested(self, format_callback=None, **kwargs):
     
    206156        Passes **kwargs back to the format_callback as kwargs.
    207157
    208158        """
     159        seen = set()
    209160        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))
    213163        return roots
    214164
    215165
  • tests/modeltests/invalid_models/models.py

     
    207207    tgt = models.ForeignKey(FKTarget, to_field='good')
    208208
    209209
     210class InvalidSetNull(models.Model):
     211    fk = models.ForeignKey('self', on_delete=models.SET_NULL)
     212
     213class InvalidSetDefault(models.Model):
     214    fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT)
     215
     216
    210217model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer.
    211218invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer.
    212219invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer.
     
    311318invalid_models.uniquem2m: ManyToManyFields cannot be unique.  Remove the unique argument on 'unique_people'.
    312319invalid_models.nonuniquefktarget1: Field 'bad' under model 'FKTarget' must have a unique=True constraint.
    313320invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have a unique=True constraint.
     321invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null.
     322invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value.
    314323"""
  • tests/modeltests/delete/tests.py

     
    11from django.db.models import sql
    22from django.db.models.loading import cache
    3 from django.db.models.query import CollectedObjects
    4 from django.db.models.query_utils import CyclicDependency
    53from django.test import TestCase
    64
    75from models import A, B, C, D, E, F
     
    2422        self.order_models("a", "b", "c", "d", "e", "f")
    2523        self.clear_rel_obj_caches(A, B, C, D, E, F)
    2624
    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, doing
    48         # m.delete() here can work but fail in a real situation, since it may
    49         # delete all objects, but not in the right order. So we manually check
    50         # that the order of deletion is correct.
    51 
    52         # Also, it is possible that the order is correct 'accidentally', due
    53         # solely to order of imports etc.  To check this, we set the order that
    54         # 'get_models()' will retrieve to a known 'nice' order, and then try
    55         # again with a known 'tricky' order.  Slightly naughty access to
    56         # 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 related
    60         #    object caches
    61         #  - 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 order
    74         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 = f1
    98         e1.save()
    99 
    100         # Since E.f is nullable, we should delete F first (after nulling out
    101         # 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 is
    108         # actually nulled out first
    109 
    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.UpdateQuery
    116         sql.UpdateQuery = LoggingUpdateQuery
    117 
    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 = f2
    125         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

     
     1from django.test import TestCase
     2from django.db import models, IntegrityError
     3
     4class R(models.Model):
     5    is_default = models.BooleanField(default=False)
     6
     7    def __str__(self):
     8        return "%s" % self.pk
     9
     10get_default_r = lambda: R.objects.get_or_create(is_default=True)[0]
     11   
     12class S(models.Model):
     13    r = models.ForeignKey(R)
     14   
     15class T(models.Model):
     16    s = models.ForeignKey(S)
     17
     18class U(models.Model):
     19    t = models.ForeignKey(T)
     20
     21
     22class 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   
     35def 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   
     43class 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   
     48class MR(models.Model):
     49    m = models.ForeignKey(M)
     50    r = models.ForeignKey(R)
     51
     52class MRNull(models.Model):
     53    m = models.ForeignKey(M)
     54    r = models.ForeignKey(R, null=True, on_delete=models.SET_NULL)
     55
     56class 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

     
    2626    def _check(self, target):
    2727        self.assertEquals(self.n.nested(lambda obj: obj.num), target)
    2828
    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])
    3235
    3336    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)
    3840        self._check([0, 1, [2]])
    3941
    4042    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)
    4546        self._check([0, [1, 2]])
    4647
    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 
    5548    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)
    5851        self._check([0])
    5952
    6053    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)
    6658        self._check([0, [1, [2]]])
    6759
    68 
    6960class UtilTests(unittest.TestCase):
    7061    def test_values_from_lookup_field(self):
    7162        """
  • tests/regressiontests/admin_util/models.py

     
    1818
    1919class Count(models.Model):
    2020    num = models.PositiveSmallIntegerField()
     21    parent = models.ForeignKey('self', null=True)
    2122
     23    def __unicode__(self):
     24        return unicode(self.num)
     25
    2226class Event(models.Model):
    2327    date = models.DateTimeField(auto_now_add=True)
    2428
Back to Top