Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/agreements/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand Down
38 changes: 38 additions & 0 deletions apps/agreements/templates/agreements/email/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>{% block title %}{% endblock %}</title>
</head>
<body style="margin:0;padding:0;background-color:#f2f4f6;">
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;">{% block preheader %}{% endblock %}</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#f2f4f6;">
<tr>
<td align="center" style="padding:32px 12px;">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border:1px solid #caccce;border-radius:6px;overflow:hidden;">
<tr>
<td style="background-color:#2b5b84;padding:22px 32px;border-bottom:4px solid #ffd343;">
<a href="https://www.python.org/psf/" style="text-decoration:none;">
<img src="https://www.python.org/static/img/psf-logo.png" width="220" height="54" alt="Python Software Foundation" style="display:block;border:0;width:220px;height:auto;color:#ffffff;font-family:Arial,Helvetica,sans-serif;font-size:18px;font-weight:bold;">
</a>
</td>
</tr>
<tr>
<td style="padding:32px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#222222;">
{% block content %}{% endblock %}
</td>
</tr>
<tr>
<td style="padding:20px 32px;background-color:#f2f4f6;border-top:1px solid #e6e8ea;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:1.5;color:#666666;">
Python Software Foundation &middot; <a href="https://www.python.org/psf/" style="color:#3776ab;text-decoration:none;">python.org/psf</a><br>
{% block footer %}You received this email because the PSF sent you an agreement.{% endblock %}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
29 changes: 29 additions & 0 deletions apps/agreements/templates/agreements/email/executed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{% extends "agreements/email/base.html" %}
{% block title %}Executed: {{ agreement.title }}{% endblock %}
{% block preheader %}{{ agreement.title }} with {{ agreement.counterparty_name }} has been countersigned. Your copy is attached.{% endblock %}

{% block content %}
<p style="margin:0 0 16px;">Hello {{ agreement.signer_name }},</p>
<p style="margin:0 0 24px;">The Python Software Foundation has countersigned your agreement with <strong>{{ agreement.counterparty_name }}</strong>.</p>

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 24px;">
<tr>
<td style="background-color:#f2f4f6;border-left:4px solid #82b043;border-radius:4px;padding:16px 20px;">
<div style="font-size:12px;font-weight:bold;letter-spacing:0.08em;text-transform:uppercase;color:#3d5a1a;">Executed</div>
<div style="margin-top:4px;font-size:18px;font-weight:bold;color:#2b5b84;">{{ agreement.title }}</div>
<div style="margin-top:4px;font-size:13px;color:#666666;">Reference <span style="font-family:Menlo,Consolas,monospace;color:#444444;">{{ agreement.reference }}</span> &middot; Effective {{ agreement.countersigned_at|date:"F j, Y" }}</div>
</td>
</tr>
</table>

<p style="margin:0 0 8px;"><strong>Attached</strong></p>
<ul style="margin:0 0 24px;padding-left:20px;color:#444444;font-size:14px;line-height:1.7;">
<li>Signed copy: <span style="font-family:Menlo,Consolas,monospace;">psf-agreement-{{ agreement.reference }}.pdf</span></li>
{% for version in terms_versions %}
<li>{{ version.terms.title }}, version {{ version.version }}</li>
{% endfor %}
</ul>

<p style="margin:0 0 16px;font-size:14px;color:#444444;">{% 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.</p>
<p style="margin:0;">Python Software Foundation</p>
{% endblock %}
41 changes: 41 additions & 0 deletions apps/agreements/templates/agreements/email/signing_link.html
Original file line number Diff line number Diff line change
@@ -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 %}
<p style="margin:0 0 16px;">Hello {{ link.name }},</p>
<p style="margin:0 0 24px;">The Python Software Foundation has prepared the following for <strong>{{ agreement.counterparty_name }}</strong> to sign:</p>

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 28px;">
<tr>
<td style="background-color:#f2f4f6;border-left:4px solid #3776ab;border-radius:4px;padding:16px 20px;">
<div style="font-size:18px;font-weight:bold;color:#2b5b84;">{{ agreement.title }}</div>
<div style="margin-top:4px;font-size:13px;color:#666666;">Reference <span style="font-family:Menlo,Consolas,monospace;color:#444444;">{{ agreement.reference }}</span></div>
</td>
</tr>
</table>

<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 24px;">
<tr>
<td style="border-radius:4px;background-color:#3776ab;">
<a href="{{ url }}" style="display:inline-block;padding:13px 28px;font-family:Arial,Helvetica,sans-serif;font-size:16px;font-weight:bold;color:#ffffff;text-decoration:none;border-radius:4px;">Review and sign</a>
</td>
</tr>
</table>

<p style="margin:0 0 24px;font-size:13px;line-height:1.5;color:#666666;">Button not working? Paste this link into your browser:<br>
<a href="{{ url }}" style="color:#3776ab;word-break:break-all;">{{ url }}</a></p>

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 16px;">
<tr>
<td style="background-color:#fff7dc;border:1px solid #ffd343;border-radius:4px;padding:12px 16px;font-size:14px;line-height:1.5;color:#444444;">
The link works once and expires on <strong>{{ link.expires_at|date:"F j, Y" }}</strong>. Your name, title, email, the time, and your IP address are recorded with your signature.
</td>
</tr>
</table>

<p style="margin:0 0 16px;font-size:14px;color:#444444;">If you would rather sign through DocuSign or on paper, reply to this email.</p>
<p style="margin:0;">Python Software Foundation</p>
{% endblock %}

{% block footer %}If you weren't expecting this, you can ignore it.{% endblock %}
6 changes: 4 additions & 2 deletions apps/agreements/tests/test_signing_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, [])
Expand Down Expand Up @@ -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)
Expand Down
Loading