Skip to content

Commit

Permalink
Fixed #18399 – Added a way to get ContentTypes for proxy models
Browse files Browse the repository at this point in the history
Added kwargs for_concrete_model and for_concrete_models to ContentType
methods get_for_model() and get_for_models(). By setting the flag to
False, it is possible to get the contenttype for proxy models.
  • Loading branch information
charettes authored and akaariai committed Jun 8, 2012
1 parent 9098504 commit b6d533a
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 8 deletions.
17 changes: 11 additions & 6 deletions django/contrib/contenttypes/models.py
Expand Up @@ -16,20 +16,24 @@ def get_by_natural_key(self, app_label, model):
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:
Expand All @@ -45,18 +49,19 @@ def get_for_model(self, model):

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
needed_app_labels = set()
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:
Expand Down
88 changes: 88 additions & 0 deletions django/contrib/contenttypes/tests.py
Expand Up @@ -11,6 +11,13 @@
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
Expand Down Expand Up @@ -114,6 +121,87 @@ def test_get_for_models_full_cache(self):
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"
Expand Down
17 changes: 15 additions & 2 deletions docs/ref/contrib/contenttypes.txt
Expand Up @@ -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
Expand Down Expand Up @@ -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
=================

Expand Down
10 changes: 10 additions & 0 deletions docs/releases/1.5.txt
Expand Up @@ -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() <django.contrib.contenttypes.models.ContentTypeManager.get_for_model()>`
and :meth:`ContentTypeManager.get_for_models() <django.contrib.contenttypes.models.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 <django.contrib.contenttypes.models.ContentType>`
associated with proxy models.

Minor features
~~~~~~~~~~~~~~

Expand Down

0 comments on commit b6d533a

Please sign in to comment.