モデルの1つでフィールドが変更されたときにアクションを実行するにはどうすればよいですか?この特定のケースでは、私はこのモデルを持っています:
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を作成し、 'started'フィールドに現在の日時(特に)を入力したいと思います。
モデルのインスタンスメソッドが必要だと思いますが、ドキュメントには、この方法での使用について多くのことは述べられていないようです。
更新:ゲームクラスに次のコードを追加しました。
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
基本的に、save
メソッドをオーバーライドし、state
フィールドが変更されたかどうかを確認し、必要に応じてstarted
を設定して、モデルの基本クラスがデータベースに永続化するようにします。
トリッキーな部分は、フィールドが変更されたかどうかを理解することです。この質問のミックスインと他のソリューションをチェックして、これを手助けしてください:
答えは出ましたが、以下はシグナル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)
Djangoには signals と呼ばれる気の利いた機能があり、特定の時間に効果的にトリガーされます。
完全な情報についてはドキュメントをお読みください。ただし、レシーバー関数を作成して信号として登録するだけです。これは通常、models.pyで行われます。
from Django.core.signals import request_finished
def my_callback(sender, **kwargs):
print "Request finished!"
request_finished.connect(my_callback)
シンプルですね
1つの方法は、状態のセッターを追加することです。これは通常の方法であり、特別なことは何もありません。
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
の代わりに(代わりに)Game
の__setattr__
メソッドを使用できます。これは次のようなものです。
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 docsで見つかりません。これは(__setattr__
)が標準のPython機能、ドキュメント化されているため here)であるため 、Django固有ではありません。
注:Django 1.2より前のバージョンについては不明ですが、__setattr__
を使用するこのコードは機能しません。self.state
にアクセスしようとすると、2番目のif
の直後で失敗します。
私は同様のことを試みましたが、__init__
でstate
を(__new__
で最初に)強制的に初期化してこの問題を解決しようとしましたが、これは予期しない動作を引き起こす可能性があります。
明らかな理由でコメントするのではなく編集しています:古い(または将来の)バージョンのDjangoでも動作する可能性があるため、このコードを削除していません。また、self.state
の問題に別の回避策がある可能性があります。知らない
@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
私の解決策は、次のコードをアプリの__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
フィールドが変更されると、sold_dt
フィールドが変更されます。
モデルで定義されたフィールドの変更は、古いオブジェクトと新しいオブジェクトをパラメーターとして、<field_name>_changed
メソッドをトリガーします。
ダーティを使用して変更を検出し、保存方法を上書きしますダーティフィールド
私の前と: 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)