Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #7539, #13067 -- Added on_delete argument to ForeignKey to cont…

…rol cascade behavior. Also refactored deletion for efficiency and code clarity. Many thanks to Johannes Dollinger and Michael Glassford for extensive work on the patch, and to Alex Gaynor, Russell Keith-Magee, and Jacob Kaplan-Moss for review.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14507 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 616b30227d901a7452810a9ffaa55eaf186dc9e1 1 parent 3ba3294
Carl Meyer authored November 09, 2010

Showing 28 changed files with 874 additions and 632 deletions. Show diff stats Hide diff stats

  1. 6  django/contrib/admin/actions.py
  2. 7  django/contrib/admin/options.py
  3. 173  django/contrib/admin/util.py
  4. 18  django/contrib/contenttypes/generic.py
  5. 8  django/core/management/validation.py
  6. 4  django/db/backends/__init__.py
  7. 1  django/db/backends/oracle/base.py
  8. 1  django/db/backends/postgresql/base.py
  9. 1  django/db/backends/postgresql_psycopg2/base.py
  10. 1  django/db/models/__init__.py
  11. 100  django/db/models/base.py
  12. 245  django/db/models/deletion.py
  13. 22  django/db/models/fields/related.py
  14. 29  django/db/models/options.py
  15. 95  django/db/models/query.py
  16. 108  django/db/models/query_utils.py
  17. 19  django/db/models/sql/subqueries.py
  18. 10  docs/ref/contrib/contenttypes.txt
  19. 47  docs/ref/models/fields.txt
  20. 12  docs/ref/models/querysets.txt
  21. 13  docs/releases/1.3.txt
  22. 13  docs/topics/db/queries.txt
  23. 1  tests/modeltests/delete/__init__.py
  24. 124  tests/modeltests/delete/models.py
  25. 380  tests/modeltests/delete/tests.py
  26. 9  tests/modeltests/invalid_models/models.py
  27. 4  tests/regressiontests/admin_util/models.py
  28. 55  tests/regressiontests/admin_util/tests.py
6  django/contrib/admin/actions.py
@@ -6,6 +6,7 @@
6 6
 from django.core.exceptions import PermissionDenied
7 7
 from django.contrib.admin import helpers
8 8
 from django.contrib.admin.util import get_deleted_objects, model_ngettext
  9
+from django.db import router
9 10
 from django.shortcuts import render_to_response
10 11
 from django.utils.encoding import force_unicode
11 12
 from django.utils.translation import ugettext_lazy, ugettext as _
@@ -27,9 +28,12 @@ def delete_selected(modeladmin, request, queryset):
27 28
     if not modeladmin.has_delete_permission(request):
28 29
         raise PermissionDenied
29 30
 
  31
+    using = router.db_for_write(modeladmin.model)
  32
+
30 33
     # Populate deletable_objects, a data structure of all related objects that
31 34
     # will also be deleted.
32  
-    deletable_objects, perms_needed = get_deleted_objects(queryset, opts, request.user, modeladmin.admin_site)
  35
+    deletable_objects, perms_needed = get_deleted_objects(
  36
+        queryset, opts, request.user, modeladmin.admin_site, using)
33 37
 
34 38
     # The user has already confirmed the deletion.
35 39
     # Do the deletion and return a None to display the change list view again.
7  django/contrib/admin/options.py
@@ -9,7 +9,7 @@
9 9
 from django.contrib import messages
10 10
 from django.views.decorators.csrf import csrf_protect
11 11
 from django.core.exceptions import PermissionDenied, ValidationError
12  
-from django.db import models, transaction
  12
+from django.db import models, transaction, router
13 13
 from django.db.models.fields import BLANK_CHOICE_DASH
14 14
 from django.http import Http404, HttpResponse, HttpResponseRedirect
15 15
 from django.shortcuts import get_object_or_404, render_to_response
@@ -1110,9 +1110,12 @@ def delete_view(self, request, object_id, extra_context=None):
1110 1110
         if obj is None:
1111 1111
             raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
1112 1112
 
  1113
+        using = router.db_for_write(self.model)
  1114
+
1113 1115
         # Populate deleted_objects, a data structure of all related objects that
1114 1116
         # will also be deleted.
1115  
-        (deleted_objects, perms_needed) = get_deleted_objects((obj,), opts, request.user, self.admin_site)
  1117
+        (deleted_objects, perms_needed) = get_deleted_objects(
  1118
+            [obj], opts, request.user, self.admin_site, using)
1116 1119
 
1117 1120
         if request.POST: # The user has already confirmed the deletion.
1118 1121
             if perms_needed:
173  django/contrib/admin/util.py
... ...
@@ -1,4 +1,5 @@
1 1
 from django.db import models
  2
+from django.db.models.deletion import Collector
2 3
 from django.db.models.related import RelatedObject
3 4
 from django.forms.forms import pretty_name
4 5
 from django.utils import formats
@@ -10,6 +11,7 @@
10 11
 from django.core.urlresolvers import reverse, NoReverseMatch
11 12
 from django.utils.datastructures import SortedDict
12 13
 
  14
+
13 15
 def quote(s):
14 16
     """
15 17
     Ensure that primary key values do not confuse the admin URLs by escaping
@@ -26,6 +28,7 @@ def quote(s):
26 28
             res[i] = '_%02X' % ord(c)
27 29
     return ''.join(res)
28 30
 
  31
+
29 32
 def unquote(s):
30 33
     """
31 34
     Undo the effects of quote(). Based heavily on urllib.unquote().
@@ -46,6 +49,7 @@ def unquote(s):
46 49
             myappend('_' + item)
47 50
     return "".join(res)
48 51
 
  52
+
49 53
 def flatten_fieldsets(fieldsets):
50 54
     """Returns a list of field names from an admin fieldsets structure."""
51 55
     field_names = []
@@ -58,144 +62,94 @@ def flatten_fieldsets(fieldsets):
58 62
                 field_names.append(field)
59 63
     return field_names
60 64
 
61  
-def _format_callback(obj, user, admin_site, perms_needed):
62  
-    has_admin = obj.__class__ in admin_site._registry
63  
-    opts = obj._meta
64  
-    if has_admin:
65  
-        admin_url = reverse('%s:%s_%s_change'
66  
-                            % (admin_site.name,
67  
-                               opts.app_label,
68  
-                               opts.object_name.lower()),
69  
-                            None, (quote(obj._get_pk_val()),))
70  
-        p = '%s.%s' % (opts.app_label,
71  
-                       opts.get_delete_permission())
72  
-        if not user.has_perm(p):
73  
-            perms_needed.add(opts.verbose_name)
74  
-        # Display a link to the admin page.
75  
-        return mark_safe(u'%s: <a href="%s">%s</a>' %
76  
-                         (escape(capfirst(opts.verbose_name)),
77  
-                          admin_url,
78  
-                          escape(obj)))
79  
-    else:
80  
-        # Don't display link to edit, because it either has no
81  
-        # admin or is edited inline.
82  
-        return u'%s: %s' % (capfirst(opts.verbose_name),
83  
-                            force_unicode(obj))
84 65
 
85  
-def get_deleted_objects(objs, opts, user, admin_site):
  66
+def get_deleted_objects(objs, opts, user, admin_site, using):
86 67
     """
87  
-    Find all objects related to ``objs`` that should also be
88  
-    deleted. ``objs`` should be an iterable of objects.
  68
+    Find all objects related to ``objs`` that should also be deleted. ``objs``
  69
+    must be a homogenous iterable of objects (e.g. a QuerySet).
89 70
 
90 71
     Returns a nested list of strings suitable for display in the
91 72
     template with the ``unordered_list`` filter.
92 73
 
93 74
     """
94  
-    collector = NestedObjects()
95  
-    for obj in objs:
96  
-        # TODO using a private model API!
97  
-        obj._collect_sub_objects(collector)
98  
-
  75
+    collector = NestedObjects(using=using)
  76
+    collector.collect(objs)
99 77
     perms_needed = set()
100 78
 
101  
-    to_delete = collector.nested(_format_callback,
102  
-                                 user=user,
103  
-                                 admin_site=admin_site,
104  
-                                 perms_needed=perms_needed)
105  
-
106  
-    return to_delete, perms_needed
107  
-
108  
-
109  
-class NestedObjects(object):
110  
-    """
111  
-    A directed acyclic graph collection that exposes the add() API
112  
-    expected by Model._collect_sub_objects and can present its data as
113  
-    a nested list of objects.
114  
-
115  
-    """
116  
-    def __init__(self):
117  
-        # Use object keys of the form (model, pk) because actual model
118  
-        # objects may not be unique
  79
+    def format_callback(obj):
  80
+        has_admin = obj.__class__ in admin_site._registry
  81
+        opts = obj._meta
119 82
 
120  
-        # maps object key to list of child keys
121  
-        self.children = SortedDict()
  83
+        if has_admin:
  84
+            admin_url = reverse('%s:%s_%s_change'
  85
+                                % (admin_site.name,
  86
+                                   opts.app_label,
  87
+                                   opts.object_name.lower()),
  88
+                                None, (quote(obj._get_pk_val()),))
  89
+            p = '%s.%s' % (opts.app_label,
  90
+                           opts.get_delete_permission())
  91
+            if not user.has_perm(p):
  92
+                perms_needed.add(opts.verbose_name)
  93
+            # Display a link to the admin page.
  94
+            return mark_safe(u'%s: <a href="%s">%s</a>' %
  95
+                             (escape(capfirst(opts.verbose_name)),
  96
+                              admin_url,
  97
+                              escape(obj)))
  98
+        else:
  99
+            # Don't display link to edit, because it either has no
  100
+            # admin or is edited inline.
  101
+            return u'%s: %s' % (capfirst(opts.verbose_name),
  102
+                                force_unicode(obj))
122 103
 
123  
-        # maps object key to parent key
124  
-        self.parents = SortedDict()
  104
+    to_delete = collector.nested(format_callback)
125 105
 
126  
-        # maps object key to actual object
127  
-        self.seen = SortedDict()
  106
+    return to_delete, perms_needed
128 107
 
129  
-    def add(self, model, pk, obj,
130  
-            parent_model=None, parent_obj=None, nullable=False):
131  
-        """
132  
-        Add item ``obj`` to the graph. Returns True (and does nothing)
133  
-        if the item has been seen already.
134  
-
135  
-        The ``parent_obj`` argument must already exist in the graph; if
136  
-        not, it's ignored (but ``obj`` is still added with no
137  
-        parent). In any case, Model._collect_sub_objects (for whom
138  
-        this API exists) will never pass a parent that hasn't already
139  
-        been added itself.
140  
-
141  
-        These restrictions in combination ensure the graph will remain
142  
-        acyclic (but can have multiple roots).
143  
-
144  
-        ``model``, ``pk``, and ``parent_model`` arguments are ignored
145  
-        in favor of the appropriate lookups on ``obj`` and
146  
-        ``parent_obj``; unlike CollectedObjects, we can't maintain
147  
-        independence from the knowledge that we're operating on model
148  
-        instances, and we don't want to allow for inconsistency.
149  
-
150  
-        ``nullable`` arg is ignored: it doesn't affect how the tree of
151  
-        collected objects should be nested for display.
152  
-        """
153  
-        model, pk = type(obj), obj._get_pk_val()
154 108
 
155  
-        # auto-created M2M models don't interest us
156  
-        if model._meta.auto_created:
157  
-            return True
  109
+class NestedObjects(Collector):
  110
+    def __init__(self, *args, **kwargs):
  111
+        super(NestedObjects, self).__init__(*args, **kwargs)
  112
+        self.edges = {} # {from_instance: [to_instances]}
158 113
 
159  
-        key = model, pk
  114
+    def add_edge(self, source, target):
  115
+        self.edges.setdefault(source, []).append(target)
160 116
 
161  
-        if key in self.seen:
162  
-            return True
163  
-        self.seen.setdefault(key, obj)
  117
+    def collect(self, objs, source_attr=None, **kwargs):
  118
+        for obj in objs:
  119
+            if source_attr:
  120
+                self.add_edge(getattr(obj, source_attr), obj)
  121
+            else:
  122
+                self.add_edge(None, obj)
  123
+        return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
164 124
 
165  
-        if parent_obj is not None:
166  
-            parent_model, parent_pk = (type(parent_obj),
167  
-                                       parent_obj._get_pk_val())
168  
-            parent_key = (parent_model, parent_pk)
169  
-            if parent_key in self.seen:
170  
-                self.children.setdefault(parent_key, list()).append(key)
171  
-                self.parents.setdefault(key, parent_key)
  125
+    def related_objects(self, related, objs):
  126
+        qs = super(NestedObjects, self).related_objects(related, objs)
  127
+        return qs.select_related(related.field.name)
172 128
 
173  
-    def _nested(self, key, format_callback=None, **kwargs):
174  
-        obj = self.seen[key]
  129
+    def _nested(self, obj, seen, format_callback):
  130
+        if obj in seen:
  131
+            return []
  132
+        seen.add(obj)
  133
+        children = []
  134
+        for child in self.edges.get(obj, ()):
  135
+            children.extend(self._nested(child, seen, format_callback))
175 136
         if format_callback:
176  
-            ret = [format_callback(obj, **kwargs)]
  137
+            ret = [format_callback(obj)]
177 138
         else:
178 139
             ret = [obj]
179  
-
180  
-        children = []
181  
-        for child in self.children.get(key, ()):
182  
-            children.extend(self._nested(child, format_callback, **kwargs))
183 140
         if children:
184 141
             ret.append(children)
185  
-
186 142
         return ret
187 143
 
188  
-    def nested(self, format_callback=None, **kwargs):
  144
+    def nested(self, format_callback=None):
189 145
         """
190 146
         Return the graph as a nested list.
191 147
 
192  
-        Passes **kwargs back to the format_callback as kwargs.
193  
-
194 148
         """
  149
+        seen = set()
195 150
         roots = []
196  
-        for key in self.seen.keys():
197  
-            if key not in self.parents:
198  
-                roots.extend(self._nested(key, format_callback, **kwargs))
  151
+        for root in self.edges.get(None, ()):
  152
+            roots.extend(self._nested(root, seen, format_callback))
199 153
         return roots
200 154
 
201 155
 
@@ -218,6 +172,7 @@ def model_format_dict(obj):
218 172
         'verbose_name_plural': force_unicode(opts.verbose_name_plural)
219 173
     }
220 174
 
  175
+
221 176
 def model_ngettext(obj, n=None):
222 177
     """
223 178
     Return the appropriate `verbose_name` or `verbose_name_plural` value for
@@ -236,6 +191,7 @@ def model_ngettext(obj, n=None):
236 191
     singular, plural = d["verbose_name"], d["verbose_name_plural"]
237 192
     return ungettext(singular, plural, n or 0)
238 193
 
  194
+
239 195
 def lookup_field(name, obj, model_admin=None):
240 196
     opts = obj._meta
241 197
     try:
@@ -262,6 +218,7 @@ def lookup_field(name, obj, model_admin=None):
262 218
         value = getattr(obj, name)
263 219
     return f, attr, value
264 220
 
  221
+
265 222
 def label_for_field(name, model, model_admin=None, return_attr=False):
266 223
     attr = None
267 224
     try:
18  django/contrib/contenttypes/generic.py
@@ -5,7 +5,7 @@
5 5
 from django.core.exceptions import ObjectDoesNotExist
6 6
 from django.db import connection
7 7
 from django.db.models import signals
8  
-from django.db import models, router
  8
+from django.db import models, router, DEFAULT_DB_ALIAS
9 9
 from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
10 10
 from django.db.models.loading import get_model
11 11
 from django.forms import ModelForm
@@ -13,6 +13,9 @@
13 13
 from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets
14 14
 from django.utils.encoding import smart_unicode
15 15
 
  16
+from django.contrib.contenttypes.models import ContentType
  17
+
  18
+
16 19
 class GenericForeignKey(object):
17 20
     """
18 21
     Provides a generic relation to any object through content-type/object-id
@@ -167,6 +170,19 @@ def extra_filters(self, pieces, pos, negate):
167 170
         return [("%s__%s" % (prefix, self.content_type_field_name),
168 171
             content_type)]
169 172
 
  173
+    def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
  174
+        """
  175
+        Return all objects related to ``objs`` via this ``GenericRelation``.
  176
+
  177
+        """
  178
+        return self.rel.to._base_manager.db_manager(using).filter(**{
  179
+                "%s__pk" % self.content_type_field_name:
  180
+                    ContentType.objects.db_manager(using).get_for_model(self.model).pk,
  181
+                "%s__in" % self.object_id_field_name:
  182
+                    [obj.pk for obj in objs]
  183
+                })
  184
+
  185
+
170 186
 class ReverseGenericRelatedObjectsDescriptor(object):
171 187
     """
172 188
     This class provides the functionality that makes the related-object
8  django/core/management/validation.py
@@ -22,6 +22,7 @@ def get_validation_errors(outfile, app=None):
22 22
     from django.db import models, connection
23 23
     from django.db.models.loading import get_app_errors
24 24
     from django.db.models.fields.related import RelatedObject
  25
+    from django.db.models.deletion import SET_NULL, SET_DEFAULT
25 26
 
26 27
     e = ModelErrorCollection(outfile)
27 28
 
@@ -85,6 +86,13 @@ def get_validation_errors(outfile, app=None):
85 86
             # Perform any backend-specific field validation.
86 87
             connection.validation.validate_field(e, opts, f)
87 88
 
  89
+            # Check if the on_delete behavior is sane
  90
+            if f.rel and hasattr(f.rel, 'on_delete'):
  91
+                if f.rel.on_delete == SET_NULL and not f.null:
  92
+                    e.add(opts, "'%s' specifies on_delete=SET_NULL, but cannot be null." % f.name)
  93
+                elif f.rel.on_delete == SET_DEFAULT and not f.has_default():
  94
+                    e.add(opts, "'%s' specifies on_delete=SET_DEFAULT, but has no default value." % f.name)
  95
+
88 96
             # Check to see if the related field will clash with any existing
89 97
             # fields, m2m fields, m2m related objects or related objects
90 98
             if f.rel:
4  django/db/backends/__init__.py
@@ -150,6 +150,10 @@ class BaseDatabaseFeatures(object):
150 150
     # Can an object have a primary key of 0? MySQL says No.
151 151
     allows_primary_key_0 = True
152 152
 
  153
+    # Do we need to NULL a ForeignKey out, or can the constraint check be
  154
+    # deferred
  155
+    can_defer_constraint_checks = False
  156
+
153 157
     # Features that need to be confirmed at runtime
154 158
     # Cache whether the confirmation has been performed.
155 159
     _confirmed = False
1  django/db/backends/oracle/base.py
@@ -53,6 +53,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
53 53
     supports_subqueries_in_group_by = True
54 54
     supports_timezones = False
55 55
     supports_bitwise_or = False
  56
+    can_defer_constraint_checks = True
56 57
 
57 58
 class DatabaseOperations(BaseDatabaseOperations):
58 59
     compiler_module = "django.db.backends.oracle.compiler"
1  django/db/backends/postgresql/base.py
@@ -82,6 +82,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
82 82
     uses_savepoints = True
83 83
     requires_rollback_on_dirty_transaction = True
84 84
     has_real_datatype = True
  85
+    can_defer_constraint_checks = True
85 86
 
86 87
 class DatabaseWrapper(BaseDatabaseWrapper):
87 88
     vendor = 'postgresql'
1  django/db/backends/postgresql_psycopg2/base.py
@@ -69,6 +69,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
69 69
     can_return_id_from_insert = False
70 70
     requires_rollback_on_dirty_transaction = True
71 71
     has_real_datatype = True
  72
+    can_defer_constraint_checks = True
72 73
 
73 74
 class DatabaseOperations(PostgresqlDatabaseOperations):
74 75
     def last_executed_query(self, cursor, sql, params):
1  django/db/models/__init__.py
@@ -11,6 +11,7 @@
11 11
 from django.db.models.fields.subclassing import SubfieldBase
12 12
 from django.db.models.fields.files import FileField, ImageField
13 13
 from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
  14
+from django.db.models.deletion import CASCADE, PROTECT, SET, SET_NULL, SET_DEFAULT, DO_NOTHING
14 15
 from django.db.models import signals
15 16
 
16 17
 # Admin stages.
100  django/db/models/base.py
@@ -7,10 +7,12 @@
7 7
 from django.db.models.fields import AutoField, FieldDoesNotExist
8 8
 from django.db.models.fields.related import (OneToOneRel, ManyToOneRel,
9 9
     OneToOneField, add_lazy_relation)
10  
-from django.db.models.query import delete_objects, Q
11  
-from django.db.models.query_utils import CollectedObjects, DeferredAttribute
  10
+from django.db.models.query import Q
  11
+from django.db.models.query_utils import DeferredAttribute
  12
+from django.db.models.deletion import Collector
12 13
 from django.db.models.options import Options
13  
-from django.db import connections, router, transaction, DatabaseError, DEFAULT_DB_ALIAS
  14
+from django.db import (connections, router, transaction, DatabaseError,
  15
+    DEFAULT_DB_ALIAS)
14 16
 from django.db.models import signals
15 17
 from django.db.models.loading import register_models, get_model
16 18
 from django.utils.translation import ugettext_lazy as _
@@ -561,99 +563,13 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False,
561 563
 
562 564
     save_base.alters_data = True
563 565
 
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  
-
647 566
     def delete(self, using=None):
648 567
         using = using or router.db_for_write(self.__class__, instance=self)
649 568
         assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
650 569
 
651  
-        # Find all the objects than need to be deleted.
652  
-        seen_objs = CollectedObjects()
653  
-        self._collect_sub_objects(seen_objs)
654  
-
655  
-        # Actually delete the objects.
656  
-        delete_objects(seen_objs, using)
  570
+        collector = Collector(using=using)
  571
+        collector.collect([self])
  572
+        collector.delete()
657 573
 
658 574
     delete.alters_data = True
659 575
 
245  django/db/models/deletion.py
... ...
@@ -0,0 +1,245 @@
  1
+from operator import attrgetter
  2
+
  3
+from django.db import connections, transaction, IntegrityError
  4
+from django.db.models import signals, sql
  5
+from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
  6
+from django.utils.datastructures import SortedDict
  7
+from django.utils.functional import wraps
  8
+
  9
+
  10
+def CASCADE(collector, field, sub_objs, using):
  11
+    collector.collect(sub_objs, source=field.rel.to,
  12
+                      source_attr=field.name, nullable=field.null)
  13
+    if field.null and not connections[using].features.can_defer_constraint_checks:
  14
+        collector.add_field_update(field, None, sub_objs)
  15
+
  16
+def PROTECT(collector, field, sub_objs, using):
  17
+    raise IntegrityError("Cannot delete some instances of model '%s' because "
  18
+        "they are referenced through a protected foreign key: '%s.%s'" % (
  19
+            field.rel.to.__name__, sub_objs[0].__class__.__name__, field.name
  20
+    ))
  21
+
  22
+def SET(value):
  23
+    if callable(value):
  24
+        def set_on_delete(collector, field, sub_objs, using):
  25
+            collector.add_field_update(field, value(), sub_objs)
  26
+    else:
  27
+        def set_on_delete(collector, field, sub_objs, using):
  28
+            collector.add_field_update(field, value, sub_objs)
  29
+    return set_on_delete
  30
+
  31
+SET_NULL = SET(None)
  32
+
  33
+def SET_DEFAULT(collector, field, sub_objs, using):
  34
+    collector.add_field_update(field, field.get_default(), sub_objs)
  35
+
  36
+def DO_NOTHING(collector, field, sub_objs, using):
  37
+    pass
  38
+
  39
+def force_managed(func):
  40
+    @wraps(func)
  41
+    def decorated(self, *args, **kwargs):
  42
+        if not transaction.is_managed(using=self.using):
  43
+            transaction.enter_transaction_management(using=self.using)
  44
+            forced_managed = True
  45
+        else:
  46
+            forced_managed = False
  47
+        try:
  48
+            func(self, *args, **kwargs)
  49
+            if forced_managed:
  50
+                transaction.commit(using=self.using)
  51
+            else:
  52
+                transaction.commit_unless_managed(using=self.using)
  53
+        finally:
  54
+            if forced_managed:
  55
+                transaction.leave_transaction_management(using=self.using)
  56
+    return decorated
  57
+
  58
+class Collector(object):
  59
+    def __init__(self, using):
  60
+        self.using = using
  61
+        self.data = {} # {model: [instances]}
  62
+        self.batches = {} # {model: {field: set([instances])}}
  63
+        self.field_updates = {} # {model: {(field, value): set([instances])}}
  64
+        self.dependencies = {} # {model: set([models])}
  65
+
  66
+    def add(self, objs, source=None, nullable=False):
  67
+        """
  68
+        Adds 'objs' to the collection of objects to be deleted.  If the call is
  69
+        the result of a cascade, 'source' should be the model that caused it
  70
+        and 'nullable' should be set to True, if the relation can be null.
  71
+
  72
+        Returns a list of all objects that were not already collected.
  73
+        """
  74
+        if not objs:
  75
+            return []
  76
+        new_objs = []
  77
+        model = objs[0].__class__
  78
+        instances = self.data.setdefault(model, [])
  79
+        for obj in objs:
  80
+            if obj not in instances:
  81
+                new_objs.append(obj)
  82
+        instances.extend(new_objs)
  83
+        # Nullable relationships can be ignored -- they are nulled out before
  84
+        # deleting, and therefore do not affect the order in which objects have
  85
+        # to be deleted.
  86
+        if new_objs and source is not None and not nullable:
  87
+            self.dependencies.setdefault(source, set()).add(model)
  88
+        return new_objs
  89
+
  90
+    def add_batch(self, model, field, objs):
  91
+        """
  92
+        Schedules a batch delete. Every instance of 'model' that is related to
  93
+        an instance of 'obj' through 'field' will be deleted.
  94
+        """
  95
+        self.batches.setdefault(model, {}).setdefault(field, set()).update(objs)
  96
+
  97
+    def add_field_update(self, field, value, objs):
  98
+        """
  99
+        Schedules a field update. 'objs' must be a homogenous iterable
  100
+        collection of model instances (e.g. a QuerySet).
  101
+        """
  102
+        if not objs:
  103
+            return
  104
+        model = objs[0].__class__
  105
+        self.field_updates.setdefault(
  106
+            model, {}).setdefault(
  107
+            (field, value), set()).update(objs)
  108
+
  109
+    def collect(self, objs, source=None, nullable=False, collect_related=True,
  110
+        source_attr=None):
  111
+        """
  112
+        Adds 'objs' to the collection of objects to be deleted as well as all
  113
+        parent instances.  'objs' must be a homogenous iterable collection of
  114
+        model instances (e.g. a QuerySet).  If 'collect_related' is True,
  115
+        related objects will be handled by their respective on_delete handler.
  116
+
  117
+        If the call is the result of a cascade, 'source' should be the model
  118
+        that caused it and 'nullable' should be set to True, if the relation
  119
+        can be null.
  120
+        """
  121
+
  122
+        new_objs = self.add(objs, source, nullable)
  123
+        if not new_objs:
  124
+            return
  125
+        model = new_objs[0].__class__
  126
+
  127
+        # Recursively collect parent models, but not their related objects.
  128
+        # These will be found by meta.get_all_related_objects()
  129
+        for parent_model, ptr in model._meta.parents.iteritems():
  130
+            if ptr:
  131
+                parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
  132
+                self.collect(parent_objs, source=model,
  133
+                             source_attr=ptr.rel.related_name,
  134
+                             collect_related=False)
  135
+
  136
+        if collect_related:
  137
+            for related in model._meta.get_all_related_objects(include_hidden=True):
  138
+                field = related.field
  139
+                if related.model._meta.auto_created:
  140
+                    self.add_batch(related.model, field, new_objs)
  141
+                else:
  142
+                    sub_objs = self.related_objects(related, new_objs)
  143
+                    if not sub_objs:
  144
+                        continue
  145
+                    field.rel.on_delete(self, field, sub_objs, self.using)
  146
+
  147
+            # TODO This entire block is only needed as a special case to
  148
+            # support cascade-deletes for GenericRelation. It should be
  149
+            # removed/fixed when the ORM gains a proper abstraction for virtual
  150
+            # or composite fields, and GFKs are reworked to fit into that.
  151
+            for relation in model._meta.many_to_many:
  152
+                if not relation.rel.through:
  153
+                    sub_objs = relation.bulk_related_objects(new_objs, self.using)
  154
+                    self.collect(sub_objs,
  155
+                                 source=model,
  156
+                                 source_attr=relation.rel.related_name,
  157
+                                 nullable=True)
  158
+
  159
+    def related_objects(self, related, objs):
  160
+        """
  161
+        Gets a QuerySet of objects related to ``objs`` via the relation ``related``.
  162
+
  163
+        """
  164
+        return related.model._base_manager.using(self.using).filter(
  165
+            **{"%s__in" % related.field.name: objs}
  166
+        )
  167
+
  168
+    def instances_with_model(self):
  169
+        for model, instances in self.data.iteritems():
  170
+            for obj in instances:
  171
+                yield model, obj
  172
+
  173
+    def sort(self):
  174
+        sorted_models = []
  175
+        models = self.data.keys()
  176
+        while len(sorted_models) < len(models):
  177
+            found = False
  178
+            for model in models:
  179
+                if model in sorted_models:
  180
+                    continue
  181
+                dependencies = self.dependencies.get(model)
  182
+                if not (dependencies and dependencies.difference(sorted_models)):
  183
+                    sorted_models.append(model)
  184
+                    found = True
  185
+            if not found:
  186
+                return
  187
+        self.data = SortedDict([(model, self.data[model])
  188
+                                for model in sorted_models])
  189
+
  190
+    @force_managed
  191
+    def delete(self):
  192
+        # sort instance collections
  193
+        for instances in self.data.itervalues():
  194
+            instances.sort(key=attrgetter("pk"))
  195
+
  196
+        # if possible, bring the models in an order suitable for databases that
  197
+        # don't support transactions or cannot defer contraint checks until the
  198
+        # end of a transaction.
  199
+        self.sort()
  200
+
  201
+        # send pre_delete signals
  202
+        for model, obj in self.instances_with_model():
  203
+            if not model._meta.auto_created:
  204
+                signals.pre_delete.send(
  205
+                    sender=model, instance=obj, using=self.using
  206
+                )
  207
+
  208
+        # update fields
  209
+        for model, instances_for_fieldvalues in self.field_updates.iteritems():
  210
+            query = sql.UpdateQuery(model)
  211
+            for (field, value), instances in instances_for_fieldvalues.iteritems():
  212
+                query.update_batch([obj.pk for obj in instances],
  213
+                                   {field.name: value}, self.using)
  214
+
  215
+        # reverse instance collections
  216
+        for instances in self.data.itervalues():
  217
+            instances.reverse()
  218
+
  219
+        # delete batches
  220
+        for model, batches in self.batches.iteritems():
  221
+            query = sql.DeleteQuery(model)
  222
+            for field, instances in batches.iteritems():
  223
+                query.delete_batch([obj.pk for obj in instances], self.using, field)
  224
+
  225
+        # delete instances
  226
+        for model, instances in self.data.iteritems():
  227
+            query = sql.DeleteQuery(model)
  228
+            pk_list = [obj.pk for obj in instances]
  229
+            query.delete_batch(pk_list, self.using)
  230
+
  231
+        # send post_delete signals
  232
+        for model, obj in self.instances_with_model():
  233
+            if not model._meta.auto_created:
  234
+                signals.post_delete.send(
  235
+                    sender=model, instance=obj, using=self.using
  236
+                )
  237
+
  238
+        # update collected instances
  239
+        for model, instances_for_fieldvalues in self.field_updates.iteritems():
  240
+            for (field, value), instances in instances_for_fieldvalues.iteritems():
  241
+                for obj in instances:
  242
+                    setattr(obj, field.attname, value)
  243
+        for model, instances in self.data.iteritems():
  244
+            for instance in instances:
  245
+                setattr(instance, model._meta.pk.attname, None)
22  django/db/models/fields/related.py
@@ -7,8 +7,10 @@
@@ -733,8 +735,8 @@ def __set__(self, instance, value):
@@ -744,9 +746,9 @@ def __init__(self, to, field_name, related_name=None,
@@ -764,11 +766,12 @@ def get_related_field(self):
@@ -820,8 +823,9 @@ def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
29  django/db/models/options.py
@@ -11,6 +11,11 @@
11 11
 from django.utils.encoding import force_unicode, smart_str
12 12
 from django.utils.datastructures import SortedDict
13 13
 
  14
+try:
  15
+    all
  16
+except NameError:
  17
+    from django.utils.itercompat import all
  18
+
14 19
 # Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces".
15 20
 get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip()
16 21
 
@@ -339,16 +344,12 @@ def get_change_permission(self):
339 344
     def get_delete_permission(self):
340 345
         return 'delete_%s' % self.object_name.lower()
341 346
 
342  
-    def get_all_related_objects(self, local_only=False):
343  
-        try:
344  
-            self._related_objects_cache
345  
-        except AttributeError:
346  
-            self._fill_related_objects_cache()
347  
-        if local_only:
348  
-            return [k for k, v in self._related_objects_cache.items() if not v]
349  
-        return self._related_objects_cache.keys()
  347
+    def get_all_related_objects(self, local_only=False, include_hidden=False):
  348
+        return [k for k, v in self.get_all_related_objects_with_model(
  349
+                local_only=local_only, include_hidden=include_hidden)]
350 350
 
351  
-    def get_all_related_objects_with_model(self):
  351
+    def get_all_related_objects_with_model(self, local_only=False,
  352
+                                           include_hidden=False):
352 353
         """
353 354
         Returns a list of (related-object, model) pairs. Similar to
354 355
         get_fields_with_model().
@@ -357,7 +358,13 @@ def get_all_related_objects_with_model(self):
357 358
             self._related_objects_cache
358 359
         except AttributeError:
359 360
             self._fill_related_objects_cache()
360  
-        return self._related_objects_cache.items()
  361
+        predicates = []
  362
+        if local_only:
  363
+            predicates.append(lambda k, v: not v)
  364
+        if not include_hidden:
  365
+            predicates.append(lambda k, v: not k.field.rel.is_hidden())
  366
+        return filter(lambda t: all([p(*t) for p in predicates]),
  367
+                      self._related_objects_cache.items())
361 368
 
362 369
     def _fill_related_objects_cache(self):
363 370
         cache = SortedDict()
@@ -370,7 +377,7 @@ def _fill_related_objects_cache(self):
370 377
                     cache[obj] = parent
371 378
                 else:
372 379
                     cache[obj] = model
373  
-        for klass in get_models():
  380
+        for klass in get_models(include_auto_created=True):
374 381
             for f in klass._meta.local_fields:
375 382
                 if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
376 383
                     cache[RelatedObject(f.rel.to, klass, f)] = None
95  django/db/models/query.py
@@ -8,7 +8,8 @@
8 8
 from django.db.models.aggregates import Aggregate
9 9
 from django.db.models.fields import DateField
10 10
 from django.db.models.query_utils import (Q, select_related_descend,
11  
-    CollectedObjects, CyclicDependency, deferred_class_factory, InvalidQuery)
  11
+    deferred_class_factory, InvalidQuery)
  12
+from django.db.models.deletion import Collector
12 13
 from django.db.models import signals, sql
13 14
 from django.utils.copycompat import deepcopy
14 15
 
@@ -427,22 +428,9 @@ def delete(self):
427 428
         del_query.query.select_related = False
428 429
         del_query.query.clear_ordering()
429 430
 
430  
-        # Delete objects in chunks to prevent the list of related objects from
431  
-        # becoming too long.
432  
-        seen_objs = None
433  
-        del_itr = iter(del_query)
434  
-        while 1:
435  
-            # Collect a chunk of objects to be deleted, and then all the
436  
-            # objects that are related to the objects that are to be deleted.
437  
-            # The chunking *isn't* done by slicing the del_query because we
438  
-            # need to maintain the query cache on del_query (see #12328)
439  
-            seen_objs = CollectedObjects(seen_objs)
440  
-            for i, obj in izip(xrange(CHUNK_SIZE), del_itr):
441  
-                obj._collect_sub_objects(seen_objs)
442  
-
443  
-            if not seen_objs:
444  
-                break
445  
-            delete_objects(seen_objs, del_query.db)
  431
+        collector = Collector(using=del_query.db)
  432
+        collector.collect(del_query)
  433
+        collector.delete()
446 434
 
447 435
         # Clear the result cache, in case this QuerySet gets reused.
448 436
         self._result_cache = None
@@ -1287,79 +1275,6 @@ def get_cached_row(klass, row, index_start, using, max_depth=0, cur_depth=0,
1287 1275
                                     pass
1288 1276
     return obj, index_end
1289 1277
 
1290  
-def delete_objects(seen_objs, using):
1291  
-    """
1292  
-    Iterate through a list of seen classes, and remove any instances that are
1293  
-    referred to.
1294  
-    """
1295  
-    connection = connections[using]
1296  
-    if not transaction.is_managed(using=using):
1297  
-        transaction.enter_transaction_management(using=using)
1298  
-        forced_managed = True
1299  
-    else:
1300  
-        forced_managed = False
1301  
-    try:
1302  
-        ordered_classes = seen_objs.keys()
1303  
-    except CyclicDependency:
1304  
-        # If there is a cyclic dependency, we cannot in general delete the
1305  
-        # objects.  However, if an appropriate transaction is set up, or if the
1306  
-        # database is lax enough, it will succeed. So for now, we go ahead and
1307  
-        # try anyway.
1308  
-        ordered_classes = seen_objs.unordered_keys()
1309  
-
1310  
-    obj_pairs = {}
1311  
-    try:
1312  
-        for cls in ordered_classes:
1313  
-            items = seen_objs[cls].items()
1314  
-            items.sort()
1315  
-            obj_pairs[cls] = items
1316  
-
1317  
-            # Pre-notify all instances to be deleted.
1318  
-            for pk_val, instance in items:
1319  
-                if not cls._meta.auto_created:
1320  
-                    signals.pre_delete.send(sender=cls, instance=instance,
1321  
-                        using=using)
1322  
-
1323  
-            pk_list = [pk for pk,instance in items]
1324  
-
1325  
-            update_query = sql.UpdateQuery(cls)
1326  
-            for field, model in cls._meta.get_fields_with_model():
1327  
-                if (field.rel and field.null and field.rel.to in seen_objs and
1328  
-                        filter(lambda f: f.column == field.rel.get_related_field().column,
1329  
-                        field.rel.to._meta.fields)):
1330  
-                    if model:
1331  
-                        sql.UpdateQuery(model).clear_related(field, pk_list, using=using)
1332  
-                    else:
1333  
-                        update_query.clear_related(field, pk_list, using=using)
1334  
-
1335  
-        # Now delete the actual data.
1336  
-        for cls in ordered_classes:
1337  
-            items = obj_pairs[cls]
1338  
-            items.reverse()
1339  
-
1340  
-            pk_list = [pk for pk,instance in items]
1341  
-            del_query = sql.DeleteQuery(cls)
1342  
-            del_query.delete_batch(pk_list, using=using)
1343  
-
1344  
-            # Last cleanup; set NULLs where there once was a reference to the
1345  
-            # object, NULL the primary key of the found objects, and perform
1346  
-            # post-notification.
1347  
-            for pk_val, instance in items:
1348  
-                for field in cls._meta.fields:
1349  
-                    if field.rel and field.null and field.rel.to in seen_objs:
1350  
-                        setattr(instance, field.attname, None)
1351  
-
1352  
-                if not cls._meta.auto_created:
1353  
-                    signals.post_delete.send(sender=cls, instance=instance, using=using)
1354  
-                setattr(instance, cls._meta.pk.attname, None)
1355  
-
1356  
-        if forced_managed:
1357  
-            transaction.commit(using=using)
1358  
-        else:
1359  
-            transaction.commit_unless_managed(using=using)
1360  
-    finally:
1361  
-        if forced_managed:
1362  
-            transaction.leave_transaction_management(using=using)
1363 1278
 
1364 1279
 class RawQuerySet(object):
1365 1280
     """
108  django/db/models/query_utils.py
@@ -14,13 +14,6 @@
14 14
 from django.utils.datastructures import SortedDict
15 15
 
16 16
 
17  
-class CyclicDependency(Exception):
18  
-    """
19  
-    An error when dealing with a collection of objects that have a cyclic
20  
-    dependency, i.e. when deleting multiple objects.
21  
-    """
22  
-    pass
23  
-
24 17
 class InvalidQuery(Exception):
25 18
     """
26 19
     The query passed to raw isn't a safe query to use with raw.
@@ -28,107 +21,6 @@ class InvalidQuery(Exception):
28 21
     pass
29 22
 
30 23
 
31  
-class CollectedObjects(object):
32  
-    """
33  
-    A container that stores keys and lists of values along with remembering 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()