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. #} + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +{# Debug info that is not user-visible #} + + 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"%}