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