1# -*- coding: utf-8 -*-
2"""
3PolymorphicModel Meta Class
4"""
5from __future__ import absolute_import
6
7import inspect
8import os
9import sys
10import warnings
11
12import django
13from django.core.exceptions import ImproperlyConfigured
14from django.db import models
15from django.db.models.base import ModelBase
16from django.db.models.manager import ManagerDescriptor
17
18from .managers import PolymorphicManager
19from .query import PolymorphicQuerySet
20
21# PolymorphicQuerySet Q objects (and filter()) support these additional key words.
22# These are forbidden as field names (a descriptive exception is raised)
23POLYMORPHIC_SPECIAL_Q_KWORDS = ["instance_of", "not_instance_of"]
24
25DUMPDATA_COMMAND = os.path.join(
26    "django", "core", "management", "commands", "dumpdata.py"
27)
28
29
30class ManagerInheritanceWarning(RuntimeWarning):
31    pass
32
33
34###################################################################################
35# PolymorphicModel meta class
36
37
38class PolymorphicModelBase(ModelBase):
39    """
40    Manager inheritance is a pretty complex topic which may need
41    more thought regarding how this should be handled for polymorphic
42    models.
43
44    In any case, we probably should propagate 'objects' and 'base_objects'
45    from PolymorphicModel to every subclass. We also want to somehow
46    inherit/propagate _default_manager as well, as it needs to be polymorphic.
47
48    The current implementation below is an experiment to solve this
49    problem with a very simplistic approach: We unconditionally
50    inherit/propagate any and all managers (using _copy_to_model),
51    as long as they are defined on polymorphic models
52    (the others are left alone).
53
54    Like Django ModelBase, we special-case _default_manager:
55    if there are any user-defined managers, it is set to the first of these.
56
57    We also require that _default_manager as well as any user defined
58    polymorphic managers produce querysets that are derived from
59    PolymorphicQuerySet.
60    """
61
62    def __new__(self, model_name, bases, attrs):
63        # print; print '###', model_name, '- bases:', bases
64
65        # Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses:
66        if not attrs and model_name == "NewBase":
67            return super(PolymorphicModelBase, self).__new__(
68                self, model_name, bases, attrs
69            )
70
71        # Make sure that manager_inheritance_from_future is set, since django-polymorphic 1.x already
72        # simulated that behavior on the polymorphic manager to all subclasses behave like polymorphics
73        if django.VERSION < (2, 0):
74            if "Meta" in attrs:
75                if not hasattr(attrs["Meta"], "manager_inheritance_from_future"):
76                    attrs["Meta"].manager_inheritance_from_future = True
77            else:
78                attrs["Meta"] = type(
79                    "Meta", (object,), {"manager_inheritance_from_future": True}
80                )
81
82        # create new model
83        new_class = self.call_superclass_new_method(model_name, bases, attrs)
84
85        # check if the model fields are all allowed
86        self.validate_model_fields(new_class)
87
88        # validate resulting default manager
89        if not new_class._meta.abstract and not new_class._meta.swapped:
90            self.validate_model_manager(new_class.objects, model_name, "objects")
91
92        # for __init__ function of this class (monkeypatching inheritance accessors)
93        new_class.polymorphic_super_sub_accessors_replaced = False
94
95        # determine the name of the primary key field and store it into the class variable
96        # polymorphic_primary_key_name (it is needed by query.py)
97        for f in new_class._meta.fields:
98            if f.primary_key and type(f) != models.OneToOneField:
99                new_class.polymorphic_primary_key_name = f.name
100                break
101
102        return new_class
103
104    @classmethod
105    def call_superclass_new_method(self, model_name, bases, attrs):
106        """call __new__ method of super class and return the newly created class.
107        Also work around a limitation in Django's ModelBase."""
108        # There seems to be a general limitation in Django's app_label handling
109        # regarding abstract models (in ModelBase). See issue 1 on github - TODO: propose patch for Django
110        # We run into this problem if polymorphic.py is located in a top-level directory
111        # which is directly in the python path. To work around this we temporarily set
112        # app_label here for PolymorphicModel.
113        meta = attrs.get("Meta", None)
114        do_app_label_workaround = (
115            meta
116            and attrs["__module__"] == "polymorphic"
117            and model_name == "PolymorphicModel"
118            and getattr(meta, "app_label", None) is None
119        )
120
121        if do_app_label_workaround:
122            meta.app_label = "poly_dummy_app_label"
123        new_class = super(PolymorphicModelBase, self).__new__(
124            self, model_name, bases, attrs
125        )
126        if do_app_label_workaround:
127            del meta.app_label
128        return new_class
129
130    @classmethod
131    def validate_model_fields(self, new_class):
132        "check if all fields names are allowed (i.e. not in POLYMORPHIC_SPECIAL_Q_KWORDS)"
133        for f in new_class._meta.fields:
134            if f.name in POLYMORPHIC_SPECIAL_Q_KWORDS:
135                e = 'PolymorphicModel: "%s" - field name "%s" is not allowed in polymorphic models'
136                raise AssertionError(e % (new_class.__name__, f.name))
137
138    @classmethod
139    def validate_model_manager(self, manager, model_name, manager_name):
140        """check if the manager is derived from PolymorphicManager
141        and its querysets from PolymorphicQuerySet - throw AssertionError if not"""
142
143        if not issubclass(type(manager), PolymorphicManager):
144            if django.VERSION < (2, 0):
145                extra = "\nConsider using Meta.manager_inheritance_from_future = True for Django 1.x projects"
146            else:
147                extra = ""
148            e = (
149                'PolymorphicModel: "{0}.{1}" manager is of type "{2}", but must be a subclass of'
150                " PolymorphicManager.{extra} to support retrieving subclasses".format(
151                    model_name, manager_name, type(manager).__name__, extra=extra
152                )
153            )
154            warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
155            return manager
156
157        if not getattr(manager, "queryset_class", None) or not issubclass(
158            manager.queryset_class, PolymorphicQuerySet
159        ):
160            e = (
161                'PolymorphicModel: "{0}.{1}" has been instantiated with a queryset class '
162                "which is not a subclass of PolymorphicQuerySet (which is required)".format(
163                    model_name, manager_name
164                )
165            )
166            warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
167        return manager
168
169    @property
170    def base_objects(self):
171        warnings.warn(
172            "Using PolymorphicModel.base_objects is deprecated.\n"
173            "Use {0}.objects.non_polymorphic() instead.".format(
174                self.__class__.__name__
175            ),
176            DeprecationWarning,
177            stacklevel=2,
178        )
179        return self._base_objects
180
181    @property
182    def _base_objects(self):
183        # Create a manager so the API works as expected. Just don't register it
184        # anymore in the Model Meta, so it doesn't substitute our polymorphic
185        # manager as default manager for the third level of inheritance when
186        # that third level doesn't define a manager at all.
187        manager = models.Manager()
188        manager.name = "base_objects"
189        manager.model = self
190        return manager
191
192    @property
193    def _default_manager(self):
194        if len(sys.argv) > 1 and sys.argv[1] == "dumpdata":
195            # TODO: investigate Django how this can be avoided
196            # hack: a small patch to Django would be a better solution.
197            # Django's management command 'dumpdata' relies on non-polymorphic
198            # behaviour of the _default_manager. Therefore, we catch any access to _default_manager
199            # here and return the non-polymorphic default manager instead if we are called from 'dumpdata.py'
200            # Otherwise, the base objects will be upcasted to polymorphic models, and be outputted as such.
201            # (non-polymorphic default manager is 'base_objects' for polymorphic models).
202            # This way we don't need to patch django.core.management.commands.dumpdata
203            # for all supported Django versions.
204            frm = inspect.stack()[
205                1
206            ]  # frm[1] is caller file name, frm[3] is caller function name
207            if DUMPDATA_COMMAND in frm[1]:
208                return self._base_objects
209
210        manager = super(PolymorphicModelBase, self)._default_manager
211        if not isinstance(manager, PolymorphicManager):
212            warnings.warn(
213                "{0}._default_manager is not a PolymorphicManager".format(
214                    self.__class__.__name__
215                ),
216                ManagerInheritanceWarning,
217            )
218
219        return manager
220