Commit f1ccf1c0 by Renzo Lucioni

Integrate split testing and LMS tabs experiments

parent fd06640d
...@@ -25,6 +25,8 @@ from courseware.model_data import FieldDataCache ...@@ -25,6 +25,8 @@ from courseware.model_data import FieldDataCache
from open_ended_grading import open_ended_notifications from open_ended_grading import open_ended_notifications
import waffle
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -55,32 +57,46 @@ TabImpl = namedtuple('TabImpl', 'validator generator') ...@@ -55,32 +57,46 @@ TabImpl = namedtuple('TabImpl', 'validator generator')
##### Generators for various tabs. ##### Generators for various tabs.
def _courseware(tab, user, course, active_page, request):
def _courseware(tab, user, course, active_page): """
This returns a tab containing the course content.
"""
link = reverse('courseware', args=[course.id]) 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]) link = reverse('info', args=[course.id])
return [CourseTab(tab['name'], link, active_page == "info")] 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(): if user.is_authenticated():
link = reverse('progress', args=[course.id]) link = reverse('progress', args=[course.id])
return [CourseTab(tab['name'], link, active_page == "progress")] return [CourseTab(tab['name'], link, active_page == "progress")]
return [] 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: if settings.WIKI_ENABLED:
link = reverse('course_wiki', args=[course.id]) link = reverse('course_wiki', args=[course.id])
return [CourseTab(tab['name'], link, active_page == 'wiki')] return [CourseTab(tab['name'], link, active_page == 'wiki')]
return [] 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. This tab format only supports the new Berkeley discussion forums.
""" """
...@@ -91,25 +107,25 @@ def _discussion(tab, user, course, active_page): ...@@ -91,25 +107,25 @@ def _discussion(tab, user, course, active_page):
return [] 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 This returns a tab that links to an external discussion service
""" """
return [CourseTab('Discussion', tab['link'], active_page == 'discussion')] 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 # external links are never active
return [CourseTab(tab['name'], tab['link'], False)] 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']]) link = reverse('static_tab', args=[course.id, tab['url_slug']])
active_str = 'static_tab_{0}'.format(tab['url_slug']) active_str = 'static_tab_{0}'.format(tab['url_slug'])
return [CourseTab(tab['name'], link, active_page == active_str)] 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. Generates one tab per textbook. Only displays if user is authenticated.
""" """
...@@ -120,7 +136,8 @@ def _textbooks(tab, user, course, active_page): ...@@ -120,7 +136,8 @@ def _textbooks(tab, user, course, active_page):
for index, textbook in enumerate(course.textbooks)] for index, textbook in enumerate(course.textbooks)]
return [] 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. Generates one tab per textbook. Only displays if user is authenticated.
""" """
...@@ -131,7 +148,8 @@ def _pdf_textbooks(tab, user, course, active_page): ...@@ -131,7 +148,8 @@ def _pdf_textbooks(tab, user, course, active_page):
for index, textbook in enumerate(course.pdf_textbooks)] for index, textbook in enumerate(course.pdf_textbooks)]
return [] 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. Generates one tab per textbook. Only displays if user is authenticated.
""" """
...@@ -142,7 +160,8 @@ def _html_textbooks(tab, user, course, active_page): ...@@ -142,7 +160,8 @@ def _html_textbooks(tab, user, course, active_page):
for index, textbook in enumerate(course.html_textbooks)] for index, textbook in enumerate(course.html_textbooks)]
return [] return []
def _staff_grading(tab, user, course, active_page):
def _staff_grading(tab, user, course, active_page, request):
if has_access(user, course, 'staff'): if has_access(user, course, 'staff'):
link = reverse('staff_grading', args=[course.id]) link = reverse('staff_grading', args=[course.id])
...@@ -157,14 +176,13 @@ def _staff_grading(tab, user, course, active_page): ...@@ -157,14 +176,13 @@ def _staff_grading(tab, user, course, active_page):
return [] return []
def _syllabus(tab, user, course, active_page): def _syllabus(tab, user, course, active_page, request):
"""Display the syllabus tab""" """Display the syllabus tab"""
link = reverse('syllabus', args=[course.id]) link = reverse('syllabus', args=[course.id])
return [CourseTab('Syllabus', link, active_page == 'syllabus')] 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(): if user.is_authenticated():
link = reverse('peer_grading', args=[course.id]) link = reverse('peer_grading', args=[course.id])
tab_name = "Peer grading" tab_name = "Peer grading"
...@@ -178,7 +196,7 @@ def _peer_grading(tab, user, course, active_page): ...@@ -178,7 +196,7 @@ def _peer_grading(tab, user, course, active_page):
return [] 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(): if user.is_authenticated():
link = reverse('open_ended_notifications', args=[course.id]) link = reverse('open_ended_notifications', args=[course.id])
tab_name = "Open Ended Panel" tab_name = "Open Ended Panel"
...@@ -191,15 +209,15 @@ def _combined_open_ended_grading(tab, user, course, active_page): ...@@ -191,15 +209,15 @@ def _combined_open_ended_grading(tab, user, course, active_page):
return tab return tab
return [] 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'): if user.is_authenticated() and settings.MITX_FEATURES.get('ENABLE_STUDENT_NOTES'):
link = reverse('notes', args=[course.id]) link = reverse('notes', args=[course.id])
return [CourseTab(tab['name'], link, active_page == 'notes')] return [CourseTab(tab['name'], link, active_page == 'notes')]
return [] return []
#### Validators
#### Validators
def key_checker(expected_keys): def key_checker(expected_keys):
""" """
Returns a function that checks that specified keys are present in a dict Returns a function that checks that specified keys are present in a dict
...@@ -263,12 +281,15 @@ def validate_tabs(course): ...@@ -263,12 +281,15 @@ def validate_tabs(course):
if len(tabs) < 2: if len(tabs) < 2:
raise InvalidTabsException("Expected at least two tabs. tabs: '{0}'".format(tabs)) raise InvalidTabsException("Expected at least two tabs. tabs: '{0}'".format(tabs))
if tabs[0]['type'] != 'courseware': if tabs[0]['type'] != 'courseware':
raise InvalidTabsException( raise InvalidTabsException(
"Expected first tab to have type 'courseware'. tabs: '{0}'".format(tabs)) "Expected first tab to have type 'courseware'. tabs: '{0}'".format(tabs))
if tabs[1]['type'] != 'course_info': if tabs[1]['type'] != 'course_info':
raise InvalidTabsException( raise InvalidTabsException(
"Expected second tab to have type 'course_info'. tabs: '{0}'".format(tabs)) "Expected second tab to have type 'course_info'. tabs: '{0}'".format(tabs))
for t in tabs: for t in tabs:
if t['type'] not in VALID_TAB_TYPES: if t['type'] not in VALID_TAB_TYPES:
raise InvalidTabsException("Unknown tab type {0}. Known types: {1}" raise InvalidTabsException("Unknown tab type {0}. Known types: {1}"
...@@ -280,12 +301,12 @@ def validate_tabs(course): ...@@ -280,12 +301,12 @@ def validate_tabs(course):
# are actually unique (otherwise, will break active tag code) # 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. Return the tabs to show a particular user, as a list of CourseTab items.
""" """
if not hasattr(course, 'tabs') or not course.tabs: 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 # 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 # load, but not from inside xmodule, since that doesn't (and probably
...@@ -293,12 +314,18 @@ def get_course_tabs(user, course, active_page): ...@@ -293,12 +314,18 @@ def get_course_tabs(user, course, active_page):
validate_tabs(course) validate_tabs(course)
tabs = [] 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 # expect handlers to return lists--handles things that are turned off
# via feature flags, and things like 'textbook' which might generate # via feature flags, and things like 'textbook' which might generate
# multiple tabs. # multiple tabs.
gen = VALID_TAB_TYPES[tab['type']].generator 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 # Instructor tab is special--automatically added if user is staff for the course
if has_access(user, course, 'staff'): if has_access(user, course, 'staff'):
...@@ -314,7 +341,7 @@ def get_discussion_link(course): ...@@ -314,7 +341,7 @@ def get_discussion_link(course):
Return the URL for the discussion tab for the given `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 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, 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 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 discussion view URL. Otherwise, return None to indicate the lack of a
...@@ -330,28 +357,33 @@ def get_discussion_link(course): ...@@ -330,28 +357,33 @@ def get_discussion_link(course):
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id]) 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 # When calling the various _tab methods, can omit the 'type':'blah' from the
# first arg, since that's only used for dispatch # first arg, since that's only used for dispatch
tabs = [] 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: if hasattr(course, 'syllabus_present') and course.syllabus_present:
link = reverse('syllabus', args=[course.id]) link = reverse('syllabus', args=[course.id])
tabs.append(CourseTab('Syllabus', link, active_page == 'syllabus')) 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) discussion_link = get_discussion_link(course)
if discussion_link: if discussion_link:
tabs.append(CourseTab('Discussion', discussion_link, active_page == 'discussion')) 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: 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'): if has_access(user, course, 'staff'):
link = reverse('instructor_dashboard', args=[course.id]) link = reverse('instructor_dashboard', args=[course.id])
...@@ -376,7 +408,6 @@ def get_static_tab_by_slug(course, tab_slug): ...@@ -376,7 +408,6 @@ def get_static_tab_by_slug(course, tab_slug):
def get_static_tab_contents(request, course, tab): def get_static_tab_contents(request, course, tab):
loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug']) 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, field_data_cache = FieldDataCache.cache_for_descriptor_descendents(course.id,
request.user, modulestore().get_instance(course.id, loc), depth=0) request.user, modulestore().get_instance(course.id, loc), depth=0)
......
...@@ -11,6 +11,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -11,6 +11,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
FAKE_REQUEST = None
class ProgressTestCase(TestCase): class ProgressTestCase(TestCase):
...@@ -29,20 +30,20 @@ class ProgressTestCase(TestCase): ...@@ -29,20 +30,20 @@ class ProgressTestCase(TestCase):
def test_progress(self): def test_progress(self):
self.assertEqual(tabs._progress(self.tab, self.mockuser0, self.course, self.assertEqual(tabs._progress(self.tab, self.mockuser0, self.course,
self.active_page0), []) self.active_page0, FAKE_REQUEST), [])
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course, self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
self.active_page1)[0].name, 'same') self.active_page1, FAKE_REQUEST)[0].name, 'same')
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course, self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
self.active_page1)[0].link, self.active_page1, FAKE_REQUEST)[0].link,
reverse('progress', args=[self.course.id])) reverse('progress', args=[self.course.id]))
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course, self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
self.active_page0)[0].is_active, False) self.active_page0, FAKE_REQUEST)[0].is_active, False)
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course, self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
self.active_page1)[0].is_active, True) self.active_page1, FAKE_REQUEST)[0].is_active, True)
class WikiTestCase(TestCase): class WikiTestCase(TestCase):
...@@ -60,26 +61,26 @@ class WikiTestCase(TestCase): ...@@ -60,26 +61,26 @@ class WikiTestCase(TestCase):
def test_wiki_enabled(self): def test_wiki_enabled(self):
self.assertEqual(tabs._wiki(self.tab, self.user, self.assertEqual(tabs._wiki(self.tab, self.user,
self.course, self.active_page1)[0].name, self.course, self.active_page1, FAKE_REQUEST)[0].name,
'same') 'same')
self.assertEqual(tabs._wiki(self.tab, self.user, self.assertEqual(tabs._wiki(self.tab, self.user,
self.course, self.active_page1)[0].link, self.course, self.active_page1, FAKE_REQUEST)[0].link,
reverse('course_wiki', args=[self.course.id])) reverse('course_wiki', args=[self.course.id]))
self.assertEqual(tabs._wiki(self.tab, self.user, self.assertEqual(tabs._wiki(self.tab, self.user,
self.course, self.active_page1)[0].is_active, self.course, self.active_page1, FAKE_REQUEST)[0].is_active,
True) True)
self.assertEqual(tabs._wiki(self.tab, self.user, self.assertEqual(tabs._wiki(self.tab, self.user,
self.course, self.active_page0)[0].is_active, self.course, self.active_page0, FAKE_REQUEST)[0].is_active,
False) False)
@override_settings(WIKI_ENABLED=False) @override_settings(WIKI_ENABLED=False)
def test_wiki_enabled_false(self): def test_wiki_enabled_false(self):
self.assertEqual(tabs._wiki(self.tab, self.user, self.assertEqual(tabs._wiki(self.tab, self.user,
self.course, self.active_page1), []) self.course, self.active_page1, FAKE_REQUEST), [])
class ExternalLinkTestCase(TestCase): class ExternalLinkTestCase(TestCase):
...@@ -95,19 +96,19 @@ class ExternalLinkTestCase(TestCase): ...@@ -95,19 +96,19 @@ class ExternalLinkTestCase(TestCase):
def test_external_link(self): def test_external_link(self):
self.assertEqual(tabs._external_link(self.tabby, self.user, self.assertEqual(tabs._external_link(self.tabby, self.user,
self.course, self.active_page0)[0].name, self.course, self.active_page0, FAKE_REQUEST)[0].name,
'same') 'same')
self.assertEqual(tabs._external_link(self.tabby, self.user, self.assertEqual(tabs._external_link(self.tabby, self.user,
self.course, self.active_page0)[0].link, self.course, self.active_page0, FAKE_REQUEST)[0].link,
'blink') 'blink')
self.assertEqual(tabs._external_link(self.tabby, self.user, self.assertEqual(tabs._external_link(self.tabby, self.user,
self.course, self.active_page0)[0].is_active, self.course, self.active_page0, FAKE_REQUEST)[0].is_active,
False) False)
self.assertEqual(tabs._external_link(self.tabby, self.user, self.assertEqual(tabs._external_link(self.tabby, self.user,
self.course, self.active_page00)[0].is_active, self.course, self.active_page00, FAKE_REQUEST)[0].is_active,
False) False)
...@@ -125,20 +126,20 @@ class StaticTabTestCase(TestCase): ...@@ -125,20 +126,20 @@ class StaticTabTestCase(TestCase):
def test_static_tab(self): def test_static_tab(self):
self.assertEqual(tabs._static_tab(self.tabby, self.user, self.assertEqual(tabs._static_tab(self.tabby, self.user,
self.course, self.active_page1)[0].name, self.course, self.active_page1, FAKE_REQUEST)[0].name,
'same') 'same')
self.assertEqual(tabs._static_tab(self.tabby, self.user, self.assertEqual(tabs._static_tab(self.tabby, self.user,
self.course, self.active_page1)[0].link, self.course, self.active_page1, FAKE_REQUEST)[0].link,
reverse('static_tab', args=[self.course.id, reverse('static_tab', args=[self.course.id,
self.tabby['url_slug']])) self.tabby['url_slug']]))
self.assertEqual(tabs._static_tab(self.tabby, self.user, self.assertEqual(tabs._static_tab(self.tabby, self.user,
self.course, self.active_page1)[0].is_active, self.course, self.active_page1, FAKE_REQUEST)[0].is_active,
True) True)
self.assertEqual(tabs._static_tab(self.tabby, self.user, self.assertEqual(tabs._static_tab(self.tabby, self.user,
self.course, self.active_page0)[0].is_active, self.course, self.active_page0, FAKE_REQUEST)[0].is_active,
False) False)
...@@ -166,45 +167,45 @@ class TextbooksTestCase(TestCase): ...@@ -166,45 +167,45 @@ class TextbooksTestCase(TestCase):
def test_textbooks1(self): def test_textbooks1(self):
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_page0)[0].name, self.course, self.active_page0, FAKE_REQUEST)[0].name,
'Algebra') 'Algebra')
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_page0)[0].link, self.course, self.active_page0, FAKE_REQUEST)[0].link,
reverse('book', args=[self.course.id, 0])) reverse('book', args=[self.course.id, 0]))
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_page0)[0].is_active, self.course, self.active_page0, FAKE_REQUEST)[0].is_active,
True) True)
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_pageX)[0].is_active, self.course, self.active_pageX, FAKE_REQUEST)[0].is_active,
False) False)
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_page1)[1].name, self.course, self.active_page1, FAKE_REQUEST)[1].name,
'Topology') 'Topology')
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_page1)[1].link, self.course, self.active_page1, FAKE_REQUEST)[1].link,
reverse('book', args=[self.course.id, 1])) reverse('book', args=[self.course.id, 1]))
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_page1)[1].is_active, self.course, self.active_page1, FAKE_REQUEST)[1].is_active,
True) True)
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_pageX)[1].is_active, self.course, self.active_pageX, FAKE_REQUEST)[1].is_active,
False) False)
@override_settings(MITX_FEATURES={'ENABLE_TEXTBOOK': False}) @override_settings(MITX_FEATURES={'ENABLE_TEXTBOOK': False})
def test_textbooks0(self): def test_textbooks0(self):
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_pageX), []) self.course, self.active_pageX, FAKE_REQUEST), [])
self.assertEqual(tabs._textbooks(self.tab, self.mockuser0, self.assertEqual(tabs._textbooks(self.tab, self.mockuser0,
self.course, self.active_pageX), []) self.course, self.active_pageX, FAKE_REQUEST), [])
class KeyCheckerTestCase(TestCase): class KeyCheckerTestCase(TestCase):
......
...@@ -728,6 +728,7 @@ def submission_history(request, course_id, student_username, location): ...@@ -728,6 +728,7 @@ def submission_history(request, course_id, student_username, location):
Right now this only works for problems because that's all Right now this only works for problems because that's all
StudentModuleHistory records. StudentModuleHistory records.
""" """
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
......
...@@ -80,7 +80,7 @@ MITX_FEATURES = { ...@@ -80,7 +80,7 @@ MITX_FEATURES = {
'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard) '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_SQL_TRACKING_LOGS': False,
'ENABLE_LMS_MIGRATION': False, 'ENABLE_LMS_MIGRATION': False,
'ENABLE_MANUAL_GIT_RELOAD': False, 'ENABLE_MANUAL_GIT_RELOAD': False,
...@@ -523,6 +523,14 @@ MOCK_STAFF_GRADING = False ...@@ -523,6 +523,14 @@ MOCK_STAFF_GRADING = False
################################# Jasmine ################################### ################################# Jasmine ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' 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 ################################### ################################# Middleware ###################################
# List of finder classes that know how to find static files in # List of finder classes that know how to find static files in
# various locations. # various locations.
...@@ -570,6 +578,9 @@ MIDDLEWARE_CLASSES = ( ...@@ -570,6 +578,9 @@ MIDDLEWARE_CLASSES = (
# catches any uncaught RateLimitExceptions and returns a 403 instead of a 500 # catches any uncaught RateLimitExceptions and returns a 403 instead of a 500
'ratelimitbackend.middleware.RateLimitMiddleware', 'ratelimitbackend.middleware.RateLimitMiddleware',
# For A/B testing
'waffle.middleware.WaffleMiddleware',
) )
############################### Pipeline ####################################### ############################### Pipeline #######################################
...@@ -832,6 +843,9 @@ INSTALLED_APPS = ( ...@@ -832,6 +843,9 @@ INSTALLED_APPS = (
# Foldit integration # Foldit integration
'foldit', 'foldit',
# For A/B testing
'waffle',
# For testing # For testing
'django.contrib.admin', # only used in DEBUG mode 'django.contrib.admin', # only used in DEBUG mode
'django_nose', 'django_nose',
......
...@@ -255,7 +255,7 @@ ANALYTICS_API_KEY = "" ...@@ -255,7 +255,7 @@ ANALYTICS_API_KEY = ""
##### segment-io ###### ##### 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') SEGMENT_IO_LMS_KEY = os.environ.get('SEGMENT_IO_LMS_KEY')
if SEGMENT_IO_LMS_KEY: if SEGMENT_IO_LMS_KEY:
MITX_FEATURES['SEGMENT_IO_LMS'] = True MITX_FEATURES['SEGMENT_IO_LMS'] = True
......
...@@ -23,6 +23,17 @@ nav.course-material { ...@@ -23,6 +23,17 @@ nav.course-material {
list-style: none; list-style: none;
margin-right: 6px; 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 { a {
border-radius: 3px; border-radius: 3px;
color: #555; color: #555;
......
...@@ -13,19 +13,24 @@ def url_class(is_active): ...@@ -13,19 +13,24 @@ def url_class(is_active):
%> %>
<%! from courseware.tabs import get_course_tabs %> <%! from courseware.tabs import get_course_tabs %>
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<% import waffle %>
<nav class="${active_page} course-material"> <nav class="${active_page} course-material">
<div class="inner-wrapper"> <div class="inner-wrapper">
<ol class="course-tabs"> <ol class="course-tabs">
% for tab in get_course_tabs(user, course, active_page): % for tab in get_course_tabs(user, course, active_page, request):
% if waffle.flag_is_active(request, 'visual_treatment') or waffle.flag_is_active(request, 'merge_course_tabs'):
<li class="${"prominent" if tab.name in ("Courseware", "Course Content") else ""}">
% else:
<li> <li>
% endif
<a href="${tab.link | h}" class="${url_class(tab.is_active)}"> <a href="${tab.link | h}" class="${url_class(tab.is_active)}">
${tab.name | h} ${tab.name | h}
% if tab.is_active == True: % if tab.is_active == True:
<span class="sr">, current location</span> <span class="sr">, current location</span>
%endif %endif
% if tab.has_img == True: % if tab.has_img == True:
<img src="${tab.img}"/> <img src="${tab.img}"/>
%endif %endif
</a> </a>
</li> </li>
......
<%! from django.utils.translation import ugettext as _ %> <%!
from django.utils.translation import ugettext as _
import waffle
%>
<h2>${chapter_module.display_name_with_default}</h2> <h2>${chapter_module.display_name_with_default}</h2>
<p>${_("You were most recently in {section_link}. If you\'re done with that, choose another section on the left.").format( <p>${_("You were most recently in {section_link}. If you\'re done with that, choose another section on the left.").format(
...@@ -7,3 +11,31 @@ ...@@ -7,3 +11,31 @@
section_name=prev_section.display_name_with_default, section_name=prev_section.display_name_with_default,
) )
)}</p> )}</p>
% if waffle.flag_is_active(request, 'merge_course_tabs'):
<%! from courseware.courses import get_course_info_section %>
<section class="container">
<div class="info-wrapper">
% if user.is_authenticated():
<section class="updates">
<h1>${_("Course Updates &amp; News")}</h1>
${get_course_info_section(request, course, 'updates')}
</section>
<section aria-label="${_('Handout Navigation')}" class="handouts">
<h1>${course.info_sidebar_name}</h1>
${get_course_info_section(request, course, 'handouts')}
</section>
% else:
<section class="updates">
<h1>${_("Course Updates &amp; News")}</h1>
${get_course_info_section(request, course, 'guest_updates')}
</section>
<section aria-label="${_('Handout Navigation')}" class="handouts">
<h1>${_("Course Handouts")}</h1>
${get_course_info_section(request, course, 'guest_handouts')}
</section>
% endif
</div>
</section>
% endif
...@@ -5,8 +5,11 @@ ...@@ -5,8 +5,11 @@
from courseware.courses import course_image_url, get_course_about_section from courseware.courses import course_image_url, get_course_about_section
from courseware.access import has_access from courseware.access import has_access
from certificates.models import CertificateStatuses from certificates.models import CertificateStatuses
from xmodule.modulestore import MONGO_MODULESTORE_TYPE from xmodule.modulestore import MONGO_MODULESTORE_TYPE
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import waffle
%> %>
<%inherit file="main.html" /> <%inherit file="main.html" />
...@@ -163,7 +166,10 @@ ...@@ -163,7 +166,10 @@
<li class="course-item"> <li class="course-item">
<article class="course ${enrollment.mode}"> <article class="course ${enrollment.mode}">
<% <%
course_target = reverse('info', args=[course.id]) if waffle.flag_is_active(request, 'merge_course_tabs'):
course_target = reverse('courseware', args=[course.id])
else:
course_target = reverse('info', args=[course.id])
%> %>
% if course.id in show_courseware_links_for: % if course.id in show_courseware_links_for:
......
% if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'): % if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'):
<!-- begin Segment.io --> <!-- begin Segment.io -->
<%! from django.core.urlresolvers import reverse %>
<%! import waffle %>
<% active_flags = " + ".join(waffle.get_flags(request)) %>
<!-- <script src="${ reverse('wafflejs') }"></script> -->
<script type="text/javascript"> <script type="text/javascript">
var analytics=analytics||[];analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);var r=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}},i=["identify","track","trackLink","trackForm","trackClick","trackSubmit","pageview","ab","alias","ready"];for(var s=0;s<i.length;s++)analytics[i[s]]=r(i[s])}; var analytics=analytics||[];analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);var r=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}},i=["identify","track","trackLink","trackForm","trackClick","trackSubmit","pageview","ab","alias","ready"];for(var s=0;s<i.length;s++)analytics[i[s]]=r(i[s])};
analytics.load("${ settings.SEGMENT_IO_LMS_KEY }"); analytics.load("${ settings.SEGMENT_IO_LMS_KEY }");
% if user.is_authenticated(): % if user.is_authenticated():
analytics.identify("${ user.id }", {
email : "${ user.email }", analytics.identify("${ user.id }", {
username : "${ user.username }" email : "${ user.email }",
}); username : "${ user.username }",
"Active Flags" : "${ active_flags }",
});
% endif % endif
</script> </script>
......
...@@ -59,6 +59,7 @@ urlpatterns = ('', # nopep8 ...@@ -59,6 +59,7 @@ urlpatterns = ('', # nopep8
url(r'^user_api/', include('user_api.urls')), url(r'^user_api/', include('user_api.urls')),
url(r'^', include('waffle.urls')),
) )
# if settings.MITX_FEATURES.get("MULTIPLE_ENROLLMENT_ROLES"): # if settings.MITX_FEATURES.get("MULTIPLE_ENROLLMENT_ROLES"):
......
...@@ -18,3 +18,4 @@ ...@@ -18,3 +18,4 @@
-e git+https://github.com/edx/codejail.git@0a1b468#egg=codejail -e git+https://github.com/edx/codejail.git@0a1b468#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.2.3#egg=diff_cover -e git+https://github.com/edx/diff-cover.git@v0.2.3#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.0.7#egg=js_test_tool -e git+https://github.com/edx/js-test-tool.git@v0.0.7#egg=js_test_tool
-e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment