diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 65ed2519be..9a12d0ff0a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +LMS: Add split testing functionality for internal use. + LMS: Improved accessibility of parts of forum navigation sidebar. LMS: enhanced accessibility labeling and aria support for the discussion forum new post dropdown as well as response and comment area labeling. diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index ce49e5a201..6579e631d6 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -25,6 +25,8 @@ from courseware.model_data import FieldDataCache from open_ended_grading import open_ended_notifications +import waffle + log = logging.getLogger(__name__) @@ -55,32 +57,46 @@ TabImpl = namedtuple('TabImpl', 'validator generator') ##### Generators for various tabs. - -def _courseware(tab, user, course, active_page): +def _courseware(tab, user, course, active_page, request): + """ + This returns a tab containing the course content. + """ link = reverse('courseware', args=[course.id]) - return [CourseTab('Courseware', link, active_page == "courseware")] + if waffle.flag_is_active(request, 'merge_course_tabs'): + return [CourseTab('Course Content', link, active_page == "courseware")] + else: + return [CourseTab('Courseware', link, active_page == "courseware")] -def _course_info(tab, user, course, active_page): +def _course_info(tab, user, course, active_page, request): + """ + This returns a tab containing information about the course. + """ link = reverse('info', args=[course.id]) return [CourseTab(tab['name'], link, active_page == "info")] -def _progress(tab, user, course, active_page): +def _progress(tab, user, course, active_page, request): + """ + This returns a tab containing information about the authenticated user's progress. + """ if user.is_authenticated(): link = reverse('progress', args=[course.id]) return [CourseTab(tab['name'], link, active_page == "progress")] return [] -def _wiki(tab, user, course, active_page): +def _wiki(tab, user, course, active_page, request): + """ + This returns a tab containing the course wiki. + """ if settings.WIKI_ENABLED: link = reverse('course_wiki', args=[course.id]) return [CourseTab(tab['name'], link, active_page == 'wiki')] return [] -def _discussion(tab, user, course, active_page): +def _discussion(tab, user, course, active_page, request): """ This tab format only supports the new Berkeley discussion forums. """ @@ -91,25 +107,25 @@ def _discussion(tab, user, course, active_page): return [] -def _external_discussion(tab, user, course, active_page): +def _external_discussion(tab, user, course, active_page, request): """ This returns a tab that links to an external discussion service """ return [CourseTab('Discussion', tab['link'], active_page == 'discussion')] -def _external_link(tab, user, course, active_page): +def _external_link(tab, user, course, active_page, request): # external links are never active return [CourseTab(tab['name'], tab['link'], False)] -def _static_tab(tab, user, course, active_page): +def _static_tab(tab, user, course, active_page, request): link = reverse('static_tab', args=[course.id, tab['url_slug']]) active_str = 'static_tab_{0}'.format(tab['url_slug']) return [CourseTab(tab['name'], link, active_page == active_str)] -def _textbooks(tab, user, course, active_page): +def _textbooks(tab, user, course, active_page, request): """ Generates one tab per textbook. Only displays if user is authenticated. """ @@ -120,7 +136,8 @@ def _textbooks(tab, user, course, active_page): for index, textbook in enumerate(course.textbooks)] return [] -def _pdf_textbooks(tab, user, course, active_page): + +def _pdf_textbooks(tab, user, course, active_page, request): """ Generates one tab per textbook. Only displays if user is authenticated. """ @@ -131,7 +148,8 @@ def _pdf_textbooks(tab, user, course, active_page): for index, textbook in enumerate(course.pdf_textbooks)] return [] -def _html_textbooks(tab, user, course, active_page): + +def _html_textbooks(tab, user, course, active_page, request): """ Generates one tab per textbook. Only displays if user is authenticated. """ @@ -142,7 +160,8 @@ def _html_textbooks(tab, user, course, active_page): for index, textbook in enumerate(course.html_textbooks)] return [] -def _staff_grading(tab, user, course, active_page): + +def _staff_grading(tab, user, course, active_page, request): if has_access(user, course, 'staff'): link = reverse('staff_grading', args=[course.id]) @@ -157,14 +176,13 @@ def _staff_grading(tab, user, course, active_page): return [] -def _syllabus(tab, user, course, active_page): +def _syllabus(tab, user, course, active_page, request): """Display the syllabus tab""" link = reverse('syllabus', args=[course.id]) return [CourseTab('Syllabus', link, active_page == 'syllabus')] -def _peer_grading(tab, user, course, active_page): - +def _peer_grading(tab, user, course, active_page, request): if user.is_authenticated(): link = reverse('peer_grading', args=[course.id]) tab_name = "Peer grading" @@ -178,7 +196,7 @@ def _peer_grading(tab, user, course, active_page): return [] -def _combined_open_ended_grading(tab, user, course, active_page): +def _combined_open_ended_grading(tab, user, course, active_page, request): if user.is_authenticated(): link = reverse('open_ended_notifications', args=[course.id]) tab_name = "Open Ended Panel" @@ -191,15 +209,15 @@ def _combined_open_ended_grading(tab, user, course, active_page): return tab return [] -def _notes_tab(tab, user, course, active_page): + +def _notes_tab(tab, user, course, active_page, request): if user.is_authenticated() and settings.MITX_FEATURES.get('ENABLE_STUDENT_NOTES'): link = reverse('notes', args=[course.id]) return [CourseTab(tab['name'], link, active_page == 'notes')] return [] + #### Validators - - def key_checker(expected_keys): """ Returns a function that checks that specified keys are present in a dict @@ -263,12 +281,15 @@ def validate_tabs(course): if len(tabs) < 2: raise InvalidTabsException("Expected at least two tabs. tabs: '{0}'".format(tabs)) + if tabs[0]['type'] != 'courseware': raise InvalidTabsException( "Expected first tab to have type 'courseware'. tabs: '{0}'".format(tabs)) + if tabs[1]['type'] != 'course_info': raise InvalidTabsException( "Expected second tab to have type 'course_info'. tabs: '{0}'".format(tabs)) + for t in tabs: if t['type'] not in VALID_TAB_TYPES: raise InvalidTabsException("Unknown tab type {0}. Known types: {1}" @@ -280,12 +301,12 @@ def validate_tabs(course): # are actually unique (otherwise, will break active tag code) -def get_course_tabs(user, course, active_page): +def get_course_tabs(user, course, active_page, request): """ Return the tabs to show a particular user, as a list of CourseTab items. """ if not hasattr(course, 'tabs') or not course.tabs: - return get_default_tabs(user, course, active_page) + return get_default_tabs(user, course, active_page, request) # TODO (vshnayder): There needs to be a place to call this right after course # load, but not from inside xmodule, since that doesn't (and probably @@ -293,12 +314,18 @@ def get_course_tabs(user, course, active_page): validate_tabs(course) tabs = [] - for tab in course.tabs: + + if waffle.flag_is_active(request, 'merge_course_tabs'): + course_tabs = [tab for tab in course.tabs if tab['type'] != "course_info"] + else: + course_tabs = course.tabs + + for tab in course_tabs: # expect handlers to return lists--handles things that are turned off # via feature flags, and things like 'textbook' which might generate # multiple tabs. gen = VALID_TAB_TYPES[tab['type']].generator - tabs.extend(gen(tab, user, course, active_page)) + tabs.extend(gen(tab, user, course, active_page, request)) # Instructor tab is special--automatically added if user is staff for the course if has_access(user, course, 'staff'): @@ -314,7 +341,7 @@ def get_discussion_link(course): Return the URL for the discussion tab for the given `course`. If they have a discussion link specified, use that even if we disable - discussions. Disabling discsussions is mostly a server safety feature at + discussions. Disabling discussions is mostly a server safety feature at this point, and we don't need to worry about external sites. Otherwise, if the course has a discussion tab or uses the default tabs, return the discussion view URL. Otherwise, return None to indicate the lack of a @@ -330,28 +357,33 @@ def get_discussion_link(course): return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id]) -def get_default_tabs(user, course, active_page): - +def get_default_tabs(user, course, active_page, request): + """ + Return the default set of tabs. + """ # When calling the various _tab methods, can omit the 'type':'blah' from the # first arg, since that's only used for dispatch tabs = [] - tabs.extend(_courseware({''}, user, course, active_page)) - tabs.extend(_course_info({'name': 'Course Info'}, user, course, active_page)) + + tabs.extend(_courseware({''}, user, course, active_page, request)) + + if not waffle.flag_is_active(request, 'merge_course_tabs'): + tabs.extend(_course_info({'name': 'Course Info'}, user, course, active_page, request)) if hasattr(course, 'syllabus_present') and course.syllabus_present: link = reverse('syllabus', args=[course.id]) tabs.append(CourseTab('Syllabus', link, active_page == 'syllabus')) - tabs.extend(_textbooks({}, user, course, active_page)) + tabs.extend(_textbooks({}, user, course, active_page, request)) discussion_link = get_discussion_link(course) if discussion_link: tabs.append(CourseTab('Discussion', discussion_link, active_page == 'discussion')) - tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page)) + tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page, request)) if user.is_authenticated() and not course.hide_progress_tab: - tabs.extend(_progress({'name': 'Progress'}, user, course, active_page)) + tabs.extend(_progress({'name': 'Progress'}, user, course, active_page, request)) if has_access(user, course, 'staff'): link = reverse('instructor_dashboard', args=[course.id]) @@ -376,7 +408,6 @@ def get_static_tab_by_slug(course, tab_slug): def get_static_tab_contents(request, course, tab): - loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug']) field_data_cache = FieldDataCache.cache_for_descriptor_descendents(course.id, request.user, modulestore().get_instance(course.id, loc), depth=0) diff --git a/lms/djangoapps/courseware/tests/test_tabs.py b/lms/djangoapps/courseware/tests/test_tabs.py index 5de7a39f63..4274ebf85d 100644 --- a/lms/djangoapps/courseware/tests/test_tabs.py +++ b/lms/djangoapps/courseware/tests/test_tabs.py @@ -11,38 +11,38 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE +FAKE_REQUEST = None + +def tab_constructor(active_page, course, user, tab={'name': 'same'}, generator=tabs._progress): + return generator(tab, user, course, active_page, FAKE_REQUEST) class ProgressTestCase(TestCase): def setUp(self): - self.mockuser1 = MagicMock() - self.mockuser0 = MagicMock() + self.user = MagicMock() + self.anonymous_user = MagicMock() self.course = MagicMock() - self.mockuser1.is_authenticated.return_value = True - self.mockuser0.is_authenticated.return_value = False + self.user.is_authenticated.return_value = True + self.anonymous_user.is_authenticated.return_value = False self.course.id = 'edX/toy/2012_Fall' self.tab = {'name': 'same'} - self.active_page1 = 'progress' - self.active_page0 = 'stagnation' + self.progress_page = 'progress' + self.stagnation_page = 'stagnation' def test_progress(self): - self.assertEqual(tabs._progress(self.tab, self.mockuser0, self.course, - self.active_page0), []) + self.assertEqual(tab_constructor(self.stagnation_page, self.course, self.anonymous_user), []) - self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course, - self.active_page1)[0].name, 'same') - - self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course, - self.active_page1)[0].link, - reverse('progress', args=[self.course.id])) - - self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course, - self.active_page0)[0].is_active, False) - - self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course, - self.active_page1)[0].is_active, True) + self.assertEqual(tab_constructor(self.progress_page, self.course, self.user)[0].name, 'same') + + tab_list = tab_constructor(self.progress_page, self.course, self.user) + expected_link = reverse('progress', args=[self.course.id]) + self.assertEqual(tab_list[0].link, expected_link) + + self.assertEqual(tab_constructor(self.stagnation_page, self.course, self.user)[0].is_active, False) + + self.assertEqual(tab_constructor(self.progress_page, self.course, self.user)[0].is_active, True) class WikiTestCase(TestCase): @@ -53,33 +53,30 @@ class WikiTestCase(TestCase): self.course = MagicMock() self.course.id = 'edX/toy/2012_Fall' self.tab = {'name': 'same'} - self.active_page1 = 'wiki' - self.active_page0 = 'miki' + self.wiki_page = 'wiki' + self.miki_page = 'miki' @override_settings(WIKI_ENABLED=True) def test_wiki_enabled(self): - self.assertEqual(tabs._wiki(self.tab, self.user, - self.course, self.active_page1)[0].name, - 'same') + tab_list = tab_constructor(self.wiki_page, self.course, self.user, generator=tabs._wiki) + self.assertEqual(tab_list[0].name, 'same') - self.assertEqual(tabs._wiki(self.tab, self.user, - self.course, self.active_page1)[0].link, - reverse('course_wiki', args=[self.course.id])) + tab_list = tab_constructor(self.wiki_page, self.course, self.user, generator=tabs._wiki) + expected_link = reverse('course_wiki', args=[self.course.id]) + self.assertEqual(tab_list[0].link, expected_link) - self.assertEqual(tabs._wiki(self.tab, self.user, - self.course, self.active_page1)[0].is_active, - True) + tab_list = tab_constructor(self.wiki_page, self.course, self.user, generator=tabs._wiki) + self.assertEqual(tab_list[0].is_active, True) - self.assertEqual(tabs._wiki(self.tab, self.user, - self.course, self.active_page0)[0].is_active, - False) + tab_list = tab_constructor(self.miki_page, self.course, self.user, generator=tabs._wiki) + self.assertEqual(tab_list[0].is_active, False) @override_settings(WIKI_ENABLED=False) def test_wiki_enabled_false(self): - self.assertEqual(tabs._wiki(self.tab, self.user, - self.course, self.active_page1), []) + tab_list = tab_constructor(self.wiki_page, self.course, self.user, generator=tabs._wiki) + self.assertEqual(tab_list, []) class ExternalLinkTestCase(TestCase): @@ -89,26 +86,30 @@ class ExternalLinkTestCase(TestCase): self.user = MagicMock() self.course = MagicMock() self.tabby = {'name': 'same', 'link': 'blink'} - self.active_page0 = None - self.active_page00 = True + self.no_page = None + self.true = True def test_external_link(self): - self.assertEqual(tabs._external_link(self.tabby, self.user, - self.course, self.active_page0)[0].name, - 'same') + tab_list = tab_constructor( + self.no_page, self.course, self.user, tab=self.tabby, generator=tabs._external_link + ) + self.assertEqual(tab_list[0].name, 'same') - self.assertEqual(tabs._external_link(self.tabby, self.user, - self.course, self.active_page0)[0].link, - 'blink') + tab_list = tab_constructor( + self.no_page, self.course, self.user, tab=self.tabby, generator=tabs._external_link + ) + self.assertEqual(tab_list[0].link, 'blink') - self.assertEqual(tabs._external_link(self.tabby, self.user, - self.course, self.active_page0)[0].is_active, - False) + tab_list = tab_constructor( + self.no_page, self.course, self.user, tab=self.tabby, generator=tabs._external_link + ) + self.assertEqual(tab_list[0].is_active, False) - self.assertEqual(tabs._external_link(self.tabby, self.user, - self.course, self.active_page00)[0].is_active, - False) + tab_list = tab_constructor( + self.true, self.course, self.user, tab=self.tabby, generator=tabs._external_link + ) + self.assertEqual(tab_list[0].is_active, False) class StaticTabTestCase(TestCase): @@ -119,107 +120,124 @@ class StaticTabTestCase(TestCase): self.course = MagicMock() self.tabby = {'name': 'same', 'url_slug': 'schmug'} self.course.id = 'edX/toy/2012_Fall' - self.active_page1 = 'static_tab_schmug' - self.active_page0 = 'static_tab_schlug' + self.schmug = 'static_tab_schmug' + self.schlug = 'static_tab_schlug' def test_static_tab(self): - self.assertEqual(tabs._static_tab(self.tabby, self.user, - self.course, self.active_page1)[0].name, - 'same') + tab_list = tab_constructor( + self.schmug, self.course, self.user, tab=self.tabby, generator=tabs._static_tab + ) + self.assertEqual(tab_list[0].name, 'same') - self.assertEqual(tabs._static_tab(self.tabby, self.user, - self.course, self.active_page1)[0].link, - reverse('static_tab', args=[self.course.id, - self.tabby['url_slug']])) + tab_list = tab_constructor( + self.schmug, self.course, self.user, tab=self.tabby, generator=tabs._static_tab + ) + expected_link = reverse('static_tab', args=[self.course.id,self.tabby['url_slug']]) + self.assertEqual(tab_list[0].link, expected_link) - self.assertEqual(tabs._static_tab(self.tabby, self.user, - self.course, self.active_page1)[0].is_active, - True) - - self.assertEqual(tabs._static_tab(self.tabby, self.user, - self.course, self.active_page0)[0].is_active, - False) + tab_list = tab_constructor( + self.schmug, self.course, self.user, tab=self.tabby, generator=tabs._static_tab + ) + self.assertEqual(tab_list[0].is_active, True) + tab_list = tab_constructor( + self.schlug, self.course, self.user, tab=self.tabby, generator=tabs._static_tab + ) + self.assertEqual(tab_list[0].is_active, False) class TextbooksTestCase(TestCase): def setUp(self): - self.mockuser1 = MagicMock() - self.mockuser0 = MagicMock() + self.user = MagicMock() + self.anonymous_user = MagicMock() self.course = MagicMock() self.tab = MagicMock() A = MagicMock() T = MagicMock() - self.mockuser1.is_authenticated.return_value = True - self.mockuser0.is_authenticated.return_value = False - self.course.id = 'edX/toy/2012_Fall' - self.active_page0 = 'textbook/0' - self.active_page1 = 'textbook/1' - self.active_pageX = 'you_shouldnt_be_seein_this' A.title = 'Algebra' T.title = 'Topology' self.course.textbooks = [A, T] + self.user.is_authenticated.return_value = True + self.anonymous_user.is_authenticated.return_value = False + self.course.id = 'edX/toy/2012_Fall' + self.textbook_0 = 'textbook/0' + self.textbook_1 = 'textbook/1' + self.prohibited_page = 'you_shouldnt_be_seein_this' @override_settings(MITX_FEATURES={'ENABLE_TEXTBOOK': True}) def test_textbooks1(self): - self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, - self.course, self.active_page0)[0].name, - 'Algebra') + tab_list = tab_constructor( + self.textbook_0, self.course, self.user, tab=self.tab, generator=tabs._textbooks + ) + self.assertEqual(tab_list[0].name, 'Algebra') - self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, - self.course, self.active_page0)[0].link, - reverse('book', args=[self.course.id, 0])) + tab_list = tab_constructor( + self.textbook_0, self.course, self.user, tab=self.tab, generator=tabs._textbooks + ) + expected_link = reverse('book', args=[self.course.id, 0]) + self.assertEqual(tab_list[0].link, expected_link) - self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, - self.course, self.active_page0)[0].is_active, - True) + tab_list = tab_constructor( + self.textbook_0, self.course, self.user, tab=self.tab, generator=tabs._textbooks + ) + self.assertEqual(tab_list[0].is_active, True) - self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, - self.course, self.active_pageX)[0].is_active, - False) + tab_list = tab_constructor( + self.prohibited_page, self.course, self.user, tab=self.tab, generator=tabs._textbooks + ) + self.assertEqual(tab_list[0].is_active, False) - self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, - self.course, self.active_page1)[1].name, - 'Topology') + tab_list = tab_constructor( + self.textbook_1, self.course, self.user, tab=self.tab, generator=tabs._textbooks + ) + self.assertEqual(tab_list[1].name, 'Topology') - self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, - self.course, self.active_page1)[1].link, - reverse('book', args=[self.course.id, 1])) + tab_list = tab_constructor( + self.textbook_1, self.course, self.user, tab=self.tab, generator=tabs._textbooks + ) + expected_link = reverse('book', args=[self.course.id, 1]) + self.assertEqual(tab_list[1].link, expected_link) - self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, - self.course, self.active_page1)[1].is_active, - True) + tab_list = tab_constructor( + self.textbook_1, self.course, self.user, tab=self.tab, generator=tabs._textbooks + ) + self.assertEqual(tab_list[1].is_active, True) - self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, - self.course, self.active_pageX)[1].is_active, - False) + tab_list = tab_constructor( + self.prohibited_page, self.course, self.user, tab=self.tab, generator=tabs._textbooks + ) + self.assertEqual(tab_list[1].is_active, False) @override_settings(MITX_FEATURES={'ENABLE_TEXTBOOK': False}) def test_textbooks0(self): - self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, - self.course, self.active_pageX), []) + tab_list = tab_constructor( + self.prohibited_page, self.course, self.user, tab=self.tab, generator=tabs._textbooks + ) + self.assertEqual(tab_list, []) - self.assertEqual(tabs._textbooks(self.tab, self.mockuser0, - self.course, self.active_pageX), []) + tab_list = tab_constructor( + self.prohibited_page, self.course, self.anonymous_user, tab=self.tab, generator=tabs._textbooks + ) + self.assertEqual(tab_list, []) class KeyCheckerTestCase(TestCase): def setUp(self): - self.expected_keys1 = ['a', 'b'] - self.expected_keys0 = ['a', 'v', 'g'] + self.valid_keys = ['a', 'b'] + self.invalid_keys = ['a', 'v', 'g'] self.dictio = {'a': 1, 'b': 2, 'c': 3} def test_key_checker(self): - self.assertIsNone(tabs.key_checker(self.expected_keys1)(self.dictio)) + self.assertIsNone(tabs.key_checker(self.valid_keys)(self.dictio)) self.assertRaises(tabs.InvalidTabsException, - tabs.key_checker(self.expected_keys0), self.dictio) + tabs.key_checker(self.invalid_keys), self.dictio) class NullValidatorTestCase(TestCase): diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 695d7ea55d..66c91f1b9b 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -728,6 +728,7 @@ def submission_history(request, course_id, student_username, location): Right now this only works for problems because that's all StudentModuleHistory records. """ + course = get_course_with_access(request.user, course_id, 'load') staff_access = has_access(request.user, course, 'staff') diff --git a/lms/envs/common.py b/lms/envs/common.py index 466ec262f9..47f0083957 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -80,7 +80,7 @@ MITX_FEATURES = { 'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard) - 'ENABLE_DJANGO_ADMIN_SITE': False, # set true to enable django's admin site, even on prod (e.g. for course ops) + 'ENABLE_DJANGO_ADMIN_SITE': True, # set true to enable django's admin site, even on prod (e.g. for course ops) 'ENABLE_SQL_TRACKING_LOGS': False, 'ENABLE_LMS_MIGRATION': False, 'ENABLE_MANUAL_GIT_RELOAD': False, @@ -523,6 +523,14 @@ MOCK_STAFF_GRADING = False ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' +################################# Waffle ################################### + +# Name prepended to cookies set by Waffle +WAFFLE_COOKIE = "waffle_flag_%s" + +# Two weeks (in sec) +WAFFLE_MAX_AGE = 1209600 + ################################# Middleware ################################### # List of finder classes that know how to find static files in # various locations. @@ -570,6 +578,9 @@ MIDDLEWARE_CLASSES = ( # catches any uncaught RateLimitExceptions and returns a 403 instead of a 500 'ratelimitbackend.middleware.RateLimitMiddleware', + + # For A/B testing + 'waffle.middleware.WaffleMiddleware', ) ############################### Pipeline ####################################### @@ -832,6 +843,9 @@ INSTALLED_APPS = ( # Foldit integration 'foldit', + # For A/B testing + 'waffle', + # For testing 'django.contrib.admin', # only used in DEBUG mode 'django_nose', diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 1ec5030f7a..c596208b3f 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -255,7 +255,7 @@ ANALYTICS_API_KEY = "" ##### segment-io ###### -# If there's an environment variable set, grab it and turn on segment io +# If there's an environment variable set, grab it and turn on Segment.io SEGMENT_IO_LMS_KEY = os.environ.get('SEGMENT_IO_LMS_KEY') if SEGMENT_IO_LMS_KEY: MITX_FEATURES['SEGMENT_IO_LMS'] = True diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss index 77a80b481c..95765fc93c 100644 --- a/lms/static/sass/course/layout/_courseware_header.scss +++ b/lms/static/sass/course/layout/_courseware_header.scss @@ -23,6 +23,17 @@ nav.course-material { list-style: none; margin-right: 6px; + &.prominent { + margin-right: 16px; + background: rgba(255, 255, 255, .5); + border-radius: 3px; + } + + &.prominent + li { + padding-left: 15px; + border-left: 1px solid #333; + } + a { border-radius: 3px; color: #555; diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html index 303a12f142..8cd5368ad0 100644 --- a/lms/templates/courseware/course_navigation.html +++ b/lms/templates/courseware/course_navigation.html @@ -13,19 +13,24 @@ def url_class(is_active): %> <%! from courseware.tabs import get_course_tabs %> <%! from django.utils.translation import ugettext as _ %> +<% import waffle %>