From f5767a961c3b07e8c4e49f41fbfc90d03f2b2fa0 Mon Sep 17 00:00:00 2001
From: Renzo Lucioni <renzo@renzolucioni.com>
Date: Mon, 24 Nov 2014 15:42:46 -0500
Subject: [PATCH] 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.
---
 common/djangoapps/util/cache.py               | 61 +++++++++------
 lms/djangoapps/branding/views.py              |  4 +-
 lms/djangoapps/courseware/tests/test_views.py | 74 +++++++++++++++++--
 lms/djangoapps/courseware/views.py            | 21 +++---
 lms/djangoapps/static_template_view/views.py  |  4 +-
 lms/envs/common.py                            |  3 +
 .../courseware/mktg_course_about.html         | 30 +++++++-
 7 files changed, 152 insertions(+), 45 deletions(-)

diff --git a/common/djangoapps/util/cache.py b/common/djangoapps/util/cache.py
index 7262fabd099..cede8acb715 100644
--- a/common/djangoapps/util/cache.py
+++ b/common/djangoapps/util/cache.py
@@ -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)
-
-            return response
-
-        else:
-            #Don't use the cache
-            return view_func(request, *args, **kwargs)
-
-    return _decorated
+    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
+
+                # 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))
+
+                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 response
+
+            else:
+                #Don't use the cache
+                return view_func(request, *args, **kwargs)
+
+        return wrapper
+    return decorator
diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py
index 667ab7a9e01..b79df70f557 100644
--- a/lms/djangoapps/branding/views.py
+++ b/lms/djangoapps/branding/views.py
@@ -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
diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py
index b13c77165a2..7556530daaa 100644
--- a/lms/djangoapps/courseware/tests/test_views.py
+++ b/lms/djangoapps/courseware/tests/test_views.py
@@ -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
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 5b0835d5bcf..c1ec2e3d381 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -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.
diff --git a/lms/djangoapps/static_template_view/views.py b/lms/djangoapps/static_template_view/views.py
index 4cff3c77ac7..ea2639795ba 100644
--- a/lms/djangoapps/static_template_view/views.py
+++ b/lms/djangoapps/static_template_view/views.py
@@ -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,
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 8e167c00741..7cd38d9d28a 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -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.
diff --git a/lms/templates/courseware/mktg_course_about.html b/lms/templates/courseware/mktg_course_about.html
index 3b63e61ebd4..be0be4e2613 100644
--- a/lms/templates/courseware/mktg_course_about.html
+++ b/lms/templates/courseware/mktg_course_about.html
@@ -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">
-- 
GitLab