support a GA tracking pixel

parent 68890dab
......@@ -101,10 +101,9 @@ def _get_course_language(course_id):
def _build_message_context(context):
message_context = get_base_template_context(Site.objects.get(id=context['site_id']))
message_context = get_base_template_context(context['site'])
message_context.update(_deserialize_context_dates(context))
message_context['post_link'] = _get_thread_url(context)
message_context['ga_tracking_pixel_url'] = _generate_ga_pixel_url(context)
return message_context
......@@ -122,25 +121,3 @@ def _get_thread_url(context):
'id': context['thread_id'],
}
return urljoin(context['site'].domain, permalink(thread_content))
def _generate_ga_pixel_url(context):
# used for analytics
query_params = {
'v': '1', # version, required for GA
't': 'event', #
'ec': 'email', # event category
'ea': 'edx.bi.email.opened', # event action: in this case, the user opened the email
'tid': get_value("GOOGLE_ANALYTICS_TRACKING_ID", getattr(settings, "GOOGLE_ANALYTICS_TRACKING_ID", None)), # tracking ID to associate this link with our GA instance
'uid': context['thread_author_id'],
'cs': 'sailthru', # Campaign source - what sent the email
'cm': 'email', # Campaign medium - how the content is being delivered
'cn': 'triggered_discussionnotification', # Campaign name - human-readable name for this particular class of message
'dp': '/email/ace/discussions/responsenotification/{0}/'.format(context['course_id']), # document path, used for drilling down into specific events
'dt': 'Reply to {0} at {1}'.format(context['thread_title'], context['comment_created_at']), # document title, should match the title of the email
}
return u"{url}?{params}".format(
url="https://www.google-analytics.com/collect",
params=urlencode(query_params)
)
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{% block title %}Reply to {{ thread_title }} at {{ comment_created_at }} {% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style type="text/css">
@media only screen and (min-device-width: 601px) {
.content {
width: 600px !important;
}
}
@-ms-viewport{
width: device-width;
}
/* Column Drop Layout Pattern CSS */
@media only screen and (max-width: 450px) {
td[class="col"] {
display: block;
width: 100%;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
float: left;
text-align: left !important;
padding-bottom: 20px;
}
}
</style>
\ No newline at end of file
{% extends 'schedules/edx_ace/common/base_body.html' %}
{% load i18n %}
{% load static %}
{% block preview_text %}
{% blocktrans trimmed %}
Hi {{ thread_username }}
{{ comment_username }} replied to your thread.
{% endblocktrans %}
{% endblock %}
......@@ -12,9 +14,9 @@
<tr>
<td>
{% blocktrans trimmed %}
<h1>
<p>
Hi {{ thread_username }},
</h1>
</p>
<p>
{{ comment_username }} made the following reply to {{ thread_title }} at {{ comment_created_at }}.
......@@ -23,40 +25,11 @@
<p>
{{ comment_body }}
</p>
<p>
<a href="{{ post_link }}"> View the discussion.</a>
</p>
{% endblocktrans %}
<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="{{ upsell_link }}"
{% else %}
href="{{ dashboard_url }}"
{% endif %}
style="
color: #ffffff;
text-decoration: none;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
background-color: #005686;
border-top: 10px solid #005686;
border-bottom: 10px solid #005686;
border-right: 16px solid #005686;
border-left: 16px solid #005686;
display: inline-block;
">
</a>
</p>
{% trans "View the discussion" as course_cta_text %}
{% include "schedules/edx_ace/common/return_to_course_cta.html" with course_cta_text=course_cta_text course_cta_url=post_link%}
</td>
</tr>
</table>
{% endblock %}
{% block google_analytics_pixel %}
<img src="{{ ga_tracking_pixel_url }}" alt="" role="presentation" aria-hidden="true" />
{% endblock %}
"""
Tests the execution of forum notification tasks.
"""
from contextlib import contextmanager
from datetime import datetime, timedelta
import json
import math
from urlparse import urljoin
import ddt
from django.conf import settings
from django.contrib.sites.models import Site
import mock
import lms.lib.comment_client as cc
from django_comment_common.models import ForumsConfig
from django_comment_common.signals import comment_created
from edx_ace.recipient import Recipient
from edx_ace.utils import date
from lms.djangoapps.discussion.config.waffle import waffle, FORUM_RESPONSE_NOTIFICATIONS, SEND_NOTIFICATIONS_FOR_COURSE
from lms.djangoapps.discussion.signals.handlers import ENABLE_FORUM_NOTIFICATIONS_FOR_SITE_KEY
from lms.djangoapps.discussion.tasks import _should_send_message, _generate_ga_pixel_url
import lms.lib.comment_client as cc
from lms.djangoapps.discussion.tasks import _should_send_message
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.schedules.template_context import get_base_template_context
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
......@@ -204,11 +202,10 @@ class TaskTestCase(ModuleStoreTestCase):
'thread_title': 'thread-title',
'thread_username': self.thread_author.username,
'thread_commentable_id': self.thread['commentable_id'],
'post_link': urljoin(site.domain, self.mock_permalink.return_value),
'post_link': self.mock_permalink.return_value,
'site': site,
'site_id': site.id
})
expected_message_context['ga_tracking_pixel_url'] = _generate_ga_pixel_url(expected_message_context)
expected_recipient = Recipient(self.thread_author.username, self.thread_author.email)
actual_message = self.mock_ace_send.call_args_list[0][0][0]
self.assertEqual(expected_message_context, actual_message.context)
......
......@@ -400,7 +400,9 @@ class ScheduleSendEmailTestBase(FilteredQueryCountMixin, CacheIsolationTestCase)
self.assertEqual(len(sent_messages), num_expected_messages)
with self.assertNumQueries(NUM_QUERIES_PER_MESSAGE_DELIVERY):
self.deliver_task(*sent_messages[0])
with patch('analytics.track') as mock_analytics_track:
self.deliver_task(*sent_messages[0])
self.assertEqual(mock_analytics_track.call_count, 1)
self.assertEqual(mock_channel.deliver.call_count, 1)
for (_name, (_msg, email), _kwargs) in mock_channel.deliver.mock_calls:
......
......@@ -20,10 +20,7 @@ from openedx.core.djangoapps.schedules.content_highlights import get_week_highli
from openedx.core.djangoapps.schedules.exceptions import CourseUpdateDoesNotExist
from openedx.core.djangoapps.schedules.models import Schedule, ScheduleExperience
from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin
from openedx.core.djangoapps.schedules.template_context import (
absolute_url,
get_base_template_context
)
from openedx.core.djangoapps.schedules.template_context import get_base_template_context
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
......@@ -247,9 +244,7 @@ class RecurringNudgeResolver(BinnedSchedulesBaseResolver):
first_schedule = user_schedules[0]
context = {
'course_name': first_schedule.enrollment.course.display_name,
'course_url': absolute_url(
self.site, reverse('course_root', args=[str(first_schedule.enrollment.course_id)])
),
'course_url': reverse('course_root', args=[str(first_schedule.enrollment.course_id)]),
}
# Information for including upsell messaging in template.
......@@ -289,7 +284,7 @@ class UpgradeReminderResolver(BinnedSchedulesBaseResolver):
course_id_str = str(schedule.enrollment.course_id)
course_id_strs.append(course_id_str)
course_links.append({
'url': absolute_url(self.site, reverse('course_root', args=[course_id_str])),
'url': reverse('course_root', args=[course_id_str]),
'name': schedule.enrollment.course.display_name
})
......@@ -300,7 +295,7 @@ class UpgradeReminderResolver(BinnedSchedulesBaseResolver):
context = {
'course_links': course_links,
'first_course_name': first_schedule.enrollment.course.display_name,
'cert_image': absolute_url(self.site, static('course_experience/images/verified-cert.png')),
'cert_image': static('course_experience/images/verified-cert.png'),
'course_ids': course_id_strs,
}
context.update(first_valid_upsell_context)
......@@ -365,9 +360,7 @@ class CourseUpdateResolver(BinnedSchedulesBaseResolver):
course_id_str = str(enrollment.course_id)
template_context.update({
'course_name': schedule.enrollment.course.display_name,
'course_url': absolute_url(
self.site, reverse('course_root', args=[course_id_str])
),
'course_url': reverse('course_root', args=[course_id_str]),
'week_num': week_num,
'week_highlights': week_highlights,
......
import datetime
import logging
import analytics
from celery.task import task, Task
from crum import CurrentRequestUserMiddleware
from django.conf import settings
......@@ -180,7 +181,7 @@ class ScheduleCourseUpdate(ScheduleMessageBaseTask):
def _schedule_send(msg_str, site_id, delivery_config_var, log_prefix):
site = Site.objects.get(pk=site_id)
site = Site.objects.select_related('configuration').get(pk=site_id)
if _is_delivery_enabled(site, delivery_config_var, log_prefix):
msg = Message.from_string(msg_str)
......@@ -193,6 +194,31 @@ def _schedule_send(msg_str, site_id, delivery_config_var, log_prefix):
_annonate_send_task_for_monitoring(msg)
LOG.debug('%s: Sending message = %s', log_prefix, msg_str)
ace.send(msg)
_track_message_sent(site, user, msg)
def _track_message_sent(site, user, msg):
properties = {
'site': site.domain,
'app_label': msg.app_label,
'name': msg.name,
'language': msg.language,
'uuid': msg.uuid,
'send_uuid': msg.send_uuid,
}
course_ids = msg.context.get('course_ids', [])
if len(course_ids) > 0:
properties['course_ids'] = course_ids[:10]
properties['primary_course_id'] = course_ids[0]
if len(course_ids) > 1:
properties['num_courses'] = len(course_ids)
analytics.track(
user_id=user.id,
event='edx.bi.email.sent',
properties=properties
)
def _is_delivery_enabled(site, delivery_config_var, log_prefix):
......
from urlparse import urlparse
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.http import urlquote
from edxmako.shortcuts import marketing_link
from openedx.core.djangoapps.schedules.utils import get_config_value_from_site_or_settings
def get_base_template_context(site):
"""Dict with entries needed for all templates that use the base template"""
return {
# Platform information
'homepage_url': encode_url(marketing_link('ROOT')),
'dashboard_url': absolute_url(site, reverse('dashboard')),
'template_revision': settings.EDX_PLATFORM_REVISION,
'platform_name': site.configuration.get_value('platform_name', settings.PLATFORM_NAME),
'contact_mailing_address': site.configuration.get_value(
'contact_mailing_address',
settings.CONTACT_MAILING_ADDRESS
),
'social_media_urls': encode_urls_in_dict(
site.configuration.get_value(
'SOCIAL_MEDIA_FOOTER_URLS',
getattr(settings, 'SOCIAL_MEDIA_FOOTER_URLS', {})
)
),
'mobile_store_urls': encode_urls_in_dict(
site.configuration.get_value(
'MOBILE_STORE_URLS',
getattr(settings, 'MOBILE_STORE_URLS', {})
)
),
'homepage_url': marketing_link('ROOT'),
'dashboard_url': reverse('dashboard'),
'template_revision': getattr(settings, 'EDX_PLATFORM_REVISION', None),
'platform_name': get_config_value_from_site_or_settings('PLATFORM_NAME', site=site, site_config_name='platform_name'),
'contact_mailing_address': get_config_value_from_site_or_settings(
'CONTACT_MAILING_ADDRESS', site=site, site_config_name='contact_mailing_address'),
'social_media_urls': get_config_value_from_site_or_settings('SOCIAL_MEDIA_FOOTER_URLS', site=site),
'mobile_store_urls': get_config_value_from_site_or_settings('MOBILE_STORE_URLS', site=site),
}
def encode_url(url):
# Sailthru has a bug where URLs that contain "+" characters in their path components are misinterpreted
# when GA instrumentation is enabled. We need to percent-encode the path segments of all URLs that are
# injected into our templates to work around this issue.
parsed_url = urlparse(url)
modified_url = parsed_url._replace(path=urlquote(parsed_url.path))
return modified_url.geturl()
def absolute_url(site, relative_path):
"""
Add site.domain to the beginning of the given relative path.
If the given URL is already absolute (has a netloc part), then it is just returned.
"""
if bool(urlparse(relative_path).netloc):
# Given URL is already absolute
return relative_path
root = site.domain.rstrip('/')
relative_path = relative_path.lstrip('/')
return encode_url(u'https://{root}/{path}'.format(root=root, path=relative_path))
def encode_urls_in_dict(mapping):
urls = {}
for key, value in mapping.iteritems():
urls[key] = encode_url(value)
return urls
{% load i18n %}
{% load ace %}
{% get_current_language as LANGUAGE_CODE %}
{% get_current_language_bidi as LANGUAGE_BIDI %}
......@@ -23,6 +24,8 @@
{# 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" />
{% google_analytics_tracking_pixel %}
<div bgcolor="#f5f5f5" lang="{{ LANGUAGE_CODE|default:"en" }}" dir="{{ LANGUAGE_BIDI|yesno:"rtl,ltr" }}" style="
margin: 0;
padding: 0;
......@@ -57,12 +60,12 @@
<table role="presentation" width="100%" align="left" border="0" cellpadding="0" cellspacing="0">
<tr>
<td width="70">
<a href="{{ homepage_url }}"><img
<a href="{% with_link_tracking homepage_url %}"><img
src="https://media.sailthru.com/595/1k1/8/o/599f355101b3f.png" width="70"
height="30" 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: #005686;">{% trans "Sign In" %}</a>
<a class="login" href="{% with_link_tracking dashboard_url %}" style="color: #005686;">{% trans "Sign In" %}</a>
</td>
</tr>
</table>
......@@ -90,7 +93,7 @@
<tr>
{% if social_media_urls.linkedin %}
<td height="32" width="42">
<a href="{{ social_media_urls.linkedin }}">
<a href="{{ social_media_urls.linkedin|safe }}">
<img src="https://media.sailthru.com/595/1k1/8/o/599f354ec70cb.png"
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on LinkedIn{% endblocktrans %}"/>
</a>
......@@ -98,7 +101,7 @@
{% endif %}
{% if social_media_urls.twitter %}
<td height="32" width="42">
<a href="{{ social_media_urls.twitter }}">
<a href="{{ social_media_urls.twitter|safe }}">
<img src="https://media.sailthru.com/595/1k1/8/o/599f354d9c26e.png"
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on Twitter{% endblocktrans %}"/>
</a>
......@@ -106,7 +109,7 @@
{% endif %}
{% if social_media_urls.facebook %}
<td height="32" width="42">
<a href="{{ social_media_urls.facebook }}">
<a href="{{ social_media_urls.facebook|safe }}">
<img src="https://media.sailthru.com/595/1k1/8/o/599f355052c8e.png"
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on Facebook{% endblocktrans %}"/>
</a>
......@@ -114,7 +117,7 @@
{% endif %}
{% if social_media_urls.google_plus %}
<td height="32" width="42">
<a href="{{ social_media_urls.google_plus }}">
<a href="{{ social_media_urls.google_plus|safe }}">
<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>
......@@ -122,7 +125,7 @@
{% endif %}
{% if social_media_urls.reddit %}
<td height="32" width="42">
<a href="{{ social_media_urls.reddit }}">
<a href="{{ social_media_urls.reddit|safe }}">
<img src="https://media.sailthru.com/595/1k1/8/o/599f354e326b9.png"
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on Reddit{% endblocktrans %}"/>
</a>
......@@ -136,14 +139,14 @@
<!-- APP BUTTONS -->
<td style="padding-bottom: 20px;">
{% if mobile_store_urls.apple %}
<a href="{{ mobile_store_urls.apple }}" style="text-decoration: none">
<a href="{{ mobile_store_urls.apple|safe }}" 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">
<a href="{{ mobile_store_urls.google|safe }}" 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"/>
......
{% load i18n %}
{% load ace %}
<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 }}"
{% if course_cta_url %}
href="{% with_link_tracking course_cta_url %}"
{% else %}
href="{{ course_url }}"
{%if course_ids|length > 1 %}
href="{% with_link_tracking dashboard_url %}"
{% else %}
href="{% with_link_tracking course_url %}"
{% endif %}
{% endif %}
style="
color: #ffffff;
......
{% load i18n %}
{% load ace %}
{% if show_upsell %}
<p>
......@@ -8,7 +9,7 @@
{% endblocktrans %}
</p>
<p>
<a href="{{ upsell_link }}"
<a href="{% with_link_tracking upsell_link %}"
style="
color: #1e8142;
text-decoration: none;
......
{% load i18n %}
{% load ace %}
{% if show_upsell %}
{% blocktrans trimmed %}
Don't miss the opportunity to highlight your new knowledge and skills by earning a verified
certificate. Upgrade by {{ user_schedule_upgrade_deadline_time }}.
Upgrade Now! <{{ upsell_link }}>
{% endblocktrans %}
{% trans "Upgrade Now" %} <{% with_link_tracking upsell_link %}>
{% endif %}
{% extends 'schedules/edx_ace/common/base_body.html' %}
{% load i18n %}
{% load static %}
{% block preview_text %}
{% blocktrans trimmed %}
......
{% load i18n %}
{% load ace %}
{% if course_ids|length > 1 %}
{% blocktrans trimmed %}
Many edX learners are completing more problems every week, and
participating in the discussion forums. What do you want to do to keep learning?
{% endblocktrans %}
{% trans "Keep learning" %} <{{dashboard_url}}>
{% trans "Keep learning" %} <{% with_link_tracking dashboard_url %}>
{% else %}
{% blocktrans trimmed %}
Many edX learners in {{course_name}} are completing more problems every week, and
participating in the discussion forums. What do you want to do to keep learning?
{% endblocktrans %}
{% trans "Keep learning" %} <{{course_url}}>
{% trans "Keep learning" %} <{% with_link_tracking course_url %}>
{% endif %}
{% include "schedules/edx_ace/common/upsell_cta.txt"%}
{% load i18n %}
{% load ace %}
{% 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 }}>
{% trans "Start learning now" %} <{% with_link_tracking 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 }}>
{% trans "Start learning now" %} <{% with_link_tracking course_url %}>
{% endif %}
{% include "schedules/edx_ace/common/upsell_cta.txt"%}
{% extends 'schedules/edx_ace/common/base_body.html' %}
{% load i18n %}
{% load static %}
{% load ace %}
{% block preview_text %}
{% if course_ids|length > 1 %}
......@@ -56,7 +57,7 @@
<ul style="margin-bottom: 30px;">
{% for course_link in course_links %}
<li>
<a href="{{ course_link.url }}">{{ course_link.name }}</a>
<a href="{% with_link_tracking course_link.url %}">{{ course_link.name }}</a>
</li>
{% endfor %}
</ul>
......@@ -82,9 +83,9 @@
{# 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="{{ upsell_link }}"
href="{% with_link_tracking upsell_link %}"
{% else %}
href="{{ dashboard_url }}"
href="{% with_link_tracking dashboard_url %}"
{% endif %}
style="
color: #ffffff;
......
{% load i18n %}
{% load ace %}
{% if course_ids|length > 1 %}
{% blocktrans trimmed %}
We hope you are enjoying learning with us so far on {{ platform_name }}! A verified certificate
......@@ -11,11 +11,11 @@ Upgrade by {{ user_schedule_upgrade_deadline_time }}.
{% if course_ids|length > 1 and course_ids|length < 10 %}
{% for course_link in course_links %}
* {{ course_link.name }} <{{ course_link.url }}>
* {{ course_link.name }} <{% with_link_tracking course_link.url %}>
{% endfor %}
{% endif %}
{% trans "Upgrade now at" %} <{{ dashboard_url }}>
{% trans "Upgrade now at" %} <{% with_link_tracking dashboard_url %}>
{% else %}
{% blocktrans trimmed %}
We hope you are enjoying learning with us so far in {{ first_course_name }}! A verified certificate
......@@ -25,5 +25,5 @@ official and easily shareable.
Upgrade by {{ user_schedule_upgrade_deadline_time }}.
{% endblocktrans %}
{% trans "Upgrade now at" %} <{{ upsell_link }}>
{% trans "Upgrade now at" %} <{% with_link_tracking upsell_link %}>
{% endif %}
from urlparse import urlparse, parse_qs
from crum import get_current_request
from django import template
from django.utils.safestring import mark_safe
from openedx.core.djangoapps.schedules.tracking import CampaignTrackingInfo, GoogleAnalyticsTrackingPixel
from openedx.core.djangolib.markup import HTML
register = template.Library()
@register.simple_tag(takes_context=True)
def with_link_tracking(context, url):
"""
Modifies the provided URL to ensure it is safe for usage in an email template and adds UTM parameters to it.
The provided URL can be relative or absolute. If it is relative, it will be made absolute.
All URLs will be augmented to include UTM parameters so that clicks can be tracked.
Args:
context (dict): The template context. Must include a "request" and "message".
url (str): The url to rewrite.
Returns:
str: The URL as an absolute URL with appropriate query string parameters that allow clicks to be tracked.
"""
site, _user, message = _get_variables_from_context(context, 'with_link_tracking')
campaign = CampaignTrackingInfo(
source=message.app_label,
campaign=message.name,
content=message.uuid,
)
course_ids = context.get('course_ids')
if course_ids is not None and len(course_ids) > 0:
campaign.term = course_ids[0]
return mark_safe(
modify_url_to_track_clicks(
ensure_url_is_absolute(site, url),
campaign=campaign
)
)
def _get_variables_from_context(context, tag_name):
if 'request' in context:
request = context['request']
else:
request = get_current_request()
if request is None:
raise template.VariableDoesNotExist(
'The {0} template tag requires a "request" to be present in the template context. Consider using '
'"emulate_http_request" if you are rendering the template in a celery task.'.format(tag_name)
)
message = context.get('message')
if message is None:
raise template.VariableDoesNotExist(
'The {0} template tag requires a "message" to be present in the template context.'.format(tag_name)
)
return request.site, request.user, message
@register.simple_tag(takes_context=True)
def google_analytics_tracking_pixel(context):
"""
If configured, inject a google analytics tracking pixel into the template.
This tracking pixel will allow email open events to be tracked.
Args:
context (dict): The template context. Must include a "request" and "message".
Returns:
str: A string containing an HTML image tag that implements the GA measurement protocol or an empty string if
GA is not configured. For this to work, the site or settings must include the GA tracking ID.
"""
image_url = _get_google_analytics_tracking_url(context)
if image_url is not None:
return mark_safe(
HTML('<img src="{0}" alt="" role="presentation" aria-hidden="true" />').format(HTML(image_url))
)
else:
return ''
def _get_google_analytics_tracking_url(context):
site, user, message = _get_variables_from_context(context, 'google_analytics_tracking_pixel')
pixel = GoogleAnalyticsTrackingPixel(
site=site,
user_id=user.id,
campaign_source=message.app_label,
campaign_name=message.name,
campaign_content=message.uuid,
document_path='/email/{0}/{1}/{2}/{3}'.format(
message.app_label,
message.name,
message.send_uuid,
message.uuid,
),
)
course_ids = context.get('course_ids')
if course_ids is not None and len(course_ids) > 0:
pixel.course_id = course_ids[0]
return pixel.image_url
def modify_url_to_track_clicks(url, campaign=None):
"""
Given a URL, this method modifies the query string parameters to include UTM tracking parameters.
These UTM codes are used to by Google Analytics to identify the source of traffic. This will help us better
understand how users behave when they come to the site by clicking a link in this email.
Arguments:
url (str): pass
campaign (CampaignTrackingInfo): pass
Returns:
str: The url with appropriate query string parameters.
"""
parsed_url = urlparse(url)
if campaign is None:
campaign = CampaignTrackingInfo()
modified_url = parsed_url._replace(query=campaign.to_query_string(parsed_url.query))
return modified_url.geturl()
def ensure_url_is_absolute(site, relative_path):
"""
Add site.domain to the beginning of the given relative path.
If the given URL is already absolute (has a netloc part), then it is just returned.
"""
if bool(urlparse(relative_path).netloc):
# Given URL is already absolute
url = relative_path
else:
root = site.domain.rstrip('/')
relative_path = relative_path.lstrip('/')
url = u'https://{root}/{path}'.format(root=root, path=relative_path)
return url
from urlparse import parse_qs, urlparse
class QueryStringAssertionMixin(object):
def assert_query_string_equal(self, expected_qs, actual_qs):
"""
Compares two query strings to see if they are equivalent. Note that order of parameters is not significant.
Args:
expected_qs (str): The expected query string.
actual_qs (str): The actual query string.
Raises:
AssertionError: If the two query strings are not equal.
"""
self.assertDictEqual(parse_qs(expected_qs), parse_qs(actual_qs))
def assert_url_components_equal(self, url, **kwargs):
"""
Assert that the provided URL has the expected components with the expected values.
Args:
url (str): The URL to parse and make assertions about.
**kwargs: The expected component values. For example: scheme='https' would assert that the URL scheme was
https.
Raises:
AssertionError: If any of the expected components do not match.
"""
parsed_url = urlparse(url)
for expected_component, expected_value in kwargs.items():
if expected_component == 'query':
self.assert_query_string_equal(expected_value, parsed_url.query)
else:
self.assertEqual(expected_value, getattr(parsed_url, expected_component))
def assert_query_string_parameters_equal(self, url, **kwargs):
"""
Assert that the provided URL has query string paramters that match the kwargs.
Args:
url (str): The URL to parse and make assertions about.
**kwargs: The expected query string parameter values. For example: foo='bar' would assert that foo=bar
appeared in the query string.
Raises:
AssertionError: If any of the expected parameters values do not match.
"""
parsed_url = urlparse(url)
parsed_qs = parse_qs(parsed_url.query)
for expected_key, expected_value in kwargs.items():
self.assertEqual(parsed_qs[expected_key], [str(expected_value)])
from openedx.core.djangoapps.schedules.template_context import absolute_url
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
@skip_unless_lms
class TestTemplateContext(CacheIsolationTestCase):
def setUp(self):
self.site = SiteFactory.create()
self.site.domain = 'example.com'
def test_absolute_url(self):
absolute = absolute_url(self.site, '/foo/bar')
self.assertEqual(absolute, 'https://example.com/foo/bar')
def test_absolute_url_domain_lstrip(self):
self.site.domain = 'example.com/'
absolute = absolute_url(self.site, 'foo/bar')
self.assertEqual(absolute, 'https://example.com/foo/bar')
def test_absolute_url_already_absolute(self):
absolute = absolute_url(self.site, 'https://some-cdn.com/foo/bar')
self.assertEqual(absolute, 'https://some-cdn.com/foo/bar')
import uuid
from django.http import HttpRequest
from django.template import VariableDoesNotExist
from django.test import override_settings
from mock import patch
from edx_ace import Message, Recipient
from openedx.core.djangoapps.schedules.templatetags.ace import (
ensure_url_is_absolute,
with_link_tracking,
google_analytics_tracking_pixel,
_get_google_analytics_tracking_url
)
from openedx.core.djangoapps.schedules.tests.mixins import QueryStringAssertionMixin
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
from student.tests.factories import UserFactory
@skip_unless_lms
class TestAbsoluteUrl(CacheIsolationTestCase):
def setUp(self):
self.site = SiteFactory.create()
self.site.domain = 'example.com'
def test_absolute_url(self):
absolute = ensure_url_is_absolute(self.site, '/foo/bar')
self.assertEqual(absolute, 'https://example.com/foo/bar')
def test_absolute_url_domain_lstrip(self):
self.site.domain = 'example.com/'
absolute = ensure_url_is_absolute(self.site, 'foo/bar')
self.assertEqual(absolute, 'https://example.com/foo/bar')
def test_absolute_url_already_absolute(self):
absolute = ensure_url_is_absolute(self.site, 'https://some-cdn.com/foo/bar')
self.assertEqual(absolute, 'https://some-cdn.com/foo/bar')
class EmailTemplateTagMixin(object):
def setUp(self):
patcher = patch('openedx.core.djangoapps.schedules.templatetags.ace.get_current_request')
self.mock_get_current_request = patcher.start()
self.addCleanup(patcher.stop)
self.fake_request = HttpRequest()
self.fake_request.user = UserFactory.create()
self.fake_request.site = SiteFactory.create()
self.fake_request.site.domain = 'example.com'
self.mock_get_current_request.return_value = self.fake_request
self.message = Message(
app_label='test_app_label',
name='test_name',
recipient=Recipient(username='test_user'),
context={},
send_uuid=uuid.uuid4(),
)
self.context = {
'message': self.message
}
@skip_unless_lms
class TestLinkTrackingTag(QueryStringAssertionMixin, EmailTemplateTagMixin, CacheIsolationTestCase):
def test_default(self):
result_url = str(with_link_tracking(self.context, 'http://example.com/foo'))
self.assert_url_components_equal(
result_url,
scheme='http',
netloc='example.com',
path='/foo',
query='utm_source=test_app_label&utm_campaign=test_name&utm_medium=email&utm_content={uuid}'.format(
uuid=self.message.uuid
)
)
def test_missing_request(self):
self.mock_get_current_request.return_value = None
with self.assertRaises(VariableDoesNotExist):
with_link_tracking(self.context, 'http://example.com/foo')
def test_missing_message(self):
del self.context['message']
with self.assertRaises(VariableDoesNotExist):
with_link_tracking(self.context, 'http://example.com/foo')
def test_course_id(self):
self.context['course_ids'] = ['foo/bar/baz']
result_url = str(with_link_tracking(self.context, 'http://example.com/foo'))
self.assert_query_string_parameters_equal(
result_url,
utm_term='foo/bar/baz',
)
def test_multiple_course_ids(self):
self.context['course_ids'] = ['foo/bar/baz', 'course-v1:FooX+bar+baz']
result_url = str(with_link_tracking(self.context, 'http://example.com/foo'))
self.assert_query_string_parameters_equal(
result_url,
utm_term='foo/bar/baz',
)
def test_relative_url(self):
result_url = str(with_link_tracking(self.context, '/foobar'))
self.assert_url_components_equal(
result_url,
scheme='https',
netloc='example.com',
path='/foobar'
)
@skip_unless_lms
@override_settings(GOOGLE_ANALYTICS_TRACKING_ID='UA-123456-1')
class TestGoogleAnalyticsPixelTag(QueryStringAssertionMixin, EmailTemplateTagMixin, CacheIsolationTestCase):
def test_default(self):
result_url = _get_google_analytics_tracking_url(self.context)
self.assert_query_string_parameters_equal(
result_url,
uid=self.fake_request.user.id,
cs=self.message.app_label,
cn=self.message.name,
cc=self.message.uuid,
dp='/email/test_app_label/test_name/{send_uuid}/{uuid}'.format(
send_uuid=self.message.send_uuid,
uuid=self.message.uuid,
)
)
def test_missing_request(self):
self.mock_get_current_request.return_value = None
with self.assertRaises(VariableDoesNotExist):
google_analytics_tracking_pixel(self.context)
def test_missing_message(self):
del self.context['message']
with self.assertRaises(VariableDoesNotExist):
google_analytics_tracking_pixel(self.context)
def test_course_id(self):
self.context['course_ids'] = ['foo/bar/baz']
result_url = _get_google_analytics_tracking_url(self.context)
self.assert_query_string_parameters_equal(
result_url,
el='foo/bar/baz',
)
def test_multiple_course_ids(self):
self.context['course_ids'] = ['foo/bar/baz', 'course-v1:FooX+bar+baz']
result_url = _get_google_analytics_tracking_url(self.context)
self.assert_query_string_parameters_equal(
result_url,
el='foo/bar/baz',
)
def test_html_emitted(self):
result_html = google_analytics_tracking_pixel(self.context)
self.assertIn('<img src', result_html)
@override_settings(GOOGLE_ANALYTICS_TRACKING_ID=None)
def test_no_html_emitted_if_not_enabled(self):
result_html = google_analytics_tracking_pixel(self.context)
self.assertEqual('', result_html)
from unittest import TestCase
from django.test import override_settings
from openedx.core.djangoapps.schedules.tests.mixins import QueryStringAssertionMixin
from openedx.core.djangoapps.schedules.tracking import (
CampaignTrackingInfo,
DEFAULT_CAMPAIGN_SOURCE,
DEFAULT_CAMPAIGN_MEDIUM,
GoogleAnalyticsTrackingPixel)
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
class TestCampaignTrackingInfo(QueryStringAssertionMixin, TestCase):
def test_default_campaign_info(self):
campaign = CampaignTrackingInfo()
self.assertEqual(campaign.source, DEFAULT_CAMPAIGN_SOURCE)
self.assertEqual(campaign.medium, DEFAULT_CAMPAIGN_MEDIUM)
self.assertIsNone(campaign.campaign)
self.assertIsNone(campaign.term)
self.assertIsNone(campaign.content)
def test_to_query_string(self):
campaign = CampaignTrackingInfo(
source='test_source with spaces',
medium='test_medium',
campaign='test_campaign',
term='test_term',
content='test_content'
)
self.assert_query_string_equal(
'utm_source=test_source%20with%20spaces&utm_medium=test_medium&utm_campaign=test_campaign'
'&utm_term=test_term&utm_content=test_content',
campaign.to_query_string(),
)
def test_query_string_with_existing_parameters(self):
campaign = CampaignTrackingInfo(
source='test_source',
medium=None
)
self.assert_query_string_equal(
'some_parameter=testing&utm_source=test_source&other=test2',
campaign.to_query_string('some_parameter=testing&other=test2')
)
def test_query_string_with_existing_repeated_parameters(self):
campaign = CampaignTrackingInfo(
source='test_source',
medium=None
)
self.assert_query_string_equal(
'some_parameter=testing&utm_source=test_source&other=test2&some_parameter=baz',
campaign.to_query_string('some_parameter=testing&other=test2&some_parameter=baz')
)
def test_query_string_with_existing_utm_parameters(self):
campaign = CampaignTrackingInfo(
source='test_source',
medium=None
)
self.assert_query_string_equal(
'utm_source=test_source&utm_medium=custom_medium',
campaign.to_query_string('utm_source=custom_source&utm_medium=custom_medium')
)
class TestGoogleAnalyticsTrackingPixel(QueryStringAssertionMixin, CacheIsolationTestCase):
@override_settings(GOOGLE_ANALYTICS_TRACKING_ID='UA-123456-1')
def test_default_parameters(self):
pixel = GoogleAnalyticsTrackingPixel()
self.assertIsNotNone(pixel.image_url)
self.assert_url_components_equal(
pixel.image_url,
scheme='https',
netloc='www.google-analytics.com',
path='/collect',
query='v=1&t=event&cs={cs}&cm={cm}&ec=email&ea=edx.bi.email.opened&cid={cid}&tid=UA-123456-1'.format(
cs=DEFAULT_CAMPAIGN_SOURCE,
cm=DEFAULT_CAMPAIGN_MEDIUM,
cid=GoogleAnalyticsTrackingPixel.ANONYMOUS_USER_CLIENT_ID,
)
)
@override_settings(GOOGLE_ANALYTICS_TRACKING_ID='UA-123456-1')
def test_all_parameters(self):
pixel = GoogleAnalyticsTrackingPixel(
version=2,
hit_type='ev',
campaign_source='test_cs',
campaign_medium='test_cm',
campaign_name='test_cn',
campaign_content='test_cc',
event_category='test_ec',
event_action='test_ea',
event_label='test_el',
document_path='test_dp',
client_id='123456.123456',
)
self.assertIsNotNone(pixel.image_url)
self.assert_url_components_equal(
pixel.image_url,
scheme='https',
netloc='www.google-analytics.com',
path='/collect',
query='tid=UA-123456-1&v=2&t=ev&cs=test_cs&cm=test_cm&cn=test_cn&ec=test_ec&ea=test_ea&el=test_el'
'&dp=test_dp&cid=123456.123456&cc=test_cc'
)
def test_missing_settings(self):
pixel = GoogleAnalyticsTrackingPixel()
self.assertIsNone(pixel.image_url)
@override_settings(GOOGLE_ANALYTICS_TRACKING_ID='UA-123456-1')
def test_site_config_override(self):
site_config = SiteConfigurationFactory.create(
values=dict(
GOOGLE_ANALYTICS_ACCOUNT='UA-654321-1'
)
)
pixel = GoogleAnalyticsTrackingPixel(site=site_config.site)
self.assert_query_string_parameters_equal(pixel.image_url, tid='UA-654321-1')
@override_settings(
GOOGLE_ANALYTICS_TRACKING_ID='UA-123456-1',
GOOGLE_ANALYTICS_USER_ID_CUSTOM_DIMENSION=40
)
def test_custom_dimension(self):
pixel = GoogleAnalyticsTrackingPixel(user_id=10, campaign_source=None, campaign_medium=None)
self.assertIsNotNone(pixel.image_url)
self.assert_url_components_equal(
pixel.image_url,
query='v=1&t=event&ec=email&ea=edx.bi.email.opened&cid={cid}&tid=UA-123456-1&cd40=10&uid=10'.format(
cid=GoogleAnalyticsTrackingPixel.ANONYMOUS_USER_CLIENT_ID,
)
)
@override_settings(
GOOGLE_ANALYTICS_TRACKING_ID='UA-123456-1',
GOOGLE_ANALYTICS_USER_ID_CUSTOM_DIMENSION=40
)
def test_custom_dimension_without_user_id(self):
pixel = GoogleAnalyticsTrackingPixel(campaign_source=None, campaign_medium=None)
self.assertIsNotNone(pixel.image_url)
self.assert_url_components_equal(
pixel.image_url,
query='v=1&t=event&ec=email&ea=edx.bi.email.opened&cid={cid}&tid=UA-123456-1'.format(
cid=GoogleAnalyticsTrackingPixel.ANONYMOUS_USER_CLIENT_ID,
)
)
@override_settings(GOOGLE_ANALYTICS_TRACKING_ID='UA-123456-1')
def test_course_id(self):
course_id = 'foo/bar/baz'
pixel = GoogleAnalyticsTrackingPixel(course_id=course_id)
self.assertIsNotNone(pixel.image_url)
self.assert_query_string_parameters_equal(pixel.image_url, el=course_id)
@override_settings(GOOGLE_ANALYTICS_TRACKING_ID='UA-123456-1')
def test_course_id_with_event_label(self):
pixel = GoogleAnalyticsTrackingPixel(course_id='foo/bar/baz', event_label='test_label')
self.assertIsNotNone(pixel.image_url)
self.assert_query_string_parameters_equal(pixel.image_url, el='test_label')
from urlparse import parse_qs
import attr
from django.utils.http import urlencode
from openedx.core.djangoapps.schedules.utils import get_config_value_from_site_or_settings
DEFAULT_CAMPAIGN_SOURCE = 'ace'
DEFAULT_CAMPAIGN_MEDIUM = 'email'
@attr.s
class CampaignTrackingInfo(object):
"""
A struct for storing the set of UTM parameters that are recognized by tracking tools when included in URLs.
"""
source = attr.ib(default=DEFAULT_CAMPAIGN_SOURCE)
medium = attr.ib(default=DEFAULT_CAMPAIGN_MEDIUM)
campaign = attr.ib(default=None)
term = attr.ib(default=None)
content = attr.ib(default=None)
def to_query_string(self, existing_query_string=None):
"""
Generate a query string that includes the tracking parameters in addition to any existing parameters.
Note that any existing UTM parameters will be overridden by the values in this instance of CampaignTrackingInfo.
Args:
existing_query_string (str): An existing query string that needs to be updated to include this tracking
information.
Returns:
str: The URL encoded string that should be used as the query string in the URL.
"""
parameters = {}
if existing_query_string is not None:
parameters = parse_qs(existing_query_string)
for attribute, value in attr.asdict(self).iteritems():
if value is not None:
parameters['utm_' + attribute] = [value]
return urlencode(parameters, doseq=True)
@attr.s
class GoogleAnalyticsTrackingPixel(object):
"""
Implementation of the Google Analytics measurement protocol for email tracking.
See this document for more info: https://developers.google.com/analytics/devguides/collection/protocol/v1/email
"""
ANONYMOUS_USER_CLIENT_ID = 555
site = attr.ib(default=None)
course_id = attr.ib(default=None)
version = attr.ib(default=1, metadata={'param_name': 'v'})
hit_type = attr.ib(default='event', metadata={'param_name': 't'})
campaign_source = attr.ib(default=DEFAULT_CAMPAIGN_SOURCE, metadata={'param_name': 'cs'})
campaign_medium = attr.ib(default=DEFAULT_CAMPAIGN_MEDIUM, metadata={'param_name': 'cm'})
campaign_name = attr.ib(default=None, metadata={'param_name': 'cn'})
campaign_content = attr.ib(default=None, metadata={'param_name': 'cc'})
event_category = attr.ib(default='email', metadata={'param_name': 'ec'})
event_action = attr.ib(default='edx.bi.email.opened', metadata={'param_name': 'ea'})
event_label = attr.ib(default=None, metadata={'param_name': 'el'})
document_path = attr.ib(default=None, metadata={'param_name': 'dp'})
user_id = attr.ib(default=None, metadata={'param_name': 'uid'})
client_id = attr.ib(default=ANONYMOUS_USER_CLIENT_ID, metadata={'param_name': 'cid'})
@property
def image_url(self):
"""
A URL to a clear image that can be embedded in HTML documents to track email open events.
The query string of this URL is used to capture data about the email and visitor.
"""
parameters = {}
fields = attr.fields(self.__class__)
for attribute in fields:
value = getattr(self, attribute.name, None)
if value is not None and 'param_name' in attribute.metadata:
parameter_name = attribute.metadata['param_name']
parameters[parameter_name] = str(value)
tracking_id = get_config_value_from_site_or_settings("GOOGLE_ANALYTICS_ACCOUNT", site=self.site)
if tracking_id is None:
tracking_id = get_config_value_from_site_or_settings("GOOGLE_ANALYTICS_TRACKING_ID", site=self.site)
if tracking_id is None:
return None
parameters['tid'] = tracking_id
user_id_dimension = get_config_value_from_site_or_settings("GOOGLE_ANALYTICS_USER_ID_CUSTOM_DIMENSION", site=self.site)
if user_id_dimension is not None and self.user_id is not None:
parameter_name = 'cd{0}'.format(user_id_dimension)
parameters[parameter_name] = self.user_id
if self.course_id is not None and self.event_label is None:
param_name = fields.event_label.metadata['param_name']
parameters[param_name] = unicode(self.course_id)
return u"https://www.google-analytics.com/collect?{params}".format(params=urlencode(parameters))
import logging
from django.conf import settings
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from openedx.core.djangoapps.theming.helpers import get_current_site
LOG = logging.getLogger(__name__)
......@@ -15,3 +20,37 @@ class PrefixedDebugLoggerMixin(object):
def log_debug(self, message, *args, **kwargs):
LOG.debug(self.log_prefix + ': ' + message, *args, **kwargs)
def get_config_value_from_site_or_settings(name, site=None, site_config_name=None):
"""
Given a configuration setting name, try to get it from the site configuration and then fall back on the settings.
If site_config_name is not specified then "name" is used as the key for both collections.
Args:
name (str): The name of the setting to get the value of.
site: The site that we are trying to fetch the value for.
site_config_name: The name of the setting within the site configuration.
Returns:
The value stored in the configuration.
"""
if site_config_name is None:
site_config_name = name
if site is None:
site = get_current_site()
site_configuration = None
if site is not None:
try:
site_configuration = getattr(site, "configuration", None)
except SiteConfiguration.DoesNotExist:
pass
value_from_settings = getattr(settings, name, None)
if site_configuration is not None:
return site_configuration.get_value(site_config_name, default=value_from_settings)
else:
return value_from_settings
{% load i18n %}
{% load ace %}
{% get_current_language as LANGUAGE_CODE %}
{% get_current_language_bidi as LANGUAGE_BIDI %}
......@@ -25,6 +26,8 @@
{# 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" />
{% google_analytics_tracking_pixel %}
<div bgcolor="#f5f5f5" lang="{{ LANGUAGE_CODE|default:"en" }}" dir="{{ LANGUAGE_BIDI|yesno:"rtl,ltr" }}" style="
margin: 0;
padding: 0;
......@@ -59,12 +62,12 @@
<table role="presentation" width="100%" align="left" border="0" cellpadding="0" cellspacing="0">
<tr>
<td width="70">
<a href="{{ homepage_url }}"><img
<a href="{% with_link_tracking 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>
<a class="login" href="{% with_link_tracking dashboard_url %}" style="color: #960909;">{% trans "Sign In" %}</a>
</td>
</tr>
</table>
......@@ -92,7 +95,7 @@
<tr>
{% if social_media_urls.linkedin %}
<td height="32" width="42">
<a href="{{ social_media_urls.linkedin }}">
<a href="{{ social_media_urls.linkedin|safe }}">
<img src="https://media.sailthru.com/595/1k1/8/o/599f354ec70cb.png"
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on LinkedIn{% endblocktrans %}"/>
</a>
......@@ -100,7 +103,7 @@
{% endif %}
{% if social_media_urls.twitter %}
<td height="32" width="42">
<a href="{{ social_media_urls.twitter }}">
<a href="{{ social_media_urls.twitter|safe }}">
<img src="https://media.sailthru.com/595/1k1/8/o/599f354d9c26e.png"
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on Twitter{% endblocktrans %}"/>
</a>
......@@ -108,7 +111,7 @@
{% endif %}
{% if social_media_urls.facebook %}
<td height="32" width="42">
<a href="{{ social_media_urls.facebook }}">
<a href="{{ social_media_urls.facebook|safe }}">
<img src="https://media.sailthru.com/595/1k1/8/o/599f355052c8e.png"
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on Facebook{% endblocktrans %}"/>
</a>
......@@ -116,7 +119,7 @@
{% endif %}
{% if social_media_urls.google_plus %}
<td height="32" width="42">
<a href="{{ social_media_urls.google_plus }}">
<a href="{{ social_media_urls.google_plus|safe }}">
<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>
......@@ -124,7 +127,7 @@
{% endif %}
{% if social_media_urls.reddit %}
<td height="32" width="42">
<a href="{{ social_media_urls.reddit }}">
<a href="{{ social_media_urls.reddit|safe }}">
<img src="https://media.sailthru.com/595/1k1/8/o/599f354e326b9.png"
width="32" height="32" alt="{% blocktrans %}{{ platform_name }} on Reddit{% endblocktrans %}"/>
</a>
......@@ -138,14 +141,14 @@
<!-- APP BUTTONS -->
<td style="padding-bottom: 20px;">
{% if mobile_store_urls.apple %}
<a href="{{ mobile_store_urls.apple }}" style="text-decoration: none">
<a href="{{ mobile_store_urls.apple|safe }}" 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">
<a href="{{ mobile_store_urls.google|safe }}" 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"/>
......
{% load i18n %}
{% load ace %}
<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 }}"
{% if course_cta_url %}
href="{% with_link_tracking course_cta_url %}"
{% else %}
href="{{ course_url }}"
{%if course_ids|length > 1 %}
href="{% with_link_tracking dashboard_url %}"
{% else %}
href="{% with_link_tracking course_url %}"
{% endif %}
{% endif %}
style="
color: #ffffff;
......
{% load i18n %}
{% load ace %}
This is the RED theme!
{% if course_ids|length > 1 %}
......@@ -7,13 +8,13 @@ This is the RED theme!
to have you! Come see what everyone is learning.
{% endblocktrans %}
{% trans "Start learning now" %} <{{ dashboard_url }}>
{% trans "Start learning now" %} <{% with_link_tracking 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 }}>
{% trans "Start learning now" %} <{% with_link_tracking course_url %}>
{% endif %}
{% include "schedules/edx_ace/common/upsell_cta.txt"%}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment