it-swarm.com.de

Django: Wie können Sie beim Speichern prüfen, ob sich ein Feld geändert hat?

In meinem Modell habe ich:

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

Was zum ersten Mal gut funktioniert, ändert sich der remote_image

Wie kann ich ein neues Bild abrufen, wenn jemand den remote_image am Alias ​​geändert hat? Und zweitens: Gibt es eine bessere Möglichkeit, ein Remote-Image zwischenzuspeichern?

237
Paul Tarjan

Obwohl es etwas spät ist, lass mich diese Lösung für andere, die auf diesen Beitrag stoßen, wegwerfen. Im Wesentlichen möchten Sie die __init__-Methode von models.Model überschreiben, sodass Sie eine Kopie des ursprünglichen Werts behalten. Dies macht es so, dass Sie keine weitere DB-Suche durchführen müssen (was immer eine gute Sache ist).

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
370
Josh

Ich verwende folgendes 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])

Verwendungszweck:

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

Hinweis

Bitte beachten Sie, dass diese Lösung nur im Zusammenhang mit der aktuellen Anfrage funktioniert. Daher ist es vor allem für einfache Fälle geeignet. In einer gleichzeitigen Umgebung, in der mehrere Anforderungen dieselbe Modellinstanz gleichzeitig bearbeiten können, ist auf jeden Fall ein anderer Ansatz erforderlich.

163
iperelivskiy

Und nun zur direkten Antwort: Eine Möglichkeit, um zu überprüfen, ob sich der Wert für das Feld geändert hat, ist das Abrufen der Originaldaten aus der Datenbank vor dem Speichern der Instanz. Betrachten Sie dieses Beispiel:

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)

Gleiches gilt für die Arbeit mit einem Formular. Sie können es an der Clean- oder Save-Methode einer ModelForm erkennen:

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 = []
130
zgoda

Am besten geht es mit einem pre_save-Signal. War '09 vielleicht keine Option, als diese Frage gestellt und beantwortet wurde, aber wer das heute sieht, sollte dies folgendermaßen tun:

@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
129
Chris Pratt

Seit der Veröffentlichung von Django 1.8 können Sie from_db classmethod verwenden, um den alten Wert von remote_image zwischenzuspeichern. In der Methode save können Sie dann den alten und den neuen Feldwert vergleichen, um zu prüfen, ob sich der Wert geändert hat.

@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!
49
Serge

Beachten Sie, dass die Nachverfolgung von Feldänderungen in Django-Modell-Utils verfügbar ist.

https://Django-model-utils.readthedocs.org/de/latest/index.html

15
Lee Hinde

Wenn Sie ein Formular verwenden, können Sie die changed_data von Form ( docs ) verwenden:

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
13
laffuste

Ich bin etwas spät dran, aber ich habe auch diese Lösung gefunden: Django Dirty Fields

5
Fred Campos

Ab Django 1.8 gibt es die from_db-Methode, wie Serge erwähnt. In der Tat enthalten die Django-Dokumente diesen speziellen Anwendungsfall als Beispiel:

https://docs.djangoproject.com/de/dev/ref/models/instances/#customizing-model-loading

Das folgende Beispiel zeigt, wie die Anfangswerte von Feldern aufgezeichnet werden, die aus der Datenbank geladen werden

5

Sie können Django-model-changes verwenden, um dies ohne zusätzliche Datenbanksuche zu tun:

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
3
Robert Kajic

Die optimale Lösung ist wahrscheinlich eine, die keine zusätzliche Datenbankleseoperation vor dem Speichern der Modellinstanz oder einer weiteren Django-Bibliothek enthält. Deshalb sind die Lösungen von Laffuste vorzuziehen. Im Kontext einer Admin-Site kann man einfach die save_model-Methode überschreiben und dort die has_changed-Methode des Formulars aufrufen, genau wie in der obigen Antwort von Sion. Sie kommen zu etwas wie dem, indem Sie auf Sions Beispieleinstellungen zurückgreifen, aber "changed_data" verwenden, um jede mögliche Änderung zu erhalten:

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)
  • Überschreibe save_model:

https://docs.djangoproject.com/de/1.10/ref/contrib/admin/#Django.contrib.admin.ModelAdmin.save_model

  • Eingebaute veränderte_Datenmethode für ein Feld:

https://docs.djangoproject.com/de/1.10/ref/forms/api/#Django.forms.Form.changed_data

3
user3061675

Das funktioniert für mich in Django 1.8

def clean(self):
    if self.cleaned_data['name'] != self.initial['name']:
        # Do something
2
jhrs21

Ich hatte diese Situation, bevor meine Lösung die pre_save()-Methode der Zielfeldklasse überschrieben hat. Sie wird nur aufgerufen, wenn das Feld geändert wurde 
nützlich bei FileField Beispiel: 

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

nachteil:
nicht nützlich, wenn Sie eine (post_save) -Operation ausführen möchten, z. B. das erstellte Objekt in einem Job verwenden (wenn sich ein bestimmtes Feld geändert hat)

2
MYaser

Eine weitere späte Antwort, aber wenn Sie nur versuchen zu sehen, ob eine neue Datei in ein Dateifeld hochgeladen wurde, versuchen Sie Folgendes: (entsprechend Christopher Adams 'Kommentar zu dem Link http://zmsmith.com/2010/05/Django-check-if-a-field-has-geändert/ in zachs Kommentar hier)

Aktualisierte Verbindung: 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
2
Aaron McMillin

Obwohl dies Ihre Frage eigentlich nicht beantwortet, würde ich das anders angehen.

Löschen Sie einfach das Feld remote_image, nachdem Sie die lokale Kopie erfolgreich gespeichert haben. In Ihrer Speichermethode können Sie das Bild immer aktualisieren, wenn remote_image nicht leer ist.

Wenn Sie einen Verweis auf die URL beibehalten möchten, können Sie ein nicht bearbeitbares boolesches Feld verwenden, um das Caching-Flag und nicht das remote_image-Feld selbst zu behandeln.

2
SmileyChris

verbesserung der @josh-Antwort für alle Bereiche:

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)

um zu klären, funktioniert der getattr so, dass er Felder wie person.name mit Strings erhält (d. h. getattr(person, "name")

2
Hassek

Ich habe das Mixin von @livskiy wie folgt erweitert:

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

und das DictField ist:

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)

es kann verwendet werden, indem Sie es in Ihren Modellen erweitern. _. Ein _dict-Feld wird bei der Synchronisierung/Migration hinzugefügt und dieses Feld speichert den Status Ihrer Objekte

1
MYaser

Eine Änderung an @ ivanperelivskys Antwort:

@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

Hierbei wird stattdessen die öffentliche Methode get_fields von Django 1.10 verwendet. Dies macht den Code zukunftssicherer, aber noch wichtiger sind Fremdschlüssel und Felder, in denen editable = False ist.

Als Referenz dient hier die Implementierung von .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))
    )
0
theicfire

Hier ist eine andere Möglichkeit, dies zu tun.

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)

Laut Dokumentation: Objekte validieren

"Der zweite Schritt, den full_clean () ausführt, ist das Aufrufen von Model.clean (). Diese Methode sollte überschrieben werden, um eine benutzerdefinierte Validierung für Ihr Modell durchzuführen. Diese Methode sollte verwendet werden, um eine benutzerdefinierte Modellvalidierung bereitzustellen und um Attribute in Ihrem zu ändern Wenn gewünscht, können Sie das Modell beispielsweise verwenden, um automatisch einen Wert für ein Feld bereitzustellen oder um eine Validierung durchzuführen, die Zugriff auf mehr als ein einzelnes Feld erfordert: "

0
Gonzalo

als Erweiterung der Antwort von SmileyChris können Sie dem Modell für last_updated ein datetime-Feld hinzufügen und eine Art Grenzwert für das maximale Alter festlegen, auf das Sie zugreifen können, bevor Sie auf Änderungen prüfen

0
Jiaaro

Wenn Sie kein Interesse daran haben, die save-Methode zu überschreiben, können Sie dies tun

  model_fields = [f.name for f in YourModel._meta.get_fields()]
  valid_data = {
        key: new_data[key]
        for key in model_fields
        if key in new_data.keys()
  }

  for (key, value) in valid_data.items():
        if getattr(instance, key) != value:
           print ('Data has changed')

        setattr(instance, key, value)

 instance.save()
0
theTypan

Das Mixin von @ivanlivski ist großartig. 

Ich habe es bis erweitert

  • Stellen Sie sicher, dass es mit Dezimalfeldern funktioniert.
  • Zeigen Sie Eigenschaften an, um die Verwendung zu vereinfachen

Der aktualisierte Code ist hier verfügbar: https://github.com/sknutsonsf/python-contrib/blob/master/src/Django/utils/ModelDiffMixin.py

Um Leuten zu helfen, die sich mit Python oder Django noch nicht auskennen, werde ich ein vollständigeres Beispiel geben. Diese spezielle Verwendung ist, um eine Datei von einem Datenprovider zu übernehmen und sicherzustellen, dass die Datensätze in der Datenbank die Datei wiedergeben. 

Mein Modellobjekt:

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

Die Klasse, die die Datei lädt, verfügt über folgende Methoden:

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()
0
sknutsonsf

Wie wäre es mit der Lösung von David Cramer:

http://cramer.io/2010/12/06/tracking-changes-to-fields-in-Django/

Ich hatte Erfolg mit dieser Anwendung:

@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!"
0
Sion