diff --git a/docs/admin/deployment.rst b/docs/admin/deployment.rst index a7f64b68e0..f2225fef3b 100644 --- a/docs/admin/deployment.rst +++ b/docs/admin/deployment.rst @@ -161,6 +161,10 @@ you create: It allows to explain what type of communication to expect and to link to deployment-specific privacy notices among other things. +``EMAIL_COMMUNICATIONS_FOOTER_PRE_TEXT`` + Optional. Text to be shown in the footer of the non-transactional emails sent + using the Messaging Center, just above the unsubscribe text. + ``ENABLE_BUGS_TAB`` Optional. Enables Bugs tab on team pages, which pulls team data from bugzilla.mozilla.org. Specific for Mozilla deployments. @@ -296,6 +300,12 @@ you create: Optional. Set your `SYSTRAN Translate API key` to use machine translation by SYSTRAN. +``TBX_DESCRIPTION`` + Optional. Description to be used in the header of the Terminology (.TBX) files. + +``TBX_TITLE`` + Optional. Title to be used in the header of the Terminology (.TBX) files. + ``THROTTLE_ENABLED`` Optional. Enables traffic throttling based on IP address (default: ``False``). diff --git a/pontoon/messaging/views.py b/pontoon/messaging/views.py index 0ce41b21d3..1aec239bb8 100644 --- a/pontoon/messaging/views.py +++ b/pontoon/messaging/views.py @@ -2,6 +2,8 @@ import logging import uuid +from urllib.parse import urljoin + from guardian.decorators import permission_required_or_403 from notifications.signals import notify @@ -306,9 +308,10 @@ def send_message(request): log.info(f"Notifications sent to {len(recipients)} users.") if is_email: + unsubscribe_url = urljoin(settings.SITE_URL, f"unsubscribe/{uuid}") footer = ( - """

-You’re receiving this email as a contributor to Mozilla localization on Pontoon.
To no longer receive emails like these, unsubscribe here: Unsubscribe. + f"""

+{ settings.EMAIL_COMMUNICATIONS_FOOTER_PRE_TEXT }
To no longer receive emails like these, unsubscribe here: Unsubscribe. """ if not is_transactional else "" diff --git a/pontoon/settings/base.py b/pontoon/settings/base.py index f09bcf729a..885690e640 100644 --- a/pontoon/settings/base.py +++ b/pontoon/settings/base.py @@ -223,6 +223,9 @@ def _default_from_email(): EMAIL_CONSENT_MAIN_TEXT = os.environ.get("EMAIL_CONSENT_MAIN_TEXT", "") EMAIL_CONSENT_PRIVACY_NOTICE = os.environ.get("EMAIL_CONSENT_PRIVACY_NOTICE", "") EMAIL_COMMUNICATIONS_HELP_TEXT = os.environ.get("EMAIL_COMMUNICATIONS_HELP_TEXT", "") +EMAIL_COMMUNICATIONS_FOOTER_PRE_TEXT = os.environ.get( + "EMAIL_COMMUNICATIONS_FOOTER_PRE_TEXT", "" +) # Log emails to console if the SendGrid credentials are missing. if EMAIL_HOST_USER and EMAIL_HOST_PASSWORD: @@ -1137,3 +1140,7 @@ def account_username(user): ) DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +# Used in the header of the Terminology (.TBX) files. +TBX_TITLE = os.environ.get("TBX_TITLE", "Pontoon Terminology") +TBX_DESCRIPTION = os.environ.get("TBX_DESCRIPTION", "Terms localized in Pontoon") diff --git a/pontoon/terminology/tests/test_tbx.py b/pontoon/terminology/tests/test_tbx.py new file mode 100644 index 0000000000..df806bc6af --- /dev/null +++ b/pontoon/terminology/tests/test_tbx.py @@ -0,0 +1,136 @@ +import xml.etree.ElementTree as ET + +from unittest.mock import MagicMock + +import pytest + +from pontoon.terminology.utils import build_tbx_v2_file, build_tbx_v3_file + + +@pytest.fixture +def mock_term_translations(): + term_mock = MagicMock() + term_mock.pk = 1 + term_mock.text = "Sample Term" + term_mock.part_of_speech = "noun" + term_mock.definition = "Sample Definition" + term_mock.usage = "Sample Usage" + + translation_mock = MagicMock() + translation_mock.term = term_mock + translation_mock.text = "Translation" + + return [translation_mock] + + +def test_build_tbx_v2_file(settings, mock_term_translations): + settings.TBX_TITLE = "Sample Title" + settings.TBX_DESCRIPTION = "Sample Description" + locale = "fr-FR" + + result = "".join(build_tbx_v2_file(mock_term_translations, locale)) + + # Fail if the result is not a valid XML + ET.fromstring(result) + + # Construct the expected XML + expected = """ + + + + + + Sample Title + + +

Sample Description

+
+
+ +

TBXXCSV02.xcs

+
+
+ + + + Sample Usage + + + + Sample Term + noun + + + + Sample Definition + + + + + + Translation + + + + + + +
""" + + # Assert that the generated result matches the expected XML + assert result.strip() == expected.strip() + + +def test_build_tbx_v3_file(settings, mock_term_translations): + settings.TBX_TITLE = "Sample Title" + settings.TBX_DESCRIPTION = "Sample Description" + locale = "fr-FR" + + result = "".join(build_tbx_v3_file(mock_term_translations, locale)) + + # Fail if the result is not a valid XML + ET.fromstring(result) + + # Construct the expected XML + expected = """ + + + + + + + Sample Title + + +

Sample Description

+
+
+ +

TBXXCSV02.xcs

+
+
+ + + + + + Sample Term + noun + + Sample Definition + Sample Usage + + + + + + Translation + + + + + +
""" + + # Assert that the generated result matches the expected XML + assert result.strip() == expected.strip() diff --git a/pontoon/terminology/utils.py b/pontoon/terminology/utils.py index e89848a02d..4dd0a8d6be 100644 --- a/pontoon/terminology/utils.py +++ b/pontoon/terminology/utils.py @@ -1,5 +1,7 @@ from xml.sax.saxutils import escape, quoteattr +from django.conf import settings + def build_tbx_v2_file(term_translations, locale): """ @@ -8,63 +10,58 @@ def build_tbx_v2_file(term_translations, locale): TBX files could contain large amount of entries and it's impossible to render all the data with django templates. Rendering a string in memory is a lot faster. """ - yield ( - '' - '\n' - '\n' - "\n\t" - "\n\t\t" - "\n\t\t\t" - "\n\t\t\t\tMozilla Terms" - "\n\t\t\t" - "\n\t\t\t" - "\n\t\t\t\t

from a Mozilla termbase

" - "\n\t\t\t
" - "\n\t\t
" - "\n\t\t" - '\n\t\t\t

TBXXCSV02.xcs

' - "\n\t\t
" - "\n\t
" - "\n\t" - "\n\t\t" - ) + # Header + yield f""" + + + + + + {escape(settings.TBX_TITLE)} + + +

{escape(settings.TBX_DESCRIPTION)}

+
+
+ +

TBXXCSV02.xcs

+
+
+ + """ + # Body for translation in term_translations: term = translation.term - yield ( - '\n\t\t\t' - '\n\t\t\t\t%(usage)s' - '\n\t\t\t\t' - "\n\t\t\t\t\t" - "\n\t\t\t\t\t\t" - "\n\t\t\t\t\t\t\t%(term)s" - '\n\t\t\t\t\t\t\t%(part_of_speech)s' - "\n\t\t\t\t\t\t" - "\n\t\t\t\t\t" - "\n\t\t\t\t\t" - '\n\t\t\t\t\t\t%(definition)s' - "\n\t\t\t\t\t" - "\n\t\t\t\t" - "\n\t\t\t\t" - "\n\t\t\t\t\t" - "\n\t\t\t\t\t\t" - "\n\t\t\t\t\t\t\t%(translation)s" - "\n\t\t\t\t\t\t" - "\n\t\t\t\t\t" - "\n\t\t\t\t" - "\n\t\t\t" - % { - "id": term.pk, - "term": escape(term.text), - "part_of_speech": escape(term.part_of_speech), - "definition": escape(term.definition), - "usage": escape(term.usage), - "locale": quoteattr(locale), - "translation": escape(translation.text), - } - ) + yield f""" + + {escape(term.usage)} + + + + {escape(term.text)} + {escape(term.part_of_speech)} + + + + {escape(term.definition)} + + + + + + {escape(translation.text)} + + + + """ - yield ("\n\t\t" "\n\t" "\n
\n") + # Footer + yield """ + +
+
+""" def build_tbx_v3_file(term_translations, locale): @@ -74,57 +71,52 @@ def build_tbx_v3_file(term_translations, locale): TBX files could contain large amount of entries and it's impossible to render all the data with django templates. Rendering a string in memory is a lot faster. """ - yield ( - '' - '\n' - '\n' - '\n' - "\n\t" - "\n\t\t" - "\n\t\t\t" - "\n\t\t\t\tMozilla Terms" - "\n\t\t\t" - "\n\t\t\t" - "\n\t\t\t\t

from a Mozilla termbase

" - "\n\t\t\t
" - "\n\t\t
" - "\n\t\t" - '\n\t\t\t

TBXXCSV02.xcs

' - "\n\t\t
" - "\n\t
" - "\n\t" - "\n\t\t" - ) + # Header + yield f""" + + + + + + + {escape(settings.TBX_TITLE)} + + +

{escape(settings.TBX_DESCRIPTION)}

+
+
+ +

TBXXCSV02.xcs

+
+
+ + """ + # Body for translation in term_translations: term = translation.term - yield ( - '\n\t\t\t' - '\n\t\t\t\t' - "\n\t\t\t\t\t" - "\n\t\t\t\t\t\t%(term)s" - '\n\t\t\t\t\t\t%(part_of_speech)s' - "\n\t\t\t\t\t\t" - '\n\t\t\t\t\t\t\t%(definition)s' - '\n\t\t\t\t\t\t\t%(usage)s' - "\n\t\t\t\t\t\t" - "\n\t\t\t\t\t" - "\n\t\t\t\t" - "\n\t\t\t\t" - "\n\t\t\t\t\t" - "\n\t\t\t\t\t\t%(translation)s" - "\n\t\t\t\t\t" - "\n\t\t\t\t" - "\n\t\t\t" - % { - "id": term.pk, - "term": escape(term.text), - "part_of_speech": escape(term.part_of_speech), - "definition": escape(term.definition), - "usage": escape(term.usage), - "locale": quoteattr(locale), - "translation": escape(translation.text), - } - ) + yield f""" + + + + {escape(term.text)} + {escape(term.part_of_speech)} + + {escape(term.definition)} + {escape(term.usage)} + + + + + + {escape(translation.text)} + + + """ - yield ("\n\t\t" "\n\t" "\n
\n") + # Footer + yield """ + +
+
+"""