[1.8.x] Fixed #25563 -- Cached deferred models in their proxied model's _meta.apps.

Thanks to Andriy Sokolovskiy for the report and Tim Graham for the review.

Backport of 3db3ab71e9 from master
This commit is contained in:
Simon Charette 2015-10-19 14:17:55 -04:00
parent 9ccb92ad01
commit 71962629c0
3 changed files with 30 additions and 4 deletions

View File

@ -9,7 +9,6 @@ from __future__ import unicode_literals
from collections import namedtuple from collections import namedtuple
from django.apps import apps
from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from django.db.backends import utils from django.db.backends import utils
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
@ -215,12 +214,13 @@ def deferred_class_factory(model, attrs):
""" """
if not attrs: if not attrs:
return model return model
opts = model._meta
# Never create deferred models based on deferred model # Never create deferred models based on deferred model
if model._deferred: if model._deferred:
# Deferred models are proxies for the non-deferred model. We never # Deferred models are proxies for the non-deferred model. We never
# create chains of defers => proxy_for_model is the non-deferred # create chains of defers => proxy_for_model is the non-deferred
# model. # model.
model = model._meta.proxy_for_model model = opts.proxy_for_model
# The app registry wants a unique name for each model, otherwise the new # The app registry wants a unique name for each model, otherwise the new
# class won't be created (we get an exception). Therefore, we generate # class won't be created (we get an exception). Therefore, we generate
# the name using the passed in attrs. It's OK to reuse an existing class # the name using the passed in attrs. It's OK to reuse an existing class
@ -229,13 +229,14 @@ def deferred_class_factory(model, attrs):
name = utils.truncate_name(name, 80, 32) name = utils.truncate_name(name, 80, 32)
try: try:
return apps.get_model(model._meta.app_label, name) return opts.apps.get_model(model._meta.app_label, name)
except LookupError: except LookupError:
class Meta: class Meta:
proxy = True proxy = True
app_label = model._meta.app_label apps = opts.apps
app_label = opts.app_label
overrides = {attr: DeferredAttribute(attr, model) for attr in attrs} overrides = {attr: DeferredAttribute(attr, model) for attr in attrs}
overrides["Meta"] = Meta overrides["Meta"] = Meta

View File

@ -29,3 +29,8 @@ Bugfixes
* Avoided a confusing stack trace when starting :djadmin:`runserver` with an * Avoided a confusing stack trace when starting :djadmin:`runserver` with an
invalid :setting:`INSTALLED_APPS` setting (:ticket:`25510`). This regression invalid :setting:`INSTALLED_APPS` setting (:ticket:`25510`). This regression
appeared in 1.8.5 as a side effect of fixing :ticket:`24704`. appeared in 1.8.5 as a side effect of fixing :ticket:`24704`.
* Made deferred models use their proxied model's ``_meta.apps`` for caching
and retrieval (:ticket:`25563`). This prevents any models generated in data
migrations using ``QuerySet.defer()`` from leaking to test and application
code.

View File

@ -3,8 +3,10 @@ from __future__ import unicode_literals
from operator import attrgetter from operator import attrgetter
from django.apps import apps from django.apps import apps
from django.apps.registry import Apps
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.backends.db import SessionStore from django.contrib.sessions.backends.db import SessionStore
from django.db import models
from django.db.models import Count from django.db.models import Count
from django.db.models.query_utils import ( from django.db.models.query_utils import (
DeferredAttribute, deferred_class_factory, DeferredAttribute, deferred_class_factory,
@ -263,6 +265,24 @@ class DeferRegressionTest(TestCase):
deferred_cls = deferred_class_factory(Item, ()) deferred_cls = deferred_class_factory(Item, ())
self.assertFalse(deferred_cls._deferred) self.assertFalse(deferred_cls._deferred)
def test_deferred_class_factory_apps_reuse(self):
"""
#25563 - model._meta.apps should be used for caching and
retrieval of the created proxy class.
"""
isolated_apps = Apps(['defer_regress'])
class BaseModel(models.Model):
field = models.BooleanField()
class Meta:
apps = isolated_apps
app_label = 'defer_regress'
deferred_model = deferred_class_factory(BaseModel, ['field'])
self.assertIs(deferred_model._meta.apps, isolated_apps)
self.assertIs(deferred_class_factory(BaseModel, ['field']), deferred_model)
class DeferAnnotateSelectRelatedTest(TestCase): class DeferAnnotateSelectRelatedTest(TestCase):
def test_defer_annotate_select_related(self): def test_defer_annotate_select_related(self):