AA-38 calendar sync email template

- create templates and methods to build/send calendar sync emails.
This commit is contained in:
Nicholas D'Alfonso
2020-03-09 10:16:08 -04:00
parent b1f61d929d
commit 52309d5541
12 changed files with 302 additions and 1 deletions

View File

@@ -0,0 +1,22 @@
{% extends 'ace_common/edx_ace/common/base_body.html' %}
{% load django_markup %}
{% load i18n %}
{% load static %}
{% block content %}
<table width="100%" align="left" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<h1>
{% filter force_escape %}{{ calendar_sync_headline }}{% endfilter %}
</h1>
<p style="color: rgba(0,0,0,.75);">
{% filter force_escape %}
{{ calendar_sync_body }}
{% endfilter %}
<br />
</p>
</td>
</tr>
</table>
{% endblock %}

View File

@@ -0,0 +1,2 @@
{% load i18n %}{% autoescape off %}
{{ calendar_sync_body }}

View File

@@ -0,0 +1 @@
{{ platform_name }}

View File

@@ -0,0 +1 @@
{% extends 'ace_common/edx_ace/common/base_head.html' %}

View File

@@ -0,0 +1,4 @@
{% load i18n %}
{% autoescape off %}
{{ calendar_sync_subject }}
{% endautoescape %}

View File

@@ -515,6 +515,7 @@ PASSWORD_RESET_EMAIL_RATE_LIMIT = {
'no_of_emails': 1,
'per_seconds': 60
}
RETRY_CALENDAR_SYNC_EMAIL_MAX_ATTEMPTS = 5
# Deadline message configurations
COURSE_MESSAGE_ALERT_DURATION_IN_DAYS = 14

View File

@@ -0,0 +1,13 @@
"""
ACE message types for the calendar_sync module.
"""
from openedx.core.djangoapps.ace_common.message import BaseMessageType
class CalendarSync(BaseMessageType):
def __init__(self, *args, **kwargs):
super(CalendarSync, self).__init__(*args, **kwargs)
self.options['transactional'] = True

View File

@@ -0,0 +1,71 @@
"""
This file contains celery tasks for sending email
"""
import logging
from celery.exceptions import MaxRetriesExceededError
from celery.task import task
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from edx_ace import ace
from edx_ace.errors import RecoverableChannelDeliveryError
from edx_ace.message import Message
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.lib.celery.task_utils import emulate_http_request
log = logging.getLogger('edx.celery.task')
@task(bind=True)
def send_calendar_sync_email(self, msg_string, from_address=None):
"""
Sending calendar sync email to the user.
"""
msg = Message.from_string(msg_string)
max_retries = settings.RETRY_CALENDAR_SYNC_EMAIL_MAX_ATTEMPTS
retries = self.request.retries
if from_address is None:
from_address = configuration_helpers.get_value('ACTIVATION_EMAIL_FROM_ADDRESS') or (
configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
)
msg.options['from_address'] = from_address
dest_addr = msg.recipient.email_address
site = Site.objects.get_current()
user = User.objects.get(username=msg.recipient.username)
try:
with emulate_http_request(site=site, user=user):
ace.send(msg)
# Log that the Activation Email has been sent to user without an exception
log.info("Calendar Sync Email has been sent to User {user_email}".format(
user_email=dest_addr
))
except RecoverableChannelDeliveryError:
log.info('Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format(
dest_addr=dest_addr,
attempt=retries,
max_attempts=max_retries
))
try:
self.retry(countdown=settings.RETRY_ACTIVATION_EMAIL_TIMEOUT, max_retries=max_retries)
except MaxRetriesExceededError:
log.error(
'Unable to send calendar sync email to user from "%s" to "%s"',
from_address,
dest_addr,
exc_info=True
)
except Exception:
log.exception(
'Unable to send calendar sync email to user from "%s" to "%s"',
from_address,
dest_addr,
)
raise Exception

View File

@@ -0,0 +1,83 @@
"""
Tests for the Sending activation email celery tasks
"""
import mock
from django.conf import settings
from django.test import TestCase
from six.moves import range
from edx_ace.errors import ChannelError, RecoverableChannelDeliveryError
from lms.djangoapps.courseware.tests.factories import UserFactory
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.features.calendar_sync.tasks import send_calendar_sync_email
from openedx.features.calendar_sync.views.management import compose_calendar_sync_email
class SendCalendarSyncEmailTestCase(TestCase):
"""
Test for send activation email to user
"""
def setUp(self):
""" Setup components used by each test."""
super(SendCalendarSyncEmailTestCase, self).setUp()
self.user = UserFactory()
self.course_overview = CourseOverviewFactory()
self.msg = compose_calendar_sync_email(self.user, self.course_overview)
@mock.patch('time.sleep', mock.Mock(return_value=None))
@mock.patch('openedx.features.calendar_sync.tasks.log')
@mock.patch(
'openedx.features.calendar_sync.tasks.ace.send',
mock.Mock(side_effect=RecoverableChannelDeliveryError(None, None))
)
def test_RetrySendUntilFail(self, mock_log):
"""
Tests retries when the activation email doesn't send
"""
from_address = 'task_testing@example.com'
email_max_attempts = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS
send_calendar_sync_email.delay(str(self.msg), from_address=from_address)
# Asserts sending email retry logging.
for attempt in range(email_max_attempts):
mock_log.info.assert_any_call(
'Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format(
dest_addr=self.user.email,
attempt=attempt,
max_attempts=email_max_attempts
))
self.assertEqual(mock_log.info.call_count, 6)
# Asserts that the error was logged on crossing max retry attempts.
mock_log.error.assert_called_with(
'Unable to send calendar sync email to user from "%s" to "%s"',
from_address,
self.user.email,
exc_info=True
)
self.assertEqual(mock_log.error.call_count, 1)
@mock.patch('openedx.features.calendar_sync.tasks.log')
@mock.patch('openedx.features.calendar_sync.tasks.ace.send', mock.Mock(side_effect=ChannelError))
def test_UnrecoverableSendError(self, mock_log):
"""
Tests that a major failure of the send is logged
"""
from_address = 'task_testing@example.com'
send_calendar_sync_email.delay(str(self.msg), from_address=from_address)
# Asserts that the error was logged
mock_log.exception.assert_called_with(
'Unable to send calendar sync email to user from "%s" to "%s"',
from_address,
self.user.email
)
# Assert that nothing else was logged
self.assertEqual(mock_log.info.call_count, 0)
self.assertEqual(mock_log.error.call_count, 0)
self.assertEqual(mock_log.exception.call_count, 1)

View File

@@ -8,7 +8,10 @@ import ddt
from django.test import TestCase
from django.urls import reverse
from lms.djangoapps.courseware.tests.factories import UserFactory
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.features.calendar_sync.api import SUBSCRIBE, UNSUBSCRIBE
from openedx.features.calendar_sync.views.management import compose_calendar_sync_email
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -49,3 +52,39 @@ class TestCalendarSyncView(SharedModuleStoreTestCase, TestCase):
response = self.client.post(self.calendar_sync_url, data)
self.assertEqual(response.status_code, expected_status_code)
self.assertIn(contained_text, str(response.content))
@ddt.ddt
class CalendarSyncEmailTestCase(TestCase):
"""
Test for send activation email to user
"""
@ddt.data(False, True)
def test_compose_calendar_sync_email(self, is_update):
"""
Tests that attributes of the message are being filled correctly in compose_activation_email
"""
user = UserFactory()
course_overview = CourseOverviewFactory()
course_name = course_overview.display_name
if is_update:
calendar_sync_subject = 'Updates for Your {course} Schedule'.format(course=course_name)
calendar_sync_headline = 'Update Your Calendar'
calendar_sync_body = ('Your assignment due dates for {course} were recently adjusted. Update your calendar'
'with your new schedule to ensure that you stay on track!').format(course=course_name)
else:
calendar_sync_subject = 'Stay on Track'
calendar_sync_headline = 'Mark Your Calendar'
calendar_sync_body = (
'Sticking to a schedule is the best way to ensure that you successfully complete your '
'self-paced course. This schedule of assignment due dates for {course} will help you '
'stay on track!'.format(course=course_name))
msg = compose_calendar_sync_email(user, course_overview, is_update)
self.assertEqual(msg.context['calendar_sync_subject'], calendar_sync_subject)
self.assertEqual(msg.context['calendar_sync_headline'], calendar_sync_headline)
self.assertEqual(msg.context['calendar_sync_body'], calendar_sync_body)
self.assertEqual(msg.recipient.username, user.username)
self.assertEqual(msg.recipient.email_address, user.email)

View File

@@ -0,0 +1,64 @@
"""
Calendar Sync Email Management
"""
from django.utils.translation import ugettext_lazy as _
from edx_ace.recipient import Recipient
from student.models import CourseEnrollment, CourseOverview
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
from openedx.features.calendar_sync.message_types import CalendarSync
from openedx.features.calendar_sync.tasks import send_calendar_sync_email
def compose_calendar_sync_email(user, course: CourseOverview, is_update=False):
"""
Construct all the required params for the calendar
sync email through celery task
"""
course_name = course.display_name
if is_update:
calendar_sync_subject = _('Updates for Your {course} Schedule').format(course=course_name)
calendar_sync_headline = _('Update Your Calendar')
calendar_sync_body = _('Your assignment due dates for {course} were recently adjusted. Update your calendar'
'with your new schedule to ensure that you stay on track!').format(course=course_name)
else:
calendar_sync_subject = _('Stay on Track')
calendar_sync_headline = _('Mark Your Calendar')
calendar_sync_body = _('Sticking to a schedule is the best way to ensure that you successfully complete your '
'self-paced course. This schedule of assignment due dates for {course} will help you '
'stay on track!').format(course=course_name)
email_context = {
'calendar_sync_subject': calendar_sync_subject,
'calendar_sync_headline': calendar_sync_headline,
'calendar_sync_body': calendar_sync_body,
}
msg = CalendarSync().personalize(
recipient=Recipient(user.username, user.email),
language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
user_context=email_context,
)
return msg
def compose_and_send_calendar_sync_email(user, course: CourseOverview, is_update=False):
"""
Construct all the required params and send the activation email
through celery task
Arguments:
user: current logged-in user
course: course overview object
is_update: if this should be an 'update' email
"""
if not CourseEnrollment.objects.filter(user=user, course=course).exists():
return
msg = compose_calendar_sync_email(user, course, is_update)
send_calendar_sync_email.delay(str(msg))

View File

@@ -52,7 +52,7 @@ class CourseDatesFragmentMobileView(CourseDatesFragmentView):
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
raise Http404
print('****************', CourseDatesFragmentMobileView.__dict__)
return super(CourseDatesFragmentMobileView, self).get(request, *args, **kwargs)
def css_dependencies(self):