Skip to content

Commit

Permalink
Fixed #27852 -- Fixed object deletion to show all protected related o…
Browse files Browse the repository at this point in the history
…bjects rather than just the first one.
  • Loading branch information
hramezani committed Jan 25, 2020
1 parent 5d654e1 commit 2649fa4
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 3 deletions.
18 changes: 17 additions & 1 deletion django/db/models/deletion.py
Expand Up @@ -265,6 +265,7 @@ def collect(self, objs, source=None, nullable=False, collect_related=True,
if keep_parents:
parents = set(model._meta.get_parent_list())
model_fast_deletes = defaultdict(list)
protected_objects = {'keys': [], 'sub_objs': []}
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:
Expand Down Expand Up @@ -292,7 +293,22 @@ def collect(self, objs, source=None, nullable=False, collect_related=True,
))
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:
protected_objects['sub_objs'] += sub_objs
protected_objects['keys'].append("'%s.%s'" % (sub_objs[0].__class__.__name__, field.name))

if protected_objects['keys']:
raise ProtectedError(
"Cannot delete some instances of model '%s' because they are "
"referenced through protected foreign keys: %s." % (
field.remote_field.model.__name__,
', '.join(protected_objects['keys'])
),
protected_objects['sub_objs'],
)

for related_model, related_fields in model_fast_deletes.items():
batches = self.get_del_batches(new_objs, related_fields)
for batch in batches:
Expand Down
4 changes: 4 additions & 0 deletions tests/delete/models.py
Expand Up @@ -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',
Expand Down
14 changes: 12 additions & 2 deletions tests/delete/tests.py
Expand Up @@ -6,7 +6,7 @@
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,
Expand Down Expand Up @@ -72,11 +72,21 @@ def test_protect(self):
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)
with self.assertRaisesMessage(
IntegrityError,
"Cannot delete some instances of model 'R' because they are "
"referenced through protected foreign keys: 'A.protect', 'B.protect'"
):
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.
Expand Down

0 comments on commit 2649fa4

Please sign in to comment.