Django: passing variables from pre_save to post_save signals - django

I use the pre_save and post_save signals to send analytics to Mixpanel. I prefer to keep this separated from my model's save method.
Is there a way to save the old values of an instance when the pre_save signal occurs, and then check the new values against them on post_save?
My code looks like this:
#receiver(pre_save, sender=Activity)
def send_user_profile_analytics(sender, **kwargs):
activity_completed_old_value = kwargs['instance'].is_completed
# store this value somewhere?
#receiver(post_save, sender=Activity)
def send_user_profile_analytics(sender, **kwargs):
if kwargs['instance'].is_completed != activity_completed_old_value:
# send analytics
For me it seems more robust to use post_save to send the analytics than pre_save, but at that point I can't see what has changed in the model instance. I would like to prevent using globals or implementing something in my model's save function.

You can store them as instance attributes.
#receiver(pre_save, sender=Activity)
def send_user_profile_analytics(sender, **kwargs):
instance = kwargs['instance']
instance._activity_completed_old_value = instance.is_completed
#receiver(post_save, sender=Activity)
def send_user_profile_analytics(sender, **kwargs):
instance = kwargs['instance']
if instance.is_completed != instance._activity_completed_old_value:
# send analytics
In this way you "send analytics" only if is_completed changes during save (that means that save doesn't just store the value but makes some further elaboration).
If you want to perform an action when a field is changed during instance life-time (that is from its creation till the save) you should store the initial value during post_init (and not pre_save).

Related

Django two same signal types

I don't understand how 2 signals executed together relate to each other. For example consider the following:
from django.db import models
from django.contrib.auth.models import User
from PIL import Image
from django.db.models.signals import post_save
from django.dispatch import receiver
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
image = models.ImageField(default='default.jpg', upload_to='profile_pics')
def __str__(self):
return f'{self.user.username} Profile'
# Overriding save method-image resizing
def save(self, **kwargs):
super().save(kwargs)
img = Image.open(self.image.path)
if img.height > 300 or img.width > 300:
output_size = (300, 300)
img.thumbnail(output_size)
img.save(self.image.path) # save() of parent class
#Signals when a user is registered his profile is automatically updated
#receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile(user=instance) #could do Profile.objects.create(user=instance) instead to replace the 2 lines
instance.profile.save() #reverse query
I understand the following:
create_profile: is my receiver function. It will do something when .save() method takes place, in our case will create a Profile instance and save it.
sender: is the Model that is actually signalling this to take place. So in our case, if we want a signal to take place when the save method is executed on User model then the sender would be the User.
post_save is the signal
Regarding my question now. I have seen the above function broken down into the two following functions.
#receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile(user=instance)
#receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
instance.profile.save() #reverse query
I am aware it is pointless since I could do what I did but I don't understand how does my save_profile function 'remembers' the Profile instance made on create_profile?
As far as I can tell, save_profile is a separate function that has no reference to create_profile function (example by create_profile or so on the save_profile body).
I am assuming when a user instance is saved the create_profile function is executed it makes a profile object then the save_profile function is executed and for same user instance the profile object is saved,
but I still don't understand how it knows I refer to same profile made by that instance.
Why you not state it is example from Corey tutorial ?
Corey Tutorial, part 8, and if I not mistaken he didnt have to create two separate signals, because one was enough ..
To your question:
The functions are:
#receiver(post_save, sender=User)
def create_profile_handler(sender, instance, created, **kwargs):
#Run each time a new user is created, and attach to the user a profile !
# sender - The model class (User)
# instance - The actual instance being saved. (User)
# created - A boolean. 'True' if a new record was created.
if created:
Profile.objects.create(user=instance)
#'create' - initialize and save a profile and attach it to the User
# Now, lets save the profile each time the user object is been saved.
#receiver(post_save, sender=User)
def save_profile_handler(sender, instance, **kwargs):
instance.profile.save() # User.profile.save() - saves the profile
we know from the Django Docs, that create() method include save() operation in it.
So not only that the second signal is redundant (because we can do its operations in the first signal), further more - the save is a bit redundant it self (because create calls save).

django model not in urls.py

I am new to python and django and I want to know the best way to set things up.
I have a model called OauthProviders which I set up to encrypt some fields before save in the ModelViewSet (override perform_create method). I dont want to create routes (urls) for this model.
Now, if I want to access this model in the code (I can with OauthProvider.objects.all() of course), but I have a few questions:
how do I enter data to this model NOT in code? If I use the admin portal for it, it doesn't execute my custom perform_create method, so it gets added to the database in plain text
What is the best way to decrypt a message if I retrieve data?
EDIT:
I moved the logic from the ModelViewSet to the save() method on the model with the following code:
def save(self, *args, **kwargs):
self.credentials = encrypt_message(self.credentials, '<keyhere>')
return super().save(*args, **kwargs)
This seems to work. Is this good practice? How to do this with the get method?
A good way of doing this is using django signals.
Assuming you have:
class OauthProvider(models.Model):
pass # some fields here
In your case you can use either:
pre_save - code that will be executed before OauthProvider gets saved in the database.
post_save - code that will be executed after OauthProvider gets saved in the database.
It's up to you to decide when exactly you want to execute additional code.
Example with pre_save:
from django.db.models.signals import pre_save
from django.dispatch import receiver
class OauthProvider(models.Model):
pass # some fields here
#receiver(pre_save, sender=OauthProvider)
def oauthprovider_on_save(sender, instance, **kwargs):
instance.credentials = encrypt_message(self.credentials, '<keyhere>')
# no need to call instance.save() in pre_save signal
The pre_save signal will be executed:
If you call obj.save() programatically.
If you save the obj through django admin site.
If you create/update the obj within rest api

post_save error Message' has no attribute

I'm trying to use signals post_save for the first time. I have read the documents, but still need some advice.
I'm trying to update my model field called 'charge'.
#receiver(post_save, sender=Message)
def my_handler(sender, **kwargs):
if not sender.charge:
sender(charge=sender.length(sender))
sender.save()
However, this gives the error Message' has no attribute 'charge', but charge does exist within message!
sender here is the Message class itself, not the instance that's being saved. The actual instance is passed as the keyword argument instance. Also, with post_save, if you're not careful, you'll get yourself in an infinite loop. Better to use pre_save.
#receiver(pre_save, sender=Message)
def my_handler(sender, **kwargs):
instance = kwargs['instance']
if not instance.charge:
instance.charge = instance.length()
# No need to save, as we're slipping the value in
# before we hit the database.

Django - many to many with post_save gridlock

I want to send out an email when a model instance is saved. To do this I listen out for the post_save signal:
#models.py
#receiver(post_save, sender=MyModel, dispatch_uid="something")
def send_email(sender, **kwargs):
instance = kwargs['instance']
email = ModelsEmailMessage(instance)
email.send()
In my view I process the form and append subscribers to the object:
#views.py
object = form.save()
object.subscribers.add(*users)
My problem is that the form save triggers the post_save signal before the users have been added.
But if I do this:
object = form.save(commit=False)
then I can't add m2m instances to an object that has no id.
Heyulp!
Most likely you will have to write your own signal to send email.
Event though you are implemented that tries to send email when object is saved, but that is not what you want. You want to send email when object is saved and has some subscribers added after processing some view. i.e. its 2 step operation.

django signals, how to use "instance"

I am trying to create a system which enables user to upload a zipfile, and then extract it using post_save signal.
class Project:
....
file_zip=FileField(upload_to='projects/%Y/%m/%d')
#receiver(post_save, sender=Project)
def unzip_and_process(sender, **kwargs):
#project_zip = FieldFile.open(file_zip, mode='rb')
file_path = sender.instance.file_zip.path
with zipfile.ZipFile(file_path, 'r') as project_zip:
project_zip.extractall(re.search('[^\s]+(?=\.zip)', file_path).group(0))
project_zip.close()
unzip_and_process method works fine when correct file paths are provided(in this case, i need to provide instance.file_zip.path. However, I couldn't get/set the instance with the signals. Django documentation about signals is not clear and have no examples. So, what do I do?
Actually, Django's documentation about signals is very clear and does contain examples.
In your case, the post_save signals sends the following arguments: sender (the model class), instance (the instance of class sender), created, raw, and using. If you need to access instance, you can access it using kwargs['instance'] in your example or, better, change your callback function to accept the argument:
#receiver(post_save, sender=Project)
def unzip_and_process(sender, instance, created, raw, using, **kwargs):
# Now *instance* is the instance you want
# ...
This worked for me when connecting Django Signals:
Here is the models.py:
class MyModel(models.Model):
name = models.CharField(max_length=100)
And the Signal that access it post_save:
#receiver(post_save, sender=MyModel)
def print_name(sender, instance, **kwargs):
print '%s' % instance.name