Comment les actions se produisent-elles lorsqu'un champ est modifié dans l'un de mes modèles? Dans ce cas particulier, j'ai ce modèle:
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 j'aimerais que des unités soient créées et que le champ 'démarré' soit renseigné avec la date/heure actuelle (entre autres), lorsque l'état passe de Configuration à Actif.
Je soupçonne qu'une méthode d'instance de modèle est nécessaire, mais les documents ne semblent pas avoir grand-chose à dire sur leur utilisation de cette manière.
Mise à jour: J'ai ajouté les éléments suivants à 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
En gros, vous devez redéfinir la méthode save
, vérifier si le champ state
a été modifié, définir started
si nécessaire, puis laisser la classe de base du modèle persister dans la base de données.
La partie délicate consiste à déterminer si le champ a été modifié. Découvrez les mixins et autres solutions dans cette question pour vous aider avec ceci:
On y a répondu, mais voici un exemple d'utilisation de signaux, post_init et 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)
Django a une fonctionnalité astucieuse appelée signaux , qui sont en réalité des déclencheurs déclenchés à des moments spécifiques:
Lisez la documentation pour obtenir des informations complètes, mais il vous suffit de créer une fonction de récepteur et de l’enregistrer en tant que signal. Cela se fait généralement 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?
Une façon est d'ajouter un passeur 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.
Update: Si vous souhaitez que cela soit déclenché à chaque fois une modification est apportée à une instance de modèle, vous pouvez (à la place de set_state
ci-dessus) utiliser une méthode __setattr__
dans Game
qui ressemble à 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 le trouverez pas spécialement dans les documents Django, car il s'agit (__setattr__
) d'une fonctionnalité Python standard, documentée ici , et n'est pas spécifique à Django.
remarque: Je ne connais pas les versions de Django antérieures à la 1.2, mais ce code utilisant __setattr__
ne fonctionnera pas, il échouera juste après la seconde if
, lorsque vous tenterez d'accéder à self.state
.
J'ai essayé quelque chose de similaire et j'ai essayé de résoudre ce problème en forçant l'initialisation de state
(premier dans __init__
puis) dans __new__
, mais cela entraînerait un comportement inattendu et désagréable.
J'édite au lieu de commenter pour des raisons évidentes, également: je ne supprime pas ce morceau de code car il pourrait peut-être fonctionner avec les versions plus anciennes (ou futures?) De Django, et il pourrait y avoir une autre solution de contournement au problème self.state
suis pas au courant
@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
Ma solution est de mettre le code suivant dans le __init__.py
de l'application:
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 ajoutez la méthode statique <field_name>_changed
à 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
changera lorsque le champ sold
sera modifié.
Toute modification d'un champ défini dans le modèle déclenchera la méthode <field_name>_changed
, avec l'ancien et le nouvel objet comme paramètres.