Django: quand vous sauvegardez, Comment Pouvez-vous vérifier si un champ a changé?
dans mon modèle j'ai:
class Alias(MyBaseModel):
remote_image = models.URLField(max_length=500, null=True, help_text="A URL that is downloaded and cached for the image. Only
used when the alias is made")
image = models.ImageField(upload_to='alias', default='alias-default.png', help_text="An image representing the alias")
def save(self, *args, **kw):
if (not self.image or self.image.name == 'alias-default.png') and self.remote_image :
try :
data = utils.fetch(self.remote_image)
image = StringIO.StringIO(data)
image = Image.open(image)
buf = StringIO.StringIO()
image.save(buf, format='PNG')
self.image.save(hashlib.md5(self.string_id).hexdigest() + ".png", ContentFile(buf.getvalue()))
except IOError :
pass
qui fonctionne bien pour la première fois le remote_image
change.
Comment puis-je récupérer une nouvelle image quand quelqu'un a modifié le remote_image
sur l'alias? Et deuxièmement, y a-t-il un meilleur moyen de cacher une image à distance?
22 réponses
Si c'est un peu tard, permettez-moi de jeter cette solution pour les autres qui viennent sur ce post. Essentiellement, vous voulez remplacer la méthode __init__
de models.Model
de sorte que vous conservez une copie de la valeur originale. Cela fait en sorte que vous n'avez pas à faire une autre recherche DB (ce qui est toujours une bonne chose).
class Person(models.Model):
name = models.CharField()
__original_name = None
def __init__(self, *args, **kwargs):
super(Person, self).__init__(*args, **kwargs)
self.__original_name = self.name
def save(self, force_insert=False, force_update=False, *args, **kwargs):
if self.name != self.__original_name:
# name changed - do something here
super(Person, self).save(force_insert, force_update, *args, **kwargs)
self.__original_name = self.name
j'utilise le mixin suivant:
from django.forms.models import model_to_dict
class ModelDiffMixin(object):
"""
A model mixin that tracks model fields' values and provide some useful api
to know what fields have been changed.
"""
def __init__(self, *args, **kwargs):
super(ModelDiffMixin, self).__init__(*args, **kwargs)
self.__initial = self._dict
@property
def diff(self):
d1 = self.__initial
d2 = self._dict
diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
return dict(diffs)
@property
def has_changed(self):
return bool(self.diff)
@property
def changed_fields(self):
return self.diff.keys()
def get_field_diff(self, field_name):
"""
Returns a diff for field if it's changed and None otherwise.
"""
return self.diff.get(field_name, None)
def save(self, *args, **kwargs):
"""
Saves model and set initial state.
"""
super(ModelDiffMixin, self).save(*args, **kwargs)
self.__initial = self._dict
@property
def _dict(self):
return model_to_dict(self, fields=[field.name for field in
self._meta.fields])
Utilisation:
>>> p = Place()
>>> p.has_changed
False
>>> p.changed_fields
[]
>>> p.rank = 42
>>> p.has_changed
True
>>> p.changed_fields
['rank']
>>> p.diff
{'rank': (0, 42)}
>>> p.categories = [1, 3, 5]
>>> p.diff
{'categories': (None, [1, 3, 5]), 'rank': (0, 42)}
>>> p.get_field_diff('categories')
(None, [1, 3, 5])
>>> p.get_field_diff('rank')
(0, 42)
>>>
Note
veuillez noter que cette solution fonctionne bien dans le contexte de la requête actuelle seulement. Ainsi, il convient principalement pour les cas simples. Dans un environnement concurrent où plusieurs requêtes peuvent manipuler simultanément la même instance de model, vous avez certainement besoin d'une approche différente.
et maintenant pour la réponse directe: une façon de vérifier si la valeur du champ a changé est de récupérer les données originales de la base de données avant de sauvegarder l'instance. Prenons l'exemple suivant:
class MyModel(models.Model):
f1 = models.CharField(max_length=1)
def save(self, *args, **kw):
if self.pk is not None:
orig = MyModel.objects.get(pk=self.pk)
if orig.f1 != self.f1:
print 'f1 changed'
super(MyModel, self).save(*args, **kw)
la même chose s'applique lorsqu'on travaille avec un formulaire. Vous pouvez le détecter à la méthode clean ou save d'un ModelForm:
class MyModelForm(forms.ModelForm):
def clean(self):
cleaned_data = super(ProjectForm, self).clean()
#if self.has_changed(): # new instance or existing updated (form has data to save)
if self.instance.pk is not None: # new instance only
if self.instance.f1 != cleaned_data['f1']:
print 'f1 changed'
return cleaned_data
class Meta:
model = MyModel
exclude = []
la Meilleure façon est avec un pre_save
du signal. Peut-être n'était-ce pas une option en 2009 lorsque cette question a été posée et qu'on y a répondu, Mais quiconque voit cela aujourd'hui devrait le faire de cette façon:
@receiver(pre_save, sender=MyModel)
def do_something_if_changed(sender, instance, **kwargs):
try:
obj = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist:
pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
else:
if not obj.some_field == instance.some_field: # Field has changed
# do something
depuis que Django 1.8 est sorti, vous pouvez utiliser de_db classmethod pour mettre en cache l'ancienne valeur de remote_image. Puis dans enregistrer méthode, vous pouvez comparer l'ancienne et la nouvelle valeur du champ pour vérifier si la valeur a changé.
@classmethod
def from_db(cls, db, field_names, values):
new = super(Alias, cls).from_db(db, field_names, values)
# cache value went from the base
new._loaded_remote_image = values[field_names.index('remote_image')]
return new
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
if (self._state.adding and self.remote_image) or \
(not self._state.adding and self._loaded_remote_image != self.remote_image):
# If it is first save and there is no cached remote_image but there is new one,
# or the value of remote_image has changed - do your stuff!
notez que le suivi des changements de terrain est disponible dans django-model-utils.
https://django-model-utils.readthedocs.org/en/latest/index.html
si vous utilisez un formulaire ,vous pouvez utiliser le formulaire changed_data ( docs ):
class AliasForm(ModelForm):
def save(self, commit=True):
if 'remote_image' in self.changed_data:
# do things
remote_image = self.cleaned_data['remote_image']
do_things(remote_image)
super(AliasForm, self).save(commit)
class Meta:
model = Alias
à partir de Django 1.8, il y a la méthode from_db
, comme Serge le mentionne. En fait, les Docs de Django incluent ce cas d'utilisation spécifique comme exemple:
https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading
ci-dessous est un exemple montrant comment enregistrer les valeurs initiales des champs qui sont chargés à partir de la base de données
je suis un peu en retard à la fête, mais j'ai trouvé cette solution aussi: Django Sale Champs
vous pouvez utiliser django-model-changes pour ce faire sans une recherche de base de données supplémentaire:
from django.dispatch import receiver
from django_model_changes import ChangesMixin
class Alias(ChangesMixin, MyBaseModel):
# your model
@receiver(pre_save, sender=Alias)
def do_something_if_changed(sender, instance, **kwargs):
if 'remote_image' in instance.changes():
# do something
la solution optimale est probablement celle qui n'inclut pas une opération de lecture de base de données supplémentaire avant de sauvegarder l'instance du modèle, ni aucune autre django-bibliothèque. C'est pourquoi les solutions de laffuste sont préférables. Dans le contexte d'un site administrateur, on peut tout simplement outrepasser la méthode save_model, et y invoquer la méthode has_changed de la forme, tout comme dans la réponse de Sion ci-dessus. Vous arrivez à quelque chose comme ceci, en utilisant le paramètre exemple de Sion mais en utilisant "changed_data" pour obtenir changement possible:
class ModelAdmin(admin.ModelAdmin):
fields=['name','mode']
def save_model(self, request, obj, form, change):
form.changed_data #output could be ['name']
#do somethin the changed name value...
#call the super method
super(self,ModelAdmin).save_model(request, obj, form, change)
- Écraser save_model:
https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model
- Built-in changed_data-méthode pour un Champ:
https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data
bien que cela ne réponde pas vraiment à votre question, je m'en occuperais d'une manière différente.
effacez simplement le champ remote_image
après avoir sauvegardé avec succès la copie locale. Ensuite, dans votre méthode de sauvegarde, vous pouvez toujours mettre à jour l'image lorsque remote_image
n'est pas vide.
si vous souhaitez conserver une référence à l'url, vous pouvez utiliser un champ booléen non modifiable pour gérer le drapeau de cache plutôt que le champ remote_image
lui-même.
j'ai eu cette situation avant que ma solution était de passer outre la méthode pre_save()
de la classe de champ cible il sera appelé seulement si le champ a été changé
"
utile avec FileField
exemple:
class PDFField(FileField):
def pre_save(self, model_instance, add):
# do some operations on your file
# if and only if you have changed the filefield
désavantage:
pas utile si vous voulez faire une opération (post_save) comme utiliser l'objet créé dans un travail (si certains champs ont changé)
améliorer @josh réponse pour tous les domaines:
class Person(models.Model):
name = models.CharField()
def __init__(self, *args, **kwargs):
super(Person, self).__init__(*args, **kwargs)
self._original_fields = dict([(field.attname, getattr(self, field.attname))
for field in self._meta.local_fields if not isinstance(field, models.ForeignKey)])
def save(self, *args, **kwargs):
if self.id:
for field in self._meta.local_fields:
if not isinstance(field, models.ForeignKey) and\
self._original_fields[field.name] != getattr(self, field.name):
# Do Something
super(Person, self).save(*args, **kwargs)
juste pour clarifier, le getattr fonctionne pour obtenir des champs comme person.name
avec des cordes (i.e. getattr(person, "name")
ça marche pour moi à Django 1.8
def clean(self):
if self.cleaned_data['name'] != self.initial['name']:
# Do something
une autre réponse tardive, mais si vous essayez juste de voir si un nouveau fichier a été téléchargé dans un champ Fichier, essayez ceci: (adapté du commentaire de Christopher Adams sur le lien http://zmsmith.com/2010/05/django-check-if-a-field-has-changed / in zach's comment here)
lien mis à jour: https://web.archive.org/web/20130101010327/http://zmsmith.com:80/2010/05/django-check-if-a-field-has-changed /
def save(self, *args, **kw):
from django.core.files.uploadedfile import UploadedFile
if hasattr(self.image, 'file') and isinstance(self.image.file, UploadedFile) :
# Handle FileFields as special cases, because the uploaded filename could be
# the same as the filename that's already there even though there may
# be different file contents.
# if a file was just uploaded, the storage model with be UploadedFile
# Do new file stuff here
pass
j'ai étendu le mixin de @livskiy comme suit:
class ModelDiffMixin(models.Model):
"""
A model mixin that tracks model fields' values and provide some useful api
to know what fields have been changed.
"""
_dict = DictField(editable=False)
def __init__(self, *args, **kwargs):
super(ModelDiffMixin, self).__init__(*args, **kwargs)
self._initial = self._dict
@property
def diff(self):
d1 = self._initial
d2 = self._dict
diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
return dict(diffs)
@property
def has_changed(self):
return bool(self.diff)
@property
def changed_fields(self):
return self.diff.keys()
def get_field_diff(self, field_name):
"""
Returns a diff for field if it's changed and None otherwise.
"""
return self.diff.get(field_name, None)
def save(self, *args, **kwargs):
"""
Saves model and set initial state.
"""
object_dict = model_to_dict(self,
fields=[field.name for field in self._meta.fields])
for field in object_dict:
# for FileFields
if issubclass(object_dict[field].__class__, FieldFile):
try:
object_dict[field] = object_dict[field].path
except :
object_dict[field] = object_dict[field].name
# TODO: add other non-serializable field types
self._dict = object_dict
super(ModelDiffMixin, self).save(*args, **kwargs)
class Meta:
abstract = True
et le DictField est:
class DictField(models.TextField):
__metaclass__ = models.SubfieldBase
description = "Stores a python dict"
def __init__(self, *args, **kwargs):
super(DictField, self).__init__(*args, **kwargs)
def to_python(self, value):
if not value:
value = {}
if isinstance(value, dict):
return value
return json.loads(value)
def get_prep_value(self, value):
if value is None:
return value
return json.dumps(value)
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_db_prep_value(value)
il peut être utilisé en l'étendant dans vos modèles un champ _dict sera ajouté lorsque vous synchronisez / migrez et ce champ stockera l'état de vos objets
comme une extension de la réponse de SmileyChris, vous pouvez ajouter un champ datetime au modèle pour last_updated, et définir une sorte de limite pour l'âge max que vous le laisserez arriver avant de vérifier un changement""
le mixin de @ivanlivski est génial.
Je l'ai étendu à
- assurez-vous qu'il fonctionne avec des Champs décimaux.
- exposer les propriétés pour simplifier l'utilisation
le code mis à jour est disponible ici: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py
pour aider les personnes nouvelles en Python ou Django, je vais donner un exemple plus complet. Cette utilisation est de prendre un fichier à partir d'un fournisseur de données et d'assurer les enregistrements dans la base de données reflètent le fichier.
mon modèle:
class Station(ModelDiffMixin.ModelDiffMixin, models.Model):
station_name = models.CharField(max_length=200)
nearby_city = models.CharField(max_length=200)
precipitation = models.DecimalField(max_digits=5, decimal_places=2)
# <list of many other fields>
def is_float_changed (self,v1, v2):
''' Compare two floating values to just two digit precision
Override Default precision is 5 digits
'''
return abs (round (v1 - v2, 2)) > 0.01
La classe qui charge le fichier a ces méthodes:
class UpdateWeather (object)
# other methods omitted
def update_stations (self, filename):
# read all existing data
all_stations = models.Station.objects.all()
self._existing_stations = {}
# insert into a collection for referencing while we check if data exists
for stn in all_stations.iterator():
self._existing_stations[stn.id] = stn
# read the file. result is array of objects in known column order
data = read_tabbed_file(filename)
# iterate rows from file and insert or update where needed
for rownum in range(sh.nrows):
self._update_row(sh.row(rownum));
# now anything remaining in the collection is no longer active
# since it was not found in the newest file
# for now, delete that record
# there should never be any of these if the file was created properly
for stn in self._existing_stations.values():
stn.delete()
self._num_deleted = self._num_deleted+1
def _update_row (self, rowdata):
stnid = int(rowdata[0].value)
name = rowdata[1].value.strip()
# skip the blank names where data source has ids with no data today
if len(name) < 1:
return
# fetch rest of fields and do sanity test
nearby_city = rowdata[2].value.strip()
precip = rowdata[3].value
if stnid in self._existing_stations:
stn = self._existing_stations[stnid]
del self._existing_stations[stnid]
is_update = True;
else:
stn = models.Station()
is_update = False;
# object is new or old, don't care here
stn.id = stnid
stn.station_name = name;
stn.nearby_city = nearby_city
stn.precipitation = precip
# many other fields updated from the file
if is_update == True:
# we use a model mixin to simplify detection of changes
# at the cost of extra memory to store the objects
if stn.has_changed == True:
self._num_updated = self._num_updated + 1;
stn.save();
else:
self._num_created = self._num_created + 1;
stn.save()
Que Diriez-vous d'utiliser la solution de David Cramer:
http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django /
j'ai eu du succès en l'utilisant comme ça:
@track_data('name')
class Mode(models.Model):
name = models.CharField(max_length=5)
mode = models.CharField(max_length=5)
def save(self, *args, **kwargs):
if self.has_changed('name'):
print 'name changed'
# OR #
@classmethod
def post_save(cls, sender, instance, created, **kwargs):
if instance.has_changed('name'):
print "Hooray!"
une modification à la réponse de @ivanperelivskiy:
@property
def _dict(self):
ret = {}
for field in self._meta.get_fields():
if isinstance(field, ForeignObjectRel):
# foreign objects might not have corresponding objects in the database.
if hasattr(self, field.get_accessor_name()):
ret[field.get_accessor_name()] = getattr(self, field.get_accessor_name())
else:
ret[field.get_accessor_name()] = None
else:
ret[field.attname] = getattr(self, field.attname)
return ret
utilise plutôt la méthode publique de django 1.10 get_fields
. Cela rend le code plus de preuve future, mais plus important aussi inclut des clés étrangères et des champs où modifiable=False.
pour référence, voici la mise en œuvre de .fields
@cached_property
def fields(self):
"""
Returns a list of all forward fields on the model and its parents,
excluding ManyToManyFields.
Private API intended only to be used by Django itself; get_fields()
combined with filtering of field properties is the public API for
obtaining this field list.
"""
# For legacy reasons, the fields property should only contain forward
# fields that are not private or with a m2m cardinality. Therefore we
# pass these three filters as filters to the generator.
# The third lambda is a longwinded way of checking f.related_model - we don't
# use that property directly because related_model is a cached property,
# and all the models may not have been loaded yet; we don't want to cache
# the string reference to the related_model.
def is_not_an_m2m_field(f):
return not (f.is_relation and f.many_to_many)
def is_not_a_generic_relation(f):
return not (f.is_relation and f.one_to_many)
def is_not_a_generic_foreign_key(f):
return not (
f.is_relation and f.many_to_one and not (hasattr(f.remote_field, 'model') and f.remote_field.model)
)
return make_immutable_fields_list(
"fields",
(f for f in self._get_fields(reverse=False)
if is_not_an_m2m_field(f) and is_not_a_generic_relation(f) and is_not_a_generic_foreign_key(f))
)
Voici une autre façon de faire.
class Parameter(models.Model):
def __init__(self, *args, **kwargs):
super(Parameter, self).__init__(*args, **kwargs)
self.__original_value = self.value
def clean(self,*args,**kwargs):
if self.__original_value == self.value:
print("igual")
else:
print("distinto")
def save(self,*args,**kwargs):
self.full_clean()
return super(Parameter, self).save(*args, **kwargs)
self.__original_value = self.value
key = models.CharField(max_length=24, db_index=True, unique=True)
value = models.CharField(max_length=128)
selon les documents: validation des objets
"La deuxième étape full_clean() effectue l'est de l'appeler Modèle.propre.)( Cette méthode doit être remplacée pour effectuer une validation personnalisée sur votre modèle. Cette méthode doit être utilisée pour fournir une validation de modèle personnalisée, et pour modifier les attributs sur votre modèle si vous le souhaitez. Par exemple, vous pouvez l'utiliser pour fournir automatiquement une valeur pour un champ, ou pour effectuer une validation qui nécessite l'accès à plus d'un seul champ: "