2009-07-29 13 views
23

¿Cómo se producen las acciones cuando se cambia un campo en uno de mis modelos? En este caso particular, tengo este modelo:Acciones desencadenadas por el cambio de campo en 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') 

y me gustaría tener unidades creadas, y el campo 'iniciado' rellena con la fecha y hora actuales (entre otras cosas), cuando el estado pasa de configuración para Activo.

Sospecho que se necesita un método de instancia modelo, pero los documentos no parecen tener mucho que decir sobre su uso de esta manera.

Actualización: he añadido lo siguiente a mi clase de juego:

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

He actualizado mi respuesta de acuerdo con su comentario. –

+0

django-model-utils implementa un campo de monitor útil para su caso de campo iniciado: https://django-model-utils.readthedocs.org/en/latest/fields.html#monitorfield – jdcaballerov

Respuesta

10

Básicamente, es necesario reemplazar el método save, comprobar si el campo state fue cambiado, establecer started si es necesario y luego dejar que el acabado modelo de clase base que persiste a la base de datos.

La parte difícil es averiguar si se cambió el campo. Echa un vistazo a los mixins y otras soluciones en esta pregunta para ayudarle a salir con esto:

+1

No estoy seguro de cómo me siento acerca de métodos de anulación en el ORM de Django. OMI, esto se lograría mejor usando django.db.models.signals.post_save. –

+3

@cpharmston: Creo que esto es aceptable: http://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods - Uso señales cuando una entidad que no sea el modelo quiere ser notificado, pero en este caso la entidad es el modelo en sí, así que simplemente anule el guardado (esto es bastante convencional con métodos orientados a objetos). – ars

+0

La anulación de save() no funcionó en las operaciones de administración masiva la última vez que probé (1.0 creo) –

14

Django tiene una muy buena característica llamada signals, que son efectivamente provoca que se puso en marcha en momentos específicos:

  • Antes/después del método de guardado de un modelo se llama
  • Antes/después del método de eliminación de un modelo se llama
  • antes/después de una petición HTTP se hace

Lea la documentación para información completa, pero todo lo que tiene que hacer es crear una función de receptor y se registra como una señal. Esto generalmente se hace en models.py.

from django.core.signals import request_finished 

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

request_finished.connect(my_callback) 

Simple, eh?

5

Una forma es añadir un regulador para el estado. Es solo un método normal, nada especial.

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. 

Actualización: Si desea que esta se active siempre se realiza un cambio a una instancia de modelo, puede (lugar de set_state arriba) utilizar un método __setattr__ en Game que es algo así como esto:

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. 

Tenga en cuenta que usted no encontrará todo esto en la documentación de Django, ya que (__setattr__) es una característica estándar de Python, documentado here, y no es específico de Django.

nota: No sé sobre versiones de django anteriores a 1.2, pero este código que usa __setattr__ no funcionará, fallará justo después del segundo if, al intentar acceder al self.state.

he intentado algo similar, y yo tratamos de solucionar este problema al forzar la inicialización de state (por primera vez en __init__ a continuación) en __new__ pero esto dará lugar a un comportamiento inesperado desagradable.

Estoy editando en lugar de comentar por razones obvias, también: no borraré este fragmento de código ya que tal vez podría funcionar con versiones anteriores (o futuras) de django, y puede haber otra solución al problema. self.state problema que desconozco

+0

Sí, pero no estaba claro cómo hacer que ese método se use cada vez que se edita , por ejemplo, desde la página de administración. –

4

@dcramer se le ocurrió una solución más elegante (en mi opinión) para este 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

Se ha contestado, pero aquí es un ejemplo de la utilización de señales, post_init y 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

Mi solución es poner el siguiente código de aplicación 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 

y añadir <field_name>_changed método estático a mi clase de modelo:

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 

entonces el campo sold_dt cambiará cuando sold cambios de campo.

Cualquier cambio en cualquier campo definido en el modelo activará el método <field_name>_changed, con el objeto antiguo y el nuevo como parámetros.

Cuestiones relacionadas