Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keeping track of changes since the last save in django models

A couple of times I've run into a situation, when at save time I need to know which model fields are going to be updated and act accordingly.

The most obvious solution to this is to take the primary key field and retrieve a copy of the model from the database:

class MyModel(models.Model):

    def save(self, force_insert=False, force_update=False, using=None):
        if self.id is not None:
            unsaved_copy = MyModel.objects.get(id=self.id)
            # Do your comparisons here
        super(MyModel, self).save(force_insert, force_update, using)

That works perfectly fine, however, it hits the database for every instance of the model you are saving (might be quite inconvenient if you are doing a lot of such saves).

It is obvious, that if one can "remember" the old field values at the start of model instance's lifetime (__init__), there should be no need to retrieve a copy of the model from the database. So I came up with this little hack:

class MyModel(models.Model):

    def __init__(self, *args, **kwargs):
        super(MyModel, self).__init__(*args, **kwargs)
        self.unsaved = {}
        for field in self._meta.fields:
            self.unsaved[field.name] = getattr(self, field.name, None)

    def save(self, force_insert=False, force_update=False, using=None):
        for name, value in self.unsaved.iteritems():
            print "Field:%s Old:%s New:%s" % (name, value, getattr(self, name, None))
        # old values can be accessed through the self.unsaved member
        super(MyModel, self).save(force_insert, force_update, using)

This seems to work, however it makes use of the non-public interface of django.db.models.Model.

Perhaps someone knows a cleaner way to do it?

like image 579
shylent Avatar asked Jan 08 '10 18:01

shylent


1 Answers

I think your solution looks reasonable.

Alternatively you could have a Manager method called get_and_copy() (or something) that hung a copy of the original object off of what is returned. You could then use another Manager method, save_and_check() which took advantage of the copied original.

FWIW: If you are playing with contrib/admin templates there is a context variable called original which is a copy of the original object.

Update: I looked more closely at what admin is doing. In class ModelAdmin (located in django/contrib/admin/options.py) there is a method called construct_change_message(). It is being driven by formset.changed_data and formset.changed_objects, so django/forms/models.py class BaseModelFormSet is where the action is. See the method save_existing_objects(). Also look at the method _existing_object(). It's a little more complicated than what I mentioned before because they are dealing with the possibility of multiple objects, but they are basically caching the results of the query set on first access.

like image 181
Peter Rowell Avatar answered Sep 21 '22 12:09

Peter Rowell