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.
This commit is contained in:
@@ -20,8 +20,9 @@ except Exception:
|
||||
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
|
||||
in, but should not be cached when the user is logged in (because
|
||||
of the navigation bar at the top with the username).
|
||||
@@ -31,32 +32,46 @@ def cache_if_anonymous(view_func):
|
||||
the cookie to the vary header, and so every page is cached seperately
|
||||
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
|
||||
contain the csrftoken within the html. The csrf token can be included
|
||||
in the header by ordering the decorators as such:
|
||||
|
||||
@ensure_csrftoken
|
||||
@cache_if_anonymous
|
||||
@cache_if_anonymous()
|
||||
def myView(request):
|
||||
"""
|
||||
|
||||
@wraps(view_func)
|
||||
def _decorated(request, *args, **kwargs):
|
||||
if not request.user.is_authenticated():
|
||||
#Use the cache
|
||||
# same view accessed through different domain names may
|
||||
# return different things, so include the domain name in the key.
|
||||
domain = str(request.META.get('HTTP_HOST')) + '.'
|
||||
cache_key = domain + "cache_if_anonymous." + get_language() + '.' + request.path
|
||||
response = cache.get(cache_key)
|
||||
if not response:
|
||||
response = view_func(request, *args, **kwargs)
|
||||
cache.set(cache_key, response, 60 * 3)
|
||||
def decorator(view_func):
|
||||
"""The outer wrapper, used to allow the decorator to take optional
|
||||
arguments.
|
||||
"""
|
||||
@wraps(view_func)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
"""The inner wrapper, which wraps the view function."""
|
||||
if not request.user.is_authenticated():
|
||||
#Use the cache
|
||||
# same view accessed through different domain names may
|
||||
# return different things, so include the domain name in the key.
|
||||
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.
|
||||
for get_parameter in get_parameters:
|
||||
cache_key = cache_key + '.' + unicode(request.GET.get(get_parameter))
|
||||
|
||||
else:
|
||||
#Don't use the cache
|
||||
return view_func(request, *args, **kwargs)
|
||||
response = cache.get(cache_key) # pylint: disable=maybe-no-member
|
||||
if not response:
|
||||
response = view_func(request, *args, **kwargs)
|
||||
cache.set(cache_key, response, 60 * 3) # pylint: disable=maybe-no-member
|
||||
|
||||
return _decorated
|
||||
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):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
@cache_if_anonymous()
|
||||
def index(request):
|
||||
'''
|
||||
Redirects to main page -- info page if user authenticated, or marketing if not
|
||||
@@ -81,7 +81,7 @@ def index(request):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
@cache_if_anonymous()
|
||||
def courses(request):
|
||||
"""
|
||||
Render the "find courses" page. If the marketing site is enabled, redirect
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Tests courseware views.py
|
||||
"""
|
||||
import unittest
|
||||
import cgi
|
||||
from datetime import datetime
|
||||
|
||||
from mock import MagicMock, patch, create_autospec
|
||||
@@ -99,6 +100,10 @@ class ViewsTestCase(TestCase):
|
||||
chapter = 'Overview'
|
||||
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")
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})
|
||||
def test_course_about_in_cart(self):
|
||||
@@ -256,17 +261,26 @@ class ViewsTestCase(TestCase):
|
||||
# generate/store a real password.
|
||||
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):
|
||||
# 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'})
|
||||
response = self.client.get(url)
|
||||
response = self.client.get(url, {'organization_full_name': self.organization_full_name})
|
||||
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):
|
||||
response = self._load_mktg_about()
|
||||
response = self._load_mktg_about(organization_full_name=self.organization_full_name)
|
||||
self.assertIn('Enroll in', 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):
|
||||
CourseMode.objects.get_or_create(
|
||||
mode_slug='honor',
|
||||
@@ -279,12 +293,42 @@ class ViewsTestCase(TestCase):
|
||||
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('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
|
||||
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})
|
||||
def test_mktg_about_language_edx_domain(self):
|
||||
# Since we're in an edx-controlled domain, and our marketing site
|
||||
@@ -340,9 +384,8 @@ class ViewsTestCase(TestCase):
|
||||
response = self.client.get(url)
|
||||
self.assertFalse('<script>' in response.content)
|
||||
|
||||
def _load_mktg_about(self, language=None):
|
||||
"""
|
||||
Retrieve the marketing about button (iframed into the marketing site)
|
||||
def _load_mktg_about(self, language=None, organization_full_name=None):
|
||||
"""Retrieve the marketing about button (iframed into the marketing site)
|
||||
and return the HTTP response.
|
||||
|
||||
Keyword Args:
|
||||
@@ -362,7 +405,22 @@ class ViewsTestCase(TestCase):
|
||||
headers['HTTP_ACCEPT_LANGUAGE'] = language
|
||||
|
||||
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
|
||||
|
||||
@@ -5,6 +5,7 @@ Courseware views functions
|
||||
import logging
|
||||
import urllib
|
||||
import json
|
||||
import cgi
|
||||
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
@@ -93,7 +94,7 @@ def user_groups(user):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
@cache_if_anonymous()
|
||||
def courses(request):
|
||||
"""
|
||||
Render "find courses" page. The course selection work is done in courseware.courses.
|
||||
@@ -713,7 +714,7 @@ def registered_for_course(course, user):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
@cache_if_anonymous()
|
||||
def course_about(request, course_id):
|
||||
"""
|
||||
Display the course's about page.
|
||||
@@ -802,13 +803,10 @@ def course_about(request, course_id):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
@cache_if_anonymous('organization_full_name')
|
||||
@ensure_valid_course_key
|
||||
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)
|
||||
|
||||
try:
|
||||
@@ -818,8 +816,7 @@ def mktg_course_about(request, course_id):
|
||||
)
|
||||
course = get_course_with_access(request.user, permission_name, course_key)
|
||||
except (ValueError, Http404):
|
||||
# if a course does not exist yet, display a coming
|
||||
# soon button
|
||||
# If a course does not exist yet, display a "Coming Soon" button
|
||||
return render_to_response(
|
||||
'courseware/mktg_coming_soon.html', {'course_id': course_key.to_deprecated_string()}
|
||||
)
|
||||
@@ -846,6 +843,12 @@ def mktg_course_about(request, course_id):
|
||||
'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.
|
||||
# To avoid displaying a different language in the register / access button,
|
||||
# we force the language to English.
|
||||
|
||||
@@ -30,7 +30,7 @@ def index(request, template):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
@cache_if_anonymous()
|
||||
def render(request, template):
|
||||
"""
|
||||
This view function renders the template sent without checking that it
|
||||
@@ -43,7 +43,7 @@ def render(request, template):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
@cache_if_anonymous()
|
||||
def render_press_release(request, slug):
|
||||
"""
|
||||
Render a press release given a slug. Similar to the "render" function above,
|
||||
|
||||
@@ -283,6 +283,9 @@ FEATURES = {
|
||||
# Enable the combined login/registration form
|
||||
'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
|
||||
# to allow an upload of a CSV file that contains a list of new accounts to create
|
||||
# and register for course.
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
$(".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();
|
||||
event.preventDefault();
|
||||
});
|
||||
@@ -29,7 +37,9 @@
|
||||
window.top.location.href = "${reverse('dashboard')}";
|
||||
}
|
||||
} 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 {
|
||||
$('#register_error').html(
|
||||
(xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.")}")
|
||||
@@ -71,6 +81,23 @@
|
||||
</span>
|
||||
%endif
|
||||
</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:
|
||||
<div class="action registration-closed is-disabled">${_("Enrollment Is Closed")}</div>
|
||||
%endif
|
||||
@@ -83,6 +110,7 @@
|
||||
<fieldset class="enroll_fieldset">
|
||||
<input name="course_id" type="hidden" value="${course.id | h}">
|
||||
<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 }">
|
||||
</fieldset>
|
||||
<div class="submit">
|
||||
|
||||
Reference in New Issue
Block a user