feat: added channels column to send notification on specific channels (#34289)
* feat: added channels column to send notifications on specific channels
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
'<p><strong>test_user</strong> responded to your post <strong>This is a test post.</strong></p>'
|
||||
)
|
||||
|
||||
@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):
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user