|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- """
- PolymorphicModel Meta Class
- """
- import inspect
- import os
- import sys
- import warnings
-
- import django
- from django.core.exceptions import ImproperlyConfigured
- from django.db import models
- from django.db.models.base import ModelBase
- from django.db.models.manager import ManagerDescriptor
-
- from .managers import PolymorphicManager
- from .query import PolymorphicQuerySet
-
- # PolymorphicQuerySet Q objects (and filter()) support these additional key words.
- # These are forbidden as field names (a descriptive exception is raised)
- POLYMORPHIC_SPECIAL_Q_KWORDS = ["instance_of", "not_instance_of"]
-
- DUMPDATA_COMMAND = os.path.join("django", "core", "management", "commands", "dumpdata.py")
-
-
- class ManagerInheritanceWarning(RuntimeWarning):
- pass
-
-
- ###################################################################################
- # PolymorphicModel meta class
-
-
- class PolymorphicModelBase(ModelBase):
- """
- Manager inheritance is a pretty complex topic which may need
- more thought regarding how this should be handled for polymorphic
- models.
-
- In any case, we probably should propagate 'objects' and 'base_objects'
- from PolymorphicModel to every subclass. We also want to somehow
- inherit/propagate _default_manager as well, as it needs to be polymorphic.
-
- The current implementation below is an experiment to solve this
- problem with a very simplistic approach: We unconditionally
- inherit/propagate any and all managers (using _copy_to_model),
- as long as they are defined on polymorphic models
- (the others are left alone).
-
- Like Django ModelBase, we special-case _default_manager:
- if there are any user-defined managers, it is set to the first of these.
-
- We also require that _default_manager as well as any user defined
- polymorphic managers produce querysets that are derived from
- PolymorphicQuerySet.
- """
-
- def __new__(self, model_name, bases, attrs, **kwargs):
- # print; print '###', model_name, '- bases:', bases
-
- # Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses:
- if not attrs and model_name == "NewBase":
- return super().__new__(self, model_name, bases, attrs, **kwargs)
-
- # Make sure that manager_inheritance_from_future is set, since django-polymorphic 1.x already
- # simulated that behavior on the polymorphic manager to all subclasses behave like polymorphics
- if django.VERSION < (2, 0):
- if "Meta" in attrs:
- if not hasattr(attrs["Meta"], "manager_inheritance_from_future"):
- attrs["Meta"].manager_inheritance_from_future = True
- else:
- attrs["Meta"] = type("Meta", (object,), {"manager_inheritance_from_future": True})
-
- # create new model
- new_class = self.call_superclass_new_method(model_name, bases, attrs, **kwargs)
-
- # check if the model fields are all allowed
- self.validate_model_fields(new_class)
-
- # validate resulting default manager
- if not new_class._meta.abstract and not new_class._meta.swapped:
- self.validate_model_manager(new_class.objects, model_name, "objects")
-
- # for __init__ function of this class (monkeypatching inheritance accessors)
- new_class.polymorphic_super_sub_accessors_replaced = False
-
- # determine the name of the primary key field and store it into the class variable
- # polymorphic_primary_key_name (it is needed by query.py)
- for f in new_class._meta.fields:
- if f.primary_key and type(f) != models.OneToOneField:
- new_class.polymorphic_primary_key_name = f.name
- break
-
- return new_class
-
- @classmethod
- def call_superclass_new_method(self, model_name, bases, attrs, **kwargs):
- """call __new__ method of super class and return the newly created class.
- Also work around a limitation in Django's ModelBase."""
- # There seems to be a general limitation in Django's app_label handling
- # regarding abstract models (in ModelBase). See issue 1 on github - TODO: propose patch for Django
- # We run into this problem if polymorphic.py is located in a top-level directory
- # which is directly in the python path. To work around this we temporarily set
- # app_label here for PolymorphicModel.
- meta = attrs.get("Meta", None)
- do_app_label_workaround = (
- meta
- and attrs["__module__"] == "polymorphic"
- and model_name == "PolymorphicModel"
- and getattr(meta, "app_label", None) is None
- )
-
- if do_app_label_workaround:
- meta.app_label = "poly_dummy_app_label"
- new_class = super().__new__(self, model_name, bases, attrs, **kwargs)
- if do_app_label_workaround:
- del meta.app_label
- return new_class
-
- @classmethod
- def validate_model_fields(self, new_class):
- "check if all fields names are allowed (i.e. not in POLYMORPHIC_SPECIAL_Q_KWORDS)"
- for f in new_class._meta.fields:
- if f.name in POLYMORPHIC_SPECIAL_Q_KWORDS:
- e = 'PolymorphicModel: "%s" - field name "%s" is not allowed in polymorphic models'
- raise AssertionError(e % (new_class.__name__, f.name))
-
- @classmethod
- def validate_model_manager(self, manager, model_name, manager_name):
- """check if the manager is derived from PolymorphicManager
- and its querysets from PolymorphicQuerySet - throw AssertionError if not"""
-
- if not issubclass(type(manager), PolymorphicManager):
- if django.VERSION < (2, 0):
- extra = "\nConsider using Meta.manager_inheritance_from_future = True for Django 1.x projects"
- else:
- extra = ""
- e = (
- 'PolymorphicModel: "{0}.{1}" manager is of type "{2}", but must be a subclass of'
- " PolymorphicManager.{extra} to support retrieving subclasses".format(
- model_name, manager_name, type(manager).__name__, extra=extra
- )
- )
- warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
- return manager
-
- if not getattr(manager, "queryset_class", None) or not issubclass(
- manager.queryset_class, PolymorphicQuerySet
- ):
- e = (
- 'PolymorphicModel: "{}.{}" has been instantiated with a queryset class '
- "which is not a subclass of PolymorphicQuerySet (which is required)".format(
- model_name, manager_name
- )
- )
- warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
- return manager
-
- @property
- def base_objects(self):
- warnings.warn(
- "Using PolymorphicModel.base_objects is deprecated.\n"
- "Use {}.objects.non_polymorphic() instead.".format(self.__class__.__name__),
- DeprecationWarning,
- stacklevel=2,
- )
- return self._base_objects
-
- @property
- def _base_objects(self):
- # 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.
- # 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()._default_manager
- if not isinstance(manager, PolymorphicManager):
- warnings.warn(
- "{}._default_manager is not a PolymorphicManager".format(self.__class__.__name__),
- ManagerInheritanceWarning,
- )
-
- return manager
|