diff --git a/pontoon/administration/tests/test_views.py b/pontoon/administration/tests/test_views.py index c8d61c4d28..6a244b2712 100644 --- a/pontoon/administration/tests/test_views.py +++ b/pontoon/administration/tests/test_views.py @@ -382,7 +382,7 @@ def test_manage_project_strings_download_csv(client_superuser): @pytest.mark.django_db def test_manage_project_translate_link_excludes_obsolete_resources(client_superuser): - """Test that translate_locale is only set when non-obsolete resources exist.""" + """Test that Translate link is only shown when non-obsolete resources exist.""" locale_kl = LocaleFactory.create(code="tlh", name="Klingon") project = ProjectFactory.create( data_source=Project.DataSource.DATABASE, @@ -390,20 +390,24 @@ def test_manage_project_translate_link_excludes_obsolete_resources(client_superu repositories=[], ) + url = reverse("pontoon.admin.project", args=(project.slug,)) + translate_url = reverse( + "pontoon.localizations.localization", args=(locale_kl.code, project.slug) + ) + # add obsolete resource ResourceFactory.create(project=project, obsolete=True) - url = reverse("pontoon.admin.project", args=(project.slug,)) response = client_superuser.get(url) assert response.status_code == 200 - assert "translate_locale" not in response.context + assert translate_url.encode() not in response.content # add non-obsolete resource ResourceFactory.create(project=project, obsolete=False) response = client_superuser.get(url) assert response.status_code == 200 - assert response.context["translate_locale"] == "tlh" + assert translate_url.encode() in response.content @pytest.mark.django_db diff --git a/pontoon/messaging/emails.py b/pontoon/messaging/emails.py index ca497cdc96..3a36c3cdbe 100644 --- a/pontoon/messaging/emails.py +++ b/pontoon/messaging/emails.py @@ -6,7 +6,6 @@ from celery import shared_task from dateutil.relativedelta import relativedelta -from django_jinja.backend import Jinja2 from notifications.models import Notification from django.conf import settings @@ -18,15 +17,20 @@ from pontoon.actionlog.models import ActionLog from pontoon.base.models import Locale, UserProfile +from pontoon.base.templatetags.helpers import full_url from pontoon.insights.models import LocaleInsightsSnapshot from pontoon.messaging.models import EmailContent from pontoon.messaging.utils import html_to_plain_text_with_links -jinja_env = Jinja2.get_default().env log = logging.getLogger(__name__) +class SafeDict(dict): + def __missing__(self, key): + return "{" + key + "}" + + def _get_monthly_user_actions(users, months_ago): month_date = timezone.now() - relativedelta(months=months_ago) @@ -334,7 +338,17 @@ def send_onboarding_email_1(user): Sends 1st onboarding email to a new user. """ email_content = EmailContent.objects.get(email="onboarding_1") - content = jinja_env.from_string(email_content.body).render() + content = email_content.body.format_map( + SafeDict( + { + "tutorial_url": full_url( + "pontoon.translate", "projects", "tutorial", "playground" + ), + "settings_url": full_url("pontoon.contributors.settings"), + "teams_url": full_url("pontoon.teams"), + } + ) + ) subject = email_content.subject template = get_template("messaging/emails/transactional.html") @@ -370,7 +384,15 @@ def send_onboarding_emails_2(users): log.info("Start sending 2nd onboarding emails.") email_content = EmailContent.objects.get(email="onboarding_2") - content = jinja_env.from_string(email_content.body).render() + content = email_content.body.format_map( + SafeDict( + { + "homepage_url": full_url("pontoon.homepage"), + "projects_url": full_url("pontoon.projects"), + "teams_url": full_url("pontoon.teams"), + } + ) + ) subject = email_content.subject template = get_template("messaging/emails/transactional.html") @@ -406,7 +428,14 @@ def send_onboarding_emails_3(users): log.info("Start sending 3rd onboarding emails.") email_content = EmailContent.objects.get(email="onboarding_3") - content = jinja_env.from_string(email_content.body).render() + content = email_content.body.format_map( + SafeDict( + { + "docs_url": full_url("pontoon.docs"), + "settings_url": full_url("pontoon.contributors.settings"), + } + ) + ) subject = email_content.subject template = get_template("messaging/emails/transactional.html") @@ -442,7 +471,14 @@ def send_inactive_contributor_emails(users): log.info("Start sending inactive contributor emails.") email_content = EmailContent.objects.get(email="inactive_contributor") - content = jinja_env.from_string(email_content.body).render() + content = email_content.body.format_map( + SafeDict( + { + "INACTIVE_CONTRIBUTOR_PERIOD": settings.INACTIVE_CONTRIBUTOR_PERIOD, + "homepage_url": full_url("pontoon.homepage"), + } + ) + ) subject = email_content.subject template = get_template("messaging/emails/transactional.html") @@ -490,7 +526,14 @@ def send_inactive_translator_emails(users, translator_map): log.error(f"User {user} is not a translator of any locale.") continue - content = jinja_env.from_string(email_content.body).render({"locale": locale}) + content = email_content.body.format_map( + SafeDict( + { + "INACTIVE_TRANSLATOR_PERIOD": settings.INACTIVE_TRANSLATOR_PERIOD, + "team_url": full_url("pontoon.teams.team", locale.code), + } + ) + ) body_html = template.render( { "content": content, @@ -532,8 +575,17 @@ def send_inactive_manager_emails(users, manager_map): except IndexError: log.error(f"User {user} is not a manager of any locale.") continue - - content = jinja_env.from_string(email_content.body).render({"locale": locale}) + content = email_content.body.format_map( + SafeDict( + { + "INACTIVE_MANAGER_PERIOD": settings.INACTIVE_MANAGER_PERIOD, + "contributors_url": full_url( + "pontoon.teams.contributors", locale.code + ), + "team_url": full_url("pontoon.teams.team", locale.code), + } + ) + ) body_html = template.render( { "content": content, diff --git a/pontoon/messaging/templates/messaging/emails/content/inactive_contributor.html b/pontoon/messaging/templates/messaging/emails/content/inactive_contributor.html index 51c957ff3c..88bc39c6b7 100644 --- a/pontoon/messaging/templates/messaging/emails/content/inactive_contributor.html +++ b/pontoon/messaging/templates/messaging/emails/content/inactive_contributor.html @@ -1,9 +1,7 @@

It's been a while since we’ve last seen you, but we wanted to thank you for contributing to localization efforts at - {{ full_url('pontoon.homepage') }}. + {homepage_url}.

@@ -15,18 +13,16 @@

As part of our efforts to keep our localization community active and engaged, we periodically review account activity. We’ve noticed you haven’t signed in - to your Pontoon account in the past {{ settings.INACTIVE_CONTRIBUTOR_PERIOD }} - months. While we completely understand that circumstances change, we wanted to - inform you that we may deactivate dormant accounts in the future to ensure the + to your Pontoon account in the past {INACTIVE_CONTRIBUTOR_PERIOD} months. + While we completely understand that circumstances change, we wanted to inform + you that we may deactivate dormant accounts in the future to ensure the security and efficiency of our platform.

If you’d like to keep your Pontoon account active (we hope you will!) please sign in to Pontoon by visiting - {{ full_url('pontoon.homepage') }} + {homepage_url} and click the “Sign in” button in the top right corner.

diff --git a/pontoon/messaging/templates/messaging/emails/content/inactive_manager.html b/pontoon/messaging/templates/messaging/emails/content/inactive_manager.html index b0b4767fac..9f4950cf69 100644 --- a/pontoon/messaging/templates/messaging/emails/content/inactive_manager.html +++ b/pontoon/messaging/templates/messaging/emails/content/inactive_manager.html @@ -10,10 +10,10 @@

As part of our efforts to keep our localization community active and engaged, we periodically review account activity. We’ve noticed that in the past - {{ settings.INACTIVE_MANAGER_PERIOD }} months you have not submitted any - translations or reviewed any translation submissions for your locale. We - understand that life can get busy, and finding time to contribute to projects - isn't always easy. We get it. + {INACTIVE_MANAGER_PERIOD} months you have not submitted any translations or + reviewed any translation submissions for your locale. We understand that life + can get busy, and finding time to contribute to projects isn't always easy. We + get it.

@@ -40,18 +40,14 @@

In addition, another Team Manager responsibility is to evaluate candidates for advanced roles within their community. Please review the - contributors page + contributors page for your team and see if there are any motivated and active community members that may be interested in taking on additional responsibilities within your team.

- {{ full_url('pontoon.teams.team', locale.code) }} + {team_url}

diff --git a/pontoon/messaging/templates/messaging/emails/content/inactive_translator.html b/pontoon/messaging/templates/messaging/emails/content/inactive_translator.html index 2e5849a4f5..f8f38c9bb4 100644 --- a/pontoon/messaging/templates/messaging/emails/content/inactive_translator.html +++ b/pontoon/messaging/templates/messaging/emails/content/inactive_translator.html @@ -9,10 +9,10 @@

As part of our efforts to keep our localization community active and engaged, we periodically review account activity. We’ve noticed that in the past - {{ settings.INACTIVE_TRANSLATOR_PERIOD }} months you have not submitted any - translations or reviewed any translation submissions for your locale. We - understand that life can get busy, and finding time to contribute to projects - isn't always easy. We get it. + {INACTIVE_TRANSLATOR_PERIOD} months you have not submitted any translations or + reviewed any translation submissions for your locale. We understand that life + can get busy, and finding time to contribute to projects isn't always easy. We + get it.

@@ -37,9 +37,7 @@

- {{ full_url('pontoon.teams.team', locale.code) }} + {team_url}

diff --git a/pontoon/messaging/templates/messaging/emails/content/onboarding_1.html b/pontoon/messaging/templates/messaging/emails/content/onboarding_1.html index 957848c47d..f27cc168b4 100644 --- a/pontoon/messaging/templates/messaging/emails/content/onboarding_1.html +++ b/pontoon/messaging/templates/messaging/emails/content/onboarding_1.html @@ -10,22 +10,19 @@

  1. Take a - tour + tour of Pontoon to get familiar with its workflow and features, then play around in the sandbox to try it out for yourself.
  2. Update your - settings to - personalize your Pontoon profile by adding an avatar and username, select - between a dark and light theme, and choose your default locale. + settings to personalize your Pontoon profile by + adding an avatar and username, select between a dark and light theme, and + choose your default locale.
  3. - Check out your team page to - find more information and resources about your locale. + Check out your team page to find more information + and resources about your locale.
diff --git a/pontoon/messaging/templates/messaging/emails/content/onboarding_2.html b/pontoon/messaging/templates/messaging/emails/content/onboarding_2.html index 0bb581f543..a304d82e5a 100644 --- a/pontoon/messaging/templates/messaging/emails/content/onboarding_2.html +++ b/pontoon/messaging/templates/messaging/emails/content/onboarding_2.html @@ -1,9 +1,7 @@

Thank you again for joining translation community on Pontoon. As always, you can continue contributing translations at - {{ full_url('pontoon.homepage') }}. + {homepage_url}.

@@ -39,15 +37,14 @@

Translation phases

included into the product. The timing depends on the project, but often strings will be implemented at the time of the next build and ready to check in product. Be sure to check the deadlines in the - project page to understand - which strings need translations soon. + project page to understand which strings need + translations soon.

Each localization community has its own specific workflows, so do reach out to - your Team Managers to learn - more. + your Team Managers to learn more.

Team roles

diff --git a/pontoon/messaging/templates/messaging/emails/content/onboarding_3.html b/pontoon/messaging/templates/messaging/emails/content/onboarding_3.html index ed770f9c26..628d8525ca 100644 --- a/pontoon/messaging/templates/messaging/emails/content/onboarding_3.html +++ b/pontoon/messaging/templates/messaging/emails/content/onboarding_3.html @@ -13,9 +13,7 @@

Additional documentation

For in-depth documentation on Pontoon, see our - Documentation for localizers. + Documentation for localizers.

Pontoon Add-on

@@ -38,8 +36,7 @@

Email settings

Finally, if you’re interested in the latest updates, announcements about new Pontoon features and more, be sure to enable “News and updates“ in your - settings so you - can be the first to get notified. + settings so you can be the first to get notified.

diff --git a/pontoon/messaging/tests/test_emails.py b/pontoon/messaging/tests/test_emails.py index 28828355da..3d150da502 100644 --- a/pontoon/messaging/tests/test_emails.py +++ b/pontoon/messaging/tests/test_emails.py @@ -1,15 +1,27 @@ +from collections import defaultdict from datetime import date, datetime, timezone from unittest.mock import patch import pytest +from django.core import mail +from django.template import TemplateSyntaxError from django.test.client import RequestFactory +from django.urls import NoReverseMatch +from pontoon.base.models import User from pontoon.insights.models import LocaleInsightsSnapshot from pontoon.messaging.emails import ( _get_monthly_locale_stats, + send_inactive_contributor_emails, + send_inactive_manager_emails, + send_inactive_translator_emails, + send_onboarding_email_1, + send_onboarding_emails_2, + send_onboarding_emails_3, send_verification_email, ) +from pontoon.messaging.models import EmailContent from pontoon.test.factories import LocaleFactory @@ -60,3 +72,123 @@ def test_get_monthly_locale_stats_uses_end_of_month_snapshot(): assert result[locale.pk].pk == snapshot_nov_1.pk assert result[locale.pk].approved_strings == 100 assert result[locale.pk].completion == 100.0 + + +@pytest.mark.django_db +def test_send_onboarding_email_1(user_a): + try: + send_onboarding_email_1(user_a) + except EmailContent.DoesNotExist: + pytest.fail("EmailContent for 'onboarding_1' is missing from the DB.") + except NoReverseMatch as e: + pytest.fail(f"URL resolution failed: check URL config: {e}") + except TemplateSyntaxError as e: + pytest.fail(f"Template is broken: {e}") + except Exception as e: + pytest.fail(f"An unexpected error occurred: {e}") + + assert len(mail.outbox) == 2 + assert mail.outbox[0].to == [user_a.contact_email] + + +@pytest.mark.django_db +def test_send_onboarding_emails_2(user_a): + + users = User.objects.filter(pk=user_a.pk) + + try: + send_onboarding_emails_2(users) + except EmailContent.DoesNotExist: + pytest.fail("EmailContent for 'onboarding_2' is missing from the DB.") + except NoReverseMatch as e: + pytest.fail(f"URL resolution failed: check URL config: {e}") + except TemplateSyntaxError as e: + pytest.fail(f"Template is broken: {e}") + except Exception as e: + pytest.fail(f"An unexpected error occurred: {e}") + + assert len(mail.outbox) == 2 + assert mail.outbox[0].to == [user_a.contact_email] + + +@pytest.mark.django_db +def test_send_onboarding_emails_3(user_a): + + users = User.objects.filter(pk=user_a.pk) + + try: + send_onboarding_emails_3(users) + except EmailContent.DoesNotExist: + pytest.fail("EmailContent for 'onboarding_3' is missing from the DB.") + except NoReverseMatch as e: + pytest.fail(f"URL resolution failed: check URL config: {e}") + except TemplateSyntaxError as e: + pytest.fail(f"Template is broken: {e}") + except Exception as e: + pytest.fail(f"An unexpected error occurred: {e}") + + assert len(mail.outbox) == 2 + assert mail.outbox[0].to == [user_a.contact_email] + + +@pytest.mark.django_db +def test_send_inactive_contributor_emails(user_a): + + users = User.objects.filter(pk=user_a.pk) + + try: + send_inactive_contributor_emails(users) + except EmailContent.DoesNotExist: + pytest.fail("EmailContent for 'inactive_contributor' is missing from the DB.") + except NoReverseMatch as e: + pytest.fail(f"URL resolution failed: check URL config: {e}") + except TemplateSyntaxError as e: + pytest.fail(f"Template is broken: {e}") + except Exception as e: + pytest.fail(f"An unexpected error occurred: {e}") + + assert len(mail.outbox) == 2 + assert mail.outbox[0].to == [user_a.contact_email] + + +@pytest.mark.django_db +def test_send_inactive_translator_emails(user_a, locale_a): + translators = defaultdict(set) + + users = User.objects.filter(pk=user_a.pk) + translators[user_a.pk].add(locale_a) + try: + send_inactive_translator_emails(users, translators) + except EmailContent.DoesNotExist: + pytest.fail("EmailContent for 'inactive_translator' is missing from the DB.") + except NoReverseMatch as e: + pytest.fail(f"URL resolution failed: check URL config: {e}") + except TemplateSyntaxError as e: + pytest.fail(f"Template is broken: {e}") + except Exception as e: + pytest.fail(f"An unexpected error occurred: {e}") + + assert len(mail.outbox) == 2 + assert mail.outbox[0].to == [user_a.contact_email] + + +@pytest.mark.django_db +def test_send_inactive_manager_emails(user_a, locale_a): + managers = defaultdict(set) + + users = User.objects.filter(pk=user_a.pk) + managers[user_a.pk].add(locale_a) + + try: + send_inactive_manager_emails(users, managers) + except EmailContent.DoesNotExist: + pytest.fail("EmailContent for 'inactive_manager' is missing from the DB.") + except NoReverseMatch as e: + pytest.fail(f"URL resolution failed: check URL config: {e}") + except TemplateSyntaxError as e: + pytest.fail(f"Template is broken: {e}") + except Exception as e: + pytest.fail(f"An unexpected error occurred: {e}") + + assert len(mail.outbox) == 2 + assert mail.outbox[0].to == [user_a.contact_email]