I have the following Django signal which basically triggers the signal to increase the points of my previous records in my postgres db by 5 when a new record is saved, but my Django signal saves the changes to 1 previous record and I get the error RecursionError: Maximum recursion depth exceeded
# models.py
from django.db.models.signals import post_save
class Task(models.Model):
....
def update_points(sender, instance, **kwargs):
qs = Task.objects.filter(status='Active')
for task in qs:
task.points = task.points + 5
task.save()
What am I doing wrong? I am using the .save() method to save the updated records in my db after a new record has been inserted.
Likely the point.save() triggers the same signal. So your signal triggers that signal, and thus this results in endless recursion.
You can update the value with a query in bulk:
from django.db.models import F
def update_points(sender, instance, **kwargs):
Task.objects.filter(status='Active').update(points=F('points')+5)
This will not only circument the signalling problem, it will furthermore make a query in bulk, and is thus more efficient.
Note that if you post_save, then the Task that was just added, will be part of the QuerySet. Perhaps you want to exclude that Task. In that case, we thus can implement this as:
from django.db.models import Q, F
from django.db.models.signals import post_save
def update_points(sender, instance, created, **kwargs):
if created:
Task.objects.filter(~Q(pk=instance.pk), status='Active').update(
points=F('points')+5
)
post_save.connect(update_points, sender=Task)
Related
I'm struggling with the following.
I'm trying to create a custom signal that will trigger when the current time will be equal to the value of my model's notify_on DateTimeField.
Something like this:
class Notification(models.Model):
...
notify_on = models.DateTimeField()
def send_email(*args, **kwargs):
# send email
signals.when_its_time.connect(send_email, sender=User)
After I've read through all docs and I found no information on how to implement such a signal.
Any ideas?
UPDATE:
Less naive approach with ability to discard irrelevant tasks: https://stackoverflow.com/a/55337663/9631956
Ok, thanks to comments by #SergeyPugach I've done the following:
Added a post_save signal that calls a function that adds a task to the celery. apply_async let's you pass eta - estimated time of arrival which can accept DateTimeField directly, that's very convenient.
# models.py
from django.db.models import signals
from django.db import models
from .tasks import send_notification
class Notification(models.Model):
...
notify_on = models.DateTimeField()
def notification_post_save(instance, *args, **kwargs):
send_notification.apply_async((instance,), eta=instance.notify_on)
signals.post_save.connect(notification_post_save, sender=Notification)
And the actual task in the tasks.py
import logging
from user_api.celery import app
from django.core.mail import send_mail
from django.template.loader import render_to_string
#app.task
def send_notification(self, instance):
try:
mail_subject = 'Your notification.'
message = render_to_string('notify.html', {
'title': instance.title,
'content': instance.content
})
send_mail(mail_subject, message, recipient_list=[instance.user.email], from_email=None)
except instance.DoesNotExist:
logging.warning("Notification does not exist anymore")
I will not get into details of setting up celery, there's plenty of information out there.
Now I will try to figure out how to update the task after it's notification instance was updated, but that's a completely different story.
In django's documentation there is two interesting signals that may help you on this task: pre_save and post_save.
It depends on your needs, but let's say you want to check if your model's notify_on is equal to the current date after saving your model (actually after calling the save() or create() method). If it's your case you can do:
from datetime import datetime
from django.contrib.auth.models import User
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import post_save
class Notification(models.Model):
...
# Every notification is related to a user
# It depends on your model, but i guess you're doing something similar
user = models.ForeignKey(User, related_name='notify', on_delete=models.DO_NOTHING)
notify_on = models.DateTimeField()
...
def send_email(self, *args, **kwargs):
"""A model method to send email notification"""
...
#receiver(post_save, sender=User)
def create_notification(sender, instance, created, **kwargs):
# check if the user instance is created
if created:
obj = Notification.objects.create(user=instance, date=datetime.now().date())
if obj.notify_on == datetime.now().date():
obj.send_email()
And you should know, that django signals won't work by they own only if there is an action that triggers them. What this mean is that Django signals won't loop over your model's instances and perform an operation, but django signals will trigger when your application is performing an action on the model connected to a signal.
Bonus: To perform a loop over your instances and process an action regulary you may need an asyncworker with a Queue database (mostly, Celery with Redis or RabbitMQ).
My model post processing is using the post_save signal:
from django.core.signals import request_finished
from django.dispatch import receiver
from models import MyModel
from pipeline import this_takes_forever
#receiver(post_save, sender=MyModel)
def my_callback(sender, **kwargs):
this_takes_forever(sender)
The this_takes_forever routine does IO so I want to defer it to avoid blocking the request too much.
I thought this was a great use case for the new asyncio module. But I have a hard time getting my mind around the whole process.
I think I should be able to adapt the signal receiver like this:
#receiver(post_save, sender=MyModel)
def my_callback(sender, **kwargs):
loop = asyncio.get_event_loop()
loop.run_until_complete(this_takes_forever(sender))
loop.close()
Provided this_takes_forever is also adapted to be a coroutine.
#coroutine
def this_takes_forever(instance):
# do something with instance
return instance
This sounds too magical to work. And in fact it halts with an AssertionError:
AssertionError at /new/
There is no current event loop in thread 'Thread-1'.
I don't see where should I start the loop in this context. Anyone tried something like this?
You get no any benefit in your case:
#receiver(post_save, sender=MyModel)
def my_callback(sender, **kwargs):
this_takes_forever(sender)
is equal to
#receiver(post_save, sender=MyModel)
def my_callback(sender, **kwargs):
loop = asyncio.get_event_loop()
loop.run_until_complete(this_takes_forever(sender))
loop.close()
in terms of execution time. loop.run_until_complete waits for end of this_takes_forever(sender) coroutine call, so you get synchronous call in second case as well as in former one.
About AssertionError: you start Django app in multithreaded mode, but asyncio makes default event loop for main thread only -- you should to register new loop for every user-created thread where you need to call asyncio code.
But, say again, asyncio cannot solve your particular problem, it just incompatible with Django.
The standard way for Django is to defer long-running code into celery task (see http://www.celeryproject.org/)
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).
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.
I want to execute some code in a Django model when it is first created. After that whenever it is saved I want to execute some other code. The second task can be easily done by overriding the save() method.
How can I do the first task?
Extending sdolan's answer by using receiver decorator:
from django.db import models
from django.dispatch import receiver
class MyModel(models.Model):
pass
#receiver(models.signals.post_save, sender=MyModel)
def execute_after_save(sender, instance, created, *args, **kwargs):
if created:
# code
You can use django signals' post_save:
# models.py
from django.db.models import signals
class MyModel(models.Model):
pass
def my_model_post_save(sender, instance, created, *args, **kwargs):
"""Argument explanation:
sender - The model class. (MyModel)
instance - The actual instance being saved.
created - Boolean; True if a new record was created.
*args, **kwargs - Capture the unneeded `raw` and `using`(1.3) arguments.
"""
if created:
# your code goes here
# django 1.3+
from django.dispatch import dispatcher
dispatcher.connect(my_model_post_save, signal=signals.post_save, sender=MyModel)
# django <1.3
from django.db.models.signals import post_save
post_save.connect(my_model_post_save, sender=MyModel)
field.default
The default value for the field. This can be a value or a callable object. If callable it will be called every time a new object is created.
So, we can use any field callable to do what we want on creation, huh? ;-)