Merge branch 'release'
Release 2014-12-10
This commit is contained in:
@@ -258,6 +258,8 @@ class ImportRequiredTestCases(ContentStoreTestCase):
|
||||
# and assert that they contain the created modules
|
||||
self.assertIn(self.DRAFT_HTML + ".xml", draft_dir.listdir('html'))
|
||||
self.assertIn(self.DRAFT_VIDEO + ".xml", draft_dir.listdir('video'))
|
||||
# and assert the child of the orphaned draft wasn't exported
|
||||
self.assertNotIn(self.ORPHAN_DRAFT_HTML + ".xml", draft_dir.listdir('html'))
|
||||
|
||||
# check for grading_policy.json
|
||||
filesystem = OSFS(root_dir / 'test_export/policies/2012_Fall')
|
||||
|
||||
@@ -134,6 +134,8 @@ class CourseTestCase(ModuleStoreTestCase):
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
TEST_VERTICAL = 'vertical_test'
|
||||
ORPHAN_DRAFT_VERTICAL = 'orphan_draft_vertical'
|
||||
ORPHAN_DRAFT_HTML = 'orphan_draft_html'
|
||||
PRIVATE_VERTICAL = 'a_private_vertical'
|
||||
PUBLISHED_VERTICAL = 'a_published_vertical'
|
||||
SEQUENTIAL = 'vertical_sequential'
|
||||
@@ -158,6 +160,18 @@ class CourseTestCase(ModuleStoreTestCase):
|
||||
self.assertEqual(orphan_vertical.location.name, 'no_references')
|
||||
self.assertEqual(len(orphan_vertical.children), len(vertical.children))
|
||||
|
||||
# create an orphan vertical and html; we already don't try to import
|
||||
# the orphaned vertical, but we should make sure we don't import
|
||||
# the orphaned vertical's child html, too
|
||||
orphan_draft_vertical = self.store.create_item(
|
||||
self.user.id, course_id, 'vertical', self.ORPHAN_DRAFT_VERTICAL
|
||||
)
|
||||
orphan_draft_html = self.store.create_item(
|
||||
self.user.id, course_id, 'html', self.ORPHAN_DRAFT_HTML
|
||||
)
|
||||
orphan_draft_vertical.children.append(orphan_draft_html.location)
|
||||
self.store.update_item(orphan_draft_vertical, self.user.id)
|
||||
|
||||
# create a Draft vertical
|
||||
vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL), depth=1)
|
||||
draft_vertical = self.store.convert_to_draft(vertical.location, self.user.id)
|
||||
|
||||
@@ -14,7 +14,7 @@ from third_party_auth import ( # pylint: disable=unused-import
|
||||
from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=F0401
|
||||
|
||||
|
||||
def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None):
|
||||
def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None, email_opt_in=None):
|
||||
"""Retrieve URLs for each enabled third-party auth provider.
|
||||
|
||||
These URLs are used on the "sign up" and "sign in" buttons
|
||||
@@ -40,6 +40,10 @@ def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None):
|
||||
Note that `redirect_url` takes precedence over the redirect
|
||||
to the track selection page.
|
||||
|
||||
email_opt_in (unicode): The user choice to opt in for organization wide emails. If set to 'true'
|
||||
(case insensitive), user will be opted into organization-wide email. All other values will
|
||||
be treated as False, and the user will be opted out of organization-wide email.
|
||||
|
||||
Returns:
|
||||
dict mapping provider names to URLs
|
||||
|
||||
@@ -72,6 +76,7 @@ def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None):
|
||||
provider.NAME: pipeline.get_login_url(
|
||||
provider.NAME, auth_entry,
|
||||
enroll_course_id=course_id,
|
||||
email_opt_in=email_opt_in,
|
||||
redirect_url=pipeline_redirect
|
||||
)
|
||||
for provider in provider.Registry.enabled()
|
||||
|
||||
@@ -373,7 +373,7 @@ def signin_user(request):
|
||||
# party auth pipeline; distinct from the actual instance of the running
|
||||
# pipeline, if any.
|
||||
'pipeline_running': 'true' if pipeline.running(request) else 'false',
|
||||
'pipeline_url': auth_pipeline_urls(pipeline.AUTH_ENTRY_LOGIN, course_id=course_id),
|
||||
'pipeline_url': auth_pipeline_urls(pipeline.AUTH_ENTRY_LOGIN, course_id=course_id, email_opt_in=email_opt_in),
|
||||
'platform_name': microsite.get_value(
|
||||
'platform_name',
|
||||
settings.PLATFORM_NAME
|
||||
@@ -405,7 +405,7 @@ def register_user(request, extra_context=None):
|
||||
'enrollment_action': request.GET.get('enrollment_action'),
|
||||
'name': '',
|
||||
'running_pipeline': None,
|
||||
'pipeline_urls': auth_pipeline_urls(pipeline.AUTH_ENTRY_REGISTER, course_id=course_id),
|
||||
'pipeline_urls': auth_pipeline_urls(pipeline.AUTH_ENTRY_REGISTER, course_id=course_id, email_opt_in=email_opt_in),
|
||||
'platform_name': microsite.get_value(
|
||||
'platform_name',
|
||||
settings.PLATFORM_NAME
|
||||
|
||||
@@ -101,10 +101,12 @@ from . import provider
|
||||
# `AUTH_ENROLL_COURSE_ID_KEY` provides the course ID that a student
|
||||
# is trying to enroll in, used to generate analytics events
|
||||
# and auto-enroll students.
|
||||
from user_api.api import profile
|
||||
|
||||
AUTH_ENTRY_KEY = 'auth_entry'
|
||||
AUTH_REDIRECT_KEY = 'next'
|
||||
AUTH_ENROLL_COURSE_ID_KEY = 'enroll_course_id'
|
||||
AUTH_EMAIL_OPT_IN_KEY = 'email_opt_in'
|
||||
|
||||
AUTH_ENTRY_DASHBOARD = 'dashboard'
|
||||
AUTH_ENTRY_LOGIN = 'login'
|
||||
@@ -250,7 +252,7 @@ def _get_enabled_provider_by_name(provider_name):
|
||||
return enabled_provider
|
||||
|
||||
|
||||
def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, enroll_course_id=None):
|
||||
def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, enroll_course_id=None, email_opt_in=None):
|
||||
"""Creates a URL to hook into social auth endpoints."""
|
||||
kwargs = {'backend': backend_name}
|
||||
url = reverse(view_name, kwargs=kwargs)
|
||||
@@ -265,6 +267,9 @@ def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, enroll
|
||||
if enroll_course_id:
|
||||
query_params[AUTH_ENROLL_COURSE_ID_KEY] = enroll_course_id
|
||||
|
||||
if email_opt_in:
|
||||
query_params[AUTH_EMAIL_OPT_IN_KEY] = email_opt_in
|
||||
|
||||
return u"{url}?{params}".format(
|
||||
url=url,
|
||||
params=urllib.urlencode(query_params)
|
||||
@@ -309,7 +314,7 @@ def get_disconnect_url(provider_name):
|
||||
return _get_url('social:disconnect', enabled_provider.BACKEND_CLASS.name)
|
||||
|
||||
|
||||
def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id=None):
|
||||
def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id=None, email_opt_in=None):
|
||||
"""Gets the login URL for the endpoint that kicks off auth with a provider.
|
||||
|
||||
Args:
|
||||
@@ -326,6 +331,11 @@ def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id
|
||||
enroll_course_id (string): If provided, auto-enroll the user in this
|
||||
course upon successful authentication.
|
||||
|
||||
email_opt_in (string): If set to 'true' (case insensitive), user will
|
||||
be opted into organization-wide email. Any other string will
|
||||
equate to False, and the user will be opted out of organization-wide
|
||||
email.
|
||||
|
||||
Returns:
|
||||
String. URL that starts the auth pipeline for a provider.
|
||||
|
||||
@@ -339,7 +349,8 @@ def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id
|
||||
enabled_provider.BACKEND_CLASS.name,
|
||||
auth_entry=auth_entry,
|
||||
redirect_url=redirect_url,
|
||||
enroll_course_id=enroll_course_id
|
||||
enroll_course_id=enroll_course_id,
|
||||
email_opt_in=email_opt_in
|
||||
)
|
||||
|
||||
|
||||
@@ -425,7 +436,6 @@ def running(request):
|
||||
def parse_query_params(strategy, response, *args, **kwargs):
|
||||
"""Reads whitelisted query params, transforms them into pipeline args."""
|
||||
auth_entry = strategy.session.get(AUTH_ENTRY_KEY)
|
||||
|
||||
if not (auth_entry and auth_entry in _AUTH_ENTRY_CHOICES):
|
||||
raise AuthEntryError(strategy.backend, 'auth_entry missing or invalid')
|
||||
|
||||
@@ -499,7 +509,6 @@ def ensure_user_information(
|
||||
user_unset = user is None
|
||||
dispatch_to_login = is_login and (user_unset or user_inactive)
|
||||
reject_api_request = is_api and (user_unset or user_inactive)
|
||||
|
||||
if reject_api_request:
|
||||
# Content doesn't matter; we just want to exit the pipeline
|
||||
return HttpResponseBadRequest()
|
||||
@@ -512,20 +521,49 @@ def ensure_user_information(
|
||||
return
|
||||
|
||||
if dispatch_to_login:
|
||||
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN], name='signin_user')
|
||||
return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN], strategy))
|
||||
|
||||
# TODO (ECOM-369): Consolidate this with `dispatch_to_login`
|
||||
# once the A/B test completes. # pylint: disable=fixme
|
||||
if dispatch_to_login_2:
|
||||
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN_2])
|
||||
return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_LOGIN_2], strategy))
|
||||
|
||||
if is_register and user_unset:
|
||||
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER], name='register_user')
|
||||
return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER], strategy))
|
||||
|
||||
# TODO (ECOM-369): Consolidate this with `is_register`
|
||||
# once the A/B test completes. # pylint: disable=fixme
|
||||
if is_register_2 and user_unset:
|
||||
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER_2])
|
||||
return redirect(_create_redirect_url(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER_2], strategy))
|
||||
|
||||
|
||||
def _create_redirect_url(url, strategy):
|
||||
""" Given a URL and a Strategy, construct the appropriate redirect URL.
|
||||
|
||||
Construct a redirect URL and append the URL parameters that should be preserved.
|
||||
|
||||
Args:
|
||||
url (string): The base URL to use for the redirect.
|
||||
strategy (Strategy): Used to determine which URL parameters to append to the redirect.
|
||||
|
||||
Returns:
|
||||
A string representation of the URL, with parameters, for redirect.
|
||||
"""
|
||||
url_params = {}
|
||||
enroll_course_id = strategy.session_get(AUTH_ENROLL_COURSE_ID_KEY)
|
||||
if enroll_course_id:
|
||||
url_params['course_id'] = enroll_course_id
|
||||
url_params['enrollment_action'] = 'enroll'
|
||||
email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
|
||||
if email_opt_in:
|
||||
url_params[AUTH_EMAIL_OPT_IN_KEY] = email_opt_in
|
||||
if url_params:
|
||||
return u'{url}?{params}'.format(
|
||||
url=url,
|
||||
params=urllib.urlencode(url_params)
|
||||
)
|
||||
else:
|
||||
return url
|
||||
|
||||
|
||||
@partial.partial
|
||||
@@ -639,6 +677,11 @@ def change_enrollment(strategy, user=None, *args, **kwargs):
|
||||
if enroll_course_id:
|
||||
course_id = CourseKey.from_string(enroll_course_id)
|
||||
modes = CourseMode.modes_for_course_dict(course_id)
|
||||
# If the email opt in parameter is found, set the preference.
|
||||
email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
|
||||
if email_opt_in:
|
||||
opt_in = email_opt_in.lower() == 'true'
|
||||
profile.update_email_opt_in(user.username, course_id.org, opt_in)
|
||||
if CourseMode.can_auto_enroll(course_id, modes_dict=modes):
|
||||
try:
|
||||
CourseEnrollment.enroll(user, course_id, check_access=True)
|
||||
|
||||
@@ -46,7 +46,7 @@ If true, it:
|
||||
from . import provider
|
||||
|
||||
|
||||
_FIELDS_STORED_IN_SESSION = ['auth_entry', 'next', 'enroll_course_id']
|
||||
_FIELDS_STORED_IN_SESSION = ['auth_entry', 'next', 'enroll_course_id', 'email_opt_in']
|
||||
_MIDDLEWARE_CLASSES = (
|
||||
'third_party_auth.middleware.ExceptionMiddleware',
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for the change enrollment step of the pipeline. """
|
||||
from collections import namedtuple
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
@@ -17,6 +19,7 @@ from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, mixed_store_config
|
||||
)
|
||||
from user_api.models import UserOrgTag
|
||||
|
||||
|
||||
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
|
||||
@@ -42,12 +45,12 @@ class PipelineEnrollmentTest(ModuleStoreTestCase):
|
||||
self.user = UserFactory.create()
|
||||
|
||||
@ddt.data(
|
||||
([], "honor"),
|
||||
(["honor", "verified", "audit"], "honor"),
|
||||
(["professional"], None)
|
||||
([], "honor", u"False", u"False"),
|
||||
(["honor", "verified", "audit"], "honor", u"True", u"True"),
|
||||
(["professional"], None, u"Fålsœ", u"False")
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_auto_enroll_step(self, course_modes, enrollment_mode):
|
||||
def test_auto_enroll_step(self, course_modes, enrollment_mode, email_opt_in, email_opt_in_result):
|
||||
# Create the course modes for the test case
|
||||
for mode_slug in course_modes:
|
||||
CourseModeFactory.create(
|
||||
@@ -61,6 +64,7 @@ class PipelineEnrollmentTest(ModuleStoreTestCase):
|
||||
# when they started the auth process.
|
||||
strategy = self._fake_strategy()
|
||||
strategy.session_set('enroll_course_id', unicode(self.course.id))
|
||||
strategy.session_set('email_opt_in', email_opt_in)
|
||||
|
||||
result = pipeline.change_enrollment(strategy, 1, user=self.user) # pylint: disable=assignment-from-no-return,redundant-keyword-arg
|
||||
self.assertEqual(result, {})
|
||||
@@ -74,6 +78,11 @@ class PipelineEnrollmentTest(ModuleStoreTestCase):
|
||||
else:
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
# Check that the Email Opt In option was set
|
||||
tag = UserOrgTag.objects.get(user=self.user)
|
||||
self.assertIsNotNone(tag)
|
||||
self.assertEquals(tag.value, email_opt_in_result)
|
||||
|
||||
def test_add_white_label_to_cart(self):
|
||||
# Create a white label course (honor with a minimum price)
|
||||
CourseModeFactory.create(
|
||||
@@ -121,6 +130,29 @@ class PipelineEnrollmentTest(ModuleStoreTestCase):
|
||||
self.assertEqual(result, {})
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
def test_url_creation(self):
|
||||
strategy = self._fake_strategy()
|
||||
strategy.session_set('enroll_course_id', unicode(self.course.id))
|
||||
strategy.session_set('email_opt_in', u"False")
|
||||
backend = namedtuple('backend', 'name')
|
||||
backend.name = self.BACKEND_NAME
|
||||
response = pipeline.ensure_user_information(
|
||||
strategy=strategy,
|
||||
pipeline_index=1,
|
||||
details=None,
|
||||
response=None,
|
||||
uid=None,
|
||||
is_register=True,
|
||||
backend=backend
|
||||
)
|
||||
self.assertIsNotNone(response)
|
||||
self.assertEquals(response.status_code, 302)
|
||||
|
||||
# Get the location
|
||||
_, url = response._headers['location'] # pylint: disable=W0212
|
||||
self.assertIn("email_opt_in=False", url)
|
||||
self.assertIn("course_id=".format(id=unicode(self.course.id)), url)
|
||||
|
||||
def _fake_strategy(self):
|
||||
"""Simulate the strategy passed to the pipeline step. """
|
||||
request = RequestFactory().get(pipeline.get_complete_url(self.BACKEND_NAME))
|
||||
|
||||
@@ -5,6 +5,7 @@ invalidation. Import these instead of django.core.cache.
|
||||
Note that 'default' is being preserved for user session caching, which we're
|
||||
not migrating so as not to inconvenience users by logging them all out.
|
||||
"""
|
||||
import urllib
|
||||
from functools import wraps
|
||||
|
||||
from django.core import cache
|
||||
@@ -56,7 +57,13 @@ def cache_if_anonymous(*get_parameters):
|
||||
|
||||
# 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))
|
||||
parameter_value = request.GET.get(get_parameter)
|
||||
if parameter_value is not None:
|
||||
# urlencode expects data to be of type str, and doesn't deal well with Unicode data
|
||||
# since it doesn't provide a way to specify an encoding.
|
||||
cache_key = cache_key + '.' + urllib.urlencode({
|
||||
get_parameter: unicode(parameter_value).encode('utf-8')
|
||||
})
|
||||
|
||||
response = cache.get(cache_key) # pylint: disable=maybe-no-member
|
||||
if not response:
|
||||
|
||||
@@ -1483,6 +1483,16 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
# Not found, so create.
|
||||
course_assets = {'course_id': unicode(course_key), 'assets': {}}
|
||||
course_assets['assets']['_id'] = self.asset_collection.insert(course_assets)
|
||||
elif isinstance(course_assets['assets'], list):
|
||||
# This record is in the old course assets format.
|
||||
# Ensure that no data exists before updating the format.
|
||||
assert(len(course_assets['assets']) == 0)
|
||||
# Update the format to a dict.
|
||||
self.asset_collection.update(
|
||||
{'_id': course_assets['_id']},
|
||||
{'$set': {'assets': {}}}
|
||||
)
|
||||
course_assets['assets'] = {'_id': course_assets['_id']}
|
||||
else:
|
||||
course_assets['assets']['_id'] = course_assets['_id']
|
||||
|
||||
|
||||
@@ -131,18 +131,21 @@ def export_to_xml(modulestore, contentstore, course_key, root_dir, course_dir):
|
||||
draft_module.location,
|
||||
revision=ModuleStoreEnum.RevisionOption.draft_preferred
|
||||
)
|
||||
# Don't try to export orphaned items.
|
||||
if parent_loc is not None:
|
||||
logging.debug('parent_loc = {0}'.format(parent_loc))
|
||||
draft_node = draft_node_constructor(
|
||||
draft_module,
|
||||
location=draft_module.location,
|
||||
url=draft_module.location.to_deprecated_string(),
|
||||
parent_location=parent_loc,
|
||||
parent_url=parent_loc.to_deprecated_string(),
|
||||
)
|
||||
|
||||
draft_node_list.append(draft_node)
|
||||
# if module has no parent, set its parent_url to `None`
|
||||
parent_url = None
|
||||
if parent_loc is not None:
|
||||
parent_url = parent_loc.to_deprecated_string()
|
||||
|
||||
draft_node = draft_node_constructor(
|
||||
draft_module,
|
||||
location=draft_module.location,
|
||||
url=draft_module.location.to_deprecated_string(),
|
||||
parent_location=parent_loc,
|
||||
parent_url=parent_url,
|
||||
)
|
||||
|
||||
draft_node_list.append(draft_node)
|
||||
|
||||
for draft_node in get_draft_subtree_roots(draft_node_list):
|
||||
# only export the roots of the draft subtrees
|
||||
@@ -153,6 +156,13 @@ def export_to_xml(modulestore, contentstore, course_key, root_dir, course_dir):
|
||||
if not hasattr(draft_node.module, 'xml_attributes'):
|
||||
draft_node.module.xml_attributes = {}
|
||||
|
||||
# Don't try to export orphaned items
|
||||
# and their descendents
|
||||
if draft_node.parent_location is None:
|
||||
continue
|
||||
|
||||
logging.debug('parent_loc = {0}'.format(draft_node.parent_location))
|
||||
|
||||
draft_node.module.xml_attributes['parent_url'] = draft_node.parent_url
|
||||
parent = modulestore.get_item(draft_node.parent_location)
|
||||
index = parent.children.index(draft_node.module.location)
|
||||
|
||||
@@ -6,6 +6,7 @@ import cgi
|
||||
from datetime import datetime
|
||||
from pytz import UTC
|
||||
import unittest
|
||||
import ddt
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
@@ -74,6 +75,7 @@ class TestJumpTo(TestCase):
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
|
||||
class ViewsTestCase(TestCase):
|
||||
"""
|
||||
@@ -97,9 +99,8 @@ 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>"
|
||||
self.org = u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"
|
||||
self.org_html = "<p>'+Stark/Industries+'</p>"
|
||||
|
||||
@unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings")
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})
|
||||
@@ -266,23 +267,41 @@ class ViewsTestCase(TestCase):
|
||||
def test_course_mktg_about_coming_soon(self):
|
||||
# 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, {'organization_full_name': self.organization_full_name})
|
||||
response = self.client.get(url, {'org': self.org})
|
||||
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(organization_full_name=self.organization_full_name)
|
||||
@ddt.data(
|
||||
# One organization name
|
||||
(u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ", u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"),
|
||||
# Two organization names
|
||||
(",".join([u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"] * 2), u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ" + " and " + u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"),
|
||||
# Three organization names
|
||||
(",".join([u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"] * 3), u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ" + ", " + u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ" + ", " + "and " + u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ")
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_course_mktg_register(self, org, org_name_string):
|
||||
response = self._load_mktg_about(org=org)
|
||||
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)
|
||||
self._email_opt_in_checkbox(response, org_name_string)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
|
||||
def test_course_mktg_register_multiple_modes(self):
|
||||
@ddt.data(
|
||||
# One organization name
|
||||
(u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ", u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"),
|
||||
# Two organization names
|
||||
(",".join([u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"] * 2), u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ" + " and " + u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"),
|
||||
# Three organization names
|
||||
(",".join([u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"] * 3), u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ" + ", " + u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ" + ", " + "and " + u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ")
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_course_mktg_register_multiple_modes(self, org, org_name_string):
|
||||
CourseMode.objects.get_or_create(
|
||||
mode_slug='honor',
|
||||
mode_display_name='Honor Code Certificate',
|
||||
@@ -294,12 +313,12 @@ class ViewsTestCase(TestCase):
|
||||
course_id=self.course_key
|
||||
)
|
||||
|
||||
response = self._load_mktg_about(organization_full_name=self.organization_full_name)
|
||||
response = self._load_mktg_about(org=org)
|
||||
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)
|
||||
self._email_opt_in_checkbox(response, org_name_string)
|
||||
|
||||
# clean up course modes
|
||||
CourseMode.objects.all().delete()
|
||||
@@ -317,18 +336,18 @@ class ViewsTestCase(TestCase):
|
||||
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)
|
||||
response = self._load_mktg_about(org=self.org)
|
||||
|
||||
# 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)
|
||||
response = self._load_mktg_about(org=self.org_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))
|
||||
self._email_opt_in_checkbox(response, cgi.escape(self.org_html))
|
||||
|
||||
@patch.dict(settings.FEATURES, {'IS_EDX_DOMAIN': True})
|
||||
def test_mktg_about_language_edx_domain(self):
|
||||
@@ -385,12 +404,13 @@ class ViewsTestCase(TestCase):
|
||||
response = self.client.get(url)
|
||||
self.assertFalse('<script>' in response.content)
|
||||
|
||||
def _load_mktg_about(self, language=None, organization_full_name=None):
|
||||
def _load_mktg_about(self, language=None, org=None):
|
||||
"""Retrieve the marketing about button (iframed into the marketing site)
|
||||
and return the HTTP response.
|
||||
|
||||
Keyword Args:
|
||||
language (string): If provided, send this in the 'Accept-Language' HTTP header.
|
||||
org (string): If provided, send the string as a GET parameter.
|
||||
|
||||
Returns:
|
||||
Response
|
||||
@@ -406,19 +426,19 @@ class ViewsTestCase(TestCase):
|
||||
headers['HTTP_ACCEPT_LANGUAGE'] = language
|
||||
|
||||
url = reverse('mktg_about_course', kwargs={'course_id': unicode(self.course_key)})
|
||||
if organization_full_name:
|
||||
return self.client.get(url, {'organization_full_name': organization_full_name}, **headers)
|
||||
if org:
|
||||
return self.client.get(url, {'org': org}, **headers)
|
||||
else:
|
||||
return self.client.get(url, **headers)
|
||||
|
||||
def _email_opt_in_checkbox(self, response, organization_full_name=None):
|
||||
def _email_opt_in_checkbox(self, response, org_name_string=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:
|
||||
if org_name_string:
|
||||
# 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)
|
||||
self.assertContains(response, org_name_string)
|
||||
else:
|
||||
# Verify that the email opt-in checkbox does not appear
|
||||
self.assertNotContains(response, checkbox_html, html=True)
|
||||
|
||||
@@ -11,6 +11,7 @@ from datetime import datetime
|
||||
from collections import defaultdict
|
||||
from django.utils import translation
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ungettext
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.context_processors import csrf
|
||||
@@ -803,7 +804,7 @@ def course_about(request, course_id):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous('organization_full_name')
|
||||
@cache_if_anonymous('org')
|
||||
@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."""
|
||||
@@ -844,10 +845,37 @@ def mktg_course_about(request, course_id):
|
||||
}
|
||||
|
||||
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
|
||||
# Drupal will pass organization names using a GET parameter, as follows:
|
||||
# ?org=Harvard
|
||||
# ?org=Harvard,MIT
|
||||
# If no full names are provided, the marketing iframe won't show the
|
||||
# email opt-in checkbox.
|
||||
org = request.GET.get('org')
|
||||
if org:
|
||||
org_list = org.split(',')
|
||||
# HTML-escape the provided organization names
|
||||
org_list = [cgi.escape(org) for org in org_list]
|
||||
if len(org_list) > 1:
|
||||
if len(org_list) > 2:
|
||||
# Translators: The join of three or more institution names (e.g., Harvard, MIT, and Dartmouth).
|
||||
org_name_string = _("{first_institutions}, and {last_institution}").format(
|
||||
first_institutions=u", ".join(org_list[:-1]),
|
||||
last_institution=org_list[-1]
|
||||
)
|
||||
else:
|
||||
# Translators: The join of two institution names (e.g., Harvard and MIT).
|
||||
org_name_string = _("{first_institution} and {second_institution}").format(
|
||||
first_institution=org_list[0],
|
||||
second_institution=org_list[1]
|
||||
)
|
||||
else:
|
||||
org_name_string = org_list[0]
|
||||
|
||||
context['checkbox_label'] = ungettext(
|
||||
"I would like to receive email from {institution_series} and learn about its other programs.",
|
||||
"I would like to receive email from {institution_series} and learn about their other programs.",
|
||||
len(org_list)
|
||||
).format(institution_series=org_name_string)
|
||||
|
||||
# The edx.org marketing site currently displays only in English.
|
||||
# To avoid displaying a different language in the register / access button,
|
||||
|
||||
@@ -285,13 +285,16 @@ def _third_party_auth_context(request):
|
||||
}
|
||||
|
||||
course_id = request.GET.get("course_id")
|
||||
email_opt_in = request.GET.get('email_opt_in')
|
||||
login_urls = auth_pipeline_urls(
|
||||
third_party_auth.pipeline.AUTH_ENTRY_LOGIN_2,
|
||||
course_id=course_id
|
||||
course_id=course_id,
|
||||
email_opt_in=email_opt_in
|
||||
)
|
||||
register_urls = auth_pipeline_urls(
|
||||
third_party_auth.pipeline.AUTH_ENTRY_REGISTER_2,
|
||||
course_id=course_id
|
||||
course_id=course_id,
|
||||
email_opt_in=email_opt_in
|
||||
)
|
||||
|
||||
if third_party_auth.is_enabled():
|
||||
|
||||
@@ -19,7 +19,6 @@ div.calc-main {
|
||||
color: #fff;
|
||||
float: right;
|
||||
height: 20px;
|
||||
@include hide-text;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
padding: 8px 12px;
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
}
|
||||
|
||||
## 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;
|
||||
window.top.location.href = $("a.register").attr("href") || "${reverse('register_user')}?course_id=${course.id | u}&enrollment_action=enroll&email_opt_in=" + emailOptIn;
|
||||
} else {
|
||||
$('#register_error').html(
|
||||
(xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.")}")
|
||||
@@ -102,17 +102,11 @@
|
||||
</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:
|
||||
## We only display the email opt-in checkbox if we've been given one or more organization names.
|
||||
% if checkbox_label:
|
||||
<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" class="register-emails">
|
||||
## 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>
|
||||
<label for="email-opt-in" class="register-emails">${checkbox_label}</label>
|
||||
</p>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
@@ -23,7 +23,7 @@ git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a
|
||||
|
||||
# Our libraries:
|
||||
-e git+https://github.com/edx/XBlock.git@2029af2a4b524310847decfb34ef39da8a30dc4e#egg=XBlock
|
||||
-e git+https://github.com/edx/codejail.git@66dd5a45e5072666ff9a70c768576e9ffd1daa4b#egg=codejail
|
||||
-e git+https://github.com/edx/codejail.git@75307b25032d8b0040b1408c01fd6cc9a1989bd5#egg=codejail
|
||||
-e git+https://github.com/edx/diff-cover.git@v0.7.2#egg=diff_cover
|
||||
-e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool
|
||||
-e git+https://github.com/edx/event-tracking.git@0.1.0#egg=event-tracking
|
||||
|
||||
Reference in New Issue
Block a user