django: recursion using post-save signal - django

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)

Related

Django.models custom blank value

thanks for tanking the time to look at this query.
I'm setting an ID field within one of my Django models. This is a CharField and looks like the following:
my_id = models.CharField(primary_key=True, max_length=5,
validators=[RegexValidator(
regex=ID_REGEX,
message=ID_ERR_MSG,
code=ID_ERR_CODE
)])
I would like to add a default/blank or null option that calls a global or class function that will cycle through the existing IDs, find the first one that doesn't exist and assign it as the next user ID. However, when I add the call blank=foo() I get an error code that the function doesn't exist.
Best,
pb
Edit1: I also tried using a separate utils file and importing the function, but (unsurprisingly) I get a circular import error as I need the call the class to get the objects.
Edit2 (Reply to Eugene): Tried that, solved the circular import but I'm getting the following error:
TypeError: super(type, obj): obj must be an instance or subtype of type
Previously my override of the save function worked perfectly:
def save(self, *args, **kwargs):
self.full_clean()
super(Staff, self).save(*args, **kwargs)
The custom id function:
def get_id_default():
from .models import MyObj
for temp_id in range(10_000, 100_000):
try:
MyObj.objects.get(my_id=str(temp_id))
except ObjectDoesNotExist:
break # Id doesn't exist
return str(hive_id)
Edit 3 (Reply to PersonPr7): Unfortunately, the kwargs doesn't seem to have my id in it. Actually, after having a print the kwargs dictionary comes back empty.
Save function:
def save(self, *args, **kwargs):
print(kwargs) # --> Returns {}
if kwargs["my_id"] is None:
kwargs["my_id"] = self.get_id_default()
self.full_clean()
super(Staff, self).save(*args, **kwargs)
Where the get_id_default is a class function:
def get_id_default(self):
for temp_id in range(10_000, 100000):
try:
self.objects.get(my_id=str(temp_id))
except ObjectDoesNotExist:
break # Id doesn't exist
return str(temp_id)
Solution1:
For those who are may be struggling with this in the future:
Create a utils/script .py file (or whatever you wanna call it) and create your custom script inside.
from .models import MyModel
def my_custom_default:
# your custom code
return your_value
Inside the main.models.py file.
from django.db import models
from .my_utils import my_custom_default
class MyModel(model.Model):
my_field = models.SomeField(..., default=my_custom_default)
Solution2: Create a static function within your Model class that will create your default value.
#staticmethod
def get_my_default():
# your logic
return your_value
# NOTE: Initially I had the function use self
# to retrieve the objects (self.objects.get(...))
# However, this raised an exception: AttributeError:
# Manager isn't accessible via Sites instances
When setting up your model give your field some kind of default i.e. default=None
Additionally, you need to override the models save function like so:
def save(self, *args, **kwargs):
if self.your_field is None:
self.my_field = self.get_my_default()
self.full_clean()
super().save(*args, **kwargs)
Try overriding the Model's save method and performing the logic there:
def save(self, *args, **kwargs):
#Custom logic
super().save(*args, **kwargs)
Edit:
You don't need to use **kwargs.
You can access your whole model from the save method and loop over objects / ids.

Model there should only be one instance of [duplicate]

I would like to control some configuration settings for my project using a database model. For example:
class JuicerBaseSettings(models.Model):
max_rpm = model.IntegerField(default=10)
min_rpm = model.IntegerField(default=0)
There should only be one instance of this model:
juicer_base = JuicerBaseSettings()
juicer_base.save()
Of course, if someone accidentally creates a new instances, it's not the end of the world. I could just do JuicerBaseSettings.objects.all().first(). However, is there a way to lock it down such that it's impossible to create more than 1 instance?
I found two related questions on SO. This answer suggests using 3rd party apps like django-singletons, which doesn't seem to be actively maintained (last update to the git repo is 5 years ago). Another answer suggests using a combination of either permissions or OneToOneField. Both answers are from 2010-2011.
Given that Django has changed a lot since then, are there any standard ways to solve this problem? Or should I just use .first() and accept that there may be duplicates?
You can override save method to control number of instances:
class JuicerBaseSettings(models.Model):
def save(self, *args, **kwargs):
if not self.pk and JuicerBaseSettings.objects.exists():
# if you'll not check for self.pk
# then error will also raised in update of exists model
raise ValidationError('There is can be only one JuicerBaseSettings instance')
return super(JuicerBaseSettings, self).save(*args, **kwargs)
Either you can override save and create a class function JuicerBaseSettings.object()
class JuicerBaseSettings(models.Model):
#classmethod
def object(cls):
return cls._default_manager.all().first() # Since only one item
def save(self, *args, **kwargs):
self.pk = self.id = 1
return super().save(*args, **kwargs)
============= OR =============
Simply, Use django_solo.
https://github.com/lazybird/django-solo
Snippet Courtsy: django-solo-documentation.
# models.py
from django.db import models
from solo.models import SingletonModel
class SiteConfiguration(SingletonModel):
site_name = models.CharField(max_length=255, default='Site Name')
maintenance_mode = models.BooleanField(default=False)
def __unicode__(self):
return u"Site Configuration"
class Meta:
verbose_name = "Site Configuration"
# admin.py
from django.contrib import admin
from solo.admin import SingletonModelAdmin
from config.models import SiteConfiguration
admin.site.register(SiteConfiguration, SingletonModelAdmin)
# There is only one item in the table, you can get it this way:
from .models import SiteConfiguration
config = SiteConfiguration.objects.get()
# get_solo will create the item if it does not already exist
config = SiteConfiguration.get_solo()
If your model is used in django-admin only, you additionally can set dynamic add permission for your model:
# some imports here
from django.contrib import admin
from myapp import models
#admin.register(models.ExampleModel)
class ExampleModelAdmin(admin.ModelAdmin):
# some code...
def has_add_permission(self, request):
# check if generally has add permission
retVal = super().has_add_permission(request)
# set add permission to False, if object already exists
if retVal and models.ExampleModel.objects.exists():
retVal = False
return retVal
i am not an expert but i guess you can overwrite the model's save() method so that it will check if there has already been a instance , if so the save() method will just return , otherwise it will call the super().save()
You could use a pre_save signal
#receiver(pre_save, sender=JuicerBaseSettings)
def check_no_conflicting_juicer(sender, instance, *args, **kwargs):
# If another JuicerBaseSettings object exists a ValidationError will be raised
if JuicerBaseSettings.objects.exclude(pk=instance.pk).exists():
raise ValidationError('A JuiceBaseSettings object already exists')
I'm a bit late to the party but if you want to ensure that only one instance of an object is created, an alternative solution to modifying a models save() function would be to always specify an ID of 1 when creating an instance - that way, if an instance already exists, an integrity error will be raised.
e.g.
JuicerBaseSettings.objects.create(id=1)
instead of:
JuicerBaseSettings.objects.create()
It's not as clean of a solution as modifying the save function but it still does the trick.
I did something like this in my admin so that I won't ever go to original add_new view at all unless there's no object already present:
def add_view(self, request, form_url='', extra_context=None):
obj = MyModel.objects.all().first()
if obj:
return self.change_view(request, object_id=str(obj.id) if obj else None)
else:
return super(type(self), self).add_view(request, form_url, extra_context)
def changelist_view(self, request, extra_context=None):
return self.add_view(request)
Works only when saving from admin

In Django, how do I avoid/skip/undo a pre-save event?

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.

Django, post_save signal recrusion. How to bypass signal firing

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()

What is the best / proper idiom in django for modifying a field during a .save() where you need to old value?

say I've got:
class LogModel(models.Model):
message = models.CharField(max_length=512)
class Assignment(models.Model):
someperson = models.ForeignKey(SomeOtherModel)
def save(self, *args, **kwargs):
super(Assignment, self).save()
old_person = #?????
LogModel(message="%s is no longer assigned to %s"%(old_person, self).save()
LogModel(message="%s is now assigned to %s"%(self.someperson, self).save()
My goal is to save to LogModel some messages about who Assignment was assigned to. Notice that I need to know the old, presave value of this field.
I have seen code that suggests, before super().save(), retrieve the instance from the database via primary key and grab the old value from there. This could work, but is a bit messy.
In addition, I plan to eventually split this code out of the .save() method via signals - namely pre_save() and post_save(). Trying to use the above logic (Retrieve from the db in pre_save, make the log entry in post_save) seemingly fails here, as pre_save and post_save are two seperate methods. Perhaps in pre_save I can retrieve the old value and stick it on the model as an attribute?
I was wondering if there was a common idiom for this. Thanks.
A couple of months ago I found somewhere online a good way to do this...
class YourModel(models.Model):
def __init__(self, *args, **kwargs):
super(YourModel, self).__init__(*args, **kwargs)
self.original = {}
id = getattr(self, 'id', None)
for field in self._meta.fields:
if id:
self.original[field.name] = getattr(self, field.name, None)
else:
self.original[field.name] = None
Basically a copy of the model fields will get saved to self.original. You can then access it elsewhere in the model...
def save(self, *args, **kwargs):
if self.original['my_property'] != self.my_property:
# ...
It can be easily done with signals. There are, respectively a pre-save and post-save signal for every Django Model.
So I came up with this:
class LogModel(models.Model):
message = models.CharField(max_length=512)
class Assignment(models.Model):
someperson = models.ForeignKey(SomeOtherModel)
import weakref
_save_magic = weakref.WeakKeyDictionary()
#connect(pre_save, Assignment)
def Assignment_presave(sender, instance, **kwargs):
if instance.pk:
_save_magic[instance] = Assignment.objects.get(pk=instance.pk).someperson
#connect(post_save, Assignment)
def Assignment_postsave(sender, instance, **kwargs):
old = None
if instance in _save_magic:
old = _save_magic[instance]
del _save_magic[instance]
LogModel(message="%s is no longer assigned to %s"%(old, self).save()
LogModel(message="%s is now assigned to %s"%(instance.someperson, self).save()
What does StackOverflow think? Anything better? Any tips?