diff --git a/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py b/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
index 7ab9d5b405..d9cc13ac06 100644
--- a/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
+++ b/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
@@ -22,6 +22,7 @@ from openedx.core.djangoapps.schedules import resolvers, tasks
from openedx.core.djangoapps.schedules.resolvers import _get_datetime_beginning_of_day
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory, ScheduleFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
+from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, FilteredQueryCountMixin
from student.models import CourseEnrollment
@@ -38,6 +39,11 @@ ORG_DEADLINE_QUERY = 1 # courseware_orgdynamicupgradedeadlineconfiguration
COURSE_DEADLINE_QUERY = 1 # courseware_coursedynamicupgradedeadlineconfiguration
COMMERCE_CONFIG_QUERY = 1 # commerce_commerceconfiguration
+USER_QUERY = 1
+THEME_PREVIEW_QUERY = 1
+THEME_QUERY = 1
+SCHEDULE_CONFIG_QUERY = 1
+
NUM_QUERIES_SITE_SCHEDULES = (
SITE_QUERY +
SITE_CONFIG_QUERY +
@@ -52,6 +58,14 @@ NUM_QUERIES_FIRST_MATCH = (
+ COMMERCE_CONFIG_QUERY
)
+NUM_QUERIES_PER_MESSAGE_DELIVERY = (
+ SITE_QUERY +
+ SCHEDULE_CONFIG_QUERY +
+ USER_QUERY +
+ THEME_PREVIEW_QUERY +
+ THEME_QUERY
+)
+
LOG = logging.getLogger(__name__)
@@ -219,10 +233,12 @@ class ScheduleSendEmailTestBase(FilteredQueryCountMixin, CacheIsolationTestCase)
@patch.object(tasks, 'ace')
@patch.object(tasks, 'Message')
def test_deliver_config(self, is_enabled, mock_message, mock_ace):
+ user = UserFactory.create()
schedule_config_kwargs = {
'site': self.site_config.site,
self.deliver_config: is_enabled,
}
+ mock_message.from_string.return_value.recipient.username = user.username
ScheduleConfigFactory.create(**schedule_config_kwargs)
mock_msg = Mock()
@@ -383,7 +399,7 @@ class ScheduleSendEmailTestBase(FilteredQueryCountMixin, CacheIsolationTestCase)
num_expected_messages = 1 if self.consolidates_emails_for_learner else message_count
self.assertEqual(len(sent_messages), num_expected_messages)
- with self.assertNumQueries(2):
+ with self.assertNumQueries(NUM_QUERIES_PER_MESSAGE_DELIVERY):
self.deliver_task(*sent_messages[0])
self.assertEqual(mock_channel.deliver.call_count, 1)
@@ -393,6 +409,8 @@ class ScheduleSendEmailTestBase(FilteredQueryCountMixin, CacheIsolationTestCase)
self.assertNotIn("{{", template)
self.assertNotIn("}}", template)
+ return mock_channel.deliver.mock_calls
+
def _check_if_email_sent_for_experience(self, test_config):
current_day, offset, target_day, _ = self._get_dates(offset=test_config.offset)
@@ -412,3 +430,10 @@ class ScheduleSendEmailTestBase(FilteredQueryCountMixin, CacheIsolationTestCase)
))
self.assertEqual(mock_ace.send.called, test_config.email_sent)
+
+ @with_comprehensive_theme('red-theme')
+ def test_templates_with_theme(self):
+ calls_to_deliver = self._assert_template_for_offset(self.expected_offsets[0], 1)
+
+ _name, (_msg, email), _kwargs = calls_to_deliver[0]
+ self.assertIn('TEST RED THEME MARKER', email.body_html)
diff --git a/openedx/core/djangoapps/schedules/tasks.py b/openedx/core/djangoapps/schedules/tasks.py
index 9a8ba16a40..88b866905b 100644
--- a/openedx/core/djangoapps/schedules/tasks.py
+++ b/openedx/core/djangoapps/schedules/tasks.py
@@ -2,7 +2,9 @@ import datetime
import logging
from celery.task import task, Task
+from crum import CurrentRequestUserMiddleware
from django.conf import settings
+from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.exceptions import ValidationError
@@ -17,7 +19,8 @@ from openedx.core.djangoapps.monitoring_utils import set_custom_metric
from openedx.core.djangoapps.schedules import message_types
from openedx.core.djangoapps.schedules.models import Schedule, ScheduleConfig
from openedx.core.djangoapps.schedules import resolvers
-
+from openedx.core.djangoapps.theming.middleware import CurrentSiteThemeMiddleware
+from openedx.core.lib.celery.task_utils import emulate_http_request
LOG = logging.getLogger(__name__)
@@ -177,15 +180,22 @@ class ScheduleCourseUpdate(ScheduleMessageBaseTask):
def _schedule_send(msg_str, site_id, delivery_config_var, log_prefix):
- if _is_delivery_enabled(site_id, delivery_config_var, log_prefix):
- msg = Message.from_string(msg_str)
- _annonate_send_task_for_monitoring(msg)
- LOG.debug('%s: Sending message = %s', log_prefix, msg_str)
- ace.send(msg)
-
-
-def _is_delivery_enabled(site_id, delivery_config_var, log_prefix):
site = Site.objects.get(pk=site_id)
+ if _is_delivery_enabled(site, delivery_config_var, log_prefix):
+ msg = Message.from_string(msg_str)
+
+ user = User.objects.get(username=msg.recipient.username)
+ middleware_classes = [
+ CurrentRequestUserMiddleware,
+ CurrentSiteThemeMiddleware,
+ ]
+ with emulate_http_request(site=site, user=user, middleware_classes=middleware_classes):
+ _annonate_send_task_for_monitoring(msg)
+ LOG.debug('%s: Sending message = %s', log_prefix, msg_str)
+ ace.send(msg)
+
+
+def _is_delivery_enabled(site, delivery_config_var, log_prefix):
if getattr(ScheduleConfig.current(site), delivery_config_var, False):
return True
else:
diff --git a/openedx/core/lib/celery/task_utils.py b/openedx/core/lib/celery/task_utils.py
new file mode 100644
index 0000000000..770a88e167
--- /dev/null
+++ b/openedx/core/lib/celery/task_utils.py
@@ -0,0 +1,49 @@
+from contextlib import contextmanager
+
+from django.http import HttpRequest, HttpResponse
+
+
+@contextmanager
+def emulate_http_request(site=None, user=None, middleware_classes=None):
+ """
+ Generate a fake HTTP request and run selected middleware on it.
+
+ This is used to enable features that assume they are running as part of an HTTP request handler. Many of these
+ features retrieve the "current" request from a thread local managed by crum. They will make a call like
+ crum.get_current_request() or something similar.
+
+ Since some tasks are kicked off by a management commands (which does not have an HTTP request) and then executed
+ in celery workers there is no "current HTTP request". Instead we just populate the global state that is most
+ commonly used on request objects.
+
+ Arguments:
+ site (Site): The site that this request should emulate. Defaults to None.
+ user (User): The user that initiated this fake request. Defaults to None
+ middleware_classes (list): A list of classes that implement Django's middleware interface.
+ """
+ request = HttpRequest()
+ request.user = user
+ request.site = site
+
+ middleware_classes = middleware_classes or []
+ middleware_instances = [klass() for klass in middleware_classes]
+ response = HttpResponse()
+
+ for middleware in middleware_instances:
+ _run_method_if_implemented(middleware, 'process_request', request)
+
+ try:
+ yield
+ except Exception as exc:
+ for middleware in reversed(middleware_instances):
+ _run_method_if_implemented(middleware, 'process_exception', request, exc)
+ else:
+ for middleware in reversed(middleware_instances):
+ _run_method_if_implemented(middleware, 'process_response', request, response)
+
+
+def _run_method_if_implemented(instance, method_name, *args, **kwargs):
+ if hasattr(instance, method_name):
+ return getattr(instance, method_name)(*args, **kwargs)
+ else:
+ return None
diff --git a/themes/red-theme/lms/templates/schedules/edx_ace/common/base_body.html b/themes/red-theme/lms/templates/schedules/edx_ace/common/base_body.html
new file mode 100644
index 0000000000..477c962003
--- /dev/null
+++ b/themes/red-theme/lms/templates/schedules/edx_ace/common/base_body.html
@@ -0,0 +1,196 @@
+{% load i18n %}
+
+{% get_current_language as LANGUAGE_CODE %}
+{% get_current_language_bidi as LANGUAGE_BIDI %}
+
+{# This is preview text that is visible in the inbox view of many email clients but not visible in the actual #}
+{# email itself. #}
+
+
+ {% block preview_text %}{% endblock %}
+
+
+
+
+{# Note {beacon_src} is not a template variable that is evaluated by the Django template engine. It is evaluated by #}
+{# Sailthru when the email is sent. Other email providers would need to replace this variable in the HTML as well. #}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {% block content %}{% endblock %}
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+{# Debug info that is not user-visible #}
+{{ message.log_id }}
+{{ template_revision }}
diff --git a/themes/red-theme/lms/templates/schedules/edx_ace/common/return_to_course_cta.html b/themes/red-theme/lms/templates/schedules/edx_ace/common/return_to_course_cta.html
new file mode 100644
index 0000000000..4251c14cbd
--- /dev/null
+++ b/themes/red-theme/lms/templates/schedules/edx_ace/common/return_to_course_cta.html
@@ -0,0 +1,27 @@
+{% load i18n %}
+
+
+ {# email client support for style sheets is pretty spotty, so we have to inline all of these styles #}
+ 1 %}
+ href="{{ dashboard_url }}"
+ {% else %}
+ href="{{ course_url }}"
+ {% endif %}
+ style="
+ color: #ffffff;
+ text-decoration: none;
+ border-radius: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ background-color: #960909;
+ border-top: 12px solid #960909;
+ border-bottom: 12px solid #960909;
+ border-right: 50px solid #960909;
+ border-left: 50px solid #960909;
+ display: inline-block;
+ ">
+ {# old email clients require the use of the font tag :( #}
+ {{ course_cta_text }}
+
+
diff --git a/themes/red-theme/lms/templates/schedules/edx_ace/recurringnudge_day3/email/body.txt b/themes/red-theme/lms/templates/schedules/edx_ace/recurringnudge_day3/email/body.txt
new file mode 100644
index 0000000000..4e9484ac03
--- /dev/null
+++ b/themes/red-theme/lms/templates/schedules/edx_ace/recurringnudge_day3/email/body.txt
@@ -0,0 +1,19 @@
+{% load i18n %}
+
+This is the RED theme!
+{% if course_ids|length > 1 %}
+{% blocktrans trimmed %}
+ Remember when you enrolled in {{ course_name }}, and other courses on edX.org? We do, and we’re glad
+ to have you! Come see what everyone is learning.
+{% endblocktrans %}
+
+{% trans "Start learning now" %} <{{ dashboard_url }}>
+{% else %}
+{% blocktrans trimmed %}
+ Remember when you enrolled in {{ course_name }} on edX.org? We do, and we’re glad
+ to have you! Come see what everyone is learning.
+{% endblocktrans %}
+
+{% trans "Start learning now" %} <{{ course_url }}>
+{% endif %}
+{% include "schedules/edx_ace/common/upsell_cta.txt"%}