diff --git a/.gitignore b/.gitignore index df997af..2bd9593 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,16 @@ -.hg -*.svn *.pyc -*~ -.project -.pydevproject -.settings -nbproject - -tmp -libraries-local - -pushgit -pushhg -pushreg -pbackup -mcmd.py -dbconfig_local.py -diffmanagement -scrapbook.py - -pip-log.txt -build -ppreadme.py -ppdocs.py -common.css -screen.css - +*.pyo +*.mo +*.db +*.egg-info/ +*.egg/ .coverage -bin/ -distribute-0.6.10.tar.gz -htmlcov/ -include/ -lib/ - -MANIFEST -dist/ -*.egg-info +.project +.idea/ +.pydevproject +.idea/workspace.xml .tox/ +.DS_Store +dist/ +docs/_build/ +htmlcov/ diff --git a/.travis.yml b/.travis.yml index 92c9958..1248012 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,15 @@ language: python - python: - "2.6" - "2.7" - env: - DJANGO=django==1.4.5 - DJANGO=django==1.5 - install: - "pip install $DJANGO --use-mirrors" script: - - python ./manage.py test - -matrix: - allow_failures: - - env: DJANGO=django==1.5 + - python runtests.py +branches: + only: + - master diff --git a/AUTHORS.rst b/AUTHORS.rst index b892224..59f9376 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,17 +1,23 @@ Main authors (commit rights to the main repository) =================================================== -* Bert Constantin 2009/2010 (disappeared :( ) -* Chris Glass (Current maintainer) +* Chris Glass +* Diederik van der Boor Contributors ============= +* Adam Wentz * Andrew Ingram (contributed setup.py) * Adam Wentz * Ben Konrath * Charles Leifer (python 2.4 compatibility) -* Diederik van der Boor (polymorphic admin interface) * Germán M. Bravo +* Jedediah Smith (proxy models support) * Martin Brochhaus + + +Former authors / maintainers +============================ +* Bert Constantin 2009/2010 (Original author, disappeared from the internet :( ) diff --git a/example/pexp/admin.py b/example/pexp/admin.py index 5eb48fc..60cd28a 100644 --- a/example/pexp/admin.py +++ b/example/pexp/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin +from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter from pexp.models import * @@ -8,6 +8,7 @@ class ProjectChildAdmin(PolymorphicChildModelAdmin): class ProjectAdmin(PolymorphicParentModelAdmin): base_model = Project + list_filter = (PolymorphicChildModelFilter,) child_models = ( (Project, ProjectChildAdmin), (ArtProject, ProjectChildAdmin), @@ -23,6 +24,7 @@ class ModelAChildAdmin(PolymorphicChildModelAdmin): class ModelAAdmin(PolymorphicParentModelAdmin): base_model = ModelA + list_filter = (PolymorphicChildModelFilter,) child_models = ( (ModelA, ModelAChildAdmin), (ModelB, ModelAChildAdmin), @@ -38,6 +40,7 @@ if 'Model2A' in globals(): class Model2AAdmin(PolymorphicParentModelAdmin): base_model = Model2A + list_filter = (PolymorphicChildModelFilter,) child_models = ( (Model2A, Model2AChildAdmin), (Model2B, Model2AChildAdmin), @@ -53,6 +56,7 @@ if 'UUIDModelA' in globals(): class UUIDModelAAdmin(PolymorphicParentModelAdmin): base_model = UUIDModelA + list_filter = (PolymorphicChildModelFilter,) child_models = ( (UUIDModelA, UUIDModelAChildAdmin), (UUIDModelB, UUIDModelAChildAdmin), @@ -61,3 +65,16 @@ if 'UUIDModelA' in globals(): admin.site.register(UUIDModelA, UUIDModelAAdmin) + +class ProxyChildAdmin(PolymorphicChildModelAdmin): + base_model = ProxyBase + +class ProxyAdmin(PolymorphicParentModelAdmin): + base_model = ProxyBase + list_filter = (PolymorphicChildModelFilter,) + child_models = ( + (ProxyA, ProxyChildAdmin), + (ProxyB, ProxyChildAdmin), + ) + +admin.site.register(ProxyBase, ProxyAdmin) diff --git a/example/pexp/models.py b/example/pexp/models.py index a21e53e..b0ed8c5 100644 --- a/example/pexp/models.py +++ b/example/pexp/models.py @@ -47,3 +47,26 @@ if 'UUIDField' in globals(): field2 = models.CharField(max_length=10) class UUIDModelC(UUIDModelB): field3 = models.CharField(max_length=10) + +class ProxyBase(PolymorphicModel): + title = models.CharField(max_length=200) + + def __unicode__(self): + return u"".format(self.polymorphic_ctype, self.title) + + class Meta: + ordering = ('title',) + +class ProxyA(ProxyBase): + class Meta: + proxy = True + + def __unicode__(self): + return u"".format(self.title) + +class ProxyB(ProxyBase): + class Meta: + proxy = True + + def __unicode__(self): + return u"".format(self.title) diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py index 14bde64..446bd2b 100644 --- a/polymorphic/__init__.py +++ b/polymorphic/__init__.py @@ -6,6 +6,7 @@ Copyright: This code and affiliated files are (C) by Bert Constantin and individual contributors. Please see LICENSE and AUTHORS for more information. """ +import django from polymorphic_model import PolymorphicModel from manager import PolymorphicManager from query import PolymorphicQuerySet @@ -29,32 +30,33 @@ Release logic: __version__ = "0.4.1.dev0" -# Proxied models need to have it's own ContentType +# Monkey-patch Django < 1.5 to allow ContentTypes for proxy models. +if django.VERSION[:2] < (1, 5): + from django.contrib.contenttypes.models import ContentTypeManager + from django.utils.encoding import smart_unicode + def get_for_model(self, model, for_concrete_model=True): + from django.utils.encoding import smart_unicode -from django.contrib.contenttypes.models import ContentTypeManager -from django.utils.encoding import smart_unicode + if for_concrete_model: + model = model._meta.concrete_model + elif model._deferred: + model = model._meta.proxy_for_model + opts = model._meta + + try: + ct = self._get_from_cache(opts) + except KeyError: + ct, created = self.get_or_create( + app_label = opts.app_label, + model = opts.object_name.lower(), + defaults = {'name': smart_unicode(opts.verbose_name_raw)}, + ) + self._add_to_cache(self.db, ct) + + return ct + + ContentTypeManager.get_for_model__original = ContentTypeManager.get_for_model + ContentTypeManager.get_for_model = get_for_model -def get_for_proxied_model(self, model): - """ - Returns the ContentType object for a given model, creating the - ContentType if necessary. Lookups are cached so that subsequent lookups - for the same model don't hit the database. - """ - opts = model._meta - key = (opts.app_label, opts.object_name.lower()) - try: - ct = self.__class__._cache[self.db][key] - except KeyError: - # Load or create the ContentType entry. The smart_unicode() is - # needed around opts.verbose_name_raw because name_raw might be a - # django.utils.functional.__proxy__ object. - ct, created = self.get_or_create( - app_label=opts.app_label, - model=opts.object_name.lower(), - defaults={'name': smart_unicode(opts.verbose_name_raw)}, - ) - self._add_to_cache(self.db, ct) - return ct -ContentTypeManager.get_for_proxied_model = get_for_proxied_model diff --git a/polymorphic/admin.py b/polymorphic/admin.py index fee0164..442bf70 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -17,7 +17,10 @@ from django.utils.encoding import force_unicode from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ -__all__ = ('PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin', 'PolymorphicChildModelAdmin') +__all__ = ( + 'PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin', + 'PolymorphicChildModelAdmin', 'PolymorphicChildModelFilter' +) class RegistrationClosed(RuntimeError): @@ -44,6 +47,31 @@ class PolymorphicModelChoiceForm(forms.Form): self.fields['ct_id'].label = self.type_label +class PolymorphicChildModelFilter(admin.SimpleListFilter): + """ + An admin list filter for the PolymorphicParentModelAdmin which enables + filtering by its child models. + """ + title = _('Content type') + parameter_name = 'polymorphic_ctype' + + def lookups(self, request, model_admin): + return model_admin.get_child_type_choices() + + def queryset(self, request, queryset): + try: + value = int(self.value()) + except TypeError: + value = None + if value: + # ensure the content type is allowed + for choice_value, _ in self.lookup_choices: + if choice_value == value: + return queryset.filter(polymorphic_ctype_id=choice_value) + raise PermissionDenied( + 'Invalid ContentType "{0}". It must be registered as child model.'.format(value)) + return queryset + class PolymorphicParentModelAdmin(admin.ModelAdmin): """ @@ -140,7 +168,7 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): """ choices = [] for model, _ in self.get_child_models(): - ct = ContentType.objects.get_for_model(model) + ct = ContentType.objects.get_for_model(model, for_concrete_model=False) choices.append((ct.id, model._meta.verbose_name)) return choices diff --git a/polymorphic/base.py b/polymorphic/base.py index 334f542..4004407 100644 --- a/polymorphic/base.py +++ b/polymorphic/base.py @@ -8,6 +8,7 @@ import inspect from django.db import models from django.db.models.base import ModelBase +from django.db.models.manager import ManagerDescriptor from manager import PolymorphicManager from query import PolymorphicQuerySet @@ -16,6 +17,11 @@ from query import PolymorphicQuerySet # These are forbidden as field names (a descriptive exception is raised) POLYMORPHIC_SPECIAL_Q_KWORDS = ['instance_of', 'not_instance_of'] +try: + from django.db.models.manager import AbstractManagerDescriptor # Django 1.5 +except ImportError: + AbstractManagerDescriptor = None + ################################################################################### ### PolymorphicModel meta class @@ -63,7 +69,8 @@ class PolymorphicModelBase(ModelBase): new_class.add_to_class(mgr_name, new_manager) # get first user defined manager; if there is one, make it the _default_manager - user_manager = new_class.get_first_user_defined_manager() + # this value is used by the related objects, restoring access to custom queryset methods on related objects. + user_manager = self.get_first_user_defined_manager(new_class) if user_manager: def_mgr = user_manager._copy_to_model(new_class) #print '## add default manager', type(def_mgr) @@ -91,6 +98,7 @@ class PolymorphicModelBase(ModelBase): use correct mro, only use managers with _inherited==False (they are of no use), skip managers that are overwritten by the user with same-named class attributes (in attrs) """ + #print "** ", self.__name__ add_managers = [] add_managers_keys = set() for base in self.__mro__[1:]: @@ -102,9 +110,23 @@ class PolymorphicModelBase(ModelBase): for key, manager in base.__dict__.items(): if type(manager) == models.manager.ManagerDescriptor: manager = manager.manager + + if AbstractManagerDescriptor is not None: + # Django 1.4 unconditionally assigned managers to a model. As of Django 1.5 however, + # the abstract models don't get any managers, only a AbstractManagerDescriptor as substitute. + # Pretend that the manager is still there, so all code works like it used to. + if type(manager) == AbstractManagerDescriptor and base.__name__ == 'PolymorphicModel': + model = manager.model + if key == 'objects': + manager = PolymorphicManager() + manager.model = model + elif key == 'base_objects': + manager = models.Manager() + manager.model = model + if not isinstance(manager, models.Manager): continue - if key in ['_base_manager']: + if key == '_base_manager': continue # let Django handle _base_manager if key in attrs: continue @@ -112,25 +134,36 @@ class PolymorphicModelBase(ModelBase): continue # manager with that name already added, skip if manager._inherited: continue # inherited managers (on the bases) have no significance, they are just copies - #print >>sys.stderr,'##',self.__name__, key + #print '## {0} {1}'.format(self.__name__, key) + if isinstance(manager, PolymorphicManager): # validate any inherited polymorphic managers self.validate_model_manager(manager, self.__name__, key) add_managers.append((base.__name__, key, manager)) add_managers_keys.add(key) + + # The ordering in the base.__dict__ may randomly change depending on which method is added. + # Make sure base_objects is on top, and 'objects' and '_default_manager' follow afterwards. + # This makes sure that the _base_manager is also assigned properly. + add_managers = sorted(add_managers, key=lambda item: item[2].creation_counter, reverse=True) return add_managers @classmethod - def get_first_user_defined_manager(self): + def get_first_user_defined_manager(mcs, new_class): + # See if there is a manager attribute directly stored at this inheritance level. mgr_list = [] - for key, val in self.__dict__.items(): - item = getattr(self, key) - if not isinstance(item, models.Manager): continue - mgr_list.append((item.creation_counter, key, item)) + for key, val in new_class.__dict__.items(): + if isinstance(val, ManagerDescriptor): + val = val.manager + if not isinstance(val, PolymorphicManager) or type(val) is PolymorphicManager: + continue + + mgr_list.append((val.creation_counter, key, val)) + # if there are user defined managers, use first one as _default_manager if mgr_list: _, manager_name, manager = sorted(mgr_list)[0] #sys.stderr.write( '\n# first user defined manager for model "{model}":\n# "{mgrname}": {mgr}\n# manager model: {mgrmodel}\n\n' - # .format( model=model_name, mgrname=manager_name, mgr=manager, mgrmodel=manager.model ) ) + # .format( model=self.__name__, mgrname=manager_name, mgr=manager, mgrmodel=manager.model ) ) return manager return None diff --git a/polymorphic/manager.py b/polymorphic/manager.py index cdef305..83db77b 100644 --- a/polymorphic/manager.py +++ b/polymorphic/manager.py @@ -2,7 +2,7 @@ """ PolymorphicManager Please see README.rst or DOCS.rst or http://chrisglass.github.com/django_polymorphic/ """ - +import warnings from django.db import models from polymorphic.query import PolymorphicQuerySet @@ -14,17 +14,23 @@ class PolymorphicManager(models.Manager): Usually not explicitly needed, except if a custom manager or a custom queryset class is to be used. """ + # Tell Django that related fields also need to use this manager: use_for_related_fields = True + queryset_class = PolymorphicQuerySet def __init__(self, queryset_class=None, *args, **kwrags): - if not queryset_class: - self.queryset_class = PolymorphicQuerySet - else: + # Up till polymorphic 0.4, the queryset class could be specified as parameter to __init__. + # However, this doesn't work for related managers which instantiate a new version of this class. + # Hence, for custom managers the new default is using the 'queryset_class' attribute at class level instead. + if queryset_class: + warnings.warn("Using PolymorphicManager(queryset_class=..) is deprecated; override the queryset_class attribute instead", DeprecationWarning) + # For backwards compatibility, still allow the parameter: self.queryset_class = queryset_class + super(PolymorphicManager, self).__init__(*args, **kwrags) def get_query_set(self): - return self.queryset_class(self.model) + return self.queryset_class(self.model, using=self._db) # Proxy all unknown method calls to the queryset, so that its members are # directly accessible as PolymorphicModel.objects.* diff --git a/polymorphic/polymorphic_model.py b/polymorphic/polymorphic_model.py index 76cecf9..ad7cc04 100644 --- a/polymorphic/polymorphic_model.py +++ b/polymorphic/polymorphic_model.py @@ -67,6 +67,8 @@ class PolymorphicModel(models.Model): # some applications want to know the name of the fields that are added to its models polymorphic_internal_model_fields = ['polymorphic_ctype'] + # Note that Django 1.5 removes these managers because the model is abstract. + # They are pretended to be there by the metaclass in PolymorphicModelBase.get_inherited_managers() objects = PolymorphicManager() base_objects = models.Manager() @@ -83,7 +85,7 @@ class PolymorphicModel(models.Model): (used by PolymorphicQuerySet._get_real_instances) """ if not self.polymorphic_ctype_id: - self.polymorphic_ctype = ContentType.objects.get_for_model(self) + self.polymorphic_ctype = ContentType.objects.get_for_model(self, for_concrete_model=False) def save(self, *args, **kwargs): """Overridden model save function which supports the polymorphism @@ -101,6 +103,12 @@ class PolymorphicModel(models.Model): # so we use the following version, which uses the CopntentType manager cache return ContentType.objects.get_for_id(self.polymorphic_ctype_id).model_class() + def get_real_concrete_instance_class_id(self): + return ContentType.objects.get_for_model(self.get_real_instance_class(), for_concrete_model=True).pk + + def get_real_concrete_instance_class(self): + return ContentType.objects.get_for_model(self.get_real_instance_class(), for_concrete_model=True).model_class() + def get_real_instance(self): """Normally not needed. If a non-polymorphic manager (like base_objects) has been used to diff --git a/polymorphic/query.py b/polymorphic/query.py index ac097a5..6594cdc 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -17,6 +17,22 @@ from django.db.models.query import CHUNK_SIZE # this is 100 for Dj Polymorphic_QuerySet_objects_per_request = CHUNK_SIZE +def transmogrify(cls, obj): + """ + Upcast a class to a different type without asking questions. + """ + if not '__init__' in obj.__dict__: + # Just assign __class__ to a different value. + new = obj + new.__class__ = cls + else: + # Run constructor, reassign values + new = cls() + for k,v in obj.__dict__.items(): + new.__dict__[k] = v + return new + + ################################################################################### ### PolymorphicQuerySet @@ -135,25 +151,31 @@ class PolymorphicQuerySet(QuerySet): # - also record the correct result order in "ordered_id_list" # - store objects that already have the correct class into "results" base_result_objects_by_id = {} - self_model_content_type_id = ContentType.objects.get_for_model(self.model).pk + self_model_class_id = ContentType.objects.get_for_model(self.model, for_concrete_model=False).pk + self_concrete_model_class_id = ContentType.objects.get_for_model(self.model, for_concrete_model=True).pk + for base_object in base_result_objects: ordered_id_list.append(base_object.pk) # check if id of the result object occeres more than once - this can happen e.g. with base_objects.extra(tables=...) - assert not base_object.pk in base_result_objects_by_id, ( - "django_polymorphic: result objects do not have unique primary keys - model " + unicode(self.model) - ) + if not base_object.pk in base_result_objects_by_id: + base_result_objects_by_id[base_object.pk] = base_object - base_result_objects_by_id[base_object.pk] = base_object + if base_object.polymorphic_ctype_id == self_model_class_id: + # Real class is exactly the same as base class, go straight to results + results[base_object.pk] = base_object - # this object is not a derived object and already the real instance => store it right away - if (base_object.polymorphic_ctype_id == self_model_content_type_id): - results[base_object.pk] = base_object + else: + real_concrete_class = base_object.get_real_instance_class() + real_concrete_class_id = base_object.get_real_concrete_instance_class_id() - # this object is derived and its real instance needs to be retrieved - # => store it's id into the bin for this model type - else: - idlist_per_model[base_object.get_real_instance_class()].append(base_object.pk) + if real_concrete_class_id == self_concrete_model_class_id: + # Real and base classes share the same concrete ancestor, + # upcast it and put it in the results + results[base_object.pk] = transmogrify(real_concrete_class, base_object) + else: + real_concrete_class = ContentType.objects.get_for_id(real_concrete_class_id).model_class() + idlist_per_model[real_concrete_class].append(base_object.pk) # django's automatic ".pk" field does not always work correctly for # custom fields in derived objects (unclear yet who to put the blame on). @@ -169,24 +191,29 @@ class PolymorphicQuerySet(QuerySet): # Then we copy the annotate fields from the base objects to the real objects. # Then we copy the extra() select fields from the base objects to the real objects. # TODO: defer(), only(): support for these would be around here - for modelclass, idlist in idlist_per_model.items(): - qs = modelclass.base_objects.filter(pk__in=idlist) # use pk__in instead #### - qs.dup_select_related(self) # copy select related configuration to new qs + for real_concrete_class, idlist in idlist_per_model.items(): + real_objects = real_concrete_class.base_objects.filter(pk__in=idlist) # use pk__in instead #### + real_objects.query.select_related = self.query.select_related # copy select related configuration to new qs - for o in qs: - o_pk = getattr(o, pk_name) + for real_object in real_objects: + o_pk = getattr(real_object, pk_name) + real_class = real_object.get_real_instance_class() + + # If the real class is a proxy, upcast it + if real_class != real_concrete_class: + real_object = transmogrify(real_class, real_object) if self.query.aggregates: for anno_field_name in self.query.aggregates.keys(): attr = getattr(base_result_objects_by_id[o_pk], anno_field_name) - setattr(o, anno_field_name, attr) + setattr(real_object, anno_field_name, attr) if self.query.extra_select: for select_field_name in self.query.extra_select.keys(): attr = getattr(base_result_objects_by_id[o_pk], select_field_name) - setattr(o, select_field_name, attr) + setattr(real_object, select_field_name, attr) - results[o_pk] = o + results[o_pk] = real_object # re-create correct order and return result list resultlist = [results[ordered_id] for ordered_id in ordered_id_list if ordered_id in results] @@ -194,14 +221,14 @@ class PolymorphicQuerySet(QuerySet): # set polymorphic_annotate_names in all objects (currently just used for debugging/printing) if self.query.aggregates: annotate_names = self.query.aggregates.keys() # get annotate field list - for o in resultlist: - o.polymorphic_annotate_names = annotate_names + for real_object in resultlist: + real_object.polymorphic_annotate_names = annotate_names # set polymorphic_extra_select_names in all objects (currently just used for debugging/printing) if self.query.extra_select: extra_select_names = self.query.extra_select.keys() # get extra select field list - for o in resultlist: - o.polymorphic_extra_select_names = extra_select_names + for real_object in resultlist: + real_object.polymorphic_extra_select_names = extra_select_names return resultlist diff --git a/polymorphic/query_translate.py b/polymorphic/query_translate.py index 2d1b730..e42e349 100644 --- a/polymorphic/query_translate.py +++ b/polymorphic/query_translate.py @@ -220,7 +220,7 @@ def _create_model_filter_Q(modellist, not_instance_of=False): assert False, 'PolymorphicModel: instance_of expects a list of (polymorphic) models or a single (polymorphic) model' def q_class_with_subclasses(model): - q = Q(polymorphic_ctype=ContentType.objects.get_for_model(model)) + q = Q(polymorphic_ctype=ContentType.objects.get_for_model(model, for_concrete_model=False)) for subclass in model.__subclasses__(): q = q | q_class_with_subclasses(subclass) return q diff --git a/polymorphic/tests.py b/polymorphic/tests.py index 7d62d33..cf01c37 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -4,13 +4,14 @@ """ import uuid import re +from django.db.models.query import QuerySet from django.test import TestCase from django.db.models import Q,Count from django.db import models from django.contrib.contenttypes.models import ContentType -from polymorphic import PolymorphicModel, PolymorphicManager +from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet from polymorphic import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent from polymorphic.tools_for_tests import UUIDField @@ -81,7 +82,7 @@ class DiamondXY(DiamondX, DiamondY): class RelationBase(ShowFieldTypeAndContent, PolymorphicModel): field_base = models.CharField(max_length=10) - fk = models.ForeignKey('self', null=True) + fk = models.ForeignKey('self', null=True, related_name='relationbase_set') m2m = models.ManyToManyField('self') class RelationA(RelationBase): field_a = models.CharField(max_length=10) @@ -100,9 +101,16 @@ class One2OneRelatingModel(PolymorphicModel): class One2OneRelatingModelDerived(One2OneRelatingModel): field2 = models.CharField(max_length=10) +class MyManagerQuerySet(PolymorphicQuerySet): + def my_queryset_foo(self): + return self.all() # Just a method to prove the existance of the custom queryset. + class MyManager(PolymorphicManager): + queryset_class = MyManagerQuerySet + def get_query_set(self): return super(MyManager, self).get_query_set().order_by('-field1') + class ModelWithMyManager(ShowFieldTypeAndContent, Model2A): objects = MyManager() field4 = models.CharField(max_length=10) @@ -117,6 +125,33 @@ class MROBase3(models.Model): class MRODerived(MROBase2, MROBase3): pass +class ParentModelWithManager(PolymorphicModel): + pass +class ChildModelWithManager(PolymorphicModel): + # Also test whether foreign keys receive the manager: + fk = models.ForeignKey(ParentModelWithManager, related_name='childmodel_set') + objects = MyManager() + + +class PlainMyManagerQuerySet(QuerySet): + def my_queryset_foo(self): + return self.all() # Just a method to prove the existance of the custom queryset. + +class PlainMyManager(models.Manager): + def my_queryset_foo(self): + return self.get_query_set().my_queryset_foo() + + def get_query_set(self): + return PlainMyManagerQuerySet(self.model, using=self._db) + +class PlainParentModelWithManager(models.Model): + pass + +class PlainChildModelWithManager(models.Model): + fk = models.ForeignKey(PlainParentModelWithManager, related_name='childmodel_set') + objects = PlainMyManager() + + class MgrInheritA(models.Model): mgrA = models.Manager() mgrA2 = models.Manager() @@ -161,7 +196,6 @@ class Middle(Top): class Bottom(Middle): author = models.CharField(max_length=50) - class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel): uuid_primary_key = UUIDField(primary_key = True) topic = models.CharField(max_length = 30) @@ -178,6 +212,24 @@ class UUIDPlainB(UUIDPlainA): class UUIDPlainC(UUIDPlainB): field3 = models.CharField(max_length=10) +# base -> proxy +class ProxyBase(PolymorphicModel): + some_data = models.CharField(max_length=128) +class ProxyChild(ProxyBase): + class Meta: + proxy = True + +# base -> proxy -> real models +class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel): + name = models.CharField(max_length=10) +class ProxyModelBase(ProxiedBase): + class Meta: + proxy = True +class ProxyModelA(ProxyModelBase): + field1 = models.CharField(max_length=10) +class ProxyModelB(ProxyModelBase): + field2 = models.CharField(max_length=10) + # test bad field name #class TestBadFieldModel(ShowFieldType, PolymorphicModel): @@ -194,7 +246,6 @@ class PolymorphicTests(TestCase): """ The test suite """ - def test_diamond_inheritance(self): # Django diamond problem o1 = DiamondXY.objects.create(field_b='b', field_x='x', field_y='y') @@ -399,9 +450,11 @@ class PolymorphicTests(TestCase): self.assertEqual(show_base_manager(PlainA), " ") self.assertEqual(show_base_manager(PlainB), " ") self.assertEqual(show_base_manager(PlainC), " ") + self.assertEqual(show_base_manager(Model2A), " ") self.assertEqual(show_base_manager(Model2B), " ") self.assertEqual(show_base_manager(Model2C), " ") + self.assertEqual(show_base_manager(One2OneRelatingModel), " ") self.assertEqual(show_base_manager(One2OneRelatingModelDerived), " ") @@ -594,23 +647,104 @@ class PolymorphicTests(TestCase): ModelWithMyManager.objects.create(field1='D1a', field4='D4a') ModelWithMyManager.objects.create(field1='D1b', field4='D4b') - objects = ModelWithMyManager.objects.all() + objects = ModelWithMyManager.objects.all() # MyManager should reverse the sorting of field1 self.assertEqual(repr(objects[0]), '') self.assertEqual(repr(objects[1]), '') self.assertEqual(len(objects), 2) - self.assertEqual(repr(type(ModelWithMyManager.objects)), "") - self.assertEqual(repr(type(ModelWithMyManager._default_manager)), "") + self.assertIs(type(ModelWithMyManager.objects), MyManager) + self.assertIs(type(ModelWithMyManager._default_manager), MyManager) + self.assertIs(type(ModelWithMyManager.base_objects), models.Manager) def test_manager_inheritance(self): - self.assertEqual(repr(type(MRODerived.objects)), "") # MRO + # by choice of MRO, should be MyManager from MROBase1. + self.assertIs(type(MRODerived.objects), MyManager) # check for correct default manager - self.assertEqual(repr(type(MROBase1._default_manager)), "") + self.assertIs(type(MROBase1._default_manager), MyManager) # Django vanilla inheritance does not inherit MyManager as _default_manager here - self.assertEqual(repr(type(MROBase2._default_manager)), "") + self.assertIs(type(MROBase2._default_manager), MyManager) + + + def test_queryset_assignment(self): + # This is just a consistency check for now, testing standard Django behavior. + parent = PlainParentModelWithManager.objects.create() + child = PlainChildModelWithManager.objects.create(fk=parent) + self.assertIs(type(PlainParentModelWithManager._default_manager), models.Manager) + self.assertIs(type(PlainChildModelWithManager._default_manager), PlainMyManager) + self.assertIs(type(PlainChildModelWithManager.objects), PlainMyManager) + self.assertIs(type(PlainChildModelWithManager.objects.all()), PlainMyManagerQuerySet) + + # A related set is created using the model's _default_manager, so does gain extra methods. + self.assertIs(type(parent.childmodel_set.my_queryset_foo()), PlainMyManagerQuerySet) + + # For polymorphic models, the same should happen. + parent = ParentModelWithManager.objects.create() + child = ChildModelWithManager.objects.create(fk=parent) + self.assertIs(type(ParentModelWithManager._default_manager), PolymorphicManager) + self.assertIs(type(ChildModelWithManager._default_manager), MyManager) + self.assertIs(type(ChildModelWithManager.objects), MyManager) + self.assertIs(type(ChildModelWithManager.objects.my_queryset_foo()), MyManagerQuerySet) + + # A related set is created using the model's _default_manager, so does gain extra methods. + self.assertIs(type(parent.childmodel_set.my_queryset_foo()), MyManagerQuerySet) + + + def test_proxy_models(self): + # prepare some data + for data in ('bleep bloop', 'I am a', 'computer'): + ProxyChild.objects.create(some_data=data) + + # this caches ContentType queries so they don't interfere with our query counts later + list(ProxyBase.objects.all()) + + # one query per concrete class + with self.assertNumQueries(1): + items = list(ProxyBase.objects.all()) + + self.assertIsInstance(items[0], ProxyChild) + + + def test_content_types_for_proxy_models(self): + """Checks if ContentType is capable of returning proxy models.""" + from django.db.models import Model + from django.contrib.contenttypes.models import ContentType + + ct = ContentType.objects.get_for_model(ProxyChild, for_concrete_model=False) + self.assertEqual(ProxyChild, ct.model_class()) + + + def test_proxy_model_inheritance(self): + """ + Polymorphic abilities should also work when the base model is a proxy object. + """ + # The managers should point to the proper objects. + # otherwise, the whole excersise is pointless. + self.assertEqual(ProxiedBase.objects.model, ProxiedBase) + self.assertEqual(ProxyModelBase.objects.model, ProxyModelBase) + self.assertEqual(ProxyModelA.objects.model, ProxyModelA) + self.assertEqual(ProxyModelB.objects.model, ProxyModelB) + + # Create objects + ProxyModelA.objects.create(name="object1") + ProxyModelB.objects.create(name="object2", field2="bb") + + # Getting single objects + object1 = ProxyModelBase.objects.get(name='object1') + object2 = ProxyModelBase.objects.get(name='object2') + self.assertEqual(repr(object1), '') + self.assertEqual(repr(object2), '') + self.assertIsInstance(object1, ProxyModelA) + self.assertIsInstance(object2, ProxyModelB) + + # Same for lists + objects = list(ProxyModelBase.objects.all().order_by('name')) + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + self.assertIsInstance(objects[0], ProxyModelA) + self.assertIsInstance(objects[1], ProxyModelB) def test_fix_getattribute(self):