Skip to content

Commit

Permalink
feat: add email cadence setting in notification preferences for emails
Browse files Browse the repository at this point in the history
  • Loading branch information
Saad Yousaf authored and saadyousafarbi committed Apr 15, 2024
1 parent 1968d8f commit 0270809
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 35 deletions.
16 changes: 13 additions & 3 deletions openedx/core/djangoapps/notifications/base_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
"""
from django.utils.translation import gettext_lazy as _

from .email_notifications import EmailCadence
from .utils import find_app_in_normalized_apps, find_pref_in_normalized_prefs
from ..django_comment_common.models import FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA

FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE = 'filter_audit_expired_users_with_no_role'


COURSE_NOTIFICATION_TYPES = {
'new_comment_on_response': {
'notification_app': 'discussion',
Expand Down Expand Up @@ -56,6 +58,7 @@
'info': '',
'web': False,
'email': False,
'email_cadence': EmailCadence.DAILY,
'push': False,
'non_editable': [],
'content_template': _('<{p}><{strong}>{username}</{strong}> posted <{strong}>{post_title}</{strong}></{p}>'),
Expand All @@ -73,6 +76,7 @@
'info': '',
'web': False,
'email': False,
'email_cadence': EmailCadence.DAILY,
'push': False,
'non_editable': [],
'content_template': _('<{p}><{strong}>{username}</{strong}> asked <{strong}>{post_title}</{strong}></{p}>'),
Expand Down Expand Up @@ -121,6 +125,7 @@
'info': '',
'web': True,
'email': True,
'email_cadence': EmailCadence.DAILY,
'push': True,
'non_editable': [],
'content_template': _('<p><strong>{username}’s </strong> {content_type} has been reported <strong> {'
Expand Down Expand Up @@ -170,6 +175,7 @@
'web': True,
'email': True,
'push': True,
'email_cadence': EmailCadence.DAILY,
'non_editable': [],
'content_template': _('<{p}>You have a new course update: '
'<{strong}>{course_update_content}</{strong}></{p}>'),
Expand All @@ -189,6 +195,7 @@
'core_web': True,
'core_email': True,
'core_push': True,
'core_email_cadence': EmailCadence.DAILY,
'non_editable': ['web']
},
'updates': {
Expand All @@ -197,6 +204,7 @@
'core_web': True,
'core_email': True,
'core_push': True,
'core_email_cadence': EmailCadence.DAILY,
'non_editable': []
},
}
Expand Down Expand Up @@ -263,7 +271,7 @@ def denormalize_preferences(normalized_preferences):
'web': preference.get('web'),
'push': preference.get('push'),
'email': preference.get('email'),
'info': preference.get('info'),
'email_cadence': preference.get('email_cadence'),
}
return denormalized_preferences

Expand Down Expand Up @@ -298,8 +306,8 @@ def update_preferences(preferences):
app_name = preference.get('app_name')
pref = find_pref_in_normalized_prefs(pref_name, app_name, old_preferences.get('preferences'))
if pref:
for channel in ['web', 'email', 'push']:
preference[channel] = pref[channel]
for channel in ['web', 'email', 'push', 'email_cadence']:
preference[channel] = pref.get(channel, preference.get(channel))
return NotificationPreferenceSyncManager.denormalize_preferences(new_prefs)


Expand Down Expand Up @@ -357,6 +365,7 @@ def get_non_core_notification_type_preferences(non_core_notification_types):
'web': notification_type.get('web', False),
'email': notification_type.get('email', False),
'push': notification_type.get('push', False),
'email_cadence': notification_type.get('email_cadence', 'Daily'),
}
return non_core_notification_type_preferences

Expand Down Expand Up @@ -388,6 +397,7 @@ def add_core_notification_preference(self, notification_app_attrs, notification_
'web': notification_app_attrs.get('core_web', False),
'email': notification_app_attrs.get('core_email', False),
'push': notification_app_attrs.get('core_push', False),
'email_cadence': notification_app_attrs.get('core_email_cadence', 'Daily'),
}

def add_core_notification_non_editable(self, notification_app_attrs, non_editable_channels):
Expand Down
35 changes: 35 additions & 0 deletions openedx/core/djangoapps/notifications/email_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
Email notifications module.
"""
from django.utils.translation import gettext_lazy as _


class EmailCadence:
"""
Email cadence class
"""
DAILY = 'Daily'
WEEKLY = 'Weekly'
INSTANTLY = 'Instantly'
NEVER = 'Never'
EMAIL_CADENCE_CHOICES = [
(DAILY, _('Daily')),
(WEEKLY, _('Weekly')),
(INSTANTLY, _('Instantly')),
(NEVER, _('Never')),
]
EMAIL_CADENCE_CHOICES_DICT = dict(EMAIL_CADENCE_CHOICES)

@classmethod
def get_email_cadence_choices(cls):
"""
Returns email cadence choices.
"""
return cls.EMAIL_CADENCE_CHOICES

@classmethod
def get_email_cadence_value(cls, email_cadence):
"""
Returns email cadence display for the given email cadence.
"""
return cls.EMAIL_CADENCE_CHOICES_DICT.get(email_cadence, None)
5 changes: 4 additions & 1 deletion openedx/core/djangoapps/notifications/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ def notification_preference_update_event(user, course_id, updated_preference):
"""
context = contexts.course_context_from_course_id(course_id)
with tracker.get_tracker().context(NOTIFICATION_PREFERENCES_UPDATED, context):
value = updated_preference.get('value', '')
if updated_preference.get('notification_channel', '') == 'email_cadence':
value = updated_preference.get('email_cadence', '')
tracker.emit(
NOTIFICATION_PREFERENCES_UPDATED,
{
Expand All @@ -136,7 +139,7 @@ def notification_preference_update_event(user, course_id, updated_preference):
'notification_app': updated_preference.get('notification_app', ''),
'notification_type': updated_preference.get('notification_type', ''),
'notification_channel': updated_preference.get('notification_channel', ''),
'value': updated_preference.get('value', ''),
'value': value
}
)

Expand Down
24 changes: 21 additions & 3 deletions openedx/core/djangoapps/notifications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

NOTIFICATION_CHANNELS = ['web', 'push', 'email']

ADDITIONAL_NOTIFICATION_CHANNEL_SETTINGS = ['email_cadence']

# Update this version when there is a change to any course specific notification type or app.
COURSE_NOTIFICATION_CONFIG_VERSION = 7

Expand Down Expand Up @@ -84,6 +86,13 @@ def get_notification_channels():
return NOTIFICATION_CHANNELS


def get_additional_notification_channel_settings():
"""
Returns the additional notification channel settings.
"""
return ADDITIONAL_NOTIFICATION_CHANNEL_SETTINGS


class Notification(TimeStampedModel):
"""
Model to store notifications for users
Expand Down Expand Up @@ -224,9 +233,18 @@ def get_channels_for_notification_type(self, app_name, notification_type) -> lis
['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)]
notification_channels = [channel for channel in NOTIFICATION_CHANNELS if
self.get_core_config(app_name).get(channel, False)]
additional_channel_settings = [channel for channel in ADDITIONAL_NOTIFICATION_CHANNEL_SETTINGS if
self.get_core_config(app_name).get(channel, False)]
else:
notification_channels = [channel for channel in NOTIFICATION_CHANNELS if
self.get_notification_type_config(app_name, notification_type).get(channel, False)]
additional_channel_settings = [channel for channel in ADDITIONAL_NOTIFICATION_CHANNEL_SETTINGS if
self.get_notification_type_config(app_name, notification_type).get(channel,
False)]

return notification_channels + additional_channel_settings

def is_core(self, app_name, notification_type) -> bool:
"""
Expand Down
49 changes: 39 additions & 10 deletions openedx/core/djangoapps/notifications/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from openedx.core.djangoapps.notifications.models import (
CourseNotificationPreference,
Notification,
get_notification_channels
get_notification_channels, get_additional_notification_channel_settings
)
from .base_notification import COURSE_NOTIFICATION_APPS, COURSE_NOTIFICATION_TYPES
from .base_notification import COURSE_NOTIFICATION_APPS, COURSE_NOTIFICATION_TYPES, EmailCadence
from .utils import filter_course_wide_preferences, remove_preferences_with_no_access


Expand Down Expand Up @@ -90,9 +90,10 @@ class UserNotificationPreferenceUpdateSerializer(serializers.Serializer):
"""

notification_app = serializers.CharField()
value = serializers.BooleanField()
value = serializers.BooleanField(required=False)
notification_type = serializers.CharField(required=False)
notification_channel = serializers.CharField(required=False)
email_cadence = serializers.CharField(required=False)

def validate(self, attrs):
"""
Expand All @@ -101,17 +102,24 @@ def validate(self, attrs):
notification_app = attrs.get('notification_app')
notification_type = attrs.get('notification_type')
notification_channel = attrs.get('notification_channel')
notification_email_cadence = attrs.get('email_cadence')

notification_app_config = self.instance.notification_preference_config

if notification_email_cadence:
if not notification_type:
raise ValidationError(
'notification_type is required for email_cadence.'
)
if EmailCadence.get_email_cadence_value(notification_email_cadence) is None:
raise ValidationError(
f'{attrs.get("value")} is not a valid email cadence.'
)

if notification_type and not notification_channel:
raise ValidationError(
'notification_channel is required for notification_type.'
)
if notification_channel and not notification_type:
raise ValidationError(
'notification_type is required for notification_channel.'
)

if not notification_app_config.get(notification_app, None):
raise ValidationError(
Expand All @@ -126,9 +134,13 @@ def validate(self, attrs):
f'{notification_type} is not a valid notification type.'
)

if notification_channel and notification_channel not in get_notification_channels():
if (
notification_channel and
notification_channel not in get_notification_channels()
and notification_channel not in get_additional_notification_channel_settings()
):
raise ValidationError(
f'{notification_channel} is not a valid notification channel.'
f'{notification_channel} is not a valid notification channel setting.'
)

return attrs
Expand All @@ -141,13 +153,30 @@ def update(self, instance, validated_data):
notification_type = validated_data.get('notification_type')
notification_channel = validated_data.get('notification_channel')
value = validated_data.get('value')
notification_email_cadence = validated_data.get('email_cadence')

user_notification_preference_config = instance.notification_preference_config

if notification_type and notification_channel:
# Notification email cadence update
if notification_email_cadence and notification_type:
user_notification_preference_config[notification_app]['notification_types'][notification_type][
'email_cadence'] = notification_email_cadence

# Notification type channel update
elif notification_type and notification_channel:
# Update the notification preference for specific notification type
user_notification_preference_config[
notification_app]['notification_types'][notification_type][notification_channel] = value

# Notification app-wide channel update
elif notification_channel and not notification_type:
app_prefs = user_notification_preference_config[notification_app]
for notification_type_name, notification_type_preferences in app_prefs['notification_types'].items():
non_editable_channels = app_prefs['non_editable'].get(notification_type_name, [])
if notification_channel not in non_editable_channels:
app_prefs['notification_types'][notification_type_name][notification_channel] = value

# Notification app update
else:
# Update the notification preference for notification_app
user_notification_preference_config[notification_app]['enabled'] = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def _create_notification_app(self, overrides=None):
'core_web': True,
'core_email': True,
'core_push': True,
'core_email_cadence': 'Daily',
}
if overrides is not None:
notification_app.update(overrides)
Expand All @@ -104,6 +105,7 @@ def _create_notification_type(self, name, overrides=None):
'web': True,
'email': True,
'push': True,
'email_cadence': 'Daily',
'info': '',
'non_editable': [],
'content_template': '',
Expand Down Expand Up @@ -255,6 +257,7 @@ def test_validate_notification_apps(self):
for app_data in notification_apps.values():
assert 'core_info' in app_data.keys()
assert isinstance(app_data['non_editable'], list)
assert isinstance(app_data['core_email_cadence'], str)
for key in bool_keys:
assert isinstance(app_data[key], bool)

Expand Down Expand Up @@ -290,6 +293,7 @@ def test_validate_non_core_notification_types(self):
assert 'content_template' in notification_type.keys()
assert isinstance(notification_type['content_context'], dict)
assert isinstance(notification_type['non_editable'], list)
assert isinstance(notification_type['email_cadence'], str)
for key in str_keys:
assert isinstance(notification_type[key], str)
for key in bool_keys:
Expand Down
Loading

0 comments on commit 0270809

Please sign in to comment.