django-model-utils
django-model-utils copied to clipboard
The relationship between FieldTracker and post_save signal is not clearly documented
Problem
According to the docs, tracker.previous "returns the value of the given field during the last save"
This implies that a call to tracker.previous within a post_save signal would return the current value of the field, as the "last save" has just happened.
However, this does not seem to be the case. A call to tracker.previous within a post_save returns the value of the field before the last save. If you then call it again after the post_save signal, it has updated to the latest value.
From my understanding, the order seems to be as follows: save post_save signal tracker updates values
If someone could confirm that this is how it works, that would be much appreciated. It would then be great to have this in the docs, as it seems to be a point of confusion on stack overflow threads.
Environment
- Django Model Utils version: 3.0.0
- Django version: 1.11.1
- Python version: 3.6.0
- Other libraries used, if any: boto3==1.4.4 botocore==1.5.58 certifi==2017.4.17 chardet==3.0.4 Django==1.11.1 django-extensions==1.7.9 django-model-utils==3.0.0 django-storages==1.5.2 djangorestframework==3.6.3 docutils==0.13.1 et-xmlfile==1.0.1 idna==2.5 jdcal==1.3 jmespath==0.9.3 openpyxl==2.4.8 psycopg2==2.7.1 python-dateutil==2.6.0 pytz==2017.2 s3transfer==0.1.10 six==1.10.0 urllib3==1.21.1
Code examples
Give code example that demonstrates the issue, or even better, write new tests that fails because of that issue.
models.py:
class MyModel(models.Model): name = models.CharField(max_length=64) tracker = FieldTracker()
signals.py:
@receiver(signals.post_save, sender=MyModel) def log_mymodel_name(sender, instance, **kwargs): logging.info('Previous name: {0}, Current name: {1}'.format(instance.tracker.previous('name'), instance.name))
python shell:
m = MyModel(name='A') m.save()
logs -> 2017-06-27 08:45:51,108 - INFO - Previous name: None, Current name: A
m.name = 'B' m.save()
logs -> 2017-06-27 09:15:57,172 - INFO - Previous name: A, Current name: B
m.save()
logs -> 2017-06-27 09:17:09,593 - INFO - Previous name: B, Current name: B
Hope this makes sense!
Before saying how field tracker works, let's see how Django internally handles pre-save and post-save (I recently learned this in a related issue)
def save(self, *args, **kwargs)
print "Before Model Save"
// Super call
print "After Model Save"
Now let's add one pre-save (which print Pre-save) and one post-save (which prints Post-save) signal to this model.
Now, when we execute the save method, this would be the order of execution:
Before Model Save
Pre-save
Post-save
After Model Save
This means, Django first fires all post-save signals and then execute rest of Save function! (An interesting implication was covered in Pickle-Issue 83)
Now, with this understanding let's see where field update actually takes place.
Looking for relevant code for Tracker, here it is:
def patch_save(self, instance):
original_save = instance.save
def save(**kwargs):
ret = original_save(**kwargs)
// do_some_updates
return ret
instance.save = save
So, yes, it looks like Field Tracker updates save method after it has executed complete save function. so, if we add check has_changed(field) to all four instances, all four instances will return True (of course, when there has been relevant change)
Thank you for your reply. It is really helpful to know how Django handles pre-save and post-saves on the model, alongside pre_save and post_save signals. It is useful to see those four instances all returning True for tracker.has_changed().
Given that we agree that Field Tracker is updating its tracker.previous property only after the post_save signal, the current explanation that tracker.previous "Returns the value of the given field during the last save" is misleading in the context of post_save signals.
Perhaps a simple addition to the docs stating that the value of tracker.previous is updated after post_save signals are called would suffice.
An example to illustrate:
@receiver(signals.post_save, sender=MyModel) def MyModel_post_save(sender, instance, **kwargs): print("post_save - tracker.previous('name'): {}".format(instance.tracker.previous('name')))
>>> m = MyModel(name='foo')
>>> m.save()
post_save - tracker.previous('name') = None
>>> m.tracker.previous('name')
'foo'
See #404 for some details of FieldTracker implementation
I agree, I think this needs to be added to the documentation. I found myself scrounging around in the codebase after realizing that the behavior was absolutely different between handling the tracker in post_save signals and outside of them.
Since it has not been mentioned explicitly, the way to access the updated fields using FieldTracker works like so:
changed_fields = instance.tracker.changed() updated_values = { field: getattr(instance, field) for field in changed_fields }
This uses FieldTracker to get the fields that were updated and since the instance is already updated, we can directly access the updated values from there.