Actions triggered by field change in Django

后端 未结 7 1037
囚心锁ツ
囚心锁ツ 2020-12-07 23:09

How do I have actions occur when a field gets changed in one of my models? In this particular case, I have this model:

class Game(models.Model):
    STATE_C         


        
相关标签:
7条回答
  • 2020-12-07 23:17

    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

    0 讨论(0)
  • 2020-12-07 23:24

    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)
    
    0 讨论(0)
  • 2020-12-07 23:26

    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.

    0 讨论(0)
  • 2020-12-07 23:33

    @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
    
    0 讨论(0)
  • 2020-12-07 23:38

    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, **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)
    
    0 讨论(0)
  • 2020-12-07 23:43

    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:

    • Dirty fields in django
    0 讨论(0)
提交回复
热议问题