web-dev-qa-db-fra.com

Actions déclenchées par un changement de champ dans Django

Comment les actions se produisent-elles lorsqu'un champ est modifié dans l'un de mes modèles? Dans ce cas particulier, j'ai ce modèle:

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')

et j'aimerais que des unités soient créées et que le champ 'démarré' soit renseigné avec la date/heure actuelle (entre autres), lorsque l'état passe de Configuration à Actif.

Je soupçonne qu'une méthode d'instance de modèle est nécessaire, mais les documents ne semblent pas avoir grand-chose à dire sur leur utilisation de cette manière.

Mise à jour: J'ai ajouté les éléments suivants à ma classe de jeu:

    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
27
Jeff Bradberry

En gros, vous devez redéfinir la méthode save, vérifier si le champ state a été modifié, définir started si nécessaire, puis laisser la classe de base du modèle persister dans la base de données.

La partie délicate consiste à déterminer si le champ a été modifié. Découvrez les mixins et autres solutions dans cette question pour vous aider avec ceci:

11
ars

On y a répondu, mais voici un exemple d'utilisation de signaux, post_init et 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)
19
Daniel Backman

Django a une fonctionnalité astucieuse appelée signaux , qui sont en réalité des déclencheurs déclenchés à des moments spécifiques:

  • Avant/après l'appel de la méthode de sauvegarde d'un modèle
  • Avant/après l'appel de la méthode de suppression d'un modèle
  • Avant/après une requête HTTP

Lisez la documentation pour obtenir des informations complètes, mais il vous suffit de créer une fonction de récepteur et de l’enregistrer en tant que signal. Cela se fait généralement dans models.py.

from Django.core.signals import request_finished

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

request_finished.connect(my_callback)

Simple, hein?

14
c_harm

Une façon est d'ajouter un passeur pour l'état. C'est juste une méthode normale, rien de spécial.

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: Si vous souhaitez que cela soit déclenché à chaque fois une modification est apportée à une instance de modèle, vous pouvez (à la place de set_state ci-dessus) utiliser une méthode __setattr__ dans Game qui ressemble à ceci:

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.

Notez que vous ne le trouverez pas spécialement dans les documents Django, car il s'agit (__setattr__) d'une fonctionnalité Python standard, documentée ici , et n'est pas spécifique à Django.

remarque: Je ne connais pas les versions de Django antérieures à la 1.2, mais ce code utilisant __setattr__ ne fonctionnera pas, il échouera juste après la seconde if, lorsque vous tenterez d'accéder à self.state.

J'ai essayé quelque chose de similaire et j'ai essayé de résoudre ce problème en forçant l'initialisation de state (premier dans __init__ puis) ​​dans __new__, mais cela entraînerait un comportement inattendu et désagréable.

J'édite au lieu de commenter pour des raisons évidentes, également: je ne supprime pas ce morceau de code car il pourrait peut-être fonctionner avec les versions plus anciennes (ou futures?) De Django, et il pourrait y avoir une autre solution de contournement au problème self.state suis pas au courant

8
Vinay Sajip

@dcramer a proposé une solution plus élégante (à mon avis) pour ce problème.

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

Ma solution est de mettre le code suivant dans le __init__.py de l'application:

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

et ajoutez la méthode statique <field_name>_changed à ma classe de modèle:

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

alors le champ sold_dt changera lorsque le champ sold sera modifié.

Toute modification d'un champ défini dans le modèle déclenchera la méthode <field_name>_changed, avec l'ancien et le nouvel objet comme paramètres.

0
Richard Chien