Skip to content

Commit 6c9e185

Browse files
committed
🐛(backend) email invite in receivers language
E-mails sent for granting access are sent in the receiving users language. Falling back to system default language.
1 parent 2194301 commit 6c9e185

File tree

14 files changed

+113
-93
lines changed

14 files changed

+113
-93
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ and this project adheres to
99

1010
## [Unreleased]
1111

12+
## Added
13+
14+
## Changed
15+
16+
## Fixed
17+
18+
- 🐛(backend) invitation e-mails in receivers language #401
19+
1220

1321
## [1.8.0] - 2024-11-25
1422

src/backend/core/api/viewsets.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -668,10 +668,9 @@ class DocumentAccessViewSet(
668668
def perform_create(self, serializer):
669669
"""Add a new access to the document and send an email to the new added user."""
670670
access = serializer.save()
671-
language = self.request.headers.get("Content-Language", "en-us")
672671

673672
access.document.email_invitation(
674-
language,
673+
access.user.language,
675674
access.user.email,
676675
access.role,
677676
self.request.user,
@@ -883,10 +882,11 @@ def perform_create(self, serializer):
883882
"""Save invitation to a document then send an email to the invited user."""
884883
invitation = serializer.save()
885884

886-
language = self.request.headers.get("Content-Language", "en-us")
887-
888885
invitation.document.email_invitation(
889-
language, invitation.email, invitation.role, self.request.user
886+
self.request.user.language,
887+
invitation.email,
888+
invitation.role,
889+
self.request.user,
890890
)
891891

892892

src/backend/core/tests/documents/test_api_document_accesses.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
pytestmark = pytest.mark.django_db
1616

1717

18+
# List
19+
20+
1821
def test_api_document_accesses_list_anonymous():
1922
"""Anonymous users should not be allowed to list document accesses."""
2023
document = factories.DocumentFactory()
@@ -128,6 +131,9 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
128131
)
129132

130133

134+
# Retrieve
135+
136+
131137
def test_api_document_accesses_retrieve_anonymous():
132138
"""
133139
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
216222
}
217223

218224

225+
# Update
226+
227+
219228
def test_api_document_accesses_update_anonymous():
220229
"""Anonymous users should not be allowed to update a document access."""
221230
access = factories.UserDocumentAccessFactory()

src/backend/core/tests/documents/test_api_document_accesses_create.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
pytestmark = pytest.mark.django_db
1717

1818

19+
# Create
20+
21+
1922
def test_api_document_accesses_create_anonymous():
2023
"""Anonymous users should not be allowed to create document accesses."""
2124
document = factories.DocumentFactory()
@@ -123,7 +126,7 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
123126
document=document, team="lasuite", role="administrator"
124127
)
125128

126-
other_user = factories.UserFactory()
129+
other_user = factories.UserFactory(language="en-us")
127130

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

201-
other_user = factories.UserFactory()
204+
other_user = factories.UserFactory(language="en-us")
202205

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

@@ -233,3 +236,72 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
233236
in email_content
234237
)
235238
assert "docs/" + str(document.id) + "/" in email_content
239+
240+
241+
@pytest.mark.parametrize("via", VIA)
242+
def test_api_document_accesses_create_email_in_receivers_language(via, mock_user_teams):
243+
"""
244+
The email sent to the accesses to notify them of the adding, should be in their language.
245+
"""
246+
user = factories.UserFactory()
247+
248+
client = APIClient()
249+
client.force_login(user)
250+
251+
document = factories.DocumentFactory()
252+
if via == USER:
253+
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
254+
elif via == TEAM:
255+
mock_user_teams.return_value = ["lasuite", "unknown"]
256+
factories.TeamDocumentAccessFactory(
257+
document=document, team="lasuite", role="owner"
258+
)
259+
260+
role = random.choice([role[0] for role in models.RoleChoices.choices])
261+
262+
assert len(mail.outbox) == 0
263+
264+
other_users = (
265+
factories.UserFactory(language="en-us"),
266+
factories.UserFactory(language="fr-fr"),
267+
)
268+
269+
for index, other_user in enumerate(other_users):
270+
expected_language = other_user.language
271+
response = client.post(
272+
f"/api/v1.0/documents/{document.id!s}/accesses/",
273+
{
274+
"user_id": str(other_user.id),
275+
"role": role,
276+
},
277+
format="json",
278+
)
279+
280+
assert response.status_code == 201
281+
assert models.DocumentAccess.objects.filter(user=other_user).count() == 1
282+
new_document_access = models.DocumentAccess.objects.filter(
283+
user=other_user
284+
).get()
285+
other_user_data = serializers.UserSerializer(instance=other_user).data
286+
assert response.json() == {
287+
"id": str(new_document_access.id),
288+
"user": other_user_data,
289+
"team": "",
290+
"role": role,
291+
"abilities": new_document_access.get_abilities(user),
292+
}
293+
assert len(mail.outbox) == index + 1
294+
email = mail.outbox[index]
295+
assert email.to == [other_user_data["email"]]
296+
email_content = " ".join(email.body.split())
297+
if expected_language == "en-us":
298+
assert (
299+
f"{user.full_name} shared a document with you: {document.title}"
300+
in email_content
301+
)
302+
elif expected_language == "fr-fr":
303+
assert (
304+
f"{user.full_name} a partagé un document avec vous: {document.title}"
305+
in email_content
306+
)
307+
assert "docs/" + str(document.id) + "/" in email_content

src/backend/core/tests/documents/test_api_document_invitations.py

Lines changed: 5 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ def test_api_document_invitations_create_privileged_members(
368368
Only owners and administrators should be able to invite new users.
369369
Only owners can invite owners.
370370
"""
371-
user = factories.UserFactory()
371+
user = factories.UserFactory(language="en-us")
372372
document = factories.DocumentFactory()
373373
if via == USER:
374374
factories.UserDocumentAccessFactory(document=document, user=user, role=inviting)
@@ -417,11 +417,11 @@ def test_api_document_invitations_create_privileged_members(
417417
}
418418

419419

420-
def test_api_document_invitations_create_email_from_content_language():
420+
def test_api_document_invitations_create_email_from_senders_language():
421421
"""
422-
The email generated is from the language set in the Content-Language header
422+
The email generated is from the language set on the sending user
423423
"""
424-
user = factories.UserFactory()
424+
user = factories.UserFactory(language="fr-fr")
425425
document = factories.DocumentFactory()
426426
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
427427

@@ -439,7 +439,6 @@ def test_api_document_invitations_create_email_from_content_language():
439439
f"/api/v1.0/documents/{document.id!s}/invitations/",
440440
invitation_values,
441441
format="json",
442-
headers={"Content-Language": "fr-fr"},
443442
)
444443

445444
assert response.status_code == 201
@@ -458,53 +457,11 @@ def test_api_document_invitations_create_email_from_content_language():
458457
)
459458

460459

461-
def test_api_document_invitations_create_email_from_content_language_not_supported():
462-
"""
463-
If the language from the Content-Language is not supported
464-
it will display the default language, English.
465-
"""
466-
user = factories.UserFactory()
467-
document = factories.DocumentFactory()
468-
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
469-
470-
invitation_values = {
471-
"email": "guest@example.com",
472-
"role": "reader",
473-
}
474-
475-
assert len(mail.outbox) == 0
476-
477-
client = APIClient()
478-
client.force_login(user)
479-
480-
response = client.post(
481-
f"/api/v1.0/documents/{document.id!s}/invitations/",
482-
invitation_values,
483-
format="json",
484-
headers={"Content-Language": "not-supported"},
485-
)
486-
487-
assert response.status_code == 201
488-
assert response.json()["email"] == "guest@example.com"
489-
assert models.Invitation.objects.count() == 1
490-
assert len(mail.outbox) == 1
491-
492-
email = mail.outbox[0]
493-
494-
assert email.to == ["guest@example.com"]
495-
496-
email_content = " ".join(email.body.split())
497-
assert (
498-
f"{user.full_name} shared a document with you: {document.title}"
499-
in email_content
500-
)
501-
502-
503460
def test_api_document_invitations_create_email_full_name_empty():
504461
"""
505462
If the full name of the user is empty, it will display the email address.
506463
"""
507-
user = factories.UserFactory(full_name="")
464+
user = factories.UserFactory(full_name="", language="en-us")
508465
document = factories.DocumentFactory()
509466
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
510467

src/backend/core/tests/test_api_config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ def test_api_config(is_authenticated):
3838
"CRISP_WEBSITE_ID": "123",
3939
"ENVIRONMENT": "test",
4040
"FRONTEND_THEME": "test-theme",
41-
"LANGUAGES": [["en-us", "English"], ["fr-fr", "French"], ["de-de", "German"]],
41+
"LANGUAGES": [
42+
["en-us", "English"],
43+
["fr-fr", "Français"],
44+
["de-de", "Deutsch"],
45+
],
4246
"LANGUAGE_CODE": "en-us",
4347
"MEDIA_BASE_URL": "http://testserver/",
4448
"SENTRY_DSN": "https://sentry.test/123",

src/backend/core/tests/test_api_users.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ def test_api_users_retrieve_me_authenticated():
163163
"id": str(user.id),
164164
"email": user.email,
165165
"full_name": user.full_name,
166+
"language": user.language,
166167
"short_name": user.short_name,
167168
}
168169

src/backend/impress/settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,9 @@ class Base(Configuration):
232232
# fallback/default languages throughout the app.
233233
LANGUAGES = values.SingleNestedTupleValue(
234234
(
235-
("en-us", _("English")),
236-
("fr-fr", _("French")),
237-
("de-de", _("German")),
235+
("en-us", "English"),
236+
("fr-fr", "Français"),
237+
("de-de", "Deutsch"),
238238
)
239239
)
240240

src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ const config = {
1212
MEDIA_BASE_URL: 'http://localhost:8083',
1313
LANGUAGES: [
1414
['en-us', 'English'],
15-
['fr-fr', 'French'],
16-
['de-de', 'German'],
15+
['fr-fr', 'Français'],
16+
['de-de', 'Deutsch'],
1717
],
1818
LANGUAGE_CODE: 'en-us',
1919
SENTRY_DSN: null,

src/frontend/apps/impress/src/features/docs/members/invitation-list/api/useCreateDocInvitation.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { APIError, errorCauses, fetchAPI } from '@/api';
44
import { User } from '@/core/auth';
55
import { Doc, Role } from '@/features/docs/doc-management';
66
import { OptionType } from '@/features/docs/members/members-add/types';
7-
import { ContentLanguage } from '@/i18n/types';
87

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

@@ -14,20 +13,15 @@ interface CreateDocInvitationParams {
1413
email: User['email'];
1514
role: Role;
1615
docId: Doc['id'];
17-
contentLanguage: ContentLanguage;
1816
}
1917

2018
export const createDocInvitation = async ({
2119
email,
2220
role,
2321
docId,
24-
contentLanguage,
2522
}: CreateDocInvitationParams): Promise<Invitation> => {
2623
const response = await fetchAPI(`documents/${docId}/invitations/`, {
2724
method: 'POST',
28-
headers: {
29-
'Content-Language': contentLanguage,
30-
},
3125
body: JSON.stringify({
3226
email,
3327
role,

src/frontend/apps/impress/src/features/docs/members/members-add/api/useCreateDocAccess.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
Role,
1111
} from '@/features/docs/doc-management';
1212
import { KEY_LIST_DOC_ACCESSES } from '@/features/docs/members/members-list';
13-
import { ContentLanguage } from '@/i18n/types';
1413
import { useBroadcastStore } from '@/stores';
1514

1615
import { OptionType } from '../types';
@@ -21,20 +20,15 @@ interface CreateDocAccessParams {
2120
role: Role;
2221
docId: Doc['id'];
2322
memberId: User['id'];
24-
contentLanguage: ContentLanguage;
2523
}
2624

2725
export const createDocAccess = async ({
2826
memberId,
2927
role,
3028
docId,
31-
contentLanguage,
3229
}: CreateDocAccessParams): Promise<Access> => {
3330
const response = await fetchAPI(`documents/${docId}/accesses/`, {
3431
method: 'POST',
35-
headers: {
36-
'Content-Language': contentLanguage,
37-
},
3832
body: JSON.stringify({
3933
user_id: memberId,
4034
role,

src/frontend/apps/impress/src/features/docs/members/members-add/components/AddMembers.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { APIError } from '@/api';
1010
import { Box, Card, IconBG } from '@/components';
1111
import { Doc, Role } from '@/features/docs/doc-management';
1212
import { useCreateDocInvitation } from '@/features/docs/members/invitation-list/';
13-
import { useLanguage } from '@/i18n/hooks/useLanguage';
1413
import { useResponsiveStore } from '@/stores';
1514

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

3837
export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
39-
const { contentLanguage } = useLanguage();
4038
const { t } = useTranslation();
4139
const { isSmallMobile } = useResponsiveStore();
4240
const [selectedUsers, setSelectedUsers] = useState<OptionsSelect>([]);
@@ -56,7 +54,6 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
5654
email: selectedUser.value.email,
5755
role: selectedRole,
5856
docId: doc.id,
59-
contentLanguage,
6057
});
6158
break;
6259

@@ -65,7 +62,6 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
6562
role: selectedRole,
6663
docId: doc.id,
6764
memberId: selectedUser.value.id,
68-
contentLanguage,
6965
});
7066
break;
7167
}

0 commit comments

Comments
 (0)