Django中字段更改触发的操作

Jef*_*rry 33 python django django-models

当我的某个模型中的字段发生变化时,如何操作?在这种特殊情况下,我有这个模型:

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')
Run Code Online (Sandbox Code Playgroud)

当状态从Setup变为Active时,我想创建Units,并且'started'字段填充当前日期时间(以及其他内容).

我怀疑需要一个模型实例方法,但是文档似乎没有太多关于以这种方式使用它们的说法.

更新:我已将以下内容添加到我的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
Run Code Online (Sandbox Code Playgroud)

Dan*_*man 27

它已被回答,但这是一个使用信号,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)
Run Code Online (Sandbox Code Playgroud)

  • 对于那些仍在寻找上述问题解决方案的人来说,这对我来说就像一个魅力。唯一的编辑是我必须在 models.py 的开头导入 post_save 和 post_init 函数: `from django.db.models.signals import post_save, post_init` (2认同)

小智 16

Django有一个叫做信号的漂亮功能,它是在特定时间启动的有效触发器:

  • 在调用模型的save方法之前/之后
  • 调用模型的删除方法之前/之后
  • 在发出HTTP请求之前/之后

阅读文档以获取完整信息,但您需要做的就是创建一个接收器功能并将其注册为信号.这通常在models.py中完成.

from django.core.signals import request_finished

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

request_finished.connect(my_callback)
Run Code Online (Sandbox Code Playgroud)

简单,嗯?


ars*_*ars 16

基本上,您需要覆盖该save方法,检查state字段是否已更改,started如果需要进行设置,然后让模型基类完成持久化到数据库.

棘手的部分是弄清楚场地是否已经改变.查看此问题中的mixins和其他解决方案,以帮助您解决此问题:

  • FWIW在进行批量更新时不会调用"保存"和信号.请参阅http://docs.djangoproject.com/en/dev/ref/models/querysets/#update上的示例下面的注释. (4认同)
  • @cpharmston:我认为这是可以接受的:http://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods - 我在模型以外的实体想要的时候使用信号要通知,但在这种情况下,实体是模型本身,所以只需覆盖保存(这对于面向对象的方法来说是相当传统的). (3认同)

Vin*_*jip 8

一种方法是为州添加一个setter.这只是一种常规方法,没什么特别的.

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.
Run Code Online (Sandbox Code Playgroud)

更新:如果你想要这个被触发每当一个变化是到模型实例,您就可以(代替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.
Run Code Online (Sandbox Code Playgroud)

请注意,您不会在Django文档中特别发现这一点,因为它(__setattr__)是标准的Python特性,在此处记录,并且不是特定于Django的.

注意:不知道版本低于1.2的django,但是这个代码使用__setattr__不起作用,它会在第二次if尝试访问时失败self.state.

我试过类似的东西,我试图通过强制初始化state(先在__init__那时)来解决这个问题,__new__但这会导致令人讨厌的意外行为.

我正在编辑而不是出于显而易见的原因进行评论,还有:我不会删除这段代码,因为它可能适用于较旧(或未来?)版本的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
Run Code Online (Sandbox Code Playgroud)