django – comparing old and new field value before saving
Question:
I have a django model, and I need to compare old and new values of field BEFORE saving.
I’ve tried the save()
inheritance, and pre_save
signal. It was triggered correctly, but I can’t find the list of actually changed fields and can’t compare old and new values. Is there a way? I need it for optimization of pre-save actions.
Thank you!
Answers:
There is very simple django way for doing it.
“Memorise” the values in model init like this:
def __init__(self, *args, **kwargs):
super(MyClass, self).__init__(*args, **kwargs)
self.initial_parametername = self.parametername
---
self.initial_parameternameX = self.parameternameX
Real life example:
At class:
def __init__(self, *args, **kwargs):
super(MyClass, self).__init__(*args, **kwargs)
self.__important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date']
for field in self.__important_fields:
setattr(self, '__original_%s' % field, getattr(self, field))
def has_changed(self):
for field in self.__important_fields:
orig = '__original_%s' % field
if getattr(self, orig) != getattr(self, field):
return True
return False
And then in modelform save method:
def save(self, force_insert=False, force_update=False, commit=True):
# Prep the data
obj = super(MyClassForm, self).save(commit=False)
if obj.has_changed():
# If we're down with commitment, save this shit
if commit:
obj.save(force_insert=True)
return obj
It is better to do this at ModelForm level.
There you get all the Data that you need for comparison in save method:
- self.data : Actual Data passed to the Form.
- self.cleaned_data : Data cleaned after validations, Contains Data eligible to be saved in the Model
- self.changed_data : List of Fields which have changed. This will be empty if nothing has changed
If you want to do this at Model level then you can follow the method specified in Odif’s answer.
Also you can use FieldTracker from django-model-utils for this:
-
Just add tracker field to your model:
tracker = FieldTracker()
-
Now in pre_save and post_save you can use:
instance.tracker.previous('modelfield') # get the previous value
instance.tracker.has_changed('modelfield') # just check if it is changed
Here is an app that gives you access to previous and current value of a field right before model will be saved: django-smartfields
Here is how this problem can be solved in a nice declarative may:
from django.db import models
from smartfields import fields, processors
from smartfields.dependencies import Dependency
class ConditionalProcessor(processors.BaseProcessor):
def process(self, value, stashed_value=None, **kwargs):
if value != stashed_value:
# do any necessary modifications to new value
value = ...
return value
class MyModel(models.Model):
my_field = fields.CharField(max_length=10, dependencies=[
Dependency(processor=ConditionalProcessor())
])
Moreover, this processor will be invoked, only in case that field’s value was replaced
My use case for this was that I needed to set a denormalized value in the model whenever some field changed its value. However, as the field being monitored was a m2m relation, I didn’t want to have to do that DB lookup whenever save was called in order to check whether the denormalized field needed updating. So, instead I wrote this little mixin (using @Odif Yitsaeb’s answer as inspiration) in order to only update the denormalized field when necessary.
class HasChangedMixin(object):
""" this mixin gives subclasses the ability to set fields for which they want to monitor if the field value changes """
monitor_fields = []
def __init__(self, *args, **kwargs):
super(HasChangedMixin, self).__init__(*args, **kwargs)
self.field_trackers = {}
def __setattr__(self, key, value):
super(HasChangedMixin, self).__setattr__(key, value)
if key in self.monitor_fields and key not in self.field_trackers:
self.field_trackers[key] = value
def changed_fields(self):
"""
:return: `list` of `str` the names of all monitor_fields which have changed
"""
changed_fields = []
for field, initial_field_val in self.field_trackers.items():
if getattr(self, field) != initial_field_val:
changed_fields.append(field)
return changed_fields
I agree with Sahil that it is better and easier to do this with ModelForm. However, you would customize the ModelForm’s clean method and perform validation there. In my case, I wanted to prevent updates to a model’s instance if a field on the model is set.
My code looked like this:
from django.forms import ModelForm
class ExampleForm(ModelForm):
def clean(self):
cleaned_data = super(ExampleForm, self).clean()
if self.instance.field:
raise Exception
return cleaned_data
Something like this also works:
class MyModel(models.Model):
my_field = fields.IntegerField()
def save(self, *args, **kwargs):
# Compare old vs new
if self.pk:
obj = MyModel.objects.values('my_value').get(pk=self.pk)
if obj['my_value'] != self.my_value:
# Do stuff...
pass
super().save(*args, **kwargs)
Django’s documentation contains an example showing exactly how to do this:
Django 1.8+ and above (Including Django 2.x and 3.x), there is a from_db
classmethod, which can be used to customize model instance creation when loading from the database.
Note: There is NO additional database query if you use this method.
from django.db import Model
class MyClass(models.Model):
@classmethod
def from_db(cls, db, field_names, values):
instance = super().from_db(db, field_names, values)
# save original values, when model is loaded from database,
# in a separate attribute on the model
instance._loaded_values = dict(zip(field_names, values))
return instance
So now the original values are available in the _loaded_values
attribute on the model. You can access this attribute inside your save
method to check if some value is being updated.
class MyClass(models.Model):
field_1 = models.CharField(max_length=1)
@classmethod
def from_db(cls, db, field_names, values):
...
# use code from above
def save(self, *args, **kwargs):
# check if a new db row is being added
# When this happens the `_loaded_values` attribute will not be available
if not self._state.adding:
# check if field_1 is being updated
if self._loaded_values['field_1'] != self.field_1:
# do something
super().save(*args, **kwargs)
Another way to achieve this is using the post_init
and post_save
signals to store the initial state of the model.
@receiver(models.signals.post_init)
@receiver(models.signals.post_save)
def _set_initial_state(
sender: Type[Any],
instance: Optional[models.Model] = None,
**kwargs: Any,
) -> None:
"""
Store the initial state of the model
"""
if isinstance(instance, MyModel):
instance._initial_state = instance.state
Where state
is the name of a field in MyModel
, with _initial_state
being the initial version, copied when the modal is initialised/saved.
Be aware if state
is a container type (e.g. a dict), you may wish to use deepcopy
as appropriate.
In modern Django, there is a matter of great importance to add to the content of the answer accepted among the above answers. You can fall into an infinite recursion when you use defer
or only
QuerySet API.
__get__()
method of django.db.models.query_utils.DeferredAttribute
calls refresh_from_db()
method of django.db.models.Model
. There is a line db_instance = db_instance_qs.get()
in refresh_from_db()
, and this line calls __init__()
method of the instance recursively.
So, it is necessary to add ensuring that the target attributes are not deferred.
def __init__(self, *args, **kwargs):
super(MyClass, self).__init__(*args, **kwargs)
deferred_fields = self.get_deferred_fields()
important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date']
self.__important_fields = list(filter(lambda x: x not in deferred_fields, important_fields))
for field in self.__important_fields:
setattr(self, '__original_%s' % field, getattr(self, field))
Here is how I do. comparing field ‘state’ for example.
and checking permission against user.
admin.py
def save_model(self, request, obj, form, change):
if change is False:
obj.created_by = request.user
else:
# check if field_1 is being updated
if obj._loaded_values['state'] != obj.state and not request.user.has_perm('mtasks.change_status', obj):
messages.set_level(request, messages.ERROR)
messages.error(request, "You don't have permission to change state")
return
super().save_model(request, obj, form, change)
in models.py
class ClassName
...
@classmethod
def from_db(cls, db, field_names, values):
instance = super().from_db(db, field_names, values)
# save original values, when model is loaded from database,
instance._loaded_values = dict(zip(field_names, values))
return instance
I have a django model, and I need to compare old and new values of field BEFORE saving.
I’ve tried the save()
inheritance, and pre_save
signal. It was triggered correctly, but I can’t find the list of actually changed fields and can’t compare old and new values. Is there a way? I need it for optimization of pre-save actions.
Thank you!
There is very simple django way for doing it.
“Memorise” the values in model init like this:
def __init__(self, *args, **kwargs):
super(MyClass, self).__init__(*args, **kwargs)
self.initial_parametername = self.parametername
---
self.initial_parameternameX = self.parameternameX
Real life example:
At class:
def __init__(self, *args, **kwargs):
super(MyClass, self).__init__(*args, **kwargs)
self.__important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date']
for field in self.__important_fields:
setattr(self, '__original_%s' % field, getattr(self, field))
def has_changed(self):
for field in self.__important_fields:
orig = '__original_%s' % field
if getattr(self, orig) != getattr(self, field):
return True
return False
And then in modelform save method:
def save(self, force_insert=False, force_update=False, commit=True):
# Prep the data
obj = super(MyClassForm, self).save(commit=False)
if obj.has_changed():
# If we're down with commitment, save this shit
if commit:
obj.save(force_insert=True)
return obj
It is better to do this at ModelForm level.
There you get all the Data that you need for comparison in save method:
- self.data : Actual Data passed to the Form.
- self.cleaned_data : Data cleaned after validations, Contains Data eligible to be saved in the Model
- self.changed_data : List of Fields which have changed. This will be empty if nothing has changed
If you want to do this at Model level then you can follow the method specified in Odif’s answer.
Also you can use FieldTracker from django-model-utils for this:
-
Just add tracker field to your model:
tracker = FieldTracker()
-
Now in pre_save and post_save you can use:
instance.tracker.previous('modelfield') # get the previous value instance.tracker.has_changed('modelfield') # just check if it is changed
Here is an app that gives you access to previous and current value of a field right before model will be saved: django-smartfields
Here is how this problem can be solved in a nice declarative may:
from django.db import models
from smartfields import fields, processors
from smartfields.dependencies import Dependency
class ConditionalProcessor(processors.BaseProcessor):
def process(self, value, stashed_value=None, **kwargs):
if value != stashed_value:
# do any necessary modifications to new value
value = ...
return value
class MyModel(models.Model):
my_field = fields.CharField(max_length=10, dependencies=[
Dependency(processor=ConditionalProcessor())
])
Moreover, this processor will be invoked, only in case that field’s value was replaced
My use case for this was that I needed to set a denormalized value in the model whenever some field changed its value. However, as the field being monitored was a m2m relation, I didn’t want to have to do that DB lookup whenever save was called in order to check whether the denormalized field needed updating. So, instead I wrote this little mixin (using @Odif Yitsaeb’s answer as inspiration) in order to only update the denormalized field when necessary.
class HasChangedMixin(object):
""" this mixin gives subclasses the ability to set fields for which they want to monitor if the field value changes """
monitor_fields = []
def __init__(self, *args, **kwargs):
super(HasChangedMixin, self).__init__(*args, **kwargs)
self.field_trackers = {}
def __setattr__(self, key, value):
super(HasChangedMixin, self).__setattr__(key, value)
if key in self.monitor_fields and key not in self.field_trackers:
self.field_trackers[key] = value
def changed_fields(self):
"""
:return: `list` of `str` the names of all monitor_fields which have changed
"""
changed_fields = []
for field, initial_field_val in self.field_trackers.items():
if getattr(self, field) != initial_field_val:
changed_fields.append(field)
return changed_fields
I agree with Sahil that it is better and easier to do this with ModelForm. However, you would customize the ModelForm’s clean method and perform validation there. In my case, I wanted to prevent updates to a model’s instance if a field on the model is set.
My code looked like this:
from django.forms import ModelForm
class ExampleForm(ModelForm):
def clean(self):
cleaned_data = super(ExampleForm, self).clean()
if self.instance.field:
raise Exception
return cleaned_data
Something like this also works:
class MyModel(models.Model):
my_field = fields.IntegerField()
def save(self, *args, **kwargs):
# Compare old vs new
if self.pk:
obj = MyModel.objects.values('my_value').get(pk=self.pk)
if obj['my_value'] != self.my_value:
# Do stuff...
pass
super().save(*args, **kwargs)
Django’s documentation contains an example showing exactly how to do this:
Django 1.8+ and above (Including Django 2.x and 3.x), there is a from_db
classmethod, which can be used to customize model instance creation when loading from the database.
Note: There is NO additional database query if you use this method.
from django.db import Model
class MyClass(models.Model):
@classmethod
def from_db(cls, db, field_names, values):
instance = super().from_db(db, field_names, values)
# save original values, when model is loaded from database,
# in a separate attribute on the model
instance._loaded_values = dict(zip(field_names, values))
return instance
So now the original values are available in the _loaded_values
attribute on the model. You can access this attribute inside your save
method to check if some value is being updated.
class MyClass(models.Model):
field_1 = models.CharField(max_length=1)
@classmethod
def from_db(cls, db, field_names, values):
...
# use code from above
def save(self, *args, **kwargs):
# check if a new db row is being added
# When this happens the `_loaded_values` attribute will not be available
if not self._state.adding:
# check if field_1 is being updated
if self._loaded_values['field_1'] != self.field_1:
# do something
super().save(*args, **kwargs)
Another way to achieve this is using the post_init
and post_save
signals to store the initial state of the model.
@receiver(models.signals.post_init)
@receiver(models.signals.post_save)
def _set_initial_state(
sender: Type[Any],
instance: Optional[models.Model] = None,
**kwargs: Any,
) -> None:
"""
Store the initial state of the model
"""
if isinstance(instance, MyModel):
instance._initial_state = instance.state
Where state
is the name of a field in MyModel
, with _initial_state
being the initial version, copied when the modal is initialised/saved.
Be aware if state
is a container type (e.g. a dict), you may wish to use deepcopy
as appropriate.
In modern Django, there is a matter of great importance to add to the content of the answer accepted among the above answers. You can fall into an infinite recursion when you use defer
or only
QuerySet API.
__get__()
method of django.db.models.query_utils.DeferredAttribute
calls refresh_from_db()
method of django.db.models.Model
. There is a line db_instance = db_instance_qs.get()
in refresh_from_db()
, and this line calls __init__()
method of the instance recursively.
So, it is necessary to add ensuring that the target attributes are not deferred.
def __init__(self, *args, **kwargs):
super(MyClass, self).__init__(*args, **kwargs)
deferred_fields = self.get_deferred_fields()
important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date']
self.__important_fields = list(filter(lambda x: x not in deferred_fields, important_fields))
for field in self.__important_fields:
setattr(self, '__original_%s' % field, getattr(self, field))
Here is how I do. comparing field ‘state’ for example.
and checking permission against user.
admin.py
def save_model(self, request, obj, form, change):
if change is False:
obj.created_by = request.user
else:
# check if field_1 is being updated
if obj._loaded_values['state'] != obj.state and not request.user.has_perm('mtasks.change_status', obj):
messages.set_level(request, messages.ERROR)
messages.error(request, "You don't have permission to change state")
return
super().save_model(request, obj, form, change)
in models.py
class ClassName
...
@classmethod
def from_db(cls, db, field_names, values):
instance = super().from_db(db, field_names, values)
# save original values, when model is loaded from database,
instance._loaded_values = dict(zip(field_names, values))
return instance