I have an abstract model that keeps an on-disk cache. When I delete the model, I need it to delete the cache. I want this to happen for every derived model as well.
If I connect the signal specifying the abstract model, this does not propagate to the derived models:
pre_delete.connect(clear_cache, sender=MyAbstractModel, weak=False)
If I try to connect the signal in an init, where I can get the derived class name, it works, but I'm afraid it will attempt to clear the cache as many times as I've initialized a derived model, not just once.
Where should I connect the signal?
Building upon Justin Abrahms' answer, I've created a custom manager that binds a post_save signal to every child of a class, be it abstract or not.
This is some one-off, poorly tested code and is therefore provided with no warranties! It seems to works, though.
In this example, we allow an abstract model to define CachedModelManager as a manager, which then extends basic caching functionality to the model and its children. It allows you to define a list of volatile keys (a class attribute called volatile_cache_keys) that should be deleted upon every save (hence the post_save signal) and adds a couple of helper functions to generate cache keys, as well as retrieving, setting and deleting keys.
This of course assumes you have a cache backend setup and working properly.
# helperapp\models.py
# -*- coding: UTF-8
from django.db import models
from django.core.cache import cache
class CachedModelManager(models.Manager):
def contribute_to_class(self, model, name):
super(CachedModelManager, self).contribute_to_class(model, name)
setattr(model, 'volatile_cache_keys',
getattr(model, 'volatile_cache_keys', []))
setattr(model, 'cache_key', getattr(model, 'cache_key', cache_key))
setattr(model, 'get_cache', getattr(model, 'get_cache', get_cache))
setattr(model, 'set_cache', getattr(model, 'set_cache', set_cache))
setattr(model, 'del_cache', getattr(model, 'del_cache', del_cache))
self._bind_flush_signal(model)
def _bind_flush_signal(self, model):
models.signals.post_save.connect(flush_volatile_keys, model)
def flush_volatile_keys(sender, **kwargs):
instance = kwargs.pop('instance', False)
for key in instance.volatile_cache_keys:
instance.del_cache(key)
def cache_key(instance, key):
if not instance.pk:
name = "%s.%s" % (instance._meta.app_label, instance._meta.module_name)
raise models.ObjectDoesNotExist("Can't generate a cache key for " +
"this instance of '%s' " % name +
"before defining a primary key.")
else:
return "%s.%s.%s.%s" % (instance._meta.app_label,
instance._meta.module_name,
instance.pk, key)
def get_cache(instance, key):
result = cache.get(instance.cache_key(key))
return result
def set_cache(instance, key, value, timeout=60*60*24*3):
result = cache.set(instance.cache_key(key), value, timeout)
return result
def del_cache(instance, key):
result = cache.delete(instance.cache_key(key))
return result
# myapp\models.py
from django.contrib.auth.models import User
from django.db import models
from helperapp.models import CachedModelManager
class Abstract(models.Model):
creator = models.ForeignKey(User)
cache = CachedModelManager()
class Meta:
abstract = True
class Community(Abstract):
members = models.ManyToManyField(User)
volatile_cache_keys = ['members_list',]
#property
def members_list(self):
result = self.get_cache('members_list')
if not result:
result = self.members.all()
self.set_cache('members_list', result)
return result
Patches welcome!
I think you can connect to post_delete without specifying sender, and then check if actual sender is in list of model classes. Something like:
def my_handler(sender, **kwargs):
if sender.__class__ in get_models(someapp.models):
...
Obviously you'll need more sophisticated checking etc, but you get the idea.
Create a custom manager for your model. In its contribute_to_classmethod, have it set a signal for class_prepared. This signal calls a function which binds more signals to the model.
Related
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)
I am using django_fsm to manage state in my model. My model looks like:
from django.db import models,
from django_fsm import FSMField, transition
class MyModel(models.Model):
STATES = (
('pending', _('Pending')),
('active', _('Active'))
)
state = FSMField(choices=STATES, default='pending', protected=True)
#transition(field=state, source='pending', target='active')
def change_state(self):
pass
Should I add self.save() to change_state? Will it be called?
If calling change_state() succeeds without raising an exception, the state field will be changed, but not written to the database.
So for making changes into database you need to call obj.save() explicitly
def change_view(request, model_id):
obj = get_object__or_404(MyModel, pk=model_id)
obj.change_state()
obj.save()
return redirect('/')
You can use a post_transition signal to handle this:
from django_fsm.signals import post_transition
#receiver(post_transition, sender=models.MyModel)
def save_new_workflow_state(sender, instance, name, source, target, **kwargs):
""" Save the new workflow state following successful django_fsm transition. """
if source != target:
instance.save()
This comes from this issue.
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 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)