2009-07-29 7 views
23

Comment des actions peuvent-elles se produire lorsqu'un champ est modifié dans l'un de mes modèles? Dans ce cas particulier, j'ai ce modèle:Actions déclenchées par un changement de champ dans 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') 

et je voudrais demander que les unités créées, et le champ « commencé », peuplée avec le datetime courant (entre autres), lorsque l'état passe de configuration à Actif.

Je suppose qu'une méthode d'instance de modèle est nécessaire, mais les docs ne semblent pas avoir beaucoup à dire sur leur utilisation de cette manière.

Mise à jour: J'ai ajouté ce qui suit à 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 
+0

J'ai mis à jour ma réponse en fonction de votre commentaire. –

+0

django-model-utils implémente un champ de contrôle utile pour votre cas de terrain démarré: https://django-model-utils.readthedocs.org/en/latest/fields.html#monitorfield – jdcaballerov

Répondre

10

Fondamentalement, vous devez remplacer la méthode save, vérifier si le champ state a été modifié, mis started si nécessaire, puis laisser la finition de classe de base de modèle persistant à la base de données.

La partie difficile est de déterminer si le champ a été changé. Consultez les mixins et d'autres solutions à cette question pour vous aider avec ceci:

+1

Je ne suis pas sûr de ce que je ressens à propos de méthodes dominantes dans l'ORM de Django. IMO, cela serait mieux accompli en utilisant django.db.models.signals.post_save. –

+3

@cpharmston: Je pense que c'est acceptable: http://docs.djangoproject.com/fr/dev/topics/db/models/#overriding-predefined-model-methods - J'utilise des signaux quand une entité autre que le modèle veut être notifié, mais dans ce cas, l'entité est le modèle lui-même, il suffit donc de surcharger save (c'est assez classique avec les méthodes orientées objet). – ars

+0

Remplacer save() ne fonctionnait pas sur les opérations d'administration en bloc la dernière fois que j'ai essayé (1.0 je pense) –

14

Django a une fonctionnalité intéressante appelée signals, qui sont déclenche effectivement qui sont lancés à des moments précis:

  • 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 qu'une requête HTTP est effectuée

Lisez les docs pour plus d'informations, mais il vous suffit de créer une fonction de réception et de l'enregistrer en tant que signal. Cela est généralement fait 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?

5

Une façon est d'ajouter un setter 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. 

Mise à jour: Si vous voulez que cela soit déclenché chaque fois une modification est apportée à une instance de modèle, vous pouvez (au lieu de set_state ci-dessus) utiliser une méthode __setattr__ dans Game qui est quelque chose comme 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 trouveriez pas particulièrement ce dans la documentation de Django, comme il (__setattr__) est une fonction standard de Python, documenté here, et n'est pas spécifique à Django.

note: Ne pas connaître les versions de django antérieures à 1.2, mais ce code en utilisant __setattr__ ne fonctionnera pas, il va échouer juste après la seconde if, en essayant d'accéder à self.state.

J'ai essayé quelque chose de semblable, et j'ai essayé de résoudre ce problème en forçant l'initialisation de state (premier __init__ alors) dans __new__ mais cela conduira à un comportement inattendu méchant. Je suis en train d'éditer au lieu de commenter pour des raisons évidentes, aussi: Je ne supprime pas ce morceau de code puisqu'il pourrait peut-être fonctionner avec des versions plus anciennes (ou futures?) De django, et il pourrait y avoir une autre solution pour contourner le problème. self.state problème que je ne connais pas

+0

Oui, mais je ne comprenais pas comment utiliser une telle méthode lors de l'édition , disons, de la page d'administration. –

4

@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 
14

Il a été répondu, mais voici un exemple d'utilisation des signaux, post_init et post_save.

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

Ma solution est de mettre le code suivant à l'application de __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 

et ajouter <field_name>_changed méthode statique à 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 change lorsque sold changements de champ.

Toute modification d'un champ défini dans le modèle déclenchera la méthode <field_name>_changed, avec les anciens et nouveaux objets comme paramètres.

Questions connexes