Ticket #16128: 16128.6.diff

File 16128.6.diff, 13.4 KB (added by akaariai, 4 years ago)
  • django/contrib/contenttypes/models.py

    diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py
    index 1bb07e0..bc9de56 100644
    a b class ContentTypeManager(models.Manager): 
    1616        return ct
    1717
    1818    def _get_opts(self, model):
    19         opts = model._meta
    20         while opts.proxy:
    21             model = opts.proxy_for_model
    22             opts = model._meta
    23         return opts
     19        """
     20        Returns the options for the concrete parent of the model.
     21        """
     22        return model._meta.concrete_class._meta
    2423
    2524    def _get_from_cache(self, opts):
    2625        key = (opts.app_label, opts.object_name.lower())
  • django/db/models/base.py

    diff --git a/django/db/models/base.py b/django/db/models/base.py
    index b5ce39e..033bf37 100644
    a b class ModelBase(type): 
    7575                    new_class._meta.get_latest_by = base_meta.get_latest_by
    7676
    7777        is_proxy = new_class._meta.proxy
     78        if not is_proxy and not abstract:
     79            new_class._meta.concrete_class = new_class
    7880
    7981        if getattr(new_class, '_default_manager', None):
    8082            if not is_proxy:
    class ModelBase(type): 
    122124            if (new_class._meta.local_fields or
    123125                    new_class._meta.local_many_to_many):
    124126                raise FieldError("Proxy model '%s' contains model fields." % name)
    125             while base._meta.proxy:
    126                 base = base._meta.proxy_for_model
    127127            new_class._meta.setup_proxy(base)
     128            # Add this class to 'proxying_models' for every proxied parent of
     129            # this class.
     130            while base:
     131                base._meta.proxying_models.append(new_class)
     132                base = base._meta.proxy_for_model
    128133
    129134        # Do the appropriate setup for any model parents.
    130135        o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields
    class ModelBase(type): 
    149154                                        (field.name, name, base.__name__))
    150155            if not base._meta.abstract:
    151156                # Concrete classes...
    152                 while base._meta.proxy:
    153                     # Skip over a proxy class to the "real" base it proxies.
    154                     base = base._meta.proxy_for_model
     157                base = base._meta.concrete_class
    155158                if base in o2o_map:
    156159                    field = o2o_map[base]
    157160                elif not is_proxy:
  • django/db/models/deletion.py

    diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py
    index 9a6d499..d119534 100644
    a b class Collector(object): 
    145145        if not new_objs:
    146146            return
    147147        model = new_objs[0].__class__
     148        # Always use the concrete model class for deletion. We will handle
     149        # all the proxy-child classes later on. We do this because when
     150        # we are deleting a model, we are in fact deleting the whole proxy
     151        # equivalence class.
     152        model = model._meta.concrete_class
    148153
    149154        # Recursively collect parent models, but not their related objects.
    150155        # These will be found by meta.get_all_related_objects()
    class Collector(object): 
    157162                             reverse_dependency=True)
    158163
    159164        if collect_related:
    160             for related in model._meta.get_all_related_objects(include_hidden=True):
    161                 field = related.field
    162                 if related.model._meta.auto_created:
    163                     self.add_batch(related.model, field, new_objs)
    164                 else:
    165                     sub_objs = self.related_objects(related, new_objs)
    166                     if not sub_objs:
     165            # Collect all related objects to this model and for models proxying
     166            # this model. Note that at this point the model is always the concrete
     167            # parent in the proxy chain.
     168            # When deleting a model we are also deleting every model that
     169            # is in proxy-relation to this model. Would this be better handled by
     170            # get_all_related_objects kwarg include_proxy? Are we continuously
     171            # finding the same relations (proxy has all the same relations out as
     172            # the parent).
     173            proxy_equivalance = [model] + model._meta.proxying_models
     174
     175            # keep track of duplicate related models. These can be seen because of
     176            # proxy models.
     177            seen_related = set()
     178            for base in proxy_equivalance:
     179                for related in base._meta.get_all_related_objects(include_hidden=True):
     180                    if related in seen_related:
    167181                        continue
    168                     field.rel.on_delete(self, field, sub_objs, self.using)
    169 
    170             # TODO This entire block is only needed as a special case to
    171             # support cascade-deletes for GenericRelation. It should be
    172             # removed/fixed when the ORM gains a proper abstraction for virtual
    173             # or composite fields, and GFKs are reworked to fit into that.
    174             for relation in model._meta.many_to_many:
    175                 if not relation.rel.through:
    176                     sub_objs = relation.bulk_related_objects(new_objs, self.using)
    177                     self.collect(sub_objs,
    178                                  source=model,
    179                                  source_attr=relation.rel.related_name,
    180                                  nullable=True)
     182                    seen_related.add(related)
     183                    field = related.field
     184                    if related.model._meta.auto_created:
     185                        self.add_batch(related.model, field, new_objs)
     186                    else:
     187                        sub_objs = self.related_objects(related, new_objs)
     188                        if not sub_objs:
     189                            continue
     190                        field.rel.on_delete(self, field, sub_objs, self.using)
     191
     192                # TODO This entire block is only needed as a special case to
     193                # support cascade-deletes for GenericRelation. It should be
     194                # removed/fixed when the ORM gains a proper abstraction for virtual
     195                # or composite fields, and GFKs are reworked to fit into that.
     196                for relation in base._meta.many_to_many:
     197                    if not relation.rel.through:
     198                        if relation in seen_related:
     199                            continue
     200                        seen_related.add(relation)
     201                        sub_objs = relation.bulk_related_objects(new_objs, self.using)
     202                        self.collect(sub_objs,
     203                                     source=model,
     204                                     source_attr=relation.rel.related_name,
     205                                     nullable=True)
    181206
    182207    def related_objects(self, related, objs):
    183208        """
  • django/db/models/options.py

    diff --git a/django/db/models/options.py b/django/db/models/options.py
    index 0cd52a3..853a616 100644
    a b class Options(object): 
    4040        self.abstract = False
    4141        self.managed = True
    4242        self.proxy = False
     43        # None for abstract classes, own class for concrete models
     44        # and the first concrete parent for proxy models. This does
     45        # not track multitable parents.
     46        self.concrete_class = None
    4347        self.proxy_for_model = None
     48        # We need to keep track of which models are proxying this model
     49        # so that we can fetch all references to this model when deleting.
     50        self.proxying_models = []
    4451        self.parents = SortedDict()
    4552        self.duplicate_targets = {}
    4653        self.auto_created = False
    class Options(object): 
    183190        """
    184191        self.pk = target._meta.pk
    185192        self.proxy_for_model = target
     193        self.concrete_class = target._meta.concrete_class
    186194        self.db_table = target._meta.db_table
    187195
    188196    def __repr__(self):
  • django/db/models/sql/query.py

    diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
    index 61fd2be..b40c4c8 100644
    a b class Query(object): 
    569569            return
    570570        orig_opts = self.model._meta
    571571        seen = {}
    572         if orig_opts.proxy:
    573             must_include = {orig_opts.proxy_for_model: set([orig_opts.pk])}
    574         else:
    575             must_include = {self.model: set([orig_opts.pk])}
     572        must_include = {orig_opts.concrete_class: set([orig_opts.pk])}
    576573        for field_name in field_names:
    577574            parts = field_name.split(LOOKUP_SEP)
    578575            cur_model = self.model
    def add_to_dict(data, key, value): 
    19491946        data[key] = set([value])
    19501947
    19511948def get_proxied_model(opts):
    1952     int_opts = opts
    1953     proxied_model = None
    1954     while int_opts.proxy:
    1955         proxied_model = int_opts.proxy_for_model
    1956         int_opts = proxied_model._meta
    1957     return proxied_model
     1949    return opts.concrete_class
  • new file tests/regressiontests/proxy_bug/models.py

    diff --git a/tests/regressiontests/proxy_bug/__init__.py b/tests/regressiontests/proxy_bug/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/regressiontests/proxy_bug/models.py b/tests/regressiontests/proxy_bug/models.py
    new file mode 100644
    index 0000000..7389ffe
    - +  
     1from django.db import models
     2
     3class File(models.Model):
     4    pass
     5
     6class Image(File):
     7    class Meta:
     8        proxy = True
     9
     10class Photo(Image):
     11    class Meta:
     12        proxy = True
     13
     14class FooImage(models.Model):
     15    my_image = models.ForeignKey(Image)
     16   
     17class FooFile(models.Model):
     18    my_file = models.ForeignKey(File)
     19
     20class FooPhoto(models.Model):
     21    my_photo = models.ForeignKey(Photo)
     22
  • new file tests/regressiontests/proxy_bug/tests.py

    diff --git a/tests/regressiontests/proxy_bug/tests.py b/tests/regressiontests/proxy_bug/tests.py
    new file mode 100644
    index 0000000..5704d35
    - +  
     1from django.test import TestCase
     2#from django.test.utils import override_settings
     3from .models import Image, File, Photo, FooFile, FooImage, FooPhoto
     4
     5
     6class ProxyDeleteImageTest(TestCase):
     7    '''
     8    Tests on_delete behaviour for proxy models. Deleting the *proxy*
     9    instance bubbles through to its non-proxy and *all* referring objects
     10    are deleted.
     11    '''
     12
     13    def setUp(self):
     14        # Create an Image
     15        self.test_image = Image()
     16        self.test_image.save()
     17        foo_image = FooImage(my_image=self.test_image)
     18        foo_image.save()
     19
     20        # Get the Image instance as a File
     21        test_file = File.objects.get(pk=self.test_image.pk)
     22        foo_file = FooFile(my_file=test_file)
     23        foo_file.save()
     24
     25    #@override_settings(DEBUG=True)
     26    def test_delete(self):
     27        self.assertTrue(Image.objects.all().delete() is None)
     28        # An Image deletion == File deletion
     29        self.assertEqual(len(Image.objects.all()), 0)
     30        self.assertEqual(len(File.objects.all()), 0)
     31        # The Image deletion cascaded and *all* references to it are deleted.
     32        self.assertEqual(len(FooImage.objects.all()), 0)
     33        self.assertEqual(len(FooFile.objects.all()), 0)
     34        #from django.db import connection
     35        #for q in connection.queries:
     36        #    print q
     37
     38
     39class ProxyDeletePhotoTest(ProxyDeleteImageTest):
     40    '''
     41    Tests on_delete behaviour for proxy-of-proxy models. Deleting the *proxy*
     42    instance should bubble through to its proxy and non-proxy variants.
     43    Deleting *all* referring objects. For some reason it seems that the
     44    intermediate proxy model isn't cleaned up.
     45    '''
     46
     47    def setUp(self):
     48        # Create the Image, FooImage and FooFile instances
     49        super(ProxyDeletePhotoTest, self).setUp()
     50        # Get the Image as a Photo
     51        test_photo = Photo.objects.get(pk=self.test_image.pk)
     52        foo_photo = FooPhoto(my_photo=test_photo)
     53        foo_photo.save()
     54
     55
     56    #@override_settings(DEBUG=True)
     57    def test_delete(self):
     58        self.assertTrue(Photo.objects.all().delete() is None)
     59        # A Photo deletion == Image deletion == File deletion
     60        self.assertEqual(len(Photo.objects.all()), 0)
     61        self.assertEqual(len(Image.objects.all()), 0)
     62        self.assertEqual(len(File.objects.all()), 0)
     63        # The Photo deletion should have cascaded and deleted *all*
     64        # references to it.
     65        self.assertEqual(len(FooPhoto.objects.all()), 0)
     66        self.assertEqual(len(FooFile.objects.all()), 0)
     67        self.assertEqual(len(FooImage.objects.all()), 0)
     68        #from django.db import connection
     69        #for q in connection.queries:
     70        #    print q
     71        #print len(connection.queries)
     72
     73
     74class ProxyDeleteFileTest(ProxyDeleteImageTest):
     75    '''
     76    Tests on_delete cascade behaviour for proxy models. Deleting the
     77    *non-proxy* instance of a model should somehow notify it's proxy.
     78    Currently it doesn't, making this test fail.
     79    '''
     80
     81    def test_delete(self):
     82        # This should *not* raise an IntegrityError on databases that support
     83        # FK constraints.
     84        self.assertTrue(File.objects.all().delete() is None)
     85        # A File deletion == Image deletion
     86        self.assertEqual(len(File.objects.all()), 0)
     87        self.assertEqual(len(Image.objects.all()), 0)
     88        # The File deletion should have cascaded and deleted *all* references
     89        # to it.
     90        self.assertEqual(len(FooFile.objects.all()), 0)
     91        self.assertEqual(len(FooImage.objects.all()), 0)
Back to Top