From 37cc1feec6c8089f25f94d0f9c6d09dbc57c7035 Mon Sep 17 00:00:00 2001 From: rvveber Date: Mon, 18 Nov 2024 17:55:42 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(backend)=20email=20invite=20in=20r?= =?UTF-8?q?eceivers=20language?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E-mails sent for granting access are sent in the receiving users language. Falling back to system default language. --- CHANGELOG.md | 1 + src/backend/core/api/viewsets.py | 10 +-- .../documents/test_api_document_accesses.py | 9 ++ .../test_api_document_accesses_create.py | 76 +++++++++++++++- .../test_api_document_invitations.py | 87 +------------------ src/backend/core/tests/test_api_users.py | 1 + .../api/useCreateDocInvitation.tsx | 6 -- .../members-add/api/useCreateDocAccess.tsx | 6 -- .../members-add/components/AddMembers.tsx | 4 - .../impress/src/i18n/hooks/useLanguage.tsx | 15 ---- src/frontend/apps/impress/src/i18n/types.ts | 2 +- 11 files changed, 93 insertions(+), 124 deletions(-) delete mode 100644 src/frontend/apps/impress/src/i18n/hooks/useLanguage.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f418b8226..a6b92b82a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index ad5acec0a..c20e15879 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -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, @@ -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, ) diff --git a/src/backend/core/tests/documents/test_api_document_accesses.py b/src/backend/core/tests/documents/test_api_document_accesses.py index 9d04d9240..7ac12578b 100644 --- a/src/backend/core/tests/documents/test_api_document_accesses.py +++ b/src/backend/core/tests/documents/test_api_document_accesses.py @@ -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() @@ -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. @@ -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() diff --git a/src/backend/core/tests/documents/test_api_document_accesses_create.py b/src/backend/core/tests/documents/test_api_document_accesses_create.py index bd96d04dd..43f40299a 100644 --- a/src/backend/core/tests/documents/test_api_document_accesses_create.py +++ b/src/backend/core/tests/documents/test_api_document_accesses_create.py @@ -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() @@ -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( @@ -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]) @@ -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 diff --git a/src/backend/core/tests/documents/test_api_document_invitations.py b/src/backend/core/tests/documents/test_api_document_invitations.py index 1b9e61688..4c4a5fe06 100644 --- a/src/backend/core/tests/documents/test_api_document_invitations.py +++ b/src/backend/core/tests/documents/test_api_document_invitations.py @@ -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) @@ -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": "guest@example.com", - "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"] == "guest@example.com" - assert models.Invitation.objects.count() == 1 - assert len(mail.outbox) == 1 - - email = mail.outbox[0] - - assert email.to == ["guest@example.com"] - - 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": "guest@example.com", - "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"] == "guest@example.com" - assert models.Invitation.objects.count() == 1 - assert len(mail.outbox) == 1 - - email = mail.outbox[0] - - assert email.to == ["guest@example.com"] - - 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") diff --git a/src/backend/core/tests/test_api_users.py b/src/backend/core/tests/test_api_users.py index e739d4d1f..321d0c011 100644 --- a/src/backend/core/tests/test_api_users.py +++ b/src/backend/core/tests/test_api_users.py @@ -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, } diff --git a/src/frontend/apps/impress/src/features/docs/members/invitation-list/api/useCreateDocInvitation.tsx b/src/frontend/apps/impress/src/features/docs/members/invitation-list/api/useCreateDocInvitation.tsx index 85aa80d88..6c63e32fb 100644 --- a/src/frontend/apps/impress/src/features/docs/members/invitation-list/api/useCreateDocInvitation.tsx +++ b/src/frontend/apps/impress/src/features/docs/members/invitation-list/api/useCreateDocInvitation.tsx @@ -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'; @@ -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 => { const response = await fetchAPI(`documents/${docId}/invitations/`, { method: 'POST', - headers: { - 'Content-Language': contentLanguage, - }, body: JSON.stringify({ email, role, diff --git a/src/frontend/apps/impress/src/features/docs/members/members-add/api/useCreateDocAccess.tsx b/src/frontend/apps/impress/src/features/docs/members/members-add/api/useCreateDocAccess.tsx index a2230b3de..ca3537a1b 100644 --- a/src/frontend/apps/impress/src/features/docs/members/members-add/api/useCreateDocAccess.tsx +++ b/src/frontend/apps/impress/src/features/docs/members/members-add/api/useCreateDocAccess.tsx @@ -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'; @@ -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 => { const response = await fetchAPI(`documents/${docId}/accesses/`, { method: 'POST', - headers: { - 'Content-Language': contentLanguage, - }, body: JSON.stringify({ user_id: memberId, role, diff --git a/src/frontend/apps/impress/src/features/docs/members/members-add/components/AddMembers.tsx b/src/frontend/apps/impress/src/features/docs/members/members-add/components/AddMembers.tsx index 3b8fddb30..970d8ff31 100644 --- a/src/frontend/apps/impress/src/features/docs/members/members-add/components/AddMembers.tsx +++ b/src/frontend/apps/impress/src/features/docs/members/members-add/components/AddMembers.tsx @@ -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'; @@ -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([]); @@ -56,7 +54,6 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => { email: selectedUser.value.email, role: selectedRole, docId: doc.id, - contentLanguage, }); break; @@ -65,7 +62,6 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => { role: selectedRole, docId: doc.id, memberId: selectedUser.value.id, - contentLanguage, }); break; } diff --git a/src/frontend/apps/impress/src/i18n/hooks/useLanguage.tsx b/src/frontend/apps/impress/src/i18n/hooks/useLanguage.tsx deleted file mode 100644 index 4879bda8e..000000000 --- a/src/frontend/apps/impress/src/i18n/hooks/useLanguage.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -import { ContentLanguage } from '../types'; - -export const useLanguage = (): { - language: string; - contentLanguage: ContentLanguage; -} => { - const { i18n } = useTranslation(); - - return { - language: i18n.language, - contentLanguage: i18n.language === 'fr' ? 'fr-fr' : 'en-us', - }; -}; diff --git a/src/frontend/apps/impress/src/i18n/types.ts b/src/frontend/apps/impress/src/i18n/types.ts index d9b0064aa..ccedc350c 100644 --- a/src/frontend/apps/impress/src/i18n/types.ts +++ b/src/frontend/apps/impress/src/i18n/types.ts @@ -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';