2009-07-29 6 views
23

Come si verificano le azioni quando un campo viene modificato in uno dei miei modelli? In questo caso particolare, ho questo modello:Azioni attivate dal cambio di campo in 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') 

e vorrei avere unità create, e il campo 'iniziato' popolato con il datetime corrente (tra l'altro), quando lo stato passa da Setup per Attivo.

Ho il sospetto che sia necessario un metodo di istanza di modello, ma i documenti non sembrano avere molto da dire sull'utilizzo in questo modo.

Update: ho aggiunto quanto segue per la mia classe di gioco:

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

Ho aggiornato la mia risposta in linea con il tuo commento. –

+0

django-model-utils implementa un campo monitor utile per il caso di campo avviato: https://django-model-utils.readthedocs.org/en/latest/fields.html#monitorfield – jdcaballerov

risposta

10

Fondamentalmente, è necessario eseguire l'override del metodo save, controllare se il campo state è stata cambiata, impostare started se necessario, e poi lasciare che la finitura classe base del modello persistente al database.

La parte difficile è capire se il campo è stato modificato. Scopri le mixins e altre soluzioni in questa domanda per aiutarvi con questo:

+1

Non sono sicuro di come mi sento riguardo metodi prevalenti nell'ORM di Django. IMO, questo sarebbe meglio realizzato usando django.db.models.signals.post_save. –

+3

@cpharmston: Penso che sia accettabile: http://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods - Io uso i segnali quando un'entità diversa dal modello vuole essere avvisato, ma in questo caso l'entità è il modello stesso, quindi basta salvare (questo è abbastanza convenzionale con i metodi orientati agli oggetti). – ars

+0

L'override di save() non ha funzionato sulle operazioni di amministrazione bulk l'ultima volta che ho provato (1.0 Penso) –

14

Django ha una bella funzionalità denominata signals, che sono innesca efficace che si esaltano in momenti specifici:

  • Prima/dopo il metodo di salvataggio di un modello viene chiamato
  • Prima/dopo il metodo di eliminazione di un modello viene chiamato
  • Prima/dopo una richiesta HTTP viene effettuata

Leggi i documenti per informazioni complete, ma tutto ciò che devi fare è creare una funzione ricevitore e registrarla come un segnale. Questo di solito è fatto in models.py.

from django.core.signals import request_finished 

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

request_finished.connect(my_callback) 

Semplice, eh?

5

Un modo è quello di aggiungere un setter per lo Stato. È solo un metodo normale, niente di speciale.

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. 

Aggiornamento: Se si desidera che questo essere attivato ogni volta è fatto a un'istanza di modello di un cambiamento, è possibile (invece di set_state sopra) utilizzare un metodo __setattr__ in Game che è qualcosa di simile questo:

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. 

Nota che non sarebbe particolarmente trovato questo nella documentazione Django, in quanto (__setattr__) è una caratteristica standard di Python, documentato here, e non è specifico per Django.

nota: non si conoscono versioni di django più vecchie di 1.2, ma questo codice che utilizza __setattr__ non funzionerà, fallirà subito dopo il secondo if, quando si tenta di accedere a self.state.

ho provato qualcosa di simile, e ho cercato di risolvere questo problema forzando l'inizializzazione del state (prima in __init__ allora) in __new__ ma questo porterà ad un comportamento imprevisto brutto.

sto modificando invece di commentare per ovvi motivi, anche: non sto cancellando questo pezzo di codice dal momento che forse potrebbe funzionare con le vecchie versioni di Django, e ci possono essere un'altra soluzione alla (o futuro?) self.state problema che non sono a conoscenza di

+0

Sì, ma non mi è stato chiaro come utilizzare un tale metodo ogni volta che si modifica , ad esempio, dalla pagina di amministrazione. –

4

@dcramer ha trovato una soluzione più elegante (a mio parere) per questo problema.

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

E 'stato risposto, ma ecco un esempio di utilizzo di segnali, post_init e 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

La mia soluzione è quella di mettere il seguente codice per app di __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 

e aggiungere <field_name>_changed metodo statico alla mia classe del modello:

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 

allora il campo sold_dt cambierà quando sold cambi di campo.

Qualsiasi modifica di qualsiasi campo definito nel modello attiverà il metodo <field_name>_changed, con il vecchio e il nuovo oggetto come parametri.

Problemi correlati