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,
in roughly chronological order, most recent first. Add your entries at or near
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.
Studio: Support for viewing built-in tabs on the Pages page. STUD-1193
......
......@@ -27,26 +27,26 @@ Feature: CMS.Pages
@skip_safari
Scenario: Users can reorder static pages
Given I have created two different static pages
When I reorder the static pages
Then the static pages are in the reverse order
When I drag the first static page to the last
Then the static pages are switched
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
Given I have opened the pages page in a new course
Then the built-in pages are in the default order
When I reorder the pages
Then the built-in pages are in the reverse 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 in the reverse order
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 reorder the pages
Then the pages are in the reverse order
When I drag the first page to the last
Then the pages are switched
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
Given I have opened the pages page in a new course
......
......@@ -6,6 +6,9 @@ from lettuce import world, step
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$')
def go_to_static(step):
menu_css = 'li.nav-course-courseware'
......@@ -51,24 +54,24 @@ def change_name(step, new_name):
world.css_click(save_button)
@step(u'I reorder the static pages')
def reorder_static_pages(_step):
reorder_pages_with_css_class('.component')
@step(u'I drag the first static page to the last$')
def drag_first_static_page_to_last(step):
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):
step.given('I have opened the pages page in a new course')
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):
step.given('I have opened a new course in Studio')
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):
step.given('I have created a static page')
step.given('I "edit" the static page')
......@@ -78,8 +81,8 @@ def create_two_pages(step):
_verify_page_names('First', 'Empty')
@step(u'the static pages are in the reverse order')
def static_pages_in_reverse_order(step):
@step(u'the static pages are switched$')
def static_pages_are_switched(step):
_verify_page_names('Empty', 'First')
......@@ -90,51 +93,51 @@ def _verify_page_names(first, second):
timeout_msg="Timed out waiting for two pages to be present"
)
pages = world.css_find('.xmodule_StaticTabModule')
assert pages[0].text == first
assert pages[1].text == second
assert_equal(pages[0].text, first)
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):
expected_pages = ['Courseware', 'Course Info', 'Discussion', 'Wiki', 'Progress']
see_pages_in_expected_order(expected_pages)
@step(u'the built-in pages are in the reverse order')
def built_in_pages_in_reverse_order(step):
@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')
@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 in the reverse order')
def pages_in_reverse_order(step):
@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 reorder the pages')
def reorder_pages(step):
reorder_pages_with_css_class('.sortable-tab')
@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 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):
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.
draggables = world.css_find(css_class + ' .drag-handle')
source = draggables.first
......@@ -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))
for i, page_name in enumerate(page_names_in_expected_order):
assert_in(page_name, pages[i].text)
......@@ -150,7 +150,7 @@ class TestGitExport(CourseTestCase):
'--format=%an|%ae'], cwd=cwd)
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()
git_export_utils.export_to_git(
self.course.id,
......
......@@ -409,9 +409,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
else:
built_in_tabs.append(tab)
tab_ids = [{'tab_id': tab.tab_id} for tab in (built_in_tabs + reverse_static_tabs)]
self.client.ajax_post(new_location.url_reverse('tabs'), {'tabs': tab_ids})
# 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)
......
......@@ -918,9 +918,9 @@ def textbooks_detail_handler(request, tid, tag=None, package_id=None, branch=Non
if not textbook:
return JsonResponse(status=404)
i = course.pdf_textbooks.index(textbook)
new_textbooks = course.pdf_textbooks[0:i]
new_textbooks.extend(course.pdf_textbooks[i + 1:])
course.pdf_textbooks = new_textbooks
remaining_textbooks = course.pdf_textbooks[0:i]
remaining_textbooks.extend(course.pdf_textbooks[i + 1:])
course.pdf_textbooks = remaining_textbooks
store.update_item(course, request.user.id)
return JsonResponse()
......
......@@ -16,7 +16,7 @@ from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator
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']
......@@ -69,16 +69,16 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N
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 = modulestore('direct').get_item(static_tab_loc)
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)
return render_to_response('edit-tabs.html', {
'context_course': course_item,
'tabs_to_render': tabs_to_render,
'course_locator': locator
'course_locator': locator,
'lms_link': get_lms_link_for_item(course_item.location),
})
else:
return HttpResponseNotFound()
......@@ -93,14 +93,14 @@ def reorder_tabs_handler(course_item, request):
# 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.
ids_locators_of_new_tab_order = request.json['tabs']
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 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)
if tab is None:
return JsonResponse(
......
......@@ -48,14 +48,17 @@ class TabsPageTests(CourseTestCase):
with self.assertRaises(NotImplementedError):
self.client.ajax_post(
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
with self.assertRaises(NotImplementedError):
self.client.ajax_post(
self.url,
data={'invalid_request': None}
data={'invalid_request': None},
)
def test_view_index(self):
......@@ -63,7 +66,7 @@ class TabsPageTests(CourseTestCase):
resp = self.client.get_html(self.url)
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):
"""Test re-ordering of tabs"""
......@@ -87,7 +90,7 @@ class TabsPageTests(CourseTestCase):
# post the request
resp = self.client.ajax_post(
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)
......@@ -109,7 +112,7 @@ class TabsPageTests(CourseTestCase):
# post the request
resp = self.client.ajax_post(
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)
resp_content = json.loads(resp.content)
......@@ -123,7 +126,7 @@ class TabsPageTests(CourseTestCase):
# post the request
resp = self.client.ajax_post(
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)
......@@ -141,7 +144,7 @@ class TabsPageTests(CourseTestCase):
self.url,
data=json.dumps({
'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)
......
......@@ -43,9 +43,11 @@ FEATURES = {
'GITHUB_PUSH': False,
# for consistency in user-experience, keep the value of this setting in sync with the
# one in lms/envs/common.py
# for consistency in user-experience, keep the value of the following 3 settings
# in sync with the ones in lms/envs/common.py
'ENABLE_DISCUSSION_SERVICE': True,
'ENABLE_TEXTBOOK': True,
'ENABLE_STUDENT_NOTES': True,
'AUTH_USE_CERTIFICATES': False,
......
......@@ -19,7 +19,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
@options.mast.find('.new-tab').on('click', @addNewTab)
$('.add-pages .new-tab').on('click', @addNewTab)
$('.toggle-checkbox').on('click', @toggleVisibilityOfTab)
@$('.course-nav-tab-list').sortable(
@$('.course-nav-list').sortable(
handle: '.drag-handle'
update: @tabMoved
helper: 'clone'
......@@ -27,7 +27,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
placeholder: 'component-placeholder'
forcePlaceholderSize: true
axis: 'y'
items: '> .sortable-tab'
items: '> .is-movable'
)
toggleVisibilityOfTab: (event, ui) =>
......@@ -85,7 +85,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
)
$('.new-component-item').before(editor.$el)
editor.$el.addClass('course-tab sortable-tab')
editor.$el.addClass('course-tab is-movable')
editor.$el.addClass('new')
setTimeout(=>
editor.$el.removeClass('new')
......
......@@ -40,7 +40,10 @@
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
<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>
</ul>
</nav>
......@@ -58,39 +61,34 @@
<ol class="course-nav-list course components">
% 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):
<li class="component course-tab sortable-tab" data-locator="${tab.locator}" data-tab-id="${tab.tab_id}"></li>
% else:
<li class="component ${css_class}" data-locator="${tab.locator}" data-tab-id="${tab.tab_id}"></li>
<%
tab_name = _(tab.name)
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}">
% else:
<li class="course-nav-item ${css_class}" data-tab-id="${tab.tab_id}">
<div class="course-nav-item-header">
% if tab.is_collection:
<h3 class="title-sub">${tab_name}</h3>
<ul class="course-nav-item-children">
% for item_name in item_names:
<li class="course-nav-item-child title">
${item_name}
</li>
% endfor
</ul>
<h3 class="title-sub">${_(tab.name)}</h3>
<ul class="course-nav-item-children">
% for item in tab.items(context_course):
<li class="course-nav-item-child title">
${_(item.name)}
</li>
% endfor
</ul>
% else:
<h3 class="title">${tab_name}</h3>
<h3 class="title">${_(tab.name)}</h3>
% endif
</div>
......@@ -99,13 +97,13 @@
% 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>
<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
......@@ -123,30 +121,7 @@
% endif
</li>
% else:
<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
% endif
% endfor
<li class="new-component-item"></li>
......
......@@ -37,7 +37,7 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
# Class property that specifies whether the tab is a collection of other tabs
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.
......@@ -57,8 +57,6 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
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
"""
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
return self.name
elif key == 'type':
return self.type
elif key == 'is_hidden':
return self.is_hidden
elif key == 'tab_id':
return self.tab_id
else:
......@@ -123,8 +119,6 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
self.name = value
elif key == 'tab_id':
self.tab_id = value
elif key == 'is_hidden':
self.is_hidden = value
else:
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
# 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'])
# only compare the persisted/serialized members: 'type', 'name', and 'is_hidden'
return self.type == other.get('type') and name_is_eq and self.is_hidden == other.get('is_hidden', False)
# only compare the persisted/serialized members: 'type' and 'name'
return self.type == other.get('type') and name_is_eq
def __ne__(self, other):
"""
......@@ -152,12 +146,12 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
return not (self == other)
@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.
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):
"""
......@@ -167,13 +161,10 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
Returns:
a dictionary with keys for the properties of the CourseTab object.
"""
to_json_val = {'type': self.type, 'name': self.name}
if self.is_hidden:
to_json_val.update({'is_hidden': True})
return to_json_val
return {'type': self.type, 'name': self.name}
@staticmethod
def from_json(tab):
def from_json(tab_dict):
"""
Deserializes a CourseTab from a json-like representation.
......@@ -206,15 +197,15 @@ class CourseTab(object): # pylint: disable=incomplete-protocol
'instructor': InstructorTab, # not persisted
}
tab_type = tab.get('type')
tab_type = tab_dict.get('type')
if tab_type not in sub_class_types:
raise InvalidTabsException(
'Unknown tab type {0}. Known types: {1}'.format(tab_type, sub_class_types)
)
tab_class = sub_class_types[tab['type']]
tab_class.validate(tab)
return tab_class(tab=tab)
tab_class = sub_class_types[tab_dict['type']]
tab_class.validate(tab_dict)
return tab_class(tab_dict=tab_dict)
class AuthenticatedCourseTab(CourseTab):
......@@ -233,6 +224,44 @@ class StaffTab(AuthenticatedCourseTab):
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):
"""
A tab containing the course content.
......@@ -241,13 +270,12 @@ class CoursewareTab(CourseTab):
type = 'courseware'
is_movable = False
def __init__(self, tab=None):
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(CoursewareTab, self).__init__(
# 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
tab_id=self.type,
link_func=link_reverse_func(self.type),
tab=tab,
)
......@@ -259,18 +287,17 @@ class CourseInfoTab(CourseTab):
type = 'course_info'
is_movable = False
def __init__(self, tab=None):
def __init__(self, tab_dict=None):
super(CourseInfoTab, self).__init__(
# 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',
link_func=link_reverse_func('info'),
tab=tab,
)
@classmethod
def validate(cls, tab, raise_error=True):
return super(CourseInfoTab, cls).validate(tab, raise_error) and need_name(tab, raise_error)
def validate(cls, tab_dict, raise_error=True):
return super(CourseInfoTab, cls).validate(tab_dict, raise_error) and need_name(tab_dict, raise_error)
class ProgressTab(AuthenticatedCourseTab):
......@@ -280,46 +307,44 @@ class ProgressTab(AuthenticatedCourseTab):
type = 'progress'
def __init__(self, tab=None):
def __init__(self, tab_dict=None):
super(ProgressTab, self).__init__(
# 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,
link_func=link_reverse_func(self.type),
tab=tab,
)
def can_display(self, course, settings, is_user_authenticated, is_user_staff):
return not course.hide_progress_tab
@classmethod
def validate(cls, tab, raise_error=True):
return super(ProgressTab, cls).validate(tab, raise_error) and need_name(tab, raise_error)
def validate(cls, tab_dict, raise_error=True):
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'
is_hideable = True
def __init__(self, tab=None):
def __init__(self, tab_dict=None):
super(WikiTab, self).__init__(
# 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,
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):
return settings.WIKI_ENABLED
@classmethod
def validate(cls, tab, raise_error=True):
return super(WikiTab, cls).validate(tab, raise_error) and need_name(tab, raise_error)
def validate(cls, tab_dict, raise_error=True):
return super(WikiTab, cls).validate(tab_dict, raise_error) and need_name(tab_dict, raise_error)
class DiscussionTab(CourseTab):
......@@ -329,21 +354,20 @@ class DiscussionTab(CourseTab):
type = 'discussion'
def __init__(self, tab=None):
def __init__(self, tab_dict=None):
super(DiscussionTab, self).__init__(
# 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,
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):
return settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE')
@classmethod
def validate(cls, tab, raise_error=True):
return super(DiscussionTab, cls).validate(tab, raise_error) and need_name(tab, raise_error)
def validate(cls, tab_dict, raise_error=True):
return super(DiscussionTab, cls).validate(tab_dict, raise_error) and need_name(tab_dict, raise_error)
class LinkTab(CourseTab):
......@@ -352,13 +376,12 @@ class LinkTab(CourseTab):
"""
link_value = ''
def __init__(self, name, tab_id, link_value, tab):
def __init__(self, name, tab_id, link_value):
self.link_value = link_value
super(LinkTab, self).__init__(
name=name,
tab_id=tab_id,
link_func=link_value_func(self.link_value),
tab=tab,
)
def __getitem__(self, key):
......@@ -384,8 +407,8 @@ class LinkTab(CourseTab):
return self.link_value == other.get('link')
@classmethod
def validate(cls, tab, raise_error=True):
return super(LinkTab, cls).validate(tab, raise_error) and key_checker(['link'])(tab, raise_error)
def validate(cls, tab_dict, raise_error=True):
return super(LinkTab, cls).validate(tab_dict, raise_error) and key_checker(['link'])(tab_dict, raise_error)
class ExternalDiscussionTab(LinkTab):
......@@ -395,13 +418,12 @@ class ExternalDiscussionTab(LinkTab):
type = 'external_discussion'
def __init__(self, tab=None, link_value=None):
def __init__(self, tab_dict=None, link_value=None):
super(ExternalDiscussionTab, self).__init__(
# Translators: 'Discussion' refers to the tab in the courseware that leads to the discussion forums
name=_('Discussion'),
tab_id='discussion',
link_value=tab['link'] if tab else link_value,
tab=tab,
link_value=tab_dict['link'] if tab_dict else link_value,
)
......@@ -411,12 +433,11 @@ class ExternalLinkTab(LinkTab):
"""
type = 'external_link'
def __init__(self, tab):
def __init__(self, tab_dict):
super(ExternalLinkTab, self).__init__(
name=tab['name'],
name=tab_dict['name'],
tab_id=None, # External links are never active.
link_value=tab['link'],
tab=tab,
link_value=tab_dict['link'],
)
......@@ -427,16 +448,15 @@ class StaticTab(CourseTab):
type = 'static_tab'
@classmethod
def validate(cls, tab, raise_error=True):
return super(StaticTab, cls).validate(tab, raise_error) and key_checker(['name', 'url_slug'])(tab, raise_error)
def validate(cls, tab_dict, raise_error=True):
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):
self.url_slug = tab['url_slug'] if tab else url_slug
def __init__(self, tab_dict=None, name=None, url_slug=None):
self.url_slug = tab_dict['url_slug'] if tab_dict else url_slug
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),
link_func=lambda course, reverse_func: reverse_func(self.type, args=[course.id, self.url_slug]),
tab=tab,
)
def __getitem__(self, key):
......@@ -481,12 +501,12 @@ class TextbookTabsBase(AuthenticatedCourseTab):
"""
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
super(TextbookTabsBase, self).__init__(
name=_("Textbooks"),
tab_id=tab_id,
link_func=None, tab=tab
link_func=None,
)
@abstractmethod
......@@ -504,10 +524,9 @@ class TextbookTabs(TextbookTabsBase):
"""
type = 'textbooks'
def __init__(self, tab=None):
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(TextbookTabs, self).__init__(
tab_id=self.type,
tab=tab,
)
def can_display(self, course, settings, is_user_authenticated, is_user_staff):
......@@ -519,7 +538,6 @@ class TextbookTabs(TextbookTabsBase):
name=textbook.title,
tab_id='textbook/{0}'.format(index),
link_func=lambda course, reverse_func: reverse_func('book', args=[course.id, index]),
tab=None
)
......@@ -529,10 +547,9 @@ class PDFTextbookTabs(TextbookTabsBase):
"""
type = 'pdf_textbooks'
def __init__(self, tab=None):
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(PDFTextbookTabs, self).__init__(
tab_id=self.type,
tab=tab,
)
def items(self, course):
......@@ -541,7 +558,6 @@ class PDFTextbookTabs(TextbookTabsBase):
name=textbook['tab_title'],
tab_id='pdftextbook/{0}'.format(index),
link_func=lambda course, reverse_func: reverse_func('pdf_book', args=[course.id, index]),
tab=None
)
......@@ -551,10 +567,9 @@ class HtmlTextbookTabs(TextbookTabsBase):
"""
type = 'html_textbooks'
def __init__(self, tab=None):
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(HtmlTextbookTabs, self).__init__(
tab_id=self.type,
tab=tab,
)
def items(self, course):
......@@ -563,7 +578,6 @@ class HtmlTextbookTabs(TextbookTabsBase):
name=textbook['tab_title'],
tab_id='htmltextbook/{0}'.format(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):
"""
type = 'staff_grading'
def __init__(self, tab=None):
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(StaffGradingTab, self).__init__(
# Translators: "Staff grading" appears on a tab that allows
# staff to view open-ended problems that require staff grading
name=_("Staff grading"),
tab_id=self.type,
link_func=link_reverse_func(self.type),
tab=tab,
)
......@@ -597,14 +610,13 @@ class PeerGradingTab(AuthenticatedCourseTab, GradingTab):
"""
type = 'peer_grading'
def __init__(self, tab=None):
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(PeerGradingTab, self).__init__(
# Translators: "Peer grading" appears on a tab that allows
# students to view open-ended problems that require grading
name=_("Peer grading"),
tab_id=self.type,
link_func=link_reverse_func(self.type),
tab=tab,
)
......@@ -614,14 +626,13 @@ class OpenEndedGradingTab(AuthenticatedCourseTab, GradingTab):
"""
type = 'open_ended'
def __init__(self, tab=None):
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(OpenEndedGradingTab, self).__init__(
# 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
name=_("Open Ended Panel"),
tab_id=self.type,
link_func=link_reverse_func('open_ended_notifications'),
tab=tab,
)
......@@ -634,13 +645,12 @@ class SyllabusTab(CourseTab):
def can_display(self, course, settings, is_user_authenticated, is_user_staff):
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__(
# Translators: "Syllabus" appears on a tab that, when clicked, opens the syllabus of the course.
name=_('Syllabus'),
tab_id=self.type,
link_func=link_reverse_func(self.type),
tab=tab,
)
......@@ -653,17 +663,16 @@ class NotesTab(AuthenticatedCourseTab):
def can_display(self, course, settings, is_user_authenticated, is_user_staff):
return settings.FEATURES.get('ENABLE_STUDENT_NOTES')
def __init__(self, tab=None):
def __init__(self, tab_dict=None):
super(NotesTab, self).__init__(
name=tab['name'],
name=tab_dict['name'],
tab_id=self.type,
link_func=link_reverse_func(self.type),
tab=tab,
)
@classmethod
def validate(cls, tab, raise_error=True):
return super(NotesTab, cls).validate(tab, raise_error) and need_name(tab, raise_error)
def validate(cls, tab_dict, raise_error=True):
return super(NotesTab, cls).validate(tab_dict, raise_error) and need_name(tab_dict, raise_error)
class InstructorTab(StaffTab):
......@@ -672,14 +681,13 @@ class InstructorTab(StaffTab):
"""
type = 'instructor'
def __init__(self, tab=None):
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
super(InstructorTab, self).__init__(
# 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
name=_('Instructor'),
tab_id=self.type,
link_func=link_reverse_func('instructor_dashboard'),
tab=tab,
)
......@@ -771,7 +779,9 @@ class CourseTabList(List):
the given user with the provided access settings.
"""
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:
for item in tab.items(course):
yield item
......@@ -787,11 +797,14 @@ class CourseTabList(List):
settings
):
"""
Generator method for iterating through all tabs that can be displayed for the given course and
the given user with the provided access 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
......@@ -862,7 +875,7 @@ class CourseTabList(List):
Overrides the from_json method to de-serialize the CourseTab objects from a json-like representation.
"""
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
......
......@@ -17,7 +17,7 @@ class TabTestCase(unittest.TestCase):
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"""
"""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)
......@@ -76,7 +76,7 @@ class TabTestCase(unittest.TestCase):
return 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__
ne_dict_tab = dict_tab
ne_dict_tab['type'] = 'fake_type'
......@@ -84,13 +84,13 @@ class TabTestCase(unittest.TestCase):
self.assertNotEquals(tab, {'fake_key': 'fake_value'}) # test __ne__: missing type
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()
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):
"""checks can display results for various users"""
"""Checks can display results for various users"""
if for_staff_only:
self.assertEquals(
expected_value,
......@@ -108,7 +108,7 @@ class TabTestCase(unittest.TestCase):
)
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['tab_id'], tab.tab_id)
with self.assertRaises(KeyError):
......@@ -120,7 +120,7 @@ class TabTestCase(unittest.TestCase):
tab['invalid_key'] = 'New Value'
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]
new_value = 'New Value'
tab[key] = new_value
......@@ -540,7 +540,7 @@ class CourseTabListTestCase(TabListTestCase):
)):
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(
self.course,
self.settings,
......@@ -555,8 +555,21 @@ class CourseTabListTestCase(TabListTestCase):
# all other tabs must match the expected 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"""
"""Tests the get_tab methods in CourseTabList"""
self.course.tabs = self.all_valid_tab_list
for tab in self.course.tabs:
......@@ -587,7 +600,7 @@ class DiscussionLinkTestCase(TabTestCase):
@staticmethod
def _reverse(course):
"""custom reverse function"""
"""Custom reverse function"""
def reverse_discussion_link(viewname, args):
"""reverse lookup for discussion link"""
if viewname == "django_comment_client.forum.views.forum_form_discussion" and args == [course.id]:
......
......@@ -76,10 +76,11 @@ FEATURES = {
'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
'ENABLE_TEXTBOOK': True,
# for consistency in user-experience, keep the value of this setting in sync with the one in cms/envs/common.py
# for consistency in user-experience, keep the value of the following 3 settings
# in sync with the corresponding ones in cms/envs/common.py
'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.
# this should remain off in production until digest notifications are online.
......@@ -146,9 +147,6 @@ FEATURES = {
# segment.io for LMS--need to explicitly turn it on for production.
'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)
'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