/polymorphic/base.py
Python | 191 lines | 137 code | 20 blank | 34 comment | 31 complexity | e56e45b7781fc903769da4f951d90349 MD5 | raw file
1# -*- coding: utf-8 -*- 2""" PolymorphicModel Meta Class 3 Please see README.rst or DOCS.rst or http://bserve.webhop.org/wiki/django_polymorphic 4""" 5 6import sys 7import inspect 8 9from django.db import models 10from django.db.models.base import ModelBase 11 12from manager import PolymorphicManager 13from query import PolymorphicQuerySet 14 15# PolymorphicQuerySet Q objects (and filter()) support these additional key words. 16# These are forbidden as field names (a descriptive exception is raised) 17POLYMORPHIC_SPECIAL_Q_KWORDS = [ 'instance_of', 'not_instance_of'] 18 19 20################################################################################### 21### PolymorphicModel meta class 22 23class PolymorphicModelBase(ModelBase): 24 """ 25 Manager inheritance is a pretty complex topic which may need 26 more thought regarding how this should be handled for polymorphic 27 models. 28 29 In any case, we probably should propagate 'objects' and 'base_objects' 30 from PolymorphicModel to every subclass. We also want to somehow 31 inherit/propagate _default_manager as well, as it needs to be polymorphic. 32 33 The current implementation below is an experiment to solve this 34 problem with a very simplistic approach: We unconditionally 35 inherit/propagate any and all managers (using _copy_to_model), 36 as long as they are defined on polymorphic models 37 (the others are left alone). 38 39 Like Django ModelBase, we special-case _default_manager: 40 if there are any user-defined managers, it is set to the first of these. 41 42 We also require that _default_manager as well as any user defined 43 polymorphic managers produce querysets that are derived from 44 PolymorphicQuerySet. 45 """ 46 47 def __new__(self, model_name, bases, attrs): 48 #print; print '###', model_name, '- bases:', bases 49 50 # create new model 51 new_class = self.call_superclass_new_method(model_name, bases, attrs) 52 53 # check if the model fields are all allowed 54 self.validate_model_fields(new_class) 55 56 # create list of all managers to be inherited from the base classes 57 inherited_managers = new_class.get_inherited_managers(attrs) 58 59 # add the managers to the new model 60 for source_name, mgr_name, manager in inherited_managers: 61 #print '** add inherited manager from model %s, manager %s, %s' % (source_name, mgr_name, manager.__class__.__name__) 62 new_manager = manager._copy_to_model(new_class) 63 new_class.add_to_class(mgr_name, new_manager) 64 65 # get first user defined manager; if there is one, make it the _default_manager 66 user_manager = self.get_first_user_defined_manager(model_name, attrs) 67 if user_manager: 68 def_mgr = user_manager._copy_to_model(new_class) 69 #print '## add default manager', type(def_mgr) 70 new_class.add_to_class('_default_manager', def_mgr) 71 new_class._default_manager._inherited = False # the default mgr was defined by the user, not inherited 72 73 # validate resulting default manager 74 self.validate_model_manager(new_class._default_manager, model_name, '_default_manager') 75 76 # for __init__ function of this class (monkeypatching inheritance accessors) 77 new_class.polymorphic_super_sub_accessors_replaced = False 78 79 # determine the name of the primary key field and store it into the class variable 80 # polymorphic_primary_key_name (it is needed by query.py) 81 for f in new_class._meta.fields: 82 if f.primary_key and type(f)!=models.OneToOneField: 83 new_class.polymorphic_primary_key_name=f.name 84 break 85 86 return new_class 87 88 def get_inherited_managers(self, attrs): 89 """ 90 Return list of all managers to be inherited/propagated from the base classes; 91 use correct mro, only use managers with _inherited==False (they are of no use), 92 skip managers that are overwritten by the user with same-named class attributes (in attrs) 93 """ 94 add_managers = []; add_managers_keys = set() 95 for base in self.__mro__[1:]: 96 if not issubclass(base, models.Model): continue 97 if not getattr(base, 'polymorphic_model_marker', None): continue # leave managers of non-polym. models alone 98 99 for key, manager in base.__dict__.items(): 100 if type(manager) == models.manager.ManagerDescriptor: manager = manager.manager 101 if not isinstance(manager, models.Manager): continue 102 if key in ['_base_manager']: continue # let Django handle _base_manager 103 if key in attrs: continue 104 if key in add_managers_keys: continue # manager with that name already added, skip 105 if manager._inherited: continue # inherited managers (on the bases) have no significance, they are just copies 106 #print >>sys.stderr,'##',self.__name__, key 107 if isinstance(manager, PolymorphicManager): # validate any inherited polymorphic managers 108 self.validate_model_manager(manager, self.__name__, key) 109 add_managers.append((base.__name__, key, manager)) 110 add_managers_keys.add(key) 111 return add_managers 112 113 @classmethod 114 def get_first_user_defined_manager(self, model_name, attrs): 115 mgr_list = [] 116 for key, val in attrs.items(): 117 if not isinstance(val, models.Manager): continue 118 mgr_list.append((val.creation_counter, key, val)) 119 # if there are user defined managers, use first one as _default_manager 120 if mgr_list: # 121 _, manager_name, manager = sorted(mgr_list)[0] 122 #sys.stderr.write( '\n# first user defined manager for model "{model}":\n# "{mgrname}": {mgr}\n# manager model: {mgrmodel}\n\n' 123 # .format( model=model_name, mgrname=manager_name, mgr=manager, mgrmodel=manager.model ) ) 124 return manager 125 return None 126 127 @classmethod 128 def call_superclass_new_method(self, model_name, bases, attrs): 129 """call __new__ method of super class and return the newly created class. 130 Also work around a limitation in Django's ModelBase.""" 131 # There seems to be a general limitation in Django's app_label handling 132 # regarding abstract models (in ModelBase). See issue 1 on github - TODO: propose patch for Django 133 # We run into this problem if polymorphic.py is located in a top-level directory 134 # which is directly in the python path. To work around this we temporarily set 135 # app_label here for PolymorphicModel. 136 meta = attrs.get('Meta', None) 137 model_module_name = attrs['__module__'] 138 do_app_label_workaround = (meta 139 and model_module_name == 'polymorphic' 140 and model_name == 'PolymorphicModel' 141 and getattr(meta, 'app_label', None) is None ) 142 143 if do_app_label_workaround: meta.app_label = 'poly_dummy_app_label' 144 new_class = super(PolymorphicModelBase, self).__new__(self, model_name, bases, attrs) 145 if do_app_label_workaround: del(meta.app_label) 146 return new_class 147 148 def validate_model_fields(self): 149 "check if all fields names are allowed (i.e. not in POLYMORPHIC_SPECIAL_Q_KWORDS)" 150 for f in self._meta.fields: 151 if f.name in POLYMORPHIC_SPECIAL_Q_KWORDS: 152 e = 'PolymorphicModel: "%s" - field name "%s" is not allowed in polymorphic models' 153 raise AssertionError(e % (self.__name__, f.name) ) 154 155 @classmethod 156 def validate_model_manager(self, manager, model_name, manager_name): 157 """check if the manager is derived from PolymorphicManager 158 and its querysets from PolymorphicQuerySet - throw AssertionError if not""" 159 160 if not issubclass(type(manager), PolymorphicManager): 161 e = 'PolymorphicModel: "' + model_name + '.' + manager_name + '" manager is of type "' + type(manager).__name__ 162 e += '", but must be a subclass of PolymorphicManager' 163 raise AssertionError(e) 164 if not getattr(manager, 'queryset_class', None) or not issubclass(manager.queryset_class, PolymorphicQuerySet): 165 e = 'PolymorphicModel: "' + model_name + '.' + manager_name + '" (PolymorphicManager) has been instantiated with a queryset class which is' 166 e += ' not a subclass of PolymorphicQuerySet (which is required)' 167 raise AssertionError(e) 168 return manager 169 170 171 # hack: a small patch to Django would be a better solution. 172 # Django's management command 'dumpdata' relies on non-polymorphic 173 # behaviour of the _default_manager. Therefore, we catch any access to _default_manager 174 # here and return the non-polymorphic default manager instead if we are called from 'dumpdata.py' 175 # (non-polymorphic default manager is 'base_objects' for polymorphic models). 176 # This way we don't need to patch django.core.management.commands.dumpdata 177 # for all supported Django versions. 178 # TODO: investigate Django how this can be avoided 179 _dumpdata_command_running = False 180 if len(sys.argv)>1: _dumpdata_command_running = ( sys.argv[1] == 'dumpdata' ) 181 def __getattribute__(self, name): 182 if name=='_default_manager': 183 if self._dumpdata_command_running: 184 frm = inspect.stack()[1] # frm[1] is caller file name, frm[3] is caller function name 185 if 'django/core/management/commands/dumpdata.py' in frm[1]: 186 return self.base_objects 187 #caller_mod_name = inspect.getmodule(frm[0]).__name__ # does not work with python 2.4 188 #if caller_mod_name == 'django.core.management.commands.dumpdata': 189 190 return super(PolymorphicModelBase, self).__getattribute__(name) 191