♻️(backend) change email invitation content
Change the email invitation content. More document related variables are added. To benefit of the document inheritance, we moved the function email_invitation to the document model.
This commit is contained in:
@@ -31,7 +31,6 @@ from rest_framework import (
|
||||
)
|
||||
|
||||
from core import models
|
||||
from core.utils import email_invitation
|
||||
|
||||
from . import permissions, serializers, utils
|
||||
|
||||
@@ -567,9 +566,10 @@ class DocumentAccessViewSet(
|
||||
def perform_create(self, serializer):
|
||||
"""Add a new access to the document and send an email to the new added user."""
|
||||
access = serializer.save()
|
||||
|
||||
language = self.request.headers.get("Content-Language", "en-us")
|
||||
email_invitation(language, access.user.email, access.document.id)
|
||||
access.document.email_invitation(
|
||||
language, access.user.email, access.role, self.request.user.email
|
||||
)
|
||||
|
||||
|
||||
class TemplateViewSet(
|
||||
@@ -769,4 +769,6 @@ class InvitationViewset(
|
||||
invitation = serializer.save()
|
||||
|
||||
language = self.request.headers.get("Content-Language", "en-us")
|
||||
email_invitation(language, invitation.email, invitation.document.id)
|
||||
invitation.document.email_invitation(
|
||||
language, invitation.email, invitation.role, self.request.user.email
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ Declare and configure the models for the impress core application
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import smtplib
|
||||
import tempfile
|
||||
import textwrap
|
||||
import uuid
|
||||
@@ -13,16 +14,20 @@ 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.contrib.sites.models import Site
|
||||
from django.core import exceptions, mail, validators
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.mail import send_mail
|
||||
from django.db import models
|
||||
from django.http import FileResponse
|
||||
from django.template.base import Template as DjangoTemplate
|
||||
from django.template.context import Context
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import html, timezone
|
||||
from django.utils.functional import cached_property, lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import override
|
||||
|
||||
import frontmatter
|
||||
import markdown
|
||||
@@ -522,6 +527,39 @@ class Document(BaseModel):
|
||||
"versions_retrieve": can_get_versions,
|
||||
}
|
||||
|
||||
def email_invitation(self, language, email, role, username_sender):
|
||||
"""Send email invitation."""
|
||||
|
||||
domain = Site.objects.get_current().domain
|
||||
|
||||
try:
|
||||
with override(language):
|
||||
title = _("%(username)s shared a document with you: %(document)s") % {
|
||||
"username": username_sender,
|
||||
"document": self.title,
|
||||
}
|
||||
template_vars = {
|
||||
"title": title,
|
||||
"domain": domain,
|
||||
"document": self,
|
||||
"link": f"{domain}/docs/{self.id}/",
|
||||
"username": username_sender,
|
||||
"role": RoleChoices(role).label.lower(),
|
||||
}
|
||||
msg_html = render_to_string("mail/html/invitation.html", template_vars)
|
||||
msg_plain = render_to_string("mail/text/invitation.txt", template_vars)
|
||||
send_mail(
|
||||
title,
|
||||
msg_plain,
|
||||
settings.EMAIL_FROM,
|
||||
[email],
|
||||
html_message=msg_html,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
except smtplib.SMTPException as exception:
|
||||
logger.error("invitation to %s was not sent: %s", email, exception)
|
||||
|
||||
|
||||
class LinkTrace(BaseModel):
|
||||
"""
|
||||
|
||||
@@ -171,7 +171,7 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
|
||||
email = mail.outbox[0]
|
||||
assert email.to == [other_user["email"]]
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Docs!" in email_content
|
||||
assert f"{user.email} shared a document with you: {document.title}" in email_content
|
||||
assert "docs/" + str(document.id) + "/" in email_content
|
||||
|
||||
|
||||
@@ -225,5 +225,5 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
|
||||
email = mail.outbox[0]
|
||||
assert email.to == [other_user["email"]]
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Docs!" in email_content
|
||||
assert f"{user.email} shared a document with you: {document.title}" in email_content
|
||||
assert "docs/" + str(document.id) + "/" in email_content
|
||||
|
||||
@@ -118,7 +118,10 @@ def test_api_document_invitations__create__privileged_members(
|
||||
email = mail.outbox[0]
|
||||
assert email.to == ["guest@example.com"]
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Docs!" in email_content
|
||||
assert (
|
||||
f"{user.email} shared a document with you: {document.title}"
|
||||
in email_content
|
||||
)
|
||||
else:
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert models.Invitation.objects.exists() is False
|
||||
@@ -158,7 +161,10 @@ def test_api_document_invitations__create__email_from_content_language():
|
||||
assert email.to == ["guest@example.com"]
|
||||
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation à rejoindre Docs !" in email_content
|
||||
assert (
|
||||
f"{user.email} a partagé un document avec vous: {document.title}"
|
||||
in email_content
|
||||
)
|
||||
|
||||
|
||||
def test_api_document_invitations__create__email_from_content_language_not_supported():
|
||||
@@ -196,7 +202,7 @@ def test_api_document_invitations__create__email_from_content_language_not_suppo
|
||||
assert email.to == ["guest@example.com"]
|
||||
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Docs!" in email_content
|
||||
assert f"{user.email} shared a document with you: {document.title}" in email_content
|
||||
|
||||
|
||||
def test_api_document_invitations__create__issuer_and_document_override():
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
Unit tests for the Document model
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core import mail
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
@@ -322,3 +327,94 @@ def test_models_documents_version_duplicate():
|
||||
Bucket=default_storage.bucket_name, Prefix=file_key
|
||||
)
|
||||
assert len(response["Versions"]) == 2
|
||||
|
||||
|
||||
def test_models_documents__email_invitation__success():
|
||||
"""
|
||||
The email invitation is sent successfully.
|
||||
"""
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
document.email_invitation(
|
||||
"en", "guest@example.com", models.RoleChoices.EDITOR, "sender@example.com"
|
||||
)
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == ["guest@example.com"]
|
||||
email_content = " ".join(email.body.split())
|
||||
|
||||
assert (
|
||||
f"sender@example.com invited you as an editor on the following document : {document.title}"
|
||||
in email_content
|
||||
)
|
||||
assert f"docs/{document.id}/" in email_content
|
||||
|
||||
|
||||
def test_models_documents__email_invitation__success_fr():
|
||||
"""
|
||||
The email invitation is sent successfully in french.
|
||||
"""
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
document.email_invitation(
|
||||
"fr-fr", "guest2@example.com", models.RoleChoices.OWNER, "sender2@example.com"
|
||||
)
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == ["guest2@example.com"]
|
||||
email_content = " ".join(email.body.split())
|
||||
|
||||
assert (
|
||||
f"sender2@example.com vous a invité en tant que propriétaire "
|
||||
f"sur le document suivant : {document.title}" in email_content
|
||||
)
|
||||
assert f"docs/{document.id}/" in email_content
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"core.models.send_mail",
|
||||
side_effect=smtplib.SMTPException("Error SMTPException"),
|
||||
)
|
||||
@mock.patch.object(Logger, "error")
|
||||
def test_models_documents__email_invitation__failed(mock_logger, _mock_send_mail):
|
||||
"""Check mail behavior when an SMTP error occurs when sent an email invitation."""
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
document.email_invitation(
|
||||
"en", "guest3@example.com", models.RoleChoices.ADMIN, "sender3@example.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 == "guest3@example.com"
|
||||
assert isinstance(exception, smtplib.SMTPException)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"""
|
||||
Unit tests for the Invitation model
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
|
||||
from django.core import mail
|
||||
|
||||
import pytest
|
||||
|
||||
from core.utils import email_invitation
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_utils__email_invitation_success():
|
||||
"""
|
||||
The email invitation is sent successfully.
|
||||
"""
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
email_invitation("en", "guest@example.com", "123-456-789")
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == ["guest@example.com"]
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Docs!" in email_content
|
||||
assert "docs/123-456-789/" in email_content
|
||||
|
||||
|
||||
def test_utils__email_invitation_success_fr():
|
||||
"""
|
||||
The email invitation is sent successfully in french.
|
||||
"""
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
email_invitation("fr-fr", "guest@example.com", "123-456-789")
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == ["guest@example.com"]
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation à rejoindre Docs !" in email_content
|
||||
assert "docs/123-456-789/" in email_content
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"core.utils.send_mail",
|
||||
side_effect=smtplib.SMTPException("Error SMTPException"),
|
||||
)
|
||||
@mock.patch.object(Logger, "error")
|
||||
def test_utils__email_invitation_failed(mock_logger, _mock_send_mail):
|
||||
"""Check mail behavior when an SMTP error occurs when sent an email invitation."""
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
email_invitation("en", "guest@example.com", "123-456-789")
|
||||
|
||||
# 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 == "guest@example.com"
|
||||
assert isinstance(exception, smtplib.SMTPException)
|
||||
@@ -1,40 +0,0 @@
|
||||
"""
|
||||
Utilities for the core app.
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import override
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
def email_invitation(language, email, document_id):
|
||||
"""Send email invitation."""
|
||||
try:
|
||||
with override(language):
|
||||
title = _("Invitation to join Docs!")
|
||||
template_vars = {
|
||||
"title": title,
|
||||
"site": Site.objects.get_current(),
|
||||
"document_id": document_id,
|
||||
}
|
||||
msg_html = render_to_string("mail/html/invitation.html", template_vars)
|
||||
msg_plain = render_to_string("mail/text/invitation.txt", template_vars)
|
||||
send_mail(
|
||||
title,
|
||||
msg_plain,
|
||||
settings.EMAIL_FROM,
|
||||
[email],
|
||||
html_message=msg_html,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
except smtplib.SMTPException as exception:
|
||||
logger.error("invitation to %s was not sent: %s", email, exception)
|
||||
Reference in New Issue
Block a user