Ticket #12953: 12953_r12756.diff

File 12953_r12756.diff, 11.5 KB (added by Carl Meyer, 9 years ago)

patch with additional test for odd M2M case

  • django/contrib/admin/util.py

    diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
    a b  
    108108        # TODO using a private model API!
    109109        obj._collect_sub_objects(collector)
    110110
    111     # TODO This next bit is needed only because GenericRelations are
    112     # cascade-deleted way down in the internals in
    113     # DeleteQuery.delete_batch_related, instead of being found by
    114     # _collect_sub_objects. Refs #12593.
    115     from django.contrib.contenttypes import generic
    116     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 the
    121                 # generic-related object also has FKs pointing to it
    122                 # from elsewhere. DeleteQuery does not follow those
    123                 # FKs or delete any such objects explicitly (which is
    124                 # probably a bug). Some databases may cascade those
    125                 # deletes themselves, and some won't. So do we report
    126                 # those objects as to-be-deleted? No right answer; for
    127                 # now we opt to report only on objects that Django
    128                 # will explicitly delete, at risk that some further
    129                 # objects will be silently deleted by a
    130                 # referential-integrity-maintaining database.
    131                 collector.add(related.__class__, related.pk, related,
    132                               obj.__class__, obj)
    133 
    134111    perms_needed = set()
    135112
    136113    to_delete = collector.nested(_format_callback,
     
    188165        """
    189166        model, pk = type(obj), obj._get_pk_val()
    190167
     168        # auto-created M2M models don't interest us
     169        if model._meta.auto_created:
     170            return True
     171       
    191172        key = model, pk
    192173
    193174        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  
    555555
    556556        for related in self._meta.get_all_related_objects():
    557557            rel_opts_name = related.get_accessor_name()
    558             if isinstance(related.field.rel, OneToOneRel):
     558            if not related.field.rel.multiple:
    559559                try:
    560560                    sub_obj = getattr(self, rel_opts_name)
    561561                except ObjectDoesNotExist:
     
    581581                for sub_obj in delete_qs:
    582582                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
    583583
     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
    584608        # Handle any ancestors (for the model-inheritance case). We do this by
    585609        # traversing to the most remote parent classes -- those with no parents
    586610        # 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  
    12781278                    signals.pre_delete.send(sender=cls, instance=instance)
    12791279
    12801280            pk_list = [pk for pk,instance in items]
    1281             del_query = sql.DeleteQuery(cls)
    1282             del_query.delete_batch_related(pk_list, using=using)
    12831281
    12841282            update_query = sql.UpdateQuery(cls)
    12851283            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  
    2626        self.where = where
    2727        self.get_compiler(using).execute_sql(None)
    2828
    29     def delete_batch_related(self, pk_list, using):
    30         """
    31         Set up and execute delete queries for all the objects related to the
    32         primary key values in pk_list. To delete the objects themselves, use
    33         the delete_batch() method.
    34 
    35         More than one physical query may be executed if there are a
    36         lot of values in pk_list.
    37         """
    38         from django.contrib.contenttypes import generic
    39         cls = self.model
    40         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 = None
    54             if isinstance(f, generic.GenericRelation):
    55                 from django.contrib.contenttypes.models import ContentType
    56                 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_value
    61             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 
    7129    def delete_batch(self, pk_list, using):
    7230        """
    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.
    7532
    7633        More than one physical query may be executed if there are a
    7734        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  
    33from django.db.models import sql, query
    44from django.test import TransactionTestCase
    55
     6from django.contrib.contenttypes import generic
     7from django.contrib.contenttypes.models import ContentType
     8
     9class 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
     15class AwardNote(models.Model):
     16    award = models.ForeignKey(Award)
     17    note = models.CharField(max_length=100)
     18
     19class Person(models.Model):
     20    name = models.CharField(max_length=25)
     21    awards = generic.GenericRelation(Award)
     22
    623class Book(models.Model):
    724    pagecount = models.IntegerField()
    825
     26class Toy(models.Model):
     27    name = models.CharField(max_length=50)
     28
     29class Child(models.Model):
     30    name = models.CharField(max_length=50)
     31    toys = models.ManyToManyField(Toy, through='PlayedWith')
     32
     33class PlayedWith(models.Model):
     34    child = models.ForeignKey(Child)
     35    toy = models.ForeignKey(Toy)
     36    date = models.DateField()
     37
     38class PlayedWithNote(models.Model):
     39    played = models.ForeignKey(PlayedWith)
     40    note = models.TextField()
     41
    942# Can't run this test under SQLite, because you can't
    1043# get two connections to an in-memory database.
    1144if 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
    - +  
     1import datetime
     2
     3from django.test import TestCase
     4
     5from models import Award, AwardNote, Person, Child, Toy, PlayedWith, PlayedWithNote
     6
     7class 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)
Back to Top