Battling with Django Inheritance: Subclass Casting

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

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.