feat: added support to use account level preferences with email digest (#36976)

This commit is contained in:
Muhammad Adeel Tajamul
2025-07-03 12:04:47 +05:00
committed by GitHub
parent f996ed7c16
commit 113f0ff7e4
3 changed files with 234 additions and 5 deletions

View File

@@ -10,10 +10,12 @@ from edx_ace import ace
from edx_ace.recipient import Recipient
from edx_django_utils.monitoring import set_code_owner_attribute
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_ACCOUNT_LEVEL_PREFERENCES
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
from openedx.core.djangoapps.notifications.models import (
CourseNotificationPreference,
Notification,
NotificationPreference,
get_course_notification_preference_config_version
)
from .events import send_immediate_email_digest_sent_event, send_user_email_digest_sent_event
@@ -23,6 +25,7 @@ from .utils import (
create_app_notifications_dict,
create_email_digest_context,
create_email_template_context,
filter_email_enabled_notifications,
filter_notification_with_email_enabled_preferences,
get_course_info,
get_language_preference_for_users,
@@ -99,9 +102,15 @@ def send_digest_email_to_user(user, cadence_type, start_date, end_date, user_lan
return
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 ENABLE_ACCOUNT_LEVEL_PREFERENCES.is_enabled():
preferences = NotificationPreference.objects.filter(user=user)
notifications = filter_email_enabled_notifications(notifications, preferences, user,
cadence_type=cadence_type)
else:
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'<Email Cadence> No filtered notification for {user.username} ==Temp Log==')
return

View File

@@ -9,7 +9,9 @@ from unittest.mock import patch
from edx_toggles.toggles.testutils import override_waffle_flag
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS, ENABLE_EMAIL_NOTIFICATIONS
from openedx.core.djangoapps.notifications.config.waffle import (
ENABLE_ACCOUNT_LEVEL_PREFERENCES, ENABLE_NOTIFICATIONS, ENABLE_EMAIL_NOTIFICATIONS
)
from openedx.core.djangoapps.notifications.tasks import send_notifications
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
from openedx.core.djangoapps.notifications.email.tasks import (
@@ -18,7 +20,7 @@ from openedx.core.djangoapps.notifications.email.tasks import (
send_digest_email_to_user
)
from openedx.core.djangoapps.notifications.email.utils import get_start_end_date
from openedx.core.djangoapps.notifications.models import CourseNotificationPreference
from openedx.core.djangoapps.notifications.models import CourseNotificationPreference, NotificationPreference
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -125,6 +127,107 @@ class TestEmailDigestForUser(ModuleStoreTestCase):
assert mock_func.called is notification_created
@override_waffle_flag(ENABLE_ACCOUNT_LEVEL_PREFERENCES, True)
@ddt.ddt
class TestEmailDigestForUserWithAccountPreferences(ModuleStoreTestCase):
"""
Tests email notification for a specific user
"""
def setUp(self):
"""
Setup
"""
super().setUp()
self.user = UserFactory()
self.course = CourseFactory.create(display_name='test course', run="Testing_course")
@patch('edx_ace.ace.send')
def test_email_is_not_sent_if_no_notifications(self, mock_func):
"""
Tests email is sent iff waffle flag is enabled
"""
start_date, end_date = get_start_end_date(EmailCadence.DAILY)
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date)
assert not mock_func.called
@ddt.data(True, False)
@patch('edx_ace.ace.send')
def test_email_is_sent_iff_flag_enabled(self, flag_value, mock_func):
"""
Tests email is sent iff waffle flag is enabled
"""
created_date = datetime.datetime.now() - datetime.timedelta(days=1)
create_notification(self.user, self.course.id, created=created_date)
start_date, end_date = get_start_end_date(EmailCadence.DAILY)
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, flag_value):
send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date)
assert mock_func.called is flag_value
@patch('edx_ace.ace.send')
def test_notification_not_send_if_created_on_next_day(self, mock_func):
"""
Tests email is not sent if notification is created on next day
"""
start_date, end_date = get_start_end_date(EmailCadence.DAILY)
create_notification(self.user, self.course.id, created=end_date + datetime.timedelta(minutes=2))
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date)
assert not mock_func.called
@ddt.data(True, False)
@patch('edx_ace.ace.send')
def test_email_not_send_to_disable_user(self, value, mock_func):
"""
Tests email is not sent to disabled user
"""
created_date = datetime.datetime.now() - datetime.timedelta(days=1)
create_notification(self.user, self.course.id, created=created_date)
start_date, end_date = get_start_end_date(EmailCadence.DAILY)
if value:
self.user.set_password("12345678")
else:
self.user.set_unusable_password()
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date)
assert mock_func.called is value
@patch('edx_ace.ace.send')
def test_notification_not_send_if_created_day_before_yesterday(self, mock_func):
"""
Tests email is not sent if notification is created day before yesterday
"""
start_date, end_date = get_start_end_date(EmailCadence.DAILY)
created_date = datetime.datetime.now() - datetime.timedelta(days=1, minutes=18)
create_notification(self.user, self.course.id, created=created_date)
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date)
assert not mock_func.called
@ddt.data(
(EmailCadence.DAILY, datetime.datetime.now() - datetime.timedelta(days=1, minutes=30), False),
(EmailCadence.DAILY, datetime.datetime.now() - datetime.timedelta(minutes=10), True),
(EmailCadence.DAILY, datetime.datetime.now() - datetime.timedelta(days=1), True),
(EmailCadence.DAILY, datetime.datetime.now() + datetime.timedelta(minutes=20), False),
(EmailCadence.WEEKLY, datetime.datetime.now() - datetime.timedelta(days=7, minutes=30), False),
(EmailCadence.WEEKLY, datetime.datetime.now() - datetime.timedelta(days=7), True),
(EmailCadence.WEEKLY, datetime.datetime.now() - datetime.timedelta(minutes=20), True),
(EmailCadence.WEEKLY, datetime.datetime.now() + datetime.timedelta(minutes=20), False),
)
@ddt.unpack
@patch('edx_ace.ace.send')
def test_notification_content(self, cadence_type, created_time, notification_created, mock_func):
"""
Tests email only contains notification created within date
"""
start_date, end_date = get_start_end_date(cadence_type)
create_notification(self.user, self.course.id, created=created_time)
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date)
assert mock_func.called is notification_created
@ddt.ddt
class TestEmailDigestAudience(ModuleStoreTestCase):
"""
@@ -261,6 +364,65 @@ class TestPreferences(ModuleStoreTestCase):
assert not mock_func.called
@override_waffle_flag(ENABLE_ACCOUNT_LEVEL_PREFERENCES, True)
@ddt.ddt
class TestAccountPreferences(ModuleStoreTestCase):
"""
Tests preferences
"""
def setUp(self):
"""
Setup
"""
super().setUp()
self.user = UserFactory()
self.course = CourseFactory.create(display_name='test course', run="Testing_course")
self.preference, _ = NotificationPreference.objects.get_or_create(user=self.user, app="discussion",
type="new_discussion_post")
created_date = datetime.datetime.now() - datetime.timedelta(hours=23)
create_notification(self.user, self.course.id, notification_type='new_discussion_post', created=created_date)
@patch('edx_ace.ace.send')
def test_email_send_for_digest_preference(self, mock_func):
"""
Tests email is send for digest notification preference
"""
start_date, end_date = get_start_end_date(EmailCadence.DAILY)
self.preference.email = True
self.preference.email_cadence = EmailCadence.DAILY
self.preference.save()
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date)
assert mock_func.called
@ddt.data(True, False)
@patch('edx_ace.ace.send')
def test_email_send_for_email_preference_value(self, pref_value, mock_func):
"""
Tests email is sent iff preference value is True
"""
start_date, end_date = get_start_end_date(EmailCadence.DAILY)
self.preference.email = pref_value
self.preference.email_cadence = EmailCadence.DAILY
self.preference.save()
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date)
assert mock_func.called is pref_value
@patch('edx_ace.ace.send')
def test_email_not_send_if_different_digest_preference(self, mock_func):
"""
Tests email is not send if digest notification preference doesnot match
"""
start_date, end_date = get_start_end_date(EmailCadence.DAILY)
self.preference.email = True
self.preference.email_cadence = EmailCadence.WEEKLY
self.preference.save()
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY, start_date, end_date)
assert not mock_func.called
class TestImmediateEmail(ModuleStoreTestCase):
"""
Tests immediate email

View File

@@ -23,6 +23,7 @@ from openedx.core.djangoapps.notifications.email_notifications import EmailCaden
from openedx.core.djangoapps.notifications.events import notification_preference_unsubscribe_event
from openedx.core.djangoapps.notifications.models import (
CourseNotificationPreference,
NotificationPreference,
get_course_notification_preference_config_version
)
from openedx.core.djangoapps.user_api.models import UserPreference
@@ -325,6 +326,63 @@ def filter_notification_with_email_enabled_preferences(notifications, preference
return filtered_notifications
def create_missing_account_level_preferences(notifications, preferences, user):
"""
Creates missing account level preferences for notifications
"""
preferences = list(preferences)
notification_types = list(set(
(notification.app_name, "core") if COURSE_NOTIFICATION_TYPES[notification.notification_type]["is_core"]
else (notification.app_name, notification.notification_type)
for notification in notifications
))
missing_prefs = []
for notification_type in notification_types:
if not any(
preference.app == notification_type[0] and preference.type == notification_type[1]
for preference in preferences
):
if notification_type[1] == "core":
app_pref = COURSE_NOTIFICATION_APPS.get(notification_type[0], {})
default_pref = {
"notification_app": notification_type[0],
"web": app_pref["core_web"],
"push": app_pref["core_push"],
"email": app_pref["core_email"],
"email_cadence": app_pref["core_email_cadence"]
}
else:
default_pref = COURSE_NOTIFICATION_TYPES.get(notification_type[1], {})
missing_prefs.append(
NotificationPreference(
user=user, type=notification_type[1], app=notification_type[0], web=default_pref['web'],
push=default_pref['push'], email=default_pref['email'], email_cadence=default_pref['email_cadence'],
)
)
if missing_prefs:
created_prefs = NotificationPreference.objects.bulk_create(missing_prefs, ignore_conflicts=True)
preferences = preferences + list(created_prefs)
return preferences
def filter_email_enabled_notifications(notifications, preferences, user, cadence_type=EmailCadence.DAILY):
"""
Filter notifications with email enabled in account level preferences
"""
preferences = create_missing_account_level_preferences(notifications, preferences, user)
enabled_course_prefs = [
preference.type
for preference in preferences
if preference.email and preference.email_cadence == cadence_type
]
filtered_notifications = []
for notification in notifications:
if notification.notification_type in enabled_course_prefs:
filtered_notifications.append(notification)
filtered_notifications.sort(key=lambda elem: elem.created, reverse=True)
return filtered_notifications
def encrypt_string(string):
"""
Encrypts input string