Skip to content

Commit

Permalink
🐛(backend) email invite in receivers language
Browse files Browse the repository at this point in the history
E-mails sent for granting access are sent
in the receiving users language.
Falling back to system default language.
  • Loading branch information
rvveber committed Nov 25, 2024
1 parent 09d02b7 commit 37cc1fe
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 124 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to
- 🦺(backend) add comma to sub regex #408
- 🐛(editor) collaborative user tag hidden when read only #385
- 🐛(frontend) users have view access when revoked #387
- 🐛(backend) invitation e-mails in receivers language #401


## [1.7.0] - 2024-10-24
Expand Down
10 changes: 5 additions & 5 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,10 +668,9 @@ 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")

access.document.email_invitation(
language,
access.user.language,
access.user.email,
access.role,
self.request.user,
Expand Down Expand Up @@ -883,10 +882,11 @@ def perform_create(self, serializer):
"""Save invitation to a document then send an email to the invited user."""
invitation = serializer.save()

language = self.request.headers.get("Content-Language", "en-us")

invitation.document.email_invitation(
language, invitation.email, invitation.role, self.request.user
self.request.user.language,
invitation.email,
invitation.role,
self.request.user,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
pytestmark = pytest.mark.django_db


# List


def test_api_document_accesses_list_anonymous():
"""Anonymous users should not be allowed to list document accesses."""
document = factories.DocumentFactory()
Expand Down Expand Up @@ -128,6 +131,9 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
)


# Retrieve


def test_api_document_accesses_retrieve_anonymous():
"""
Anonymous users should not be allowed to retrieve a document access.
Expand Down Expand Up @@ -216,6 +222,9 @@ def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_tea
}


# Update


def test_api_document_accesses_update_anonymous():
"""Anonymous users should not be allowed to update a document access."""
access = factories.UserDocumentAccessFactory()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
pytestmark = pytest.mark.django_db


# Create


def test_api_document_accesses_create_anonymous():
"""Anonymous users should not be allowed to create document accesses."""
document = factories.DocumentFactory()
Expand Down Expand Up @@ -123,7 +126,7 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
document=document, team="lasuite", role="administrator"
)

other_user = factories.UserFactory()
other_user = factories.UserFactory(language="en-us")

# It should not be allowed to create an owner access
response = client.post(
Expand Down Expand Up @@ -198,7 +201,7 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
document=document, team="lasuite", role="owner"
)

other_user = factories.UserFactory()
other_user = factories.UserFactory(language="en-us")

role = random.choice([role[0] for role in models.RoleChoices.choices])

Expand Down Expand Up @@ -233,3 +236,72 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
in email_content
)
assert "docs/" + str(document.id) + "/" in email_content


@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_create_email_in_receivers_language(via, mock_user_teams):
"""
The email sent to the accesses to notify them of the adding, should be in their language.
"""
user = factories.UserFactory()

client = APIClient()
client.force_login(user)

document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)

role = random.choice([role[0] for role in models.RoleChoices.choices])

assert len(mail.outbox) == 0

other_users = (
factories.UserFactory(language="en-us"),
factories.UserFactory(language="fr-fr"),
)

for index, other_user in enumerate(other_users):
expected_language = other_user.language
response = client.post(
f"/api/v1.0/documents/{document.id!s}/accesses/",
{
"user_id": str(other_user.id),
"role": role,
},
format="json",
)

assert response.status_code == 201
assert models.DocumentAccess.objects.filter(user=other_user).count() == 1
new_document_access = models.DocumentAccess.objects.filter(
user=other_user
).get()
other_user_data = serializers.UserSerializer(instance=other_user).data
assert response.json() == {
"id": str(new_document_access.id),
"user": other_user_data,
"team": "",
"role": role,
"abilities": new_document_access.get_abilities(user),
}
assert len(mail.outbox) == index + 1
email = mail.outbox[index]
assert email.to == [other_user_data["email"]]
email_content = " ".join(email.body.split())
if expected_language == "en-us":
assert (
f"{user.full_name} shared a document with you: {document.title}"
in email_content
)
elif expected_language == "fr-fr":
assert (
f"{user.full_name} a partagé un document avec vous: {document.title}"
in email_content
)
assert "docs/" + str(document.id) + "/" in email_content
87 changes: 2 additions & 85 deletions src/backend/core/tests/documents/test_api_document_invitations.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ def test_api_document_invitations_create_privileged_members(
Only owners and administrators should be able to invite new users.
Only owners can invite owners.
"""
user = factories.UserFactory()
user = factories.UserFactory(language="en-us")
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=inviting)
Expand Down Expand Up @@ -417,94 +417,11 @@ def test_api_document_invitations_create_privileged_members(
}


def test_api_document_invitations_create_email_from_content_language():
"""
The email generated is from the language set in the Content-Language header
"""
user = factories.UserFactory()
document = factories.DocumentFactory()
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")

invitation_values = {
"email": "[email protected]",
"role": "reader",
}

assert len(mail.outbox) == 0

client = APIClient()
client.force_login(user)

response = client.post(
f"/api/v1.0/documents/{document.id!s}/invitations/",
invitation_values,
format="json",
headers={"Content-Language": "fr-fr"},
)

assert response.status_code == 201
assert response.json()["email"] == "[email protected]"
assert models.Invitation.objects.count() == 1
assert len(mail.outbox) == 1

email = mail.outbox[0]

assert email.to == ["[email protected]"]

email_content = " ".join(email.body.split())
assert (
f"{user.full_name} a partagé un document avec vous: {document.title}"
in email_content
)


def test_api_document_invitations_create_email_from_content_language_not_supported():
"""
If the language from the Content-Language is not supported
it will display the default language, English.
"""
user = factories.UserFactory()
document = factories.DocumentFactory()
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")

invitation_values = {
"email": "[email protected]",
"role": "reader",
}

assert len(mail.outbox) == 0

client = APIClient()
client.force_login(user)

response = client.post(
f"/api/v1.0/documents/{document.id!s}/invitations/",
invitation_values,
format="json",
headers={"Content-Language": "not-supported"},
)

assert response.status_code == 201
assert response.json()["email"] == "[email protected]"
assert models.Invitation.objects.count() == 1
assert len(mail.outbox) == 1

email = mail.outbox[0]

assert email.to == ["[email protected]"]

email_content = " ".join(email.body.split())
assert (
f"{user.full_name} shared a document with you: {document.title}"
in email_content
)


def test_api_document_invitations_create_email_full_name_empty():
"""
If the full name of the user is empty, it will display the email address.
"""
user = factories.UserFactory(full_name="")
user = factories.UserFactory(full_name="", language="en-us")
document = factories.DocumentFactory()
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")

Expand Down
1 change: 1 addition & 0 deletions src/backend/core/tests/test_api_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ def test_api_users_retrieve_me_authenticated():
"id": str(user.id),
"email": user.email,
"full_name": user.full_name,
"language": user.language,
"short_name": user.short_name,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { APIError, errorCauses, fetchAPI } from '@/api';
import { User } from '@/core/auth';
import { Doc, Role } from '@/features/docs/doc-management';
import { OptionType } from '@/features/docs/members/members-add/types';
import { ContentLanguage } from '@/i18n/types';

import { Invitation } from '../types';

Expand All @@ -14,20 +13,15 @@ interface CreateDocInvitationParams {
email: User['email'];
role: Role;
docId: Doc['id'];
contentLanguage: ContentLanguage;
}

export const createDocInvitation = async ({
email,
role,
docId,
contentLanguage,
}: CreateDocInvitationParams): Promise<Invitation> => {
const response = await fetchAPI(`documents/${docId}/invitations/`, {
method: 'POST',
headers: {
'Content-Language': contentLanguage,
},
body: JSON.stringify({
email,
role,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
Role,
} from '@/features/docs/doc-management';
import { KEY_LIST_DOC_ACCESSES } from '@/features/docs/members/members-list';
import { ContentLanguage } from '@/i18n/types';
import { useBroadcastStore } from '@/stores';

import { OptionType } from '../types';
Expand All @@ -21,20 +20,15 @@ interface CreateDocAccessParams {
role: Role;
docId: Doc['id'];
memberId: User['id'];
contentLanguage: ContentLanguage;
}

export const createDocAccess = async ({
memberId,
role,
docId,
contentLanguage,
}: CreateDocAccessParams): Promise<Access> => {
const response = await fetchAPI(`documents/${docId}/accesses/`, {
method: 'POST',
headers: {
'Content-Language': contentLanguage,
},
body: JSON.stringify({
user_id: memberId,
role,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { APIError } from '@/api';
import { Box, Card, IconBG } from '@/components';
import { Doc, Role } from '@/features/docs/doc-management';
import { useCreateDocInvitation } from '@/features/docs/members/invitation-list/';
import { useLanguage } from '@/i18n/hooks/useLanguage';
import { useResponsiveStore } from '@/stores';

import { useCreateDocAccess } from '../api';
Expand All @@ -36,7 +35,6 @@ interface ModalAddMembersProps {
}

export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
const { contentLanguage } = useLanguage();
const { t } = useTranslation();
const { isSmallMobile } = useResponsiveStore();
const [selectedUsers, setSelectedUsers] = useState<OptionsSelect>([]);
Expand All @@ -56,7 +54,6 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
email: selectedUser.value.email,
role: selectedRole,
docId: doc.id,
contentLanguage,
});
break;

Expand All @@ -65,7 +62,6 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
role: selectedRole,
docId: doc.id,
memberId: selectedUser.value.id,
contentLanguage,
});
break;
}
Expand Down
15 changes: 0 additions & 15 deletions src/frontend/apps/impress/src/i18n/hooks/useLanguage.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/frontend/apps/impress/src/i18n/types.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// See: https://github.com/numerique-gouv/impress/blob/ac58341984c99c10ebfac7f8bbe1e8756c48e4d4/src/backend/impress/settings.py#L156-L161
export type ContentLanguage = 'en-us' | 'fr-fr';
export type UserLanguage = 'en-us' | 'fr-fr';

0 comments on commit 37cc1fe

Please sign in to comment.