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.
Related
I have a few views which handle the forms for new objects being created which seem to lag the user experience due to having to connect to the Twilio API. For example, the following form_valid method sends an SMS message to all receivers of a memo:
def form_valid(self, form):
form.instance.sender = self.request.user
form.save()
receiving_groups = form.cleaned_data['receiver']
for group in receiving_groups:
username = list(
employees.models.Employee.objects.filter(
employee_type=group))
for user in username:
form.instance.unread.add(user)
memo_url = form.instance.pk
user.send_sms('memo', memo_url)
user.send_email('memo', memo_url)
form.save()
return super(MemoCreateView, self).form_valid(form)
The issue here is that the send_sms method has to wait for a response before continuing. This really makes the site seem slow to the user.
I have started to create a request_finished signal but I would have to pass a lot of parameters to the signal so this seems like a bad option.
What other options do I have for sending the SMS messages post-success/redirect of the object being created?
The default notification setting for a wagtail user instance seems to be "Receive notification when a page is submitted for moderation", which does not fit our needs: we do not want each and every wagtail user to be notified (emailed) when a page gets submitted for moderation.
The default for this user notification setting is "True":
# wagtail/wagtail/users/models.py
# https://github.com/wagtail/wagtail/blob/master/wagtail/users/models.py#L25
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
submitted_notifications = models.BooleanField(
verbose_name=_('submitted notifications'),
default=True,
help_text=_("Receive notification when a page is submitted for moderation")
)
… and I would like to change the default to "False" (e.g. users should opt-in for getting email notifications).
But I do not know what’s the “best practices approach” for this task.
And I was a little surprised to have this problem at all, since I assumed that only members of the "Moderators" group/role would be notified. (which does not seem to be true).
What I tried/thought of:
Modifying this setting pro grammatically (which worked):
Existing user instances could be modified via shell:
./manage.py shell
from django.contrib.auth import get_user_model
User = get_user_model()
for user in User.objects.all():
user.wagtail_userprofile.update(submitted_notifications=False)
user.wagtail_userprofile.save()
Modifying this setting in my CustomUser save() method (does not work):
# project/users/models.py (pseudocode)
class CustomUser(AbstractUser):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.wagtail_userprofile.update(submitted_notifications=False)
self.wagtail_userprofile.save()
This raises a "RelatedObjectDoesNotExist" error.
Adding a data migration to alter the default value (just an idea – I did not know if that’s an option or how to implement this).
Setup:
django 2.0.8
wagtail 2.2.2
You can use a post save signal
import UserProfile from wagtail.users.models
#receiver(post_save, sender=UserProfile)
def update_user_profile(sender, instance, created, **kwargs):
if created:
instance.submitted_notifications = False
instance.save()
So this triggers when ever a new Profile is created and sets the 'submitted_notifications' default to False. Hope it helps
The reason updating the related profile in the save method raises a RelatedObjectDoesNotExist is because related objects are created after save. The db transaction needs to commit to get a primary key. Obviously you need a pk to store the relation.
This is why signals work. The post save signal is fired after the db transaction is finished. I think a signal is a valid solution to your problem.
You can use transaction.on_commit in your save method too. On commit waits for the transaction to be finished.
Sometimes you need to perform an action related to the current database transaction, but only if the transaction successfully commits. Examples might include a Celery task, an email notification, cache invalidation or setting some default in a related object. This is the documentation on that topic:
https://docs.djangoproject.com/en/1.11/topics/db/transactions/#performing-actions-after-commit
In your model do something like:
from django.db import transaction
class Foo(...):
...
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
if not self.pk:
transaction.on_commit(self.update_profile)
return instance
def update_profile(self):
self.wagtail_userprofile.update(submitted_notifications=False)
# Note: update, no need to call save.
# Signals will not be fired on behalf of the UserProfile.
Disclaimer, I did not run/test this code.
How can I send email notification when delete user from list?
I have a form on the site. With it, I add the user.When the user is added, I receive a notification to the mail. How can I get notified when a user is deleted?
Views.py:
class CreateUser(CreateView):
template_name = 'staffweb/add_user.html'
form_class = AddUserForm
model = StaffPersonal
context_object_name = 'staff'
success_url = reverse_lazy('home')
def form_valid(self, form):
new_user = form.save()
new_user_name = new_user.full_name
msg_args = ['New user added: ' + new_user_name]
send_to_bot('\n'.join((msg_args)))
email_text = make_letter(*msg_args)
send_to_email(settings.ADMIN_EMAIL, email_text)
return super(CreateUser, self).form_valid(form)
class DetailUser(DetailView):
template_name = 'staffweb/user.html'
model = StaffPersonal
context_object_name = 'user'
class DeleteUser(DeleteView):
model = StaffPersonal
success_url = reverse_lazy('home')
Thank you!
Try using django signals.
The post_delete signal is sent at the end of a model’s delete() method and a queryset’s delete() method. So whenever you model gets deleted, django sends a post_delete signal. You can catch this signal and do something after deleting it. Similarly signals are sent after creation and updation as well. Signals are simple to implement.
Something like this
in your models.py
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_delete, sender=StaffPersonal)
def send_email_after_delete(sender , instance , **kwargs):
#send your mail here.
#parameter instance is the model instance being deleted.
#Note that instance has been deleted form the database and exists only at the application(django) level.
#As the docs suggest, one should be careful while using it.
return
The function send_email_after_delete will be called every time the model mentioned is deleted. Read about django signals here.
Hope this helps.
Thanks.
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).
this is my model
class Profile(models.Model):
activate = models.BooleanField(default=False)
Now i want to do is , whenever some one from admin panel makes it true , an email will be sent to this particular user whose account is activated.
But i want to sent mail only when the value becomes true from false. if the value is already true i dont want to send any mail .
tried this thing with post save , but it sends email after every save action on Profile Model
Here the code, that will do it (used pre_save signal):
from django.db.models.signals import pre_save
from django.dispatch import receiver
#receiver(pre_save, sender=Profile)
def profile_changed(sender, instance, *args, **kwargs):
if instance.activate:
if not instance.pk:
print "Send email to user here"
else:
activate_was = sender._default_manager.filter(pk=instance.pk)\
.values("activate").get()["activate"]
if activate_was != instance.activate:
print "Send email to user here"