template - httpresponseredirect django example
Django: Al guardar, ¿cómo puedes verificar si un campo ha cambiado? (22)
En mi modelo tengo:
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
Lo que funciona muy bien por primera vez, el cambio de remote_image
.
¿Cómo puedo obtener una nueva imagen cuando alguien ha modificado la remote_image
en el alias? Y en segundo lugar, ¿hay una mejor manera de almacenar en caché una imagen remota?
¿Qué hay de usar la solución de David Cramer?
http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django/
He tenido éxito al usarlo así:
@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!"
A partir de Django 1.8, está el método from_db
, como menciona Serge. De hecho, los documentos de Django incluyen este caso de uso específico como ejemplo:
https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading
A continuación se muestra un ejemplo que muestra cómo registrar los valores iniciales de los campos que se cargan desde la base de datos
Aquí hay otra forma de hacerlo.
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)
Según la documentación: validación de objetos.
"El segundo paso que realiza full_clean () es llamar a Model.clean (). Este método debe anularse para realizar una validación personalizada en su modelo. Este método debe usarse para proporcionar una validación de modelo personalizada y, si lo desea, modificar los atributos de su modelo. Por ejemplo, podría usarlo para proporcionar automáticamente un valor para un campo, o para hacer una validación que requiera acceso a más de un solo campo: "
Aunque es un poco tarde, permítanme lanzar esta solución para otros que se encuentren en esta publicación. Esencialmente, desea anular el método __init__
de los models.Model
para mantener una copia del valor original. Esto lo hace para que no tenga que hacer otra búsqueda de base de datos (que siempre es una buena cosa).
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
Desde el lanzamiento de Django 1.8, puede usar from_db classmethod para almacenar en caché el valor antiguo de remote_image. Luego, en el método de guardar , puede comparar el valor antiguo y el nuevo del campo para verificar si el valor ha cambiado.
@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!
El mixin de @ivanlivski es genial.
Lo he extendido a
- Asegúrate de que funcione con campos decimales.
- Expone propiedades para simplificar el uso.
El código actualizado está disponible aquí: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py
Para ayudar a las personas nuevas en Python o Django, daré un ejemplo más completo. Este uso particular es tomar un archivo de un proveedor de datos y asegurar que los registros en la base de datos reflejen el archivo.
Mi objeto modelo:
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 clase que carga el archivo tiene estos métodos:
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()
Esto me funciona en Django 1.8
def clean(self):
if self.cleaned_data[''name''] != self.initial[''name'']:
# Do something
He extendido la mezcla de @livskiy de la siguiente manera:
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
y el DictField es:
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)
se puede usar extendiéndolo en sus modelos; se agregará un campo _dict cuando sincronice / migre y ese campo almacenará el estado de sus objetos
La mejor manera es con una señal de pre_save
. Puede que no haya sido una opción en el 2009 cuando se hizo y contestó esta pregunta, pero cualquiera que vea esto hoy debería hacerlo de esta manera:
@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
La solución óptima es probablemente una que no incluya una operación de lectura de base de datos adicional antes de guardar la instancia del modelo, ni ninguna otra biblioteca de django. Es por esto que las soluciones de laffuste son preferibles. En el contexto de un sitio de administración, uno simplemente puede anular el método save_model, e invocar el método has_changed del formulario allí, como en la respuesta anterior de Sion. Llegas a algo como esto, dibujando en la configuración de ejemplo de Sion pero usando "changed_data" para obtener todos los cambios posibles:
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)
- Sobrescribir save_model:
https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model
- Método modificado de datos_datos para un campo:
https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data
Llego un poco tarde a la fiesta pero también encontré esta solución: Django Dirty Fields
Mejorando la respuesta de @josh para todos los campos:
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)
solo para aclarar, getattr trabaja para obtener campos como person.name
con cadenas (es decir, getattr(person, "name")
Otra respuesta tardía, pero si solo está intentando ver si se ha cargado un nuevo archivo en un campo de archivo, intente esto: (adaptado del comentario de Christopher Adams en el enlace http://zmsmith.com/2010/05/django-check-if-a-field-has-changed/ en el comentario de zach aquí
Enlace actualizado: 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
Puede usar django-model-changes para hacer esto sin una búsqueda de base de datos adicional:
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
Si bien esto no responde realmente a tu pregunta, trataría esto de una manera diferente.
Simplemente borre el campo remote_image
después de guardar con éxito la copia local. Luego, en su método de guardar, siempre puede actualizar la imagen siempre que remote_image
no esté vacío.
Si desea mantener una referencia a la url, puede usar un campo booleano no editable para manejar la bandera de almacenamiento en caché en lugar del campo remote_image
.
Si está utilizando un formulario, puede usar los datos modificados de Form ( 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
Tenga en cuenta que el seguimiento de cambios de campo está disponible en django-model-utils.
https://django-model-utils.readthedocs.org/en/latest/index.html
Tuve esta situación antes de que mi solución fuera anular el método pre_save()
de la clase de campo de destino, se llamará solo si el campo ha sido cambiado
útil con el ejemplo FileField:
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
desventaja:
no es útil si desea realizar cualquier operación (post_save) como usar el objeto creado en algún trabajo (si ha cambiado cierto campo)
Una modificación a la respuesta 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
Esto usa el método público get_fields
django 1.10 en su lugar. Esto hace que el código sea más seguro en el futuro, pero lo más importante es que también incluye claves externas y campos donde editable = False.
Para referencia, aquí está la implementación 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))
)
Y ahora, para una respuesta directa: una forma de verificar si el valor del campo ha cambiado es obtener datos originales de la base de datos antes de guardar la instancia. Considera este ejemplo:
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)
Lo mismo se aplica cuando se trabaja con un formulario. Puede detectarlo en el método de limpieza o guardado de 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 = []
Yo uso el siguiente mixin:
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])
Uso:
>>> 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)
>>>
Nota
Tenga en cuenta que esta solución funciona bien solo en el contexto de la solicitud actual. Por lo tanto, es adecuado principalmente para casos simples. En un entorno concurrente donde varias solicitudes pueden manipular la misma instancia de modelo al mismo tiempo, definitivamente necesita un enfoque diferente.
como una extensión de la respuesta de SmileyChris, puede agregar un campo de fecha y hora al modelo para last_updated, y establecer algún tipo de límite para la edad máxima a la que podrá acceder antes de verificar un cambio