Ticket #12953: 12953_r12599.diff

File 12953_r12599.diff, 9.7 KB (added by Carl Meyer, 14 years ago)

possible fix

  • 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  
    12721272                    signals.pre_delete.send(sender=cls, instance=instance)
    12731273
    12741274            pk_list = [pk for pk,instance in items]
    1275             del_query = sql.DeleteQuery(cls)
    1276             del_query.delete_batch_related(pk_list, using=using)
    12771275
    12781276            update_query = sql.UpdateQuery(cls)
    12791277            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
  • 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
    - +  
     1from django.test import TestCase
     2
     3from models import Award, AwardNote, Person
     4
     5class DeleteCascadeTests(TestCase):
     6    def test_generic_relation_cascade(self):
     7        """
     8        Test that Django cascades deletes through generic-related
     9        objects to their reverse relations.
     10       
     11        This might falsely succeed if the database cascades deletes
     12        itself immediately; the postgresql_psycopg2 backend does not
     13        give such a false success because ForeignKeys are created with
     14        DEFERRABLE INITIALLY DEFERRED, so its internal cascade is
     15        delayed until transaction commit.
     16
     17        """
     18        person = Person.objects.create(name='Nelson Mandela')
     19        award = Award.objects.create(name='Nobel', content_object=person)
     20        note = AwardNote.objects.create(note='a peace prize',
     21                                        award=award)
     22        self.assertEquals(AwardNote.objects.count(), 1)
     23        person.delete()
     24        self.assertEquals(Award.objects.count(), 0)
     25        # first two asserts are just sanity checks, this is the kicker:
     26        self.assertEquals(AwardNote.objects.count(), 0)
Back to Top