diff --git a/openedx/core/djangoapps/notifications/email/tasks.py b/openedx/core/djangoapps/notifications/email/tasks.py index 17aeca7ab2..ebc54a7661 100644 --- a/openedx/core/djangoapps/notifications/email/tasks.py +++ b/openedx/core/djangoapps/notifications/email/tasks.py @@ -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' No filtered notification for {user.username} ==Temp Log==') return diff --git a/openedx/core/djangoapps/notifications/email/tests/test_tasks.py b/openedx/core/djangoapps/notifications/email/tests/test_tasks.py index b24213233c..005ab13a04 100644 --- a/openedx/core/djangoapps/notifications/email/tests/test_tasks.py +++ b/openedx/core/djangoapps/notifications/email/tests/test_tasks.py @@ -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 diff --git a/openedx/core/djangoapps/notifications/email/utils.py b/openedx/core/djangoapps/notifications/email/utils.py index d40024adad..0b3b394ced 100644 --- a/openedx/core/djangoapps/notifications/email/utils.py +++ b/openedx/core/djangoapps/notifications/email/utils.py @@ -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