feat: added daily and weekly email digest (#34539)

* feat: added daily and weekly email digest
This commit is contained in:
Muhammad Adeel Tajamul
2024-05-07 12:15:03 +05:00
committed by GitHub
parent 96f210559b
commit a454da9ca6
11 changed files with 862 additions and 25 deletions

View File

@@ -3,7 +3,7 @@ This module contains various configuration settings via
waffle switches for the notifications app.
"""
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlag
WAFFLE_NAMESPACE = 'notifications'
@@ -48,7 +48,6 @@ ENABLE_NOTIFICATIONS_FILTERS = CourseWaffleFlag(f"{WAFFLE_NAMESPACE}.enable_noti
# .. toggle_tickets: INF-1145
ENABLE_COURSEWIDE_NOTIFICATIONS = CourseWaffleFlag(f"{WAFFLE_NAMESPACE}.enable_coursewide_notifications", __name__)
# .. toggle_name: notifications.enable_ora_staff_notifications
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
@@ -58,3 +57,14 @@ ENABLE_COURSEWIDE_NOTIFICATIONS = CourseWaffleFlag(f"{WAFFLE_NAMESPACE}.enable_c
# .. toggle_target_removal_date: 2024-06-04
# .. toggle_tickets: INF-1304
ENABLE_ORA_STAFF_NOTIFICATION = CourseWaffleFlag(f"{WAFFLE_NAMESPACE}.enable_ora_staff_notifications", __name__)
# .. toggle_name: notifications.enable_email_notifications
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to enable the Email Notifications feature
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 2024-03-25
# .. toggle_target_removal_date: 2025-06-01
# .. toggle_warning: When the flag is ON, Email Notifications feature is enabled.
# .. toggle_tickets: INF-1259
ENABLE_EMAIL_NOTIFICATIONS = WaffleFlag(f'{WAFFLE_NAMESPACE}.enable_email_notifications', __name__)

View File

@@ -0,0 +1,116 @@
"""
Celery tasks for sending email notifications
"""
from celery import shared_task
from celery.utils.log import get_task_logger
from django.contrib.auth import get_user_model
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.email_notifications import EmailCadence
from openedx.core.djangoapps.notifications.models import (
CourseNotificationPreference,
Notification,
get_course_notification_preference_config_version
)
from .message_type import EmailNotificationMessageType
from .utils import (
create_app_notifications_dict,
create_email_digest_context,
filter_notification_with_email_enabled_preferences,
get_start_end_date,
get_unique_course_ids,
is_email_notification_flag_enabled
)
User = get_user_model()
logger = get_task_logger(__name__)
def get_audience_for_cadence_email(cadence_type):
"""
Returns users that are eligible to receive cadence email
"""
if cadence_type not in [EmailCadence.DAILY, EmailCadence.WEEKLY]:
raise ValueError("Invalid value for parameter cadence_type")
start_date, end_date = get_start_end_date(cadence_type)
user_ids = Notification.objects.filter(
email=True,
created__gte=start_date,
created__lte=end_date
).values_list('user__id', flat=True).distinct()
users = User.objects.filter(id__in=user_ids)
return users
def get_user_preferences_for_courses(course_ids, user):
"""
Returns updated user preference for course_ids
"""
# Create new preferences
new_preferences = []
preferences = CourseNotificationPreference.objects.filter(user=user, course_id__in=course_ids)
preferences = list(preferences)
for course_id in course_ids:
if not any(preference.course_id == course_id for preference in preferences):
pref = CourseNotificationPreference(user=user, course_id=course_id)
new_preferences.append(pref)
if new_preferences:
CourseNotificationPreference.objects.bulk_create(new_preferences, ignore_conflicts=True)
# Update preferences to latest config version
current_version = get_course_notification_preference_config_version()
for preference in preferences:
if preference.config_version != current_version:
preference = preference.get_user_course_preference(user.id, preference.course_id)
new_preferences.append(preference)
return new_preferences
def send_digest_email_to_user(user, cadence_type, course_language='en', courses_data=None):
"""
Send [cadence_type] email to user.
Cadence Type can be EmailCadence.DAILY or EmailCadence.WEEKLY
"""
if cadence_type not in [EmailCadence.DAILY, EmailCadence.WEEKLY]:
raise ValueError('Invalid cadence_type')
logger.info(f'<Email Cadence> Sending email to user {user.username} ==Temp Log==')
if not is_email_notification_flag_enabled(user):
logger.info(f'<Email Cadence> Flag disabled for {user.username} ==Temp Log==')
return
start_date, end_date = get_start_end_date(cadence_type)
notifications = Notification.objects.filter(user=user, email=True,
created__gte=start_date, created__lte=end_date)
if not notifications:
logger.info(f'<Email Cadence> No notification for {user.username} ==Temp Log==')
return
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
apps_dict = create_app_notifications_dict(notifications)
message_context = create_email_digest_context(apps_dict, start_date, end_date, cadence_type,
courses_data=courses_data)
recipient = Recipient(user.id, user.email)
message = EmailNotificationMessageType(
app_label="notifications", name="email_digest"
).personalize(recipient, course_language, message_context)
ace.send(message)
logger.info(f'<Email Cadence> Email sent to {user.username} ==Temp Log==')
@shared_task(ignore_result=True)
@set_code_owner_attribute
def send_digest_email_to_all_users(cadence_type):
"""
Send email digest to all eligible users
"""
logger.info(f'<Email Cadence> Sending cadence email of type {cadence_type}')
users = get_audience_for_cadence_email(cadence_type)
courses_data = {}
logger.info(f'<Email Cadence> Email Cadence Audience {len(users)}')
for user in users:
send_digest_email_to_user(user, cadence_type, courses_data=courses_data)

View File

@@ -0,0 +1,195 @@
"""
Test cases for notifications/email/tasks.py
"""
import datetime
import ddt
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_EMAIL_NOTIFICATIONS
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
from openedx.core.djangoapps.notifications.email.tasks import (
get_audience_for_cadence_email,
send_digest_email_to_all_users,
send_digest_email_to_user
)
from openedx.core.djangoapps.notifications.models import CourseNotificationPreference
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from .utils import create_notification
@ddt.ddt
class TestEmailDigestForUser(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
"""
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY)
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)
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, flag_value):
send_digest_email_to_user(self.user, EmailCadence.DAILY)
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
"""
create_notification(self.user, self.course.id)
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY)
assert not mock_func.called
@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
"""
created_date = datetime.datetime.now() - datetime.timedelta(days=2)
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)
assert not mock_func.called
@ddt.ddt
class TestEmailDigestAudience(ModuleStoreTestCase):
"""
Tests audience for notification digest email
"""
def setUp(self):
"""
Setup
"""
super().setUp()
self.user = UserFactory()
self.course = CourseFactory.create(display_name='test course', run="Testing_course")
@patch('openedx.core.djangoapps.notifications.email.tasks.send_digest_email_to_user')
def test_email_func_not_called_if_no_notification(self, mock_func):
"""
Tests email sending function is not called if user has no notifications
"""
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_all_users(EmailCadence.DAILY)
assert not mock_func.called
@patch('openedx.core.djangoapps.notifications.email.tasks.send_digest_email_to_user')
def test_email_func_called_if_user_has_notification(self, mock_func):
"""
Tests email sending function is called if user has notification
"""
created_date = datetime.datetime.now() - datetime.timedelta(days=1)
create_notification(self.user, self.course.id, created=created_date)
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_all_users(EmailCadence.DAILY)
assert mock_func.called
@patch('openedx.core.djangoapps.notifications.email.tasks.send_digest_email_to_user')
def test_email_func_not_called_if_user_notification_is_not_duration(self, mock_func):
"""
Tests email sending function is not called if user has notification
which is not in duration
"""
created_date = datetime.datetime.now() - datetime.timedelta(days=10)
create_notification(self.user, self.course.id, created=created_date)
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_all_users(EmailCadence.DAILY)
assert not mock_func.called
@patch('edx_ace.ace.send')
def test_email_is_sent_to_user_when_task_is_called(self, mock_func):
created_date = datetime.datetime.now() - datetime.timedelta(days=1)
create_notification(self.user, self.course.id, created=created_date)
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_all_users(EmailCadence.DAILY)
assert mock_func.called
assert mock_func.call_count == 1
def test_audience_query_count(self):
with self.assertNumQueries(1):
audience = get_audience_for_cadence_email(EmailCadence.DAILY)
list(audience) # evaluating queryset
@ddt.data(True, False)
@patch('edx_ace.ace.send')
def test_digest_should_contain_email_enabled_notifications(self, email_value, mock_func):
"""
Tests email is sent only when notifications with email=True exists
"""
created_date = datetime.datetime.now() - datetime.timedelta(days=1)
create_notification(self.user, self.course.id, created=created_date, email=email_value)
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY)
assert mock_func.called is email_value
class TestPreferences(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 = CourseNotificationPreference.objects.create(user=self.user, course_id=self.course.id)
created_date = datetime.datetime.now() - datetime.timedelta(days=1)
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
"""
config = self.preference.notification_preference_config
types = config['discussion']['notification_types']
types['new_discussion_post']['email_cadence'] = EmailCadence.DAILY
self.preference.save()
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY)
assert mock_func.called
@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
"""
config = self.preference.notification_preference_config
types = config['discussion']['notification_types']
types['new_discussion_post']['email_cadence'] = EmailCadence.WEEKLY
self.preference.save()
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
send_digest_email_to_user(self.user, EmailCadence.DAILY)
assert not mock_func.called

View File

@@ -0,0 +1,196 @@
"""
Test utils.py
"""
import datetime
import ddt
from pytz import utc
from waffle import get_waffle_flag_model # pylint: disable=invalid-django-waffle-import
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_EMAIL_NOTIFICATIONS
from openedx.core.djangoapps.notifications.models import Notification
from openedx.core.djangoapps.notifications.email.utils import (
add_additional_attributes_to_notifications,
create_app_notifications_dict,
create_datetime_string,
create_email_digest_context,
create_email_template_context,
get_course_info,
get_time_ago,
is_email_notification_flag_enabled,
)
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from .utils import assert_list_equal, create_notification
class TestUtilFunctions(ModuleStoreTestCase):
"""
Test utils functions
"""
def setUp(self):
"""
Setup
"""
super().setUp()
self.user = UserFactory()
self.course = CourseFactory.create(display_name='test course', run="Testing_course")
def test_additional_attributes(self):
"""
Tests additional attributes are added when notifications list is passed to
add_additional_attributes_to_notifications function
"""
notification = create_notification(self.user, self.course.id)
additional_params = ['course_name', 'icon', 'time_ago']
for param in additional_params:
assert not hasattr(notification, param)
add_additional_attributes_to_notifications([notification])
for param in additional_params:
assert hasattr(notification, param)
def test_create_app_notifications_dict(self):
"""
Tests notifications are divided based on their app_name
"""
Notification.objects.all().delete()
create_notification(self.user, self.course.id, app_name='discussion', notification_type='new_comment')
create_notification(self.user, self.course.id, app_name='updates', notification_type='course_update')
app_dict = create_app_notifications_dict(Notification.objects.all())
assert len(app_dict.keys()) == 2
for key in ['discussion', 'updates']:
assert key in app_dict.keys()
assert app_dict[key]['count'] == 1
assert len(app_dict[key]['notifications']) == 1
def test_get_course_info(self):
"""
Tests get_course_info function
"""
assert get_course_info(self.course.id) == {'name': 'test course'}
def test_get_time_ago(self):
"""
Tests time_ago string
"""
current_datetime = utc.localize(datetime.datetime.now())
assert "Today" == get_time_ago(current_datetime)
assert "1d" == get_time_ago(current_datetime - datetime.timedelta(days=1))
assert "1w" == get_time_ago(current_datetime - datetime.timedelta(days=7))
def test_datetime_string(self):
dt = datetime.datetime(2024, 3, 25)
assert create_datetime_string(dt) == "Monday, Mar 25"
@ddt.ddt
class TestContextFunctions(ModuleStoreTestCase):
"""
Test template context functions in utils.py
"""
def setUp(self):
"""
Setup
"""
super().setUp()
self.user = UserFactory()
self.course = CourseFactory.create(display_name='test course', run="Testing_course")
def test_email_template_context(self):
"""
Tests common header and footer context
"""
context = create_email_template_context()
keys = ['platform_name', 'mailing_address', 'logo_url', 'social_media', 'notification_settings_url']
for key in keys:
assert key in context
@ddt.data('Daily', 'Weekly')
def test_email_digest_context(self, digest_frequency):
"""
Tests context for email digest
"""
Notification.objects.all().delete()
discussion_notification = create_notification(self.user, self.course.id, app_name='discussion',
notification_type='new_comment')
update_notification = create_notification(self.user, self.course.id, app_name='updates',
notification_type='course_update')
app_dict = create_app_notifications_dict(Notification.objects.all())
end_date = datetime.datetime(2024, 3, 24, 12, 0)
params = {
"app_notifications_dict": app_dict,
"start_date": end_date - datetime.timedelta(days=0 if digest_frequency == "Daily" else 6),
"end_date": end_date,
"digest_frequency": digest_frequency,
"courses_data": None
}
context = create_email_digest_context(**params)
expected_start_date = 'Sunday, Mar 24' if digest_frequency == 'Daily' else 'Monday, Mar 18'
expected_digest_updates = [
{'title': 'Total Notifications', 'count': 2},
{'title': 'Discussion', 'count': 1},
{'title': 'Updates', 'count': 1},
]
expected_email_content = [
{'title': 'Discussion', 'help_text': '', 'help_text_url': '', 'notifications': [discussion_notification]},
{'title': 'Updates', 'help_text': '', 'help_text_url': '', 'notifications': [update_notification]}
]
assert context['start_date'] == expected_start_date
assert context['end_date'] == 'Sunday, Mar 24'
assert context['digest_frequency'] == digest_frequency
assert_list_equal(context['email_digest_updates'], expected_digest_updates)
assert_list_equal(context['email_content'], expected_email_content)
class TestWaffleFlag(ModuleStoreTestCase):
"""
Test user level email notifications waffle flag
"""
def setUp(self):
"""
Setup
"""
super().setUp()
self.user_1 = UserFactory()
self.user_2 = UserFactory()
self.course_1 = CourseFactory.create(display_name='test course 1', run="Testing_course_1")
self.course_1 = CourseFactory.create(display_name='test course 2', run="Testing_course_2")
def test_waffle_flag_for_everyone(self):
"""
Tests if waffle flag is enabled for everyone
"""
assert is_email_notification_flag_enabled() is False
waffle_model = get_waffle_flag_model()
flag, _ = waffle_model.objects.get_or_create(name=ENABLE_EMAIL_NOTIFICATIONS.name)
flag.everyone = True
flag.save()
assert is_email_notification_flag_enabled() is True
def test_waffle_flag_for_user(self):
"""
Tests user level waffle flag
"""
assert is_email_notification_flag_enabled() is False
waffle_model = get_waffle_flag_model()
flag, _ = waffle_model.objects.get_or_create(name=ENABLE_EMAIL_NOTIFICATIONS.name)
flag.users.add(self.user_1)
flag.save()
assert is_email_notification_flag_enabled(self.user_1) is True
assert is_email_notification_flag_enabled(self.user_2) is False
def test_waffle_flag_everyone_priority(self):
"""
Tests if everyone field has more priority over user field
"""
assert is_email_notification_flag_enabled() is False
waffle_model = get_waffle_flag_model()
flag, _ = waffle_model.objects.get_or_create(name=ENABLE_EMAIL_NOTIFICATIONS.name)
flag.everyone = False
flag.users.add(self.user_1)
flag.save()
assert is_email_notification_flag_enabled() is False
assert is_email_notification_flag_enabled(self.user_1) is False
assert is_email_notification_flag_enabled(self.user_2) is False

View File

@@ -0,0 +1,40 @@
"""
Utils for tests
"""
from openedx.core.djangoapps.notifications.models import Notification
def create_notification(user, course_key, **kwargs):
"""
Create a test notification
"""
notification_params = {
'user': user,
'course_id': course_key,
'app_name': "discussion",
'notification_type': "new_comment",
'content_url': '',
'content_context': {
"replier_name": "replier",
"username": "username",
"author_name": "author_name",
"post_title": "post_title",
"course_update_content": "Course update content",
"content_type": 'post',
"content": "post_title"
},
'email': True,
'web': True
}
notification_params.update(kwargs)
notification = Notification.objects.create(**notification_params)
return notification
def assert_list_equal(list_1, list_2):
"""
Asserts if list is equal
"""
assert len(list_1) == len(list_2)
for element in list_1:
assert element in list_2

View File

@@ -1,12 +1,46 @@
"""
Email Notifications Utils
"""
import datetime
from django.conf import settings
from pytz import utc
from waffle import get_waffle_flag_model # pylint: disable=invalid-django-waffle-import
from lms.djangoapps.branding.api import get_logo_url_for_email
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_EMAIL_NOTIFICATIONS
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
from xmodule.modulestore.django import modulestore
from .notification_icons import NotificationTypeIcons
def is_email_notification_flag_enabled(user=None):
"""
Returns if waffle flag is enabled for user or not
"""
flag_model = get_waffle_flag_model()
try:
flag = flag_model.objects.get(name=ENABLE_EMAIL_NOTIFICATIONS.name)
except flag_model.DoesNotExist:
return False
if flag.everyone is not None:
return flag.everyone
if user:
role_value = flag.is_active_for_user(user)
if role_value is not None:
return role_value
try:
return flag.users.contains(user)
except ValueError:
pass
return False
def create_datetime_string(datetime_instance):
"""
Returns string for datetime object
"""
return datetime_instance.strftime('%A, %b %d')
@@ -40,25 +74,172 @@ def create_email_template_context():
}
def create_email_digest_content(start_date, end_date=None, digest_frequency="Daily",
notifications_count=0, updates_count=0, email_content=None):
def create_email_digest_context(app_notifications_dict, start_date, end_date=None, digest_frequency="Daily",
courses_data=None):
"""
Creates email context based on content
app_notifications_dict: Mapping of notification app and its count, title and notifications
start_date: datetime instance
end_date: datetime instance
digest_frequency: EmailCadence.DAILY or EmailCadence.WEEKLY
courses_data: Dictionary to cache course info (avoid additional db calls)
"""
context = create_email_template_context()
start_date_str = create_datetime_string(start_date)
end_date_str = create_datetime_string(end_date if end_date else start_date)
email_digest_updates = [{
'title': 'Total Notifications',
'count': sum(value['count'] for value in app_notifications_dict.values())
}]
email_digest_updates.extend([
{
'title': value['title'],
'count': value['count'],
}
for key, value in app_notifications_dict.items()
])
email_content = [
{
'title': value['title'],
'help_text': value.get('help_text', ''),
'help_text_url': value.get('help_text_url', ''),
'notifications': add_additional_attributes_to_notifications(
value.get('notifications', []), courses_data=courses_data
)
}
for key, value in app_notifications_dict.items()
]
context.update({
"start_date": start_date_str,
"end_date": end_date_str,
"digest_frequency": digest_frequency,
"updates": [
{"count": updates_count, "type": "Updates"},
{"count": notifications_count, "type": "Notifications"}
],
"email_content": email_content if email_content else [],
"get_icon_url_for_notification_type": get_icon_url_for_notification_type,
"email_digest_updates": email_digest_updates,
"email_content": email_content,
})
return context
def get_start_end_date(cadence_type):
"""
Returns start_date and end_date for email digest
"""
if cadence_type not in [EmailCadence.DAILY, EmailCadence.WEEKLY]:
raise ValueError('Invalid cadence_type')
date_today = datetime.datetime.now()
yesterday = date_today - datetime.timedelta(days=1)
end_date = datetime.datetime.combine(yesterday, datetime.time.max)
start_date = end_date
if cadence_type == EmailCadence.WEEKLY:
start_date = end_date - datetime.timedelta(days=6)
start_date = datetime.datetime.combine(start_date, datetime.time.min)
return utc.localize(start_date), utc.localize(end_date)
def get_course_info(course_key):
"""
Returns course info for course_key
"""
store = modulestore()
course = store.get_course(course_key)
return {'name': course.display_name}
def get_time_ago(datetime_obj):
"""
Returns time_ago for datetime instance
"""
current_date = utc.localize(datetime.datetime.today())
days_diff = (current_date - datetime_obj).days
if days_diff == 0:
return "Today"
if days_diff >= 7:
return f"{int(days_diff / 7)}w"
return f"{days_diff}d"
def add_additional_attributes_to_notifications(notifications, courses_data=None):
"""
Add attributes required for email content to notification instance
notifications: list[Notification]
course_data: Cache course info
"""
if courses_data is None:
courses_data = {}
for notification in notifications:
notification_type = notification.notification_type
course_key = notification.course_id
course_key_str = str(course_key)
if course_key_str not in courses_data.keys():
courses_data[course_key_str] = get_course_info(course_key)
course_info = courses_data[course_key_str]
notification.course_name = course_info.get('name', '')
notification.icon = get_icon_url_for_notification_type(notification_type)
notification.time_ago = get_time_ago(notification.created)
return notifications
def create_app_notifications_dict(notifications):
"""
Return a dictionary with notification app as key and
title, count and notifications as its value
"""
app_names = list({notification.app_name for notification in notifications})
app_notifications = {
name: {
'count': 0,
'title': name.title(),
'notifications': []
}
for name in app_names
}
for notification in notifications:
app_data = app_notifications[notification.app_name]
app_data['count'] += 1
app_data['notifications'].append(notification)
return app_notifications
def get_unique_course_ids(notifications):
"""
Returns unique course_ids from notifications
"""
course_ids = []
for notification in notifications:
if notification.course_id not in course_ids:
course_ids.append(notification.course_id)
return course_ids
def get_enabled_notification_types_for_cadence(preferences, cadence_type=EmailCadence.DAILY):
"""
Returns a dictionary that returns notification_types with cadence_types for course_ids
"""
if cadence_type not in [EmailCadence.DAILY, EmailCadence.WEEKLY]:
raise ValueError('Invalid cadence_type')
course_types = {}
for preference in preferences:
key = preference.course_id
value = []
config = preference.notification_preference_config
for app_data in config.values():
for notification_type, type_dict in app_data['notification_types'].items():
if type_dict['email_cadence'] == cadence_type:
value.append(notification_type)
if 'core' in value:
value.remove('core')
value.extend(app_data['core_notification_types'])
course_types[key] = value
return course_types
def filter_notification_with_email_enabled_preferences(notifications, preferences, cadence_type=EmailCadence.DAILY):
"""
Filter notifications for types with email cadence preference enabled
"""
enabled_course_prefs = get_enabled_notification_types_for_cadence(preferences, cadence_type)
filtered_notifications = []
for notification in notifications:
if notification.notification_type in enabled_course_prefs[notification.course_id]:
filtered_notifications.append(notification)
return filtered_notifications

View File

@@ -0,0 +1,32 @@
"""
Management command for sending email digest
"""
from django.core.management.base import BaseCommand
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
from openedx.core.djangoapps.notifications.email.tasks import send_digest_email_to_all_users
class Command(BaseCommand):
"""
Invoke with:
python manage.py lms send_email_digest [cadence_type]
cadence_type: Daily or Weekly
"""
help = (
"Send email digest to user."
)
def add_arguments(self, parser):
"""
Adds management commands parser arguments
"""
cadence_type_choices = [EmailCadence.DAILY, EmailCadence.WEEKLY]
parser.add_argument('cadence_type', choices=cadence_type_choices, required=True)
def handle(self, *args, **kwargs):
"""
Start task to send email digest to users
"""
send_digest_email_to_all_users.delay(args=(kwargs['cadence_type'],))

View File

@@ -1,15 +1,18 @@
{% for update in email_content %}
<style>
.notification-content > p { margin: 0; padding: 0}
</style>
{% for notification_app in email_content %}
<h3 style="font-size: 1.375rem; font-weight:700; line-height:28px; margin: 0.75rem 0 0;">
{{ update.title }}
{{ notification_app.title }}
</h3>
{% if update.help_text %}
{% if notification_app.help_text %}
<p style="margin: 0; height: 1.5rem; font-weight: 400; font-size: 14px; line-height: 24px">
<span style="float:left; color:#707070;">
{{ update.help_text }}
{{ notification_app.help_text }}
</span>
{% if update.help_text_url %}
{% if notification_app.help_text_url %}
<span style="float:right; margin-right: 0.25rem">
<a href="{{help_text_url}}" style="text-decoration: none; color: #00688D">
<a href="{{notification_app.help_text_url}}" style="text-decoration: none; color: #00688D">
View all
</a>
</span>
@@ -20,27 +23,27 @@
<p style="margin: 0;">
<table style="background-color: #FBFAF9; border-radius: 8px; padding: 0.5rem 0" width="100%">
<tbody>
{% for content in update.content %}
{% for notification in notification_app.notifications %}
<tr>
<td align="left" valign="top" style="padding: 1rem 0 1rem 1rem">
<img
src="https://edx-notifications-static.edx.org/icons/newspaper.png"
src="{{notification.icon}}"
style="max-height: 28px; max-width: 28px; margin: 0.75rem 1rem 0.75rem 0"
/>
</td>
<td width="100%" align="left" valign="top" style=" padding: 1rem 1rem 1rem 0.5rem">
<td class="notification-content" width="100%" align="left" valign="top" style=" padding: 1rem 1rem 1rem 0.5rem">
<p style="font-size: 0.875rem; font-weight:400; line-height:24px; color:#454545; margin: 0;">
{{ content.title }}
{{ notification.content | safe }}
</p>
<p style="height: 0.5rem; margin: 0"></p>
<p style="color:#707070; margin: 0">
<span style="float: left">
<span>{{ content.course_name }}</span>
<span>{{ notification.course_name }}</span>
<span style="padding: 0 0.375rem">&middot</span>
<span>{{ content.time_ago }}</span>
<span>{{ notification.time_ago }}</span>
</span>
<span style="float: right">
<a href="{{content.url}}" style="text-decoration: none; color: #00688D">
<a href="{{notification.content_url}}" style="text-decoration: none; color: #00688D">
View
</a>
</span>

View File

@@ -37,7 +37,7 @@
<table width="100%" border="0" cellpadding="0">
<tbody>
<tr>
{% for update in updates %}
{% for update in email_digest_updates %}
<td width="50%">
<p style="
{% if not forloop.last %}padding-right: 0.5rem{% endif %}
@@ -58,7 +58,7 @@
</tr>
<tr align="center">
<td style="font-weight: 600; font-size: 0.875rem; line-height: 20px; padding: 0">
{{update.type}}
{{update.title}}
</td>
</tr>
</tbody>

View File

@@ -433,3 +433,67 @@ class TestDeleteNotificationTask(ModuleStoreTestCase):
delete_notifications({'course_id': self.course_1.id})
assert not Notification.objects.filter(course_id=self.course_1.id)
assert Notification.objects.filter(course_id=self.course_2.id)
@ddt.ddt
class NotificationCreationOnChannelsTests(ModuleStoreTestCase):
"""
Tests for notification creation and channels value.
"""
def setUp(self):
"""
Create a course and users for tests.
"""
super().setUp()
self.user = UserFactory()
self.course = CourseFactory.create(
org='testorg',
number='testcourse',
run='testrun'
)
self.preference = CourseNotificationPreference.objects.create(
user_id=self.user.id,
course_id=self.course.id,
config_version=0,
)
@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
@ddt.data(
(False, False, 0),
(False, True, 1),
(True, False, 1),
(True, True, 1),
)
@ddt.unpack
def test_notification_is_created_when_any_channel_is_enabled(self, web_value, email_value, generated_count):
"""
Tests if notification is created if any preference is enabled
"""
app_name = 'discussion'
notification_type = 'new_discussion_post'
app_prefs = self.preference.notification_preference_config[app_name]
app_prefs['notification_types'][notification_type]['web'] = web_value
app_prefs['notification_types'][notification_type]['email'] = email_value
kwargs = {
'user_ids': [self.user.id],
'course_key': str(self.course.id),
'app_name': app_name,
'notification_type': notification_type,
'content_url': 'https://example.com/',
'context': {
'post_title': 'Post title',
'username': 'user name',
},
}
self.preference.save()
with patch('openedx.core.djangoapps.notifications.tasks.notification_generated_event') as event_mock:
send_notifications(**kwargs)
notifications = Notification.objects.all()
assert len(notifications) == generated_count
if notifications:
notification = Notification.objects.all()[0]
assert notification.web == web_value
assert notification.email == email_value