Commit f1ccf1c0 by Renzo Lucioni

Integrate split testing and LMS tabs experiments

parent fd06640d
...@@ -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