From 6a677195924055f97d089f546cf340189d5e625b Mon Sep 17 00:00:00 2001 From: Muhammad Adeel Tajamul <77053848+muhammadadeeltajamul@users.noreply.github.com> Date: Mon, 26 May 2025 13:44:01 +0500 Subject: [PATCH] feat: make notification emails translatable (#36775) * feat: make notification emails translatable * fix: fixed failing tests * fix: fixed xss quality check --- .../djangoapps/notifications/email/tasks.py | 90 +++++++++++-------- .../notifications/email/tests/test_utils.py | 8 +- .../djangoapps/notifications/email/utils.py | 37 ++++++-- .../notifications/digest_content.html | 9 +- .../notifications/digest_footer.html | 13 ++- .../notifications/digest_header.html | 11 ++- .../edx_ace/email_digest/email/body.html | 1 + .../edx_ace/email_digest/email/body.txt | 8 +- .../edx_ace/email_digest/email/head.html | 4 +- .../edx_ace/email_digest/email/subject.txt | 8 +- .../edx_ace/immediate_email/email/body.html | 1 + .../edx_ace/immediate_email/email/head.html | 4 +- .../immediate_email_content.html | 7 +- openedx/core/djangoapps/user_api/models.py | 7 ++ 14 files changed, 144 insertions(+), 64 deletions(-) diff --git a/openedx/core/djangoapps/notifications/email/tasks.py b/openedx/core/djangoapps/notifications/email/tasks.py index c62348489c..c907a6f09b 100644 --- a/openedx/core/djangoapps/notifications/email/tasks.py +++ b/openedx/core/djangoapps/notifications/email/tasks.py @@ -5,6 +5,7 @@ from bs4 import BeautifulSoup from celery import shared_task from celery.utils.log import get_task_logger from django.contrib.auth import get_user_model +from django.utils.translation import gettext as _, override as translation_override from edx_ace import ace from edx_ace.recipient import Recipient from edx_django_utils.monitoring import set_code_owner_attribute @@ -24,10 +25,11 @@ from .utils import ( create_email_template_context, filter_notification_with_email_enabled_preferences, get_course_info, + get_language_preference_for_users, get_start_end_date, get_text_for_notification_type, get_unique_course_ids, - is_email_notification_flag_enabled + is_email_notification_flag_enabled, ) @@ -74,7 +76,7 @@ def get_user_preferences_for_courses(course_ids, user): return new_preferences -def send_digest_email_to_user(user, cadence_type, start_date, end_date, course_language='en', courses_data=None): +def send_digest_email_to_user(user, cadence_type, start_date, end_date, user_language='en', courses_data=None): """ Send [cadence_type] email to user. Cadence Type can be EmailCadence.DAILY or EmailCadence.WEEKLY @@ -95,24 +97,26 @@ def send_digest_email_to_user(user, cadence_type, start_date, end_date, course_l if not notifications: logger.info(f' No notification for {user.username} ==Temp Log==') return - course_ids = get_unique_course_ids(notifications) - preferences = get_user_preferences_for_courses(course_ids, user) - notifications = filter_notification_with_email_enabled_preferences(notifications, preferences, cadence_type) - if not notifications: - logger.info(f' No filtered notification for {user.username} ==Temp Log==') - return - apps_dict = create_app_notifications_dict(notifications) - message_context = create_email_digest_context(apps_dict, user.username, start_date, end_date, - cadence_type, courses_data=courses_data) - recipient = Recipient(user.id, user.email) - message = EmailNotificationMessageType( - app_label="notifications", name="email_digest" - ).personalize(recipient, course_language, message_context) - message = add_headers_to_email_message(message, message_context) - message.options['skip_disable_user_policy'] = True - ace.send(message) - send_user_email_digest_sent_event(user, cadence_type, notifications, message_context) - logger.info(f' Email sent to {user.username} ==Temp Log==') + + with translation_override(user_language): + course_ids = get_unique_course_ids(notifications) + preferences = get_user_preferences_for_courses(course_ids, user) + notifications = filter_notification_with_email_enabled_preferences(notifications, preferences, cadence_type) + if not notifications: + logger.info(f' No filtered notification for {user.username} ==Temp Log==') + return + apps_dict = create_app_notifications_dict(notifications) + message_context = create_email_digest_context(apps_dict, user.username, start_date, end_date, + cadence_type, courses_data=courses_data) + recipient = Recipient(user.id, user.email) + message = EmailNotificationMessageType( + app_label="notifications", name="email_digest" + ).personalize(recipient, user_language, message_context) + message = add_headers_to_email_message(message, message_context) + message.options['skip_disable_user_policy'] = True + ace.send(message) + send_user_email_digest_sent_event(user, cadence_type, notifications, message_context) + logger.info(f' Email sent to {user.username} ==Temp Log==') @shared_task(ignore_result=True) @@ -123,11 +127,14 @@ def send_digest_email_to_all_users(cadence_type): """ logger.info(f' Sending cadence email of type {cadence_type}') users = get_audience_for_cadence_email(cadence_type) + language_prefs = get_language_preference_for_users([user.id for user in users]) courses_data = {} start_date, end_date = get_start_end_date(cadence_type) logger.info(f' Email Cadence Audience {len(users)}') for user in users: - send_digest_email_to_user(user, cadence_type, start_date, end_date, courses_data=courses_data) + user_language = language_prefs.get(user.id, 'en') + send_digest_email_to_user(user, cadence_type, start_date, end_date, user_language=user_language, + courses_data=courses_data) def send_immediate_cadence_email(email_notification_mapping, course_key): @@ -141,6 +148,7 @@ def send_immediate_cadence_email(email_notification_mapping, course_key): return user_list = email_notification_mapping.keys() users = User.objects.filter(id__in=user_list) + language_prefs = get_language_preference_for_users(user_list) course_name = get_course_info(course_key).get("name", course_key) for user in users.iterator(chunk_size=100): if not user.has_usable_password(): @@ -153,21 +161,25 @@ def send_immediate_cadence_email(email_notification_mapping, course_key): if not notification: logger.info(f' No notification for {user.username}') continue - soup = BeautifulSoup(notification.content, "html.parser") - title = "New Course Update" if notification.notification_type == "course_updates" else soup.get_text() - message_context = create_email_template_context(user.username) - message_context.update({ - "course_id": course_key, - "course_name": course_name, - "content_url": notification.content_url, - "content_title": title, - "footer_email_reason": "You are receiving this email because you are enrolled in the edX course " - f"{course_name}", - "content": notification.content_context.get("email_content", notification.content), - "view_text": get_text_for_notification_type(notification.notification_type), - }) - message = EmailNotificationMessageType( - app_label="notifications", name="immediate_email" - ).personalize(Recipient(user.id, user.email), 'en', message_context) - message = add_headers_to_email_message(message, message_context) - ace.send(message) + + language = language_prefs.get(user.id, 'en') + with translation_override(language): + soup = BeautifulSoup(notification.content, "html.parser") + title = _("New Course Update") if notification.notification_type == "course_updates" else soup.get_text() + message_context = create_email_template_context(user.username) + message_context.update({ + "course_id": course_key, + "course_name": course_name, + "content_url": notification.content_url, + "content_title": title, + "footer_email_reason": _( + "You are receiving this email because you are enrolled in the edX course " + ) + str(course_name), + "content": notification.content_context.get("email_content", notification.content), + "view_text": get_text_for_notification_type(notification.notification_type), + }) + message = EmailNotificationMessageType( + app_label="notifications", name="immediate_email" + ).personalize(Recipient(user.id, user.email), language, message_context) + message = add_headers_to_email_message(message, message_context) + ace.send(message) diff --git a/openedx/core/djangoapps/notifications/email/tests/test_utils.py b/openedx/core/djangoapps/notifications/email/tests/test_utils.py index c7b6681624..e7b5db54e8 100644 --- a/openedx/core/djangoapps/notifications/email/tests/test_utils.py +++ b/openedx/core/djangoapps/notifications/email/tests/test_utils.py @@ -147,19 +147,21 @@ class TestContextFunctions(ModuleStoreTestCase): context = create_email_digest_context(**params) expected_start_date = 'Sunday, Mar 24' if digest_frequency == 'Daily' else 'Monday, Mar 18' expected_digest_updates = [ - {'title': 'Total Notifications', 'count': 2}, - {'title': 'Discussion', 'count': 1}, - {'title': 'Updates', 'count': 1}, + {'title': 'Total Notifications', 'translated_title': 'Total Notifications', 'count': 2}, + {'title': 'Discussion', 'translated_title': 'Discussion', 'count': 1}, + {'title': 'Updates', 'translated_title': 'Updates', 'count': 1}, ] expected_email_content = [ { 'title': 'Discussion', 'help_text': '', 'help_text_url': '', + 'translated_title': 'Discussion', 'notifications': [discussion_notification], 'total': 1, 'show_remaining_count': False, 'remaining_count': 0, 'url': 'http://learner-home-mfe/?showNotifications=true&app=discussion' }, { 'title': 'Updates', 'help_text': '', 'help_text_url': '', + 'translated_title': 'Updates', 'notifications': [update_notification], 'total': 1, 'show_remaining_count': False, 'remaining_count': 0, 'url': 'http://learner-home-mfe/?showNotifications=true&app=updates' diff --git a/openedx/core/djangoapps/notifications/email/utils.py b/openedx/core/djangoapps/notifications/email/utils.py index c606201568..efacf8aee4 100644 --- a/openedx/core/djangoapps/notifications/email/utils.py +++ b/openedx/core/djangoapps/notifications/email/utils.py @@ -8,12 +8,14 @@ from bs4 import BeautifulSoup from django.conf import settings from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext as _ from pytz import utc from waffle import get_waffle_flag_model # pylint: disable=invalid-django-waffle-import from common.djangoapps.student.models import CourseEnrollment from lms.djangoapps.branding.api import get_logo_url_for_email from lms.djangoapps.discussion.notification_prefs.views import UsernameCipher +from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.notifications.base_notification import COURSE_NOTIFICATION_APPS, COURSE_NOTIFICATION_TYPES from openedx.core.djangoapps.notifications.config.waffle import ENABLE_EMAIL_NOTIFICATIONS from openedx.core.djangoapps.notifications.email import ONE_CLICK_EMAIL_UNSUB_KEY @@ -28,6 +30,7 @@ from xmodule.modulestore.django import modulestore from .notification_icons import NotificationTypeIcons + User = get_user_model() @@ -119,12 +122,14 @@ def create_email_digest_context(app_notifications_dict, username, start_date, en end_date_str = create_datetime_string(end_date if end_date else start_date) email_digest_updates = [{ 'title': 'Total Notifications', + 'translated_title': _('Total Notifications'), 'count': sum(value['count'] for value in app_notifications_dict.values()) }] email_digest_updates.extend([ { 'title': value['title'], 'count': value['count'], + 'translated_title': value.get('translated_title', value['title']), } for key, value in app_notifications_dict.items() ]) @@ -135,6 +140,7 @@ def create_email_digest_context(app_notifications_dict, username, start_date, en total = value['count'] app_content = { 'title': value['title'], + 'translated_title': value.get('translated_title', value['title']), 'help_text': value.get('help_text', ''), 'help_text_url': value.get('help_text_url', ''), 'notifications': add_additional_attributes_to_notifications( @@ -200,7 +206,7 @@ def get_time_ago(datetime_obj): current_date = utc.localize(datetime.datetime.today()) days_diff = (current_date - datetime_obj).days if days_diff == 0: - return "Today" + return _("Today") if days_diff >= 7: return f"{int(days_diff / 7)}w" return f"{days_diff}d" @@ -252,6 +258,7 @@ def create_app_notifications_dict(notifications): name: { 'count': 0, 'title': name.title(), + 'translated_title': get_translated_app_title(name), 'notifications': [] } for name in app_names @@ -433,16 +440,36 @@ def is_notification_type_channel_editable(app_name, notification_type, channel): return channel not in COURSE_NOTIFICATION_TYPES[notification_type]['non_editable'] +def get_translated_app_title(name): + """ + Returns translated string from notification app_name key + """ + mapping = { + 'discussion': _('Discussion'), + 'updates': _('Updates'), + 'grading': _('Grading'), + } + return mapping.get(name, '') + + +def get_language_preference_for_users(user_ids): + """ + Returns mapping of user_id and language preference for users + """ + prefs = UserPreference.get_preference_for_users(user_ids, LANGUAGE_KEY) + return {pref.user_id: pref.value for pref in prefs} + + def get_text_for_notification_type(notification_type): """ Returns text for notification type """ - app_name = COURSE_NOTIFICATION_APPS.get(notification_type, {}).get('notification_app') + app_name = COURSE_NOTIFICATION_TYPES.get(notification_type, {}).get('notification_app') if not app_name: return "" mapping = { - 'discussion': 'post', - 'updates': 'update', - 'grading': 'assessment', + 'discussion': _('post'), + 'updates': _('update'), + 'grading': _('assessment'), } return mapping.get(app_name, "") diff --git a/openedx/core/djangoapps/notifications/templates/notifications/digest_content.html b/openedx/core/djangoapps/notifications/templates/notifications/digest_content.html index 51966f96f5..a4de0b6b0a 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/digest_content.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/digest_content.html @@ -1,6 +1,7 @@ +{% load i18n %} {% for notification_app in email_content %}

- {{ notification_app.title }} + {{ notification_app.translated_title }}

{% if notification_app.help_text %}

@@ -10,7 +11,7 @@ {% if notification_app.help_text_url %} - View all + {% trans "View all" as tmsg %}{{ tmsg | force_escape }} {% endif %} @@ -46,7 +47,7 @@ - View + {% trans "View" as tmsg %}{{ tmsg | force_escape }} @@ -60,7 +61,7 @@ {% if notification_app.show_remaining_count %}

- + {{ notification_app.remaining_count }} more + + {{ notification_app.remaining_count }} {% trans "more" as tmsg %}{{ tmsg | force_escape }}

{% endif %} diff --git a/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html b/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html index d47906786b..76c118f98c 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html @@ -1,3 +1,4 @@ +{% load i18n %} @@ -31,18 +32,22 @@ diff --git a/openedx/core/djangoapps/notifications/templates/notifications/digest_header.html b/openedx/core/djangoapps/notifications/templates/notifications/digest_header.html index 84a702d4c2..a1963335fa 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/digest_header.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/digest_header.html @@ -1,3 +1,4 @@ +{% load i18n %}

- {{ footer_email_reason|default:"You are receiving this email because you have subscribed to email digest" }} + {% if footer_email_reason %} + {{ footer_email_reason }} + {% else %} + {% trans "You are receiving this email because you have subscribed to email digest" as tmsg %}{{ tmsg | force_escape }} + {% endif %}

- Notification Settings + {% trans "Notification Settings" as tmsg %}{{ tmsg | force_escape }} - Unsubscribe from email digest for learning activity + {% trans "Unsubscribe from email digest for learning activity" as tmsg %}{{ tmsg | force_escape }}

- © {% now "Y" %} {{ platform_name }}. All Rights Reserved
+ © {% now "Y" %} {{ platform_name }}. {% trans "All rights reserved" as tmsg %}{{ tmsg | force_escape }}
{{ mailing_address }}

@@ -20,7 +21,11 @@ @@ -55,7 +60,7 @@ diff --git a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/body.html b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/body.html index 4d4daa7ca2..e1e69eca51 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/body.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/body.html @@ -1,3 +1,4 @@ +{% load i18n %} diff --git a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/body.txt b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/body.txt index 3bbe26faf7..ab3439bf8d 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/body.txt +++ b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/body.txt @@ -1 +1,7 @@ -{{ digest_frequency }} Notifications Digest for {% if digest_frequency == "Weekly" %}the Week of {% endif %}{{ start_date }} +{% load i18n %} + +{% if digest_frequency == "Weekly" %} + {% trans "Weekly Notifications Digest for the Week of" %} {{ start_date }} +{% else %} + {% trans "Daily Notifications Digest for" %} {{ start_date }} +{% endif %} diff --git a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/head.html b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/head.html index 8d63916b7a..7a24f14872 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/head.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/head.html @@ -1,3 +1,5 @@ +{% load i18n %} +{% get_current_language as LANGUAGE_CODE %} -{{ platform_name }} +{{ platform_name }} diff --git a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/subject.txt b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/subject.txt index 3bbe26faf7..ab3439bf8d 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/subject.txt +++ b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/subject.txt @@ -1 +1,7 @@ -{{ digest_frequency }} Notifications Digest for {% if digest_frequency == "Weekly" %}the Week of {% endif %}{{ start_date }} +{% load i18n %} + +{% if digest_frequency == "Weekly" %} + {% trans "Weekly Notifications Digest for the Week of" %} {{ start_date }} +{% else %} + {% trans "Daily Notifications Digest for" %} {{ start_date }} +{% endif %} diff --git a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/immediate_email/email/body.html b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/immediate_email/email/body.html index c500109bec..47eba46313 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/immediate_email/email/body.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/immediate_email/email/body.html @@ -1,3 +1,4 @@ +{% load i18n %} diff --git a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/immediate_email/email/head.html b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/immediate_email/email/head.html index 8d63916b7a..7a24f14872 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/immediate_email/email/head.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/edx_ace/immediate_email/email/head.html @@ -1,3 +1,5 @@ +{% load i18n %} +{% get_current_language as LANGUAGE_CODE %} -{{ platform_name }} +{{ platform_name }} diff --git a/openedx/core/djangoapps/notifications/templates/notifications/immediate_email_content.html b/openedx/core/djangoapps/notifications/templates/notifications/immediate_email_content.html index 7608b352ca..37f69582f5 100644 --- a/openedx/core/djangoapps/notifications/templates/notifications/immediate_email_content.html +++ b/openedx/core/djangoapps/notifications/templates/notifications/immediate_email_content.html @@ -1,9 +1,12 @@ +{% load i18n %}
Logo - Unsubscribe + + {% trans "Unsubscribe" as tmsg %}{{ tmsg | force_escape }} +
@@ -21,7 +24,7 @@

- View {{ view_text|default:""}} + {% trans "View" as tmsg %}{{ tmsg | force_escape }} {{ view_text|default:""}}

diff --git a/openedx/core/djangoapps/user_api/models.py b/openedx/core/djangoapps/user_api/models.py index d776dac8fe..2086ebecf3 100644 --- a/openedx/core/djangoapps/user_api/models.py +++ b/openedx/core/djangoapps/user_api/models.py @@ -105,6 +105,13 @@ class UserPreference(models.Model): """ return cls.objects.filter(user=user, key=preference_key).exists() + @classmethod + def get_preference_for_users(cls, user_ids, preference_key): + """ + Returns preference for list of users + """ + return cls.objects.filter(user__in=user_ids, key=preference_key) + @receiver(pre_save, sender=UserPreference) def pre_save_callback(sender, **kwargs):
- Unsubscribe + {% trans "Unsubscribe" as tmsg %}{{ tmsg | force_escape }}
- {{ digest_frequency }} email digest + {% if digest_frequency == "Weekly" %} + {% trans "Weekly email digest" as tmsg %}{{ tmsg | force_escape }} + {% else %} + {% trans "Daily email digest" as tmsg %}{{ tmsg | force_escape }} + {% endif %}
- {{update.title}} + {{update.translated_title}}