Code

Opened 3 years ago

Last modified 9 months ago

#17193 new New feature

Send templated email.

Reported by: tomchristie Owned by: julianapplebaum
Component: Core (Mail) Version:
Severity: Normal Keywords:
Cc: streeter, artem.rizhov@…, chuck-norris@… Triage Stage: Accepted
Has patch: yes Needs documentation: yes
Needs tests: yes Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description

If your sending email it's likely that you want to render the body text from template, but there's currently no shortcut to send an email based on a template.

The attached patch is based on a stripped down version of https://github.com/bradwhittington/django-templated-email

It adds django.shortcuts.send_templated_mail, which mirrors the existing send_mail, but which renders the subject and body of the mail from a template, rather than taking their values explicitly. It also supports multipart html/plaintext emails.

The docs will look something like this...

send_templated_mail(template_name, from_email, recipient_list, dictionary=None, context_instance=None, fail_silently=False, auth_user=None, auth_password=None, connection=None):

Sends a mail, rendering the subject and body of the email from a template.

The template should contain a block named 'subject', and either/both of a 'plain' and/or 'html' block.

If only the 'plain' block exists, a plaintext email will be sent.

If only the 'html' block exists, the plaintext component will be automatically generated from the html, and a multipart email will be sent.

If both the 'plain' and 'html' blocks exist, a multipart email will be sent.

Required arguments:

template_name - The template that should be used to render the email.

from_email - The sender's email address.

recipient_list - A list of reciepient's email addresses.

Optional arguments:

dictionary - The context dictionary used to render the template. By default, this is an empty dictionary.

context_instance - The Context instance used to render the template. By default, the template will be rendered with a Context instance (filled with values from dictionary).

fail_silently - As in send_mail.

auth_user - As in send_mail.

auth_password - As in send_mail.

connection - As in send_mail.

Attachments (5)

send_templated_mail.diff (5.2 KB) - added by tomchristie 3 years ago.
send_templated_mail.2.diff (10.5 KB) - added by tomchristie 3 years ago.
send_templated_mail.3.diff (11.2 KB) - added by tomchristie 3 years ago.
send_templated_mail_4.diff (8.8 KB) - added by julianapplebaum 3 years ago.
email.py (2.1 KB) - added by carljm 16 months ago.
Simple ex ampleimplementation (would need integration as Django patch, tests & docs)

Download all attachments as: .zip

Change History (34)

Changed 3 years ago by tomchristie

comment:1 Changed 3 years ago by tomchristie

  • Needs documentation set
  • Needs tests set
  • Patch needs improvement unset

comment:2 Changed 3 years ago by aaugustin

  • Triage Stage changed from Unreviewed to Accepted

Yes, I think Django could provide this feature.

comment:3 Changed 3 years ago by julianapplebaum

I'm new to to the django dev community, and was hoping to write some unit tests for this shortcut. Should I claim the ticket before doing that? Sorry if this isn't the right place to be asking that.

Thanks,
Julian Applebaum

Last edited 3 years ago by julianapplebaum (previous) (diff)

comment:4 follow-up: Changed 3 years ago by aaugustin

Yes, you should assign the ticket to yourself if you intend to work on it.

You'll find more information about our development process in the contributing guide.

Changed 3 years ago by tomchristie

comment:5 Changed 3 years ago by tomchristie

  • Needs documentation unset

Updated the patch to include documentation.

Also tweaked the API slightly - the email connection and message sending has been decoupled from the message generation, and I've made some tweaks to the arguments that send_templated_mail takes.

I think this should pretty much hit the sweet spot of making sure the shortcut interface isn't overly complicated, whilst still providing for the common case.

Last edited 3 years ago by tomchristie (previous) (diff)

Changed 3 years ago by tomchristie

comment:6 Changed 3 years ago by tomchristie

Added some extra documentation in the existing email section, referring to the shortcut.

comment:7 in reply to: ↑ 4 Changed 3 years ago by julianapplebaum

  • Owner changed from nobody to julianapplebaum
  • Status changed from new to assigned

Replying to aaugustin:

Yes, you should assign the ticket to yourself if you intend to work on it.

You'll find more information about our development process in the contributing guide.

Thanks, and will do. Should have the patch up by Sunday night.

comment:8 Changed 3 years ago by anonymous

Quick status update:

It took me and the group I'm with longer than anticipated to get everything up and running. We've written a few unit tests for _render_mail, and aim to finish the rest this coming week. We apologize for the delay.

Julian

comment:9 Changed 3 years ago by tomchristie

We apologize for the delay.

Nothing to apologize for, it's a community effort - everything's appreciated. :)

comment:10 Changed 3 years ago by tevans.uk@…

I have a few doubts about how this has been designed. With both html and plain 'templates' actually just being blocks of a single template, it is not possible to use template inheritance to design consistent emails, which is a common use of the template system. Without this, styles would need to be replicated across multiple templates.

In our send_mail wrapper at $JOB, we allow the user to pass in the stub name of a template. The utility then attempts to render the templates stub+'.html' and stub+'.txt', and uses the results of these to determine whether we send a text/plain, text/html or multipart/alternative.

Instead of extracting the subject from a template, we allow it to be passed in directly. We also add a language argument, which is used to activate the appropriate translation prior to rendering the emails, or coercing the subject to unicode. This allows us to send fully translated emails appropriate for the user receiving the email - which may not be the same as the language currently activated, if there is any, or the language that has been activated for the current view.

Cheers

Tom (one of these days I'll remember my trac login)

comment:11 Changed 3 years ago by tomchristie

It's not quite true that you can't use template inheritance with this style, although I've come around to thinking that it probably is a an abuse of the 'block' tags to change their semantics for email templates.

I don't like the stub+'.txt', stub+'.html' for 2 reasons:

Firstly it's very easy for the two separate files to get out of sync, with no easy way of noticing that's happened.
Secondly the stub + extension style isn't used anywhere else.

I think a better solution would be a single template and a flag to switch between plaintext only, or html + autogenerated plaintext.

It's not impossible that I might get a spare couple of hours between now and the 1.4 checkin deadline, but it's not looking all that likely, so may have to defer this to 1.5.

Changed 3 years ago by julianapplebaum

comment:12 Changed 3 years ago by julianapplebaum

Hey Everyone

Just uploaded the diff file for my group's unit tests. Let me know if you run into any issues with the tests/the diff file, etc.

Best,
Julian

comment:13 Changed 3 years ago by anonymous

Thanks Julian,

There may be some redesign needed of this feature, but either way those tests look fairly comprehensive - should come in useful.

Tom

comment:14 Changed 3 years ago by julianapplebaum

  • Needs tests unset

comment:15 follow-up: Changed 3 years ago by julien

  • Triage Stage changed from Accepted to Design decision needed

Thank you all for your great work on this patch. However, I'm struggling a bit to see the value of adding this much new code for something that is already quite easily achievable using render_to_string() and separate templates.

I do agree that creating html+plain emails is still a little too hard in Django, but in my opinion this could be simplified by introducing a new helper class wrapping around EmailMultiAlternatives and providing a simpler API and sensible defaults. For example, something like: HTMLEmailMessage(body_plain='...', body_html='...')

In any case, I personally think that any template rendering should be left to the user to do separately (for example by using render_to_string()).

For now I'm marking this ticket as DDN instead of wontfixing so that more discussion can occur.

comment:16 Changed 3 years ago by anonymous

Agreed that this isn't quite the right way to approach it.

I still think a send_templated_email, makes a lot of sense, but I've not had time to revisit this yet and propose something simpler.

I'll take it to django-developers if and when that happens.
If anyone else takes this to the group first, please link to the discussion from this ticket.

comment:17 Changed 2 years ago by streeter

  • Cc streeter added

comment:18 in reply to: ↑ 15 Changed 2 years ago by artem.rizhov@…

  • Cc artem.rizhov@… added

Hi there!

Replying to julien:

I do agree that creating html+plain emails is still a little too hard in Django, but in my opinion this could be simplified by introducing a new helper class wrapping around EmailMultiAlternatives and providing a simpler API and sensible defaults. For example, something like: HTMLEmailMessage(body_plain='...', body_html='...')

My implementation is based on EmailMultiAlternatives :) It's available at GitHub https://github.com/artemrizhov/django-mail-templated

Actuall, for me it
I've emailed to django-developers mailing list and proposed to merge it into Django. However my proposition was declined. http://groups.google.com/group/django-developers/browse_frm/thread/4728f20dd8023dd9

I'd be glad to continue work on it, but there is no any reason to polish it more for a while. :( At least unless I need some additional functionality, or some activity is registered on the github project. :) Propositions and suggestions are welcome.

comment:19 Changed 2 years ago by artem.rizhov@…

Ooops..
s/Actuall, for me it //
Sorry for trash in the message.

comment:20 Changed 23 months ago by chuck-norris@…

  • Cc chuck-norris@… added

comment:21 Changed 16 months ago by carljm

  • Needs tests set
  • Patch needs improvement set
  • Triage Stage changed from Design decision needed to Accepted

Rendering a template is simple, but there are some minor gotchas in templated email (like remembering to remove newlines from the rendered subject template) that would be reasonable to put in a convenience function in Django rather than requiring everyone to implement them independently. And the EmailMultiAlternatives API is just enough boilerplate to always require looking up in the docs.

IMO the blocks approach implemented above (both in the patch and the linked external project) is too complex, too much code, and too unusual a use of templates (blocks are for template inheritance, not a way to combine multiple templates in one file.) I would rather favor a simple extension naming convention (i.e. give path/to/myemail and it looks for templates path/to/myemail.txt, path/to/myemail.html, and path/to/myemail.subject.txt). I've attached a sample implementation of this approach (not yet integrated as a patch to Django); it really isn't very much code. (This implements a two-layer API, with both a send_multipart function that accepts strings, and a send_templated_multipart that does the template-finding and rendering.)

I guess I'm personally +0 or +0.5 on including something like this in Django. I think it's a very common pattern that's just irritating enough to re-implement to be worth considering for inclusion, but I won't mind at all if another core dev decides to close this wontfix.

Changed 16 months ago by carljm

Simple ex ampleimplementation (would need integration as Django patch, tests & docs)

comment:22 Changed 16 months ago by carljm

  • Needs documentation set

comment:23 Changed 16 months ago by artem.rizhov@…

too unusual a use of templates (blocks are for template inheritance, not a way to combine multiple templates in one file.)

Maybe you are right. However 3 files per message is too many. It may be hard and annoying to maintain so many files. And somebody may forget to change one of templates.

And why you say "multiple templates"? Imho this is single template of multipart message :) Let's add this at the top of template:

{% extends 'email/multipart.html' %}

Does it look better now? ;) We also may add 'email/text.html', 'email/html.html' and so on. This will make the template code looking more usual.

comment:24 Changed 16 months ago by carljm

If the implementation requires Python code pulling out the blocks from the rendered template for individual specialized handling, then IMO it's a misuse of template blocks and it should be separate templates instead. If the hypothetical email/multipart.html base actually contained a full template for a multipart message, that would be a reasonable use of templates (but still not necessarily a good idea, as the template would then be re-implementing things already handled by EmailMultiAlternatives).

I don't buy the idea that maintaining three files is too hard. But for people who feel that way, alternative approaches would always be available as third-party packages. (Or we can just wontfix this and let all implementations stay outside core, I'm fine with that too.)

comment:25 Changed 16 months ago by anonymous

Or we can just wontfix this

Ok, you won :)

I hope it is possible to make both html and text vesrions not required.

comment:26 Changed 9 months ago by timo

  • Resolution set to duplicate
  • Status changed from assigned to closed

Marking this as a duplicate of #20817 which added an html_message parameter to send_mail().

comment:27 Changed 9 months ago by anonymous

timo, does html_message parameter allow django templates to be used?

comment:28 Changed 9 months ago by charettes

@anon yes, you could use the render_to_string function to build the html_message kwarg passed to send_mail.

comment:29 Changed 9 months ago by mjtamlyn

  • Resolution duplicate deleted
  • Status changed from closed to new

No, this does not close the original OPs request, and concept as mentioned by Carl.

This ticket is to introduce a helper function of some sort to handle send_mail(render_to_string(...)). It's just a helper function, it's achievable without, but it's an exceedingly common pattern and personally I'm +1 on having this. A significant number of projects I've worked on have included a utility function named something like render_to_send_mail().

The exact API is up for discussion still, which is probably the blocker for this ticket. As mentioned in #20817, it may be better as extension around the EmailMessage or EmailMultiAlternatives APIs rather than just as a helper function. I can see something like the following API being useful:

message = TemplatedEmail(
     from_email=...,
     to=[...],
     subject_template='my_email_subject.txt',
     body_template='my_email_template.txt',
     html_body_template='my_enhanced_email_template.html',
     context=context,
)
message.send()

A functional approach to this could be added as well as a shortcut function, though this is already significantly easier than the existing APIs to do this. If we take Carl's earlier suggestion about a naming convention, it becomes simpler, alternatively we could add complexity by also supporting kwargs for subject, body and html_body which can take rendered strings as alternatives.

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as new
The owner will be changed from julianapplebaum to anonymous. Next status will be 'assigned'
as The resolution will be set. Next status will be 'closed'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.