diff --git a/cms/envs/common.py b/cms/envs/common.py index 15e1c39d19..a73974ba34 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2681,7 +2681,7 @@ EDXAPP_PARSE_KEYS = {} ############## NOTIFICATIONS EXPIRY ############## NOTIFICATIONS_EXPIRY = 60 EXPIRED_NOTIFICATIONS_DELETE_BATCH_SIZE = 10000 -NOTIFICATION_CREATION_BATCH_SIZE = 99 +NOTIFICATION_CREATION_BATCH_SIZE = 83 ############################ AI_TRANSLATIONS ################################## AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1' diff --git a/lms/envs/common.py b/lms/envs/common.py index e41a9b131b..3e651a915f 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -5390,7 +5390,7 @@ SUBSCRIPTIONS_SERVICE_WORKER_USERNAME = 'subscriptions_worker' ############## NOTIFICATIONS ############## NOTIFICATIONS_EXPIRY = 60 EXPIRED_NOTIFICATIONS_DELETE_BATCH_SIZE = 10000 -NOTIFICATION_CREATION_BATCH_SIZE = 99 +NOTIFICATION_CREATION_BATCH_SIZE = 83 NOTIFICATIONS_DEFAULT_FROM_EMAIL = "no-reply@example.com" NOTIFICATION_TYPE_ICONS = {} DEFAULT_NOTIFICATION_ICON_URL = "" diff --git a/openedx/core/djangoapps/notifications/migrations/0005_notification_email_notification_web.py b/openedx/core/djangoapps/notifications/migrations/0005_notification_email_notification_web.py new file mode 100644 index 0000000000..013cc7e33d --- /dev/null +++ b/openedx/core/djangoapps/notifications/migrations/0005_notification_email_notification_web.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.10 on 2024-03-18 12:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0004_auto_20230607_0757'), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='email', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='notification', + name='web', + field=models.BooleanField(default=True), + ), + ] diff --git a/openedx/core/djangoapps/notifications/models.py b/openedx/core/djangoapps/notifications/models.py index 1fae970a40..e0724fbe9b 100644 --- a/openedx/core/djangoapps/notifications/models.py +++ b/openedx/core/djangoapps/notifications/models.py @@ -96,6 +96,8 @@ class Notification(TimeStampedModel): notification_type = models.CharField(max_length=64) content_context = models.JSONField(default=dict) content_url = models.URLField(null=True, blank=True) + web = models.BooleanField(default=True, null=False, blank=False) + email = models.BooleanField(default=False, null=False, blank=False) last_read = models.DateTimeField(null=True, blank=True) last_seen = models.DateTimeField(null=True, blank=True) @@ -205,6 +207,27 @@ class CourseNotificationPreference(TimeStampedModel): return self.get_core_config(app_name).get('web', False) return self.get_notification_type_config(app_name, notification_type).get('web', False) + def is_enabled_for_any_channel(self, app_name, notification_type) -> bool: + """ + Returns True if the notification type is enabled for any channel. + """ + if self.is_core(app_name, notification_type): + return any(self.get_core_config(app_name).get(channel, False) for channel in NOTIFICATION_CHANNELS) + return any(self.get_notification_type_config(app_name, notification_type).get(channel, False) for channel in + NOTIFICATION_CHANNELS) + + def get_channels_for_notification_type(self, app_name, notification_type) -> list: + """ + Returns the channels for the given app name and notification type. + if notification is core then return according to core settings + Sample Response: + ['web', 'push'] + """ + if self.is_core(app_name, notification_type): + return [channel for channel in NOTIFICATION_CHANNELS if self.get_core_config(app_name).get(channel, False)] + return [channel for channel in NOTIFICATION_CHANNELS if + self.get_notification_type_config(app_name, notification_type).get(channel, False)] + def is_core(self, app_name, notification_type) -> bool: """ Returns True if the given notification type is a core notification type. diff --git a/openedx/core/djangoapps/notifications/tasks.py b/openedx/core/djangoapps/notifications/tasks.py index 7038d35990..137303fd5a 100644 --- a/openedx/core/djangoapps/notifications/tasks.py +++ b/openedx/core/djangoapps/notifications/tasks.py @@ -24,7 +24,7 @@ from openedx.core.djangoapps.notifications.filters import NotificationFilter from openedx.core.djangoapps.notifications.models import ( CourseNotificationPreference, Notification, - get_course_notification_preference_config_version + get_course_notification_preference_config_version, ) from openedx.core.djangoapps.notifications.utils import get_list_in_batches @@ -132,11 +132,13 @@ def send_notifications(user_ids, course_key: str, app_name, notification_type, c for preference in preferences: user_id = preference.user_id preference = update_user_preference(preference, user_id, course_key) + if ( preference and - preference.get_web_config(app_name, notification_type) and + preference.is_enabled_for_any_channel(app_name, notification_type) and preference.get_app_config(app_name).get('enabled', False) ): + notification_preferences = preference.get_channels_for_notification_type(app_name, notification_type) notifications.append( Notification( user_id=user_id, @@ -145,6 +147,8 @@ def send_notifications(user_ids, course_key: str, app_name, notification_type, c content_context=context, content_url=content_url, course_id=course_key, + web='web' in notification_preferences, + email='email' in notification_preferences, ) ) generated_notification_audience.append(user_id) @@ -170,7 +174,7 @@ def is_notification_valid(notification_type, context): """ try: get_notification_content(notification_type, context) - except Exception: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except return False return True diff --git a/openedx/core/djangoapps/notifications/tests/test_tasks.py b/openedx/core/djangoapps/notifications/tests/test_tasks.py index bc3aa6a2e4..4ef627b749 100644 --- a/openedx/core/djangoapps/notifications/tests/test_tasks.py +++ b/openedx/core/djangoapps/notifications/tests/test_tasks.py @@ -176,6 +176,8 @@ class SendNotificationsTest(ModuleStoreTestCase): preference = CourseNotificationPreference.get_user_course_preference(self.user.id, self.course_1.id) app_prefs = preference.notification_preference_config[app_name] app_prefs['notification_types']['core']['web'] = False + app_prefs['notification_types']['core']['email'] = False + app_prefs['notification_types']['core']['push'] = False preference.save() send_notifications([self.user.id], str(self.course_1.id), app_name, notification_type, context, content_url) diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py index 2c44f4804c..0ac589a40a 100644 --- a/openedx/core/djangoapps/notifications/tests/test_views.py +++ b/openedx/core/djangoapps/notifications/tests/test_views.py @@ -570,11 +570,10 @@ class UserNotificationChannelPreferenceAPITest(ModuleStoreTestCase): if expected_status == status.HTTP_200_OK: expected_data = self._expected_api_response() expected_app_prefs = expected_data['notification_preference_config'][notification_app] - for notification_type_name, notification_type_preferences in expected_app_prefs[ - 'notification_types'].items(): - non_editable_channels = expected_app_prefs['non_editable'].get(notification_type_name, []) + for notification_type, __ in expected_app_prefs['notification_types'].items(): + non_editable_channels = expected_app_prefs['non_editable'].get(notification_type, []) if notification_channel not in non_editable_channels: - expected_app_prefs['notification_types'][notification_type_name][notification_channel] = value + expected_app_prefs['notification_types'][notification_type][notification_channel] = value expected_data = remove_notifications_with_visibility_settings(expected_data) self.assertEqual(response.data, expected_data) event_name, event_data = mock_emit.call_args[0] @@ -584,6 +583,7 @@ class UserNotificationChannelPreferenceAPITest(ModuleStoreTestCase): self.assertEqual(event_data['value'], value) +@ddt.ddt class NotificationListAPIViewTest(APITestCase): """ Tests suit for the NotificationListAPIView. @@ -663,6 +663,43 @@ class NotificationListAPIViewTest(APITestCase): '

test_user responded to your post This is a test post.

' ) + @ddt.data( + ([], 0), + (['web'], 1), + (['email'], 0), + (['web', 'email'], 1), + (['web', 'email', 'push'], 1), + ) + @ddt.unpack + def test_list_notifications_with_channels(self, channels, expected_count): + """ + Test that the view can filter notifications by app name and channels. + """ + + Notification.objects.create( + user=self.user, + app_name='discussion', + notification_type='new_response', + content_context={ + 'replier_name': 'test_user', + 'post_title': 'This is a test post.', + }, + web='web' in channels, + email='email' in channels + ) + + self.client.login(username=self.user.username, password=self.TEST_PASSWORD) + + # Make a request to the view with the app_name query parameter set to 'app1'. + response = self.client.get(self.url + "?app_name=discussion") + + # Assert that the response is successful. + self.assertEqual(response.status_code, 200) + + # Assert that the response contains expected results i.e. channels contains web or is null. + data = response.data['results'] + self.assertEqual(len(data), expected_count) + @mock.patch("eventtracking.tracker.emit") def test_list_notifications_with_tray_opened_param(self, mock_emit): """ diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py index 364a619ec0..4bdbda5eb6 100644 --- a/openedx/core/djangoapps/notifications/views.py +++ b/openedx/core/djangoapps/notifications/views.py @@ -330,18 +330,15 @@ class NotificationListAPIView(generics.ListAPIView): if self.request.query_params.get('tray_opened'): unseen_count = Notification.objects.filter(user_id=self.request.user, last_seen__isnull=True).count() notification_tray_opened_event(self.request.user, unseen_count) + params = { + 'user': self.request.user, + 'created__gte': expiry_date, + 'web': True + } if app_name: - return Notification.objects.filter( - user=self.request.user, - app_name=app_name, - created__gte=expiry_date, - ).order_by('-id') - else: - return Notification.objects.filter( - user=self.request.user, - created__gte=expiry_date, - ).order_by('-id') + params['app_name'] = app_name + return Notification.objects.filter(**params).order_by('-id') @allow_any_authenticated_user()