Подтвердить что ты не робот

Действия, вызванные изменением поля в 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
4b9b3361

Ответ 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 со старым и новым объектом в качестве параметров.