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 + )