23
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:
46
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)
- [Django]-How to break "for loop" after 1 iteration in Django template
- [Django]-How do I get odd and even values in a Django for loop template?
- [Django]-Celery Flower Security in Production
20
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?
- [Django]-Django multiple template inheritance – is this the right style?
- [Django]-How to add url parameters to Django template url tag?
- [Django]-Django 1.8 KeyError: 'manager' on relationship
9
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
- [Django]-How to require login for Django Generic Views?
- [Django]-Django: Switching to Jinja2?
- [Django]-TextField missing in django.forms
5
@dcramer came up with a more elegant solution (in my opinion) for this issue.
https://gist.github.com/730765
from django.db.models.signals import post_init
def track_data(*fields):
"""
Tracks property changes on a model instance.
The changed list of properties is refreshed on model initialization
and save.
>>> @track_data('name')
>>> class Post(models.Model):
>>> name = models.CharField(...)
>>>
>>> @classmethod
>>> def post_save(cls, sender, instance, created, **kwargs):
>>> if instance.has_changed('name'):
>>> print "Hooray!"
"""
UNSAVED = dict()
def _store(self):
"Updates a local copy of attributes values"
if self.id:
self.__data = dict((f, getattr(self, f)) for f in fields)
else:
self.__data = UNSAVED
def inner(cls):
# contains a local copy of the previous values of attributes
cls.__data = {}
def has_changed(self, field):
"Returns ``True`` if ``field`` has changed since initialization."
if self.__data is UNSAVED:
return False
return self.__data.get(field) != getattr(self, field)
cls.has_changed = has_changed
def old_value(self, field):
"Returns the previous value of ``field``"
return self.__data.get(field)
cls.old_value = old_value
def whats_changed(self):
"Returns a list of changed attributes."
changed = {}
if self.__data is UNSAVED:
return changed
for k, v in self.__data.iteritems():
if v != getattr(self, k):
changed[k] = v
return changed
cls.whats_changed = whats_changed
# Ensure we are updating local attributes on model init
def _post_init(sender, instance, **kwargs):
_store(instance)
post_init.connect(_post_init, sender=cls, weak=False)
# Ensure we are updating local attributes on model save
def save(self, *args, **kwargs):
save._original(self, *args, **kwargs)
_store(self)
save._original = cls.save
cls.save = save
return cls
return inner
- [Django]-Django REST Framework custom fields validation
- [Django]-How to get an ImageField URL within a template?
- [Django]-Django Admin – Disable the 'Add' action for a specific model
1
My solution is to put the following code to app’s __init__.py
:
from django.db.models import signals
from django.dispatch import receiver
@receiver(signals.pre_save)
def models_pre_save(sender, instance, **_):
if not sender.__module__.startswith('myproj.myapp.models'):
# ignore models of other apps
return
if instance.pk:
old = sender.objects.get(pk=instance.pk)
fields = sender._meta.local_fields
for field in fields:
try:
func = getattr(sender, field.name + '_changed', None) # class function or static function
if func and callable(func) and getattr(old, field.name, None) != getattr(instance, field.name, None):
# field has changed
func(old, instance)
except:
pass
and add <field_name>_changed
static method to my model class:
class Product(models.Model):
sold = models.BooleanField(default=False, verbose_name=_('Product|sold'))
sold_dt = models.DateTimeField(null=True, blank=True, verbose_name=_('Product|sold datetime'))
@staticmethod
def sold_changed(old_obj, new_obj):
if new_obj.sold is True:
new_obj.sold_dt = timezone.now()
else:
new_obj.sold_dt = None
then the sold_dt
field will change when sold
field changes.
Any changes of any field defined in the model will trigger the <field_name>_changed
method, with old and new object as parameters.
- [Django]-Django URL Redirect
- [Django]-Tailwindcss: fixed/sticky footer on the bottom
- [Django]-No module named pkg_resources
0
Using Dirty to detect changes and over-writing save method
dirty field
My prev ans: Actions triggered by field change in Django
class Game(DirtyFieldsMixin, models.Model):
STATE_CHOICES = (
('S', 'Setup'),
('A', 'Active'),
('P', 'Paused'),
('F', 'Finished')
)
state = models.CharField(max_length=1, choices=STATE_CHOICES, default='S')
def save(self, *args, **kwargs):
if self.is_dirty():
dirty_fields = self.get_dirty_fields()
if 'state' in dirty_fields:
Do_some_action()
super().save(*args, **kwargs)
- [Django]-How can I use Django OAuth Toolkit with Python Social Auth?
- [Django]-Django rest framework nested self-referential objects
- [Django]-Mixin common fields between serializers in Django Rest Framework
0
If you use PostgreSQL you can create a trigger:
https://www.postgresql.org/docs/current/sql-createtrigger.html
Example:
CREATE TRIGGER check_update
BEFORE UPDATE ON accounts
FOR EACH ROW
WHEN (OLD.balance IS DISTINCT FROM NEW.balance)
EXECUTE FUNCTION check_account_update();
- [Django]-Django returning HTTP 301?
- [Django]-ImageField overwrite image file with same name
- [Django]-How to install libpq-fe.h?