Django-simple-history is inserting new record on each save of target model. In docs the problem with F expressions is described. I try to circumvent this with overriden save method.
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# some other actions
self.refresh_from_db()
But it seems, that this is not working. Is the post_save signal of base model called directly after super().save() call? If so, is there a way to solve this problem keeping the F expression in target model update?
UPDATE: Saved instance has one of its attributes defined using an F expression, so this code is called in some other module:
instance.some_attribute = (F('some_attribute') + 15)
instance.save(update_fields=['some_attribute'])
This throws an error in django-simple-history's post_save signal, when it tries to insert a extended copy of instance to history table. I tried to refresh the instance in overriden save method to get rid of the F expression in some_attribute so the actual value is loaded. From the traceback it seems that the post_save is called right after super().save() call, before the refresh. Is it the way Django post_save with overriden save works? If so, is there a way to not change the update code (leave the update with F expression) and solve the history insert in model's save?
django-simple-history provides signals for before and after the historical record is created: https://django-simple-history.readthedocs.io/en/2.7.0/signals.html
I suggest using these to update the instance before it gets saved to the historical table. Something like this should work:
from django.dispatch import receiver
from simple_history.signals import (
pre_create_historical_record,
post_create_historical_record
)
#receiver(pre_create_historical_record)
def pre_create_historical_record_callback(sender, **kwargs):
instance = kwargs["instance"]
history_instance = kwargs["history_instance"]
if isinstance(instance, ModelYouWantToRefresh)
instance.refresh_from_db()
history_instance.some_attribute = instance.some_attribute
Based on Ross Mechanic answer I made a universal solution
from django import dispatch
from django.db.models import expressions
from simple_history import signals
#dispatch.receiver(
signals.pre_create_historical_record, dispatch_uid="simple_history_refresh"
)
def remove_f_expressions(sender, instance, history_instance, **kwargs) -> None: # noqa
f_expression_fields = []
for field in history_instance._meta.fields: # noqa
field_value = getattr(history_instance, field.name)
if isinstance(field_value, expressions.BaseExpression):
f_expression_fields.append(field.name)
if f_expression_fields:
instance.refresh_from_db()
for field_name in f_expression_fields:
field_value = getattr(instance, field_name)
setattr(history_instance, field_name, field_value)
Related
I am using django_extensions TimeStampedModel, which provides a modified field that sets itself via a pre_save event. Which is great, except I am converting an old schema and want to preserve the original modified datestamp. How can I monkeypatch, avoid, cancel, or replace the pre_save'd modified with another value?
In the end, I just did an end-around:
from django.db import connection
cursor = connection.cursor()
cursor.execute("update %s set modified='%s' where id=%s" % (
my_model._meta.db_table, desired_modified_date, my_model.id))
You cannot. Not in the sense you're asking.
What you can is create a fake field and populate it on clean().
Class MyModel(models.Model):
def clean(self):
self._modified = self.modified
...
#receiver(pre_save, sender=MyModel)
def receiver_(self, *args, **kwargs):
self.modified = self._modified
So you're backing up the field value and putting it back later. notes: ensure your application is loaded later.
I have a model that saves an Excursion. The user can change this excursion, but I need to know what the excursion was before he change it, because I keep track of how many "bookings" are made per excursion, and if you change your excursion, I need to remove one booking from the previous excursion.
Im not entirely sure how this should be done.
Im guessing you use a signal for this?
Should I use pre_save, pre_init or what would be the best for this?
pre_save is not the correct one it seems, as it prints the new values, not the "old value" as I expected
#receiver(pre_save, sender=Delegate)
def my_callback(sender, instance, *args, **kwargs):
print instance.excursion
Do you have several options.
First one is to overwrite save method:
#Delegate
def save(self, *args, **kwargs):
if self.pk:
previous_excursion = Delegate.objects.get(self.pk).excursion
super(Model, self).save(*args, **kwargs)
if self.pk and self.excursion != previous_excursion:
#change booking
Second one is binding function to post save signal + django model utils field tracker:
#receiver(post_save, sender=Delegate)
def create_change_booking(sender,instance, signal, created, **kwargs):
if created:
previous_excursion = get it from django model utils field tracker
#change booking
And another solution is in pre_save as you are running:
#receiver(pre_save, sender=Delegate)
def my_callback(sender, instance, *args, **kwargs):
previous_excursion = Delegate.objects.get(self.pk).excursion
if instance.pk and instance.excursion != previous_excursion:
#change booking
You can use django model utils to track django model fields. check this example.
pip install django-model-utils
Then you can define your model and use fieldtracker in your model .
from django.db import models
from model_utils import FieldTracker
class Post(models.Model):
title = models.CharField(max_length=100)
body = models.TextField()
tracker = FieldTracker()
status = models.CharField(choices=STATUS, default=STATUS.draft, max_length=20)
after that in post save you can use like this :
#receiver(post_save, sender=Post)
def my_callback(sender, instance,*args, **kwargs):
print (instance.title)
print (instance.tracker.previous('title'))
print (instance.status)
print (instance.tracker.previous('status'))
This will help you a lot to do activity on status change. as because overwrite save method is not good idea.
As an alternative and if you are using Django forms:
The to-be version of your instance is stored in form.instance of the Django form of your model. On save, validations are run and this new version is applied to the model and then the model is saved.
Meaning that you can check differences between the new and the old version by comparing form.instance to the current model.
This is what happens when the Django Admin's save_model method is called. (See contrib/admin/options.py)
If you can make use of Django forms, this is the most Djangothic way to go, I'd say.
This is the essence on using the Django form for handling data changes:
form = ModelForm(request.POST, request.FILES, instance=obj)
new_object = form.instance # not saved yet
# changes are stored in form.changed_data
new_saved_object = form.save()
form.changed_data will contain the changed fields which means that it is empty if there are no changes.
There's yet another option:
Django's documentation has an example showing exactly how you could do this by overriding model methods.
In short:
override Model.from_db() to add a dynamic attribute containing the original values
override the Model.save() method to compare the new values against the originals
This has the advantage that it does not require an additional database query.
I am curious if there is a way to see what has changed on an object after saving it using the Django Rest Framework. I have some special behavior I need to check if a field has been changed from its original value that I was hoping to handle using the post_save on generics.RetrieveUpdateDestroyAPIView.
My first thought was to check using pre_save but it seems that pre_save's object argument already has the changes applied to it.
OLD ANSWER for django rest framework version 2.3.12:
To check if anything has changed on update, you will have to compare the unchanged model instance which is self.object with the changed model instance which is serializer.object.
The object argument which is passed to the pre_save method is the serializer.object which is not yet saved in the database with the new changes.
The unchanged model instance is the self.object which has been fetched from the database using self.get_object_or_none(). Compare it with the obj argument in the pre_save method.
def pre_save(self,obj):
unchanged_instance = self.object
changed_instance = obj
..... # comparison code
NEW ANSWER for django rest framework 3.3:
pre_save and post_save are no longer valid.
Now you can place any pre save or post save logic in perform_update method. For example:
def perform_update(self, serializer):
# NOTE: serializer.instance gets updated after calling save
# if you want to use the old_obj after saving the serializer you should
# use self.get_object() to get the old instance.
# other wise serializer.instance would do fine
old_obj = self.get_object()
new_data_dict = serializer.validated_data
# pre save logic
if old_obj.name != new_data_dict['name']:
do_something
.....
new_obj = serializer.save()
# post save logic
......
I was able to do this with help from model_utils FieldTracker. You can install a tracker on the relevant model, then in pre_save (by post_save it's too late) you can do this:
def pre_save(self, obj):
if hasattr(obj, 'tracker'):
self.changed_fields = obj.tracker.changed()
else:
self.changed_fields = None
changed_fields will look like this: {'is_public': False, 'desc': None}
I have a situation where when one of my models is saved MyModel I want to check a field, and trigger the same change in any other Model with the same some_key.
The code works fine, but its recursively calling the signals. As a result I am wasting CPU/DB/API calls. I basically want to bypass the signals during the .save(). Any suggestions?
class MyModel(models.Model):
#bah
some_field = #
some_key = #
#in package code __init__.py
#receiver(models_.post_save_for, sender=MyModel)
def my_model_post_processing(sender, **kwargs):
# do some unrelated logic...
logic = 'fun! '
#if something has changed... update any other field with the same id
cascade_update = MyModel.exclude(id=sender.id).filter(some_key=sender.some_key)
for c in cascade_update:
c.some_field = sender.some_field
c.save()
Disconnect the signal before calling save and then reconnect it afterwards:
post_save.disconnect(my_receiver_function, sender=MyModel)
instance.save()
post_save.connect(my_receiver_function, sender=MyModel)
Disconnecting a signal is not a DRY and consistent solution, such as using update() instead of save().
To bypass signal firing on your model, a simple way to go is to set an attribute on the current instance to prevent upcoming signals firing.
This can be done using a simple decorator that checks if the given instance has the 'skip_signal' attribute, and if so prevents the method from being called:
from functools import wraps
def skip_signal(signal_func):
#wraps(signal_func)
def _decorator(sender, instance, **kwargs):
if hasattr(instance, 'skip_signal'):
return None
return signal_func(sender, instance, **kwargs)
return _decorator
Based on your example, that gives us:
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=MyModel)
#skip_signal()
def my_model_post_save(sender, instance, **kwargs):
instance.some_field = my_value
# Here we flag the instance with 'skip_signal'
# and my_model_post_save won't be called again
# thanks to our decorator, avoiding any signal recursion
instance.skip_signal = True
instance.save()
Hope This helps.
A solution may be use update() method to bypass signal:
cascade_update = MyModel.exclude(
id=sender.id).filter(
some_key=sender.some_key).update(
some_field = sender.some_field )
"Be aware that the update() method is converted directly to an SQL statement. It is a bulk operation for direct updates. It doesn't run any save() methods on your models, or emit the pre_save or post_save signals"
You could move related objects update code into MyModel.save method. No playing with signal is needed then:
class MyModel(models.Model):
some_field = #
some_key = #
def save(self, *args, **kwargs):
super(MyModel, self).save(*args, **kwargs)
for c in MyModel.objects.exclude(id=self.id).filter(some_key=self.some_key):
c.some_field = self.some_field
c.save()
Here's the situation:
Let's say I have a model A in django. When I'm saving an object (of class A) I need to save it's fields into all other objects of this class. I mean I need every other A object to be copy of lat saved one.
When I use signals (post-save for example) I get a recursion (objects try to save each other I guess) and my python dies.
I men I expected that using .save() method on the same class in pre/post-save signal would cause a recursion but just don't know how to avoid it.
What do we do?
#ShawnFumo Disconnecting a signal is dangerous if the same model is saved elsewhere at the same time, don't do that !
#Aram Dulyan, your solution works but prevent you from using signals which are so powerful !
If you want to avoid recursion and keep using signals (), a simple way to go is to set an attribute on the current instance to prevent upcoming signals firing.
This can be done using a simple decorator that checks if the given instance has the 'skip_signal' attribute, and if so prevents the method from being called:
from functools import wraps
def skip_signal():
def _skip_signal(signal_func):
#wraps(signal_func)
def _decorator(sender, instance, **kwargs):
if hasattr(instance, 'skip_signal'):
return None
return signal_func(sender, instance, **kwargs)
return _decorator
return _skip_signal
We can now use it this way:
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=MyModel)
#skip_signal()
def my_model_post_save(sender, instance, **kwargs):
# you processing
pass
m = MyModel()
# Here we flag the instance with 'skip_signal'
# and my_model_post_save won't be called
# thanks to our decorator, avoiding any signal recursion
m.skip_signal = True
m.save()
Hope This helps.
This will work:
class YourModel(models.Model):
name = models.CharField(max_length=50)
def save_dupe(self):
super(YourModel, self).save()
def save(self, *args, **kwargs):
super(YourModel, self).save(*args, **kwargs)
for model in YourModel.objects.exclude(pk=self.pk):
model.name = self.name
# Repeat the above for all your other fields
model.save_dupe()
If you have a lot of fields, you'll probably want to iterate over them when copying them to the other model. I'll leave that to you.
Another way to handle this is to remove the listener while saving. So:
class Foo(models.Model):
...
def foo_post_save(instance):
post_save.disconnect(foo_post_save, sender=Foo)
do_stuff_toSaved_instance(instance)
instance.save()
post_save.connect(foo_post_save, sender=Foo)
post_save.connect(foo_post_save, sender=Foo)