From ca4067e2797759d1a12676cb756ada2c32df5a8e Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Fri, 5 Apr 2013 13:14:36 +0200 Subject: [PATCH 01/21] Add proxy model test that fails in Django 1.5 --- polymorphic/tests.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/polymorphic/tests.py b/polymorphic/tests.py index 7d62d33..c6188e0 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -178,6 +178,17 @@ class UUIDPlainB(UUIDPlainA): class UUIDPlainC(UUIDPlainB): field3 = models.CharField(max_length=10) +# 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 +205,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') @@ -613,6 +623,37 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(type(MROBase2._default_manager)), "") + 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): ### fixed issue in PolymorphicModel.__getattribute__: field name same as model name o = ModelFieldNameTest.objects.create(modelfieldnametest='1') From 1f263026322d452322d220f665848e3d15676e5f Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sun, 7 Apr 2013 00:28:57 +0200 Subject: [PATCH 02/21] Fix Django 1.5 support, tests pass again. The reason polymorphic broke was because it couldn't find some managers anymore in the inheritance tree. Django 1.5 removes these and replaces them with an `AbstractManagerDescriptor`. This patch restores those objects --- polymorphic/base.py | 29 ++++++++++++++++++++++++++--- polymorphic/polymorphic_model.py | 2 ++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/polymorphic/base.py b/polymorphic/base.py index 334f542..a7db9b1 100644 --- a/polymorphic/base.py +++ b/polymorphic/base.py @@ -16,6 +16,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 @@ -91,6 +96,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 +108,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,7 +132,8 @@ 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)) @@ -121,16 +142,18 @@ class PolymorphicModelBase(ModelBase): @classmethod def get_first_user_defined_manager(self): + # 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)) + # 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/polymorphic_model.py b/polymorphic/polymorphic_model.py index 76cecf9..baf986c 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() From 78253bfe1238b5e46fed2597838427204666b5de Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sun, 7 Apr 2013 01:36:30 +0200 Subject: [PATCH 03/21] Fix passing custom querysets to related managers. * The custom manager was not assigned to _default_manager; get_first_user_defined_manager() always returned None * The PolymorphicManager couldn't remember it's custom queryset; a RelatedManager creates a new instance of a manager, so the queryset parameter should be known at class-level, not object level. * The old method of providing a custom queryset class has been deprecated. --- polymorphic/base.py | 17 +++++---- polymorphic/manager.py | 16 ++++++--- polymorphic/tests.py | 79 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 93 insertions(+), 19 deletions(-) diff --git a/polymorphic/base.py b/polymorphic/base.py index a7db9b1..c36b080 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 @@ -68,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) @@ -141,13 +143,16 @@ class PolymorphicModelBase(ModelBase): 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: 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/tests.py b/polymorphic/tests.py index c6188e0..ecf4c52 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() @@ -409,9 +444,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), " ") @@ -604,23 +641,49 @@ 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_model_inheritance(self): From 3566c5ed5ccce73284ac2ca41637ec97547bbbf4 Mon Sep 17 00:00:00 2001 From: Chris Glass Date: Sun, 7 Apr 2013 23:29:58 +0200 Subject: [PATCH 04/21] Made Diederik a core developer! Thanks a lot for your work! (Core developers have master commit rights) --- AUTHORS.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index b892224..7163304 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,8 +1,8 @@ Main authors (commit rights to the main repository) =================================================== -* Bert Constantin 2009/2010 (disappeared :( ) -* Chris Glass (Current maintainer) +* Chris Glass +* Diederik van der Boor Contributors @@ -12,6 +12,10 @@ Contributors * Adam Wentz * Ben Konrath * Charles Leifer (python 2.4 compatibility) -* Diederik van der Boor (polymorphic admin interface) * Germán M. Bravo * Martin Brochhaus + + +Former authors / maintainers +============================ +* Bert Constantin 2009/2010 (Original author, disappeared from the internet :( ) From e2cfbf38980ca3f351ea78e3edc8b5113a14e4bf Mon Sep 17 00:00:00 2001 From: Jedediah Smith Date: Wed, 14 Nov 2012 17:24:52 -0500 Subject: [PATCH 05/21] Support proxy models (still requires one query per proxied model, not optimal) --- polymorphic/__init__.py | 42 +++++++++++++++++--------------- polymorphic/polymorphic_model.py | 2 +- polymorphic/query.py | 2 +- polymorphic/query_translate.py | 2 +- polymorphic/tests.py | 34 ++++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 22 deletions(-) diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py index 7f4f0fd..83ced79 100644 --- a/polymorphic/__init__.py +++ b/polymorphic/__init__.py @@ -25,33 +25,37 @@ def get_version(): version += ' %s' % VERSION[3] return version - -# Proxied models need to have it's own ContentType - - from django.contrib.contenttypes.models import ContentTypeManager from django.utils.encoding import smart_unicode -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. - """ +# Monkey-patch Django to allow ContentTypes for proxy models. This is compatible with an +# upcoming change in Django 1.5 and should be removed when we upgrade. There is a test +# in MonkeyPatchTests that checks for this. +# https://code.djangoproject.com/ticket/18399 + +def get_for_model(self, model, for_concrete_model=True): + 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 - key = (opts.app_label, opts.object_name.lower()) + try: - ct = self.__class__._cache[self.db][key] + ct = self._get_from_cache(opts) 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)}, + 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 + +ContentTypeManager.get_for_model__original = ContentTypeManager.get_for_model +ContentTypeManager.get_for_model = get_for_model + diff --git a/polymorphic/polymorphic_model.py b/polymorphic/polymorphic_model.py index baf986c..cca724d 100644 --- a/polymorphic/polymorphic_model.py +++ b/polymorphic/polymorphic_model.py @@ -85,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 diff --git a/polymorphic/query.py b/polymorphic/query.py index ac097a5..860e8fd 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -135,7 +135,7 @@ 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_content_type_id = ContentType.objects.get_for_model(self.model, for_concrete_model=False).pk for base_object in base_result_objects: ordered_id_list.append(base_object.pk) 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 ecf4c52..dac0825 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -749,3 +749,37 @@ class RegressionTests(TestCase): expected_queryset = [bottom] self.assertQuerysetEqual(Bottom.objects.all(), [repr(r) for r in expected_queryset]) + +class MonkeyPatchTests(TestCase): + + def test_content_types_for_proxy_models_patch(self): + from django.db.models import Model + from django.contrib.contenttypes.models import ContentType + + class Base(Model): + pass + + class Proxy(Base): + class Meta: + proxy = True + + ct = ContentType.objects.get_for_model(Proxy, for_concrete_model=False) + self.assertEqual(Proxy, ct.model_class()) + + def test_content_types_for_proxy_models_patch_still_required(self): + """ + If this test fails then our monkey patch of ContentTypeManager.get_for_model + is no longer required and should be removed + """ + from django.db.models import Model + from django.contrib.contenttypes.models import ContentType + + class MyModel(Model): + pass + + self.assertRaisesMessage( + TypeError, + "get_for_model() got an unexpected keyword argument 'for_concrete_model'", + ContentType.objects.get_for_model__original, + MyModel, for_concrete_model=False + ) From 74389bb23dcf7459322ebc0341d494527fdaaae2 Mon Sep 17 00:00:00 2001 From: floppya Date: Wed, 20 Mar 2013 17:38:03 -0600 Subject: [PATCH 06/21] Update for proxy models and Django 1.5 Only monkeypatch when using Django < 1.5 Updated test --- polymorphic/__init__.py | 50 ++++++++++++++++++++--------------------- polymorphic/tests.py | 22 ++---------------- 2 files changed, 26 insertions(+), 46 deletions(-) diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py index 83ced79..35a33e1 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 @@ -25,37 +26,34 @@ def get_version(): version += ' %s' % VERSION[3] return version -from django.contrib.contenttypes.models import ContentTypeManager -from django.utils.encoding import smart_unicode - -# Monkey-patch Django to allow ContentTypes for proxy models. This is compatible with an -# upcoming change in Django 1.5 and should be removed when we upgrade. There is a test -# in MonkeyPatchTests that checks for this. -# https://code.djangoproject.com/ticket/18399 - -def get_for_model(self, model, for_concrete_model=True): +# 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 - if for_concrete_model: - model = model._meta.concrete_model - elif model._deferred: - model = model._meta.proxy_for_model + def get_for_model(self, model, for_concrete_model=True): + from django.utils.encoding import smart_unicode - opts = model._meta + if for_concrete_model: + model = model._meta.concrete_model + elif model._deferred: + model = model._meta.proxy_for_model - 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) + opts = model._meta - return ct + 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) -ContentTypeManager.get_for_model__original = ContentTypeManager.get_for_model -ContentTypeManager.get_for_model = get_for_model + return ct + + ContentTypeManager.get_for_model__original = ContentTypeManager.get_for_model + ContentTypeManager.get_for_model = get_for_model diff --git a/polymorphic/tests.py b/polymorphic/tests.py index dac0825..da727a2 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -750,9 +750,8 @@ class RegressionTests(TestCase): self.assertQuerysetEqual(Bottom.objects.all(), [repr(r) for r in expected_queryset]) -class MonkeyPatchTests(TestCase): - - def test_content_types_for_proxy_models_patch(self): +class ProxiedModelTests(TestCase): + def test_content_types_for_proxy_models(self): from django.db.models import Model from django.contrib.contenttypes.models import ContentType @@ -766,20 +765,3 @@ class MonkeyPatchTests(TestCase): ct = ContentType.objects.get_for_model(Proxy, for_concrete_model=False) self.assertEqual(Proxy, ct.model_class()) - def test_content_types_for_proxy_models_patch_still_required(self): - """ - If this test fails then our monkey patch of ContentTypeManager.get_for_model - is no longer required and should be removed - """ - from django.db.models import Model - from django.contrib.contenttypes.models import ContentType - - class MyModel(Model): - pass - - self.assertRaisesMessage( - TypeError, - "get_for_model() got an unexpected keyword argument 'for_concrete_model'", - ContentType.objects.get_for_model__original, - MyModel, for_concrete_model=False - ) From 58c4f6f69760a1cb1b12d4c9ec77bf0f832c1653 Mon Sep 17 00:00:00 2001 From: Jedediah Smith Date: Tue, 27 Nov 2012 17:23:39 -0500 Subject: [PATCH 07/21] Optimization - don't do extra queries for proxy models --- polymorphic/polymorphic_model.py | 6 ++++++ polymorphic/query.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/polymorphic/polymorphic_model.py b/polymorphic/polymorphic_model.py index cca724d..ad7cc04 100644 --- a/polymorphic/polymorphic_model.py +++ b/polymorphic/polymorphic_model.py @@ -103,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 860e8fd..721c77c 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -17,6 +17,15 @@ from django.db.models.query import CHUNK_SIZE # this is 100 for Dj Polymorphic_QuerySet_objects_per_request = CHUNK_SIZE +def transmogrify(cls, obj): + """ + Clone an object as a different class, by instantiating that class and copying the __dict__ + """ + new = cls() + for k,v in obj.__dict__.items(): + new.__dict__[k] = v + return new + ################################################################################### ### PolymorphicQuerySet @@ -136,6 +145,8 @@ class PolymorphicQuerySet(QuerySet): # - 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, for_concrete_model=False).pk + self_concrete_model_content_type_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) @@ -146,14 +157,21 @@ class PolymorphicQuerySet(QuerySet): base_result_objects_by_id[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): + # Real class is exactly the same as base class, go straight to results results[base_object.pk] = base_object - # 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) + modelclass = base_object.get_real_instance_class() + real_concrete_class_id = base_object.get_real_concrete_instance_class_id() + + if real_concrete_class_id == self_concrete_model_content_type_id: + # Real and base classes share the same concrete ancestor, + # upcast it and put it in the results + results[base_object.pk] = transmogrify(modelclass, base_object) + else: + modelclass = ContentType.objects.get_for_id(real_concrete_class_id).model_class() + idlist_per_model[modelclass].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). @@ -175,6 +193,11 @@ class PolymorphicQuerySet(QuerySet): for o in qs: o_pk = getattr(o, pk_name) + real_class = o.get_real_instance_class() + + # If the real class is a proxy, upcast it + if real_class != modelclass: + o = transmogrify(real_class, o) if self.query.aggregates: for anno_field_name in self.query.aggregates.keys(): From 19d5ed233807356a0eb774b0c74784804934da58 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 8 Apr 2013 00:28:36 +0200 Subject: [PATCH 08/21] Fix base_manager unit test As more methods are added to the PolymorphicModel, the attr dict changes ordering in the meta class. By making the ordering of managers consistent, this problem no longer occurs. --- polymorphic/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/polymorphic/base.py b/polymorphic/base.py index c36b080..4004407 100644 --- a/polymorphic/base.py +++ b/polymorphic/base.py @@ -140,6 +140,11 @@ class PolymorphicModelBase(ModelBase): 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 From ea9cb91e78b11c76895250ac613fd8cab97907de Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sun, 7 Apr 2013 23:58:40 +0200 Subject: [PATCH 09/21] Rename variables in polymorphic/query.py for clarity (split from previous commit by @jedediah) --- polymorphic/query.py | 46 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/polymorphic/query.py b/polymorphic/query.py index 721c77c..911c0ca 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -144,8 +144,8 @@ 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, for_concrete_model=False).pk - self_concrete_model_content_type_id = ContentType.objects.get_for_model(self.model, for_concrete_model=True).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) @@ -157,21 +157,21 @@ class PolymorphicQuerySet(QuerySet): base_result_objects_by_id[base_object.pk] = base_object - if (base_object.polymorphic_ctype_id == self_model_content_type_id): + 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 else: - modelclass = base_object.get_real_instance_class() + real_concrete_class = base_object.get_real_instance_class() real_concrete_class_id = base_object.get_real_concrete_instance_class_id() - if real_concrete_class_id == self_concrete_model_content_type_id: + 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(modelclass, base_object) + results[base_object.pk] = transmogrify(real_concrete_class, base_object) else: - modelclass = ContentType.objects.get_for_id(real_concrete_class_id).model_class() - idlist_per_model[modelclass].append(base_object.pk) + 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). @@ -187,29 +187,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.dup_select_related(self) # copy select related configuration to new qs - for o in qs: - o_pk = getattr(o, pk_name) - real_class = o.get_real_instance_class() + 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 != modelclass: - o = transmogrify(real_class, o) + 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] @@ -217,14 +217,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 From 1a6f3a45e5715f942f6e2e6bd213032f5da55f4f Mon Sep 17 00:00:00 2001 From: Jedediah Smith Date: Thu, 29 Nov 2012 15:38:40 -0500 Subject: [PATCH 10/21] Handle non-distinct results from base object query --- polymorphic/query.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/polymorphic/query.py b/polymorphic/query.py index 911c0ca..164402d 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -151,27 +151,24 @@ class PolymorphicQuerySet(QuerySet): 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 - 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 - - else: - real_concrete_class = base_object.get_real_instance_class() - real_concrete_class_id = base_object.get_real_concrete_instance_class_id() - - 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) + real_concrete_class = base_object.get_real_instance_class() + real_concrete_class_id = base_object.get_real_concrete_instance_class_id() + + 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). From 20ac209dbb89b1188c5d640617a54e6d818bf9e6 Mon Sep 17 00:00:00 2001 From: floppya Date: Fri, 22 Mar 2013 16:47:05 -0600 Subject: [PATCH 11/21] Fixes proxy models in the admin The PolymorphicParentModelAdmin was getting the concrete model ids for the add form. --- polymorphic/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polymorphic/admin.py b/polymorphic/admin.py index fee0164..5ce1d59 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -140,7 +140,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 From 54cf2f37d8bf65c64922da8367868503bf8f9465 Mon Sep 17 00:00:00 2001 From: floppya Date: Sat, 23 Mar 2013 02:47:23 -0600 Subject: [PATCH 12/21] Updated proxy model tests Extracted classes from original proxy test. Added another simple proxy model test. Moved the test case to a seemingly better spot. --- polymorphic/tests.py | 48 ++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/polymorphic/tests.py b/polymorphic/tests.py index da727a2..cf01c37 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -196,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) @@ -213,6 +212,13 @@ 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) @@ -686,6 +692,30 @@ class PolymorphicTests(TestCase): 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. @@ -749,19 +779,3 @@ class RegressionTests(TestCase): expected_queryset = [bottom] self.assertQuerysetEqual(Bottom.objects.all(), [repr(r) for r in expected_queryset]) - -class ProxiedModelTests(TestCase): - def test_content_types_for_proxy_models(self): - from django.db.models import Model - from django.contrib.contenttypes.models import ContentType - - class Base(Model): - pass - - class Proxy(Base): - class Meta: - proxy = True - - ct = ContentType.objects.get_for_model(Proxy, for_concrete_model=False) - self.assertEqual(Proxy, ct.model_class()) - From a0ab068449cde36c017990987909b0a52b9a4c96 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sun, 7 Apr 2013 22:16:41 +0200 Subject: [PATCH 13/21] Included Proxy models in example app. --- example/pexp/admin.py | 12 ++++++++++++ example/pexp/models.py | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/example/pexp/admin.py b/example/pexp/admin.py index 5eb48fc..43b095b 100644 --- a/example/pexp/admin.py +++ b/example/pexp/admin.py @@ -61,3 +61,15 @@ if 'UUIDModelA' in globals(): admin.site.register(UUIDModelA, UUIDModelAAdmin) + +class ProxyChildAdmin(PolymorphicChildModelAdmin): + base_model = ProxyBase + +class ProxyAdmin(PolymorphicParentModelAdmin): + base_model = ProxyBase + 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) From bb0a4daddc23a7eb351fe2a6a8242d48cc21ddda Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sun, 7 Apr 2013 22:26:31 +0200 Subject: [PATCH 14/21] Optimize transmogrify() function, assign __class__ instead. --- polymorphic/query.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/polymorphic/query.py b/polymorphic/query.py index 164402d..9f2cb58 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -19,13 +19,20 @@ Polymorphic_QuerySet_objects_per_request = CHUNK_SIZE def transmogrify(cls, obj): """ - Clone an object as a different class, by instantiating that class and copying the __dict__ + Upcast a class to a different type without asking questions. """ - new = cls() - for k,v in obj.__dict__.items(): - new.__dict__[k] = v + 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 From 04dce421e57b2d756593c0d31ede6149ccda2b1a Mon Sep 17 00:00:00 2001 From: Adam Wentz Date: Sat, 23 Mar 2013 22:30:40 -0600 Subject: [PATCH 15/21] Fixes PolymorphicQuerySet for django 1.6 QuerySet.dup_select_related has been removed. --- polymorphic/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polymorphic/query.py b/polymorphic/query.py index 9f2cb58..6594cdc 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -193,7 +193,7 @@ class PolymorphicQuerySet(QuerySet): # TODO: defer(), only(): support for these would be around here 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.dup_select_related(self) # copy select related configuration to new qs + real_objects.query.select_related = self.query.select_related # copy select related configuration to new qs for real_object in real_objects: o_pk = getattr(real_object, pk_name) From a214cd58f7910dd9d1791ad38721904c081e03bc Mon Sep 17 00:00:00 2001 From: floppya Date: Sat, 23 Mar 2013 14:06:04 -0600 Subject: [PATCH 16/21] Adds admin list filter for polymorphic child models. --- polymorphic/admin.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/polymorphic/admin.py b/polymorphic/admin.py index 5ce1d59..1f2eaab 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -17,7 +17,9 @@ 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 +46,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): """ From 4106ad72967b4a2964c82d3c844af5bbaf448fc4 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sun, 7 Apr 2013 23:03:28 +0200 Subject: [PATCH 17/21] Apply list filter in example app --- example/pexp/admin.py | 7 ++++++- polymorphic/admin.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/example/pexp/admin.py b/example/pexp/admin.py index 43b095b..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), @@ -67,6 +71,7 @@ class ProxyChildAdmin(PolymorphicChildModelAdmin): class ProxyAdmin(PolymorphicParentModelAdmin): base_model = ProxyBase + list_filter = (PolymorphicChildModelFilter,) child_models = ( (ProxyA, ProxyChildAdmin), (ProxyB, ProxyChildAdmin), diff --git a/polymorphic/admin.py b/polymorphic/admin.py index 1f2eaab..442bf70 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -19,7 +19,8 @@ from django.utils.translation import ugettext_lazy as _ __all__ = ( 'PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin', - 'PolymorphicChildModelAdmin', 'PolymorphicChildModelFilter') + 'PolymorphicChildModelAdmin', 'PolymorphicChildModelFilter' +) class RegistrationClosed(RuntimeError): From ec1fd53974812cf947c2d40be322433aee3e5718 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 8 Apr 2013 11:29:55 +0200 Subject: [PATCH 18/21] Added Adam en Jedediah to AUTHORS for their contributions! --- AUTHORS.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 7163304..59f9376 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -8,11 +8,13 @@ Main authors (commit rights to the main repository) Contributors ============= +* Adam Wentz * Andrew Ingram (contributed setup.py) * Adam Wentz * Ben Konrath * Charles Leifer (python 2.4 compatibility) * Germán M. Bravo +* Jedediah Smith (proxy models support) * Martin Brochhaus From 7889293227d65b27fa67f95efaea2de6048edb3b Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 8 Apr 2013 11:30:50 +0200 Subject: [PATCH 19/21] Applied an overal cleanup of the .gitignore file --- .gitignore | 49 +++++++++++++------------------------------------ 1 file changed, 13 insertions(+), 36 deletions(-) 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/ From 2e109101fc79bb41a544b811de6f1020853696a8 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 8 Apr 2013 11:34:45 +0200 Subject: [PATCH 20/21] Fix and update Travis configuration --- .travis.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 92c9958..073d61f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,21 @@ 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 +notifications: + irc: + channels: + - "irc.freenode.org#django-polymorphic" + template: + - "%{repository}#%{build_number} (%{commit}) %{message} -- %{build_url}" From 762265e28a1202790f069420561c1f34542d80ad Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 8 Apr 2013 12:28:33 +0200 Subject: [PATCH 21/21] Travis: avoid bombarding IRC with everyone's fork --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 073d61f..1248012 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,9 +13,3 @@ script: branches: only: - master -notifications: - irc: - channels: - - "irc.freenode.org#django-polymorphic" - template: - - "%{repository}#%{build_number} (%{commit}) %{message} -- %{build_url}"