Django: podczas zapisywania, jak możesz sprawdzić, czy Pole się zmieniło?
W moim modelu mam:
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
Który działa świetnie po raz pierwszy remote_image
zmiany.
Jak mogę pobrać nowy obrazek, gdy ktoś zmodyfikował remote_image
na aliasie? A po drugie, czy istnieje lepszy sposób na buforowanie zdalnego obrazu?
22 answers
Choć jest trochę późno, pozwól, że wyrzucę To rozwiązanie dla innych, którzy natkną się na ten post. Zasadniczo, chcesz nadpisać metodę __init__
z models.Model
, aby zachować kopię oryginalnej wartości. To sprawia, że nie musisz wykonywać kolejnego wyszukiwania DB (co zawsze jest dobre).
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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-11-19 21:51:16
Używam następującej 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])
Użycie:
>>> 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)
>>>
Uwaga
Należy pamiętać, że to rozwiązanie działa dobrze tylko w kontekście bieżącego żądania. Dlatego nadaje się przede wszystkim do prostych przypadków. W środowisku równoległym, w którym wiele żądań może manipulować tą samą instancją modelu w tym samym czasie, zdecydowanie potrzebujesz innego podejścia.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-17 04:37:43
A teraz odpowiedź bezpośrednia: jednym ze sposobów sprawdzenia, czy wartość pola się zmieniła, jest pobranie oryginalnych danych z bazy danych przed zapisaniem instancji. Rozważ ten przykład:
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)
To samo dotyczy pracy z formularzem. Można go wykryć za pomocą metody clean lub save Modelu:
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 = []
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-09 22:20:07
Najlepszym sposobem jest sygnał pre_save
. Może nie było opcji w 2009, kiedy to pytanie zostało zadane i odpowiedzi, ale każdy, kto dziś to zobaczy, powinien zrobić to w ten sposób: {]}
@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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-11 14:51:58
Od wydania Django 1.8, możesz użyć from_db classmethod do buforowania starej wartości remote_image. Następnie w metodzie save można porównać starą i nową wartość pola, aby sprawdzić, czy wartość się zmieniła.
@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!
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-09 01:06:43
Zauważ, że śledzenie zmian pól jest dostępne w django-model-utils.
Https://django-model-utils.readthedocs.org/en/latest/index.html
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-24 21:14:25
Jeśli używasz formularza, możesz użyć formularza 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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-02 02:10:53
Od Django 1.8 istnieje metoda from_db
, jak wspomina Serge. W rzeczywistości dokumenty Django zawierają ten konkretny przypadek użycia jako przykład:
Https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading
Poniżej znajduje się przykład pokazujący jak zapisywać początkowe wartości pól załadowanych z bazy danych
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-16 11:07:36
Trochę się spóźniłem na imprezę, ale znalazłem też takie rozwiązanie: Django Dirty Fields
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-19 07:37:01
Możesz użyć django-model-changes aby to zrobić bez dodatkowego przeszukiwania bazy danych:
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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-14 14:50:58
Optymalnym rozwiązaniem jest prawdopodobnie takie, które nie zawiera dodatkowej operacji odczytu bazy danych przed zapisaniem instancji modelu, ani żadnej innej biblioteki django. Dlatego preferowane są rozwiązania laffuste. W kontekście strony administratora, można po prostu nadpisać metodę save_model i wywołać tam metodę has_changed formularza, tak jak w odpowiedzi Sion powyżej. Dochodzimy do czegoś takiego, czerpiąc z przykładowego ustawienia Sion, ale używając "changed_data", aby uzyskać wszystkie możliwe zmiana:
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)
- Zastąp save_model:
Https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model
- Wbudowana metoda changed_data dla pola:
Https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-04 10:36:50
Chociaż to nie odpowiada na twoje pytanie, potraktowałbym to inaczej.
Po pomyślnym zapisaniu kopii lokalnej wyczyść pole remote_image
. Następnie w swojej metodzie save możesz zawsze zaktualizować obrazek, gdy remote_image
nie jest pusty.
Jeśli chcesz zachować odniesienie do adresu url, możesz użyć nieedytowalnego pola logicznego do obsługi znacznika buforowania, a nie samego pola remote_image
.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-30 23:21:51
Miałem taką sytuację, zanim moim rozwiązaniem było nadpisanie metody pre_save()
klasy pola docelowego, która zostanie wywołana tylko wtedy, gdy pole zostało zmienione
przydatne z FileField
przykład:
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
nie jest to przydatne, jeśli chcesz wykonać dowolną operację (post_save), jak użycie utworzonego obiektu w jakimś zadaniu (jeśli pewne pole się zmieniło)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-07 12:41:35
Poprawa @josh odpowiedzi dla wszystkich pól:
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)
Dla wyjaśnienia, getattr działa, aby uzyskać pola takie jak {[1] } z łańcuchami (np. getattr(person, "name")
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-31 16:11:14
To działa dla mnie w Django 1.8
def clean(self):
if self.cleaned_data['name'] != self.initial['name']:
# Do something
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-05 15:54:33
Kolejna późna odpowiedź, ale jeśli próbujesz sprawdzić, czy nowy plik został przesłany do pola Pliku, spróbuj tego: (zaadaptowane z komentarza Christophera Adamsa na link http://zmsmith.com/2010/05/django-check-if-a-field-has-changed/ in zach ' s comment here)
Zaktualizowany link: 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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-12 14:19:34
Rozszerzyłem mixin @livskiy w następujący sposób:
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
A Dyktafon to:
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)
Można go używać rozszerzając go w swoich modelach pole _dict zostanie dodane podczas synchronizacji/migracji i to pole będzie przechowywać stan Twoich obiektów
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-10 10:47:32
Jako rozszerzenie odpowiedzi SmileyChris, możesz dodać pole datetime do modelu dla last_updated i ustawić jakiś limit dla maksymalnego wieku, na który pozwoli Ci się dostać przed sprawdzeniem zmiany
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-31 14:56:16
Mixin od @ ivanlivski jest świetny.
Rozszerzyłem go na
- upewnij się, że działa z polami dziesiętnymi.
- Expose właściwości w celu uproszczenia użytkowania
Zaktualizowany kod jest dostępny tutaj: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py
Aby pomóc ludziom nowym w Pythonie lub Django, podam bardziej kompletny przykład. Tym szczególnym zastosowaniem jest pobranie pliku od dostawcy danych i zapewnienie rekordy w bazie danych odzwierciedlają plik.
Mój obiekt modelu:
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
Klasa, która ładuje plik ma następujące metody:
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()
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-30 18:20:07
A co powiesz na użycie rozwiązania Davida Cramer ' a:
Http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django/
Odniosłem sukces używając go w ten sposób:
@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!"
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-22 16:57:37
Modyfikacja odpowiedzi @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
Używa zamiast tego publicznej metody django 1.10 get_fields
. To sprawia, że kod jest bardziej przyszłościowy, ale co ważniejsze zawiera również klucze obce i pola, w których editable=False.
Dla odniesienia, oto implementacja .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))
)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-06 21:56:22
Oto inny sposób na zrobienie tego.
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)
Zgodnie z dokumentacją: Walidacja obiektów
" drugim krokiem full_clean () jest wywołanie modelu.clean (). Ta metoda powinna zostać nadpisana, aby wykonać walidację niestandardową w modelu. Ta metoda powinna być używana do walidacji modelu niestandardowego i w razie potrzeby do modyfikowania atrybutów modelu. Można na przykład użyć go do automatycznego podania wartości pola lub do walidacji wymagającej dostępu do więcej niż jedno pole: "
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-24 20:18:55