AA-38 calendar sync email template
- create templates and methods to build/send calendar sync emails.
This commit is contained in:
@@ -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 %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}{% autoescape off %}
|
||||
{{ calendar_sync_body }}
|
||||
@@ -0,0 +1 @@
|
||||
{{ platform_name }}
|
||||
@@ -0,0 +1 @@
|
||||
{% extends 'ace_common/edx_ace/common/base_head.html' %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{{ calendar_sync_subject }}
|
||||
{% endautoescape %}
|
||||
@@ -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
|
||||
|
||||
|
||||
13
openedx/features/calendar_sync/message_types.py
Normal file
13
openedx/features/calendar_sync/message_types.py
Normal 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
|
||||
71
openedx/features/calendar_sync/tasks.py
Normal file
71
openedx/features/calendar_sync/tasks.py
Normal 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
|
||||
83
openedx/features/calendar_sync/tests/test_tasks.py
Normal file
83
openedx/features/calendar_sync/tests/test_tasks.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
64
openedx/features/calendar_sync/views/management.py
Normal file
64
openedx/features/calendar_sync/views/management.py
Normal 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))
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user