Six Pearls Designs

Django class_prepared signal

2013 February 20

Sometimes it is necessary to dynamically change the attributes of a Django model without modifying the codebase for that model. For example, my mediacracy app integrates and extends several 3rd party apps.

In development, I followed Jupo Systems's recommendation of attaching a listener to the class_prepared signal that injects fields using the field's contribute_to_class method as follows:

from django.db.models.signals import class_prepared
def add_image_fields(sender, **kwargs):
    """
    class_prepared signal handler that checks for the model massmedia.Image
    and adds sized image fields
    """
    if sender.__name__ == "Image" and sender._meta.app_label == 'massmedia':
        large = models.ImageField(upload_to=".", blank=True, verbose_name=_('large image file'))
        medium = models.ImageField(upload_to=".", blank=True, verbose_name=_('medium image file'))
        small = models.ImageField(upload_to=".", blank=True, verbose_name=_('small image file'))

        large.contribute_to_class(sender, "large")
        medium.contribute_to_class(sender, "medium")
        small.contribute_to_class(sender, "small")

class_prepared.connect(add_image_fields)

Since we're using the class_prepared signal, we need to make sure our listener gets connected before the inject-ee model gets loaded. In principal, we do that by keeping the injecting app above the inject-ee app in the INSTALLED_APPS settings. Unfortunately, the app order only ensures load order when executed from the shell (using management commands).[1,2]

Luckily! We basically only care if these fields get contributed when the model loads during shell commands (like syncdb). Since most of the time we don't care if the fields get added at any particular time, we can immediately import the models and add our fields if they aren't there.

from massmedia.models import Image
try:
    Image._meta.get_field_by_name('large')[0]
except:
    add_image_fields(Image)

Now if we are guaranteed that our new fields will be on the model. If Django loads app models in the specified order, our injecting app will catch the class_prepared signal and add our fields (and in fact, will force django to load the model immediately). If Django loads app models out of order (which it often does), the injecting app will add the fields when it is loaded. This seems to work well enough. Incorporating a South migration is a nice touch to ensure a way of getting the database synced with the new model fields.

Footnotes:
  1. Petrounias.org
  2. Django-users lister