Where is the right place to send an email in a ModelForm

I am building a modelform for a booking request form

I would like the form to be submitted and on a successful form submission an email needs to be sent
to the customer and/or a user within the company.

I’m trying to understand where is the optimal/best place/best practice place for the message to be sent?

Option 1 - Override the save() in the modelform

class BookingRequestForm(forms.ModelForm):
    class Meta:
        model = BookingRequest
        fields = [
            "title",
            "first_name",
            "last_name",
            "email",
            "phone",
            "date_of_birth",
            "address",
            "referrer",
            "returning",
            "reason",
        ]


    def save(self):
        instance = super(BookingRequestForm, self).save()
        send_mail(
            "Subject here",
            "Here is the message.",
            "from@example.com",
            ["to@example.com"],
            fail_silently=False,
        )
        return instance

Option 2 - override form_valid in the view and send email before model is saved.


class BookingRequestView(CreateView):
    form_class = BookingRequestForm
    template_name = "booking/booking_request.html"
    success_url = reverse_lazy("booking-thanks")

    def form_valid(self, form):
        """If the form is valid, send an email and then save the form."""
        send_mail(
            "Subject here",
            "Here is the message.",
            "from@example.com",
            ["to@example.com"],
            fail_silently=False,
        )
        return super(BookingRequestView, self).form_valid(form)

Option 3 - Both are wrong and I should be using signals?

Option 4 - The whole approach is wrong and I should be doing it another way ??

Both option 1 and 2 work but I was just wondering about best practice? I think the Django docs could use some clarity in this area which I will gladly provide one I get my head around this :slight_smile:

My gut says option 1 is the best as we are only sending after the object has been persisted whereas in option 2 and email may get sent but something goes wrong and the object does not get saved.

Or maybe I’m just overthinking this and it doesn’t matter!?

Thanks for your help in advance :pray:

Kind regards
Chris

I would use an on_commit callback within save()

Thank you Adam this is exactly what I want!

For others that may need this here is a link in the docs:
https://docs.djangoproject.com/en/3.2/topics/db/transactions/#performing-actions-after-commit

from django.db import transaction

def send_booking_email():
    send_mail(
        "Subject here",
        "Here is the message.",
        "from@example.com",
        ["to@example.com"],
        fail_silently=False,
    )


class BookingRequestForm(forms.ModelForm):
    

    class Meta:
        model = BookingRequest
        # other form stuff
        ...
        def save(self):
            instance = super(BookingRequestForm, self).save()
            transaction.on_commit(send_booking_email)
        return instance

FYI normally more useful to use an inner function within the save method, as it can then reference variables in the enclosing scope.

Can you show me or point me to an example?

Hi Chris,

I think Adam is suggesting something similar what what’s below. By defining the method inline, you have access to instance and any other locals from save in send_booking_email.

class BookingRequestForm(forms.ModelForm):
        ...
        def save(self):
            instance = super(BookingRequestForm, self).save()
            def send_booking_email():
              message = f"generate the message from the {instance}"
              send_mail(
                "Subject here",
                message,
                "from@example.com",
                ["to@example.com"],
                fail_silently=False,
               )
            transaction.on_commit(send_booking_email)
        return instance

Thanks Tim/Adam yes that is a much better way :slight_smile:

Indeed I am. It’s also possible to use on_commit as a decorator:

class BookingRequestForm(forms.ModelForm):
        ...
        def save(self):
            instance = super(BookingRequestForm, self).save()

            @transaction.on_commit
            def send_booking_email():
              message = f"generate the message from the {instance}"
              send_mail(
                "Subject here",
                message,
                "from@example.com",
                [self.cleaned_data["user"].emailÓ],
                fail_silently=False,
               )
        return instance
2 Likes

Ooh, TIL, thanks! I’m sure I’ll find a use for that at some point!

1 Like

@chriswedgwood I think you’ve got a few answers here, but I think it’s worth adding a comment about performance here.

If you’re going to be sending a lot of emails like this, and you expect heavy traffic for the site, you could find it becomes a bit of a problem. At this point, you will likely need to set up some kind of background process to handle this stuff. There are a few options:

  1. Instead of sending the email directly add it to a table and then run a background cron job to send the mails every x minutes/seconds. This is the simplest solution if the mails don’t need to go immediately (i.e. it’s not a password reset or “confirm mail before you can do anything” mail)

  2. Use a django package that handles background tasks, there are a few of them.

  3. (Bit of an extension of point 2 really but …) Use a full on message broker (redis or rabbitMQ) with Celery to handle background tasks.

2 Likes