Commit 1bd6792a by Nimisha Asthagiri

Support for "View Live" on Pages page.

Settings for ENABLE_STUDENT_NOTES and ENABLE_TEXTBOOK in CMS.
Do not display empty collection tabs.
Updated Changelog.
parent 3b0f7148
...@@ -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
......
...@@ -27,26 +27,26 @@ Feature: CMS.Pages ...@@ -27,26 +27,26 @@ 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 pages When I drag the first static page to the last
Then the static pages are in the reverse order Then the static pages are switched
And I reload the page And I reload the page
Then the static pages are in the reverse order Then the static pages are switched
Scenario: Users can reorder built-in pages Scenario: Users can reorder built-in pages
Given I have opened the pages page in a new course Given I have opened the pages page in a new course
Then the built-in pages are in the default order Then the built-in pages are in the default order
When I reorder the pages When I drag the first page to the last
Then the built-in pages are in the reverse order Then the built-in pages are switched
And I reload the page And I reload the page
Then the built-in pages are in the reverse order Then the built-in pages are switched
Scenario: Users can reorder built-in pages amongst static pages Scenario: Users can reorder built-in pages amongst static pages
Given I have created two different static pages Given I have created two different static pages
Then the pages are in the default order Then the pages are in the default order
When I reorder the pages When I drag the first page to the last
Then the pages are in the reverse order Then the pages are switched
And I reload the page And I reload the page
Then the pages are in the reverse order Then the pages are switched
Scenario: Users can toggle visibility on hideable pages Scenario: Users can toggle visibility on hideable pages
Given I have opened the pages page in a new course Given I have opened the pages page in a new course
......
...@@ -6,6 +6,9 @@ from lettuce import world, step ...@@ -6,6 +6,9 @@ from lettuce import world, step
from nose.tools import assert_equal, assert_in # 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$')
def go_to_static(step): def go_to_static(step):
menu_css = 'li.nav-course-courseware' menu_css = 'li.nav-course-courseware'
...@@ -51,24 +54,24 @@ def change_name(step, new_name): ...@@ -51,24 +54,24 @@ def change_name(step, new_name):
world.css_click(save_button) world.css_click(save_button)
@step(u'I reorder the static pages') @step(u'I drag the first static page to the last$')
def reorder_static_pages(_step): def drag_first_static_page_to_last(step):
reorder_pages_with_css_class('.component') drag_first_to_last_with_css('.component')
@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')
...@@ -78,8 +81,8 @@ def create_two_pages(step): ...@@ -78,8 +81,8 @@ def create_two_pages(step):
_verify_page_names('First', 'Empty') _verify_page_names('First', 'Empty')
@step(u'the static pages are in the reverse order') @step(u'the static pages are switched$')
def static_pages_in_reverse_order(step): def static_pages_are_switched(step):
_verify_page_names('Empty', 'First') _verify_page_names('Empty', 'First')
...@@ -90,51 +93,51 @@ def _verify_page_names(first, second): ...@@ -90,51 +93,51 @@ def _verify_page_names(first, second):
timeout_msg="Timed out waiting for two pages to be present" timeout_msg="Timed out waiting for two pages to be present"
) )
pages = world.css_find('.xmodule_StaticTabModule') pages = world.css_find('.xmodule_StaticTabModule')
assert pages[0].text == first assert_equal(pages[0].text, first)
assert pages[1].text == second assert_equal(pages[1].text, second)
@step(u'the built-in pages are in the default order') @step(u'the built-in pages are in the default order$')
def built_in_pages_in_default_order(step): def built_in_pages_in_default_order(step):
expected_pages = ['Courseware', 'Course Info', 'Discussion', 'Wiki', 'Progress'] expected_pages = ['Courseware', 'Course Info', 'Discussion', 'Wiki', 'Progress']
see_pages_in_expected_order(expected_pages) see_pages_in_expected_order(expected_pages)
@step(u'the built-in pages are in the reverse order') @step(u'the built-in pages are switched$')
def built_in_pages_in_reverse_order(step): def built_in_pages_switched(step):
expected_pages = ['Courseware', 'Course Info', 'Wiki', 'Progress', 'Discussion'] expected_pages = ['Courseware', 'Course Info', 'Wiki', 'Progress', 'Discussion']
see_pages_in_expected_order(expected_pages) see_pages_in_expected_order(expected_pages)
@step(u'the pages are in the default order') @step(u'the pages are in the default order$')
def pages_in_default_order(step): def pages_in_default_order(step):
expected_pages = ['Courseware', 'Course Info', 'Discussion', 'Wiki', 'Progress', 'First', 'Empty'] expected_pages = ['Courseware', 'Course Info', 'Discussion', 'Wiki', 'Progress', 'First', 'Empty']
see_pages_in_expected_order(expected_pages) see_pages_in_expected_order(expected_pages)
@step(u'the pages are in the reverse order') @step(u'the pages are switched$$')
def pages_in_reverse_order(step): def pages_are_switched(step):
expected_pages = ['Courseware', 'Course Info', 'Wiki', 'Progress', 'First', 'Empty', 'Discussion'] expected_pages = ['Courseware', 'Course Info', 'Wiki', 'Progress', 'First', 'Empty', 'Discussion']
see_pages_in_expected_order(expected_pages) see_pages_in_expected_order(expected_pages)
@step(u'I reorder the pages') @step(u'I drag the first page to the last$')
def reorder_pages(step): def drag_first_page_to_last(step):
reorder_pages_with_css_class('.sortable-tab') drag_first_to_last_with_css('.is-movable')
@step(u'I should see the "([^"]*)" page as "(visible|hidden)"$') @step(u'I should see the "([^"]*)" page as "(visible|hidden)"$')
def page_is_visible_or_hidden(step, page_id, visible_or_hidden): def page_is_visible_or_hidden(step, page_id, visible_or_hidden):
hidden = visible_or_hidden == "hidden" hidden = visible_or_hidden == "hidden"
assert world.css_find("li[data-tab-id='{0}'] input.toggle-checkbox".format(page_id)).checked == hidden assert_equal(world.css_find(CSS_FOR_TAB_ELEMENT.format(page_id)).checked, hidden)
@step(u'I toggle the visibility of the "([^"]*)" page') @step(u'I toggle the visibility of the "([^"]*)" page$')
def page_toggle_visibility(step, page_id): def page_toggle_visibility(step, page_id):
world.css_find("li[data-tab-id='{0}'] input.toggle-checkbox".format(page_id))[0].click() world.css_find(CSS_FOR_TAB_ELEMENT.format(page_id))[0].click()
def reorder_pages_with_css_class(css_class): def drag_first_to_last_with_css(css_class):
# For some reason, the drag_and_drop method did not work in this case. # For some reason, the drag_and_drop method did not work in this case.
draggables = world.css_find(css_class + ' .drag-handle') draggables = world.css_find(css_class + ' .drag-handle')
source = draggables.first source = draggables.first
...@@ -149,4 +152,3 @@ def see_pages_in_expected_order(page_names_in_expected_order): ...@@ -149,4 +152,3 @@ def see_pages_in_expected_order(page_names_in_expected_order):
assert_equal(len(page_names_in_expected_order), len(pages)) assert_equal(len(page_names_in_expected_order), len(pages))
for i, page_name in enumerate(page_names_in_expected_order): for i, page_name in enumerate(page_names_in_expected_order):
assert_in(page_name, pages[i].text) assert_in(page_name, pages[i].text)
...@@ -150,7 +150,7 @@ class TestGitExport(CourseTestCase): ...@@ -150,7 +150,7 @@ 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_course() self.populate_course()
git_export_utils.export_to_git( git_export_utils.export_to_git(
self.course.id, self.course.id,
......
...@@ -409,9 +409,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -409,9 +409,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
else: else:
built_in_tabs.append(tab) built_in_tabs.append(tab)
tab_ids = [{'tab_id': tab.tab_id} for tab in (built_in_tabs + reverse_static_tabs)] # create the requested tab_id_locators list
tab_id_locators = [
self.client.ajax_post(new_location.url_reverse('tabs'), {'tabs': tab_ids}) {
'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)
......
...@@ -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()
......
...@@ -16,7 +16,7 @@ from xmodule.modulestore.django import loc_mapper ...@@ -16,7 +16,7 @@ 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, InvalidTabsException from xmodule.tabs import CourseTabList, StaticTab, CourseTab, InvalidTabsException
from ..utils import get_modulestore from ..utils import get_modulestore, get_lms_link_for_item
__all__ = ['tabs_handler'] __all__ = ['tabs_handler']
...@@ -69,16 +69,16 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N ...@@ -69,16 +69,16 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N
if isinstance(tab, StaticTab): if isinstance(tab, StaticTab):
# static tab needs its locator information to render itself as an xmodule # 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_tab = modulestore('direct').get_item(static_tab_loc)
tab.locator = loc_mapper().translate_location( tab.locator = loc_mapper().translate_location(
course_item.location.course_id, static_tab.location, False, True course_item.location.course_id, static_tab_loc, False, True
) )
tabs_to_render.append(tab) tabs_to_render.append(tab)
return render_to_response('edit-tabs.html', { return render_to_response('edit-tabs.html', {
'context_course': course_item, 'context_course': course_item,
'tabs_to_render': tabs_to_render, 'tabs_to_render': tabs_to_render,
'course_locator': locator 'course_locator': locator,
'lms_link': get_lms_link_for_item(course_item.location),
}) })
else: else:
return HttpResponseNotFound() return HttpResponseNotFound()
...@@ -93,14 +93,14 @@ def reorder_tabs_handler(course_item, request): ...@@ -93,14 +93,14 @@ def reorder_tabs_handler(course_item, request):
# The locators are used to identify static tabs since they are xmodules. # 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 # 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. # their tab_ids since the xmodule editor uses only locators to identify new objects.
ids_locators_of_new_tab_order = request.json['tabs'] requested_tab_id_locators = request.json['tabs']
# original tab list in original order # original tab list in original order
old_tab_list = course_item.tabs old_tab_list = course_item.tabs
# create a new list in the new order # create a new list in the new order
new_tab_list = [] new_tab_list = []
for tab_id_locator in ids_locators_of_new_tab_order: for tab_id_locator in requested_tab_id_locators:
tab = get_tab_by_tab_id_locator(old_tab_list, tab_id_locator) tab = get_tab_by_tab_id_locator(old_tab_list, tab_id_locator)
if tab is None: if tab is None:
return JsonResponse( return JsonResponse(
......
...@@ -48,14 +48,17 @@ class TabsPageTests(CourseTestCase): ...@@ -48,14 +48,17 @@ class TabsPageTests(CourseTestCase):
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
self.client.ajax_post( self.client.ajax_post(
self.url, self.url,
data={'tab_id': WikiTab.type, 'unsupported_request': None} data=json.dumps({
'tab_id_locator': {'tab_id': WikiTab.type},
'unsupported_request': None,
}),
) )
# invalid JSON POST request # invalid JSON POST request
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
self.client.ajax_post( self.client.ajax_post(
self.url, self.url,
data={'invalid_request': None} data={'invalid_request': None},
) )
def test_view_index(self): def test_view_index(self):
...@@ -63,7 +66,7 @@ class TabsPageTests(CourseTestCase): ...@@ -63,7 +66,7 @@ class TabsPageTests(CourseTestCase):
resp = self.client.get_html(self.url) resp = self.client.get_html(self.url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertIn('course-nav-tab-list', resp.content) self.assertIn('course-nav-list', resp.content)
def test_reorder_tabs(self): def test_reorder_tabs(self):
"""Test re-ordering of tabs""" """Test re-ordering of tabs"""
...@@ -87,7 +90,7 @@ class TabsPageTests(CourseTestCase): ...@@ -87,7 +90,7 @@ class TabsPageTests(CourseTestCase):
# post the request # post the request
resp = self.client.ajax_post( resp = self.client.ajax_post(
self.url, self.url,
data={'tabs': [{'tab_id': tab_id} for tab_id in tab_ids]} data={'tabs': [{'tab_id': tab_id} for tab_id in tab_ids]},
) )
self.assertEqual(resp.status_code, 204) self.assertEqual(resp.status_code, 204)
...@@ -109,7 +112,7 @@ class TabsPageTests(CourseTestCase): ...@@ -109,7 +112,7 @@ class TabsPageTests(CourseTestCase):
# post the request # post the request
resp = self.client.ajax_post( resp = self.client.ajax_post(
self.url, self.url,
data={'tabs': [{'tab_id': tab_id} for tab_id in tab_ids]} data={'tabs': [{'tab_id': tab_id} for tab_id in tab_ids]},
) )
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
resp_content = json.loads(resp.content) resp_content = json.loads(resp.content)
...@@ -123,7 +126,7 @@ class TabsPageTests(CourseTestCase): ...@@ -123,7 +126,7 @@ class TabsPageTests(CourseTestCase):
# post the request # post the request
resp = self.client.ajax_post( resp = self.client.ajax_post(
self.url, self.url,
data={'tabs': [{'tab_id': tab_id} for tab_id in invalid_tab_ids]} data={'tabs': [{'tab_id': tab_id} for tab_id in invalid_tab_ids]},
) )
self.check_invalid_tab_id_response(resp) self.check_invalid_tab_id_response(resp)
...@@ -141,7 +144,7 @@ class TabsPageTests(CourseTestCase): ...@@ -141,7 +144,7 @@ class TabsPageTests(CourseTestCase):
self.url, self.url,
data=json.dumps({ data=json.dumps({
'tab_id_locator': {'tab_id': old_tab.tab_id}, 'tab_id_locator': {'tab_id': old_tab.tab_id},
'is_hidden': new_is_hidden_setting 'is_hidden': new_is_hidden_setting,
}), }),
) )
self.assertEqual(resp.status_code, 204) self.assertEqual(resp.status_code, 204)
......
...@@ -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 this setting in sync with the # for consistency in user-experience, keep the value of the 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,
......
...@@ -19,7 +19,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views ...@@ -19,7 +19,7 @@ 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)
$('.toggle-checkbox').on('click', @toggleVisibilityOfTab) $('.toggle-checkbox').on('click', @toggleVisibilityOfTab)
@$('.course-nav-tab-list').sortable( @$('.course-nav-list').sortable(
handle: '.drag-handle' handle: '.drag-handle'
update: @tabMoved update: @tabMoved
helper: 'clone' helper: 'clone'
...@@ -27,7 +27,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views ...@@ -27,7 +27,7 @@ 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: '> .sortable-tab' items: '> .is-movable'
) )
toggleVisibilityOfTab: (event, ui) => toggleVisibilityOfTab: (event, ui) =>
...@@ -85,7 +85,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views ...@@ -85,7 +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 sortable-tab') 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')
......
...@@ -40,7 +40,10 @@ ...@@ -40,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>
...@@ -58,39 +61,34 @@ ...@@ -58,39 +61,34 @@
<ol class="course-nav-list course components"> <ol class="course-nav-list course components">
% for tab in tabs_to_render: % for tab in tabs_to_render:
<%
css_class = "course-tab"
if tab.is_movable:
css_class = css_class + " is-movable"
elif (not tab.is_movable) and (not tab.is_hideable):
css_class = css_class + " is-fixed"
%>
% if isinstance(tab, StaticTab): % if isinstance(tab, StaticTab):
<li class="component course-tab sortable-tab" data-locator="${tab.locator}" data-tab-id="${tab.tab_id}"></li> <li class="component ${css_class}" data-locator="${tab.locator}" data-tab-id="${tab.tab_id}"></li>
% else:
<% % else:
tab_name = _(tab.name) <li class="course-nav-item ${css_class}" data-tab-id="${tab.tab_id}">
item_names_formatted = ""
item_names = []
num_items = 0
if tab.is_collection:
item_names = [_(item.name) for item in tab.items(context_course)]
num_items = sum(1 for item in tab.items(context_course))
css_class = "course-nav-item course-nav-tab course-tab"
if tab.is_movable:
css_class = css_class + " sortable-tab"
%>
% if tab.is_hideable or tab.is_movable:
<li class="${css_class}" data-tab-id="${tab.tab_id}">
<div class="course-nav-item-header"> <div class="course-nav-item-header">
% if tab.is_collection: % if tab.is_collection:
<h3 class="title-sub">${tab_name}</h3>
<ul class="course-nav-item-children"> <h3 class="title-sub">${_(tab.name)}</h3>
% for item_name in item_names: <ul class="course-nav-item-children">
<li class="course-nav-item-child title"> % for item in tab.items(context_course):
${item_name} <li class="course-nav-item-child title">
</li> ${_(item.name)}
% endfor </li>
</ul> % endfor
</ul>
% else: % else:
<h3 class="title">${tab_name}</h3> <h3 class="title">${_(tab.name)}</h3>
% endif % endif
</div> </div>
...@@ -99,13 +97,13 @@ ...@@ -99,13 +97,13 @@
% if tab.is_hideable: % if tab.is_hideable:
<li class="action-item action-visible"> <li class="action-item action-visible">
<label><span class="sr">${_("Show this page")}</span></label> <label><span class="sr">${_("Show this page")}</span></label>
% if tab.is_hidden: % if tab.is_hidden:
<input type="checkbox" class="toggle-checkbox" data-tooltip="${_('Show/hide page')}" checked /> <input type="checkbox" class="toggle-checkbox" data-tooltip="${_('Show/hide page')}" checked />
% else: % else:
<input type="checkbox" class="toggle-checkbox" data-tooltip="${_('Show/hide page')}" /> <input type="checkbox" class="toggle-checkbox" data-tooltip="${_('Show/hide page')}" />
% endif % endif
<div class="action-button"><i class="icon-eye-open"></i><i class="icon-eye-close"></i></div> <div class="action-button"><i class="icon-eye-open"></i><i class="icon-eye-close"></i></div>
</li> </li>
% endif % endif
...@@ -123,30 +121,7 @@ ...@@ -123,30 +121,7 @@
% endif % endif
</li> </li>
% else: % endif
<li class="course-nav-item course_tab is-fixed" data-tab-id="${tab.tab_id}">
<div class="course-nav-item-header">
<h3 class="title">${tab_name}</h3>
% if tab.is_collection:
<ul class="course-nav-item-children">
% for item_name in item_names:
<li class="course-nav-item-child">
${item_name}
</li>
% endfor
</ul>
% endif
</ul>
</div>
<div class="drag-handle is-fixed" data-tooltip="${_('This page cannot be reordered')}">
<span class="sr">${_("This page cannot be reordered")}</span>
</div>
</li>
% endif
% endif
% endfor % endfor
<li class="new-component-item"></li> <li class="new-component-item"></li>
......
...@@ -37,7 +37,7 @@ class CourseTab(object): # pylint: disable=incomplete-protocol ...@@ -37,7 +37,7 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
# Class property that specifies whether the tab is a collection of other tabs # Class property that specifies whether the tab is a collection of other tabs
is_collection = False is_collection = False
def __init__(self, name, tab_id, link_func, tab): 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.
...@@ -57,8 +57,6 @@ class CourseTab(object): # pylint: disable=incomplete-protocol ...@@ -57,8 +57,6 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
self.link_func = link_func self.link_func = link_func
self.is_hidden = tab.get('is_hidden', False) if tab else 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.
...@@ -105,8 +103,6 @@ class CourseTab(object): # pylint: disable=incomplete-protocol ...@@ -105,8 +103,6 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
return self.name return self.name
elif key == 'type': elif key == 'type':
return self.type return self.type
elif key == 'is_hidden':
return self.is_hidden
elif key == 'tab_id': elif key == 'tab_id':
return self.tab_id return self.tab_id
else: else:
...@@ -123,8 +119,6 @@ class CourseTab(object): # pylint: disable=incomplete-protocol ...@@ -123,8 +119,6 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
self.name = value self.name = value
elif key == 'tab_id': elif key == 'tab_id':
self.tab_id = value self.tab_id = value
elif key == 'is_hidden':
self.is_hidden = value
else: else:
raise KeyError('Key {0} cannot be set in tab {1}'.format(key, self.to_json())) raise KeyError('Key {0} cannot be set in tab {1}'.format(key, self.to_json()))
...@@ -142,8 +136,8 @@ class CourseTab(object): # pylint: disable=incomplete-protocol ...@@ -142,8 +136,8 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
# allow tabs without names; if a name is required, its presence was checked in the validator. # allow tabs without names; if a name is required, its presence was checked in the validator.
name_is_eq = (other.get('name') is None or self.name == other['name']) name_is_eq = (other.get('name') is None or self.name == other['name'])
# only compare the persisted/serialized members: 'type', 'name', and 'is_hidden' # only compare the persisted/serialized members: 'type' and 'name'
return self.type == other.get('type') and name_is_eq and self.is_hidden == other.get('is_hidden', False) return self.type == other.get('type') and name_is_eq
def __ne__(self, other): def __ne__(self, other):
""" """
...@@ -152,12 +146,12 @@ class CourseTab(object): # pylint: disable=incomplete-protocol ...@@ -152,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): 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):
""" """
...@@ -167,13 +161,10 @@ class CourseTab(object): # pylint: disable=incomplete-protocol ...@@ -167,13 +161,10 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
Returns: Returns:
a dictionary with keys for the properties of the CourseTab object. a dictionary with keys for the properties of the CourseTab object.
""" """
to_json_val = {'type': self.type, 'name': self.name} return {'type': self.type, 'name': self.name}
if self.is_hidden:
to_json_val.update({'is_hidden': True})
return to_json_val
@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.
...@@ -206,15 +197,15 @@ class CourseTab(object): # pylint: disable=incomplete-protocol ...@@ -206,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):
...@@ -233,6 +224,44 @@ class StaffTab(AuthenticatedCourseTab): ...@@ -233,6 +224,44 @@ 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.
...@@ -241,13 +270,12 @@ class CoursewareTab(CourseTab): ...@@ -241,13 +270,12 @@ class CoursewareTab(CourseTab):
type = 'courseware' type = 'courseware'
is_movable = False is_movable = False
def __init__(self, tab=None): 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
tab_id=self.type, tab_id=self.type,
link_func=link_reverse_func(self.type), link_func=link_reverse_func(self.type),
tab=tab,
) )
...@@ -259,18 +287,17 @@ class CourseInfoTab(CourseTab): ...@@ -259,18 +287,17 @@ class CourseInfoTab(CourseTab):
type = 'course_info' type = 'course_info'
is_movable = False 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'),
tab=tab,
) )
@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):
...@@ -280,46 +307,44 @@ class ProgressTab(AuthenticatedCourseTab): ...@@ -280,46 +307,44 @@ 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),
tab=tab,
) )
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 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(CourseTab): class WikiTab(HideableTab):
""" """
A tab containing the course wiki. A tab_dict containing the course wiki.
""" """
type = 'wiki' type = 'wiki'
is_hideable = True
def __init__(self, tab=None): def __init__(self, tab_dict=None):
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=tab, 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):
...@@ -329,21 +354,20 @@ class DiscussionTab(CourseTab): ...@@ -329,21 +354,20 @@ 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'),
tab=tab,
) )
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_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):
...@@ -352,13 +376,12 @@ class LinkTab(CourseTab): ...@@ -352,13 +376,12 @@ class LinkTab(CourseTab):
""" """
link_value = '' link_value = ''
def __init__(self, name, tab_id, link_value, tab): def __init__(self, name, tab_id, link_value):
self.link_value = link_value self.link_value = link_value
super(LinkTab, self).__init__( super(LinkTab, self).__init__(
name=name, name=name,
tab_id=tab_id, tab_id=tab_id,
link_func=link_value_func(self.link_value), link_func=link_value_func(self.link_value),
tab=tab,
) )
def __getitem__(self, key): def __getitem__(self, key):
...@@ -384,8 +407,8 @@ class LinkTab(CourseTab): ...@@ -384,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):
...@@ -395,13 +418,12 @@ class ExternalDiscussionTab(LinkTab): ...@@ -395,13 +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,
tab=tab,
) )
...@@ -411,12 +433,11 @@ class ExternalLinkTab(LinkTab): ...@@ -411,12 +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'],
tab=tab,
) )
...@@ -427,16 +448,15 @@ class StaticTab(CourseTab): ...@@ -427,16 +448,15 @@ class StaticTab(CourseTab):
type = 'static_tab' type = 'static_tab'
@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
super(StaticTab, self).__init__( super(StaticTab, self).__init__(
name=tab['name'] if tab else 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]),
tab=tab,
) )
def __getitem__(self, key): def __getitem__(self, key):
...@@ -481,12 +501,12 @@ class TextbookTabsBase(AuthenticatedCourseTab): ...@@ -481,12 +501,12 @@ class TextbookTabsBase(AuthenticatedCourseTab):
""" """
is_collection = True is_collection = True
def __init__(self, tab_id, tab): def __init__(self, tab_id):
# Translators: 'Textbooks' refers to the tab in the course that leads to the course' textbooks # Translators: 'Textbooks' refers to the tab in the course that leads to the course' textbooks
super(TextbookTabsBase, self).__init__( super(TextbookTabsBase, self).__init__(
name=_("Textbooks"), name=_("Textbooks"),
tab_id=tab_id, tab_id=tab_id,
link_func=None, tab=tab link_func=None,
) )
@abstractmethod @abstractmethod
...@@ -504,10 +524,9 @@ class TextbookTabs(TextbookTabsBase): ...@@ -504,10 +524,9 @@ class TextbookTabs(TextbookTabsBase):
""" """
type = 'textbooks' type = 'textbooks'
def __init__(self, tab=None): def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(TextbookTabs, self).__init__( super(TextbookTabs, self).__init__(
tab_id=self.type, tab_id=self.type,
tab=tab,
) )
def can_display(self, course, settings, is_user_authenticated, is_user_staff): def can_display(self, course, settings, is_user_authenticated, is_user_staff):
...@@ -519,7 +538,6 @@ class TextbookTabs(TextbookTabsBase): ...@@ -519,7 +538,6 @@ class TextbookTabs(TextbookTabsBase):
name=textbook.title, name=textbook.title,
tab_id='textbook/{0}'.format(index), tab_id='textbook/{0}'.format(index),
link_func=lambda course, reverse_func: reverse_func('book', args=[course.id, index]), link_func=lambda course, reverse_func: reverse_func('book', args=[course.id, index]),
tab=None
) )
...@@ -529,10 +547,9 @@ class PDFTextbookTabs(TextbookTabsBase): ...@@ -529,10 +547,9 @@ class PDFTextbookTabs(TextbookTabsBase):
""" """
type = 'pdf_textbooks' type = 'pdf_textbooks'
def __init__(self, tab=None): def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(PDFTextbookTabs, self).__init__( super(PDFTextbookTabs, self).__init__(
tab_id=self.type, tab_id=self.type,
tab=tab,
) )
def items(self, course): def items(self, course):
...@@ -541,7 +558,6 @@ class PDFTextbookTabs(TextbookTabsBase): ...@@ -541,7 +558,6 @@ class PDFTextbookTabs(TextbookTabsBase):
name=textbook['tab_title'], name=textbook['tab_title'],
tab_id='pdftextbook/{0}'.format(index), tab_id='pdftextbook/{0}'.format(index),
link_func=lambda course, reverse_func: reverse_func('pdf_book', args=[course.id, index]), link_func=lambda course, reverse_func: reverse_func('pdf_book', args=[course.id, index]),
tab=None
) )
...@@ -551,10 +567,9 @@ class HtmlTextbookTabs(TextbookTabsBase): ...@@ -551,10 +567,9 @@ class HtmlTextbookTabs(TextbookTabsBase):
""" """
type = 'html_textbooks' type = 'html_textbooks'
def __init__(self, tab=None): def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(HtmlTextbookTabs, self).__init__( super(HtmlTextbookTabs, self).__init__(
tab_id=self.type, tab_id=self.type,
tab=tab,
) )
def items(self, course): def items(self, course):
...@@ -563,7 +578,6 @@ class HtmlTextbookTabs(TextbookTabsBase): ...@@ -563,7 +578,6 @@ class HtmlTextbookTabs(TextbookTabsBase):
name=textbook['tab_title'], name=textbook['tab_title'],
tab_id='htmltextbook/{0}'.format(index), tab_id='htmltextbook/{0}'.format(index),
link_func=lambda course, reverse_func: reverse_func('html_book', args=[course.id, index]), link_func=lambda course, reverse_func: reverse_func('html_book', args=[course.id, index]),
tab=None
) )
...@@ -580,14 +594,13 @@ class StaffGradingTab(StaffTab, GradingTab): ...@@ -580,14 +594,13 @@ class StaffGradingTab(StaffTab, GradingTab):
""" """
type = 'staff_grading' type = 'staff_grading'
def __init__(self, tab=None): 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
name=_("Staff grading"), name=_("Staff grading"),
tab_id=self.type, tab_id=self.type,
link_func=link_reverse_func(self.type), link_func=link_reverse_func(self.type),
tab=tab,
) )
...@@ -597,14 +610,13 @@ class PeerGradingTab(AuthenticatedCourseTab, GradingTab): ...@@ -597,14 +610,13 @@ class PeerGradingTab(AuthenticatedCourseTab, GradingTab):
""" """
type = 'peer_grading' type = 'peer_grading'
def __init__(self, tab=None): 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
name=_("Peer grading"), name=_("Peer grading"),
tab_id=self.type, tab_id=self.type,
link_func=link_reverse_func(self.type), link_func=link_reverse_func(self.type),
tab=tab,
) )
...@@ -614,14 +626,13 @@ class OpenEndedGradingTab(AuthenticatedCourseTab, GradingTab): ...@@ -614,14 +626,13 @@ class OpenEndedGradingTab(AuthenticatedCourseTab, GradingTab):
""" """
type = 'open_ended' type = 'open_ended'
def __init__(self, tab=None): 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
name=_("Open Ended Panel"), name=_("Open Ended Panel"),
tab_id=self.type, tab_id=self.type,
link_func=link_reverse_func('open_ended_notifications'), link_func=link_reverse_func('open_ended_notifications'),
tab=tab,
) )
...@@ -634,13 +645,12 @@ class SyllabusTab(CourseTab): ...@@ -634,13 +645,12 @@ 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): 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'),
tab_id=self.type, tab_id=self.type,
link_func=link_reverse_func(self.type), link_func=link_reverse_func(self.type),
tab=tab,
) )
...@@ -653,17 +663,16 @@ class NotesTab(AuthenticatedCourseTab): ...@@ -653,17 +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),
tab=tab,
) )
@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):
...@@ -672,14 +681,13 @@ class InstructorTab(StaffTab): ...@@ -672,14 +681,13 @@ class InstructorTab(StaffTab):
""" """
type = 'instructor' type = 'instructor'
def __init__(self, tab=None): 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
name=_('Instructor'), name=_('Instructor'),
tab_id=self.type, tab_id=self.type,
link_func=link_reverse_func('instructor_dashboard'), link_func=link_reverse_func('instructor_dashboard'),
tab=tab,
) )
...@@ -771,7 +779,9 @@ class CourseTabList(List): ...@@ -771,7 +779,9 @@ class CourseTabList(List):
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) and not tab.is_hidden: if tab.can_display(
course, settings, is_user_authenticated, is_user_staff
) and (not tab.is_hideable or not tab.is_hidden):
if tab.is_collection: if tab.is_collection:
for item in tab.items(course): for item in tab.items(course):
yield item yield item
...@@ -787,11 +797,14 @@ class CourseTabList(List): ...@@ -787,11 +797,14 @@ class CourseTabList(List):
settings settings
): ):
""" """
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
the given user with the provided access settings. with the provided settings.
""" """
for tab in course.tabs: for tab in course.tabs:
if tab.can_display(course, settings, is_user_authenticated=True, is_user_staff=True): 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 yield tab
@classmethod @classmethod
...@@ -862,7 +875,7 @@ class CourseTabList(List): ...@@ -862,7 +875,7 @@ 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
......
...@@ -17,7 +17,7 @@ class TabTestCase(unittest.TestCase): ...@@ -17,7 +17,7 @@ class TabTestCase(unittest.TestCase):
self.books = None self.books = None
def set_up_books(self, num_books): def set_up_books(self, num_books):
"""initializes the textbooks in the course and adds the given number of books to each textbook""" """Initializes the textbooks in the course and adds the given number of books to each textbook"""
self.books = [MagicMock() for _ in range(num_books)] self.books = [MagicMock() for _ in range(num_books)]
for book_index, book in enumerate(self.books): for book_index, book in enumerate(self.books):
book.title = 'Book{0}'.format(book_index) book.title = 'Book{0}'.format(book_index)
...@@ -76,7 +76,7 @@ class TabTestCase(unittest.TestCase): ...@@ -76,7 +76,7 @@ class TabTestCase(unittest.TestCase):
return tab return tab
def check_tab_equality(self, tab, dict_tab): def check_tab_equality(self, tab, dict_tab):
"""tests the equality methods on the given 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'
...@@ -84,13 +84,13 @@ class TabTestCase(unittest.TestCase): ...@@ -84,13 +84,13 @@ class TabTestCase(unittest.TestCase):
self.assertNotEquals(tab, {'fake_key': 'fake_value'}) # test __ne__: missing type self.assertNotEquals(tab, {'fake_key': 'fake_value'}) # test __ne__: missing type
def check_tab_json_methods(self, tab): def check_tab_json_methods(self, tab):
"""tests the json from and to methods on the given tab""" """Tests the json from and to methods on the given tab"""
serialized_tab = tab.to_json() serialized_tab = tab.to_json()
deserialized_tab = tab.from_json(serialized_tab) deserialized_tab = tab.from_json(serialized_tab)
self.assertEquals(serialized_tab, deserialized_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):
"""checks can display results for various users""" """Checks can display results for various users"""
if for_staff_only: if for_staff_only:
self.assertEquals( self.assertEquals(
expected_value, expected_value,
...@@ -108,7 +108,7 @@ class TabTestCase(unittest.TestCase): ...@@ -108,7 +108,7 @@ class TabTestCase(unittest.TestCase):
) )
def check_get_and_set_methods(self, tab): def check_get_and_set_methods(self, tab):
"""test __getitem__ and __setitem__ calls""" """Test __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):
...@@ -120,7 +120,7 @@ class TabTestCase(unittest.TestCase): ...@@ -120,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):
"""test __getitem__ and __setitem__ for the given key""" """Test __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
...@@ -540,7 +540,7 @@ class CourseTabListTestCase(TabListTestCase): ...@@ -540,7 +540,7 @@ class CourseTabListTestCase(TabListTestCase):
)): )):
self.assertEquals(tab.type, self.course.tabs[i].type) self.assertEquals(tab.type, self.course.tabs[i].type)
# enumerate the tabs and verify textbooks and the instructor tab # 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,
...@@ -555,8 +555,21 @@ class CourseTabListTestCase(TabListTestCase): ...@@ -555,8 +555,21 @@ class CourseTabListTestCase(TabListTestCase):
# all other tabs must match the expected type # 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): def test_get_tab_by_methods(self):
"""tests the get_tab methods in CourseTabList""" """Tests the get_tab methods in CourseTabList"""
self.course.tabs = self.all_valid_tab_list self.course.tabs = self.all_valid_tab_list
for tab in self.course.tabs: for tab in self.course.tabs:
...@@ -587,7 +600,7 @@ class DiscussionLinkTestCase(TabTestCase): ...@@ -587,7 +600,7 @@ class DiscussionLinkTestCase(TabTestCase):
@staticmethod @staticmethod
def _reverse(course): def _reverse(course):
"""custom reverse function""" """Custom 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]:
......
...@@ -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,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment