diff --git a/apps/agreements/notifications.py b/apps/agreements/notifications.py index 50e284a5f..950ef8c43 100644 --- a/apps/agreements/notifications.py +++ b/apps/agreements/notifications.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from django.conf import settings -from django.core.mail import EmailMessage +from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.urls import reverse @@ -28,7 +28,8 @@ def _send( ) -> None: subject = " ".join(render_to_string(f"agreements/email/{template}_subject.txt", context).split()) body = render_to_string(f"agreements/email/{template}.txt", context) - email = EmailMessage(subject=subject, body=body, from_email=settings.DEFAULT_FROM_EMAIL, to=[to]) + email = EmailMultiAlternatives(subject=subject, body=body, from_email=settings.DEFAULT_FROM_EMAIL, to=[to]) + email.attach_alternative(render_to_string(f"agreements/email/{template}.html", context), "text/html") for attachment in attachments: email.attach(*attachment) email.send() diff --git a/apps/agreements/templates/agreements/email/base.html b/apps/agreements/templates/agreements/email/base.html new file mode 100644 index 000000000..c68a8c700 --- /dev/null +++ b/apps/agreements/templates/agreements/email/base.html @@ -0,0 +1,38 @@ + + +
+ + + +| + + | +
Hello {{ agreement.signer_name }},
+The Python Software Foundation has countersigned your agreement with {{ agreement.counterparty_name }}.
+ +|
+ Executed
+ {{ agreement.title }}
+ Reference {{ agreement.reference }} · Effective {{ agreement.countersigned_at|date:"F j, Y" }}
+ |
+
Attached
+{% if terms_versions %}The cited terms versions are included so you can retain them even if their online pages are private. {% endif %}Keep these documents for your records.
+Python Software Foundation
+{% endblock %} diff --git a/apps/agreements/templates/agreements/email/signing_link.html b/apps/agreements/templates/agreements/email/signing_link.html new file mode 100644 index 000000000..a692f6b15 --- /dev/null +++ b/apps/agreements/templates/agreements/email/signing_link.html @@ -0,0 +1,41 @@ +{% extends "agreements/email/base.html" %} +{% block title %}Please sign: {{ agreement.title }}{% endblock %} +{% block preheader %}{{ agreement.title }} for {{ agreement.counterparty_name }} is ready for your signature.{% endblock %} + +{% block content %} +Hello {{ link.name }},
+The Python Software Foundation has prepared the following for {{ agreement.counterparty_name }} to sign:
+ +|
+ {{ agreement.title }}
+ Reference {{ agreement.reference }}
+ |
+
| + Review and sign + | +
Button not working? Paste this link into your browser:
+{{ url }}
| + The link works once and expires on {{ link.expires_at|date:"F j, Y" }}. Your name, title, email, the time, and your IP address are recorded with your signature. + | +
If you would rather sign through DocuSign or on paper, reply to this email.
+Python Software Foundation
+{% endblock %} + +{% block footer %}If you weren't expecting this, you can ignore it.{% endblock %} diff --git a/apps/agreements/tests/test_signing_recovery.py b/apps/agreements/tests/test_signing_recovery.py index c19ad93bc..114dc6818 100644 --- a/apps/agreements/tests/test_signing_recovery.py +++ b/apps/agreements/tests/test_signing_recovery.py @@ -74,7 +74,9 @@ def test_failed_invitation_does_not_leave_a_pending_link_and_can_be_retried(self ) url = reverse("agreements:send_link", args=[self.agreement.pk]) data = {"name": "Invited Signer", "email": "invited@example.com"} - with patch("apps.agreements.notifications.EmailMessage.send", side_effect=SMTPException("mail unavailable")): + with patch( + "apps.agreements.notifications.EmailMultiAlternatives.send", side_effect=SMTPException("mail unavailable") + ): response = self.client.post(url, data) self.assertEqual(response.status_code, 302) self.assertEqual(mail.outbox, []) @@ -126,7 +128,7 @@ def test_delivery_can_be_retried_without_changing_the_countersignature(self) -> self.client.force_login(self.officer) for target, error in ( ("apps.agreements.notifications.render_pdf", RuntimeError("renderer unavailable")), - ("apps.agreements.notifications.EmailMessage.send", SMTPException("mail unavailable")), + ("apps.agreements.notifications.EmailMultiAlternatives.send", SMTPException("mail unavailable")), ): with self.subTest(failure=target): agreement = offer_contract(self.officer, counterparty_account=self.customer)