Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
c124a330
Commit
c124a330
authored
Sep 25, 2013
by
Renzo Lucioni
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #829 from edx/renzo/ab-testing
Split Testing
parents
82af8d4b
ac0e65ab
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
287 additions
and
158 deletions
+287
-158
CHANGELOG.rst
+2
-0
lms/djangoapps/courseware/tabs.py
+65
-34
lms/djangoapps/courseware/tests/test_tabs.py
+131
-113
lms/djangoapps/courseware/views.py
+1
-0
lms/envs/common.py
+15
-1
lms/envs/dev.py
+1
-1
lms/static/sass/course/layout/_courseware_header.scss
+11
-0
lms/templates/courseware/course_navigation.html
+8
-3
lms/templates/courseware/welcome-back.html
+33
-1
lms/templates/dashboard.html
+7
-1
lms/templates/widgets/segment-io.html
+11
-4
lms/urls.py
+1
-0
requirements/edx/github.txt
+1
-0
No files found.
CHANGELOG.rst
View file @
c124a330
...
...
@@ -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.
...
...
lms/djangoapps/courseware/tabs.py
View file @
c124a330
...
...
@@ -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 disc
s
ussions 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
)
...
...
lms/djangoapps/courseware/tests/test_tabs.py
View file @
c124a330
...
...
@@ -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
(
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
(
tab_constructor
(
self
.
stagnation_page
,
self
.
course
,
self
.
anonymous_user
),
[])
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'
)
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
)
self
.
assertEqual
(
tab_list
[
0
]
.
name
,
'same'
)
self
.
assertEqual
(
tabs
.
_static_tab
(
self
.
tabby
,
self
.
user
,
self
.
course
,
self
.
active_page1
)[
0
]
.
is_active
,
True
)
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_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'
)
self
.
assertEqual
(
tabs
.
_textbooks
(
self
.
tab
,
self
.
mockuser1
,
self
.
course
,
self
.
active_page0
)[
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
,
True
)
self
.
assertEqual
(
tabs
.
_textbooks
(
self
.
tab
,
self
.
mockuser1
,
self
.
course
,
self
.
active_pageX
)[
0
]
.
is_active
,
False
)
self
.
assertEqual
(
tabs
.
_textbooks
(
self
.
tab
,
self
.
mockuser1
,
self
.
course
,
self
.
active_page1
)[
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
]))
self
.
assertEqual
(
tabs
.
_textbooks
(
self
.
tab
,
self
.
mockuser1
,
self
.
course
,
self
.
active_page1
)[
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
.
textbook_0
,
self
.
course
,
self
.
user
,
tab
=
self
.
tab
,
generator
=
tabs
.
_textbooks
)
self
.
assertEqual
(
tab_list
[
0
]
.
name
,
'Algebra'
)
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
)
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
)
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
)
tab_list
=
tab_constructor
(
self
.
textbook_1
,
self
.
course
,
self
.
user
,
tab
=
self
.
tab
,
generator
=
tabs
.
_textbooks
)
self
.
assertEqual
(
tab_list
[
1
]
.
name
,
'Topology'
)
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
)
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
)
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
):
...
...
lms/djangoapps/courseware/views.py
View file @
c124a330
...
...
@@ -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'
)
...
...
lms/envs/common.py
View file @
c124a330
...
...
@@ -80,7 +80,7 @@ MITX_FEATURES = {
'ENABLE_PSYCHOMETRICS'
:
False
,
# real-time psychometrics (eg item response theory analysis in instructor dashboard)
'ENABLE_DJANGO_ADMIN_SITE'
:
Fals
e
,
# set true to enable django's admin site, even on prod (e.g. for course ops)
'ENABLE_DJANGO_ADMIN_SITE'
:
Tru
e
,
# 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'
,
...
...
lms/envs/dev.py
View file @
c124a330
...
...
@@ -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
...
...
lms/static/sass/course/layout/_courseware_header.scss
View file @
c124a330
...
...
@@ -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
;
...
...
lms/templates/courseware/course_navigation.html
View file @
c124a330
...
...
@@ -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>
...
...
lms/templates/courseware/welcome-back.html
View file @
c124a330
<
%!
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
&
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
&
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
lms/templates/dashboard.html
View file @
c124a330
...
...
@@ -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:
...
...
lms/templates/widgets/segment-io.html
View file @
c124a330
% 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
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>
...
...
lms/urls.py
View file @
c124a330
...
...
@@ -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"):
...
...
requirements/edx/github.txt
View file @
c124a330
...
...
@@ -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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment