diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 603411cfc83..6ed12ec0922 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -265,6 +265,7 @@ class Collector: if keep_parents: parents = set(model._meta.get_parent_list()) model_fast_deletes = defaultdict(list) + protected_objects = defaultdict(list) for related in get_candidate_relations_to_delete(model._meta): # Preserve parent reverse relationships if keep_parents=True. if keep_parents and related.model in parents: @@ -292,7 +293,23 @@ class Collector: )) sub_objs = sub_objs.only(*tuple(referenced_fields)) if sub_objs: - field.remote_field.on_delete(self, field, sub_objs, self.using) + try: + field.remote_field.on_delete(self, field, sub_objs, self.using) + except ProtectedError as error: + key = "'%s.%s'" % ( + error.protected_objects[0].__class__.__name__, + field.name, + ) + protected_objects[key] += error.protected_objects + if protected_objects: + raise ProtectedError( + 'Cannot delete some instances of model %r because they are ' + 'referenced through protected foreign keys: %s.' % ( + model.__name__, + ', '.join(protected_objects), + ), + chain.from_iterable(protected_objects.values()), + ) for related_model, related_fields in model_fast_deletes.items(): batches = self.get_del_batches(new_objs, related_fields) for batch in batches: diff --git a/tests/delete/models.py b/tests/delete/models.py index 7a38ecc7770..618b36bda90 100644 --- a/tests/delete/models.py +++ b/tests/delete/models.py @@ -68,6 +68,10 @@ class A(models.Model): o2o_setnull = models.ForeignKey(R, models.SET_NULL, null=True, related_name="o2o_nullable_set") +class B(models.Model): + protect = models.ForeignKey(R, models.PROTECT) + + def create_a(name): a = A(name=name) for name in ('auto', 'auto_nullable', 'setvalue', 'setnull', 'setdefault', diff --git a/tests/delete/tests.py b/tests/delete/tests.py index b93bdd52653..5046afba179 100644 --- a/tests/delete/tests.py +++ b/tests/delete/tests.py @@ -1,12 +1,14 @@ from math import ceil from django.db import IntegrityError, connection, models -from django.db.models.deletion import Collector, RestrictedError +from django.db.models.deletion import ( + Collector, ProtectedError, RestrictedError, +) from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature from .models import ( - B1, B2, MR, A, Avatar, Base, Child, DeleteBottom, DeleteTop, GenericB1, + B1, B2, MR, A, Avatar, B, Base, Child, DeleteBottom, DeleteTop, GenericB1, GenericB2, GenericDeleteBottom, HiddenUser, HiddenUserProfile, M, M2MFrom, M2MTo, MRNull, Origin, P, Parent, R, RChild, RChildChild, Referrer, S, T, User, create_a, get_default_r, @@ -72,11 +74,22 @@ class OnDeleteTests(TestCase): a = create_a('protect') msg = ( "Cannot delete some instances of model 'R' because they are " - "referenced through a protected foreign key: 'A.protect'" + "referenced through protected foreign keys: 'A.protect'." ) with self.assertRaisesMessage(IntegrityError, msg): a.protect.delete() + def test_protect_multiple(self): + a = create_a('protect') + B.objects.create(protect=a.protect) + msg = ( + "Cannot delete some instances of model 'R' because they are " + "referenced through protected foreign keys: 'A.protect', " + "'B.protect'." + ) + with self.assertRaisesMessage(ProtectedError, msg): + a.protect.delete() + def test_do_nothing(self): # Testing DO_NOTHING is a bit harder: It would raise IntegrityError for a normal model, # so we connect to pre_delete and set the fk to a known value.