Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

♻️Change email content #283

Merged
merged 1 commit into from
Sep 26, 2024
Merged
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
♻️(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.
  • Loading branch information
AntoLC committed Sep 25, 2024
commit 17df512d7cea56b8d28b089b7b6a5e8a0de2e845
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ and this project adheres to
## Changed

- 💄(frontend) error alert closeable on editor #284
- ♻️(backend) Change email content #283

## Fixed

10 changes: 6 additions & 4 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
@@ -31,7 +31,6 @@
)

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 @@ def perform_create(self, serializer):
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
)
38 changes: 38 additions & 0 deletions src/backend/core/models.py
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
"""

import hashlib
import smtplib
import tempfile
import textwrap
import uuid
@@ -13,16 +14,20 @@
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 @@ def get_abilities(self, user):
"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):
"""
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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():
96 changes: 96 additions & 0 deletions src/backend/core/tests/test_models_documents.py
Original file line number Diff line number Diff line change
@@ -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)
87 changes: 0 additions & 87 deletions src/backend/core/tests/test_utils.py

This file was deleted.

40 changes: 0 additions & 40 deletions src/backend/core/utils.py

This file was deleted.

Binary file modified src/backend/locale/en_US/LC_MESSAGES/django.mo
Binary file not shown.
Loading
Loading