diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 3bdd89cbd98..0493b0e1e20 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -305,7 +305,7 @@ class Collector: model.__name__, ', '.join(protected_objects), ), - chain.from_iterable(protected_objects.values()), + set(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) @@ -340,7 +340,7 @@ class Collector: model.__name__, ', '.join(restricted_objects), ), - chain.from_iterable(restricted_objects.values()), + set(chain.from_iterable(restricted_objects.values())), ) def related_objects(self, related_model, related_fields, objs): diff --git a/docs/releases/3.1.3.txt b/docs/releases/3.1.3.txt index ed412263896..bfd78a7648e 100644 --- a/docs/releases/3.1.3.txt +++ b/docs/releases/3.1.3.txt @@ -39,3 +39,9 @@ Bugfixes :class:`~django.contrib.postgres.constraints.ExclusionConstraint` with key transforms for :class:`~django.db.models.JSONField` in ``expressions`` (:ticket:`32096`). + +* Fixed a regression in Django 3.1 where + :exc:`ProtectedError.protected_objects ` and + :exc:`RestrictedError.restricted_objects ` + attributes returned iterators instead of :py:class:`set` of objects + (:ticket:`32107`). diff --git a/tests/delete/tests.py b/tests/delete/tests.py index 485ae1aaf5a..3dce135dd50 100644 --- a/tests/delete/tests.py +++ b/tests/delete/tests.py @@ -75,19 +75,21 @@ class OnDeleteTests(TestCase): "Cannot delete some instances of model 'R' because they are " "referenced through protected foreign keys: 'A.protect'." ) - with self.assertRaisesMessage(ProtectedError, msg): + with self.assertRaisesMessage(ProtectedError, msg) as cm: a.protect.delete() + self.assertEqual(cm.exception.protected_objects, {a}) def test_protect_multiple(self): a = create_a('protect') - B.objects.create(protect=a.protect) + b = 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): + with self.assertRaisesMessage(ProtectedError, msg) as cm: a.protect.delete() + self.assertEqual(cm.exception.protected_objects, {a, b}) def test_protect_path(self): a = create_a('protect') @@ -97,8 +99,9 @@ class OnDeleteTests(TestCase): "Cannot delete some instances of model 'P' because they are " "referenced through protected foreign keys: 'R.p'." ) - with self.assertRaisesMessage(ProtectedError, msg): + with self.assertRaisesMessage(ProtectedError, msg) as cm: a.protect.p.delete() + self.assertEqual(cm.exception.protected_objects, {a}) def test_do_nothing(self): # Testing DO_NOTHING is a bit harder: It would raise IntegrityError for a normal model, @@ -176,19 +179,21 @@ class OnDeleteTests(TestCase): "Cannot delete some instances of model 'R' because they are " "referenced through restricted foreign keys: 'A.restrict'." ) - with self.assertRaisesMessage(RestrictedError, msg): + with self.assertRaisesMessage(RestrictedError, msg) as cm: a.restrict.delete() + self.assertEqual(cm.exception.restricted_objects, {a}) def test_restrict_multiple(self): a = create_a('restrict') - B3.objects.create(restrict=a.restrict) + b3 = B3.objects.create(restrict=a.restrict) msg = ( "Cannot delete some instances of model 'R' because they are " "referenced through restricted foreign keys: 'A.restrict', " "'B3.restrict'." ) - with self.assertRaisesMessage(RestrictedError, msg): + with self.assertRaisesMessage(RestrictedError, msg) as cm: a.restrict.delete() + self.assertEqual(cm.exception.restricted_objects, {a, b3}) def test_restrict_path_cascade_indirect(self): a = create_a('restrict') @@ -198,8 +203,9 @@ class OnDeleteTests(TestCase): "Cannot delete some instances of model 'P' because they are " "referenced through restricted foreign keys: 'A.restrict'." ) - with self.assertRaisesMessage(RestrictedError, msg): + with self.assertRaisesMessage(RestrictedError, msg) as cm: a.restrict.p.delete() + self.assertEqual(cm.exception.restricted_objects, {a}) # Object referenced also with CASCADE relationship can be deleted. a.cascade.p = a.restrict.p a.cascade.save() @@ -221,13 +227,14 @@ class OnDeleteTests(TestCase): delete_top = DeleteTop.objects.create() b1 = B1.objects.create(delete_top=delete_top) b2 = B2.objects.create(delete_top=delete_top) - DeleteBottom.objects.create(b1=b1, b2=b2) + delete_bottom = DeleteBottom.objects.create(b1=b1, b2=b2) msg = ( "Cannot delete some instances of model 'B1' because they are " "referenced through restricted foreign keys: 'DeleteBottom.b1'." ) - with self.assertRaisesMessage(RestrictedError, msg): + with self.assertRaisesMessage(RestrictedError, msg) as cm: b1.delete() + self.assertEqual(cm.exception.restricted_objects, {delete_bottom}) self.assertTrue(DeleteTop.objects.exists()) self.assertTrue(B1.objects.exists()) self.assertTrue(B2.objects.exists()) @@ -243,14 +250,18 @@ class OnDeleteTests(TestCase): delete_top = DeleteTop.objects.create() generic_b1 = GenericB1.objects.create(generic_delete_top=delete_top) generic_b2 = GenericB2.objects.create(generic_delete_top=delete_top) - GenericDeleteBottom.objects.create(generic_b1=generic_b1, generic_b2=generic_b2) + generic_delete_bottom = GenericDeleteBottom.objects.create( + generic_b1=generic_b1, + generic_b2=generic_b2, + ) msg = ( "Cannot delete some instances of model 'GenericB1' because they " "are referenced through restricted foreign keys: " "'GenericDeleteBottom.generic_b1'." ) - with self.assertRaisesMessage(RestrictedError, msg): + with self.assertRaisesMessage(RestrictedError, msg) as cm: generic_b1.delete() + self.assertEqual(cm.exception.restricted_objects, {generic_delete_bottom}) self.assertTrue(DeleteTop.objects.exists()) self.assertTrue(GenericB1.objects.exists()) self.assertTrue(GenericB2.objects.exists())