Commit f1ccf1c0 by Renzo Lucioni

Integrate split testing and LMS tabs experiments

parent fd06640d
......@@ -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
#### 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)
......
......@@ -11,6 +11,7 @@ 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
class ProgressTestCase(TestCase):
......@@ -29,20 +30,20 @@ class ProgressTestCase(TestCase):
def test_progress(self):
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.active_page1)[0].name, 'same')
self.active_page1, FAKE_REQUEST)[0].name, 'same')
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]))
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.active_page1)[0].is_active, True)
self.active_page1, FAKE_REQUEST)[0].is_active, True)
class WikiTestCase(TestCase):
......@@ -60,26 +61,26 @@ class WikiTestCase(TestCase):
def test_wiki_enabled(self):
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')
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]))
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)
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)
@override_settings(WIKI_ENABLED=False)
def test_wiki_enabled_false(self):
self.assertEqual(tabs._wiki(self.tab, self.user,
self.course, self.active_page1), [])
self.course, self.active_page1, FAKE_REQUEST), [])
class ExternalLinkTestCase(TestCase):
......@@ -95,19 +96,19 @@ class ExternalLinkTestCase(TestCase):
def test_external_link(self):
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')
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')
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)
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)
......@@ -125,20 +126,20 @@ class StaticTabTestCase(TestCase):
def test_static_tab(self):
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')
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,
self.tabby['url_slug']]))
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)
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)
......@@ -166,45 +167,45 @@ class TextbooksTestCase(TestCase):
def test_textbooks1(self):
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')
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]))
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)
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)
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')
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]))
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)
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)
@override_settings(MITX_FEATURES={'ENABLE_TEXTBOOK': False})
def test_textbooks0(self):
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.course, self.active_pageX), [])
self.course, self.active_pageX, FAKE_REQUEST), [])
class KeyCheckerTestCase(TestCase):
......
......@@ -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')
......
......@@ -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',
......
......@@ -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
......
......@@ -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;
......
......@@ -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 %>
<nav class="${active_page} course-material">
<div class="inner-wrapper">
<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>
% endif
<a href="${tab.link | h}" class="${url_class(tab.is_active)}">
${tab.name | h}
% if tab.is_active == True:
<span class="sr">, current location</span>
<span class="sr">, current location</span>
%endif
% if tab.has_img == True:
<img src="${tab.img}"/>
<img src="${tab.img}"/>
%endif
</a>
</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>
<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 @@
section_name=prev_section.display_name_with_default,
)
)}</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 @@
from courseware.courses import course_image_url, get_course_about_section
from courseware.access import has_access
from certificates.models import CertificateStatuses
from xmodule.modulestore import MONGO_MODULESTORE_TYPE
from xmodule.modulestore.django import modulestore
import waffle
%>
<%inherit file="main.html" />
......@@ -163,7 +166,10 @@
<li class="course-item">
<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 settings.MITX_FEATURES.get('SEGMENT_IO_LMS'):
<!-- 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">
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 }");
% if user.is_authenticated():
analytics.identify("${ user.id }", {
email : "${ user.email }",
username : "${ user.username }"
});
analytics.identify("${ user.id }", {
email : "${ user.email }",
username : "${ user.username }",
"Active Flags" : "${ active_flags }",
});
% endif
</script>
......
......@@ -59,6 +59,7 @@ urlpatterns = ('', # nopep8
url(r'^user_api/', include('user_api.urls')),
url(r'^', include('waffle.urls')),
)
# if settings.MITX_FEATURES.get("MULTIPLE_ENROLLMENT_ROLES"):
......
......@@ -18,3 +18,4 @@
-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/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