Fixed unwanted manager replacement in Django 1.11 projects.

Django 1.11 uses the old manager inheritance system, unless it's
overwritten with manager_inheritance_from_future. With a class layout
like:

  PolymorphicModel  (abstract)
    PolymorphicMPTTModel  (abstract)
      GenericCustomer  (concrete, has objects = ...)
        CustomerGroupBase (abstract, has objects = ...)
          Partner (concrete, no manager)
            BranchPartner  (concrete, no manager)

The last level gets a normal Django Manager instead of the polymorphic
manager. Because the PolymorphicModel had a base_objects manager, this
was typically used as _default_manager. Now that the default manager is
no longer affected, it's also easier to detect why the "objects" doesn't
get the proper manager type. Using "manager_inheritance_from_future" is
recommended instead to have both the right behavior and forward
Django 2.x compatibility.
fix_request_path_info
Diederik van der Boor 2018-02-04 13:12:40 +01:00
parent 723c78065c
commit f898f80594
4 changed files with 57 additions and 29 deletions

View File

@ -7,7 +7,10 @@ from __future__ import absolute_import
import inspect import inspect
import os import os
import sys import sys
import warnings
import django
from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.db.models.base import ModelBase from django.db.models.base import ModelBase
from django.db.models.manager import ManagerDescriptor from django.db.models.manager import ManagerDescriptor
@ -134,15 +137,40 @@ class PolymorphicModelBase(ModelBase):
and its querysets from PolymorphicQuerySet - throw AssertionError if not""" and its querysets from PolymorphicQuerySet - throw AssertionError if not"""
if not issubclass(type(manager), PolymorphicManager): if not issubclass(type(manager), PolymorphicManager):
e = 'PolymorphicModel: "' + model_name + '.' + manager_name + '" manager is of type "' + type(manager).__name__ if django.VERSION < (2, 0):
e += '", but must be a subclass of PolymorphicManager' extra = "\nConsider using Meta.manager_inheritance_from_future = True for Django 1.x projects"
raise AssertionError(e) else:
extra = ''
e = ('PolymorphicModel: "{0}.{1}" manager is of type "{2}", but must be a subclass of'
' PolymorphicManager.{extra}'.format(
model_name, manager_name, type(manager).__name__, extra=extra))
raise ImproperlyConfigured(e)
if not getattr(manager, 'queryset_class', None) or not issubclass(manager.queryset_class, PolymorphicQuerySet): if not getattr(manager, 'queryset_class', None) or not issubclass(manager.queryset_class, PolymorphicQuerySet):
e = 'PolymorphicModel: "' + model_name + '.' + manager_name + '" (PolymorphicManager) has been instantiated with a queryset class which is' e = ('PolymorphicModel: "{0}.{1}" (PolymorphicManager) has been instantiated with a queryset class '
e += ' not a subclass of PolymorphicQuerySet (which is required)' 'which is not a subclass of PolymorphicQuerySet (which is required)'.format(model_name, manager_name))
raise AssertionError(e) raise ImproperlyConfigured(e)
return manager return manager
@property
def base_objects(self):
warnings.warn(
"Using PolymorphicModel.base_objects is deprecated.\n"
"Use {0}.objects.non_polymorphic() instead.".format(self.__class__.__name__),
DeprecationWarning)
# Create a manager so the API works as expected. Just don't register it
# anymore in the Model Meta, so it doesn't substitute our polymorphic
# manager as default manager for the third level of inheritance when
# that third level doesn't define a manager at all.
manager = models.Manager()
manager.name = 'base_objects'
manager.model = self
return manager
@property
def _default_manager(self):
if len(sys.argv) > 1 and sys.argv[1] == 'dumpdata':
# TODO: investigate Django how this can be avoided
# hack: a small patch to Django would be a better solution. # hack: a small patch to Django would be a better solution.
# Django's management command 'dumpdata' relies on non-polymorphic # Django's management command 'dumpdata' relies on non-polymorphic
# behaviour of the _default_manager. Therefore, we catch any access to _default_manager # behaviour of the _default_manager. Therefore, we catch any access to _default_manager
@ -151,16 +179,14 @@ class PolymorphicModelBase(ModelBase):
# (non-polymorphic default manager is 'base_objects' for polymorphic models). # (non-polymorphic default manager is 'base_objects' for polymorphic models).
# This way we don't need to patch django.core.management.commands.dumpdata # This way we don't need to patch django.core.management.commands.dumpdata
# for all supported Django versions. # for all supported Django versions.
if len(sys.argv) > 1 and sys.argv[1] == 'dumpdata':
# manage.py dumpdata is running
def __getattribute__(self, name):
if name == '_default_manager':
frm = inspect.stack()[1] # frm[1] is caller file name, frm[3] is caller function name frm = inspect.stack()[1] # frm[1] is caller file name, frm[3] is caller function name
if DUMPDATA_COMMAND in frm[1]: if DUMPDATA_COMMAND in frm[1]:
return self.base_objects return self.base_objects
# caller_mod_name = inspect.getmodule(frm[0]).__name__ # does not work with python 2.4
# if caller_mod_name == 'django.core.management.commands.dumpdata':
return super(PolymorphicModelBase, self).__getattribute__(name) manager = super(PolymorphicModelBase, self)._default_manager
# TODO: investigate Django how this can be avoided if not isinstance(manager, PolymorphicManager):
warnings.warn("{0}._default_manager is not a PolymorphicManager".format(
self.__class__.__name__
), RuntimeWarning)
return manager

View File

@ -54,7 +54,6 @@ class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)):
# Note that Django 1.5 removes these managers because the model is abstract. # Note that Django 1.5 removes these managers because the model is abstract.
# They are pretended to be there by the metaclass in PolymorphicModelBase.get_inherited_managers() # They are pretended to be there by the metaclass in PolymorphicModelBase.get_inherited_managers()
objects = PolymorphicManager() objects = PolymorphicManager()
base_objects = models.Manager()
class Meta: class Meta:
abstract = True abstract = True

View File

@ -379,7 +379,7 @@ class PolymorphicQuerySet(QuerySet):
# Then we copy the extra() select fields from the base objects to the real objects. # Then we copy the extra() select fields from the base objects to the real objects.
# TODO: defer(), only(): support for these would be around here # TODO: defer(), only(): support for these would be around here
for real_concrete_class, idlist in idlist_per_model.items(): for real_concrete_class, idlist in idlist_per_model.items():
real_objects = real_concrete_class.base_objects.db_manager(self.db).filter(**{ real_objects = real_concrete_class.objects.non_polymorphic().using(self.db).filter(**{
('%s__in' % pk_name): idlist, ('%s__in' % pk_name): idlist,
}) })
real_objects.query.select_related = self.query.select_related # copy select related configuration to new qs real_objects.query.select_related = self.query.select_related # copy select related configuration to new qs

View File

@ -191,7 +191,9 @@ class MROBase1(ShowFieldType, PolymorphicModel):
class MROBase2(MROBase1): class MROBase2(MROBase1):
pass # Django vanilla inheritance does not inherit MyManager as _default_manager here class Meta:
# Django 1.x inheritance does not inherit MyManager as _default_manager here
manager_inheritance_from_future = True
class MROBase3(models.Model): class MROBase3(models.Model):
@ -200,7 +202,8 @@ class MROBase3(models.Model):
class MRODerived(MROBase2, MROBase3): class MRODerived(MROBase2, MROBase3):
pass class Meta:
manager_inheritance_from_future = True
class ParentModelWithManager(PolymorphicModel): class ParentModelWithManager(PolymorphicModel):