diff options
| author | Daniel Izquierdo <daniel@makeleaps.com> | 2016-10-10 16:23:35 +0900 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2019-11-19 10:55:05 +0100 |
| commit | 89abecc75d7eadab681f29be5237972c6c2f997b (patch) | |
| tree | 0f0f5bcf8480308dec2899536ae9ce11b3bb555a /tests/delete | |
| parent | 4e1d809aa570c0e0736587672607f9d6c22e42c9 (diff) | |
Fixed #27272 -- Added an on_delete RESTRICT handler to allow cascading deletions while protecting direct ones.
Diffstat (limited to 'tests/delete')
| -rw-r--r-- | tests/delete/models.py | 57 | ||||
| -rw-r--r-- | tests/delete/tests.py | 88 |
2 files changed, 140 insertions, 5 deletions
diff --git a/tests/delete/models.py b/tests/delete/models.py index e2ddce0588..7a38ecc777 100644 --- a/tests/delete/models.py +++ b/tests/delete/models.py @@ -1,8 +1,17 @@ +from django.contrib.contenttypes.fields import ( + GenericForeignKey, GenericRelation, +) +from django.contrib.contenttypes.models import ContentType from django.db import models +class P(models.Model): + pass + + class R(models.Model): is_default = models.BooleanField(default=False) + p = models.ForeignKey(P, models.CASCADE, null=True) def __str__(self): return "%s" % self.pk @@ -46,10 +55,12 @@ class A(models.Model): ) cascade = models.ForeignKey(R, models.CASCADE, related_name='cascade_set') cascade_nullable = models.ForeignKey(R, models.CASCADE, null=True, related_name='cascade_nullable_set') - protect = models.ForeignKey(R, models.PROTECT, null=True) + protect = models.ForeignKey(R, models.PROTECT, null=True, related_name='protect_set') + restrict = models.ForeignKey(R, models.RESTRICT, null=True, related_name='restrict_set') donothing = models.ForeignKey(R, models.DO_NOTHING, null=True, related_name='donothing_set') child = models.ForeignKey(RChild, models.CASCADE, related_name="child") child_setnull = models.ForeignKey(RChild, models.SET_NULL, null=True, related_name="child_setnull") + cascade_p = models.ForeignKey(P, models.CASCADE, related_name='cascade_p_set', null=True) # A OneToOneField is just a ForeignKey unique=True, so we don't duplicate # all the tests; just one smoke test to ensure on_delete works for it as @@ -61,7 +72,7 @@ def create_a(name): a = A(name=name) for name in ('auto', 'auto_nullable', 'setvalue', 'setnull', 'setdefault', 'setdefault_none', 'cascade', 'cascade_nullable', 'protect', - 'donothing', 'o2o_setnull'): + 'restrict', 'donothing', 'o2o_setnull'): r = R.objects.create() setattr(a, name, r) a.child = RChild.objects.create() @@ -147,3 +158,45 @@ class SecondReferrer(models.Model): other_referrer = models.ForeignKey( Referrer, models.CASCADE, to_field='unique_field', related_name='+' ) + + +class DeleteTop(models.Model): + b1 = GenericRelation('GenericB1') + b2 = GenericRelation('GenericB2') + + +class B1(models.Model): + delete_top = models.ForeignKey(DeleteTop, models.CASCADE) + + +class B2(models.Model): + delete_top = models.ForeignKey(DeleteTop, models.CASCADE) + + +class DeleteBottom(models.Model): + b1 = models.ForeignKey(B1, models.RESTRICT) + b2 = models.ForeignKey(B2, models.CASCADE) + + +class GenericB1(models.Model): + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + generic_delete_top = GenericForeignKey('content_type', 'object_id') + + +class GenericB2(models.Model): + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + generic_delete_top = GenericForeignKey('content_type', 'object_id') + generic_delete_bottom = GenericRelation('GenericDeleteBottom') + + +class GenericDeleteBottom(models.Model): + generic_b1 = models.ForeignKey(GenericB1, models.RESTRICT) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + generic_b2 = GenericForeignKey() + + +class GenericDeleteBottomParent(models.Model): + generic_delete_bottom = models.ForeignKey(GenericDeleteBottom, on_delete=models.CASCADE) diff --git a/tests/delete/tests.py b/tests/delete/tests.py index d61fdb40c1..b93bdd5265 100644 --- a/tests/delete/tests.py +++ b/tests/delete/tests.py @@ -1,13 +1,14 @@ from math import ceil from django.db import IntegrityError, connection, models -from django.db.models.deletion import Collector +from django.db.models.deletion import Collector, RestrictedError from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature from .models import ( - MR, A, Avatar, Base, Child, HiddenUser, HiddenUserProfile, M, M2MFrom, - M2MTo, MRNull, Origin, Parent, R, RChild, RChildChild, Referrer, S, T, + B1, B2, MR, A, Avatar, 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, ) @@ -146,6 +147,87 @@ class OnDeleteTests(TestCase): a = A.objects.get(pk=a.pk) self.assertIsNone(a.o2o_setnull) + def test_restrict(self): + a = create_a('restrict') + msg = ( + "Cannot delete some instances of model 'R' because they are " + "referenced through a restricted foreign key: 'A.restrict'." + ) + with self.assertRaisesMessage(RestrictedError, msg): + a.restrict.delete() + + def test_restrict_path_cascade_indirect(self): + a = create_a('restrict') + a.restrict.p = P.objects.create() + a.restrict.save() + msg = ( + "Cannot delete some instances of model 'R' because they are " + "referenced through a restricted foreign key: 'A.restrict'." + ) + with self.assertRaisesMessage(RestrictedError, msg): + a.restrict.p.delete() + # Object referenced also with CASCADE relationship can be deleted. + a.cascade.p = a.restrict.p + a.cascade.save() + a.restrict.p.delete() + self.assertFalse(A.objects.filter(name='restrict').exists()) + self.assertFalse(R.objects.filter(pk=a.restrict_id).exists()) + + def test_restrict_path_cascade_direct(self): + a = create_a('restrict') + a.restrict.p = P.objects.create() + a.restrict.save() + a.cascade_p = a.restrict.p + a.save() + a.restrict.p.delete() + self.assertFalse(A.objects.filter(name='restrict').exists()) + self.assertFalse(R.objects.filter(pk=a.restrict_id).exists()) + + def test_restrict_path_cascade_indirect_diamond(self): + 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) + msg = ( + "Cannot delete some instances of model 'B1' because they are " + "referenced through a restricted foreign key: 'DeleteBottom.b1'." + ) + with self.assertRaisesMessage(RestrictedError, msg): + b1.delete() + self.assertTrue(DeleteTop.objects.exists()) + self.assertTrue(B1.objects.exists()) + self.assertTrue(B2.objects.exists()) + self.assertTrue(DeleteBottom.objects.exists()) + # Object referenced also with CASCADE relationship can be deleted. + delete_top.delete() + self.assertFalse(DeleteTop.objects.exists()) + self.assertFalse(B1.objects.exists()) + self.assertFalse(B2.objects.exists()) + self.assertFalse(DeleteBottom.objects.exists()) + + def test_restrict_gfk_no_fast_delete(self): + 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) + msg = ( + "Cannot delete some instances of model 'GenericB1' because they " + "are referenced through a restricted foreign key: " + "'GenericDeleteBottom.generic_b1'." + ) + with self.assertRaisesMessage(RestrictedError, msg): + generic_b1.delete() + self.assertTrue(DeleteTop.objects.exists()) + self.assertTrue(GenericB1.objects.exists()) + self.assertTrue(GenericB2.objects.exists()) + self.assertTrue(GenericDeleteBottom.objects.exists()) + # Object referenced also with CASCADE relationship can be deleted. + delete_top.delete() + self.assertFalse(DeleteTop.objects.exists()) + self.assertFalse(GenericB1.objects.exists()) + self.assertFalse(GenericB2.objects.exists()) + self.assertFalse(GenericDeleteBottom.objects.exists()) + class DeletionTests(TestCase): |
