Действия, вызванные изменением поля в Django
Как произойти действие, когда поле изменяется на одной из моих моделей? В этом конкретном случае у меня есть эта модель:
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')
и я хотел бы создать Units и поле "start", заполненное текущим временем и временем (в том числе), когда состояние переходит из Setup в Active.
Я подозреваю, что нужен метод экземпляра модели, но документы, похоже, не так много говорят об использовании их таким образом.
Обновление: Я добавил следующее в класс Game:
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
В принципе, вам нужно переопределить метод save
, проверьте, было ли поле state
изменено, при необходимости установите started
, а затем дайте базовому классу модели продолжить работу с базой данных.
Сложная часть выясняет, было ли поле изменено. Ознакомьтесь с микшинами и другими решениями в этом вопросе, чтобы помочь вам в этом:
Ответ 2
На него уже дан ответ, но вот пример использования сигналов post_init и 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, **kwargs):
instance = kwargs.get('instance')
created = kwargs.get('created')
if instance.previous_state != instance.state or created:
do_something_with_state_change()
@staticmethod
def remember_state(sender, **kwargs):
instance = kwargs.get('instance')
instance.previous_state = instance.state
post_save.connect(MyModel.post_save, sender=MyModel)
post_init.connect(MyModel.remember_state, sender=MyModel)
Ответ 3
В Django есть отличная функция, называемая signals, которые эффективно запускаются в определенное время:
- До/после метода сохранения модели называется
- До/после модели метод удаления называется
- До/после выполнения HTTP-запроса
Прочитайте документы для получения полной информации, но все, что вам нужно сделать, это создать функцию приемника и зарегистрировать ее как сигнал. Обычно это делается в models.py.
from django.core.signals import request_finished
def my_callback(sender, **kwargs):
print "Request finished!"
request_finished.connect(my_callback)
Простой, eh?
Ответ 4
Один из способов - добавить установщик состояния. Это просто обычный метод, ничего особенного.
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.
Обновление: если вы хотите, чтобы это срабатывало при каждом изменении экземпляра модели, вы можете (вместо set_state
выше) использовать метод __setattr__
в Game
который выглядит примерно так:
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.
Обратите внимание, что вы не найдете этого в документации по Django, поскольку она (__setattr__
) является стандартной функцией Python, __setattr__
здесь, и не относится к Django.
примечание: не знаю о версиях django старше 1.2, но этот код, использующий __setattr__
, не будет работать, он потерпит неудачу только через секунду, if
при попытке доступа к self.state
.
Я попытался сделать что-то подобное и попытался исправить эту проблему, принудительно инициализируя state
(сначала в __init__
затем) в __new__
но это приведет к неприятному неожиданному поведению.
Я редактирую вместо того, чтобы комментировать по очевидным причинам, а также: я не удаляю этот фрагмент кода, так как, возможно, он может работать со старыми (или будущими?) self.state
django, и может быть другой обходной путь к проблеме self.state
что я не знаю о
Ответ 5
@dcramer придумал более элегантное решение (на мой взгляд) для этой проблемы.
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
Ответ 6
Мое решение состоит в том, чтобы добавить следующий код в приложение __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
и добавьте статический метод <field_name>_changed
к моему классу модели:
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
тогда поле sold_dt
изменится при изменении поля sold
.
Любые изменения любого поля, определенного в модели, вызовут метод <field_name>_changed
со старым и новым объектом в качестве параметров.