Ticket #12953: 12953_r12756.diff
File 12953_r12756.diff, 11.5 KB (added by , 15 years ago) |
---|
-
django/contrib/admin/util.py
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
a b 108 108 # TODO using a private model API! 109 109 obj._collect_sub_objects(collector) 110 110 111 # TODO This next bit is needed only because GenericRelations are112 # cascade-deleted way down in the internals in113 # DeleteQuery.delete_batch_related, instead of being found by114 # _collect_sub_objects. Refs #12593.115 from django.contrib.contenttypes import generic116 for f in obj._meta.many_to_many:117 if isinstance(f, generic.GenericRelation):118 rel_manager = f.value_from_object(obj)119 for related in rel_manager.all():120 # There's a wierdness here in the case that the121 # generic-related object also has FKs pointing to it122 # from elsewhere. DeleteQuery does not follow those123 # FKs or delete any such objects explicitly (which is124 # probably a bug). Some databases may cascade those125 # deletes themselves, and some won't. So do we report126 # those objects as to-be-deleted? No right answer; for127 # now we opt to report only on objects that Django128 # will explicitly delete, at risk that some further129 # objects will be silently deleted by a130 # referential-integrity-maintaining database.131 collector.add(related.__class__, related.pk, related,132 obj.__class__, obj)133 134 111 perms_needed = set() 135 112 136 113 to_delete = collector.nested(_format_callback, … … 188 165 """ 189 166 model, pk = type(obj), obj._get_pk_val() 190 167 168 # auto-created M2M models don't interest us 169 if model._meta.auto_created: 170 return True 171 191 172 key = model, pk 192 173 193 174 if key in self.seen: -
django/db/models/base.py
diff --git a/django/db/models/base.py b/django/db/models/base.py
a b 555 555 556 556 for related in self._meta.get_all_related_objects(): 557 557 rel_opts_name = related.get_accessor_name() 558 if isinstance(related.field.rel, OneToOneRel):558 if not related.field.rel.multiple: 559 559 try: 560 560 sub_obj = getattr(self, rel_opts_name) 561 561 except ObjectDoesNotExist: … … 581 581 for sub_obj in delete_qs: 582 582 sub_obj._collect_sub_objects(seen_objs, self, related.field.null) 583 583 584 for related in self._meta.get_all_related_many_to_many_objects(): 585 if related.field.rel.through: 586 opts = related.field.rel.through._meta 587 reverse_field_name = related.field.m2m_reverse_field_name() 588 nullable = opts.get_field(reverse_field_name).null 589 filters = {reverse_field_name: self} 590 for sub_obj in related.field.rel.through._base_manager.filter(**filters): 591 sub_obj._collect_sub_objects(seen_objs, self, nullable) 592 593 for f in self._meta.many_to_many: 594 if f.rel.through: 595 opts = f.rel.through._meta 596 field_name = f.m2m_field_name() 597 nullable = opts.get_field(field_name).null 598 filters = {field_name: self} 599 for sub_obj in f.rel.through._base_manager.filter(**filters): 600 sub_obj._collect_sub_objects(seen_objs, self, nullable) 601 else: 602 # m2m-ish but with no through table? GenericRelation: cascade delete 603 for sub_obj in f.value_from_object(self).all(): 604 # Generic relations not enforced by db constraints, thus we can set 605 # nullable=True, order does not matter 606 sub_obj._collect_sub_objects(seen_objs, self, True) 607 584 608 # Handle any ancestors (for the model-inheritance case). We do this by 585 609 # traversing to the most remote parent classes -- those with no parents 586 610 # themselves -- and then adding those instances to the collection. That -
django/db/models/query.py
diff --git a/django/db/models/query.py b/django/db/models/query.py
a b 1278 1278 signals.pre_delete.send(sender=cls, instance=instance) 1279 1279 1280 1280 pk_list = [pk for pk,instance in items] 1281 del_query = sql.DeleteQuery(cls)1282 del_query.delete_batch_related(pk_list, using=using)1283 1281 1284 1282 update_query = sql.UpdateQuery(cls) 1285 1283 for field, model in cls._meta.get_fields_with_model(): -
django/db/models/sql/subqueries.py
diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py
a b 26 26 self.where = where 27 27 self.get_compiler(using).execute_sql(None) 28 28 29 def delete_batch_related(self, pk_list, using):30 """31 Set up and execute delete queries for all the objects related to the32 primary key values in pk_list. To delete the objects themselves, use33 the delete_batch() method.34 35 More than one physical query may be executed if there are a36 lot of values in pk_list.37 """38 from django.contrib.contenttypes import generic39 cls = self.model40 for related in cls._meta.get_all_related_many_to_many_objects():41 if not isinstance(related.field, generic.GenericRelation):42 for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):43 where = self.where_class()44 where.add((Constraint(None,45 related.field.m2m_reverse_name(), related.field),46 'in',47 pk_list[offset : offset+GET_ITERATOR_CHUNK_SIZE]),48 AND)49 self.do_query(related.field.m2m_db_table(), where, using=using)50 51 for f in cls._meta.many_to_many:52 w1 = self.where_class()53 db_prep_value = None54 if isinstance(f, generic.GenericRelation):55 from django.contrib.contenttypes.models import ContentType56 ct_field = f.rel.to._meta.get_field(f.content_type_field_name)57 w1.add((Constraint(None, ct_field.column, ct_field), 'exact',58 ContentType.objects.get_for_model(cls).id), AND)59 id_field = f.rel.to._meta.get_field(f.object_id_field_name)60 db_prep_value = id_field.get_db_prep_value61 for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):62 where = self.where_class()63 where.add((Constraint(None, f.m2m_column_name(), f), 'in',64 map(db_prep_value,65 pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE])),66 AND)67 if w1:68 where.add(w1, AND)69 self.do_query(f.m2m_db_table(), where, using=using)70 71 29 def delete_batch(self, pk_list, using): 72 30 """ 73 Set up and execute delete queries for all the objects in pk_list. This 74 should be called after delete_batch_related(), if necessary. 31 Set up and execute delete queries for all the objects in pk_list. 75 32 76 33 More than one physical query may be executed if there are a 77 34 lot of values in pk_list. -
tests/regressiontests/delete_regress/models.py
diff --git a/tests/regressiontests/delete_regress/models.py b/tests/regressiontests/delete_regress/models.py
a b 3 3 from django.db.models import sql, query 4 4 from django.test import TransactionTestCase 5 5 6 from django.contrib.contenttypes import generic 7 from django.contrib.contenttypes.models import ContentType 8 9 class Award(models.Model): 10 name = models.CharField(max_length=25) 11 object_id = models.PositiveIntegerField() 12 content_type = models.ForeignKey(ContentType) 13 content_object = generic.GenericForeignKey() 14 15 class AwardNote(models.Model): 16 award = models.ForeignKey(Award) 17 note = models.CharField(max_length=100) 18 19 class Person(models.Model): 20 name = models.CharField(max_length=25) 21 awards = generic.GenericRelation(Award) 22 6 23 class Book(models.Model): 7 24 pagecount = models.IntegerField() 8 25 26 class Toy(models.Model): 27 name = models.CharField(max_length=50) 28 29 class Child(models.Model): 30 name = models.CharField(max_length=50) 31 toys = models.ManyToManyField(Toy, through='PlayedWith') 32 33 class PlayedWith(models.Model): 34 child = models.ForeignKey(Child) 35 toy = models.ForeignKey(Toy) 36 date = models.DateField() 37 38 class PlayedWithNote(models.Model): 39 played = models.ForeignKey(PlayedWith) 40 note = models.TextField() 41 9 42 # Can't run this test under SQLite, because you can't 10 43 # get two connections to an in-memory database. 11 44 if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.sqlite3': -
new file tests/regressiontests/delete_regress/tests.py
diff --git a/tests/regressiontests/delete_regress/tests.py b/tests/regressiontests/delete_regress/tests.py new file mode 100644
- + 1 import datetime 2 3 from django.test import TestCase 4 5 from models import Award, AwardNote, Person, Child, Toy, PlayedWith, PlayedWithNote 6 7 class DeleteCascadeTests(TestCase): 8 def test_generic_relation_cascade(self): 9 """ 10 Test that Django cascades deletes through generic-related 11 objects to their reverse relations. 12 13 This might falsely succeed if the database cascades deletes 14 itself immediately; the postgresql_psycopg2 backend does not 15 give such a false success because ForeignKeys are created with 16 DEFERRABLE INITIALLY DEFERRED, so its internal cascade is 17 delayed until transaction commit. 18 19 """ 20 person = Person.objects.create(name='Nelson Mandela') 21 award = Award.objects.create(name='Nobel', content_object=person) 22 note = AwardNote.objects.create(note='a peace prize', 23 award=award) 24 self.assertEquals(AwardNote.objects.count(), 1) 25 person.delete() 26 self.assertEquals(Award.objects.count(), 0) 27 # first two asserts are just sanity checks, this is the kicker: 28 self.assertEquals(AwardNote.objects.count(), 0) 29 30 def test_fk_to_m2m_through(self): 31 """ 32 Test that if a M2M relationship has an explicitly-specified 33 through model, and some other model has an FK to that through 34 model, deletion is cascaded from one of the participants in 35 the M2M, to the through model, to its related model. 36 37 Like the above test, this could in theory falsely succeed if 38 the DB cascades deletes itself immediately. 39 40 """ 41 juan = Child.objects.create(name='Juan') 42 paints = Toy.objects.create(name='Paints') 43 played = PlayedWith.objects.create(child=juan, toy=paints, 44 date=datetime.date.today()) 45 note = PlayedWithNote.objects.create(played=played, 46 note='the next Jackson Pollock') 47 self.assertEquals(PlayedWithNote.objects.count(), 1) 48 paints.delete() 49 self.assertEquals(PlayedWith.objects.count(), 0) 50 # first two asserts just sanity checks, this is the kicker: 51 self.assertEquals(PlayedWithNote.objects.count(), 0)