views:

151

answers:

1

Django's post_save signal behaves weirdly with models using multi-table inheritance

I am noticing an odd behavior in the way Django's post_save signal works when using a model that has multi-table inheritance.

I have these two models:

class Animal(models.Model):
    category = models.CharField(max_length=20)

class Dog(Animal):
    color = models.CharField(max_length=10)

I have a post save callback called echo_category:

def echo_category(sender, **kwargs):
    print "category: '%s'" % kwargs['instance'].category
post_save.connect(echo_category, sender=Dog)

I have this fixture:

[
    {
        "pk": 1,
        "model": "animal.animal",
        "fields": {
            "category": "omnivore"
        }
    },

    {
        "pk": 1,
        "model": "animal.dog",
        "fields": {
            "color": "brown"
        }
    }
]

In every part of the program except for in the post_save callback the following is true:

from animal.models import Dog
Dog.objects.get(pk=1).category == u'omnivore' # True

When I run syncdb and the fixture is installed, the echo_category function is run. The output from syncdb is:

$ python manage.py syncdb --noinput
Installing json fixture 'initial_data' from '~/my_proj/animal/fixtures'.
category: ''
Installed 2 object(s) from 1 fixture(s)

The weird thing here is that the dog object's category attribute is an empty string. Why is it not 'omnivore' like it is everywhere else?

As a temporary (hopefully) workaround I reload the object from the database in the post_save callback:

def echo_category(sender, **kwargs):
    instance = kwargs['instance']
    instance = sender.objects.get(pk=instance.pk)
    print "category: '%s'" % instance.category
post_save.connect(echo_category, sender=Dog)

This works but it is not something I like because I must remember to do it when the model inherits from another model and it must hit the database again. The other weird thing is that I must do instance.pk to get the primary key. The normal 'id' attribute does not work (I cannot use instance.id). I do not know why this is. Maybe this is related to the reason why the category attribute is not doing the right thing?

+2  A: 

This is because data loaded from fixture with loaddata/syncdb command is saved as raw in the database: only the fields of the resulting model table get saved, to avoid hitting the database for all the models in the class hierarchy.

But, when a model is saved as raw, you get an extra raw keyword argument in your signal, so you can handle the case properly. Your signal would end up like that:

def echo_category(sender, **kwargs):
    if kwargs.get('raw', False):
        instance = sender.objects.get(pk=kwargs['instance'].pk)
    else:
        instance = kwargs['instance']
    print "category: '%s'" % instance.category

post_save.connect(echo_category, sender=Dog)

This way, you will only get the extra database query when dealing with fixtures (and I guess that's acceptable in your case).

About your other question:

The other weird thing is that I must do instance.pk to get the primary key. The normal 'id' attribute does not work (I cannot use instance.id). I do not know why this is.

id and pk have a slightly different semantic. In your example, the id of a Dog object is the AutoField defined (automagically) in your Animal class. The pk however, is an OneToOneField (once again, automagically defined) in the Dog class.

In practice, both field have the same value all the time. However, as id is a field coming from Animal, it won't exists for Dog object saved as raw.

Hope that helps.

EDIT: this issue has already been reported on django's trac here.

Clément