Django

Code

Ticket #7539: 7539.on_delete.r12009.diff

File 7539.on_delete.r12009.diff, 45.8 kB (added by emulbreh, 3 months ago)
  • tests/modeltests/invalid_models/models.py

    old new  
    181181class UniqueM2M(models.Model): 
    182182    """ Model to test for unique ManyToManyFields, which are invalid. """ 
    183183    unique_people = models.ManyToManyField( Person, unique=True ) 
     184     
     185class InvalidSetNull(models.Model): 
     186    fk = models.ForeignKey('self', on_delete=models.SET_NULL) 
     187     
     188class InvalidSetDefault(models.Model): 
     189    fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT) 
    184190 
    185191 
    186192model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. 
     
    279285invalid_models.abstractrelationmodel: 'fk1' has a relation with model AbstractModel, which has either not been installed or is abstract. 
    280286invalid_models.abstractrelationmodel: 'fk2' has an m2m relation with model AbstractModel, which has either not been installed or is abstract. 
    281287invalid_models.uniquem2m: ManyToManyFields cannot be unique.  Remove the unique argument on 'unique_people'. 
     288invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null. 
     289invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value. 
    282290""" 
  • tests/modeltests/on_delete/__init__.py

    old new  
  • tests/modeltests/on_delete/models.py

    old new  
     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        from django.conf import settings 
     145        from django.db import connection 
     146        old_debug = settings.DEBUG 
     147        settings.DEBUG = True 
     148        query_count = len(connection.queries) 
     149        func(*args, **kwargs) 
     150        self.failUnlessEqual(num, len(connection.queries) - query_count) 
     151        connection.queries = connection.queries[:query_count] 
     152        settings.DEBUG = old_debug 
     153     
     154    def test_bulk(self): 
     155        from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE 
     156        s = S.objects.create(r=R.objects.create()) 
     157        for i in xrange(2*GET_ITERATOR_CHUNK_SIZE): 
     158            T.objects.create(s=s) 
     159        #   1 (select related `T` instances) 
     160        # + 1 (select related `U` instances) 
     161        # + 2 (delete `T` instances in batches) 
     162        # + 1 (delete `s`) 
     163        self.assert_num_queries(5, s.delete) 
     164        self.failIf(S.objects.exists()) 
     165         
     166    def test_instance_update(self): 
     167        deleted = [] 
     168        related_setnull_sets = [] 
     169        def pre_delete(sender, **kwargs): 
     170            obj = kwargs['instance'] 
     171            deleted.append(obj) 
     172            if isinstance(obj, R): 
     173                related_setnull_sets.append(list(a.pk for a in obj.setnull_set.all())) 
     174 
     175        models.signals.pre_delete.connect(pre_delete) 
     176        a = create_a('update_setnull') 
     177        a.setnull.delete() 
     178         
     179        a = create_a('update_cascade') 
     180        a.cascade.delete() 
     181         
     182        for obj in deleted: 
     183            self.failUnlessEqual(None, obj.pk) 
     184             
     185        for pk_list in related_setnull_sets: 
     186            for a in A.objects.filter(id__in=pk_list): 
     187                self.failUnlessEqual(None, a.setnull) 
     188         
     189        models.signals.pre_delete.disconnect(pre_delete) 
     190 
     191    def test_deletion_order(self): 
     192        pre_delete_order = [] 
     193        post_delete_order = [] 
     194 
     195        def log_post_delete(sender, **kwargs): 
     196            pre_delete_order.append((sender, kwargs['instance'].pk)) 
     197 
     198        def log_pre_delete(sender, **kwargs): 
     199            post_delete_order.append((sender, kwargs['instance'].pk)) 
     200         
     201        models.signals.post_delete.connect(log_post_delete) 
     202        models.signals.pre_delete.connect(log_pre_delete) 
     203         
     204        r = R.objects.create(pk=1) 
     205        s1 = S.objects.create(pk=1, r=r) 
     206        s2 = S.objects.create(pk=2, r=r) 
     207        t1 = T.objects.create(pk=1, s=s1) 
     208        t2 = T.objects.create(pk=2, s=s2) 
     209        r.delete() 
     210        self.failUnlessEqual(pre_delete_order, [(T, 2), (T, 1), (S, 2), (S, 1), (R, 1)]) 
     211        self.failUnlessEqual(post_delete_order, [(T, 1), (T, 2), (S, 1), (S, 2), (R, 1)]) 
     212         
     213        models.signals.post_delete.disconnect(log_post_delete) 
     214        models.signals.post_delete.disconnect(log_pre_delete) 
     215         
  • tests/modeltests/delete/models.py

    old new  
    4444__test__ = {'API_TESTS': """ 
    4545### Tests for models A,B,C,D ### 
    4646 
    47 ## First, test the CollectedObjects data structure directly 
    48  
    49 >>> from django.db.models.query import CollectedObjects 
    50  
    51 >>> g = CollectedObjects() 
    52 >>> g.add("key1", 1, "item1", None) 
    53 False 
    54 >>> g["key1"] 
    55 {1: 'item1'} 
    56 >>> g.add("key2", 1, "item1", "key1") 
    57 False 
    58 >>> g.add("key2", 2, "item2", "key1") 
    59 False 
    60 >>> g["key2"] 
    61 {1: 'item1', 2: 'item2'} 
    62 >>> g.add("key3", 1, "item1", "key1") 
    63 False 
    64 >>> g.add("key3", 1, "item1", "key2") 
    65 True 
    66 >>> g.ordered_keys() 
    67 ['key3', 'key2', 'key1'] 
    68  
    69 >>> g.add("key2", 1, "item1", "key3") 
    70 True 
    71 >>> g.ordered_keys() 
    72 Traceback (most recent call last): 
    73     ... 
    74 CyclicDependency: There is a cyclic dependency of items to be processed. 
    75  
    76  
    77 ## Second, test the usage of CollectedObjects by Model.delete() 
    78  
    7947# Due to the way that transactions work in the test harness, 
    8048# doing m.delete() here can work but fail in a real situation, 
    8149# since it may delete all objects, but not in the right order. 
     
    11179>>> c1.save() 
    11280>>> d1 = D(c=c1, a=a1) 
    11381>>> d1.save() 
    114  
    115 >>> o = CollectedObjects() 
    116 >>> a1._collect_sub_objects(o) 
    117 >>> o.keys() 
    118 [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>] 
    11982>>> a1.delete() 
    12083 
    12184# Same again with a known bad order 
     
    13093>>> c2.save() 
    13194>>> d2 = D(c=c2, a=a2) 
    13295>>> d2.save() 
    133  
    134 >>> o = CollectedObjects() 
    135 >>> a2._collect_sub_objects(o) 
    136 >>> o.keys() 
    137 [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>] 
    13896>>> a2.delete() 
    13997 
    14098### Tests for models E,F - nullable related fields ### 
    14199 
    142 ## First, test the CollectedObjects data structure directly 
    143  
    144 >>> g = CollectedObjects() 
    145 >>> g.add("key1", 1, "item1", None) 
    146 False 
    147 >>> g.add("key2", 1, "item1", "key1", nullable=True) 
    148 False 
    149 >>> g.add("key1", 1, "item1", "key2") 
    150 True 
    151 >>> g.ordered_keys() 
    152 ['key1', 'key2'] 
    153  
    154 ## Second, test the usage of CollectedObjects by Model.delete() 
    155  
    156100>>> e1 = E() 
    157101>>> e1.save() 
    158102>>> f1 = F(e=e1) 
     
    163107# Since E.f is nullable, we should delete F first (after nulling out 
    164108# the E.f field), then E. 
    165109 
    166 >>> o = CollectedObjects() 
    167 >>> e1._collect_sub_objects(o) 
    168 >>> o.keys() 
    169 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>] 
    170  
    171110# temporarily replace the UpdateQuery class to verify that E.f is actually nulled out first 
    172111>>> import django.db.models.sql 
    173112>>> class LoggingUpdateQuery(django.db.models.sql.UpdateQuery): 
    174 ...     def clear_related(self, related_field, pk_list, using): 
    175 ...         print "CLEARING FIELD",related_field.name 
    176 ...         return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list, using) 
     113...     def update_batch(self, pk_list, values, using): 
     114...         if values == {'f': None}: 
     115...             print "CLEARING FIELD f" 
     116...         return super(LoggingUpdateQuery, self).update_batch(pk_list, values, using) 
     117 
    177118>>> original_class = django.db.models.sql.UpdateQuery 
    178119>>> django.db.models.sql.UpdateQuery = LoggingUpdateQuery 
    179120>>> e1.delete() 
     
    187128>>> e2.save() 
    188129 
    189130# Same deal as before, though we are starting from the other object. 
    190  
    191 >>> o = CollectedObjects() 
    192 >>> f2._collect_sub_objects(o) 
    193 >>> o.keys() 
    194 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>] 
    195  
    196131>>> f2.delete() 
    197132CLEARING FIELD f 
    198133 
  • django/db/models/sql/subqueries.py

    old new  
    2626        self.where = where 
    2727        self.get_compiler(using).execute_sql(None) 
    2828 
    29     def delete_batch_related(self, pk_list, using): 
     29    def delete_generic_relation_hack(self, pk_list, using): 
    3030        """ 
    31         Set up and execute delete queries for all the objects related to the 
    32         primary key values in pk_list. To delete the objects themselves, use 
    33         the delete_batch() method. 
    34  
    35         More than one physical query may be executed if there are a 
    36         lot of values in pk_list. 
     31        Delete objects related to self.model through a GenericRelation. 
     32        This should be handled by a custom `on_delete` handler in django.contrib.contentttypes. 
    3733        """ 
    3834        from django.contrib.contenttypes import generic 
    3935        cls = self.model 
    40         for related in cls._meta.get_all_related_many_to_many_objects(): 
    41             if not isinstance(related.field, generic.GenericRelation): 
    42                 for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): 
    43                     where = self.where_class() 
    44                     where.add((Constraint(None, 
    45                             related.field.m2m_reverse_name(), related.field), 
    46                             'in', 
    47                             pk_list[offset : offset+GET_ITERATOR_CHUNK_SIZE]), 
    48                             AND) 
    49                     self.do_query(related.field.m2m_db_table(), where, using=using) 
    50  
    5136        for f in cls._meta.many_to_many: 
     37            if not isinstance(f, generic.GenericRelation): 
     38                continue 
    5239            w1 = self.where_class() 
    53             if isinstance(f, generic.GenericRelation): 
    54                 from django.contrib.contenttypes.models import ContentType 
    55                 field = f.rel.to._meta.get_field(f.content_type_field_name) 
    56                 w1.add((Constraint(None, field.column, field), 'exact', 
    57                         ContentType.objects.get_for_model(cls).id), AND) 
     40            from django.contrib.contenttypes.models import ContentType 
     41            field = f.rel.to._meta.get_field(f.content_type_field_name) 
     42            w1.add((Constraint(None, field.column, field), 'exact', ContentType.objects.get_for_model(cls).id), AND) 
    5843            for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): 
    5944                where = self.where_class() 
    6045                where.add((Constraint(None, f.m2m_column_name(), f), 'in', 
    6146                        pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), 
    6247                        AND) 
    63                 if w1: 
    64                     where.add(w1, AND) 
     48                where.add(w1, AND) 
    6549                self.do_query(f.m2m_db_table(), where, using=using) 
    6650 
    67     def delete_batch(self, pk_list, using): 
     51    def delete_batch(self, pk_list, using, field=None): 
    6852        """ 
    69         Set up and execute delete queries for all the objects in pk_list. This 
    70         should be called after delete_batch_related(), if necessary. 
     53        Set up and execute delete queries for all the objects in pk_list. 
    7154 
    7255        More than one physical query may be executed if there are a 
    7356        lot of values in pk_list. 
    7457        """ 
     58        if not field: 
     59            field = self.model._meta.pk 
    7560        for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): 
    7661            where = self.where_class() 
    77             field = self.model._meta.pk 
    7862            where.add((Constraint(None, field.column, field), 'in', 
    7963                    pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND) 
    8064            self.do_query(self.model._meta.db_table, where, using=using) 
     
    10690                related_updates=self.related_updates.copy(), **kwargs) 
    10791 
    10892 
    109     def clear_related(self, related_field, pk_list, using): 
    110         """ 
    111         Set up and execute an update query that clears related entries for the 
    112         keys in pk_list. 
    113  
    114         This is used by the QuerySet.delete_objects() method. 
    115         """ 
     93    def update_batch(self, pk_list, values, using): 
     94        pk_field = self.model._meta.pk 
     95        self.add_update_values(values) 
    11696        for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): 
    11797            self.where = self.where_class() 
    118             f = self.model._meta.pk 
    119             self.where.add((Constraint(None, f.column, f), 'in', 
     98            self.where.add((Constraint(None, pk_field.column, pk_field), 'in', 
    12099                    pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), 
    121100                    AND) 
    122             self.values = [(related_field, None, None)] 
    123101            self.get_compiler(using).execute_sql(None) 
    124102 
    125103    def add_update_values(self, values): 
  • django/db/models/base.py

    old new  
    66from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError 
    77from django.db.models.fields import AutoField, FieldDoesNotExist 
    88from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField 
    9 from django.db.models.query import delete_objects, Q 
    10 from django.db.models.query_utils import CollectedObjects, DeferredAttribute 
     9from django.db.models.query import Q 
     10from django.db.models.query_utils import DeferredAttribute 
     11from django.db.models.deletion import Collector 
    1112from django.db.models.options import Options 
    1213from django.db import connections, transaction, DatabaseError, DEFAULT_DB_ALIAS 
    1314from django.db.models import signals 
     
    534535 
    535536    save_base.alters_data = True 
    536537 
    537     def _collect_sub_objects(self, seen_objs, parent=None, nullable=False): 
    538         """ 
    539         Recursively populates seen_objs with all objects related to this 
    540         object. 
    541  
    542         When done, seen_objs.items() will be in the format: 
    543             [(model_class, {pk_val: obj, pk_val: obj, ...}), 
    544              (model_class, {pk_val: obj, pk_val: obj, ...}), ...] 
    545         """ 
    546         pk_val = self._get_pk_val() 
    547         if seen_objs.add(self.__class__, pk_val, self, parent, nullable): 
    548             return 
    549  
    550         for related in self._meta.get_all_related_objects(): 
    551             rel_opts_name = related.get_accessor_name() 
    552             if isinstance(related.field.rel, OneToOneRel): 
    553                 try: 
    554                     sub_obj = getattr(self, rel_opts_name) 
    555                 except ObjectDoesNotExist: 
    556                     pass 
    557                 else: 
    558                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null) 
    559             else: 
    560                 # To make sure we can access all elements, we can't use the 
    561                 # normal manager on the related object. So we work directly 
    562                 # with the descriptor object. 
    563                 for cls in self.__class__.mro(): 
    564                     if rel_opts_name in cls.__dict__: 
    565                         rel_descriptor = cls.__dict__[rel_opts_name] 
    566                         break 
    567                 else: 
    568                     # in the case of a hidden fkey just skip it, it'll get 
    569                     # processed as an m2m 
    570                     if not related.field.rel.is_hidden(): 
    571                         raise AssertionError("Should never get here.") 
    572                     else: 
    573                         continue 
    574                 delete_qs = rel_descriptor.delete_manager(self).all() 
    575                 for sub_obj in delete_qs: 
    576                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null) 
    577  
    578         # Handle any ancestors (for the model-inheritance case). We do this by 
    579         # traversing to the most remote parent classes -- those with no parents 
    580         # themselves -- and then adding those instances to the collection. That 
    581         # will include all the child instances down to "self". 
    582         parent_stack = [p for p in self._meta.parents.values() if p is not None] 
    583         while parent_stack: 
    584             link = parent_stack.pop() 
    585             parent_obj = getattr(self, link.name) 
    586             if parent_obj._meta.parents: 
    587                 parent_stack.extend(parent_obj._meta.parents.values()) 
    588                 continue 
    589             # At this point, parent_obj is base class (no ancestor models). So 
    590             # delete it and all its descendents. 
    591             parent_obj._collect_sub_objects(seen_objs) 
    592  
    593538    def delete(self, using=None): 
    594         using = using or self._state.db or DEFAULT_DB_ALIAS 
     539        using = using or self._state.db 
    595540        connection = connections[using] 
    596541        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) 
    597542 
    598         # Find all the objects than need to be deleted. 
    599         seen_objs = CollectedObjects(
    600         self._collect_sub_objects(seen_objs
     543        collector = Collector() 
     544        collector.collect([self]
     545        collector.delete(using=using
    601546 
    602         # Actually delete the objects. 
    603         delete_objects(seen_objs, using) 
    604  
    605547    delete.alters_data = True 
    606548 
    607549    def _get_FIELD_display(self, field): 
  • django/db/models/options.py

    old new  
    376376                    cache[obj] = parent 
    377377                else: 
    378378                    cache[obj] = model 
    379         for klass in get_models(): 
     379        for klass in get_models(include_auto_created=True): 
    380380            for f in klass._meta.local_fields: 
    381381                if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta: 
    382382                    cache[RelatedObject(f.rel.to, klass, f)] = None 
  • django/db/models/fields/related.py

    old new  
    66from django.db.models.related import RelatedObject 
    77from django.db.models.query import QuerySet 
    88from django.db.models.query_utils import QueryWrapper 
     9from django.db.models.deletion import CASCADE 
    910from django.utils.encoding import smart_unicode 
    1011from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _ 
    1112from django.utils.functional import curry 
     
    644645        manager.add(*value) 
    645646 
    646647class ManyToOneRel(object): 
    647     def __init__(self, to, field_name, related_name=None, 
    648             limit_choices_to=None, lookup_overrides=None, parent_link=False): 
     648    def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): 
    649649        try: 
    650650            to._meta 
    651651        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT 
     
    655655        if limit_choices_to is None: 
    656656            limit_choices_to = {} 
    657657        self.limit_choices_to = limit_choices_to 
    658         self.lookup_overrides = lookup_overrides or {} 
    659658        self.multiple = True 
    660659        self.parent_link = parent_link 
     660        self.on_delete = on_delete 
    661661 
    662662    def is_hidden(self): 
    663663        "Should the related object be hidden?" 
     
    675675        return data[0] 
    676676 
    677677class OneToOneRel(ManyToOneRel): 
    678     def __init__(self, to, field_name, related_name=None, 
    679             limit_choices_to=None, lookup_overrides=None, parent_link=False): 
     678    def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): 
    680679        super(OneToOneRel, self).__init__(to, field_name, 
    681680                related_name=related_name, limit_choices_to=limit_choices_to, 
    682                 lookup_overrides=lookup_overrides, parent_link=parent_link) 
     681                parent_link=parent_link, on_delete=on_delete 
     682        ) 
    683683        self.multiple = False 
    684684 
    685685class ManyToManyRel(object): 
     
    725725        kwargs['rel'] = rel_class(to, to_field, 
    726726            related_name=kwargs.pop('related_name', None), 
    727727            limit_choices_to=kwargs.pop('limit_choices_to', None), 
    728             lookup_overrides=kwargs.pop('lookup_overrides', None), 
    729             parent_link=kwargs.pop('parent_link', False)) 
     728            parent_link=kwargs.pop('parent_link', False), 
     729            on_delete=kwargs.pop('on_delete', CASCADE), 
     730        ) 
    730731        Field.__init__(self, **kwargs) 
    731732 
    732733        self.db_index = True 
  • django/db/models/__init__.py

    old new  
    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

    old new  
     1from django.utils.datastructures import SortedDict 
     2from django.utils.functional import wraps 
     3from django.db import connections, transaction, IntegrityError, DEFAULT_DB_ALIAS 
     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, field.rel.to, field.null) 
     9    if field.null: 
     10        # FIXME: there should be a connection feature indicating whether nullable related fields should be nulled out before deletion 
     11        collector.add_field_update(field, None, sub_objs) 
     12 
     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, previously_collected=None): 
     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        if previously_collected: 
     59            self.blocked = set(previously_collected) 
     60            self.blocked.update(previously_collected.blocked) 
     61        else: 
     62            self.blocked = set() 
     63 
     64    def add(self, objs, source=None, nullable=False): 
     65        """ 
     66        Adds 'objs' to the collection of objects to be deleted. 
     67        If the call is the result of a cascade, 'source' should be the model that caused it  
     68        and 'nullable' should be set to True, if the relation can be null. 
     69         
     70        Returns a list of all objects that were not already collected. 
     71        """ 
     72        if not objs: 
     73            return [] 
     74        new_objs = [] 
     75        model = objs[0].__class__ 
     76        instances = self.data.setdefault(model, []) 
     77        for obj in objs: 
     78            if obj not in instances and obj not in self.blocked: 
     79                new_objs.append(obj) 
     80        instances.extend(new_objs) 
     81        # Nullable relationships can be ignored -- they are nulled out before 
     82        # deleting, and therefore do not affect the order in which objects 
     83        # have to be deleted. 
     84        if new_objs and source is not None and not nullable: 
     85            self.dependencies.setdefault(source, set()).add(model) 
     86        return new_objs 
     87         
     88    def add_batch(self, model, field, objs): 
     89        """ 
     90        Schedules a batch delete. Every instance of 'model' that related to an instance of 'obj' through 'field' will be deleted. 
     91        """ 
     92        self.batches.setdefault(model, {}).setdefault(field, set()).update(objs) 
     93         
     94    def add_field_update(self, field, value, objs): 
     95        """ 
     96        Schedules a field update. 'objs' must be a homogenous iterable collection of model instances (e.g. a QuerySet). 
     97        """ 
     98        objs = list(objs) 
     99        if not objs: 
     100            return 
     101        model = objs[0].__class__ 
     102        self.field_updates.setdefault(model, {}).setdefault((field, value), set()).update(objs) 
     103         
     104    def collect(self, objs, source=None, nullable=False, collect_related=True): 
     105        """ 
     106        Adds 'objs' to the collection of objects to be deleted as well as all parent instances. 
     107        'objs' must be a homogenous iterable collection of model instances (e.g. a QuerySet). 
     108        If 'collect_related' is True, related objects will be handled by their respective on_delete handler. 
     109         
     110        If the call is the result of a cascade, 'source' should be the model that caused it  
     111        and 'nullable' should be set to True, if the relation can be null. 
     112        """ 
     113        new_objs = self.add(objs, source, nullable) 
     114        if not new_objs: 
     115            return 
     116        model = new_objs[0].__class__ 
     117         
     118        # Recusively collect parent models, but not their related objects. 
     119        for parent, ptr in model._meta.parents.items(): 
     120            if ptr: 
     121                parent_objs = [getattr(obj, ptr.name) for obj in new_objs] 
     122                self.collect(parent_objs, model, collect_related=False) 
     123 
     124        if collect_related: 
     125            for related in model._meta.get_all_related_objects(): 
     126                if related.model._meta.auto_created: 
     127                    self.add_batch(related.model, related.field, new_objs) 
     128                else: 
     129                    sub_objs = related.model._base_manager.filter(**{"%s__in" % related.field.name: new_objs}) 
     130                    if not sub_objs: 
     131                        continue 
     132                    related.field.rel.on_delete(self, related.field, sub_objs) 
     133 
     134    def instances_with_model(self): 
     135        for model, instances in self.data.iteritems(): 
     136            for obj in instances: 
     137                yield model, obj 
     138                 
     139    def __iter__(self): 
     140        for model, obj in self.instances_with_model(): 
     141            yield obj 
     142 
     143    def __nonzero__(self): 
     144        return bool(self.data) 
     145 
     146    def sort(self): 
     147        sorted_models = [] 
     148        models = self.data.keys() 
     149        while len(sorted_models) < len(models): 
     150            found = False 
     151            for model in models: 
     152                if model in sorted_models: 
     153                    continue 
     154                dependencies = self.dependencies.get(model) 
     155                if not dependencies or not dependencies.difference(sorted_models): 
     156                    sorted_models.append(model) 
     157                    found = True 
     158            if not found: 
     159                return 
     160        self.data = SortedDict([(model, self.data[model]) for model in sorted_models]) 
     161     
     162    @force_managed 
     163    def delete(self, using=None): 
     164        using = using or DEFAULT_DB_ALIAS 
     165        # sort instance collections  
     166        for instances in self.data.itervalues(): 
     167            instances.sort(key=lambda obj: obj.pk) 
     168 
     169        # if possible, bring the models in an order suitable for databases that don't support transactions  
     170        # or cannot defer contraint checks until the end of a transaction. 
     171        self.sort() 
     172         
     173        # send pre_delete signals 
     174        for model, obj in self.instances_with_model(): 
     175            if not model._meta.auto_created: 
     176                signals.pre_delete.send(sender=model, instance=obj) 
     177 
     178        # update fields 
     179        for model, instances_for_fieldvalues in self.field_updates.iteritems(): 
     180            query = sql.UpdateQuery(model) 
     181            for (field, value), instances in instances_for_fieldvalues.iteritems(): 
     182                query.update_batch([obj.pk for obj in instances], {field.name: value}, using) 
     183 
     184        # reverse instance collections 
     185        for instances in self.data.itervalues(): 
     186            instances.reverse() 
     187 
     188        # delete batches 
     189        for model, batches in self.batches.iteritems(): 
     190            query = sql.DeleteQuery(model) 
     191            for field, instances in batches.iteritems(): 
     192                query.delete_batch([obj.pk for obj in instances], using, field) 
     193 
     194        # delete instances 
     195        for model, instances in self.data.iteritems(): 
     196            query = sql.DeleteQuery(model) 
     197            pk_list = [obj.pk for obj in instances] 
     198            query.delete_generic_relation_hack(pk_list, using) 
     199            query.delete_batch(pk_list, using) 
     200         
     201        # send post_delete signals 
     202        for model, obj in self.instances_with_model(): 
     203            if not model._meta.auto_created: 
     204                signals.post_delete.send(sender=model, instance=obj) 
     205         
     206        # update collected instances 
     207        for model, instances_for_fieldvalues in self.field_updates.iteritems(): 
     208            for (field, value), instances in instances_for_fieldvalues.iteritems(): 
     209                for obj in instances: 
     210                    setattr(obj, field.attname, value) 
     211        for model, instances in self.data.iteritems(): 
     212            for instance in instances: 
     213                setattr(instance, model._meta.pk.attname, None) 
  • django/db/models/query.py

    old new  
    77from django.db import connections, transaction, IntegrityError, DEFAULT_DB_ALIAS 
    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 
     
    419420 
    420421        # Delete objects in chunks to prevent the list of related objects from 
    421422        # becoming too long. 
    422         seen_objs = None 
     423        collector = None 
    423424        while 1: 
    424425            # Collect all the objects to be deleted in this chunk, and all the 
    425426            # objects that are related to the objects that are to be deleted. 
    426             seen_objs = CollectedObjects(seen_objs) 
    427             for object in del_query[:CHUNK_SIZE]: 
    428                 object._collect_sub_objects(seen_objs) 
    429  
    430             if not seen_objs: 
     427            collector = Collector(collector) 
     428            collector.collect(del_query[:CHUNK_SIZE]) 
     429            if not collector: 
    431430                break 
    432             delete_objects(seen_objs, del_query.db) 
     431            collector.delete(using=del_query.db) 
    433432 
    434433        # Clear the result cache, in case this QuerySet gets reused. 
    435434        self._result_cache = None 
     
    10741073                setattr(obj, f.get_cache_name(), rel_obj) 
    10751074    return obj, index_end 
    10761075 
    1077 def delete_objects(seen_objs, using): 
    1078     """ 
    1079     Iterate through a list of seen classes, and remove any instances that are 
    1080     referred to. 
    1081     """ 
    1082     connection = connections[using] 
    1083     if not transaction.is_managed(using=using): 
    1084         transaction.enter_transaction_management(using=using) 
    1085         forced_managed = True 
    1086     else: 
    1087         forced_managed = False 
    1088     try: 
    1089         ordered_classes = seen_objs.keys() 
    1090     except CyclicDependency: 
    1091         # If there is a cyclic dependency, we cannot in general delete the 
    1092         # objects.  However, if an appropriate transaction is set up, or if the 
    1093         # database is lax enough, it will succeed. So for now, we go ahead and 
    1094         # try anyway. 
    1095         ordered_classes = seen_objs.unordered_keys() 
    10961076 
    1097     obj_pairs = {} 
    1098     try: 
    1099         for cls in ordered_classes: 
    1100             items = seen_objs[cls].items() 
    1101             items.sort() 
    1102             obj_pairs[cls] = items 
    1103  
    1104             # Pre-notify all instances to be deleted. 
    1105             for pk_val, instance in items: 
    1106                 if not cls._meta.auto_created: 
    1107                     signals.pre_delete.send(sender=cls, instance=instance) 
    1108  
    1109             pk_list = [pk for pk,instance in items] 
    1110             del_query = sql.DeleteQuery(cls) 
    1111             del_query.delete_batch_related(pk_list, using=using) 
    1112  
    1113             update_query = sql.UpdateQuery(cls) 
    1114             for field, model in cls._meta.get_fields_with_model(): 
    1115                 if (field.rel and field.null and field.rel.to in seen_objs and 
    1116                         filter(lambda f: f.column == field.rel.get_related_field().column, 
    1117                         field.rel.to._meta.fields)): 
    1118                     if model: 
    1119                         sql.UpdateQuery(model).clear_related(field, pk_list, using=using) 
    1120                     else: 
    1121                         update_query.clear_related(field, pk_list, using=using) 
    1122  
    1123         # Now delete the actual data. 
    1124         for cls in ordered_classes: 
    1125             items = obj_pairs[cls] 
    1126             items.reverse() 
    1127  
    1128             pk_list = [pk for pk,instance in items] 
    1129             del_query = sql.DeleteQuery(cls) 
    1130             del_query.delete_batch(pk_list, using=using) 
    1131  
    1132             # Last cleanup; set NULLs where there once was a reference to the 
    1133             # object, NULL the primary key of the found objects, and perform 
    1134             # post-notification. 
    1135             for pk_val, instance in items: 
    1136                 for field in cls._meta.fields: 
    1137                     if field.rel and field.null and field.rel.to in seen_objs: 
    1138                         setattr(instance, field.attname, None) 
    1139  
    1140                 if not cls._meta.auto_created: 
    1141                     signals.post_delete.send(sender=cls, instance=instance) 
    1142                 setattr(instance, cls._meta.pk.attname, None) 
    1143  
    1144         if forced_managed: 
    1145             transaction.commit(using=using) 
    1146         else: 
    1147             transaction.commit_unless_managed(using=using) 
    1148     finally: 
    1149         if forced_managed: 
    1150             transaction.leave_transaction_management(using=using) 
    1151  
    11521077class RawQuerySet(object): 
    11531078    """ 
    11541079    Provides an iterator which converts the results of raw SQL queries into 
  • django/db/models/query_utils.py

    old new  
    1010from django.utils.copycompat import deepcopy 
    1111 
    1212from django.utils import tree 
    13 from django.utils.datastructures import SortedDict 
    1413 
    1514 
    16 class CyclicDependency(Exception): 
    17     """ 
    18     An error when dealing with a collection of objects that have a cyclic 
    19     dependency, i.e. when deleting multiple objects. 
    20     """ 
    21     pass 
    22  
    2315class InvalidQuery(Exception): 
    2416    """ 
    2517    The query passed to raw isn't a safe query to use with raw. 
    2618    """ 
    2719    pass 
    2820 
    29  
    30 class CollectedObjects(object): 
    31     """ 
    32     A container that stores keys and lists of values along with remembering the 
    33     parent objects for all the keys. 
    34  
    35     This is used for the database object deletion routines so that we can 
    36     calculate the 'leaf' objects which should be deleted first. 
    37  
    38     previously_seen is an optional argument. It must be a CollectedObjects 
    39     instance itself; any previously_seen collected object will be blocked from 
    40     being added to this instance. 
    41     """ 
    42  
    43     def __init__(self, previously_seen=None): 
    44         self.data = {} 
    45         self.children = {} 
    46         if previously_seen: 
    47             self.blocked = previously_seen.blocked 
    48             for cls, seen in previously_seen.data.items(): 
    49                 self.blocked.setdefault(cls, SortedDict()).update(seen) 
    50         else: 
    51             self.blocked = {} 
    52  
    53     def add(self, model, pk, obj, parent_model, nullable=False): 
    54         """ 
    55         Adds an item to the container. 
    56  
    57         Arguments: 
    58         * model - the class of the object being added. 
    59         * pk - the primary key. 
    60         * obj - the object itself. 
    61         * parent_model - the model of the parent object that this object was 
    62           reached through. 
    63         * nullable - should be True if this relation is nullable. 
    64  
    65         Returns True if the item already existed in the structure and 
    66         False otherwise. 
    67         """ 
    68         if pk in self.blocked.get(model, {}): 
    69             return True 
    70  
    71         d = self.data.setdefault(model, SortedDict()) 
    72         retval = pk in d 
    73         d[pk] = obj 
    74         # Nullable relationships can be ignored -- they are nulled out before 
    75         # deleting, and therefore do not affect the order in which objects 
    76         # have to be deleted. 
    77         if parent_model is not None and not nullable: 
    78             self.children.setdefault(parent_model, []).append(model) 
    79         return retval 
    80  
    81     def __contains__(self, key): 
    82         return self.data.__contains__(key) 
    83  
    84     def __getitem__(self, key): 
    85         return self.data[key] 
    86  
    87     def __nonzero__(self): 
    88         return bool(self.data) 
    89  
    90     def iteritems(self): 
    91         for k in self.ordered_keys(): 
    92             yield k, self[k] 
    93  
    94     def items(self): 
    95         return list(self.iteritems()) 
    96  
    97     def keys(self): 
    98         return self.ordered_keys() 
    99  
    100     def ordered_keys(self): 
    101         """ 
    102         Returns the models in the order that they should be dealt with (i.e. 
    103         models with no dependencies first). 
    104         """ 
    105         dealt_with = SortedDict() 
    106         # Start with items that have no children 
    107         models = self.data.keys() 
    108         while len(dealt_with) < len(models): 
    109             found = False 
    110             for model in models: 
    111                 if model in dealt_with: 
    112                     continue 
    113                 children = self.children.setdefault(model, []) 
    114                 if len([c for c in children if c not in dealt_with]) == 0: 
    115                     dealt_with[model] = None 
    116                     found = True 
    117             if not found: 
    118                 raise CyclicDependency( 
    119                     "There is a cyclic dependency of items to be processed.") 
    120  
    121         return dealt_with.keys() 
    122  
    123     def unordered_keys(self): 
    124         """ 
    125         Fallback for the case where is a cyclic dependency but we don't  care. 
    126         """ 
    127         return self.data.keys() 
    128  
    12921class QueryWrapper(object): 
    13022    """ 
    13123    A type that indicates the contents are an SQL fragment and the associate 
  • django/core/management/validation.py

    old new  
    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 
     
    6667            # Perform any backend-specific field validation. 
    6768            connection.validation.validate_field(e, opts, f) 
    6869 
     70            # Check if the on_delete behavior is sane 
     71            if f.rel and hasattr(f.rel, 'on_delete'): 
     72                if f.rel.on_delete == SET_NULL and not f.null: 
     73                    e.add(opts, "'%s' specifies on_delete=SET_NULL, but cannot be null." % f.name) 
     74                elif f.rel.on_delete == SET_DEFAULT and not f.has_default(): 
     75                    e.add(opts, "'%s' specifies on_delete=SET_DEFAULT, but has no default value." % f.name) 
     76 
    6977            # Check to see if the related field will clash with any existing 
    7078            # fields, m2m fields, m2m related objects or related objects 
    7179            if f.rel: