From e2cfbf38980ca3f351ea78e3edc8b5113a14e4bf Mon Sep 17 00:00:00 2001 From: Jedediah Smith Date: Wed, 14 Nov 2012 17:24:52 -0500 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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 09/10] 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 10/10] 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