¿Cómo hago para que ocurran acciones cuando se cambia un campo en uno de mis modelos? En este caso particular, tengo este modelo:

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' se rellena con la fecha y hora actual (entre otras cosas), cuando el estado pasa de Configuración a Activo.

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

Actualización: agregué 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
45
Jeff Bradberry 29 jul. 2009 a las 05:42

7 respuestas

La mejor respuesta

Básicamente, debe anular el método save, verificar si se cambió el campo state, establecer started si es necesario y luego dejar que la clase base del modelo termine de persistir en la base de datos.

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

18
Community 23 may. 2017 a las 11:47

Uso de Dirty para detectar cambios y sobrescribir el método de guardar campo sucio

Mi respuesta anterior: Acciones activadas por el cambio de campo en Django

class Game(DirtyFieldsMixin, models.Model):
    STATE_CHOICES = (
        ('S', 'Setup'),
        ('A', 'Active'),
        ('P', 'Paused'),
        ('F', 'Finished')
        )
    state = models.CharField(max_length=1, choices=STATE_CHOICES, default='S')

    def save(self, *args, **kwargs):
        if self.is_dirty():
            dirty_fields = self.get_dirty_fields()
            if 'state' in dirty_fields:
                Do_some_action()
        super().save(*args, **kwargs)
0
Hitman 27 dic. 2019 a las 06:33

Mi solución es poner el siguiente código en la aplicación __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 agrego el método estático <field_name>_changed 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 cambie el campo sold.

Cualquier cambio de cualquier campo definido en el modelo activará el método <field_name>_changed, con los objetos antiguos y nuevos como parámetros.

0
Richard Chien 23 jul. 2017 a las 15:13

Una forma es agregar un setter 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 esto se active siempre que se realice un cambio en una instancia de modelo, puede ( en su lugar de set_state arriba) use un método __setattr__ en Game que es algo 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 no encontrará esto especialmente en los documentos de Django, ya que (__setattr__) es una característica estándar de Python, documentada aquí, y no es específico de Django.

Nota: no conozco las versiones de django anteriores a 1.2, pero este código que utiliza __setattr__ no funcionará, fallará justo después del segundo if, cuando intente acceder a {{X2} }.

Intenté algo similar e intenté solucionar este problema forzando la inicialización de state (primero en __init__ y luego) en __new__, pero esto conducirá a un comportamiento desagradable e inesperado.

Estoy editando en lugar de comentar por razones obvias, también: no estoy eliminando 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 para {{X0} } problema del que no estoy al tanto

8
berdario 10 feb. 2011 a las 17:24

Django tiene una característica ingeniosa llamada señales, que son efectivamente activadores que se activan en momentos específicos:

  • Antes / después del método de guardar de un modelo se llama
  • Antes / después del método de eliminación de un modelo se llama
  • Antes / después de que se realice una solicitud HTTP

Lea los documentos para obtener información completa, pero todo lo que necesita hacer es crear una función de receptor y registrarla 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?

16
c_harmc_harm 29 jul. 2009 a las 01:56

Se ha respondido, pero aquí hay un ejemplo del uso de señales, post_init y 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)
28
Daniel Backman 23 ene. 2019 a las 15:14

@dcramer encontró 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
4
lucmult 8 ene. 2013 a las 23:51