feat: added immediate email notifications (#36749)
This commit is contained in:
committed by
GitHub
parent
bfdba3c914
commit
4e55d72e75
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Celery tasks for sending email notifications
|
||||
"""
|
||||
from bs4 import BeautifulSoup
|
||||
from celery import shared_task
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.contrib.auth import get_user_model
|
||||
@@ -20,8 +21,11 @@ from .utils import (
|
||||
add_headers_to_email_message,
|
||||
create_app_notifications_dict,
|
||||
create_email_digest_context,
|
||||
create_email_template_context,
|
||||
filter_notification_with_email_enabled_preferences,
|
||||
get_course_info,
|
||||
get_start_end_date,
|
||||
get_text_for_notification_type,
|
||||
get_unique_course_ids,
|
||||
is_email_notification_flag_enabled
|
||||
)
|
||||
@@ -105,6 +109,7 @@ def send_digest_email_to_user(user, cadence_type, start_date, end_date, course_l
|
||||
app_label="notifications", name="email_digest"
|
||||
).personalize(recipient, course_language, message_context)
|
||||
message = add_headers_to_email_message(message, message_context)
|
||||
message.options['skip_disable_user_policy'] = True
|
||||
ace.send(message)
|
||||
send_user_email_digest_sent_event(user, cadence_type, notifications, message_context)
|
||||
logger.info(f'<Email Cadence> Email sent to {user.username} ==Temp Log==')
|
||||
@@ -123,3 +128,46 @@ def send_digest_email_to_all_users(cadence_type):
|
||||
logger.info(f'<Email Cadence> Email Cadence Audience {len(users)}')
|
||||
for user in users:
|
||||
send_digest_email_to_user(user, cadence_type, start_date, end_date, courses_data=courses_data)
|
||||
|
||||
|
||||
def send_immediate_cadence_email(email_notification_mapping, course_key):
|
||||
"""
|
||||
Send immediate cadence email to users
|
||||
Parameters:
|
||||
email_notification_mapping: Dictionary of user_id and Notification object
|
||||
course_key: Course key for which the email is sent
|
||||
"""
|
||||
if not email_notification_mapping:
|
||||
return
|
||||
user_list = email_notification_mapping.keys()
|
||||
users = User.objects.filter(id__in=user_list)
|
||||
course_name = get_course_info(course_key).get("name", course_key)
|
||||
for user in users.iterator(chunk_size=100):
|
||||
if not user.has_usable_password():
|
||||
logger.info(f'<Immediate Email> User is disabled {user.username}')
|
||||
continue
|
||||
if not is_email_notification_flag_enabled(user):
|
||||
logger.info(f'<Immediate Email> Flag disabled for {user.username}')
|
||||
continue
|
||||
notification = email_notification_mapping.get(user.id, None)
|
||||
if not notification:
|
||||
logger.info(f'<Immediate Email> No notification for {user.username}')
|
||||
continue
|
||||
soup = BeautifulSoup(notification.content, "html.parser")
|
||||
title = "New Course Update" if notification.notification_type == "course_updates" else soup.get_text()
|
||||
message_context = create_email_template_context(user.username)
|
||||
message_context.update({
|
||||
"course_id": course_key,
|
||||
"course_name": course_name,
|
||||
"content_url": notification.content_url,
|
||||
"content_title": title,
|
||||
"footer_email_reason": "You are receiving this email because you are enrolled in the edX course "
|
||||
f"{course_name}",
|
||||
"content": notification.content_context.get("email_content", notification.content),
|
||||
"view_text": get_text_for_notification_type(notification.notification_type),
|
||||
})
|
||||
message = EmailNotificationMessageType(
|
||||
app_label="notifications", name="immediate_email"
|
||||
).personalize(Recipient(user.id, user.email), 'en', message_context)
|
||||
message = add_headers_to_email_message(message, message_context)
|
||||
ace.send(message)
|
||||
|
||||
@@ -9,7 +9,8 @@ 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.config.waffle import 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 (
|
||||
get_audience_for_cadence_email,
|
||||
@@ -258,3 +259,53 @@ class TestPreferences(ModuleStoreTestCase):
|
||||
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
|
||||
"""
|
||||
|
||||
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_sent_when_cadence_is_immediate(self, mock_func):
|
||||
"""
|
||||
Tests email is sent when cadence is immediate
|
||||
"""
|
||||
preference = CourseNotificationPreference.objects.create(user=self.user, course_id=self.course.id)
|
||||
app_prefs = preference.notification_preference_config['discussion']['notification_types']
|
||||
app_prefs['new_discussion_post']['email'] = True
|
||||
app_prefs['new_discussion_post']['email_cadence'] = EmailCadence.IMMEDIATELY
|
||||
preference.save()
|
||||
context = {
|
||||
'username': 'User',
|
||||
'post_title': 'title'
|
||||
}
|
||||
with override_waffle_flag(ENABLE_NOTIFICATIONS, True):
|
||||
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
|
||||
send_notifications([self.user.id], str(self.course.id), 'discussion',
|
||||
'new_discussion_post', context, 'http://test.url')
|
||||
assert mock_func.call_count == 1
|
||||
|
||||
@patch('edx_ace.ace.send')
|
||||
def test_email_not_sent_when_cadence_is_not_immediate(self, mock_func):
|
||||
"""
|
||||
Tests email is not sent when cadence is not immediate
|
||||
"""
|
||||
CourseNotificationPreference.objects.create(user=self.user, course_id=self.course.id)
|
||||
context = {
|
||||
'replier_name': 'User',
|
||||
'post_title': 'title'
|
||||
}
|
||||
with override_waffle_flag(ENABLE_NOTIFICATIONS, True):
|
||||
with override_waffle_flag(ENABLE_EMAIL_NOTIFICATIONS, True):
|
||||
send_notifications([self.user.id], str(self.course.id), 'discussion',
|
||||
'new_response', context, 'http://test.url')
|
||||
assert mock_func.call_count == 0
|
||||
|
||||
@@ -431,3 +431,18 @@ def is_notification_type_channel_editable(app_name, notification_type, channel):
|
||||
if notification_type == 'core':
|
||||
return channel not in COURSE_NOTIFICATION_APPS[app_name]['non_editable']
|
||||
return channel not in COURSE_NOTIFICATION_TYPES[notification_type]['non_editable']
|
||||
|
||||
|
||||
def get_text_for_notification_type(notification_type):
|
||||
"""
|
||||
Returns text for notification type
|
||||
"""
|
||||
app_name = COURSE_NOTIFICATION_APPS.get(notification_type, {}).get('notification_app')
|
||||
if not app_name:
|
||||
return ""
|
||||
mapping = {
|
||||
'discussion': 'post',
|
||||
'updates': 'update',
|
||||
'grading': 'assessment',
|
||||
}
|
||||
return mapping.get(app_name, "")
|
||||
|
||||
@@ -15,6 +15,7 @@ from openedx.core.djangoapps.notifications.base_notification import (
|
||||
get_notification_content
|
||||
)
|
||||
from openedx.core.djangoapps.notifications.email import ONE_CLICK_EMAIL_UNSUB_KEY
|
||||
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
|
||||
from openedx.core.djangoapps.user_api.models import UserPreference
|
||||
|
||||
User = get_user_model()
|
||||
@@ -303,3 +304,19 @@ class CourseNotificationPreference(TimeStampedModel):
|
||||
}
|
||||
"""
|
||||
return self.get_notification_types(app_name).get('core', {})
|
||||
|
||||
def is_email_enabled_for_notification_type(self, app_name, notification_type) -> bool:
|
||||
"""
|
||||
Returns True if the email is enabled for the given app name and notification type.
|
||||
"""
|
||||
if self.is_core(app_name, notification_type):
|
||||
return self.get_core_config(app_name).get('email', False)
|
||||
return self.get_notification_type_config(app_name, notification_type).get('email', False)
|
||||
|
||||
def get_email_cadence_for_notification_type(self, app_name, notification_type) -> str:
|
||||
"""
|
||||
Returns the email cadence for the given app name and notification type.
|
||||
"""
|
||||
if self.is_core(app_name, notification_type):
|
||||
return self.get_core_config(app_name).get('email_cadence', EmailCadence.NEVER)
|
||||
return self.get_notification_type_config(app_name, notification_type).get('email_cadence', EmailCadence.NEVER)
|
||||
|
||||
@@ -20,6 +20,8 @@ from openedx.core.djangoapps.notifications.base_notification import (
|
||||
get_notification_content
|
||||
)
|
||||
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATION_GROUPING, ENABLE_NOTIFICATIONS
|
||||
from openedx.core.djangoapps.notifications.email.tasks import send_immediate_cadence_email
|
||||
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
|
||||
from openedx.core.djangoapps.notifications.events import notification_generated_event
|
||||
from openedx.core.djangoapps.notifications.grouping_notifications import (
|
||||
get_user_existing_notifications,
|
||||
@@ -114,6 +116,7 @@ def delete_expired_notifications():
|
||||
logger.info(f'{total_deleted} Notifications deleted in {time_elapsed} seconds.')
|
||||
|
||||
|
||||
# pylint: disable=too-many-statements
|
||||
@shared_task
|
||||
@set_code_owner_attribute
|
||||
def send_notifications(user_ids, course_key: str, app_name, notification_type, context, content_url):
|
||||
@@ -138,6 +141,7 @@ def send_notifications(user_ids, course_key: str, app_name, notification_type, c
|
||||
sender_id = context.pop('sender_id', None)
|
||||
default_web_config = get_default_values_of_preference(app_name, notification_type).get('web', False)
|
||||
generated_notification_audience = []
|
||||
email_notification_mapping = {}
|
||||
|
||||
if group_by_id and not grouping_enabled:
|
||||
logger.info(
|
||||
@@ -179,6 +183,8 @@ def send_notifications(user_ids, course_key: str, app_name, notification_type, c
|
||||
preference.get_app_config(app_name).get('enabled', False)
|
||||
):
|
||||
notification_preferences = preference.get_channels_for_notification_type(app_name, notification_type)
|
||||
email_enabled = 'email' in preference.get_channels_for_notification_type(app_name, notification_type)
|
||||
email_cadence = preference.get_email_cadence_for_notification_type(app_name, notification_type)
|
||||
new_notification = Notification(
|
||||
user_id=user_id,
|
||||
app_name=app_name,
|
||||
@@ -187,9 +193,12 @@ def send_notifications(user_ids, course_key: str, app_name, notification_type, c
|
||||
content_url=content_url,
|
||||
course_id=course_key,
|
||||
web='web' in notification_preferences,
|
||||
email='email' in notification_preferences,
|
||||
email=email_enabled,
|
||||
group_by_id=group_by_id,
|
||||
)
|
||||
if email_enabled and (email_cadence == EmailCadence.IMMEDIATELY):
|
||||
email_notification_mapping[user_id] = new_notification
|
||||
|
||||
if grouping_enabled and existing_notifications.get(user_id, None):
|
||||
group_user_notifications(new_notification, existing_notifications[user_id])
|
||||
if not notifications_generated:
|
||||
@@ -205,6 +214,9 @@ def send_notifications(user_ids, course_key: str, app_name, notification_type, c
|
||||
notifications_generated = True
|
||||
notification_content = notification_objects[0].content
|
||||
|
||||
if email_notification_mapping:
|
||||
send_immediate_cadence_email(email_notification_mapping, course_key)
|
||||
|
||||
if notifications_generated:
|
||||
notification_generated_event(
|
||||
generated_notification_audience, app_name, notification_type, course_key, content_url,
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<p style="margin: 1.5rem 0 0 0;">
|
||||
You are receiving this email because you have subscribed to email digest
|
||||
{{ footer_email_reason|default:"You are receiving this email because you have subscribed to email digest" }}
|
||||
</p>
|
||||
<p style="margin: 0.625rem 0">
|
||||
<a href="{{notification_settings_url}}" rel="noopener noreferrer" target="_blank" style="color: black">
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<div style="margin:0; padding:0; min-width: 100%; background-color:#C9C9C9; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-smooth: never;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color:#FFFFFF; line-height:1.5; max-width:600px; font-family:Inter, Arial, Verdana, sans-serif">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 1rem;">
|
||||
{% include 'notifications/immediate_email_content.html' %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 1rem 2.5rem; background-color: #F2F0EF">
|
||||
{% include 'notifications/digest_footer.html' %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
{{ content_title | safe }}
|
||||
@@ -0,0 +1 @@
|
||||
{{ platform_name }}
|
||||
@@ -0,0 +1,3 @@
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title lang="en">{{ platform_name }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
@@ -0,0 +1 @@
|
||||
{{ content_title | safe }}
|
||||
@@ -0,0 +1,28 @@
|
||||
<blockquote style="margin: 0; height: 64px; padding: 24px 24px 8px">
|
||||
<span style="float: left">
|
||||
<img src="{{ logo_url }}" style="width: auto;" height="40" alt="Logo"/>
|
||||
</span>
|
||||
<span style="float: right">
|
||||
<a href="{{ unsubscribe_url }}" style="color: #00262B; text-decoration: none;">Unsubscribe</a>
|
||||
</span>
|
||||
</blockquote>
|
||||
<blockquote style="margin: 0; padding: 24px;">
|
||||
<blockquote style="margin: 0;">
|
||||
<p style="margin: 0 0 10px; font-size: 14px; font-weight: 500; line-height: 24px; color: #707070">
|
||||
{{ course_name }}
|
||||
</p>
|
||||
<h3 style="margin: 0; font-size: 22px; font-weight: 700; line-height: 28px; color: #454545">
|
||||
{{ content_title | safe }}
|
||||
</h3>
|
||||
</blockquote>
|
||||
<blockquote style="margin: 24px 0">
|
||||
<blockquote style="margin: 0; padding-left: 24px; border-left: 2px solid #E1DDDB"> {{ content | safe }}</blockquote>
|
||||
</blockquote>
|
||||
<blockquote style="margin: 0; padding: 10px 0">
|
||||
<p style="margin: 0; padding: 10px 16px; background-color: #D63328; width: fit-content;">
|
||||
<a href="{{ content_url }}" style="font-weight: 500; font-size: 18px; line-height: 24px; color: #FFFFFF; text-decoration: none">
|
||||
View {{ view_text|default:""}}
|
||||
</a>
|
||||
</p>
|
||||
</blockquote>
|
||||
</blockquote>
|
||||
Reference in New Issue
Block a user