it-swarm.com.de

Aktionen, die durch Feldwechsel in Django ausgelöst werden

Wie kann ich Aktionen ausführen, wenn ein Feld in einem meiner Modelle geändert wird? In diesem speziellen Fall habe ich dieses Modell:

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

und ich möchte, dass Units erstellt werden, und das Feld "gestartet" wird unter anderem mit der aktuellen datetime gefüllt, wenn der Status von "Setup" auf "Active" wechselt.

Ich vermute, dass eine Modellinstanzmethode benötigt wird, aber die Docs scheinen nicht viel zu sagen, wie sie auf diese Weise verwendet werden.

Update: Ich habe der Game-Klasse Folgendes hinzugefügt:

    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

Grundsätzlich müssen Sie die save-Methode überschreiben, prüfen, ob das state-Feld geändert wurde, gegebenenfalls started setzen und dann die Modellbasisklasse in der Datenbank persistieren lassen.

Der schwierige Teil ist herauszufinden, ob das Feld geändert wurde. Schauen Sie sich die Mixins und andere Lösungen in dieser Frage an, um Ihnen dabei zu helfen:

11
ars

Es wurde beantwortet, aber hier ist ein Beispiel für die Verwendung von Signalen, post_init und 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 verfügt über ein geschicktes Feature namens Signale , bei dem es sich um Auslöser handelt, die zu bestimmten Zeiten ausgelöst werden:

  • Vorher/Nachher wird die Speichermethode eines Modells aufgerufen
  • Vorher/Nachher wird die Löschmethode eines Modells aufgerufen
  • Vor/nach einer HTTP-Anfrage

Lesen Sie die Dokumentation, um vollständige Informationen zu erhalten. Sie müssen jedoch lediglich eine Empfängerfunktion erstellen und diese als Signal registrieren. Dies geschieht normalerweise in models.py.

from Django.core.signals import request_finished

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

request_finished.connect(my_callback)

Einfach, wie?

14
c_harm

Eine Möglichkeit besteht darin, einen Setter für den Status hinzuzufügen. Es ist nur eine normale Methode, nichts Besonderes.

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: Wenn Sie möchten, dass dies ausgelöst wird wenneine Änderung an einer Modellinstanz vorgenommen wird, können Sie (stattvon set_state oben) eine __setattr__-Methode in Game verwenden, was etwas ist so was:

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.

Beachten Sie, dass Sie dies nicht speziell in den Django-Dokumenten finden, da es sich bei (__setattr__) um eine standardmäßige Python-Funktion handelt, die hier dokumentiert ist und nicht Django-spezifisch ist.

anmerkung: Ich weiß nicht, welche Versionen von Django älter als 1.2 ist, aber dieser Code, der __setattr__ verwendet, funktioniert nicht. Er wird nach der zweiten if beim Versuch, auf self.state zuzugreifen, fehlschlagen.

Ich habe etwas Ähnliches ausprobiert und versucht, dieses Problem zu beheben, indem die Initialisierung von state (zuerst in __init__ und dann in __new__) erzwungen wurde. Dies führt jedoch zu unerwartetem Verhalten.

Ich bearbeite, anstatt aus offensichtlichen Gründen zu kommentieren, außerdem: Ich lösche diesen Code nicht, da er möglicherweise mit älteren (oder zukünftigen?) Versionen von Django zusammenarbeitet und es möglicherweise eine andere Problemumgehung für das self.state-Problem gibt, das ich bin mir dessen nicht bewusst

8
Vinay Sajip

@dcramer hat (meiner Meinung nach) eine elegantere Lösung für dieses Problem gefunden.

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

Meine Lösung besteht darin, den folgenden Code in __init__.py der App einzufügen:

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

und füge <field_name>_changed statische Methode meiner Modellklasse hinzu:

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

dann ändert sich das sold_dt-Feld, wenn sich das sold-Feld ändert.

Alle Änderungen eines im Modell definierten Feldes lösen die <field_name>_changed-Methode aus, wobei das alte und das neue Objekt als Parameter verwendet werden.

0
Richard Chien