/polymorphic/base.py

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