I'm trying to send a email after save some model in a post_save
signal but when I save the model by first time the email is not sent, if I save the model a second time the email is sent.
The model has a ManyToMany
field so I can't use pre_save
signal because it throws a error: <mymodel: mymodel_name object (None)>" needs to have a value for field "id" before this many-to-many relationship can be used.
class Message(TimeStampedModel, models.Model):
"""Representation of a Message."""
recipients = models.ManyToManyField(
to=settings.AUTH_USER_MODEL, verbose_name=_("Recipients")
)
subject = models.CharField(
verbose_name=_("Subject"),
help_text=_(
"150 numbers / letters or fewer. Only letters and numbers are allowed."
),
max_length=150,
validators=[alphabets_accents_and_numbers],
)
content = models.TextField(verbose_name=_("Message"))
@receiver(signal=post_save, sender=Message)
def send_message(sender, instance, **kwargs):
"""Send a email or Whatsapp if a new message is created."""
recipient_emails = [recipient.email for recipient in instance.recipients.all()]
attachments = []
if instance.messagefile_set:
for message_file in instance.messagefile_set.all():
attachments.append((message_file.file.name, message_file.file.read()))
send_mails(
subject=instance.subject,
message=instance.content,
recipient_list=recipient_emails,
attachments=attachments,
)
This is the function used in the signal to send emails.
def send_mails(
subject: str,
message: str,
recipient_list: List[str],
from_email: Optional[str] = None,
**kwargs,
) -> int:
"""Wrapper around Django's EmailMessage done in send_mail().
Custom from_email handling and special Auto-Submitted header.
"""
if not from_email:
if hasattr(settings, "DEFAULT_FROM_EMAIL"):
from_email = settings.DEFAULT_FROM_EMAIL
else:
from_email = "webmaster@localhost"
connection = kwargs.get("connection", False) or get_connection(
username=kwargs.get("auth_user", None),
password=kwargs.get("auth_password", None),
fail_silently=kwargs.get("fail_silently", None),
)
multi_alt_kwargs = {
"connection": connection,
"headers": {"Auto-Submitted": "auto-generated"},
}
mail = EmailMessage(
subject=subject,
body=message,
from_email=from_email,
to=recipient_list,
**multi_alt_kwargs,
)
attachments = kwargs.get("attachments", None)
if attachments:
for attachment in attachments:
if isinstance(attachment, MIMEBase):
mail.attach(attachment)
else:
mail.attach(*attachment)
return mail.send()
At the time you create the Message
, and immediately after you create (not update) the Message
(so when the post_save
signal is called), all ManyToManyField
s are empty, as well as the reverse of all ForeignKey
s). This makes sense since before you can create a many-to-many relation between two records, these records need to be saved to the database, otherwise these records do not have a primary key, and thus these can not be linked.
I would advise not to use signals for this. Strictly speaking, you could try to work with an m2m_changed
signal [Django-doc], like:
from django.contrib.auth import get_user_model
from django.db.models.signals import m2m_changed
def recipient_added(sender, instance, action, pk_set, **kwargs):
if action == 'post_add':
recipient_emails = list(get_user_model().objects.filter(pk__in=pk_set))
'm2m_changed'.connect(recipient_added, sender=Message.recipients.through)
But nevertheless, now it is possible that the messagefile_set
is saved later, and we thus still have a problem.
I would advise to encapsulate the logic of sending emails in a function, and then call that function in the view after the object was created.
So if you for example have a view with a MessageForm
:
def send_message(request):
if request.method == 'POST':
form = MessageForm(request.POST, request.FILES)
if form.is_valid():
message = form.save()
send_messages(message)
Here the form
will not only save the object, but also the many-to-many fields it handles. So after form.save()
then saving of the object is completely finished.
In the ModelAdmin
you can make use of the save_related
method [Django-doc]:
from django.contrib import admin
class MessageAdmin(admin.ModelAdmin):
def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
send_mail(form.instance)
You can of course alter it. It is important to first call the .save_related()
super method, since that will call the form.save()
that will again handle the many-to-many relation, etc.