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
2561c010
Commit
2561c010
authored
Mar 28, 2014
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #3064 from edx/nimisha/studio-reorder-and-hide-tabs
Nimisha/studio reorder and hide tabs
parents
6bcae9aa
1bd6792a
Hide whitespace changes
Inline
Side-by-side
Showing
26 changed files
with
866 additions
and
318 deletions
+866
-318
CHANGELOG.rst
+2
-0
cms/djangoapps/contentstore/features/pages.feature
+32
-7
cms/djangoapps/contentstore/features/pages.py
+77
-32
cms/djangoapps/contentstore/management/commands/tests/test_git_export.py
+2
-2
cms/djangoapps/contentstore/tests/test_contentstore.py
+22
-11
cms/djangoapps/contentstore/tests/test_course_settings.py
+1
-1
cms/djangoapps/contentstore/tests/test_export_git.py
+1
-1
cms/djangoapps/contentstore/tests/test_orphan.py
+1
-1
cms/djangoapps/contentstore/tests/utils.py
+17
-2
cms/djangoapps/contentstore/views/course.py
+3
-3
cms/djangoapps/contentstore/views/tabs.py
+113
-68
cms/djangoapps/contentstore/views/tests/test_course_index.py
+2
-2
cms/djangoapps/contentstore/views/tests/test_tabs.py
+168
-1
cms/djangoapps/contentstore/views/tests/test_textbooks.py
+10
-15
cms/envs/common.py
+4
-2
cms/static/coffee/src/views/tabs.coffee
+31
-4
cms/static/sass/views/_static-pages.scss
+14
-15
cms/templates/edit-tabs.html
+70
-32
common/lib/xmodule/xmodule/modulestore/mongo/base.py
+1
-1
common/lib/xmodule/xmodule/modulestore/xml.py
+1
-1
common/lib/xmodule/xmodule/tabs.py
+184
-79
common/lib/xmodule/xmodule/tests/test_tabs.py
+103
-29
lms/djangoapps/courseware/tests/test_tabs.py
+1
-1
lms/djangoapps/courseware/views.py
+1
-1
lms/envs/common.py
+4
-6
lms/templates/courseware/course_navigation.html
+1
-1
No files found.
CHANGELOG.rst
View file @
2561c010
...
@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
...
@@ -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
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
the top. Include a label indicating the component affected.
Studio: Add ability to reorder Pages and hide the Wiki page. STUD-1375
Blades: Added template for iFrames. BLD-611.
Blades: Added template for iFrames. BLD-611.
Studio: Support for viewing built-in tabs on the Pages page. STUD-1193
Studio: Support for viewing built-in tabs on the Pages page. STUD-1193
...
...
cms/djangoapps/contentstore/features/pages.feature
View file @
2561c010
...
@@ -15,10 +15,6 @@ Feature: CMS.Pages
...
@@ -15,10 +15,6 @@ Feature: CMS.Pages
When
I confirm the prompt
When
I confirm the prompt
Then
I should not see any static pages
Then
I should not see any static pages
Scenario
:
Users can see built-in pages
Given
I have opened the pages page in a new course
Then
I should see the default built-in pages
# Safari won't update the name properly
# Safari won't update the name properly
@skip_safari
@skip_safari
Scenario
:
Users can edit static pages
Scenario
:
Users can edit static pages
...
@@ -31,7 +27,36 @@ Feature: CMS.Pages
...
@@ -31,7 +27,36 @@ Feature: CMS.Pages
@skip_safari
@skip_safari
Scenario
:
Users can reorder static pages
Scenario
:
Users can reorder static pages
Given
I have created two different static pages
Given
I have created two different static pages
When
I
reorder the static tabs
When
I
drag the first static page to the last
Then
the static
tabs are in the reverse order
Then
the static
pages are switched
And
I reload the page
And
I reload the page
Then
the static tabs are in the reverse order
Then
the static pages are switched
Scenario
:
Users can reorder built-in pages
Given
I have opened the pages page in a new course
Then
the built-in pages are in the default order
When
I drag the first page to the last
Then
the built-in pages are switched
And
I reload the page
Then
the built-in pages are switched
Scenario
:
Users can reorder built-in pages amongst static pages
Given
I have created two different static pages
Then
the pages are in the default order
When
I drag the first page to the last
Then
the pages are switched
And
I reload the page
Then
the pages are switched
Scenario
:
Users can toggle visibility on hideable pages
Given
I have opened the pages page in a new course
Then
I should see the
"wiki"
page as
"visible"
When
I toggle the visibility of the
"wiki"
page
Then
I should see the
"wiki"
page as
"hidden"
And
I reload the page
Then
I should see the
"wiki"
page as
"hidden"
When
I toggle the visibility of the
"wiki"
page
Then
I should see the
"wiki"
page as
"visible"
And
I reload the page
Then
I should see the
"wiki"
page as
"visible"
cms/djangoapps/contentstore/features/pages.py
View file @
2561c010
...
@@ -3,7 +3,10 @@
...
@@ -3,7 +3,10 @@
# pylint: disable=W0613
# pylint: disable=W0613
from
lettuce
import
world
,
step
from
lettuce
import
world
,
step
from
nose.tools
import
assert_equal
# pylint: disable=E0611
from
nose.tools
import
assert_equal
,
assert_in
# pylint: disable=E0611
CSS_FOR_TAB_ELEMENT
=
"li[data-tab-id='{0}'] input.toggle-checkbox"
@step
(
u'I go to the pages page$'
)
@step
(
u'I go to the pages page$'
)
...
@@ -33,15 +36,6 @@ def not_see_any_static_pages(step):
...
@@ -33,15 +36,6 @@ def not_see_any_static_pages(step):
assert
(
world
.
is_css_not_present
(
pages_css
,
wait_time
=
30
))
assert
(
world
.
is_css_not_present
(
pages_css
,
wait_time
=
30
))
@step
(
u'I should see the default built-in pages'
)
def
see_default_built_in_pages
(
step
):
expected_pages
=
[
'Courseware'
,
'Course Info'
,
'Discussion'
,
'Wiki'
,
'Progress'
]
pages
=
world
.
css_find
(
"div.course-nav-tab-header h3.title"
)
assert_equal
(
len
(
expected_pages
),
len
(
pages
))
for
i
,
page_name
in
enumerate
(
expected_pages
):
assert_equal
(
pages
[
i
]
.
text
,
page_name
)
@step
(
u'I "(edit|delete)" the static page$'
)
@step
(
u'I "(edit|delete)" the static page$'
)
def
click_edit_or_delete
(
step
,
edit_or_delete
):
def
click_edit_or_delete
(
step
,
edit_or_delete
):
button_css
=
'ul.component-actions a.
%
s-button'
%
edit_or_delete
button_css
=
'ul.component-actions a.
%
s-button'
%
edit_or_delete
...
@@ -60,50 +54,101 @@ def change_name(step, new_name):
...
@@ -60,50 +54,101 @@ def change_name(step, new_name):
world
.
css_click
(
save_button
)
world
.
css_click
(
save_button
)
@step
(
u'I reorder the static tabs'
)
@step
(
u'I drag the first static page to the last$'
)
def
reorder_tabs
(
_step
):
def
drag_first_static_page_to_last
(
step
):
# For some reason, the drag_and_drop method did not work in this case.
drag_first_to_last_with_css
(
'.component'
)
draggables
=
world
.
css_find
(
'.component .drag-handle'
)
source
=
draggables
.
first
target
=
draggables
.
last
source
.
action_chains
.
click_and_hold
(
source
.
_element
)
.
perform
()
# pylint: disable=protected-access
source
.
action_chains
.
move_to_element_with_offset
(
target
.
_element
,
0
,
50
)
.
perform
()
# pylint: disable=protected-access
source
.
action_chains
.
release
()
.
perform
()
@step
(
u'I have created a static page'
)
@step
(
u'I have created a static page
$
'
)
def
create_static_page
(
step
):
def
create_static_page
(
step
):
step
.
given
(
'I have opened the pages page in a new course'
)
step
.
given
(
'I have opened the pages page in a new course'
)
step
.
given
(
'I add a new static page'
)
step
.
given
(
'I add a new static page'
)
@step
(
u'I have opened the pages page in a new course'
)
@step
(
u'I have opened the pages page in a new course
$
'
)
def
open_pages_page_in_new_course
(
step
):
def
open_pages_page_in_new_course
(
step
):
step
.
given
(
'I have opened a new course in Studio'
)
step
.
given
(
'I have opened a new course in Studio'
)
step
.
given
(
'I go to the pages page'
)
step
.
given
(
'I go to the pages page'
)
@step
(
u'I have created two different static pages'
)
@step
(
u'I have created two different static pages
$
'
)
def
create_two_pages
(
step
):
def
create_two_pages
(
step
):
step
.
given
(
'I have created a static page'
)
step
.
given
(
'I have created a static page'
)
step
.
given
(
'I "edit" the static page'
)
step
.
given
(
'I "edit" the static page'
)
step
.
given
(
'I change the name to "First"'
)
step
.
given
(
'I change the name to "First"'
)
step
.
given
(
'I add a new static page'
)
step
.
given
(
'I add a new static page'
)
# Verify order of
tab
s
# Verify order of
page
s
_verify_
tab
_names
(
'First'
,
'Empty'
)
_verify_
page
_names
(
'First'
,
'Empty'
)
@step
(
u'the static
tabs are in the reverse order
'
)
@step
(
u'the static
pages are switched$
'
)
def
tabs_in_reverse_order
(
step
):
def
static_pages_are_switched
(
step
):
_verify_
tab
_names
(
'Empty'
,
'First'
)
_verify_
page
_names
(
'Empty'
,
'First'
)
def
_verify_
tab
_names
(
first
,
second
):
def
_verify_
page
_names
(
first
,
second
):
world
.
wait_for
(
world
.
wait_for
(
func
=
lambda
_
:
len
(
world
.
css_find
(
'.xmodule_StaticTabModule'
))
==
2
,
func
=
lambda
_
:
len
(
world
.
css_find
(
'.xmodule_StaticTabModule'
))
==
2
,
timeout
=
200
,
timeout
=
200
,
timeout_msg
=
"Timed out waiting for two
tab
s to be present"
timeout_msg
=
"Timed out waiting for two
page
s to be present"
)
)
tabs
=
world
.
css_find
(
'.xmodule_StaticTabModule'
)
pages
=
world
.
css_find
(
'.xmodule_StaticTabModule'
)
assert
tabs
[
0
]
.
text
==
first
assert_equal
(
pages
[
0
]
.
text
,
first
)
assert
tabs
[
1
]
.
text
==
second
assert_equal
(
pages
[
1
]
.
text
,
second
)
@step
(
u'the built-in pages are in the default order$'
)
def
built_in_pages_in_default_order
(
step
):
expected_pages
=
[
'Courseware'
,
'Course Info'
,
'Discussion'
,
'Wiki'
,
'Progress'
]
see_pages_in_expected_order
(
expected_pages
)
@step
(
u'the built-in pages are switched$'
)
def
built_in_pages_switched
(
step
):
expected_pages
=
[
'Courseware'
,
'Course Info'
,
'Wiki'
,
'Progress'
,
'Discussion'
]
see_pages_in_expected_order
(
expected_pages
)
@step
(
u'the pages are in the default order$'
)
def
pages_in_default_order
(
step
):
expected_pages
=
[
'Courseware'
,
'Course Info'
,
'Discussion'
,
'Wiki'
,
'Progress'
,
'First'
,
'Empty'
]
see_pages_in_expected_order
(
expected_pages
)
@step
(
u'the pages are switched$$'
)
def
pages_are_switched
(
step
):
expected_pages
=
[
'Courseware'
,
'Course Info'
,
'Wiki'
,
'Progress'
,
'First'
,
'Empty'
,
'Discussion'
]
see_pages_in_expected_order
(
expected_pages
)
@step
(
u'I drag the first page to the last$'
)
def
drag_first_page_to_last
(
step
):
drag_first_to_last_with_css
(
'.is-movable'
)
@step
(
u'I should see the "([^"]*)" page as "(visible|hidden)"$'
)
def
page_is_visible_or_hidden
(
step
,
page_id
,
visible_or_hidden
):
hidden
=
visible_or_hidden
==
"hidden"
assert_equal
(
world
.
css_find
(
CSS_FOR_TAB_ELEMENT
.
format
(
page_id
))
.
checked
,
hidden
)
@step
(
u'I toggle the visibility of the "([^"]*)" page$'
)
def
page_toggle_visibility
(
step
,
page_id
):
world
.
css_find
(
CSS_FOR_TAB_ELEMENT
.
format
(
page_id
))[
0
]
.
click
()
def
drag_first_to_last_with_css
(
css_class
):
# For some reason, the drag_and_drop method did not work in this case.
draggables
=
world
.
css_find
(
css_class
+
' .drag-handle'
)
source
=
draggables
.
first
target
=
draggables
.
last
source
.
action_chains
.
click_and_hold
(
source
.
_element
)
.
perform
()
# pylint: disable=protected-access
source
.
action_chains
.
move_to_element_with_offset
(
target
.
_element
,
0
,
50
)
.
perform
()
# pylint: disable=protected-access
source
.
action_chains
.
release
()
.
perform
()
def
see_pages_in_expected_order
(
page_names_in_expected_order
):
pages
=
world
.
css_find
(
"li.course-tab"
)
assert_equal
(
len
(
page_names_in_expected_order
),
len
(
pages
))
for
i
,
page_name
in
enumerate
(
page_names_in_expected_order
):
assert_in
(
page_name
,
pages
[
i
]
.
text
)
cms/djangoapps/contentstore/management/commands/tests/test_git_export.py
View file @
2561c010
...
@@ -150,8 +150,8 @@ class TestGitExport(CourseTestCase):
...
@@ -150,8 +150,8 @@ class TestGitExport(CourseTestCase):
'--format=
%
an|
%
ae'
],
cwd
=
cwd
)
'--format=
%
an|
%
ae'
],
cwd
=
cwd
)
self
.
assertEqual
(
expect_string
,
git_log
)
self
.
assertEqual
(
expect_string
,
git_log
)
# Make changes to course so there is something commit
# Make changes to course so there is something
to
commit
self
.
populate
C
ourse
()
self
.
populate
_c
ourse
()
git_export_utils
.
export_to_git
(
git_export_utils
.
export_to_git
(
self
.
course
.
id
,
self
.
course
.
id
,
'file://{0}'
.
format
(
self
.
bare_repo_dir
),
'file://{0}'
.
format
(
self
.
bare_repo_dir
),
...
...
cms/djangoapps/contentstore/tests/test_contentstore.py
View file @
2561c010
...
@@ -400,23 +400,34 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
...
@@ -400,23 +400,34 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
course
=
module_store
.
get_item
(
course_location
)
course
=
module_store
.
get_item
(
course_location
)
# reverse the ordering
# reverse the ordering of the static tabs
reverse_tabs
=
[]
reverse_static_tabs
=
[]
built_in_tabs
=
[]
for
tab
in
course
.
tabs
:
for
tab
in
course
.
tabs
:
if
tab
[
'type'
]
==
'static_tab'
:
if
tab
[
'type'
]
==
'static_tab'
:
reverse_tabs
.
insert
(
0
,
unicode
(
self
.
_get_tab_locator
(
course
,
tab
)))
reverse_static_tabs
.
insert
(
0
,
tab
)
else
:
self
.
client
.
ajax_post
(
new_location
.
url_reverse
(
'tabs'
),
{
'tabs'
:
reverse_tabs
})
built_in_tabs
.
append
(
tab
)
# create the requested tab_id_locators list
tab_id_locators
=
[
{
'tab_id'
:
tab
.
tab_id
}
for
tab
in
built_in_tabs
]
tab_id_locators
.
extend
([
{
'tab_locator'
:
unicode
(
self
.
_get_tab_locator
(
course
,
tab
))
}
for
tab
in
reverse_static_tabs
])
self
.
client
.
ajax_post
(
new_location
.
url_reverse
(
'tabs'
),
{
'tabs'
:
tab_id_locators
})
course
=
module_store
.
get_item
(
course_location
)
course
=
module_store
.
get_item
(
course_location
)
# compare to make sure that the tabs information is in the expected order after the server call
# compare to make sure that the tabs information is in the expected order after the server call
course_tabs
=
[]
new_static_tabs
=
[
tab
for
tab
in
course
.
tabs
if
(
tab
[
'type'
]
==
'static_tab'
)]
for
tab
in
course
.
tabs
:
self
.
assertEqual
(
reverse_static_tabs
,
new_static_tabs
)
if
tab
[
'type'
]
==
'static_tab'
:
course_tabs
.
append
(
unicode
(
self
.
_get_tab_locator
(
course
,
tab
)))
self
.
assertEqual
(
reverse_tabs
,
course_tabs
)
def
test_static_tab_deletion
(
self
):
def
test_static_tab_deletion
(
self
):
module_store
,
course_location
,
_
=
self
.
_create_static_tabs
()
module_store
,
course_location
,
_
=
self
.
_create_static_tabs
()
...
...
cms/djangoapps/contentstore/tests/test_course_settings.py
View file @
2561c010
...
@@ -410,7 +410,7 @@ class CourseGradingTest(CourseTestCase):
...
@@ -410,7 +410,7 @@ class CourseGradingTest(CourseTestCase):
"""
"""
Populate the course, grab a section, get the url for the assignment type access
Populate the course, grab a section, get the url for the assignment type access
"""
"""
self
.
populate
C
ourse
()
self
.
populate
_c
ourse
()
sections
=
get_modulestore
(
self
.
course_location
)
.
get_items
(
sections
=
get_modulestore
(
self
.
course_location
)
.
get_items
(
self
.
course_location
.
replace
(
category
=
"sequential"
,
name
=
None
)
self
.
course_location
.
replace
(
category
=
"sequential"
,
name
=
None
)
)
)
...
...
cms/djangoapps/contentstore/tests/test_export_git.py
View file @
2561c010
...
@@ -102,7 +102,7 @@ class TestExportGit(CourseTestCase):
...
@@ -102,7 +102,7 @@ class TestExportGit(CourseTestCase):
subprocess
.
check_output
([
'git'
,
'--bare'
,
'init'
,
],
cwd
=
bare_repo_dir
)
subprocess
.
check_output
([
'git'
,
'--bare'
,
'init'
,
],
cwd
=
bare_repo_dir
)
self
.
populate
C
ourse
()
self
.
populate
_c
ourse
()
self
.
course_module
.
giturl
=
'file://{}'
.
format
(
bare_repo_dir
)
self
.
course_module
.
giturl
=
'file://{}'
.
format
(
bare_repo_dir
)
get_modulestore
(
self
.
course_module
.
location
)
.
update_item
(
self
.
course_module
)
get_modulestore
(
self
.
course_module
.
location
)
.
update_item
(
self
.
course_module
)
...
...
cms/djangoapps/contentstore/tests/test_orphan.py
View file @
2561c010
...
@@ -75,7 +75,7 @@ class TestOrphan(CourseTestCase):
...
@@ -75,7 +75,7 @@ class TestOrphan(CourseTestCase):
"""
"""
Test that auth restricts get and delete appropriately
Test that auth restricts get and delete appropriately
"""
"""
test_user_client
,
test_user
=
self
.
create
NonStaffAuthedUserC
lient
()
test_user_client
,
test_user
=
self
.
create
_non_staff_authed_user_c
lient
()
CourseEnrollment
.
enroll
(
test_user
,
self
.
course
.
location
.
course_id
)
CourseEnrollment
.
enroll
(
test_user
,
self
.
course
.
location
.
course_id
)
locator
=
loc_mapper
()
.
translate_location
(
self
.
course
.
location
.
course_id
,
self
.
course
.
location
,
False
,
True
)
locator
=
loc_mapper
()
.
translate_location
(
self
.
course
.
location
.
course_id
,
self
.
course
.
location
,
False
,
True
)
orphan_url
=
locator
.
url_reverse
(
'orphan/'
,
''
)
orphan_url
=
locator
.
url_reverse
(
'orphan/'
,
''
)
...
...
cms/djangoapps/contentstore/tests/utils.py
View file @
2561c010
...
@@ -12,6 +12,7 @@ from django.test.utils import override_settings
...
@@ -12,6 +12,7 @@ from django.test.utils import override_settings
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
contentstore.tests.modulestore_config
import
TEST_MODULESTORE
from
contentstore.tests.modulestore_config
import
TEST_MODULESTORE
from
contentstore.utils
import
get_modulestore
from
xmodule.modulestore.django
import
loc_mapper
from
xmodule.modulestore.django
import
loc_mapper
...
@@ -95,8 +96,9 @@ class CourseTestCase(ModuleStoreTestCase):
...
@@ -95,8 +96,9 @@ class CourseTestCase(ModuleStoreTestCase):
self
.
course_locator
=
loc_mapper
()
.
translate_location
(
self
.
course_locator
=
loc_mapper
()
.
translate_location
(
self
.
course
.
location
.
course_id
,
self
.
course
.
location
,
False
,
True
self
.
course
.
location
.
course_id
,
self
.
course
.
location
,
False
,
True
)
)
self
.
store
=
get_modulestore
(
self
.
course
.
location
)
def
create
NonStaffAuthedUserC
lient
(
self
):
def
create
_non_staff_authed_user_c
lient
(
self
):
"""
"""
Create a non-staff user, log them in, and return the client, user to use for testing.
Create a non-staff user, log them in, and return the client, user to use for testing.
"""
"""
...
@@ -114,7 +116,7 @@ class CourseTestCase(ModuleStoreTestCase):
...
@@ -114,7 +116,7 @@ class CourseTestCase(ModuleStoreTestCase):
client
.
login
(
username
=
uname
,
password
=
password
)
client
.
login
(
username
=
uname
,
password
=
password
)
return
client
,
nonstaff
return
client
,
nonstaff
def
populate
C
ourse
(
self
):
def
populate
_c
ourse
(
self
):
"""
"""
Add 2 chapters, 4 sections, 8 verticals, 16 problems to self.course (branching 2)
Add 2 chapters, 4 sections, 8 verticals, 16 problems to self.course (branching 2)
"""
"""
...
@@ -126,3 +128,16 @@ class CourseTestCase(ModuleStoreTestCase):
...
@@ -126,3 +128,16 @@ class CourseTestCase(ModuleStoreTestCase):
descend
(
child
,
stack
)
descend
(
child
,
stack
)
descend
(
self
.
course
,
[
'chapter'
,
'sequential'
,
'vertical'
,
'problem'
])
descend
(
self
.
course
,
[
'chapter'
,
'sequential'
,
'vertical'
,
'problem'
])
def
reload_course
(
self
):
"""
Reloads the course object from the database
"""
self
.
course
=
self
.
store
.
get_item
(
self
.
course
.
location
)
def
save_course
(
self
):
"""
Updates the course object in the database
"""
self
.
course
.
save
()
self
.
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
cms/djangoapps/contentstore/views/course.py
View file @
2561c010
...
@@ -918,9 +918,9 @@ def textbooks_detail_handler(request, tid, tag=None, package_id=None, branch=Non
...
@@ -918,9 +918,9 @@ def textbooks_detail_handler(request, tid, tag=None, package_id=None, branch=Non
if
not
textbook
:
if
not
textbook
:
return
JsonResponse
(
status
=
404
)
return
JsonResponse
(
status
=
404
)
i
=
course
.
pdf_textbooks
.
index
(
textbook
)
i
=
course
.
pdf_textbooks
.
index
(
textbook
)
new
_textbooks
=
course
.
pdf_textbooks
[
0
:
i
]
remaining
_textbooks
=
course
.
pdf_textbooks
[
0
:
i
]
new
_textbooks
.
extend
(
course
.
pdf_textbooks
[
i
+
1
:])
remaining
_textbooks
.
extend
(
course
.
pdf_textbooks
[
i
+
1
:])
course
.
pdf_textbooks
=
new
_textbooks
course
.
pdf_textbooks
=
remaining
_textbooks
store
.
update_item
(
course
,
request
.
user
.
id
)
store
.
update_item
(
course
,
request
.
user
.
id
)
return
JsonResponse
()
return
JsonResponse
()
...
...
cms/djangoapps/contentstore/views/tabs.py
View file @
2561c010
...
@@ -14,11 +14,9 @@ from edxmako.shortcuts import render_to_response
...
@@ -14,11 +14,9 @@ from edxmako.shortcuts import render_to_response
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
loc_mapper
from
xmodule.modulestore.django
import
loc_mapper
from
xmodule.modulestore.locator
import
BlockUsageLocator
from
xmodule.modulestore.locator
import
BlockUsageLocator
from
xmodule.tabs
import
CourseTabList
,
StaticTab
,
CourseTab
from
xmodule.tabs
import
CourseTabList
,
StaticTab
,
CourseTab
,
InvalidTabsException
from
..utils
import
get_modulestore
from
..utils
import
get_modulestore
,
get_lms_link_for_item
from
django.utils.translation
import
ugettext
as
_
__all__
=
[
'tabs_handler'
]
__all__
=
[
'tabs_handler'
]
...
@@ -53,85 +51,132 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N
...
@@ -53,85 +51,132 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N
raise
NotImplementedError
(
'coming soon'
)
raise
NotImplementedError
(
'coming soon'
)
else
:
else
:
if
'tabs'
in
request
.
json
:
if
'tabs'
in
request
.
json
:
def
get_location_for_tab
(
tab
):
return
reorder_tabs_handler
(
course_item
,
request
)
""" Returns the location (old-style) for a tab. """
elif
'tab_id_locator'
in
request
.
json
:
return
loc_mapper
()
.
translate_locator_to_location
(
BlockUsageLocator
(
tab
))
return
edit_tab_handler
(
course_item
,
request
)
tabs
=
request
.
json
[
'tabs'
]
# get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we will inadvertently drop some!
existing_static_tabs
=
[
t
for
t
in
course_item
.
tabs
if
t
[
'type'
]
==
'static_tab'
]
if
len
(
existing_static_tabs
)
!=
len
(
tabs
):
return
JsonResponse
(
{
"error"
:
"number of tabs must be {}"
.
format
(
len
(
existing_static_tabs
))},
status
=
400
)
# load all reference tabs, return BadRequest if we can't find any of them
tab_items
=
[]
for
tab
in
tabs
:
item
=
modulestore
(
'direct'
)
.
get_item
(
get_location_for_tab
(
tab
))
if
item
is
None
:
return
JsonResponse
(
{
"error"
:
"no tab for found location {}"
.
format
(
tab
)},
status
=
400
)
tab_items
.
append
(
item
)
# now just go through the existing course_tabs and re-order the static tabs
reordered_tabs
=
[]
static_tab_idx
=
0
for
tab
in
course_item
.
tabs
:
if
isinstance
(
tab
,
StaticTab
):
reordered_tabs
.
append
(
StaticTab
(
name
=
tab_items
[
static_tab_idx
]
.
display_name
,
url_slug
=
tab_items
[
static_tab_idx
]
.
location
.
name
,
)
)
static_tab_idx
+=
1
else
:
reordered_tabs
.
append
(
tab
)
# OK, re-assemble the static tabs in the new order
course_item
.
tabs
=
reordered_tabs
modulestore
(
'direct'
)
.
update_item
(
course_item
,
request
.
user
.
id
)
return
JsonResponse
()
else
:
else
:
raise
NotImplementedError
(
'Creating or changing tab content is not supported.'
)
raise
NotImplementedError
(
'Creating or changing tab content is not supported.'
)
elif
request
.
method
==
'GET'
:
# assume html
elif
request
.
method
==
'GET'
:
# assume html
# get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs
# get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs
#
we do this because this is also the order in which items are displayed in the
LMS
#
present in the same order they are displayed in
LMS
static_tabs
=
[]
tabs_to_render
=
[]
built_in_tabs
=
[]
for
tab
in
CourseTabList
.
iterate_displayable_cms
(
for
tab
in
CourseTabList
.
iterate_displayable
(
course_item
,
settings
,
include_instructor_tab
=
False
):
course_item
,
settings
,
):
if
isinstance
(
tab
,
StaticTab
):
if
isinstance
(
tab
,
StaticTab
):
# static tab needs its locator information to render itself as an xmodule
static_tab_loc
=
old_location
.
replace
(
category
=
'static_tab'
,
name
=
tab
.
url_slug
)
static_tab_loc
=
old_location
.
replace
(
category
=
'static_tab'
,
name
=
tab
.
url_slug
)
static_tabs
.
append
(
modulestore
(
'direct'
)
.
get_item
(
static_tab_loc
))
tab
.
locator
=
loc_mapper
()
.
translate_location
(
else
:
course_item
.
location
.
course_id
,
static_tab_loc
,
False
,
True
built_in_tabs
.
append
(
tab
)
)
tabs_to_render
.
append
(
tab
)
# create a list of components for each static tab
components
=
[
loc_mapper
()
.
translate_location
(
course_item
.
location
.
course_id
,
static_tab
.
location
,
False
,
True
)
for
static_tab
in
static_tabs
]
return
render_to_response
(
'edit-tabs.html'
,
{
return
render_to_response
(
'edit-tabs.html'
,
{
'context_course'
:
course_item
,
'context_course'
:
course_item
,
'
built_in_tabs'
:
built_in_tabs
,
'
tabs_to_render'
:
tabs_to_render
,
'co
mponents'
:
components
,
'co
urse_locator'
:
locator
,
'
course_locator'
:
locator
'
lms_link'
:
get_lms_link_for_item
(
course_item
.
location
),
})
})
else
:
else
:
return
HttpResponseNotFound
()
return
HttpResponseNotFound
()
def
reorder_tabs_handler
(
course_item
,
request
):
"""
Helper function for handling reorder of tabs request
"""
# Tabs are identified by tab_id or locators.
# The locators are used to identify static tabs since they are xmodules.
# Although all tabs have tab_ids, newly created static tabs do not know
# their tab_ids since the xmodule editor uses only locators to identify new objects.
requested_tab_id_locators
=
request
.
json
[
'tabs'
]
# original tab list in original order
old_tab_list
=
course_item
.
tabs
# create a new list in the new order
new_tab_list
=
[]
for
tab_id_locator
in
requested_tab_id_locators
:
tab
=
get_tab_by_tab_id_locator
(
old_tab_list
,
tab_id_locator
)
if
tab
is
None
:
return
JsonResponse
(
{
"error"
:
"Tab with id_locator '{0}' does not exist."
.
format
(
tab_id_locator
)},
status
=
400
)
new_tab_list
.
append
(
tab
)
# the old_tab_list may contain additional tabs that were not rendered in the UI because of
# global or course settings. so add those to the end of the list.
non_displayed_tabs
=
set
(
old_tab_list
)
-
set
(
new_tab_list
)
new_tab_list
.
extend
(
non_displayed_tabs
)
# validate the tabs to make sure everything is Ok (e.g., did the client try to reorder unmovable tabs?)
try
:
CourseTabList
.
validate_tabs
(
new_tab_list
)
except
InvalidTabsException
,
exception
:
return
JsonResponse
(
{
"error"
:
"New list of tabs is not valid: {0}."
.
format
(
str
(
exception
))},
status
=
400
)
# persist the new order of the tabs
course_item
.
tabs
=
new_tab_list
modulestore
(
'direct'
)
.
update_item
(
course_item
,
request
.
user
.
id
)
return
JsonResponse
()
def
edit_tab_handler
(
course_item
,
request
):
"""
Helper function for handling requests to edit settings of a single tab
"""
# Tabs are identified by tab_id or locator
tab_id_locator
=
request
.
json
[
'tab_id_locator'
]
# Find the given tab in the course
tab
=
get_tab_by_tab_id_locator
(
course_item
.
tabs
,
tab_id_locator
)
if
tab
is
None
:
return
JsonResponse
(
{
"error"
:
"Tab with id_locator '{0}' does not exist."
.
format
(
tab_id_locator
)},
status
=
400
)
if
'is_hidden'
in
request
.
json
:
# set the is_hidden attribute on the requested tab
tab
.
is_hidden
=
request
.
json
[
'is_hidden'
]
modulestore
(
'direct'
)
.
update_item
(
course_item
,
request
.
user
.
id
)
else
:
raise
NotImplementedError
(
'Unsupported request to edit tab: {0}'
.
format
(
request
.
json
))
return
JsonResponse
()
def
get_tab_by_tab_id_locator
(
tab_list
,
tab_id_locator
):
"""
Look for a tab with the specified tab_id or locator. Returns the first matching tab.
"""
if
'tab_id'
in
tab_id_locator
:
tab
=
CourseTabList
.
get_tab_by_id
(
tab_list
,
tab_id_locator
[
'tab_id'
])
elif
'tab_locator'
in
tab_id_locator
:
tab
=
get_tab_by_locator
(
tab_list
,
tab_id_locator
[
'tab_locator'
])
return
tab
def
get_tab_by_locator
(
tab_list
,
tab_locator
):
"""
Look for a tab with the specified locator. Returns the first matching tab.
"""
tab_location
=
loc_mapper
()
.
translate_locator_to_location
(
BlockUsageLocator
(
tab_locator
))
item
=
modulestore
(
'direct'
)
.
get_item
(
tab_location
)
static_tab
=
StaticTab
(
name
=
item
.
display_name
,
url_slug
=
item
.
location
.
name
,
)
return
CourseTabList
.
get_tab_by_id
(
tab_list
,
static_tab
.
tab_id
)
# "primitive" tab edit functions driven by the command line.
# "primitive" tab edit functions driven by the command line.
# These should be replaced/deleted by a more capable GUI someday.
# These should be replaced/deleted by a more capable GUI someday.
# Note that the command line UI identifies the tabs with 1-based
# Note that the command line UI identifies the tabs with 1-based
...
...
cms/djangoapps/contentstore/views/tests/test_course_index.py
View file @
2561c010
...
@@ -61,7 +61,7 @@ class TestCourseIndex(CourseTestCase):
...
@@ -61,7 +61,7 @@ class TestCourseIndex(CourseTestCase):
"""
"""
outline_url
=
self
.
course_locator
.
url_reverse
(
'course/'
,
''
)
outline_url
=
self
.
course_locator
.
url_reverse
(
'course/'
,
''
)
# register a non-staff member and try to delete the course branch
# register a non-staff member and try to delete the course branch
non_staff_client
,
_
=
self
.
create
NonStaffAuthedUserC
lient
()
non_staff_client
,
_
=
self
.
create
_non_staff_authed_user_c
lient
()
response
=
non_staff_client
.
delete
(
outline_url
,
{},
HTTP_ACCEPT
=
'application/json'
)
response
=
non_staff_client
.
delete
(
outline_url
,
{},
HTTP_ACCEPT
=
'application/json'
)
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertEqual
(
response
.
status_code
,
403
)
...
@@ -69,7 +69,7 @@ class TestCourseIndex(CourseTestCase):
...
@@ -69,7 +69,7 @@ class TestCourseIndex(CourseTestCase):
"""
"""
Make and register an course_staff and ensure they can access the courses
Make and register an course_staff and ensure they can access the courses
"""
"""
course_staff_client
,
course_staff
=
self
.
create
NonStaffAuthedUserC
lient
()
course_staff_client
,
course_staff
=
self
.
create
_non_staff_authed_user_c
lient
()
for
course
in
[
self
.
course
,
self
.
odd_course
]:
for
course
in
[
self
.
course
,
self
.
odd_course
]:
new_location
=
loc_mapper
()
.
translate_location
(
course
.
location
.
course_id
,
course
.
location
,
False
,
True
)
new_location
=
loc_mapper
()
.
translate_location
(
course
.
location
.
course_id
,
course
.
location
,
False
,
True
)
permission_url
=
new_location
.
url_reverse
(
"course_team/"
,
course_staff
.
email
)
permission_url
=
new_location
.
url_reverse
(
"course_team/"
,
course_staff
.
email
)
...
...
cms/djangoapps/contentstore/views/tests/test_tabs.py
View file @
2561c010
""" Tests for tab functions (just primitive). """
""" Tests for tab functions (just primitive). """
import
json
from
contentstore.views
import
tabs
from
contentstore.views
import
tabs
from
contentstore.tests.utils
import
CourseTestCase
from
django.test
import
TestCase
from
django.test
import
TestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
courseware.courses
import
get_course_by_id
from
courseware.courses
import
get_course_by_id
from
xmodule.tabs
import
CourseTabList
,
WikiTab
class
TabsPageTests
(
CourseTestCase
):
"""Test cases for Tabs (a.k.a Pages) page"""
def
setUp
(
self
):
"""Common setup for tests"""
# call super class to setup course, etc.
super
(
TabsPageTests
,
self
)
.
setUp
()
# Set the URL for tests
self
.
url
=
self
.
course_locator
.
url_reverse
(
'tabs'
)
# add a static tab to the course, for code coverage
ItemFactory
.
create
(
parent_location
=
self
.
course_location
,
category
=
"static_tab"
,
display_name
=
"Static_1"
)
self
.
reload_course
()
def
check_invalid_tab_id_response
(
self
,
resp
):
"""Verify response is an error listing the invalid_tab_id"""
self
.
assertEqual
(
resp
.
status_code
,
400
)
resp_content
=
json
.
loads
(
resp
.
content
)
self
.
assertIn
(
"error"
,
resp_content
)
self
.
assertIn
(
"invalid_tab_id"
,
resp_content
[
'error'
])
def
test_not_implemented
(
self
):
"""Verify not implemented errors"""
# JSON GET request not supported
with
self
.
assertRaises
(
NotImplementedError
):
self
.
client
.
get
(
self
.
url
)
# JSON POST request not supported
with
self
.
assertRaises
(
NotImplementedError
):
self
.
client
.
ajax_post
(
self
.
url
,
data
=
json
.
dumps
({
'tab_id_locator'
:
{
'tab_id'
:
WikiTab
.
type
},
'unsupported_request'
:
None
,
}),
)
# invalid JSON POST request
with
self
.
assertRaises
(
NotImplementedError
):
self
.
client
.
ajax_post
(
self
.
url
,
data
=
{
'invalid_request'
:
None
},
)
def
test_view_index
(
self
):
"""Basic check that the Pages page responds correctly"""
resp
=
self
.
client
.
get_html
(
self
.
url
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
self
.
assertIn
(
'course-nav-list'
,
resp
.
content
)
def
test_reorder_tabs
(
self
):
"""Test re-ordering of tabs"""
# get the original tab ids
orig_tab_ids
=
[
tab
.
tab_id
for
tab
in
self
.
course
.
tabs
]
tab_ids
=
list
(
orig_tab_ids
)
num_orig_tabs
=
len
(
orig_tab_ids
)
# make sure we have enough tabs to play around with
self
.
assertTrue
(
num_orig_tabs
>=
5
)
# reorder the last two tabs
tab_ids
[
num_orig_tabs
-
1
],
tab_ids
[
num_orig_tabs
-
2
]
=
tab_ids
[
num_orig_tabs
-
2
],
tab_ids
[
num_orig_tabs
-
1
]
# remove the middle tab
# (the code needs to handle the case where tabs requested for re-ordering is a subset of the tabs in the course)
removed_tab
=
tab_ids
.
pop
(
num_orig_tabs
/
2
)
self
.
assertTrue
(
len
(
tab_ids
)
==
num_orig_tabs
-
1
)
# post the request
resp
=
self
.
client
.
ajax_post
(
self
.
url
,
data
=
{
'tabs'
:
[{
'tab_id'
:
tab_id
}
for
tab_id
in
tab_ids
]},
)
self
.
assertEqual
(
resp
.
status_code
,
204
)
# reload the course and verify the new tab order
self
.
reload_course
()
new_tab_ids
=
[
tab
.
tab_id
for
tab
in
self
.
course
.
tabs
]
self
.
assertEqual
(
new_tab_ids
,
tab_ids
+
[
removed_tab
])
self
.
assertNotEqual
(
new_tab_ids
,
orig_tab_ids
)
def
test_reorder_tabs_invalid_list
(
self
):
"""Test re-ordering of tabs with invalid tab list"""
orig_tab_ids
=
[
tab
.
tab_id
for
tab
in
self
.
course
.
tabs
]
tab_ids
=
list
(
orig_tab_ids
)
# reorder the first two tabs
tab_ids
[
0
],
tab_ids
[
1
]
=
tab_ids
[
1
],
tab_ids
[
0
]
# post the request
resp
=
self
.
client
.
ajax_post
(
self
.
url
,
data
=
{
'tabs'
:
[{
'tab_id'
:
tab_id
}
for
tab_id
in
tab_ids
]},
)
self
.
assertEqual
(
resp
.
status_code
,
400
)
resp_content
=
json
.
loads
(
resp
.
content
)
self
.
assertIn
(
"error"
,
resp_content
)
def
test_reorder_tabs_invalid_tab
(
self
):
"""Test re-ordering of tabs with invalid tab"""
invalid_tab_ids
=
[
'courseware'
,
'info'
,
'invalid_tab_id'
]
# post the request
resp
=
self
.
client
.
ajax_post
(
self
.
url
,
data
=
{
'tabs'
:
[{
'tab_id'
:
tab_id
}
for
tab_id
in
invalid_tab_ids
]},
)
self
.
check_invalid_tab_id_response
(
resp
)
def
check_toggle_tab_visiblity
(
self
,
tab_type
,
new_is_hidden_setting
):
"""Helper method to check changes in tab visibility"""
# find the tab
old_tab
=
CourseTabList
.
get_tab_by_type
(
self
.
course
.
tabs
,
tab_type
)
# visibility should be different from new setting
self
.
assertNotEqual
(
old_tab
.
is_hidden
,
new_is_hidden_setting
)
# post the request
resp
=
self
.
client
.
ajax_post
(
self
.
url
,
data
=
json
.
dumps
({
'tab_id_locator'
:
{
'tab_id'
:
old_tab
.
tab_id
},
'is_hidden'
:
new_is_hidden_setting
,
}),
)
self
.
assertEqual
(
resp
.
status_code
,
204
)
# reload the course and verify the new visibility setting
self
.
reload_course
()
new_tab
=
CourseTabList
.
get_tab_by_type
(
self
.
course
.
tabs
,
tab_type
)
self
.
assertEqual
(
new_tab
.
is_hidden
,
new_is_hidden_setting
)
def
test_toggle_tab_visibility
(
self
):
"""Test toggling of tab visiblity"""
self
.
check_toggle_tab_visiblity
(
WikiTab
.
type
,
True
)
self
.
check_toggle_tab_visiblity
(
WikiTab
.
type
,
False
)
def
test_toggle_invalid_tab_visibility
(
self
):
"""Test toggling visibility of an invalid tab"""
# post the request
resp
=
self
.
client
.
ajax_post
(
self
.
url
,
data
=
json
.
dumps
({
'tab_id_locator'
:
{
'tab_id'
:
'invalid_tab_id'
}
}),
)
self
.
check_invalid_tab_id_response
(
resp
)
class
PrimitiveTabEdit
(
TestCase
):
class
PrimitiveTabEdit
(
TestCase
):
...
...
cms/djangoapps/contentstore/views/tests/test_textbooks.py
View file @
2561c010
...
@@ -56,8 +56,7 @@ class TextbookIndexTestCase(CourseTestCase):
...
@@ -56,8 +56,7 @@ class TextbookIndexTestCase(CourseTestCase):
}
}
]
]
self
.
course
.
pdf_textbooks
=
content
self
.
course
.
pdf_textbooks
=
content
store
=
get_modulestore
(
self
.
course
.
location
)
self
.
save_course
()
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
resp
=
self
.
client
.
get
(
resp
=
self
.
client
.
get
(
self
.
url
,
self
.
url
,
...
@@ -83,12 +82,10 @@ class TextbookIndexTestCase(CourseTestCase):
...
@@ -83,12 +82,10 @@ class TextbookIndexTestCase(CourseTestCase):
)
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
# reload course
store
=
get_modulestore
(
self
.
course
.
location
)
course
=
store
.
get_item
(
self
.
course
.
location
)
# should be the same, except for added ID
# should be the same, except for added ID
no_ids
=
[]
no_ids
=
[]
for
textbook
in
course
.
pdf_textbooks
:
self
.
reload_course
()
for
textbook
in
self
.
course
.
pdf_textbooks
:
del
textbook
[
"id"
]
del
textbook
[
"id"
]
no_ids
.
append
(
textbook
)
no_ids
.
append
(
textbook
)
self
.
assertEqual
(
no_ids
,
textbooks
)
self
.
assertEqual
(
no_ids
,
textbooks
)
...
@@ -193,9 +190,7 @@ class TextbookDetailTestCase(CourseTestCase):
...
@@ -193,9 +190,7 @@ class TextbookDetailTestCase(CourseTestCase):
self
.
course
.
pdf_textbooks
=
[
self
.
textbook1
,
self
.
textbook2
]
self
.
course
.
pdf_textbooks
=
[
self
.
textbook1
,
self
.
textbook2
]
# Save the data that we've just changed to the underlying
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
# MongoKeyValueStore before we update the mongo datastore.
self
.
course
.
save
()
self
.
save_course
()
self
.
store
=
get_modulestore
(
self
.
course
.
location
)
self
.
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
self
.
url_nonexist
=
self
.
course_locator
.
url_reverse
(
"textbooks"
,
"20"
)
self
.
url_nonexist
=
self
.
course_locator
.
url_reverse
(
"textbooks"
,
"20"
)
def
test_get_1
(
self
):
def
test_get_1
(
self
):
...
@@ -221,15 +216,15 @@ class TextbookDetailTestCase(CourseTestCase):
...
@@ -221,15 +216,15 @@ class TextbookDetailTestCase(CourseTestCase):
"Delete a textbook by ID"
"Delete a textbook by ID"
resp
=
self
.
client
.
delete
(
self
.
url1
)
resp
=
self
.
client
.
delete
(
self
.
url1
)
self
.
assertEqual
(
resp
.
status_code
,
204
)
self
.
assertEqual
(
resp
.
status_code
,
204
)
course
=
self
.
store
.
get_item
(
self
.
course
.
location
)
self
.
reload_course
(
)
self
.
assertEqual
(
course
.
pdf_textbooks
,
[
self
.
textbook2
])
self
.
assertEqual
(
self
.
course
.
pdf_textbooks
,
[
self
.
textbook2
])
def
test_delete_nonexistant
(
self
):
def
test_delete_nonexistant
(
self
):
"Delete a textbook by ID, when the ID doesn't match an existing textbook"
"Delete a textbook by ID, when the ID doesn't match an existing textbook"
resp
=
self
.
client
.
delete
(
self
.
url_nonexist
)
resp
=
self
.
client
.
delete
(
self
.
url_nonexist
)
self
.
assertEqual
(
resp
.
status_code
,
404
)
self
.
assertEqual
(
resp
.
status_code
,
404
)
course
=
self
.
store
.
get_item
(
self
.
course
.
location
)
self
.
reload_course
(
)
self
.
assertEqual
(
course
.
pdf_textbooks
,
[
self
.
textbook1
,
self
.
textbook2
])
self
.
assertEqual
(
self
.
course
.
pdf_textbooks
,
[
self
.
textbook1
,
self
.
textbook2
])
def
test_create_new_by_id
(
self
):
def
test_create_new_by_id
(
self
):
"Create a textbook by ID"
"Create a textbook by ID"
...
@@ -249,9 +244,9 @@ class TextbookDetailTestCase(CourseTestCase):
...
@@ -249,9 +244,9 @@ class TextbookDetailTestCase(CourseTestCase):
self
.
assertEqual
(
resp2
.
status_code
,
200
)
self
.
assertEqual
(
resp2
.
status_code
,
200
)
compare
=
json
.
loads
(
resp2
.
content
)
compare
=
json
.
loads
(
resp2
.
content
)
self
.
assertEqual
(
compare
,
textbook
)
self
.
assertEqual
(
compare
,
textbook
)
course
=
self
.
store
.
get_item
(
self
.
course
.
location
)
self
.
reload_course
(
)
self
.
assertEqual
(
self
.
assertEqual
(
course
.
pdf_textbooks
,
self
.
course
.
pdf_textbooks
,
[
self
.
textbook1
,
self
.
textbook2
,
textbook
]
[
self
.
textbook1
,
self
.
textbook2
,
textbook
]
)
)
...
...
cms/envs/common.py
View file @
2561c010
...
@@ -43,9 +43,11 @@ FEATURES = {
...
@@ -43,9 +43,11 @@ FEATURES = {
'GITHUB_PUSH'
:
False
,
'GITHUB_PUSH'
:
False
,
# for consistency in user-experience, keep the value of th
is setting in sync with the
# for consistency in user-experience, keep the value of th
e following 3 settings
#
one
in lms/envs/common.py
#
in sync with the ones
in lms/envs/common.py
'ENABLE_DISCUSSION_SERVICE'
:
True
,
'ENABLE_DISCUSSION_SERVICE'
:
True
,
'ENABLE_TEXTBOOK'
:
True
,
'ENABLE_STUDENT_NOTES'
:
True
,
'AUTH_USE_CERTIFICATES'
:
False
,
'AUTH_USE_CERTIFICATES'
:
False
,
...
...
cms/static/coffee/src/views/tabs.coffee
View file @
2561c010
...
@@ -18,7 +18,8 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
...
@@ -18,7 +18,8 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
@
options
.
mast
.
find
(
'.new-tab'
).
on
(
'click'
,
@
addNewTab
)
@
options
.
mast
.
find
(
'.new-tab'
).
on
(
'click'
,
@
addNewTab
)
$
(
'.add-pages .new-tab'
).
on
(
'click'
,
@
addNewTab
)
$
(
'.add-pages .new-tab'
).
on
(
'click'
,
@
addNewTab
)
@
$
(
'.components'
).
sortable
(
$
(
'.toggle-checkbox'
).
on
(
'click'
,
@
toggleVisibilityOfTab
)
@
$
(
'.course-nav-list'
).
sortable
(
handle
:
'.drag-handle'
handle
:
'.drag-handle'
update
:
@
tabMoved
update
:
@
tabMoved
helper
:
'clone'
helper
:
'clone'
...
@@ -26,13 +27,38 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
...
@@ -26,13 +27,38 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
placeholder
:
'component-placeholder'
placeholder
:
'component-placeholder'
forcePlaceholderSize
:
true
forcePlaceholderSize
:
true
axis
:
'y'
axis
:
'y'
items
:
'> .
component
'
items
:
'> .
is-movable
'
)
)
toggleVisibilityOfTab
:
(
event
,
ui
)
=>
checkbox_element
=
event
.
srcElement
tab_element
=
$
(
checkbox_element
).
parents
(
".course-tab"
)[
0
]
saving
=
new
NotificationView
.
Mini
({
title
:
gettext
(
"Saving…"
)})
saving
.
show
()
$
.
ajax
({
type
:
'POST'
,
url
:
@
model
.
url
(),
data
:
JSON
.
stringify
({
tab_id_locator
:
{
tab_id
:
$
(
tab_element
).
data
(
'tab-id'
),
tab_locator
:
$
(
tab_element
).
data
(
'locator'
)
},
is_hidden
:
$
(
checkbox_element
).
is
(
':checked'
)
}),
contentType
:
'application/json'
}).
success
(
=>
saving
.
hide
())
tabMoved
:
(
event
,
ui
)
=>
tabMoved
:
(
event
,
ui
)
=>
tabs
=
[]
tabs
=
[]
@
$
(
'.component'
).
each
((
idx
,
element
)
=>
@
$
(
'.course-tab'
).
each
((
idx
,
element
)
=>
tabs
.
push
(
$
(
element
).
data
(
'locator'
))
tabs
.
push
(
{
tab_id
:
$
(
element
).
data
(
'tab-id'
),
tab_locator
:
$
(
element
).
data
(
'locator'
)
}
)
)
)
analytics
.
track
"Reordered Pages"
,
analytics
.
track
"Reordered Pages"
,
...
@@ -59,6 +85,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
...
@@ -59,6 +85,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
)
)
$
(
'.new-component-item'
).
before
(
editor
.
$el
)
$
(
'.new-component-item'
).
before
(
editor
.
$el
)
editor
.
$el
.
addClass
(
'course-tab is-movable'
)
editor
.
$el
.
addClass
(
'new'
)
editor
.
$el
.
addClass
(
'new'
)
setTimeout
(
=>
setTimeout
(
=>
editor
.
$el
.
removeClass
(
'new'
)
editor
.
$el
.
removeClass
(
'new'
)
...
...
cms/static/sass/views/_static-pages.scss
View file @
2561c010
...
@@ -180,7 +180,7 @@
...
@@ -180,7 +180,7 @@
}
}
.component
,
.component
,
.course-nav-
tab
{
.course-nav-
item
{
position
:
relative
;
position
:
relative
;
border
:
1px
solid
$mediumGrey
;
border
:
1px
solid
$mediumGrey
;
border-top
:
none
;
border-top
:
none
;
...
@@ -239,7 +239,7 @@
...
@@ -239,7 +239,7 @@
}
}
.component-actions
,
.component-actions
,
.course-nav-
tab
-actions
{
.course-nav-
item
-actions
{
display
:
inline-block
;
display
:
inline-block
;
float
:
right
;
float
:
right
;
margin-right
:
(
$baseline
*
2
);
margin-right
:
(
$baseline
*
2
);
...
@@ -289,30 +289,31 @@
...
@@ -289,30 +289,31 @@
}
}
// basic course nav items - overrides from above
// basic course nav items - overrides from above
.course-nav-
tab
{
.course-nav-
item
{
padding
:
(
$baseline
*.
75
)
(
$baseline
/
4
)
(
$baseline
*.
75
)
$baseline
;
padding
:
(
$baseline
*.
75
)
(
$baseline
/
4
)
(
$baseline
*.
75
)
$baseline
;
background
:
$white
;
&
.fixed
{
&
.is-fixed
{
@extend
%ui-disabled
;
@include
transition
(
opacity
$tmg-f2
ease-in-out
0s
);
@include
transition
(
opacity
$tmg-f2
ease-in-out
0s
);
opacity
:
.7
;
opacity
:
0
.5
;
&
:hover
{
opacity
:
1
;
}
}
}
.course-nav-
tab
-header
{
.course-nav-
item
-header
{
display
:
inline-block
;
display
:
inline-block
;
width
:
80%
;
width
:
80%
;
.title
{
.title
{
@extend
%t-title4
;
@extend
%t-title4
;
font-weight
:
300
;
}
color
:
$gray
;
.title-sub
{
@extend
%t-title7
;
color
:
$gray-l2
;
}
}
}
}
.course-nav-
tab
-actions
{
.course-nav-
item
-actions
{
display
:
inline-block
;
display
:
inline-block
;
padding
:
(
$baseline
/
10
);
padding
:
(
$baseline
/
10
);
}
}
...
@@ -335,7 +336,6 @@
...
@@ -335,7 +336,6 @@
@include
transition
(
background-color
$tmg-s3
linear
0s
);
@include
transition
(
background-color
$tmg-s3
linear
0s
);
padding
:
20px
20px
22px
;
padding
:
20px
20px
22px
;
font-size
:
24px
;
font-size
:
24px
;
font-weight
:
300
;
background
:
#fff
;
background
:
#fff
;
}
}
...
@@ -395,4 +395,3 @@
...
@@ -395,4 +395,3 @@
outline
:
0
;
outline
:
0
;
}
}
}
}
cms/templates/edit-tabs.html
View file @
2561c010
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
<
%!
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
from
django
.
utils
.
translation
import
ugettext
as
_
from
django
.
core
.
urlresolvers
import
reverse
from
django
.
core
.
urlresolvers
import
reverse
from
xmodule
.
tabs
import
StaticTab
%
>
%
>
<
%
block
name=
"title"
>
${_("Pages")}
</
%
block>
<
%
block
name=
"title"
>
${_("Pages")}
</
%
block>
<
%
block
name=
"bodyclass"
>
is-signedin course view-static-pages
</
%
block>
<
%
block
name=
"bodyclass"
>
is-signedin course view-static-pages
</
%
block>
...
@@ -39,7 +40,10 @@
...
@@ -39,7 +40,10 @@
<h3
class=
"sr"
>
${_("Page Actions")}
</h3>
<h3
class=
"sr"
>
${_("Page Actions")}
</h3>
<ul>
<ul>
<li
class=
"nav-item"
>
<li
class=
"nav-item"
>
<a
href=
"#"
class=
"button new-button new-tab"
><i
class=
"icon-plus"
></i>
${_("New Page")}
</a>
<a
href=
"#"
class=
"button new-button new-tab"
><i
class=
"icon-plus"
></i>
${_("New Page")}
</a>
</li>
<li
class=
"nav-item"
>
<a
href=
"${lms_link}"
rel=
"external"
class=
"button view-button view-live-button"
>
${_("View Live")}
</a>
</li>
</li>
</ul>
</ul>
</nav>
</nav>
...
@@ -54,39 +58,73 @@
...
@@ -54,39 +58,73 @@
<article
class=
"unit-body"
>
<article
class=
"unit-body"
>
<div
class=
"tab-list"
>
<div
class=
"tab-list"
>
<ol
class=
"course-nav-tab-list components"
>
<ol
class=
"course-nav-list course components"
>
% for tab in built_in_tabs:
% for tab in tabs_to_render:
<li
class=
"course-nav-tab fixed"
>
<
%
<div
class=
"course-nav-tab-header"
>
css_class =
"course-tab"
<h3
class=
"title"
>
${_(tab.name)}
</h3>
if
tab
.
is_movable:
</div>
css_class =
css_class
+
"
is-movable
"
<div
class=
"course-nav-tab-actions wrapper-actions-list"
>
elif
(
not
tab
.
is_movable
)
and
(
not
tab
.
is_hideable
)
:
<ul
class=
"actions-list"
>
css_class =
css_class
+
"
is-fixed
"
%
>
% if tab.is_hideable:
<li
class=
"action-item action-visible"
>
% if isinstance(tab, StaticTab):
<label
for=
"[id]"
><span
class=
"sr"
>
${_("Show this page")}
</span></label>
<li
class=
"component ${css_class}"
data-locator=
"${tab.locator}"
data-tab-id=
"${tab.tab_id}"
></li>
<input
type=
"checkbox"
id=
"[id]"
class=
"toggle-checkbox"
data-tooltip=
"${_('Show/hide page')}"
/>
<div
class=
"action-button"
><i
class=
"icon-eye-open"
></i><i
class=
"icon-eye-close"
></i></div>
% else:
</li>
<li
class=
"course-nav-item ${css_class}"
data-tab-id=
"${tab.tab_id}"
>
%endif
<div
class=
"course-nav-item-header"
>
</ul>
% if tab.is_collection:
</div>
<div
class=
"drag-handle is-fixed"
data-tooltip=
"${_('Cannot be reordered')}"
>
<h3
class=
"title-sub"
>
${_(tab.name)}
</h3>
<span
class=
"sr"
>
${_("Fixed page")}
</span>
<ul
class=
"course-nav-item-children"
>
</div>
% for item in tab.items(context_course):
</li>
<li
class=
"course-nav-item-child title"
>
${_(item.name)}
</li>
% endfor
</ul>
% else:
<h3
class=
"title"
>
${_(tab.name)}
</h3>
% endif
</div>
<div
class=
"course-nav-item-actions wrapper-actions-list"
>
<ul
class=
"actions-list"
>
% if tab.is_hideable:
<li
class=
"action-item action-visible"
>
<label><span
class=
"sr"
>
${_("Show this page")}
</span></label>
% if tab.is_hidden:
<input
type=
"checkbox"
class=
"toggle-checkbox"
data-tooltip=
"${_('Show/hide page')}"
checked
/>
% else:
<input
type=
"checkbox"
class=
"toggle-checkbox"
data-tooltip=
"${_('Show/hide page')}"
/>
% endif
<div
class=
"action-button"
><i
class=
"icon-eye-open"
></i><i
class=
"icon-eye-close"
></i></div>
</li>
% endif
</ul>
</div>
% if tab.is_movable:
<div
class=
"drag-handle"
data-tooltip=
"${_('Drag to reorder')}"
>
<span
class=
"sr"
>
${_("Drag to reorder")}
</span>
</div>
% else:
<div
class=
"drag-handle is-fixed"
data-tooltip=
"${_('This page cannot be reordered')}"
>
<span
class=
"sr"
>
${_("This page cannot be reordered")}
</span>
</div>
% endif
</li>
% endif
% endfor
% endfor
% for locator in components:
<li
class=
"new-component-item"
></li>
<li
class=
"component"
data-locator=
"${locator}"
></li>
% endfor
<li
class=
"new-component-item"
>
</li>
</ol>
</ol>
</div>
</div>
...
...
common/lib/xmodule/xmodule/modulestore/mongo/base.py
View file @
2561c010
...
@@ -797,7 +797,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
...
@@ -797,7 +797,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
if
xblock
.
category
==
'static_tab'
:
if
xblock
.
category
==
'static_tab'
:
course
=
self
.
_get_course_for_item
(
xblock
.
location
)
course
=
self
.
_get_course_for_item
(
xblock
.
location
)
# find the course's reference to this tab and update the name.
# find the course's reference to this tab and update the name.
static_tab
=
CourseTabList
.
get_tab_by_slug
(
course
,
xblock
.
location
.
name
)
static_tab
=
CourseTabList
.
get_tab_by_slug
(
course
.
tabs
,
xblock
.
location
.
name
)
# only update if changed
# only update if changed
if
static_tab
and
static_tab
[
'name'
]
!=
xblock
.
display_name
:
if
static_tab
and
static_tab
[
'name'
]
!=
xblock
.
display_name
:
static_tab
[
'name'
]
=
xblock
.
display_name
static_tab
[
'name'
]
=
xblock
.
display_name
...
...
common/lib/xmodule/xmodule/modulestore/xml.py
View file @
2561c010
...
@@ -663,7 +663,7 @@ class XMLModuleStore(ModuleStoreReadBase):
...
@@ -663,7 +663,7 @@ class XMLModuleStore(ModuleStoreReadBase):
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
# from the course policy
# from the course policy
if
category
==
"static_tab"
:
if
category
==
"static_tab"
:
tab
=
CourseTabList
.
get_tab_by_slug
(
course
=
course_descriptor
,
url_slug
=
slug
)
tab
=
CourseTabList
.
get_tab_by_slug
(
tab_list
=
course_descriptor
.
tabs
,
url_slug
=
slug
)
if
tab
:
if
tab
:
module
.
display_name
=
tab
.
name
module
.
display_name
=
tab
.
name
module
.
data_dir
=
course_dir
module
.
data_dir
=
course_dir
...
...
common/lib/xmodule/xmodule/tabs.py
View file @
2561c010
...
@@ -28,6 +28,15 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
...
@@ -28,6 +28,15 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
# subclass, shared by all instances of the subclass.
# subclass, shared by all instances of the subclass.
type
=
''
type
=
''
# Class property that specifies whether the tab can be hidden for a particular course
is_hideable
=
False
# Class property that specifies whether the tab can be moved within a course's list of tabs
is_movable
=
True
# Class property that specifies whether the tab is a collection of other tabs
is_collection
=
False
def
__init__
(
self
,
name
,
tab_id
,
link_func
):
def
__init__
(
self
,
name
,
tab_id
,
link_func
):
"""
"""
Initializes class members with values passed in by subclasses.
Initializes class members with values passed in by subclasses.
...
@@ -48,9 +57,6 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
...
@@ -48,9 +57,6 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
self
.
link_func
=
link_func
self
.
link_func
=
link_func
# indicates whether the tab can be hidden for a particular course
self
.
is_hideable
=
False
def
can_display
(
self
,
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
# pylint: disable=unused-argument
def
can_display
(
self
,
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
# pylint: disable=unused-argument
"""
"""
Determines whether the tab should be displayed in the UI for the given course and a particular user.
Determines whether the tab should be displayed in the UI for the given course and a particular user.
...
@@ -140,12 +146,12 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
...
@@ -140,12 +146,12 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
return
not
(
self
==
other
)
return
not
(
self
==
other
)
@classmethod
@classmethod
def
validate
(
cls
,
tab
,
raise_error
=
True
):
# pylint: disable=unused-argument
def
validate
(
cls
,
tab
_dict
,
raise_error
=
True
):
"""
"""
Validates the given dict-type tab object to ensure it contains the expected keys.
Validates the given dict-type tab object to ensure it contains the expected keys.
This method should be overridden by subclasses that require certain keys to be persisted in the tab.
This method should be overridden by subclasses that require certain keys to be persisted in the tab.
"""
"""
return
key_checker
([
'type'
])(
tab
,
raise_error
)
return
key_checker
([
'type'
])(
tab
_dict
,
raise_error
)
def
to_json
(
self
):
def
to_json
(
self
):
"""
"""
...
@@ -158,7 +164,7 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
...
@@ -158,7 +164,7 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
return
{
'type'
:
self
.
type
,
'name'
:
self
.
name
}
return
{
'type'
:
self
.
type
,
'name'
:
self
.
name
}
@staticmethod
@staticmethod
def
from_json
(
tab
):
def
from_json
(
tab
_dict
):
"""
"""
Deserializes a CourseTab from a json-like representation.
Deserializes a CourseTab from a json-like representation.
...
@@ -191,15 +197,15 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
...
@@ -191,15 +197,15 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
'instructor'
:
InstructorTab
,
# not persisted
'instructor'
:
InstructorTab
,
# not persisted
}
}
tab_type
=
tab
.
get
(
'type'
)
tab_type
=
tab
_dict
.
get
(
'type'
)
if
tab_type
not
in
sub_class_types
:
if
tab_type
not
in
sub_class_types
:
raise
InvalidTabsException
(
raise
InvalidTabsException
(
'Unknown tab type {0}. Known types: {1}'
.
format
(
tab_type
,
sub_class_types
)
'Unknown tab type {0}. Known types: {1}'
.
format
(
tab_type
,
sub_class_types
)
)
)
tab_class
=
sub_class_types
[
tab
[
'type'
]]
tab_class
=
sub_class_types
[
tab
_dict
[
'type'
]]
tab_class
.
validate
(
tab
)
tab_class
.
validate
(
tab
_dict
)
return
tab_class
(
tab
=
tab
)
return
tab_class
(
tab
_dict
=
tab_dict
)
class
AuthenticatedCourseTab
(
CourseTab
):
class
AuthenticatedCourseTab
(
CourseTab
):
...
@@ -218,14 +224,53 @@ class StaffTab(AuthenticatedCourseTab):
...
@@ -218,14 +224,53 @@ class StaffTab(AuthenticatedCourseTab):
return
is_user_staff
return
is_user_staff
class
HideableTab
(
CourseTab
):
"""
Abstract class for tabs that are hideable
"""
is_hideable
=
True
def
__init__
(
self
,
name
,
tab_id
,
link_func
,
tab_dict
):
super
(
HideableTab
,
self
)
.
__init__
(
name
=
name
,
tab_id
=
tab_id
,
link_func
=
link_func
,
)
self
.
is_hidden
=
tab_dict
.
get
(
'is_hidden'
,
False
)
if
tab_dict
else
False
def
__getitem__
(
self
,
key
):
if
key
==
'is_hidden'
:
return
self
.
is_hidden
else
:
return
super
(
HideableTab
,
self
)
.
__getitem__
(
key
)
def
__setitem__
(
self
,
key
,
value
):
if
key
==
'is_hidden'
:
self
.
is_hidden
=
value
else
:
super
(
HideableTab
,
self
)
.
__setitem__
(
key
,
value
)
def
to_json
(
self
):
to_json_val
=
super
(
HideableTab
,
self
)
.
to_json
()
if
self
.
is_hidden
:
to_json_val
.
update
({
'is_hidden'
:
True
})
return
to_json_val
def
__eq__
(
self
,
other
):
if
not
super
(
HideableTab
,
self
)
.
__eq__
(
other
):
return
False
return
self
.
is_hidden
==
other
.
get
(
'is_hidden'
,
False
)
class
CoursewareTab
(
CourseTab
):
class
CoursewareTab
(
CourseTab
):
"""
"""
A tab containing the course content.
A tab containing the course content.
"""
"""
type
=
'courseware'
type
=
'courseware'
is_movable
=
False
def
__init__
(
self
,
tab
=
None
):
# pylint: disable=unused-argument
def
__init__
(
self
,
tab
_dict
=
None
):
# pylint: disable=unused-argument
super
(
CoursewareTab
,
self
)
.
__init__
(
super
(
CoursewareTab
,
self
)
.
__init__
(
# Translators: 'Courseware' refers to the tab in the courseware that leads to the content of a course
# Translators: 'Courseware' refers to the tab in the courseware that leads to the content of a course
name
=
_
(
'Courseware'
),
# support fixed name for the courseware tab
name
=
_
(
'Courseware'
),
# support fixed name for the courseware tab
...
@@ -240,18 +285,19 @@ class CourseInfoTab(CourseTab):
...
@@ -240,18 +285,19 @@ class CourseInfoTab(CourseTab):
"""
"""
type
=
'course_info'
type
=
'course_info'
is_movable
=
False
def
__init__
(
self
,
tab
=
None
):
def
__init__
(
self
,
tab
_dict
=
None
):
super
(
CourseInfoTab
,
self
)
.
__init__
(
super
(
CourseInfoTab
,
self
)
.
__init__
(
# Translators: "Course Info" is the name of the course's information and updates page
# Translators: "Course Info" is the name of the course's information and updates page
name
=
tab
[
'name'
]
if
tab
else
_
(
'Course Info'
),
name
=
tab
_dict
[
'name'
]
if
tab_dict
else
_
(
'Course Info'
),
tab_id
=
'info'
,
tab_id
=
'info'
,
link_func
=
link_reverse_func
(
'info'
),
link_func
=
link_reverse_func
(
'info'
),
)
)
@classmethod
@classmethod
def
validate
(
cls
,
tab
,
raise_error
=
True
):
def
validate
(
cls
,
tab
_dict
,
raise_error
=
True
):
return
super
(
CourseInfoTab
,
cls
)
.
validate
(
tab
,
raise_error
)
and
need_name
(
tab
,
raise_error
)
return
super
(
CourseInfoTab
,
cls
)
.
validate
(
tab
_dict
,
raise_error
)
and
need_name
(
tab_dict
,
raise_error
)
class
ProgressTab
(
AuthenticatedCourseTab
):
class
ProgressTab
(
AuthenticatedCourseTab
):
...
@@ -261,10 +307,10 @@ class ProgressTab(AuthenticatedCourseTab):
...
@@ -261,10 +307,10 @@ class ProgressTab(AuthenticatedCourseTab):
type
=
'progress'
type
=
'progress'
def
__init__
(
self
,
tab
=
None
):
def
__init__
(
self
,
tab
_dict
=
None
):
super
(
ProgressTab
,
self
)
.
__init__
(
super
(
ProgressTab
,
self
)
.
__init__
(
# Translators: "Progress" is the name of the student's course progress page
# Translators: "Progress" is the name of the student's course progress page
name
=
tab
[
'name'
]
if
tab
else
_
(
'Progress'
),
name
=
tab
_dict
[
'name'
]
if
tab_dict
else
_
(
'Progress'
),
tab_id
=
self
.
type
,
tab_id
=
self
.
type
,
link_func
=
link_reverse_func
(
self
.
type
),
link_func
=
link_reverse_func
(
self
.
type
),
)
)
...
@@ -273,34 +319,32 @@ class ProgressTab(AuthenticatedCourseTab):
...
@@ -273,34 +319,32 @@ class ProgressTab(AuthenticatedCourseTab):
return
not
course
.
hide_progress_tab
return
not
course
.
hide_progress_tab
@classmethod
@classmethod
def
validate
(
cls
,
tab
,
raise_error
=
True
):
def
validate
(
cls
,
tab
_dict
,
raise_error
=
True
):
return
super
(
ProgressTab
,
cls
)
.
validate
(
tab
,
raise_error
)
and
need_name
(
tab
,
raise_error
)
return
super
(
ProgressTab
,
cls
)
.
validate
(
tab
_dict
,
raise_error
)
and
need_name
(
tab_dict
,
raise_error
)
class
WikiTab
(
Cours
eTab
):
class
WikiTab
(
Hideabl
eTab
):
"""
"""
A tab containing the course wiki.
A tab
_dict
containing the course wiki.
"""
"""
type
=
'wiki'
type
=
'wiki'
def
__init__
(
self
,
tab
=
None
):
def
__init__
(
self
,
tab_dict
=
None
):
# LATER - enable the following flag to enable hiding of the Wiki page
# self.is_hideable = True
super
(
WikiTab
,
self
)
.
__init__
(
super
(
WikiTab
,
self
)
.
__init__
(
# Translators: "Wiki" is the name of the course's wiki page
# Translators: "Wiki" is the name of the course's wiki page
name
=
tab
[
'name'
]
if
tab
else
_
(
'Wiki'
),
name
=
tab
_dict
[
'name'
]
if
tab_dict
else
_
(
'Wiki'
),
tab_id
=
self
.
type
,
tab_id
=
self
.
type
,
link_func
=
link_reverse_func
(
'course_wiki'
),
link_func
=
link_reverse_func
(
'course_wiki'
),
tab_dict
=
tab_dict
,
)
)
def
can_display
(
self
,
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
def
can_display
(
self
,
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
return
settings
.
WIKI_ENABLED
return
settings
.
WIKI_ENABLED
@classmethod
@classmethod
def
validate
(
cls
,
tab
,
raise_error
=
True
):
def
validate
(
cls
,
tab
_dict
,
raise_error
=
True
):
return
super
(
WikiTab
,
cls
)
.
validate
(
tab
,
raise_error
)
and
need_name
(
tab
,
raise_error
)
return
super
(
WikiTab
,
cls
)
.
validate
(
tab
_dict
,
raise_error
)
and
need_name
(
tab_dict
,
raise_error
)
class
DiscussionTab
(
CourseTab
):
class
DiscussionTab
(
CourseTab
):
...
@@ -310,10 +354,10 @@ class DiscussionTab(CourseTab):
...
@@ -310,10 +354,10 @@ class DiscussionTab(CourseTab):
type
=
'discussion'
type
=
'discussion'
def
__init__
(
self
,
tab
=
None
):
def
__init__
(
self
,
tab
_dict
=
None
):
super
(
DiscussionTab
,
self
)
.
__init__
(
super
(
DiscussionTab
,
self
)
.
__init__
(
# Translators: "Discussion" is the title of the course forum page
# Translators: "Discussion" is the title of the course forum page
name
=
tab
[
'name'
]
if
tab
else
_
(
'Discussion'
),
name
=
tab
_dict
[
'name'
]
if
tab_dict
else
_
(
'Discussion'
),
tab_id
=
self
.
type
,
tab_id
=
self
.
type
,
link_func
=
link_reverse_func
(
'django_comment_client.forum.views.forum_form_discussion'
),
link_func
=
link_reverse_func
(
'django_comment_client.forum.views.forum_form_discussion'
),
)
)
...
@@ -322,8 +366,8 @@ class DiscussionTab(CourseTab):
...
@@ -322,8 +366,8 @@ class DiscussionTab(CourseTab):
return
settings
.
FEATURES
.
get
(
'ENABLE_DISCUSSION_SERVICE'
)
return
settings
.
FEATURES
.
get
(
'ENABLE_DISCUSSION_SERVICE'
)
@classmethod
@classmethod
def
validate
(
cls
,
tab
,
raise_error
=
True
):
def
validate
(
cls
,
tab
_dict
,
raise_error
=
True
):
return
super
(
DiscussionTab
,
cls
)
.
validate
(
tab
,
raise_error
)
and
need_name
(
tab
,
raise_error
)
return
super
(
DiscussionTab
,
cls
)
.
validate
(
tab
_dict
,
raise_error
)
and
need_name
(
tab_dict
,
raise_error
)
class
LinkTab
(
CourseTab
):
class
LinkTab
(
CourseTab
):
...
@@ -363,8 +407,8 @@ class LinkTab(CourseTab):
...
@@ -363,8 +407,8 @@ class LinkTab(CourseTab):
return
self
.
link_value
==
other
.
get
(
'link'
)
return
self
.
link_value
==
other
.
get
(
'link'
)
@classmethod
@classmethod
def
validate
(
cls
,
tab
,
raise_error
=
True
):
def
validate
(
cls
,
tab
_dict
,
raise_error
=
True
):
return
super
(
LinkTab
,
cls
)
.
validate
(
tab
,
raise_error
)
and
key_checker
([
'link'
])(
tab
,
raise_error
)
return
super
(
LinkTab
,
cls
)
.
validate
(
tab
_dict
,
raise_error
)
and
key_checker
([
'link'
])(
tab_dict
,
raise_error
)
class
ExternalDiscussionTab
(
LinkTab
):
class
ExternalDiscussionTab
(
LinkTab
):
...
@@ -374,12 +418,12 @@ class ExternalDiscussionTab(LinkTab):
...
@@ -374,12 +418,12 @@ class ExternalDiscussionTab(LinkTab):
type
=
'external_discussion'
type
=
'external_discussion'
def
__init__
(
self
,
tab
=
None
,
link_value
=
None
):
def
__init__
(
self
,
tab
_dict
=
None
,
link_value
=
None
):
super
(
ExternalDiscussionTab
,
self
)
.
__init__
(
super
(
ExternalDiscussionTab
,
self
)
.
__init__
(
# Translators: 'Discussion' refers to the tab in the courseware that leads to the discussion forums
# Translators: 'Discussion' refers to the tab in the courseware that leads to the discussion forums
name
=
_
(
'Discussion'
),
name
=
_
(
'Discussion'
),
tab_id
=
'discussion'
,
tab_id
=
'discussion'
,
link_value
=
tab
[
'link'
]
if
tab
else
link_value
,
link_value
=
tab
_dict
[
'link'
]
if
tab_dict
else
link_value
,
)
)
...
@@ -389,11 +433,11 @@ class ExternalLinkTab(LinkTab):
...
@@ -389,11 +433,11 @@ class ExternalLinkTab(LinkTab):
"""
"""
type
=
'external_link'
type
=
'external_link'
def
__init__
(
self
,
tab
):
def
__init__
(
self
,
tab
_dict
):
super
(
ExternalLinkTab
,
self
)
.
__init__
(
super
(
ExternalLinkTab
,
self
)
.
__init__
(
name
=
tab
[
'name'
],
name
=
tab
_dict
[
'name'
],
tab_id
=
None
,
# External links are never active.
tab_id
=
None
,
# External links are never active.
link_value
=
tab
[
'link'
],
link_value
=
tab
_dict
[
'link'
],
)
)
...
@@ -402,17 +446,15 @@ class StaticTab(CourseTab):
...
@@ -402,17 +446,15 @@ class StaticTab(CourseTab):
A custom tab.
A custom tab.
"""
"""
type
=
'static_tab'
type
=
'static_tab'
url_slug
=
''
@classmethod
@classmethod
def
validate
(
cls
,
tab
,
raise_error
=
True
):
def
validate
(
cls
,
tab
_dict
,
raise_error
=
True
):
return
super
(
StaticTab
,
cls
)
.
validate
(
tab
,
raise_error
)
and
key_checker
([
'name'
,
'url_slug'
])(
tab
,
raise_error
)
return
super
(
StaticTab
,
cls
)
.
validate
(
tab
_dict
,
raise_error
)
and
key_checker
([
'name'
,
'url_slug'
])(
tab_dict
,
raise_error
)
def
__init__
(
self
,
tab
=
None
,
name
=
None
,
url_slug
=
None
):
def
__init__
(
self
,
tab_dict
=
None
,
name
=
None
,
url_slug
=
None
):
self
.
url_slug
=
tab
[
'url_slug'
]
if
tab
else
url_slug
self
.
url_slug
=
tab_dict
[
'url_slug'
]
if
tab_dict
else
url_slug
tab_name
=
tab
[
'name'
]
if
tab
else
name
super
(
StaticTab
,
self
)
.
__init__
(
super
(
StaticTab
,
self
)
.
__init__
(
name
=
tab_name
,
name
=
tab_
dict
[
'name'
]
if
tab_dict
else
name
,
tab_id
=
'static_tab_{0}'
.
format
(
self
.
url_slug
),
tab_id
=
'static_tab_{0}'
.
format
(
self
.
url_slug
),
link_func
=
lambda
course
,
reverse_func
:
reverse_func
(
self
.
type
,
args
=
[
course
.
id
,
self
.
url_slug
]),
link_func
=
lambda
course
,
reverse_func
:
reverse_func
(
self
.
type
,
args
=
[
course
.
id
,
self
.
url_slug
]),
)
)
...
@@ -446,6 +488,8 @@ class SingleTextbookTab(CourseTab):
...
@@ -446,6 +488,8 @@ class SingleTextbookTab(CourseTab):
Textbook collection tab. It should not be serialized or persisted.
Textbook collection tab. It should not be serialized or persisted.
"""
"""
type
=
'single_textbook'
type
=
'single_textbook'
is_movable
=
False
is_collection_item
=
True
def
to_json
(
self
):
def
to_json
(
self
):
raise
NotImplementedError
(
'SingleTextbookTab should not be serialized.'
)
raise
NotImplementedError
(
'SingleTextbookTab should not be serialized.'
)
...
@@ -455,11 +499,18 @@ class TextbookTabsBase(AuthenticatedCourseTab):
...
@@ -455,11 +499,18 @@ class TextbookTabsBase(AuthenticatedCourseTab):
"""
"""
Abstract class for textbook collection tabs classes.
Abstract class for textbook collection tabs classes.
"""
"""
def
__init__
(
self
,
tab
=
None
):
# pylint: disable=unused-argument
is_collection
=
True
super
(
TextbookTabsBase
,
self
)
.
__init__
(
''
,
''
,
''
)
def
__init__
(
self
,
tab_id
):
# Translators: 'Textbooks' refers to the tab in the course that leads to the course' textbooks
super
(
TextbookTabsBase
,
self
)
.
__init__
(
name
=
_
(
"Textbooks"
),
tab_id
=
tab_id
,
link_func
=
None
,
)
@abstractmethod
@abstractmethod
def
book
s
(
self
,
course
):
def
item
s
(
self
,
course
):
"""
"""
A generator for iterating through all the SingleTextbookTab book objects associated with this
A generator for iterating through all the SingleTextbookTab book objects associated with this
collection of textbooks.
collection of textbooks.
...
@@ -473,10 +524,15 @@ class TextbookTabs(TextbookTabsBase):
...
@@ -473,10 +524,15 @@ class TextbookTabs(TextbookTabsBase):
"""
"""
type
=
'textbooks'
type
=
'textbooks'
def
__init__
(
self
,
tab_dict
=
None
):
# pylint: disable=unused-argument
super
(
TextbookTabs
,
self
)
.
__init__
(
tab_id
=
self
.
type
,
)
def
can_display
(
self
,
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
def
can_display
(
self
,
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
return
settings
.
FEATURES
.
get
(
'ENABLE_TEXTBOOK'
)
return
settings
.
FEATURES
.
get
(
'ENABLE_TEXTBOOK'
)
def
book
s
(
self
,
course
):
def
item
s
(
self
,
course
):
for
index
,
textbook
in
enumerate
(
course
.
textbooks
):
for
index
,
textbook
in
enumerate
(
course
.
textbooks
):
yield
SingleTextbookTab
(
yield
SingleTextbookTab
(
name
=
textbook
.
title
,
name
=
textbook
.
title
,
...
@@ -491,7 +547,12 @@ class PDFTextbookTabs(TextbookTabsBase):
...
@@ -491,7 +547,12 @@ class PDFTextbookTabs(TextbookTabsBase):
"""
"""
type
=
'pdf_textbooks'
type
=
'pdf_textbooks'
def
books
(
self
,
course
):
def
__init__
(
self
,
tab_dict
=
None
):
# pylint: disable=unused-argument
super
(
PDFTextbookTabs
,
self
)
.
__init__
(
tab_id
=
self
.
type
,
)
def
items
(
self
,
course
):
for
index
,
textbook
in
enumerate
(
course
.
pdf_textbooks
):
for
index
,
textbook
in
enumerate
(
course
.
pdf_textbooks
):
yield
SingleTextbookTab
(
yield
SingleTextbookTab
(
name
=
textbook
[
'tab_title'
],
name
=
textbook
[
'tab_title'
],
...
@@ -506,7 +567,12 @@ class HtmlTextbookTabs(TextbookTabsBase):
...
@@ -506,7 +567,12 @@ class HtmlTextbookTabs(TextbookTabsBase):
"""
"""
type
=
'html_textbooks'
type
=
'html_textbooks'
def
books
(
self
,
course
):
def
__init__
(
self
,
tab_dict
=
None
):
# pylint: disable=unused-argument
super
(
HtmlTextbookTabs
,
self
)
.
__init__
(
tab_id
=
self
.
type
,
)
def
items
(
self
,
course
):
for
index
,
textbook
in
enumerate
(
course
.
html_textbooks
):
for
index
,
textbook
in
enumerate
(
course
.
html_textbooks
):
yield
SingleTextbookTab
(
yield
SingleTextbookTab
(
name
=
textbook
[
'tab_title'
],
name
=
textbook
[
'tab_title'
],
...
@@ -528,7 +594,7 @@ class StaffGradingTab(StaffTab, GradingTab):
...
@@ -528,7 +594,7 @@ class StaffGradingTab(StaffTab, GradingTab):
"""
"""
type
=
'staff_grading'
type
=
'staff_grading'
def
__init__
(
self
,
tab
=
None
):
# pylint: disable=unused-argument
def
__init__
(
self
,
tab
_dict
=
None
):
# pylint: disable=unused-argument
super
(
StaffGradingTab
,
self
)
.
__init__
(
super
(
StaffGradingTab
,
self
)
.
__init__
(
# Translators: "Staff grading" appears on a tab that allows
# Translators: "Staff grading" appears on a tab that allows
# staff to view open-ended problems that require staff grading
# staff to view open-ended problems that require staff grading
...
@@ -544,7 +610,7 @@ class PeerGradingTab(AuthenticatedCourseTab, GradingTab):
...
@@ -544,7 +610,7 @@ class PeerGradingTab(AuthenticatedCourseTab, GradingTab):
"""
"""
type
=
'peer_grading'
type
=
'peer_grading'
def
__init__
(
self
,
tab
=
None
):
# pylint: disable=unused-argument
def
__init__
(
self
,
tab
_dict
=
None
):
# pylint: disable=unused-argument
super
(
PeerGradingTab
,
self
)
.
__init__
(
super
(
PeerGradingTab
,
self
)
.
__init__
(
# Translators: "Peer grading" appears on a tab that allows
# Translators: "Peer grading" appears on a tab that allows
# students to view open-ended problems that require grading
# students to view open-ended problems that require grading
...
@@ -560,7 +626,7 @@ class OpenEndedGradingTab(AuthenticatedCourseTab, GradingTab):
...
@@ -560,7 +626,7 @@ class OpenEndedGradingTab(AuthenticatedCourseTab, GradingTab):
"""
"""
type
=
'open_ended'
type
=
'open_ended'
def
__init__
(
self
,
tab
=
None
):
# pylint: disable=unused-argument
def
__init__
(
self
,
tab
_dict
=
None
):
# pylint: disable=unused-argument
super
(
OpenEndedGradingTab
,
self
)
.
__init__
(
super
(
OpenEndedGradingTab
,
self
)
.
__init__
(
# Translators: "Open Ended Panel" appears on a tab that, when clicked, opens up a panel that
# Translators: "Open Ended Panel" appears on a tab that, when clicked, opens up a panel that
# displays information about open-ended problems that a user has submitted or needs to grade
# displays information about open-ended problems that a user has submitted or needs to grade
...
@@ -579,7 +645,7 @@ class SyllabusTab(CourseTab):
...
@@ -579,7 +645,7 @@ class SyllabusTab(CourseTab):
def
can_display
(
self
,
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
def
can_display
(
self
,
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
return
hasattr
(
course
,
'syllabus_present'
)
and
course
.
syllabus_present
return
hasattr
(
course
,
'syllabus_present'
)
and
course
.
syllabus_present
def
__init__
(
self
,
tab
=
None
):
# pylint: disable=unused-argument
def
__init__
(
self
,
tab
_dict
=
None
):
# pylint: disable=unused-argument
super
(
SyllabusTab
,
self
)
.
__init__
(
super
(
SyllabusTab
,
self
)
.
__init__
(
# Translators: "Syllabus" appears on a tab that, when clicked, opens the syllabus of the course.
# Translators: "Syllabus" appears on a tab that, when clicked, opens the syllabus of the course.
name
=
_
(
'Syllabus'
),
name
=
_
(
'Syllabus'
),
...
@@ -597,16 +663,16 @@ class NotesTab(AuthenticatedCourseTab):
...
@@ -597,16 +663,16 @@ class NotesTab(AuthenticatedCourseTab):
def
can_display
(
self
,
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
def
can_display
(
self
,
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
return
settings
.
FEATURES
.
get
(
'ENABLE_STUDENT_NOTES'
)
return
settings
.
FEATURES
.
get
(
'ENABLE_STUDENT_NOTES'
)
def
__init__
(
self
,
tab
=
None
):
def
__init__
(
self
,
tab
_dict
=
None
):
super
(
NotesTab
,
self
)
.
__init__
(
super
(
NotesTab
,
self
)
.
__init__
(
name
=
tab
[
'name'
],
name
=
tab
_dict
[
'name'
],
tab_id
=
self
.
type
,
tab_id
=
self
.
type
,
link_func
=
link_reverse_func
(
self
.
type
),
link_func
=
link_reverse_func
(
self
.
type
),
)
)
@classmethod
@classmethod
def
validate
(
cls
,
tab
,
raise_error
=
True
):
def
validate
(
cls
,
tab
_dict
,
raise_error
=
True
):
return
super
(
NotesTab
,
cls
)
.
validate
(
tab
,
raise_error
)
and
need_name
(
tab
,
raise_error
)
return
super
(
NotesTab
,
cls
)
.
validate
(
tab
_dict
,
raise_error
)
and
need_name
(
tab_dict
,
raise_error
)
class
InstructorTab
(
StaffTab
):
class
InstructorTab
(
StaffTab
):
...
@@ -615,7 +681,7 @@ class InstructorTab(StaffTab):
...
@@ -615,7 +681,7 @@ class InstructorTab(StaffTab):
"""
"""
type
=
'instructor'
type
=
'instructor'
def
__init__
(
self
,
tab
=
None
):
# pylint: disable=unused-argument
def
__init__
(
self
,
tab
_dict
=
None
):
# pylint: disable=unused-argument
super
(
InstructorTab
,
self
)
.
__init__
(
super
(
InstructorTab
,
self
)
.
__init__
(
# Translators: 'Instructor' appears on the tab that leads to the instructor dashboard, which is
# Translators: 'Instructor' appears on the tab that leads to the instructor dashboard, which is
# a portal where an instructor can get data and perform various actions on their course
# a portal where an instructor can get data and perform various actions on their course
...
@@ -681,36 +747,68 @@ class CourseTabList(List):
...
@@ -681,36 +747,68 @@ class CourseTabList(List):
return
None
return
None
@staticmethod
@staticmethod
def
get_tab_by_slug
(
course
,
url_slug
):
def
get_tab_by_slug
(
tab_list
,
url_slug
):
"""
"""
Look for a tab with the specified 'url_slug'. Returns the tab or None if not found.
Look for a tab with the specified 'url_slug'. Returns the tab or None if not found.
"""
"""
for
tab
in
course
.
tabs
:
return
next
((
tab
for
tab
in
tab_list
if
tab
.
get
(
'url_slug'
)
==
url_slug
),
None
)
# The validation code checks that these exist.
if
tab
.
get
(
'url_slug'
)
==
url_slug
:
return
tab
return
None
@staticmethod
@staticmethod
def
iterate_displayable
(
course
,
settings
,
is_user_authenticated
=
True
,
is_user_staff
=
True
,
include_instructor_tab
=
False
):
def
get_tab_by_type
(
tab_list
,
tab_type
):
"""
Look for a tab with the specified type. Returns the first matching tab.
"""
return
next
((
tab
for
tab
in
tab_list
if
tab
.
type
==
tab_type
),
None
)
@staticmethod
def
get_tab_by_id
(
tab_list
,
tab_id
):
"""
Look for a tab with the specified tab_id. Returns the first matching tab.
"""
return
next
((
tab
for
tab
in
tab_list
if
tab
.
tab_id
==
tab_id
),
None
)
@staticmethod
def
iterate_displayable
(
course
,
settings
,
is_user_authenticated
=
True
,
is_user_staff
=
True
,
):
"""
"""
Generator method for iterating through all tabs that can be displayed for the given course and
Generator method for iterating through all tabs that can be displayed for the given course and
the given user with the provided access settings.
the given user with the provided access settings.
"""
"""
for
tab
in
course
.
tabs
:
for
tab
in
course
.
tabs
:
if
tab
.
can_display
(
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
if
tab
.
can_display
(
if
isinstance
(
tab
,
TextbookTabsBase
):
course
,
settings
,
is_user_authenticated
,
is_user_staff
for
book
in
tab
.
books
(
course
):
)
and
(
not
tab
.
is_hideable
or
not
tab
.
is_hidden
):
yield
book
if
tab
.
is_collection
:
for
item
in
tab
.
items
(
course
):
yield
item
else
:
else
:
yield
tab
yield
tab
if
include_instructor_tab
:
instructor_tab
=
InstructorTab
()
instructor_tab
=
InstructorTab
()
if
instructor_tab
.
can_display
(
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
if
instructor_tab
.
can_display
(
course
,
settings
,
is_user_authenticated
,
is_user_staff
):
yield
instructor_tab
yield
instructor_tab
@staticmethod
def
iterate_displayable_cms
(
course
,
settings
):
"""
Generator method for iterating through all tabs that can be displayed for the given course
with the provided settings.
"""
for
tab
in
course
.
tabs
:
if
tab
.
can_display
(
course
,
settings
,
is_user_authenticated
=
True
,
is_user_staff
=
True
):
if
tab
.
is_collection
and
not
len
(
list
(
tab
.
items
(
course
))):
# do not yield collections that have no items
continue
yield
tab
@classmethod
@classmethod
def
_
validate_tabs
(
cls
,
tabs
):
def
validate_tabs
(
cls
,
tabs
):
"""
"""
Check that the tabs set for the specified course is valid. If it
Check that the tabs set for the specified course is valid. If it
isn't, raise InvalidTabsException with the complaint.
isn't, raise InvalidTabsException with the complaint.
...
@@ -776,8 +874,8 @@ class CourseTabList(List):
...
@@ -776,8 +874,8 @@ class CourseTabList(List):
"""
"""
Overrides the from_json method to de-serialize the CourseTab objects from a json-like representation.
Overrides the from_json method to de-serialize the CourseTab objects from a json-like representation.
"""
"""
self
.
_
validate_tabs
(
values
)
self
.
validate_tabs
(
values
)
return
[
CourseTab
.
from_json
(
tab
)
for
tab
in
values
]
return
[
CourseTab
.
from_json
(
tab
_dict
)
for
tab_dict
in
values
]
#### Link Functions
#### Link Functions
...
@@ -833,3 +931,10 @@ class InvalidTabsException(Exception):
...
@@ -833,3 +931,10 @@ class InvalidTabsException(Exception):
A complaint about invalid tabs.
A complaint about invalid tabs.
"""
"""
pass
pass
class
UnequalTabsException
(
Exception
):
"""
A complaint about tab lists being unequal
"""
pass
common/lib/xmodule/xmodule/tests/test_tabs.py
View file @
2561c010
...
@@ -14,6 +14,16 @@ class TabTestCase(unittest.TestCase):
...
@@ -14,6 +14,16 @@ class TabTestCase(unittest.TestCase):
self
.
settings
=
MagicMock
()
self
.
settings
=
MagicMock
()
self
.
settings
.
FEATURES
=
{}
self
.
settings
.
FEATURES
=
{}
self
.
reverse
=
lambda
name
,
args
:
"name/{0}/args/{1}"
.
format
(
name
,
","
.
join
(
str
(
a
)
for
a
in
args
))
self
.
reverse
=
lambda
name
,
args
:
"name/{0}/args/{1}"
.
format
(
name
,
","
.
join
(
str
(
a
)
for
a
in
args
))
self
.
books
=
None
def
set_up_books
(
self
,
num_books
):
"""Initializes the textbooks in the course and adds the given number of books to each textbook"""
self
.
books
=
[
MagicMock
()
for
_
in
range
(
num_books
)]
for
book_index
,
book
in
enumerate
(
self
.
books
):
book
.
title
=
'Book{0}'
.
format
(
book_index
)
self
.
course
.
textbooks
=
self
.
books
self
.
course
.
pdf_textbooks
=
self
.
books
self
.
course
.
html_textbooks
=
self
.
books
def
check_tab
(
def
check_tab
(
self
,
self
,
...
@@ -57,22 +67,30 @@ class TabTestCase(unittest.TestCase):
...
@@ -57,22 +67,30 @@ class TabTestCase(unittest.TestCase):
self
.
check_get_and_set_methods
(
tab
)
self
.
check_get_and_set_methods
(
tab
)
# check to_json and from_json methods
# check to_json and from_json methods
serialized_tab
=
tab
.
to_json
()
self
.
check_tab_json_methods
(
tab
)
deserialized_tab
=
tab_class
.
from_json
(
serialized_tab
)
self
.
assertEquals
(
serialized_tab
,
deserialized_tab
)
# check equality methods
# check equality methods
self
.
check_tab_equality
(
tab
,
dict_tab
)
# return tab for any additional tests
return
tab
def
check_tab_equality
(
self
,
tab
,
dict_tab
):
"""Tests the equality methods on the given tab"""
self
.
assertEquals
(
tab
,
dict_tab
)
# test __eq__
self
.
assertEquals
(
tab
,
dict_tab
)
# test __eq__
ne_dict_tab
=
dict_tab
ne_dict_tab
=
dict_tab
ne_dict_tab
[
'type'
]
=
'fake_type'
ne_dict_tab
[
'type'
]
=
'fake_type'
self
.
assertNotEquals
(
tab
,
ne_dict_tab
)
# test __ne__: incorrect type
self
.
assertNotEquals
(
tab
,
ne_dict_tab
)
# test __ne__: incorrect type
self
.
assertNotEquals
(
tab
,
{
'fake_key'
:
'fake_value'
})
# test __ne__: missing type
self
.
assertNotEquals
(
tab
,
{
'fake_key'
:
'fake_value'
})
# test __ne__: missing type
# return tab for any additional tests
def
check_tab_json_methods
(
self
,
tab
):
return
tab
"""Tests the json from and to methods on the given tab"""
serialized_tab
=
tab
.
to_json
()
deserialized_tab
=
tab
.
from_json
(
serialized_tab
)
self
.
assertEquals
(
serialized_tab
,
deserialized_tab
)
def
check_can_display_results
(
self
,
tab
,
expected_value
=
True
,
for_authenticated_users_only
=
False
,
for_staff_only
=
False
):
def
check_can_display_results
(
self
,
tab
,
expected_value
=
True
,
for_authenticated_users_only
=
False
,
for_staff_only
=
False
):
"""Check can display results for various users"""
"""Check
s
can display results for various users"""
if
for_staff_only
:
if
for_staff_only
:
self
.
assertEquals
(
self
.
assertEquals
(
expected_value
,
expected_value
,
...
@@ -90,7 +108,7 @@ class TabTestCase(unittest.TestCase):
...
@@ -90,7 +108,7 @@ class TabTestCase(unittest.TestCase):
)
)
def
check_get_and_set_methods
(
self
,
tab
):
def
check_get_and_set_methods
(
self
,
tab
):
"""
t
est __getitem__ and __setitem__ calls"""
"""
T
est __getitem__ and __setitem__ calls"""
self
.
assertEquals
(
tab
[
'type'
],
tab
.
type
)
self
.
assertEquals
(
tab
[
'type'
],
tab
.
type
)
self
.
assertEquals
(
tab
[
'tab_id'
],
tab
.
tab_id
)
self
.
assertEquals
(
tab
[
'tab_id'
],
tab
.
tab_id
)
with
self
.
assertRaises
(
KeyError
):
with
self
.
assertRaises
(
KeyError
):
...
@@ -102,7 +120,7 @@ class TabTestCase(unittest.TestCase):
...
@@ -102,7 +120,7 @@ class TabTestCase(unittest.TestCase):
tab
[
'invalid_key'
]
=
'New Value'
tab
[
'invalid_key'
]
=
'New Value'
def
check_get_and_set_method_for_key
(
self
,
tab
,
key
):
def
check_get_and_set_method_for_key
(
self
,
tab
,
key
):
"""
t
est __getitem__ and __setitem__ for the given key"""
"""
T
est __getitem__ and __setitem__ for the given key"""
old_value
=
tab
[
key
]
old_value
=
tab
[
key
]
new_value
=
'New Value'
new_value
=
'New Value'
tab
[
key
]
=
new_value
tab
[
key
]
=
new_value
...
@@ -149,17 +167,31 @@ class WikiTestCase(TabTestCase):
...
@@ -149,17 +167,31 @@ class WikiTestCase(TabTestCase):
)
)
def
test_wiki_enabled
(
self
):
def
test_wiki_enabled
(
self
):
"""Test wiki tab when Enabled setting is True"""
self
.
settings
.
WIKI_ENABLED
=
True
self
.
settings
.
WIKI_ENABLED
=
True
tab
=
self
.
check_wiki_tab
()
tab
=
self
.
check_wiki_tab
()
self
.
check_can_display_results
(
tab
)
self
.
check_can_display_results
(
tab
)
def
test_wiki_enabled_false
(
self
):
def
test_wiki_enabled_false
(
self
):
"""Test wiki tab when Enabled setting is False"""
self
.
settings
.
WIKI_ENABLED
=
False
self
.
settings
.
WIKI_ENABLED
=
False
tab
=
self
.
check_wiki_tab
()
tab
=
self
.
check_wiki_tab
()
self
.
check_can_display_results
(
tab
,
expected_value
=
False
)
self
.
check_can_display_results
(
tab
,
expected_value
=
False
)
def
test_wiki_visibility
(
self
):
"""Test toggling of visibility of wiki tab"""
wiki_tab
=
tabs
.
WikiTab
()
self
.
assertTrue
(
wiki_tab
.
is_hideable
)
wiki_tab
.
is_hidden
=
True
self
.
assertTrue
(
wiki_tab
[
'is_hidden'
])
self
.
check_tab_json_methods
(
wiki_tab
)
self
.
check_tab_equality
(
wiki_tab
,
wiki_tab
.
to_json
())
wiki_tab
[
'is_hidden'
]
=
False
self
.
assertFalse
(
wiki_tab
.
is_hidden
)
class
ExternalLinkTestCase
(
TabTestCase
):
class
ExternalLinkTestCase
(
TabTestCase
):
"""Test cases for External Link Tab."""
"""Test cases for External Link Tab."""
...
@@ -202,15 +234,9 @@ class TextbooksTestCase(TabTestCase):
...
@@ -202,15 +234,9 @@ class TextbooksTestCase(TabTestCase):
def
setUp
(
self
):
def
setUp
(
self
):
super
(
TextbooksTestCase
,
self
)
.
setUp
()
super
(
TextbooksTestCase
,
self
)
.
setUp
()
self
.
set_up_books
(
2
)
self
.
dict_tab
=
MagicMock
()
self
.
dict_tab
=
MagicMock
()
book1
=
MagicMock
()
book2
=
MagicMock
()
book1
.
title
=
'Book1: Algebra'
book2
.
title
=
'Book2: Topology'
books
=
[
book1
,
book2
]
self
.
course
.
textbooks
=
books
self
.
course
.
pdf_textbooks
=
books
self
.
course
.
html_textbooks
=
books
self
.
course
.
tabs
=
[
self
.
course
.
tabs
=
[
tabs
.
CoursewareTab
(),
tabs
.
CoursewareTab
(),
tabs
.
CourseInfoTab
(),
tabs
.
CourseInfoTab
(),
...
@@ -219,7 +245,7 @@ class TextbooksTestCase(TabTestCase):
...
@@ -219,7 +245,7 @@ class TextbooksTestCase(TabTestCase):
tabs
.
HtmlTextbookTabs
(),
tabs
.
HtmlTextbookTabs
(),
]
]
self
.
num_textbook_tabs
=
sum
(
1
for
tab
in
self
.
course
.
tabs
if
isinstance
(
tab
,
tabs
.
TextbookTabsBase
))
self
.
num_textbook_tabs
=
sum
(
1
for
tab
in
self
.
course
.
tabs
if
isinstance
(
tab
,
tabs
.
TextbookTabsBase
))
self
.
num_textbooks
=
self
.
num_textbook_tabs
*
len
(
books
)
self
.
num_textbooks
=
self
.
num_textbook_tabs
*
len
(
self
.
books
)
def
test_textbooks_enabled
(
self
):
def
test_textbooks_enabled
(
self
):
...
@@ -233,7 +259,7 @@ class TextbooksTestCase(TabTestCase):
...
@@ -233,7 +259,7 @@ class TextbooksTestCase(TabTestCase):
book_type
,
book_index
=
tab
.
tab_id
.
split
(
"/"
,
1
)
book_type
,
book_index
=
tab
.
tab_id
.
split
(
"/"
,
1
)
expected_link
=
self
.
reverse
(
type_to_reverse_name
[
book_type
],
args
=
[
self
.
course
.
id
,
book_index
])
expected_link
=
self
.
reverse
(
type_to_reverse_name
[
book_type
],
args
=
[
self
.
course
.
id
,
book_index
])
self
.
assertEqual
(
tab
.
link_func
(
self
.
course
,
self
.
reverse
),
expected_link
)
self
.
assertEqual
(
tab
.
link_func
(
self
.
course
,
self
.
reverse
),
expected_link
)
self
.
assertTrue
(
tab
.
name
.
startswith
(
'Book{0}
:'
.
format
(
1
+
int
(
book_index
)
)))
self
.
assertTrue
(
tab
.
name
.
startswith
(
'Book{0}
'
.
format
(
book_index
)))
num_textbooks_found
=
num_textbooks_found
+
1
num_textbooks_found
=
num_textbooks_found
+
1
self
.
assertEquals
(
num_textbooks_found
,
self
.
num_textbooks
)
self
.
assertEquals
(
num_textbooks_found
,
self
.
num_textbooks
)
...
@@ -381,10 +407,11 @@ class NeedNameTestCase(unittest.TestCase):
...
@@ -381,10 +407,11 @@ class NeedNameTestCase(unittest.TestCase):
tabs
.
need_name
(
self
.
invalid_dict
)
tabs
.
need_name
(
self
.
invalid_dict
)
class
ValidateTabsTestCase
(
unittest
.
TestCase
):
class
TabListTestCase
(
Tab
TestCase
):
"""
Test cases for validating tab
s."""
"""
Base class for Test cases involving tab list
s."""
def
setUp
(
self
):
def
setUp
(
self
):
super
(
TabListTestCase
,
self
)
.
setUp
()
# invalid tabs
# invalid tabs
self
.
invalid_tabs
=
[
self
.
invalid_tabs
=
[
...
@@ -447,6 +474,12 @@ class ValidateTabsTestCase(unittest.TestCase):
...
@@ -447,6 +474,12 @@ class ValidateTabsTestCase(unittest.TestCase):
],
],
]
]
self
.
all_valid_tab_list
=
tabs
.
CourseTabList
()
.
from_json
(
self
.
valid_tabs
[
1
])
class
ValidateTabsTestCase
(
TabListTestCase
):
"""Test cases for validating tabs."""
def
test_validate_tabs
(
self
):
def
test_validate_tabs
(
self
):
tab_list
=
tabs
.
CourseTabList
()
tab_list
=
tabs
.
CourseTabList
()
for
invalid_tab_list
in
self
.
invalid_tabs
:
for
invalid_tab_list
in
self
.
invalid_tabs
:
...
@@ -458,7 +491,7 @@ class ValidateTabsTestCase(unittest.TestCase):
...
@@ -458,7 +491,7 @@ class ValidateTabsTestCase(unittest.TestCase):
self
.
assertEquals
(
len
(
from_json_result
),
len
(
valid_tab_list
))
self
.
assertEquals
(
len
(
from_json_result
),
len
(
valid_tab_list
))
class
CourseTabListTestCase
(
TabTestCase
):
class
CourseTabListTestCase
(
Tab
List
TestCase
):
"""Testing the generator method for iterating through displayable tabs"""
"""Testing the generator method for iterating through displayable tabs"""
def
test_initialize_default_without_syllabus
(
self
):
def
test_initialize_default_without_syllabus
(
self
):
...
@@ -488,23 +521,64 @@ class CourseTabListTestCase(TabTestCase):
...
@@ -488,23 +521,64 @@ class CourseTabListTestCase(TabTestCase):
self
.
assertTrue
(
tabs
.
DiscussionTab
()
in
self
.
course
.
tabs
)
self
.
assertTrue
(
tabs
.
DiscussionTab
()
in
self
.
course
.
tabs
)
def
test_iterate_displayable
(
self
):
def
test_iterate_displayable
(
self
):
# enable all tab types
self
.
settings
.
FEATURES
[
'ENABLE_TEXTBOOK'
]
=
True
self
.
settings
.
FEATURES
[
'ENABLE_TEXTBOOK'
]
=
True
self
.
course
.
tabs
=
[
self
.
settings
.
FEATURES
[
'ENABLE_DISCUSSION_SERVICE'
]
=
True
tabs
.
CoursewareTab
(),
self
.
settings
.
FEATURES
[
'ENABLE_STUDENT_NOTES'
]
=
True
tabs
.
CourseInfoTab
(),
self
.
course
.
hide_progress_tab
=
False
tabs
.
WikiTab
(),
]
# create 1 book per textbook type
self
.
set_up_books
(
1
)
# initialize the course tabs to a list of all valid tabs
self
.
course
.
tabs
=
self
.
all_valid_tab_list
# enumerate the tabs using the CMS call
for
i
,
tab
in
enumerate
(
tabs
.
CourseTabList
.
iterate_displayable_cms
(
self
.
course
,
self
.
settings
,
)):
self
.
assertEquals
(
tab
.
type
,
self
.
course
.
tabs
[
i
]
.
type
)
# enumerate the tabs and verify textbooks and the instructor tab
for
i
,
tab
in
enumerate
(
tabs
.
CourseTabList
.
iterate_displayable
(
for
i
,
tab
in
enumerate
(
tabs
.
CourseTabList
.
iterate_displayable
(
self
.
course
,
self
.
course
,
self
.
settings
,
self
.
settings
,
include_instructor_tab
=
True
,
)):
)):
if
i
==
len
(
self
.
course
.
tabs
):
if
getattr
(
tab
,
'is_collection_item'
,
False
):
# a collection item was found as a result of a collection tab
self
.
assertTrue
(
getattr
(
self
.
course
.
tabs
[
i
],
'is_collection'
,
False
))
elif
i
==
len
(
self
.
course
.
tabs
):
# the last tab must be the Instructor tab
self
.
assertEquals
(
tab
.
type
,
tabs
.
InstructorTab
.
type
)
self
.
assertEquals
(
tab
.
type
,
tabs
.
InstructorTab
.
type
)
else
:
else
:
# all other tabs must match the expected type
self
.
assertEquals
(
tab
.
type
,
self
.
course
.
tabs
[
i
]
.
type
)
self
.
assertEquals
(
tab
.
type
,
self
.
course
.
tabs
[
i
]
.
type
)
# test including non-empty collections
self
.
assertIn
(
tabs
.
HtmlTextbookTabs
(),
list
(
tabs
.
CourseTabList
.
iterate_displayable_cms
(
self
.
course
,
self
.
settings
)),
)
# test not including empty collections
self
.
course
.
html_textbooks
=
[]
self
.
assertNotIn
(
tabs
.
HtmlTextbookTabs
(),
list
(
tabs
.
CourseTabList
.
iterate_displayable_cms
(
self
.
course
,
self
.
settings
)),
)
def
test_get_tab_by_methods
(
self
):
"""Tests the get_tab methods in CourseTabList"""
self
.
course
.
tabs
=
self
.
all_valid_tab_list
for
tab
in
self
.
course
.
tabs
:
# get tab by type
self
.
assertEquals
(
tabs
.
CourseTabList
.
get_tab_by_type
(
self
.
course
.
tabs
,
tab
.
type
),
tab
)
# get tab by id
self
.
assertEquals
(
tabs
.
CourseTabList
.
get_tab_by_id
(
self
.
course
.
tabs
,
tab
.
tab_id
),
tab
)
class
DiscussionLinkTestCase
(
TabTestCase
):
class
DiscussionLinkTestCase
(
TabTestCase
):
"""Test cases for discussion link tab."""
"""Test cases for discussion link tab."""
...
@@ -526,7 +600,7 @@ class DiscussionLinkTestCase(TabTestCase):
...
@@ -526,7 +600,7 @@ class DiscussionLinkTestCase(TabTestCase):
@staticmethod
@staticmethod
def
_reverse
(
course
):
def
_reverse
(
course
):
"""
c
ustom reverse function"""
"""
C
ustom reverse function"""
def
reverse_discussion_link
(
viewname
,
args
):
def
reverse_discussion_link
(
viewname
,
args
):
"""reverse lookup for discussion link"""
"""reverse lookup for discussion link"""
if
viewname
==
"django_comment_client.forum.views.forum_form_discussion"
and
args
==
[
course
.
id
]:
if
viewname
==
"django_comment_client.forum.views.forum_form_discussion"
and
args
==
[
course
.
id
]:
...
...
lms/djangoapps/courseware/tests/test_tabs.py
View file @
2561c010
...
@@ -45,7 +45,7 @@ class StaticTabDateTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
...
@@ -45,7 +45,7 @@ class StaticTabDateTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
def
test_get_static_tab_contents
(
self
):
def
test_get_static_tab_contents
(
self
):
course
=
get_course_by_id
(
'edX/toy/2012_Fall'
)
course
=
get_course_by_id
(
'edX/toy/2012_Fall'
)
request
=
get_request_for_user
(
UserFactory
.
create
())
request
=
get_request_for_user
(
UserFactory
.
create
())
tab
=
CourseTabList
.
get_tab_by_slug
(
course
,
'resources'
)
tab
=
CourseTabList
.
get_tab_by_slug
(
course
.
tabs
,
'resources'
)
# Test render works okay
# Test render works okay
tab_content
=
get_static_tab_contents
(
request
,
course
,
tab
)
tab_content
=
get_static_tab_contents
(
request
,
course
,
tab
)
...
...
lms/djangoapps/courseware/views.py
View file @
2561c010
...
@@ -482,7 +482,7 @@ def static_tab(request, course_id, tab_slug):
...
@@ -482,7 +482,7 @@ def static_tab(request, course_id, tab_slug):
"""
"""
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'load'
)
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'load'
)
tab
=
CourseTabList
.
get_tab_by_slug
(
course
,
tab_slug
)
tab
=
CourseTabList
.
get_tab_by_slug
(
course
.
tabs
,
tab_slug
)
if
tab
is
None
:
if
tab
is
None
:
raise
Http404
raise
Http404
...
...
lms/envs/common.py
View file @
2561c010
...
@@ -76,10 +76,11 @@ FEATURES = {
...
@@ -76,10 +76,11 @@ FEATURES = {
'FORCE_UNIVERSITY_DOMAIN'
:
False
,
# set this to the university domain to use, as an override to HTTP_HOST
'FORCE_UNIVERSITY_DOMAIN'
:
False
,
# set this to the university domain to use, as an override to HTTP_HOST
# set to None to do no university selection
# set to None to do no university selection
'ENABLE_TEXTBOOK'
:
True
,
# for consistency in user-experience, keep the value of the following 3 settings
# in sync with the corresponding ones in cms/envs/common.py
# for consistency in user-experience, keep the value of this setting in sync with the one in cms/envs/common.py
'ENABLE_DISCUSSION_SERVICE'
:
True
,
'ENABLE_DISCUSSION_SERVICE'
:
True
,
'ENABLE_TEXTBOOK'
:
True
,
'ENABLE_STUDENT_NOTES'
:
True
,
# enables the student notes API and UI.
# discussion home panel, which includes a subscription on/off setting for discussion digest emails.
# discussion home panel, which includes a subscription on/off setting for discussion digest emails.
# this should remain off in production until digest notifications are online.
# this should remain off in production until digest notifications are online.
...
@@ -146,9 +147,6 @@ FEATURES = {
...
@@ -146,9 +147,6 @@ FEATURES = {
# segment.io for LMS--need to explicitly turn it on for production.
# segment.io for LMS--need to explicitly turn it on for production.
'SEGMENT_IO_LMS'
:
False
,
'SEGMENT_IO_LMS'
:
False
,
# Enables the student notes API and UI.
'ENABLE_STUDENT_NOTES'
:
True
,
# Provide a UI to allow users to submit feedback from the LMS (left-hand help modal)
# Provide a UI to allow users to submit feedback from the LMS (left-hand help modal)
'ENABLE_FEEDBACK_SUBMISSION'
:
False
,
'ENABLE_FEEDBACK_SUBMISSION'
:
False
,
...
...
lms/templates/courseware/course_navigation.html
View file @
2561c010
...
@@ -23,7 +23,7 @@ def url_class(is_active):
...
@@ -23,7 +23,7 @@ def url_class(is_active):
<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 CourseTabList.iterate_displayable(course, settings, user.is_authenticated(), has_access(user, course, 'staff')
, include_instructor_tab=True
):
% for tab in CourseTabList.iterate_displayable(course, settings, user.is_authenticated(), has_access(user, course, 'staff')):
<
%
<
%
tab_is_active =
(tab.tab_id
==
active_page
)
tab_is_active =
(tab.tab_id
==
active_page
)
tab_image =
notification_image_for_tab(tab,
user
,
course
)
tab_image =
notification_image_for_tab(tab,
user
,
course
)
...
...
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