Merge pull request #16508 from edx/ret/fix-theming-for-ace
support theming of ACE emails
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
49
openedx/core/lib/celery/task_utils.py
Normal file
49
openedx/core/lib/celery/task_utils.py
Normal file
@@ -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
|
||||
@@ -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. #}
|
||||
|
||||
<div lang="{{ LANGUAGE_CODE|default:"en" }}" style="
|
||||
display:none;
|
||||
font-size:1px;
|
||||
line-height:1px;
|
||||
max-height:0px;
|
||||
max-width:0px;
|
||||
opacity:0;
|
||||
overflow:hidden;
|
||||
visibility:hidden;
|
||||
">
|
||||
{% block preview_text %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- TEST RED THEME MARKER: Do not remove this comment, it is used by the tests to tell if this theme was used -->
|
||||
|
||||
{# 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. #}
|
||||
<img src="{beacon_src}" alt="" role="presentation" aria-hidden="true" />
|
||||
|
||||
<div bgcolor="#f5f5f5" lang="{{ LANGUAGE_CODE|default:"en" }}" dir="{{ LANGUAGE_BIDI|yesno:"rtl,ltr" }}" style="
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 100%;
|
||||
">
|
||||
<!-- Hack for outlook 2010, which wants to render everything in Times New Roman -->
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td {font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif !important;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table role="presentation" width="600" align="center" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td>
|
||||
<![endif]-->
|
||||
|
||||
<!-- CONTENT -->
|
||||
<table class="content" role="presentation" align="center" cellpadding="0" cellspacing="0" border="0" bgcolor="#ffd1d1" width="100%" style="
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
max-width: 600px;
|
||||
padding: 0 20px 0 20px;
|
||||
">
|
||||
<tr>
|
||||
<!-- HEADER -->
|
||||
<td class="header" style="
|
||||
padding: 20px;
|
||||
">
|
||||
<table role="presentation" width="100%" align="left" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td width="70">
|
||||
<a href="{{ homepage_url }}"><img
|
||||
src="http://localhost:18000/static/red-theme/images/logo.png"
|
||||
alt="{% blocktrans %}Go to {{ platform_name }} Home Page{% endblocktrans %}"/></a>
|
||||
</td>
|
||||
<td align="right" style="text-align: {{ LANGUAGE_BIDI|yesno:"left,right" }};">
|
||||
<a class="login" href="{{ dashboard_url }}" style="color: #960909;">{% trans "Sign In" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<!-- MAIN -->
|
||||
<td class="main" bgcolor="#ffffff" style="
|
||||
padding: 30px 20px;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.25);
|
||||
">
|
||||
{% block content %}{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<!-- FOOTER -->
|
||||
<td class="footer" style="padding: 20px;">
|
||||
<table role="presentation" width="100%" align="left" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="padding-bottom: 20px;">
|
||||
<!-- SOCIAL -->
|
||||
<table role="presentation" align="{{ LANGUAGE_BIDI|yesno:"right,left" }}" border="0" border="0" cellpadding="0" cellspacing="0" width="210">
|
||||
<tr>
|
||||
{% if social_media_urls.linkedin %}
|
||||
<td height="32" width="42">
|
||||
<a href="{{ social_media_urls.linkedin }}">
|
||||
<img src="https://media.sailthru.com/595/1k1/8/o/599f354ec70cb.png"
|
||||
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on LinkedIn{% endblocktrans %}"/>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if social_media_urls.twitter %}
|
||||
<td height="32" width="42">
|
||||
<a href="{{ social_media_urls.twitter }}">
|
||||
<img src="https://media.sailthru.com/595/1k1/8/o/599f354d9c26e.png"
|
||||
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on Twitter{% endblocktrans %}"/>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if social_media_urls.facebook %}
|
||||
<td height="32" width="42">
|
||||
<a href="{{ social_media_urls.facebook }}">
|
||||
<img src="https://media.sailthru.com/595/1k1/8/o/599f355052c8e.png"
|
||||
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on Facebook{% endblocktrans %}"/>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if social_media_urls.google_plus %}
|
||||
<td height="32" width="42">
|
||||
<a href="{{ social_media_urls.google_plus }}">
|
||||
<img src="https://media.sailthru.com/595/1k1/8/o/599f354fc554a.png"
|
||||
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on Google Plus{% endblocktrans %}"/>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if social_media_urls.reddit %}
|
||||
<td height="32" width="42">
|
||||
<a href="{{ social_media_urls.reddit }}">
|
||||
<img src="https://media.sailthru.com/595/1k1/8/o/599f354e326b9.png"
|
||||
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on Reddit{% endblocktrans %}"/>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<!-- APP BUTTONS -->
|
||||
<td style="padding-bottom: 20px;">
|
||||
{% if mobile_store_urls.apple %}
|
||||
<a href="{{ mobile_store_urls.apple }}" style="text-decoration: none">
|
||||
<img src="https://media.sailthru.com/595/1k1/6/2/5931cfbba391b.png"
|
||||
alt="{% trans "Download the iOS app on the Apple Store" %}"
|
||||
width="136" height="50" style="margin-{{ LANGUAGE_BIDI|yesno:"left,right" }}: 10px"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if mobile_store_urls.google %}
|
||||
<a href="{{ mobile_store_urls.google }}" style="text-decoration: none">
|
||||
<img src="https://media.sailthru.com/595/1k1/6/2/5931cf879a033.png"
|
||||
alt="{% trans "Download the Android app on the Google Play Store" %}"
|
||||
width="136" height="50"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<!-- Actions -->
|
||||
<td style="padding-bottom: 20px;">
|
||||
{# Note that these variables are evaluated by Sailthru, not the Django template engine #}
|
||||
<p>
|
||||
<a href="{view_url}" style="color: #960909">
|
||||
<font color="#960909"><b>{% trans "View on Web" %}</b></font>
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{optout_confirm_url}" style="color: #960909">
|
||||
<font color="#960909"><b>{% trans "Unsubscribe from this list" %}</b></font>
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<!-- COPYRIGHT -->
|
||||
<td>
|
||||
© {% now "Y" %} {{ platform_name }}, {% trans "All rights reserved" %}.<br/>
|
||||
<br/>
|
||||
{% trans "Our mailing address is" %}:<br/>
|
||||
{{ contact_mailing_address }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
|
||||
</div>
|
||||
|
||||
{# Debug info that is not user-visible #}
|
||||
<span id="ace-message-id" style="display:none;">{{ message.log_id }}</span>
|
||||
<span id="template-revision" style="display:none;">{{ template_revision }}</span>
|
||||
@@ -0,0 +1,27 @@
|
||||
{% load i18n %}
|
||||
|
||||
<p>
|
||||
{# email client support for style sheets is pretty spotty, so we have to inline all of these styles #}
|
||||
<a
|
||||
{% if course_ids|length > 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 :( #}
|
||||
<font color="#ffffff"><b>{{ course_cta_text }}</b></font>
|
||||
</a>
|
||||
</p>
|
||||
@@ -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"%}
|
||||
Reference in New Issue
Block a user