views:

754

answers:

3

How do I have actions occur when a field gets changed in one of my models? In this particular case, I have this model:

class Game(models.Model):
    STATE_CHOICES = (
        ('S', 'Setup'),
        ('A', 'Active'),
        ('P', 'Paused'),
        ('F', 'Finished')
        )
    name = models.CharField(max_length=100)
    owner = models.ForeignKey(User)
    created = models.DateTimeField(auto_now_add=True)
    started = models.DateTimeField(null=True)
    state = models.CharField(max_length=1, choices=STATE_CHOICES, default='S')

and I would like to have Units created, and the 'started' field populated with the current datetime (among other things), when the state goes from Setup to Active.

I suspect that a model instance method is needed, but the docs don't seem to have much to say about using them in this manner.

Update: I've added the following to my Game class:

    def __init__(self, *args, **kwargs):
        super(Game, self).__init__(*args, **kwargs)
        self.old_state = self.state

    def save(self, force_insert=False, force_update=False):
        if self.old_state == 'S' and self.state == 'A':
            self.started = datetime.datetime.now()
        super(Game, self).save(force_insert, force_update)
        self.old_state = self.state
+1  A: 

Django has a nifty feature called signals, which are effectively triggers that are set off at specific times:

  • Before/after a model's save method is called
  • Before/after a model's delete method is called
  • Before/after an HTTP request is made

Read the docs for full info, but all you need to do is create a receiver function and register it as a signal. This is usually done in models.py.

from django.core.signals import request_finished

def my_callback(sender, **kwargs):
    print "Request finished!"

request_finished.connect(my_callback)

Simple, eh?

cpharmston
+1  A: 

Basically, you need to override the save method, check if the state field was changed, set started if needed and then let the model base class finish persisting to the database.

The tricky part is figuring out if the field was changed. Check out the mixins and other solutions in this question to help you out with this:

ars
I'm not sure how I feel about overriding methods in Django's ORM. IMO, this would be better accomplished using django.db.models.signals.post_save.
cpharmston
@cpharmston: I think this is acceptable: http://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods -- I use signals when an entity other than the model wants to be notified, but in this case the entity is the model itself, so just override save (this is fairly conventional with object oriented methods).
ars
Overriding save() did not work on bulk admin operations the last time I tried (1.0 I think)
Luper Rouch
Using signals seems to me to be more elegant, but in this case it does appear that overriding save() is the expected thing.
Jeff Bradberry
+1  A: 

One way is to add a setter for the state. It's just a normal method, nothing special.

class Game(models.Model):
   # ... other code

    def set_state(self, newstate):
        if self.state != newstate:
            oldstate = self.state
            self.state = newstate
            if oldstate == 'S' and newstate == 'A':
                self.started = datetime.now()
                # create units, etc.

Update: If you want this to be triggered whenever a change is made to a model instance, you can (instead of set_state above) use a __setattr__ method in Game which is something like this:

def __setattr__(self, name, value):
    if name != "state":
        object.__setattr__(self, name, value)
    else:
        if self.state != value:
            oldstate = self.state
            object.__setattr__(self, name, value) # use base class setter
            if oldstate == 'S' and value == 'A':
                self.started = datetime.now()
                # create units, etc.

Note that you wouldn't especially find this in the Django docs, as it (__setattr__) is a standard Python feature, documented here, and is not Django-specific.

Vinay Sajip
Yes, but it wasn't clear to me how to make such a method be used whenever editing, say, from the admin page.
Jeff Bradberry