summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorRussell Keith-Magee <russell@keith-magee.com>2010-03-15 13:15:01 +0000
committerRussell Keith-Magee <russell@keith-magee.com>2010-03-15 13:15:01 +0000
commit2d57300f5298c0dc88789fb24922f423e34e9149 (patch)
tree81f4a4f794ee6c68cff1817a4e2faf7543b6ee38 /tests
parentd8c9d21c33ce08f7e90e5996d4da9d8d25d5f8c4 (diff)
Fixed #12953 -- Ensure that deletion cascades through generic relations. Also cleans up the special-casing of generic relations in the deleted object discovery process. Thanks to carljm for the report and patch.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@12790 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'tests')
-rw-r--r--tests/regressiontests/delete_regress/models.py81
-rw-r--r--tests/regressiontests/delete_regress/tests.py108
2 files changed, 136 insertions, 53 deletions
diff --git a/tests/regressiontests/delete_regress/models.py b/tests/regressiontests/delete_regress/models.py
index ff561c9d20..4f61e78513 100644
--- a/tests/regressiontests/delete_regress/models.py
+++ b/tests/regressiontests/delete_regress/models.py
@@ -1,62 +1,37 @@
-from django.conf import settings
-from django.db import models, backend, connection, transaction, DEFAULT_DB_ALIAS
-from django.db.models import sql, query
-from django.test import TransactionTestCase
+from django.db import models
-class Book(models.Model):
- pagecount = models.IntegerField()
-
-# Can't run this test under SQLite, because you can't
-# get two connections to an in-memory database.
-if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.sqlite3':
- class DeleteLockingTest(TransactionTestCase):
- def setUp(self):
- # Create a second connection to the database
- conn_settings = settings.DATABASES[DEFAULT_DB_ALIAS]
- self.conn2 = backend.DatabaseWrapper({
- 'HOST': conn_settings['HOST'],
- 'NAME': conn_settings['NAME'],
- 'OPTIONS': conn_settings['OPTIONS'],
- 'PASSWORD': conn_settings['PASSWORD'],
- 'PORT': conn_settings['PORT'],
- 'USER': conn_settings['USER'],
- 'TIME_ZONE': settings.TIME_ZONE,
- })
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
- # Put both DB connections into managed transaction mode
- transaction.enter_transaction_management()
- transaction.managed(True)
- self.conn2._enter_transaction_management(True)
+class Award(models.Model):
+ name = models.CharField(max_length=25)
+ object_id = models.PositiveIntegerField()
+ content_type = models.ForeignKey(ContentType)
+ content_object = generic.GenericForeignKey()
- def tearDown(self):
- # Close down the second connection.
- transaction.leave_transaction_management()
- self.conn2.close()
+class AwardNote(models.Model):
+ award = models.ForeignKey(Award)
+ note = models.CharField(max_length=100)
- def test_concurrent_delete(self):
- "Deletes on concurrent transactions don't collide and lock the database. Regression for #9479"
+class Person(models.Model):
+ name = models.CharField(max_length=25)
+ awards = generic.GenericRelation(Award)
- # Create some dummy data
- b1 = Book(id=1, pagecount=100)
- b2 = Book(id=2, pagecount=200)
- b3 = Book(id=3, pagecount=300)
- b1.save()
- b2.save()
- b3.save()
+class Book(models.Model):
+ pagecount = models.IntegerField()
- transaction.commit()
+class Toy(models.Model):
+ name = models.CharField(max_length=50)
- self.assertEquals(3, Book.objects.count())
+class Child(models.Model):
+ name = models.CharField(max_length=50)
+ toys = models.ManyToManyField(Toy, through='PlayedWith')
- # Delete something using connection 2.
- cursor2 = self.conn2.cursor()
- cursor2.execute('DELETE from delete_regress_book WHERE id=1')
- self.conn2._commit();
+class PlayedWith(models.Model):
+ child = models.ForeignKey(Child)
+ toy = models.ForeignKey(Toy)
+ date = models.DateField()
- # Now perform a queryset delete that covers the object
- # deleted in connection 2. This causes an infinite loop
- # under MySQL InnoDB unless we keep track of already
- # deleted objects.
- Book.objects.filter(pagecount__lt=250).delete()
- transaction.commit()
- self.assertEquals(1, Book.objects.count())
+class PlayedWithNote(models.Model):
+ played = models.ForeignKey(PlayedWith)
+ note = models.TextField()
diff --git a/tests/regressiontests/delete_regress/tests.py b/tests/regressiontests/delete_regress/tests.py
new file mode 100644
index 0000000000..7ebe5e86a4
--- /dev/null
+++ b/tests/regressiontests/delete_regress/tests.py
@@ -0,0 +1,108 @@
+import datetime
+
+from django.conf import settings
+from django.db import backend, connection, transaction, DEFAULT_DB_ALIAS
+from django.test import TestCase, TransactionTestCase
+
+from models import Book, Award, AwardNote, Person, Child, Toy, PlayedWith, PlayedWithNote
+
+# Can't run this test under SQLite, because you can't
+# get two connections to an in-memory database.
+if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.sqlite3':
+ class DeleteLockingTest(TransactionTestCase):
+ def setUp(self):
+ # Create a second connection to the default database
+ conn_settings = settings.DATABASES[DEFAULT_DB_ALIAS]
+ self.conn2 = backend.DatabaseWrapper({
+ 'HOST': conn_settings['HOST'],
+ 'NAME': conn_settings['NAME'],
+ 'OPTIONS': conn_settings['OPTIONS'],
+ 'PASSWORD': conn_settings['PASSWORD'],
+ 'PORT': conn_settings['PORT'],
+ 'USER': conn_settings['USER'],
+ 'TIME_ZONE': settings.TIME_ZONE,
+ })
+
+ # Put both DB connections into managed transaction mode
+ transaction.enter_transaction_management()
+ transaction.managed(True)
+ self.conn2._enter_transaction_management(True)
+
+ def tearDown(self):
+ # Close down the second connection.
+ transaction.leave_transaction_management()
+ self.conn2.close()
+
+ def test_concurrent_delete(self):
+ "Deletes on concurrent transactions don't collide and lock the database. Regression for #9479"
+
+ # Create some dummy data
+ b1 = Book(id=1, pagecount=100)
+ b2 = Book(id=2, pagecount=200)
+ b3 = Book(id=3, pagecount=300)
+ b1.save()
+ b2.save()
+ b3.save()
+
+ transaction.commit()
+
+ self.assertEquals(3, Book.objects.count())
+
+ # Delete something using connection 2.
+ cursor2 = self.conn2.cursor()
+ cursor2.execute('DELETE from delete_regress_book WHERE id=1')
+ self.conn2._commit();
+
+ # Now perform a queryset delete that covers the object
+ # deleted in connection 2. This causes an infinite loop
+ # under MySQL InnoDB unless we keep track of already
+ # deleted objects.
+ Book.objects.filter(pagecount__lt=250).delete()
+ transaction.commit()
+ self.assertEquals(1, Book.objects.count())
+
+class DeleteCascadeTests(TestCase):
+ def test_generic_relation_cascade(self):
+ """
+ Test that Django cascades deletes through generic-related
+ objects to their reverse relations.
+
+ This might falsely succeed if the database cascades deletes
+ itself immediately; the postgresql_psycopg2 backend does not
+ give such a false success because ForeignKeys are created with
+ DEFERRABLE INITIALLY DEFERRED, so its internal cascade is
+ delayed until transaction commit.
+
+ """
+ person = Person.objects.create(name='Nelson Mandela')
+ award = Award.objects.create(name='Nobel', content_object=person)
+ note = AwardNote.objects.create(note='a peace prize',
+ award=award)
+ self.assertEquals(AwardNote.objects.count(), 1)
+ person.delete()
+ self.assertEquals(Award.objects.count(), 0)
+ # first two asserts are just sanity checks, this is the kicker:
+ self.assertEquals(AwardNote.objects.count(), 0)
+
+ def test_fk_to_m2m_through(self):
+ """
+ Test that if a M2M relationship has an explicitly-specified
+ through model, and some other model has an FK to that through
+ model, deletion is cascaded from one of the participants in
+ the M2M, to the through model, to its related model.
+
+ Like the above test, this could in theory falsely succeed if
+ the DB cascades deletes itself immediately.
+
+ """
+ juan = Child.objects.create(name='Juan')
+ paints = Toy.objects.create(name='Paints')
+ played = PlayedWith.objects.create(child=juan, toy=paints,
+ date=datetime.date.today())
+ note = PlayedWithNote.objects.create(played=played,
+ note='the next Jackson Pollock')
+ self.assertEquals(PlayedWithNote.objects.count(), 1)
+ paints.delete()
+ self.assertEquals(PlayedWith.objects.count(), 0)
+ # first two asserts just sanity checks, this is the kicker:
+ self.assertEquals(PlayedWithNote.objects.count(), 0)