Actions triggered by field change in Django
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
Solution 1:
It has been answered, but here's an example of using signals, post_init and post_save.
from django.db.models.signals import post_save, post_init
class MyModel(models.Model):
state = models.IntegerField()
previous_state = None
@staticmethod
def post_save(sender, instance, created, **kwargs):
if instance.previous_state != instance.state or created:
do_something_with_state_change()
@staticmethod
def remember_state(sender, instance, **kwargs):
instance.previous_state = instance.state
post_save.connect(MyModel.post_save, sender=MyModel)
post_init.connect(MyModel.remember_state, sender=MyModel)
Solution 2:
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:
- Dirty fields in django
Solution 3:
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?
Solution 4:
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.
note: Don't know about versions of django older than 1.2, but this code using __setattr__
won't work, it'll fail just after the second if
, when trying to access self.state
.
I tried something similar, and I tried to fix this problem by forcing the initialization of state
(first in __init__
then ) in __new__
but this will lead to nasty unexpected behaviour.
I'm editing instead of commenting for obvious reasons, also: I'm not deleting this piece of code since maybe it could work with older (or future?) versions of django, and there may be another workaround to the self.state
problem that i'm unaware of