web-dev-qa-db-ja.com

Djangoのフィールド変更によってトリガーされるアクション

モデルの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
35
Jeff Bradberry

基本的に、saveメソッドをオーバーライドし、stateフィールドが変更されたかどうかを確認し、必要に応じてstartedを設定して、モデルの基本クラスがデータベースに永続化するようにします。

トリッキーな部分は、フィールドが変更されたかどうかを理解することです。この質問のミックスインと他のソリューションをチェックして、これを手助けしてください:

18
ars

答えは出ましたが、以下はシグナル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)
28
Daniel Backman

Djangoには signals と呼ばれる気の利いた機能があり、特定の時間に効果的にトリガーされます。

  • モデルのsaveメソッドが呼び出される前/後
  • モデルの削除メソッドが呼び出される前/後
  • HTTPリクエストが行われる前/後

完全な情報についてはドキュメントをお読みください。ただし、レシーバー関数を作成して信号として登録するだけです。これは通常、models.pyで行われます。

from Django.core.signals import request_finished

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

request_finished.connect(my_callback)

シンプルですね

16
c_harm

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の問題に別の回避策がある可能性があります。知らない

8
Vinay Sajip

@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
4
lucmult

私の解決策は、次のコードをアプリの__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メソッドをトリガーします。

0
Richard Chien

ダーティを使用して変更を検出し、保存方法を上書きしますダーティフィールド

私の前と: 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)
0
Hitman