A django application that provides a simple way to retrieve models type casted to their original ContentType
.
>>> pip install polymodels
Make sure 'django.contrib.contenttypes'
and 'polymodels'
are in your INSTALLED_APPS
INSTALLED_APPS += ('django.contrib.contenttypes', 'polymodels')
Subclass PolymorphicModel
, an abstract model class.
from django.db import models
from polymodels.models import PolymorphicModel
class Animal(PolymorphicModel):
name = models.CharField(max_length=255)
def __unicode__(self):
return self.name
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Reptile(Animal):
pass
class Snake(Reptile):
class Meta:
proxy = True
Objects are created the same way as usual and their associated ContentType
is saved automatically:
>>> animal = Animal.objects.create(name='animal') >>> mammal = Mammal.objects.create(name='mammal') >>> reptile = Reptile.objects.create(name='reptile') >>> snake = Snake.objects.create(name='snake')
To retreive type casted instances from the Animal.objects
manager you just have to use the select_subclasses
method.
>>> Animal.objects.select_subclasses() [<Animal: animal>, <Mammal: mammal>, <Reptile: reptile>, <Snake: snake>]
You can also retreive a subset of the subclasses by passing them as arguments to select_subclass
.
>>> Animal.objects.select_subclasses(Reptile) [<Reptile: reptile>, <Snake: snake>]
Or directly from subclasses managers.
>>> Reptile.objects.select_subclasses(Snake) [<Snake: snake>]
Note that you can also retrieve original results by avoiding the select_subclasses
call.
>>> Animal.objects.all() [<Animal: animal>, <Animal: mammal>, <Animal: reptile>, <Animal: snake>]
It's also possible to select only instances of the model to which the manager is attached by using the exclude_subclasses
method.
>>> Mammal.objects.all() [<Mammal: mammal>]
Each instance of PolymorphicModel
has a type_cast
method that knows how to convert itself to the correct ContentType
.
>>> animal_snake = Animal.objects.get(pk=snake.pk) <Animal: snake> >>> animal_snake.type_cast() <Snake: snake> >>> animal_snake.type_cast(Reptile) <Reptile: snake>
If the PolymorphicModel.content_type
fields conflicts with one of your existing fields you just have to subclass polymodels.models.BasePolymorphicModel
and specify which field polymodels should use instead by defining a CONTENT_TYPE_FIELD
attribute on your model. This field must be a ForeignKey
to ContentType
.
from django.contrib.contenttypes.models import ContentType
from django.db import models
from polymodels.models import BasePolymorphicModel
class MyModel(BasePolymorphicModel):
CONTENT_TYPE_FIELD = 'polymorphic_ct'
polymorphic_ct = models.ForeignKey(ContentType)
Under the hood select_subclasses
calls seleted_related
to avoid unnecessary queries and filter
if you pass some classes to it. On queryset iteration, the fetched instanced are converted to their correct type by calling BasePolymorphicModel.type_cast
. Note that those lookups are cached on class creation to avoid computing them on every single query.
Prior to Django 1.6 it was not possible to issue a select_related
over multiple one-to-one relationships. For example, given the models defined above, Animal.objects.select_related('mammal__dog')
would throw a strange TypeError
. To work around this issue, select_subclasses
limits such lookups to one level deep if you're using a version of django with this issue.
I'm aware there's already plenty of existing projects tackling the whole model-inheritance-type-casting-thing such as django-polymorphic. However I wanted to implement this feature in a lightweight way: no __metaclass__
or __init__
overrides while using django's public API as much as possible. In the end, this was really just an extraction of django-mutant's own mecanism of handling this since I needed it as a standalone app for another project.
If you happen to encounter a bug or would like to suggest a feature addition please file an issue or create a pull request containing tests.
- Inspired by a post of Jeff Elmores.