diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 89079e0371..881b887867 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -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') diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index a147ad30f6..9eb26acf7d 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -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) diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py index d874595af9..1aea3a6a4c 100644 --- a/common/djangoapps/student/helpers.py +++ b/common/djangoapps/student/helpers.py @@ -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() diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b007e15a31..8b6292eecf 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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 diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py index 4564f5533a..f8eb13511f 100644 --- a/common/djangoapps/third_party_auth/pipeline.py +++ b/common/djangoapps/third_party_auth/pipeline.py @@ -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) diff --git a/common/djangoapps/third_party_auth/settings.py b/common/djangoapps/third_party_auth/settings.py index a94cb10da3..7244f05bfe 100644 --- a/common/djangoapps/third_party_auth/settings.py +++ b/common/djangoapps/third_party_auth/settings.py @@ -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', ) diff --git a/common/djangoapps/third_party_auth/tests/test_change_enrollment.py b/common/djangoapps/third_party_auth/tests/test_change_enrollment.py index f85ae38d48..c067b93b4e 100644 --- a/common/djangoapps/third_party_auth/tests/test_change_enrollment.py +++ b/common/djangoapps/third_party_auth/tests/test_change_enrollment.py @@ -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)) diff --git a/common/djangoapps/util/cache.py b/common/djangoapps/util/cache.py index 50b1b469ba..bd24d0bee7 100644 --- a/common/djangoapps/util/cache.py +++ b/common/djangoapps/util/cache.py @@ -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: diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/base.py b/common/lib/xmodule/xmodule/modulestore/mongo/base.py index df09ae3a96..9f8899e399 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo/base.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo/base.py @@ -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'] diff --git a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py index f1bae5f536..3cc691153e 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py @@ -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) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 48e5e3366b..fd3d33d94f 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -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 = "

'+Umbrella/Corporation+'

" + self.org = u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ" + self.org_html = "

'+Stark/Industries+'

" @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('