(backend) email invitation to new users

When generating an Invitation object within the database, our intention
is to promptly notify the user via email. We send them an invitation
to join Desk.

This code is inspired by Joanie successful order flow.

Johann's design was missing a link to Desk, I simply added a button which
redirect to the staging url. This url is hardcoded, we should refactor it
when we will deploy Desk in pre-prod or prod environments.

Johann's design relied on Marianne font. I implemented a simpler version,
which uses a google font. That's not important for MVP.

Look and feel of this first invitation template is enough to make our PoC
functionnal, which is the more important.
This commit is contained in:
Lebaud Antoine
2024-03-19 23:43:59 +01:00
committed by aleb_the_flash
parent 1919dce3a9
commit 522914b47a
10 changed files with 159 additions and 41 deletions

View File

@@ -4,22 +4,28 @@ Declare and configure the models for the People core application
import json
import os
import smtplib
import uuid
from datetime import timedelta
from logging import getLogger
from django.conf import settings
from django.contrib.auth import models as auth_models
from django.contrib.auth.base_user import AbstractBaseUser
from django.core import exceptions, mail, validators
from django.db import models
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.functional import lazy
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django.utils.translation import override
import jsonschema
from timezone_field import TimeZoneField
logger = getLogger(__name__)
current_dir = os.path.dirname(os.path.abspath(__file__))
contact_schema_path = os.path.join(current_dir, "jsonschema", "contact_data.json")
with open(contact_schema_path, "r", encoding="utf-8") as contact_schema_file:
@@ -516,6 +522,8 @@ class Invitation(BaseModel):
raise exceptions.PermissionDenied()
super().save(*args, **kwargs)
self.email_invitation()
def clean(self):
"""Validate fields."""
super().clean()
@@ -559,3 +567,24 @@ class Invitation(BaseModel):
"patch": False,
"put": False,
}
def email_invitation(self):
"""Email invitation to the user."""
try:
with override(self.issuer.language):
template_vars = {
"title": _("Invitation to join Desk!"),
}
msg_html = render_to_string("mail/html/invitation.html", template_vars)
msg_plain = render_to_string("mail/text/invitation.txt", template_vars)
mail.send_mail(
_("Invitation to join Desk!"),
msg_plain,
settings.EMAIL_FROM,
[self.email],
html_message=msg_html,
fail_silently=False,
)
except smtplib.SMTPException as exception:
logger.error("invitation to %s was not sent: %s", self.email, exception)

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -2,11 +2,14 @@
Unit tests for the Invitation model
"""
import smtplib
import time
import uuid
from logging import Logger
from unittest import mock
from django.contrib.auth.models import AnonymousUser
from django.core import exceptions
from django.core import exceptions, mail
import pytest
from faker import Faker
@@ -236,3 +239,64 @@ def test_models_team_invitations_get_abilities_member():
"patch": False,
"put": False,
}
def test_models_team_invitations_email():
"""Check email invitation during invitation creation."""
member_access = factories.TeamAccessFactory(role="member")
team = member_access.team
# pylint: disable-next=no-member
assert len(mail.outbox) == 0
factories.TeamAccessFactory(team=team)
invitation = factories.InvitationFactory(team=team, email="john@people.com")
# pylint: disable-next=no-member
assert len(mail.outbox) == 1
# pylint: disable-next=no-member
(email,) = mail.outbox
assert email.to == [invitation.email]
assert email.subject == "Invitation to join Desk!"
email_content = " ".join(email.body.split())
assert "Invitation to join Desk!" in email_content
@mock.patch(
"django.core.mail.send_mail",
side_effect=smtplib.SMTPException("Error SMTPException"),
)
@mock.patch.object(Logger, "error")
def test_models_team_invitations_email_failed(mock_logger, _mock_send_mail):
"""Check invitation behavior when an SMTP error occurs during invitation creation."""
member_access = factories.TeamAccessFactory(role="member")
team = member_access.team
# pylint: disable-next=no-member
assert len(mail.outbox) == 0
factories.TeamAccessFactory(team=team)
# No error should be raised
invitation = factories.InvitationFactory(team=team, email="john@people.com")
# No email has been sent
# pylint: disable-next=no-member
assert len(mail.outbox) == 0
# Logger should be called
mock_logger.assert_called_once()
(
_,
email,
exception,
) = mock_logger.call_args.args
assert email == invitation.email
assert isinstance(exception, smtplib.SMTPException)

View File

@@ -9,13 +9,13 @@ from .views import (
urlpatterns = [
path(
"__debug__/mail/hello_html",
"__debug__/mail/invitation_html",
DebugViewHtml.as_view(),
name="debug.mail.hello_html",
name="debug.mail.invitation_html",
),
path(
"__debug__/mail/hello_txt",
"__debug__/mail/invitation_txt",
DebugViewTxt.as_view(),
name="debug.mail.hello_txt",
name="debug.mail.invitation_txt",
),
]

View File

@@ -11,8 +11,6 @@ class DebugBaseView(TemplateView):
context = super().get_context_data(**kwargs)
context["title"] = "Development email preview"
context["email"] = "random@gmail.com"
context["fullname"] = "robert"
return context
@@ -20,10 +18,10 @@ class DebugBaseView(TemplateView):
class DebugViewHtml(DebugBaseView):
"""Debug View for HTML Email Layout"""
template_name = "mail/html/hello.html"
template_name = "mail/html/invitation.html"
class DebugViewTxt(DebugBaseView):
"""Debug View for Text Email Layout"""
template_name = "mail/text/hello.txt"
template_name = "mail/text/invitation.txt"

View File

@@ -1,28 +0,0 @@
<mjml>
<mj-include path="./partial/header.mjml" />
<mj-body mj-class="bg--blue-100">
<mj-wrapper css-class="wrapper" padding="20px 40px 40px 40px">
<mj-section>
<mj-column>
<mj-image src="{% base64_static 'people/images/logo_people.png' %}" width="200px" align="left" alt="{%trans 'Company logo' %}" />
</mj-column>
</mj-section>
<mj-section mj-class="bg--blue-100" border-radius="6px 6px 0 0" padding="30px 50px 60px 50px">
<mj-column>
<mj-text padding="0">
<p>
{%if fullname%}
{% blocktranslate with name=fullname %}Hello {{ name }}{% endblocktranslate %}
{% else %}
{%trans "Hello" %}
{% endif %}<br/>
<strong>{%trans "Thank you very much for your visit!"%}</strong>
</p>
</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
<mj-include path="./partial/footer.mjml" />
</mj-body>
</mjml>

View File

@@ -0,0 +1,57 @@
<mjml>
<mj-include path="./partial/header.mjml" />
<mj-body mj-class="bg--blue-100">
<mj-wrapper css-class="wrapper" padding="0 40px 40px 40px">
<mj-section background-url="{% base64_static 'images/mail-header-background.png' %}" background-size="cover" background-repeat="no-repeat" background-position="0 -30px">
<mj-column>
<mj-image align="center" src="{% base64_static 'images/logo-suite-numerique.png' %}" width="250px" align="left" alt="{%trans 'La Suite Numérique' %}" />
</mj-column>
</mj-section>
<mj-section mj-class="bg--white-100" padding="30px 20px 60px 20px">
<mj-column>
<mj-text font-size="14px">
<p>{% trans "Invitation to join a team" %}</p>
</mj-text>
<!-- Welcome Message -->
<mj-text>
<h1>{% trans "Welcome to" %} <strong>Equipes</strong></h1>
</mj-text>
<mj-divider border-width="1px" border-style="solid" border-color="#DDDDDD" width="30%" align="left"/>
<mj-image src="{% base64_static 'images/logo.png' %}" width="157px" align="left" alt="{%trans 'Logo' %}" />
<!-- Main Message -->
<mj-text>{% trans "We are delighted to welcome you to our community on Equipes, your new companion to simplify the management of your groups efficiently, intuitively, and securely." %}</mj-text>
<mj-text>{% trans "Our application is designed to help you organize, collaborate, and manage permissions." %}</mj-text>
<mj-text>
{% trans "With Equipes, you will be able to:" %}
<ul>
<li>{% trans "Create customized groups according to your specific needs."%}</li>
<li>{% trans "Invite members of your team or community in just a few clicks."%}</li>
<li>{% trans "Plan events, meetings, or activities effortlessly with our integrated calendar."%}</li>
<li>{% trans "Share documents, photos, and important information securely."%}</li>
<li>{% trans "Facilitate exchanges and communication with our messaging and group discussion tools."%}</li>
</ul>
</mj-text>
<mj-button href="https://desk-staging.beta.numerique.gouv.fr/" background-color="#000091" color="white" padding-bottom="30px">
{% trans "Visit Equipes"%}
</mj-button>
<mj-text>{% trans "We are confident that Equipes will help you increase efficiency and productivity while strengthening the bond among members." %}</mj-text>
<mj-text>{% trans "Feel free to explore all the features of the application and share your feedback and suggestions with us. Your feedback is valuable to us and will enable us to continually improve our service." %}</mj-text>
<mj-text>{% trans "Once again, welcome aboard! We are eager to accompany you on this group management adventure." %}</mj-text>
<!-- Signature -->
<mj-text>
<p>{% trans "Sincerely," %}</p>
<p>{% trans "The La Suite Numérique Team" %}</p>
</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
</mj-body>
<mj-include path="./partial/footer.mjml" />
</mjml>

View File

@@ -14,10 +14,8 @@
font-family="Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif"
font-size="16px"
line-height="1.5em"
color="#031963"
color="#3A3A3A"
/>
<mj-class name="text--small" font-size="0.875rem" />
<mj-class name="bg--blue-100" background-color="#EDF5FA" />
</mj-attributes>
<mj-style>
/* Reset */
@@ -33,7 +31,7 @@
<mj-style>
/* Global styles */
h1 {
color: #055FD2;
color: #161616;
font-size: 2rem;
line-height: 1em;
font-weight: 700;