Commit f5767a96 by Renzo Lucioni

Prep marketing iframe and relevant courseware view for email opt-in

Feature flagged. Puts a checkbox in the iframe. The iframe uses an organization_full_name parameter forwarded from Drupal by the courseware views and POSTs an email_opt_in parameter to the student views, preserving it on 403.
parent eacd5256
...@@ -20,8 +20,9 @@ except Exception: ...@@ -20,8 +20,9 @@ except Exception:
cache = cache.cache cache = cache.cache
def cache_if_anonymous(view_func): def cache_if_anonymous(*get_parameters):
""" """Cache a page for anonymous users.
Many of the pages in edX are identical when the user is not logged Many of the pages in edX are identical when the user is not logged
in, but should not be cached when the user is logged in (because in, but should not be cached when the user is logged in (because
of the navigation bar at the top with the username). of the navigation bar at the top with the username).
...@@ -31,32 +32,46 @@ def cache_if_anonymous(view_func): ...@@ -31,32 +32,46 @@ def cache_if_anonymous(view_func):
the cookie to the vary header, and so every page is cached seperately the cookie to the vary header, and so every page is cached seperately
for each user (because each user has a different csrf token). for each user (because each user has a different csrf token).
Optionally, provide a series of GET parameters as arguments to cache
pages with these GET parameters separately.
Note that this decorator should only be used on views that do not Note that this decorator should only be used on views that do not
contain the csrftoken within the html. The csrf token can be included contain the csrftoken within the html. The csrf token can be included
in the header by ordering the decorators as such: in the header by ordering the decorators as such:
@ensure_csrftoken @ensure_csrftoken
@cache_if_anonymous @cache_if_anonymous()
def myView(request): def myView(request):
""" """
@wraps(view_func) def decorator(view_func):
def _decorated(request, *args, **kwargs): """The outer wrapper, used to allow the decorator to take optional
if not request.user.is_authenticated(): arguments.
#Use the cache """
# same view accessed through different domain names may @wraps(view_func)
# return different things, so include the domain name in the key. def wrapper(request, *args, **kwargs):
domain = str(request.META.get('HTTP_HOST')) + '.' """The inner wrapper, which wraps the view function."""
cache_key = domain + "cache_if_anonymous." + get_language() + '.' + request.path if not request.user.is_authenticated():
response = cache.get(cache_key) #Use the cache
if not response: # same view accessed through different domain names may
response = view_func(request, *args, **kwargs) # return different things, so include the domain name in the key.
cache.set(cache_key, response, 60 * 3) domain = str(request.META.get('HTTP_HOST')) + '.'
cache_key = domain + "cache_if_anonymous." + get_language() + '.' + request.path
return response
# Include the values of GET parameters in the cache key.
else: for get_parameter in get_parameters:
#Don't use the cache cache_key = cache_key + '.' + unicode(request.GET.get(get_parameter))
return view_func(request, *args, **kwargs)
response = cache.get(cache_key) # pylint: disable=maybe-no-member
return _decorated if not response:
response = view_func(request, *args, **kwargs)
cache.set(cache_key, response, 60 * 3) # pylint: disable=maybe-no-member
return response
else:
#Don't use the cache
return view_func(request, *args, **kwargs)
return wrapper
return decorator
...@@ -33,7 +33,7 @@ def get_course_enrollments(user): ...@@ -33,7 +33,7 @@ def get_course_enrollments(user):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def index(request): def index(request):
''' '''
Redirects to main page -- info page if user authenticated, or marketing if not Redirects to main page -- info page if user authenticated, or marketing if not
...@@ -81,7 +81,7 @@ def index(request): ...@@ -81,7 +81,7 @@ def index(request):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def courses(request): def courses(request):
""" """
Render the "find courses" page. If the marketing site is enabled, redirect Render the "find courses" page. If the marketing site is enabled, redirect
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
Tests courseware views.py Tests courseware views.py
""" """
import unittest import unittest
import cgi
from datetime import datetime from datetime import datetime
from mock import MagicMock, patch, create_autospec from mock import MagicMock, patch, create_autospec
...@@ -99,6 +100,10 @@ class ViewsTestCase(TestCase): ...@@ -99,6 +100,10 @@ class ViewsTestCase(TestCase):
chapter = 'Overview' chapter = 'Overview'
self.chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter) self.chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter)
# For marketing email opt-in
self.organization_full_name = u"𝖀𝖒𝖇𝖗𝖊𝖑𝖑𝖆 𝕮𝖔𝖗𝖕𝖔𝖗𝖆𝖙𝖎𝖔𝖓"
self.organization_html = "<p>'+Umbrella/Corporation+'</p>"
@unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings") @unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings")
@patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True}) @patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})
def test_course_about_in_cart(self): def test_course_about_in_cart(self):
...@@ -256,17 +261,26 @@ class ViewsTestCase(TestCase): ...@@ -256,17 +261,26 @@ class ViewsTestCase(TestCase):
# generate/store a real password. # generate/store a real password.
self.assertEqual(chat_settings['password'], "johndoe@%s" % domain) self.assertEqual(chat_settings['password'], "johndoe@%s" % domain)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
def test_course_mktg_about_coming_soon(self): def test_course_mktg_about_coming_soon(self):
# we should not be able to find this course # We should not be able to find this course
url = reverse('mktg_about_course', kwargs={'course_id': 'no/course/here'}) url = reverse('mktg_about_course', kwargs={'course_id': 'no/course/here'})
response = self.client.get(url) response = self.client.get(url, {'organization_full_name': self.organization_full_name})
self.assertIn('Coming Soon', response.content) self.assertIn('Coming Soon', response.content)
# Verify that the checkbox is not displayed
self._email_opt_in_checkbox(response)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
def test_course_mktg_register(self): def test_course_mktg_register(self):
response = self._load_mktg_about() response = self._load_mktg_about(organization_full_name=self.organization_full_name)
self.assertIn('Enroll in', response.content) self.assertIn('Enroll in', response.content)
self.assertNotIn('and choose your student track', response.content) self.assertNotIn('and choose your student track', response.content)
# Verify that the checkbox is displayed
self._email_opt_in_checkbox(response, self.organization_full_name)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
def test_course_mktg_register_multiple_modes(self): def test_course_mktg_register_multiple_modes(self):
CourseMode.objects.get_or_create( CourseMode.objects.get_or_create(
mode_slug='honor', mode_slug='honor',
...@@ -279,12 +293,42 @@ class ViewsTestCase(TestCase): ...@@ -279,12 +293,42 @@ class ViewsTestCase(TestCase):
course_id=self.course_key course_id=self.course_key
) )
response = self._load_mktg_about() response = self._load_mktg_about(organization_full_name=self.organization_full_name)
self.assertIn('Enroll in', response.content) self.assertIn('Enroll in', response.content)
self.assertIn('and choose your student track', response.content) self.assertIn('and choose your student track', response.content)
# Verify that the checkbox is displayed
self._email_opt_in_checkbox(response, self.organization_full_name)
# clean up course modes # clean up course modes
CourseMode.objects.all().delete() CourseMode.objects.all().delete()
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
def test_course_mktg_no_organization_name(self):
# Don't pass an organization name as a GET parameter, even though the email
# opt-in feature is enabled.
response = response = self._load_mktg_about()
# Verify that the checkbox is not displayed
self._email_opt_in_checkbox(response)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': False})
def test_course_mktg_opt_in_disabled(self):
# Pass an organization name as a GET parameter, even though the email
# opt-in feature is disabled.
response = self._load_mktg_about(organization_full_name=self.organization_full_name)
# Verify that the checkbox is not displayed
self._email_opt_in_checkbox(response)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
def test_course_mktg_organization_html(self):
response = self._load_mktg_about(organization_full_name=self.organization_html)
# Verify that the checkbox is displayed with the organization name
# in the label escaped as expected.
self._email_opt_in_checkbox(response, cgi.escape(self.organization_html))
@patch.dict(settings.FEATURES, {'IS_EDX_DOMAIN': True}) @patch.dict(settings.FEATURES, {'IS_EDX_DOMAIN': True})
def test_mktg_about_language_edx_domain(self): def test_mktg_about_language_edx_domain(self):
# Since we're in an edx-controlled domain, and our marketing site # Since we're in an edx-controlled domain, and our marketing site
...@@ -340,9 +384,8 @@ class ViewsTestCase(TestCase): ...@@ -340,9 +384,8 @@ class ViewsTestCase(TestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertFalse('<script>' in response.content) self.assertFalse('<script>' in response.content)
def _load_mktg_about(self, language=None): def _load_mktg_about(self, language=None, organization_full_name=None):
""" """Retrieve the marketing about button (iframed into the marketing site)
Retrieve the marketing about button (iframed into the marketing site)
and return the HTTP response. and return the HTTP response.
Keyword Args: Keyword Args:
...@@ -362,7 +405,22 @@ class ViewsTestCase(TestCase): ...@@ -362,7 +405,22 @@ class ViewsTestCase(TestCase):
headers['HTTP_ACCEPT_LANGUAGE'] = language headers['HTTP_ACCEPT_LANGUAGE'] = language
url = reverse('mktg_about_course', kwargs={'course_id': unicode(self.course_key)}) url = reverse('mktg_about_course', kwargs={'course_id': unicode(self.course_key)})
return self.client.get(url, **headers) if organization_full_name:
return self.client.get(url, {'organization_full_name': organization_full_name}, **headers)
else:
return self.client.get(url, **headers)
def _email_opt_in_checkbox(self, response, organization_full_name=None):
"""Check if the email opt-in checkbox appears in the response content."""
checkbox_html = '<input id="email-opt-in" type="checkbox" name="opt-in" class="email-opt-in" value="true" checked>'
if organization_full_name:
# Verify that the email opt-in checkbox appears, and that the expected
# organization name is displayed.
self.assertContains(response, checkbox_html, html=True)
self.assertContains(response, organization_full_name)
else:
# Verify that the email opt-in checkbox does not appear
self.assertNotContains(response, checkbox_html, html=True)
# setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly # setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly
......
...@@ -5,6 +5,7 @@ Courseware views functions ...@@ -5,6 +5,7 @@ Courseware views functions
import logging import logging
import urllib import urllib
import json import json
import cgi
from datetime import datetime from datetime import datetime
from collections import defaultdict from collections import defaultdict
...@@ -93,7 +94,7 @@ def user_groups(user): ...@@ -93,7 +94,7 @@ def user_groups(user):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def courses(request): def courses(request):
""" """
Render "find courses" page. The course selection work is done in courseware.courses. Render "find courses" page. The course selection work is done in courseware.courses.
...@@ -713,7 +714,7 @@ def registered_for_course(course, user): ...@@ -713,7 +714,7 @@ def registered_for_course(course, user):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def course_about(request, course_id): def course_about(request, course_id):
""" """
Display the course's about page. Display the course's about page.
...@@ -802,13 +803,10 @@ def course_about(request, course_id): ...@@ -802,13 +803,10 @@ def course_about(request, course_id):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous('organization_full_name')
@ensure_valid_course_key @ensure_valid_course_key
def mktg_course_about(request, course_id): def mktg_course_about(request, course_id):
""" """This is the button that gets put into an iframe on the Drupal site."""
This is the button that gets put into an iframe on the Drupal site
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
try: try:
...@@ -818,8 +816,7 @@ def mktg_course_about(request, course_id): ...@@ -818,8 +816,7 @@ def mktg_course_about(request, course_id):
) )
course = get_course_with_access(request.user, permission_name, course_key) course = get_course_with_access(request.user, permission_name, course_key)
except (ValueError, Http404): except (ValueError, Http404):
# if a course does not exist yet, display a coming # If a course does not exist yet, display a "Coming Soon" button
# soon button
return render_to_response( return render_to_response(
'courseware/mktg_coming_soon.html', {'course_id': course_key.to_deprecated_string()} 'courseware/mktg_coming_soon.html', {'course_id': course_key.to_deprecated_string()}
) )
...@@ -846,6 +843,12 @@ def mktg_course_about(request, course_id): ...@@ -846,6 +843,12 @@ def mktg_course_about(request, course_id):
'course_modes': course_modes, 'course_modes': course_modes,
} }
if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'):
# Drupal will pass the organization's full name as a GET parameter. If no full name
# is provided, the marketing iframe won't show the email opt-in checkbox.
organization_full_name = request.GET.get('organization_full_name')
context['organization_full_name'] = cgi.escape(organization_full_name) if organization_full_name else organization_full_name
# The edx.org marketing site currently displays only in English. # The edx.org marketing site currently displays only in English.
# To avoid displaying a different language in the register / access button, # To avoid displaying a different language in the register / access button,
# we force the language to English. # we force the language to English.
......
...@@ -30,7 +30,7 @@ def index(request, template): ...@@ -30,7 +30,7 @@ def index(request, template):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def render(request, template): def render(request, template):
""" """
This view function renders the template sent without checking that it This view function renders the template sent without checking that it
...@@ -43,7 +43,7 @@ def render(request, template): ...@@ -43,7 +43,7 @@ def render(request, template):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous()
def render_press_release(request, slug): def render_press_release(request, slug):
""" """
Render a press release given a slug. Similar to the "render" function above, Render a press release given a slug. Similar to the "render" function above,
......
...@@ -283,6 +283,9 @@ FEATURES = { ...@@ -283,6 +283,9 @@ FEATURES = {
# Enable the combined login/registration form # Enable the combined login/registration form
'ENABLE_COMBINED_LOGIN_REGISTRATION': False, 'ENABLE_COMBINED_LOGIN_REGISTRATION': False,
# Enable organizational email opt-in
'ENABLE_MKTG_EMAIL_OPT_IN': False,
# Show a section in the membership tab of the instructor dashboard # Show a section in the membership tab of the instructor dashboard
# to allow an upload of a CSV file that contains a list of new accounts to create # to allow an upload of a CSV file that contains a list of new accounts to create
# and register for course. # and register for course.
......
...@@ -16,6 +16,14 @@ ...@@ -16,6 +16,14 @@
<script type="text/javascript"> <script type="text/javascript">
(function() { (function() {
$(".register").click(function(event) { $(".register").click(function(event) {
if ( !$("#email-opt-in").prop("checked") ) {
$("input[name='email_opt_in']").val("false");
}
var email_opt_in = $("input[name='email_opt_in']").val(),
current_href = $("a.register").attr("href");
$("a.register").attr("href", current_href + "&email_opt_in=" + email_opt_in)
$("#class_enroll_form").submit(); $("#class_enroll_form").submit();
event.preventDefault(); event.preventDefault();
}); });
...@@ -29,7 +37,9 @@ ...@@ -29,7 +37,9 @@
window.top.location.href = "${reverse('dashboard')}"; window.top.location.href = "${reverse('dashboard')}";
} }
} else if (xhr.status == 403) { } else if (xhr.status == 403) {
window.top.location.href = $("a.register").attr("href") || "${reverse('register_user')}?course_id=${course.id | u}&enrollment_action=enroll"; var email_opt_in = $("input[name='email_opt_in']").val();
## Ugh.
window.top.location.href = $("a.register").attr("href") || "${reverse('register_user')}?course_id=${course.id | u}&enrollment_action=enroll&email_opt_in=" + email_opt_in;
} else { } else {
$('#register_error').html( $('#register_error').html(
(xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.")}") (xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.")}")
...@@ -71,6 +81,23 @@ ...@@ -71,6 +81,23 @@
</span> </span>
%endif %endif
</a> </a>
% if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'):
## We only display the email opt-in checkbox if we've been given a valid full name (i.e., not None)
% if organization_full_name:
<p class="form-field">
<input id="email-opt-in" type="checkbox" name="opt-in" class="email-opt-in" value="true" checked>
<label for="email-opt-in">
## Translators: This line appears next a checkbox which users can leave checked or uncheck in order
## to indicate whether they want to receive emails from the organization offering the course.
${_("I would like to receive email about other {organization_full_name} programs and offers.").format(
organization_full_name=organization_full_name
)}
</label>
</p>
% endif
% endif
%else: %else:
<div class="action registration-closed is-disabled">${_("Enrollment Is Closed")}</div> <div class="action registration-closed is-disabled">${_("Enrollment Is Closed")}</div>
%endif %endif
...@@ -83,6 +110,7 @@ ...@@ -83,6 +110,7 @@
<fieldset class="enroll_fieldset"> <fieldset class="enroll_fieldset">
<input name="course_id" type="hidden" value="${course.id | h}"> <input name="course_id" type="hidden" value="${course.id | h}">
<input name="enrollment_action" type="hidden" value="enroll"> <input name="enrollment_action" type="hidden" value="enroll">
<input name="email_opt_in" type="hidden" value="true">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }"> <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
</fieldset> </fieldset>
<div class="submit"> <div class="submit">
......
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