diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index d588ff4d23..867351f6c8 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -16,20 +16,24 @@ class ContentTypeManager(models.Manager): self._add_to_cache(self.db, ct) return ct - def _get_opts(self, model): - return model._meta.concrete_model._meta + def _get_opts(self, model, for_concrete_model): + if for_concrete_model: + model = model._meta.concrete_model + elif model._deferred: + model = model._meta.proxy_for_model + return model._meta def _get_from_cache(self, opts): key = (opts.app_label, opts.object_name.lower()) return self.__class__._cache[self.db][key] - def get_for_model(self, model): + def get_for_model(self, model, for_concrete_model=True): """ 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 = self._get_opts(model) + opts = self._get_opts(model, for_concrete_model) try: ct = self._get_from_cache(opts) except KeyError: @@ -45,10 +49,11 @@ class ContentTypeManager(models.Manager): return ct - def get_for_models(self, *models): + def get_for_models(self, *models, **kwargs): """ Given *models, returns a dictionary mapping {model: content_type}. """ + for_concrete_models = kwargs.pop('for_concrete_models', True) # Final results results = {} # models that aren't already in the cache @@ -56,7 +61,7 @@ class ContentTypeManager(models.Manager): needed_models = set() needed_opts = set() for model in models: - opts = self._get_opts(model) + opts = self._get_opts(model, for_concrete_models) try: ct = self._get_from_cache(opts) except KeyError: diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py index 2eaa4c182e..0efc5222c0 100644 --- a/django/contrib/contenttypes/tests.py +++ b/django/contrib/contenttypes/tests.py @@ -11,6 +11,13 @@ from django.test import TestCase from django.utils.encoding import smart_str +class ConcreteModel(models.Model): + name = models.CharField(max_length=10) + +class ProxyModel(ConcreteModel): + class Meta: + proxy = True + class FooWithoutUrl(models.Model): """ Fake model not defining ``get_absolute_url`` for @@ -114,6 +121,87 @@ class ContentTypesTests(TestCase): FooWithUrl: ContentType.objects.get_for_model(FooWithUrl), }) + def test_get_for_concrete_model(self): + """ + Make sure the `for_concrete_model` kwarg correctly works + with concrete, proxy and deferred models + """ + concrete_model_ct = ContentType.objects.get_for_model(ConcreteModel) + + self.assertEqual(concrete_model_ct, + ContentType.objects.get_for_model(ProxyModel)) + + self.assertEqual(concrete_model_ct, + ContentType.objects.get_for_model(ConcreteModel, + for_concrete_model=False)) + + proxy_model_ct = ContentType.objects.get_for_model(ProxyModel, + for_concrete_model=False) + + self.assertNotEqual(concrete_model_ct, proxy_model_ct) + + # Make sure deferred model are correctly handled + ConcreteModel.objects.create(name="Concrete") + DeferredConcreteModel = ConcreteModel.objects.only('pk').get().__class__ + DeferredProxyModel = ProxyModel.objects.only('pk').get().__class__ + + self.assertEqual(concrete_model_ct, + ContentType.objects.get_for_model(DeferredConcreteModel)) + + self.assertEqual(concrete_model_ct, + ContentType.objects.get_for_model(DeferredConcreteModel, + for_concrete_model=False)) + + self.assertEqual(concrete_model_ct, + ContentType.objects.get_for_model(DeferredProxyModel)) + + self.assertEqual(proxy_model_ct, + ContentType.objects.get_for_model(DeferredProxyModel, + for_concrete_model=False)) + + def test_get_for_concrete_models(self): + """ + Make sure the `for_concrete_models` kwarg correctly works + with concrete, proxy and deferred models. + """ + concrete_model_ct = ContentType.objects.get_for_model(ConcreteModel) + + cts = ContentType.objects.get_for_models(ConcreteModel, ProxyModel) + self.assertEqual(cts, { + ConcreteModel: concrete_model_ct, + ProxyModel: concrete_model_ct, + }) + + proxy_model_ct = ContentType.objects.get_for_model(ProxyModel, + for_concrete_model=False) + cts = ContentType.objects.get_for_models(ConcreteModel, ProxyModel, + for_concrete_models=False) + self.assertEqual(cts, { + ConcreteModel: concrete_model_ct, + ProxyModel: proxy_model_ct, + }) + + # Make sure deferred model are correctly handled + ConcreteModel.objects.create(name="Concrete") + DeferredConcreteModel = ConcreteModel.objects.only('pk').get().__class__ + DeferredProxyModel = ProxyModel.objects.only('pk').get().__class__ + + cts = ContentType.objects.get_for_models(DeferredConcreteModel, + DeferredProxyModel) + self.assertEqual(cts, { + DeferredConcreteModel: concrete_model_ct, + DeferredProxyModel: concrete_model_ct, + }) + + cts = ContentType.objects.get_for_models(DeferredConcreteModel, + DeferredProxyModel, + for_concrete_models=False) + self.assertEqual(cts, { + DeferredConcreteModel: concrete_model_ct, + DeferredProxyModel: proxy_model_ct, + }) + + def test_shortcut_view(self): """ Check that the shortcut view (used for the admin "view on site" diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 1181e4b44f..1e9e21a4b7 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -187,13 +187,13 @@ The ``ContentTypeManager`` probably won't ever need to call this method yourself; Django will call it automatically when it's needed. - .. method:: get_for_model(model) + .. method:: get_for_model(model[, for_concrete_model=True]) Takes either a model class or an instance of a model, and returns the :class:`~django.contrib.contenttypes.models.ContentType` instance representing that model. - .. method:: get_for_models(*models) + .. method:: get_for_models(*models[, for_concrete_models=True]) Takes a variadic number of model classes, and returns a dictionary mapping the model classes to the @@ -224,6 +224,19 @@ lookup:: .. _generic-relations: +.. versionadded:: 1.5 + +Prior to Django 1.5 :meth:`~ContentTypeManager.get_for_model()` and +:meth:`~ContentTypeManager.get_for_models()` always returned the +:class:`~django.contrib.contenttypes.models.ContentType` associated with the +concrete model of the specified one(s). That means there was no way to retreive +the :class:`~django.contrib.contenttypes.models.ContentType` of a proxy model +using those methods. As of Django 1.5 you can now pass a boolean flag – +respectively ``for_concrete_model`` and ``for_concrete_models`` – to specify +wether or not you want to retreive the +:class:`~django.contrib.contenttypes.models.ContentType` for the concrete or +direct model. + Generic relations ================= diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 33f5003281..575bd8d0f8 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -69,6 +69,16 @@ To make it easier to deal with javascript templates which collide with Django's syntax, you can now use the :ttag:`verbatim` block tag to avoid parsing the tag's content. +Retreival of ``ContentType`` instances associated with proxy models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The methods :meth:`ContentTypeManager.get_for_model() ` +and :meth:`ContentTypeManager.get_for_models() ` +have a new keyword argument – respectively ``for_concrete_model`` and ``for_concrete_models``. +By passing ``False`` using this argument it is now possible to retreive the +:class:`ContentType ` +associated with proxy models. + Minor features ~~~~~~~~~~~~~~