diff --git a/common/templates/student/edx_ace/calendarsync/email/body.html b/common/templates/student/edx_ace/calendarsync/email/body.html
new file mode 100644
index 0000000000..9ee009defc
--- /dev/null
+++ b/common/templates/student/edx_ace/calendarsync/email/body.html
@@ -0,0 +1,22 @@
+{% extends 'ace_common/edx_ace/common/base_body.html' %}
+
+{% load django_markup %}
+{% load i18n %}
+{% load static %}
+{% block content %}
+
+
+
+
+ {% filter force_escape %}{{ calendar_sync_headline }}{% endfilter %}
+
+
+ {% filter force_escape %}
+ {{ calendar_sync_body }}
+ {% endfilter %}
+
+
+ |
+
+
+{% endblock %}
diff --git a/common/templates/student/edx_ace/calendarsync/email/body.txt b/common/templates/student/edx_ace/calendarsync/email/body.txt
new file mode 100644
index 0000000000..d2e6f46bb1
--- /dev/null
+++ b/common/templates/student/edx_ace/calendarsync/email/body.txt
@@ -0,0 +1,2 @@
+{% load i18n %}{% autoescape off %}
+{{ calendar_sync_body }}
diff --git a/common/templates/student/edx_ace/calendarsync/email/from_name.txt b/common/templates/student/edx_ace/calendarsync/email/from_name.txt
new file mode 100644
index 0000000000..dcbc23c004
--- /dev/null
+++ b/common/templates/student/edx_ace/calendarsync/email/from_name.txt
@@ -0,0 +1 @@
+{{ platform_name }}
diff --git a/common/templates/student/edx_ace/calendarsync/email/head.html b/common/templates/student/edx_ace/calendarsync/email/head.html
new file mode 100644
index 0000000000..366ada7ad9
--- /dev/null
+++ b/common/templates/student/edx_ace/calendarsync/email/head.html
@@ -0,0 +1 @@
+{% extends 'ace_common/edx_ace/common/base_head.html' %}
diff --git a/common/templates/student/edx_ace/calendarsync/email/subject.txt b/common/templates/student/edx_ace/calendarsync/email/subject.txt
new file mode 100644
index 0000000000..7ef3d19b84
--- /dev/null
+++ b/common/templates/student/edx_ace/calendarsync/email/subject.txt
@@ -0,0 +1,4 @@
+{% load i18n %}
+{% autoescape off %}
+{{ calendar_sync_subject }}
+{% endautoescape %}
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 1df3a4345b..93232f7bff 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -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
diff --git a/openedx/features/calendar_sync/message_types.py b/openedx/features/calendar_sync/message_types.py
new file mode 100644
index 0000000000..72af6e8009
--- /dev/null
+++ b/openedx/features/calendar_sync/message_types.py
@@ -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
diff --git a/openedx/features/calendar_sync/tasks.py b/openedx/features/calendar_sync/tasks.py
new file mode 100644
index 0000000000..834d4e5c01
--- /dev/null
+++ b/openedx/features/calendar_sync/tasks.py
@@ -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
diff --git a/openedx/features/calendar_sync/tests/test_tasks.py b/openedx/features/calendar_sync/tests/test_tasks.py
new file mode 100644
index 0000000000..787e68f548
--- /dev/null
+++ b/openedx/features/calendar_sync/tests/test_tasks.py
@@ -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)
diff --git a/openedx/features/calendar_sync/tests/test_views.py b/openedx/features/calendar_sync/tests/test_views.py
index d9892c050d..85d730105b 100644
--- a/openedx/features/calendar_sync/tests/test_views.py
+++ b/openedx/features/calendar_sync/tests/test_views.py
@@ -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)
diff --git a/openedx/features/calendar_sync/views/management.py b/openedx/features/calendar_sync/views/management.py
new file mode 100644
index 0000000000..9e4527c796
--- /dev/null
+++ b/openedx/features/calendar_sync/views/management.py
@@ -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))
diff --git a/openedx/features/course_experience/views/course_dates.py b/openedx/features/course_experience/views/course_dates.py
index 91c2bbfd21..6ffa66bc99 100644
--- a/openedx/features/course_experience/views/course_dates.py
+++ b/openedx/features/course_experience/views/course_dates.py
@@ -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):