For a day or so now I’ve been battling with Django’s model inheritance, which has a particularly frustrating approach to dynamic polymorphism. The problem is that when one is implementing a group of related models using multi-table inheritance and you attempt to iterate over the bog standard QuerySet for the parent class, we see an unfortunate phenomenon: attributes and methods specific to a subclass will not be available as the list we are returned is a list of instances of the parent class, not the various children.
For example, I have a basic CMS which models a Page as having a series of Nuggets. A Nugget could be a simple TextNugget, a TwitterNugget, a BlogNugget etc. Each nugget has a title but beyond that each subclass must implement a method content() which returns the HTML content (including templatetags, in case you’re interested) which will be rendered by the browser in the appropriate place. For ease of use I wish for my users to be able to select from available Nuggets without worrying about their type.
class Nugget(models.Model):
title_text = models.CharField(_('Title'), max_length=200)
show_title = models.BooleanField(_('Show'))
text = models.TextField(_('Text'), blank=True)
def __unicode__(self):
return self.title
def title(self):
return self.title_text
def content(self):
return self.text
# extends Nugget
class TwitterNugget(Nugget):
username = models.CharField(_('username'), max_length=30)
def __unicode__(self):
return self.title
def content(self):
return "{% insert_tweets user" + self.username + "%}"
def title(self):
return self.title_text
In order to access, for example, the content method of a Nugget
and be sure that we’re going to get the twitter templatetag
, rather than the text which the inherited method would provide, we need to be sure that the iterator provided by the QuerySet
is going to give us instances of the child (TwitterNugget
) class, rather than instances of Nugget
. In order to achieve this, we should override the Manager
for the Nugget
class to return a new kind of QuerySet
– one with overriden __getitem__
and __iter__
methods which cast our models to the correct child classes. In turn this requires us to implement this new type of QuerySet
(the PolymorphicQuerySet
)
Determining which type of object should be returned in the iterator is done using a content_type
field in the model – we have to override the save
method on the Nugget
to make sure that the content_type is written when we persist the model. content_type
is a ForeignKey
to the ContentType
model provided in the django ContentTypes
package.
from django.contrib.contenttypes.models import ContentType
from django.db.models.query import QuerySet
class PolymorphicQuerySet(QuerySet):
def __getitem__(self, k):
result = super(PolymorphicQuerySet, self).__getitem__(k)
if isinstance(result, models.Model) :
return result.as_child_class()
else :
return result
def __iter__(self):
for item in super(PolymorphicQuerySet, self).__iter__():
yield item.as_child_class()
class NuggetManager(models.Manager):
def get_query_set(self):
return PolymorphicQuerySet(self.model)
class Nugget(models.Model):
title_text = models.CharField(_('Title'), max_length=200)
show_title = models.BooleanField(_('Show'))
text = models.TextField(_('Text'), blank=True)
content_type =
models.ForeignKey(ContentType,editable=False,null=True)
objects = NuggetManager()
def save(self, *args, **kwargs):
if(not self.content_type):
self.content_type =
ContentType.objects.get_for_model(self.__class__)
super(Nugget, self).save(*args, **kwargs)
def as_child_class(self):
content_type = self.content_type
model = content_type.model_class()
if (model == Nugget):
return self
return model.objects.get(id=self.id)
def __unicode__(self):
return self.title
def title(self):
return self.title_text
def content(self):
return self.text
# extends Nugget
class TwitterNugget(Nugget):
username = models.CharField(_('username'), max_length=30)
objects = NuggetManager()
def __unicode__(self):
return self.title
def content(self):
return "{% insert_tweets user" + self.username + "%}"
def title(self):
return self.title_text