✨(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:
committed by
aleb_the_flash
parent
1919dce3a9
commit
522914b47a
@@ -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)
|
||||
|
||||
BIN
src/backend/core/static/images/logo-suite-numerique.png
Normal file
BIN
src/backend/core/static/images/logo-suite-numerique.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
src/backend/core/static/images/logo.png
Normal file
BIN
src/backend/core/static/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src/backend/core/static/images/mail-header-background.png
Normal file
BIN
src/backend/core/static/images/mail-header-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user