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,33 +137,56 @@ 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
# hack: a small patch to Django would be a better solution. @property
# Django's management command 'dumpdata' relies on non-polymorphic def base_objects(self):
# behaviour of the _default_manager. Therefore, we catch any access to _default_manager warnings.warn(
# here and return the non-polymorphic default manager instead if we are called from 'dumpdata.py' "Using PolymorphicModel.base_objects is deprecated.\n"
# Otherwise, the base objects will be upcasted to polymorphic models, and be outputted as such. "Use {0}.objects.non_polymorphic() instead.".format(self.__class__.__name__),
# (non-polymorphic default manager is 'base_objects' for polymorphic models). DeprecationWarning)
# This way we don't need to patch django.core.management.commands.dumpdata
# for all supported Django versions.
if len(sys.argv) > 1 and sys.argv[1] == 'dumpdata':
# manage.py dumpdata is running
def __getattribute__(self, name): # Create a manager so the API works as expected. Just don't register it
if name == '_default_manager': # anymore in the Model Meta, so it doesn't substitute our polymorphic
frm = inspect.stack()[1] # frm[1] is caller file name, frm[3] is caller function name # manager as default manager for the third level of inheritance when
if DUMPDATA_COMMAND in frm[1]: # that third level doesn't define a manager at all.
return self.base_objects manager = models.Manager()
# caller_mod_name = inspect.getmodule(frm[0]).__name__ # does not work with python 2.4 manager.name = 'base_objects'
# if caller_mod_name == 'django.core.management.commands.dumpdata': manager.model = self
return manager
return super(PolymorphicModelBase, self).__getattribute__(name) @property
# TODO: investigate Django how this can be avoided 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.
# Django's management command 'dumpdata' relies on non-polymorphic
# behaviour of the _default_manager. Therefore, we catch any access to _default_manager
# here and return the non-polymorphic default manager instead if we are called from 'dumpdata.py'
# Otherwise, the base objects will be upcasted to polymorphic models, and be outputted as such.
# (non-polymorphic default manager is 'base_objects' for polymorphic models).
# This way we don't need to patch django.core.management.commands.dumpdata
# for all supported Django versions.
frm = inspect.stack()[1] # frm[1] is caller file name, frm[3] is caller function name
if DUMPDATA_COMMAND in frm[1]:
return self.base_objects
manager = super(PolymorphicModelBase, self)._default_manager
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):