PageRenderTime 96ms CodeModel.GetById 85ms app.highlight 8ms RepoModel.GetById 1ms app.codeStats 0ms

/polymorphic/base.py

https://bitbucket.org/bconstantin/django_polymorphic/
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