Merge pull request #8015 from edx/andya/add-tab-extensions
Add extensible course view types for edX platform
This commit is contained in:
@@ -22,7 +22,6 @@ from xmodule.modulestore.django import modulestore, clear_existing_modulestores
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
|
||||
from xmodule.modulestore.tests.sample_courses import default_block_info_tree, TOY_BLOCK_INFO_TREE
|
||||
from xmodule.modulestore.tests.factories import XMODULE_FACTORY_LOCK
|
||||
from xmodule.tabs import CoursewareTab, CourseInfoTab, StaticTab, DiscussionTab, ProgressTab, WikiTab
|
||||
|
||||
|
||||
class StoreConstructors(object):
|
||||
@@ -379,15 +378,6 @@ class ModuleStoreTestCase(TestCase):
|
||||
"wiki_slug": "toy",
|
||||
"display_name": "Toy Course",
|
||||
"graded": True,
|
||||
"tabs": [
|
||||
CoursewareTab(),
|
||||
CourseInfoTab(),
|
||||
StaticTab(name="Syllabus", url_slug="syllabus"),
|
||||
StaticTab(name="Resources", url_slug="resources"),
|
||||
DiscussionTab(),
|
||||
WikiTab(),
|
||||
ProgressTab(),
|
||||
],
|
||||
"discussion_topics": {"General": {"id": "i4x-edX-toy-course-2012_Fall"}},
|
||||
"graceperiod": datetime.timedelta(days=2, seconds=21599),
|
||||
"start": datetime.datetime(2015, 07, 17, 12, tzinfo=pytz.utc),
|
||||
|
||||
@@ -13,8 +13,8 @@ import dogstats_wrapper as dog_stats_api
|
||||
from opaque_keys.edx.locations import Location
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from xblock.core import XBlock
|
||||
from xmodule.tabs import StaticTab
|
||||
from xmodule.modulestore import prefer_xmodules, ModuleStoreEnum
|
||||
from xmodule.tabs import CourseTab
|
||||
from xmodule.x_module import DEPRECATION_VSCOMPAT_EVENT
|
||||
|
||||
|
||||
@@ -277,10 +277,7 @@ class ItemFactory(XModuleFactory):
|
||||
|
||||
course = store.get_course(location.course_key)
|
||||
course.tabs.append(
|
||||
StaticTab(
|
||||
name=display_name,
|
||||
url_slug=location.name,
|
||||
)
|
||||
CourseTab.load('static_tab', name='Static Tab', url_slug=location.name)
|
||||
)
|
||||
store.update_item(course, user_id)
|
||||
|
||||
|
||||
@@ -373,25 +373,6 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
|
||||
'{0} is a template course'.format(course)
|
||||
)
|
||||
|
||||
def test_static_tab_names(self):
|
||||
|
||||
def get_tab_name(index):
|
||||
"""
|
||||
Helper function for pulling out the name of a given static tab.
|
||||
|
||||
Assumes the information is desired for courses[4] ('toy' course).
|
||||
"""
|
||||
course = self.draft_store.get_course(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
|
||||
return course.tabs[index]['name']
|
||||
|
||||
# There was a bug where model.save was not getting called after the static tab name
|
||||
# was set set for tabs that have a URL slug. 'Syllabus' and 'Resources' fall into that
|
||||
# category, but for completeness, I'm also testing 'Course Info' and 'Discussion' (no url slug).
|
||||
assert_equals('Course Info', get_tab_name(1))
|
||||
assert_equals('Syllabus', get_tab_name(2))
|
||||
assert_equals('Resources', get_tab_name(3))
|
||||
assert_equals('Discussion', get_tab_name(4))
|
||||
|
||||
def test_contentstore_attrs(self):
|
||||
"""
|
||||
Test getting, setting, and defaulting the locked attr and arbitrary attrs.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Test split modulestore w/o using any django stuff.
|
||||
"""
|
||||
from mock import patch
|
||||
import datetime
|
||||
from importlib import import_module
|
||||
from path import path
|
||||
@@ -36,6 +37,14 @@ BRANCH_NAME_DRAFT = ModuleStoreEnum.BranchName.draft
|
||||
BRANCH_NAME_PUBLISHED = ModuleStoreEnum.BranchName.published
|
||||
|
||||
|
||||
def mock_tab_from_json(tab_dict):
|
||||
"""
|
||||
Mocks out the CourseTab.from_json to just return the tab_dict itself so that we don't have to deal
|
||||
with plugin errors.
|
||||
"""
|
||||
return tab_dict
|
||||
|
||||
|
||||
@attr('mongo')
|
||||
class SplitModuleTest(unittest.TestCase):
|
||||
'''
|
||||
@@ -596,7 +605,8 @@ class SplitModuleCourseTests(SplitModuleTest):
|
||||
Course CRUD operation tests
|
||||
'''
|
||||
|
||||
def test_get_courses(self):
|
||||
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
|
||||
def test_get_courses(self, _from_json):
|
||||
courses = modulestore().get_courses(branch=BRANCH_NAME_DRAFT)
|
||||
# should have gotten 3 draft courses
|
||||
self.assertEqual(len(courses), 3, "Wrong number of courses")
|
||||
@@ -635,7 +645,8 @@ class SplitModuleCourseTests(SplitModuleTest):
|
||||
courses = modulestore().get_courses(branch=BRANCH_NAME_DRAFT)
|
||||
self.assertEqual(len(courses), 3)
|
||||
|
||||
def test_branch_requests(self):
|
||||
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
|
||||
def test_branch_requests(self, _from_json):
|
||||
# query w/ branch qualifier (both draft and published)
|
||||
def _verify_published_course(courses_published):
|
||||
""" Helper function for verifying published course. """
|
||||
@@ -665,7 +676,8 @@ class SplitModuleCourseTests(SplitModuleTest):
|
||||
locator_key_fields=['org', 'course', 'run']
|
||||
)
|
||||
|
||||
def test_get_course(self):
|
||||
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
|
||||
def test_get_course(self, _from_json):
|
||||
'''
|
||||
Test the various calling forms for get_course
|
||||
'''
|
||||
|
||||
@@ -5,7 +5,7 @@ well-formed and not-well-formed XML.
|
||||
import os.path
|
||||
import unittest
|
||||
from glob import glob
|
||||
from mock import patch
|
||||
from mock import patch, Mock
|
||||
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
@@ -35,6 +35,7 @@ class TestXMLModuleStore(unittest.TestCase):
|
||||
store = XMLModuleStore(DATA_DIR, source_dirs=[])
|
||||
self.assertEqual(store.get_modulestore_type(), ModuleStoreEnum.Type.xml)
|
||||
|
||||
@patch('xmodule.tabs.CourseTabList.initialize_default', Mock())
|
||||
def test_unicode_chars_in_xml_content(self):
|
||||
# edX/full/6.002_Spring_2012 has non-ASCII chars, and during
|
||||
# uniquification of names, would raise a UnicodeError. It no longer does.
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
"""
|
||||
Implement CourseTab
|
||||
"""
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from abc import ABCMeta
|
||||
import logging
|
||||
|
||||
from xblock.fields import List
|
||||
from openedx.core.lib.api.plugins import PluginError
|
||||
|
||||
# We should only scrape strings for i18n in this file, since the target language is known only when
|
||||
# they are rendered in the template. So ugettext gets called in the template.
|
||||
_ = lambda text: text
|
||||
|
||||
log = logging.getLogger("edx.courseware")
|
||||
|
||||
|
||||
class CourseTab(object):
|
||||
"""
|
||||
@@ -25,6 +31,9 @@ class CourseTab(object):
|
||||
# Class property that specifies whether the tab can be hidden for a particular course
|
||||
is_hideable = False
|
||||
|
||||
# Class property that specifies whether the tab is hidden for a particular course
|
||||
is_hidden = False
|
||||
|
||||
# Class property that specifies whether the tab can be moved within a course's list of tabs
|
||||
is_movable = True
|
||||
|
||||
@@ -51,33 +60,20 @@ class CourseTab(object):
|
||||
|
||||
self.link_func = link_func
|
||||
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled): # pylint: disable=unused-argument
|
||||
def is_enabled(self, course, user=None): # pylint: disable=unused-argument
|
||||
"""
|
||||
Determines whether the tab should be displayed in the UI for the given course and a particular user.
|
||||
This method is to be overridden by subclasses when applicable. The base class implementation
|
||||
always returns True.
|
||||
Determines whether the tab is enabled for the given course and a particular user.
|
||||
This method is to be overridden by subclasses when applicable. The base class
|
||||
implementation always returns True.
|
||||
|
||||
Args:
|
||||
course: An xModule CourseDescriptor
|
||||
|
||||
settings: The configuration settings, including values for:
|
||||
WIKI_ENABLED
|
||||
FEATURES['ENABLE_DISCUSSION_SERVICE']
|
||||
FEATURES['ENABLE_EDXNOTES']
|
||||
FEATURES['ENABLE_STUDENT_NOTES']
|
||||
FEATURES['ENABLE_TEXTBOOK']
|
||||
|
||||
is_user_authenticated: Indicates whether the user is authenticated. If the tab is of
|
||||
type AuthenticatedCourseTab and this value is False, then can_display will return False.
|
||||
|
||||
is_user_staff: Indicates whether the user has staff access to the course. If the tab is of
|
||||
type StaffTab and this value is False, then can_display will return False.
|
||||
|
||||
is_user_enrolled: Indicates whether the user is enrolled in the course
|
||||
user: An optional user for whom the tab will be displayed. If none,
|
||||
then the code should assume a staff user or an author.
|
||||
|
||||
Returns:
|
||||
A boolean value to indicate whether this instance of the tab should be displayed to a
|
||||
given user for the given course.
|
||||
A boolean value to indicate whether this instance of the tab is enabled.
|
||||
"""
|
||||
return True
|
||||
|
||||
@@ -150,6 +146,22 @@ class CourseTab(object):
|
||||
"""
|
||||
return key_checker(['type'])(tab_dict, raise_error)
|
||||
|
||||
@classmethod
|
||||
def load(cls, type_name, **kwargs):
|
||||
"""
|
||||
Constructs a tab of the given type_name.
|
||||
|
||||
Args:
|
||||
type_name (str) - the type of tab that should be constructed
|
||||
**kwargs - any other keyword arguments needed for constructing this tab
|
||||
|
||||
Returns:
|
||||
an instance of the CourseTab subclass that matches the type_name
|
||||
"""
|
||||
json_dict = kwargs.copy()
|
||||
json_dict['type'] = type_name
|
||||
return cls.from_json(json_dict)
|
||||
|
||||
def to_json(self):
|
||||
"""
|
||||
Serializes the necessary members of the CourseTab object to a json-serializable representation.
|
||||
@@ -168,610 +180,33 @@ class CourseTab(object):
|
||||
The subclass that is instantiated is determined by the value of the 'type' key in the
|
||||
given dict-type tab. The given dict-type tab is validated before instantiating the CourseTab object.
|
||||
|
||||
If the tab_type is not recognized, then an exception is logged and None is returned.
|
||||
The intention is that the user should still be able to use the course even if a
|
||||
particular tab is not found for some reason.
|
||||
|
||||
Args:
|
||||
tab: a dictionary with keys for the properties of the tab.
|
||||
|
||||
Raises:
|
||||
InvalidTabsException if the given tab doesn't have the right keys.
|
||||
"""
|
||||
sub_class_types = {
|
||||
'courseware': CoursewareTab,
|
||||
'course_info': CourseInfoTab,
|
||||
'wiki': WikiTab,
|
||||
'discussion': DiscussionTab,
|
||||
'external_discussion': ExternalDiscussionTab,
|
||||
'external_link': ExternalLinkTab,
|
||||
'textbooks': TextbookTabs,
|
||||
'pdf_textbooks': PDFTextbookTabs,
|
||||
'html_textbooks': HtmlTextbookTabs,
|
||||
'progress': ProgressTab,
|
||||
'static_tab': StaticTab,
|
||||
'peer_grading': PeerGradingTab,
|
||||
'staff_grading': StaffGradingTab,
|
||||
'open_ended': OpenEndedGradingTab,
|
||||
'notes': NotesTab,
|
||||
'edxnotes': EdxNotesTab,
|
||||
'syllabus': SyllabusTab,
|
||||
'instructor': InstructorTab, # not persisted
|
||||
'ccx_coach': CcxCoachTab, # not persisted
|
||||
}
|
||||
|
||||
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)
|
||||
# TODO: don't import openedx capabilities from common
|
||||
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager
|
||||
tab_type_name = tab_dict.get('type')
|
||||
if tab_type_name is None:
|
||||
log.error('No type included in tab_dict: %r', tab_dict)
|
||||
return None
|
||||
try:
|
||||
tab_type = CourseViewTypeManager.get_plugin(tab_type_name)
|
||||
except PluginError:
|
||||
log.exception(
|
||||
"Unknown tab type %r Known types: %r.",
|
||||
tab_type_name,
|
||||
CourseViewTypeManager.get_course_view_types()
|
||||
)
|
||||
|
||||
tab_class = sub_class_types[tab_dict['type']]
|
||||
tab_class.validate(tab_dict)
|
||||
return tab_class(tab_dict=tab_dict)
|
||||
|
||||
|
||||
class AuthenticatedCourseTab(CourseTab):
|
||||
"""
|
||||
Abstract class for tabs that can be accessed by only authenticated users.
|
||||
"""
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
|
||||
return is_user_authenticated
|
||||
|
||||
|
||||
class StaffTab(AuthenticatedCourseTab):
|
||||
"""
|
||||
Abstract class for tabs that can be accessed by only users with staff access.
|
||||
"""
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled): # pylint: disable=unused-argument
|
||||
return is_user_staff
|
||||
|
||||
|
||||
class EnrolledOrStaffTab(CourseTab):
|
||||
"""
|
||||
Abstract class for tabs that can be accessed by only users with staff access
|
||||
or users enrolled in the course.
|
||||
"""
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled): # pylint: disable=unused-argument
|
||||
return is_user_authenticated and (is_user_staff or is_user_enrolled)
|
||||
|
||||
|
||||
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(EnrolledOrStaffTab):
|
||||
"""
|
||||
A tab containing the course content.
|
||||
"""
|
||||
|
||||
type = 'courseware'
|
||||
is_movable = False
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
class CourseInfoTab(CourseTab):
|
||||
"""
|
||||
A tab containing information about the course.
|
||||
"""
|
||||
|
||||
type = 'course_info'
|
||||
is_movable = False
|
||||
|
||||
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_dict['name'] if tab_dict else _('Course Info'),
|
||||
tab_id='info',
|
||||
link_func=link_reverse_func('info'),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
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(EnrolledOrStaffTab):
|
||||
"""
|
||||
A tab containing information about the authenticated user's progress.
|
||||
"""
|
||||
|
||||
type = 'progress'
|
||||
|
||||
def __init__(self, tab_dict=None):
|
||||
super(ProgressTab, self).__init__(
|
||||
# Translators: "Progress" is the name of the student's course progress page
|
||||
name=tab_dict['name'] if tab_dict else _('Progress'),
|
||||
tab_id=self.type,
|
||||
link_func=link_reverse_func(self.type),
|
||||
)
|
||||
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
|
||||
super_can_display = super(ProgressTab, self).can_display(
|
||||
course, settings, is_user_authenticated, is_user_staff, is_user_enrolled
|
||||
)
|
||||
return super_can_display and not course.hide_progress_tab
|
||||
|
||||
@classmethod
|
||||
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(HideableTab):
|
||||
"""
|
||||
A tab_dict containing the course wiki.
|
||||
"""
|
||||
|
||||
type = 'wiki'
|
||||
|
||||
def __init__(self, tab_dict=None):
|
||||
super(WikiTab, self).__init__(
|
||||
# Translators: "Wiki" is the name of the course's wiki page
|
||||
name=tab_dict['name'] if tab_dict else _('Wiki'),
|
||||
tab_id=self.type,
|
||||
link_func=link_reverse_func('course_wiki'),
|
||||
tab_dict=tab_dict,
|
||||
)
|
||||
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
|
||||
return settings.WIKI_ENABLED and (
|
||||
course.allow_public_wiki_access or is_user_enrolled or is_user_staff
|
||||
)
|
||||
|
||||
@classmethod
|
||||
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(EnrolledOrStaffTab):
|
||||
"""
|
||||
A tab only for the new Berkeley discussion forums.
|
||||
"""
|
||||
|
||||
type = 'discussion'
|
||||
|
||||
def __init__(self, tab_dict=None):
|
||||
super(DiscussionTab, self).__init__(
|
||||
# Translators: "Discussion" is the title of the course forum page
|
||||
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'),
|
||||
)
|
||||
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
|
||||
if settings.FEATURES.get('CUSTOM_COURSES_EDX', False):
|
||||
from ccx.overrides import get_current_ccx # pylint: disable=import-error
|
||||
if get_current_ccx():
|
||||
return False
|
||||
super_can_display = super(DiscussionTab, self).can_display(
|
||||
course, settings, is_user_authenticated, is_user_staff, is_user_enrolled
|
||||
)
|
||||
return settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE') and super_can_display
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
"""
|
||||
Abstract class for tabs that contain external links.
|
||||
"""
|
||||
link_value = ''
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == 'link':
|
||||
return self.link_value
|
||||
else:
|
||||
return super(LinkTab, self).__getitem__(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key == 'link':
|
||||
self.link_value = value
|
||||
else:
|
||||
super(LinkTab, self).__setitem__(key, value)
|
||||
|
||||
def to_json(self):
|
||||
to_json_val = super(LinkTab, self).to_json()
|
||||
to_json_val.update({'link': self.link_value})
|
||||
return to_json_val
|
||||
|
||||
def __eq__(self, other):
|
||||
if not super(LinkTab, self).__eq__(other):
|
||||
return False
|
||||
return self.link_value == other.get('link')
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
"""
|
||||
A tab that links to an external discussion service.
|
||||
"""
|
||||
|
||||
type = 'external_discussion'
|
||||
|
||||
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_dict['link'] if tab_dict else link_value,
|
||||
)
|
||||
|
||||
|
||||
class ExternalLinkTab(LinkTab):
|
||||
"""
|
||||
A tab containing an external link.
|
||||
"""
|
||||
type = 'external_link'
|
||||
|
||||
def __init__(self, tab_dict):
|
||||
super(ExternalLinkTab, self).__init__(
|
||||
name=tab_dict['name'],
|
||||
tab_id=None, # External links are never active.
|
||||
link_value=tab_dict['link'],
|
||||
)
|
||||
|
||||
|
||||
class StaticTab(CourseTab):
|
||||
"""
|
||||
A custom tab.
|
||||
"""
|
||||
type = 'static_tab'
|
||||
|
||||
@classmethod
|
||||
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_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_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.to_deprecated_string(), self.url_slug]),
|
||||
)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == 'url_slug':
|
||||
return self.url_slug
|
||||
else:
|
||||
return super(StaticTab, self).__getitem__(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key == 'url_slug':
|
||||
self.url_slug = value
|
||||
else:
|
||||
super(StaticTab, self).__setitem__(key, value)
|
||||
|
||||
def to_json(self):
|
||||
to_json_val = super(StaticTab, self).to_json()
|
||||
to_json_val.update({'url_slug': self.url_slug})
|
||||
return to_json_val
|
||||
|
||||
def __eq__(self, other):
|
||||
if not super(StaticTab, self).__eq__(other):
|
||||
return False
|
||||
return self.url_slug == other.get('url_slug')
|
||||
|
||||
|
||||
class SingleTextbookTab(CourseTab):
|
||||
"""
|
||||
A tab representing a single textbook. It is created temporarily when enumerating all textbooks within a
|
||||
Textbook collection tab. It should not be serialized or persisted.
|
||||
"""
|
||||
type = 'single_textbook'
|
||||
is_movable = False
|
||||
is_collection_item = True
|
||||
|
||||
def to_json(self):
|
||||
raise NotImplementedError('SingleTextbookTab should not be serialized.')
|
||||
|
||||
|
||||
class TextbookTabsBase(AuthenticatedCourseTab):
|
||||
"""
|
||||
Abstract class for textbook collection tabs classes.
|
||||
"""
|
||||
is_collection = True
|
||||
|
||||
def __init__(self, tab_id):
|
||||
# Translators: 'Textbooks' refers to the tab in the course that leads to the course' textbooks
|
||||
super(TextbookTabsBase, self).__init__(
|
||||
name=_("Textbooks"),
|
||||
tab_id=tab_id,
|
||||
link_func=None,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def items(self, course):
|
||||
"""
|
||||
A generator for iterating through all the SingleTextbookTab book objects associated with this
|
||||
collection of textbooks.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TextbookTabs(TextbookTabsBase):
|
||||
"""
|
||||
A tab representing the collection of all textbook tabs.
|
||||
"""
|
||||
type = 'textbooks'
|
||||
|
||||
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
|
||||
super(TextbookTabs, self).__init__(
|
||||
tab_id=self.type,
|
||||
)
|
||||
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
|
||||
return settings.FEATURES.get('ENABLE_TEXTBOOK')
|
||||
|
||||
def items(self, course):
|
||||
for index, textbook in enumerate(course.textbooks):
|
||||
yield SingleTextbookTab(
|
||||
name=textbook.title,
|
||||
tab_id='textbook/{0}'.format(index),
|
||||
link_func=lambda course, reverse_func, index=index: reverse_func(
|
||||
'book', args=[course.id.to_deprecated_string(), index]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class PDFTextbookTabs(TextbookTabsBase):
|
||||
"""
|
||||
A tab representing the collection of all PDF textbook tabs.
|
||||
"""
|
||||
type = 'pdf_textbooks'
|
||||
|
||||
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
|
||||
super(PDFTextbookTabs, self).__init__(
|
||||
tab_id=self.type,
|
||||
)
|
||||
|
||||
def items(self, course):
|
||||
for index, textbook in enumerate(course.pdf_textbooks):
|
||||
yield SingleTextbookTab(
|
||||
name=textbook['tab_title'],
|
||||
tab_id='pdftextbook/{0}'.format(index),
|
||||
link_func=lambda course, reverse_func, index=index: reverse_func(
|
||||
'pdf_book', args=[course.id.to_deprecated_string(), index]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class HtmlTextbookTabs(TextbookTabsBase):
|
||||
"""
|
||||
A tab representing the collection of all Html textbook tabs.
|
||||
"""
|
||||
type = 'html_textbooks'
|
||||
|
||||
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
|
||||
super(HtmlTextbookTabs, self).__init__(
|
||||
tab_id=self.type,
|
||||
)
|
||||
|
||||
def items(self, course):
|
||||
for index, textbook in enumerate(course.html_textbooks):
|
||||
yield SingleTextbookTab(
|
||||
name=textbook['tab_title'],
|
||||
tab_id='htmltextbook/{0}'.format(index),
|
||||
link_func=lambda course, reverse_func, index=index: reverse_func(
|
||||
'html_book', args=[course.id.to_deprecated_string(), index]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class GradingTab(object):
|
||||
"""
|
||||
Abstract class for tabs that involve Grading.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class StaffGradingTab(StaffTab, GradingTab):
|
||||
"""
|
||||
A tab for staff grading.
|
||||
"""
|
||||
type = 'staff_grading'
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
class PeerGradingTab(AuthenticatedCourseTab, GradingTab):
|
||||
"""
|
||||
A tab for peer grading.
|
||||
"""
|
||||
type = 'peer_grading'
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
class OpenEndedGradingTab(AuthenticatedCourseTab, GradingTab):
|
||||
"""
|
||||
A tab for open ended grading.
|
||||
"""
|
||||
type = 'open_ended'
|
||||
|
||||
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'),
|
||||
)
|
||||
|
||||
|
||||
class SyllabusTab(CourseTab):
|
||||
"""
|
||||
A tab for the course syllabus.
|
||||
"""
|
||||
type = 'syllabus'
|
||||
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
|
||||
return hasattr(course, 'syllabus_present') and course.syllabus_present
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
class NotesTab(AuthenticatedCourseTab):
|
||||
"""
|
||||
A tab for the course notes.
|
||||
"""
|
||||
type = 'notes'
|
||||
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
|
||||
return settings.FEATURES.get('ENABLE_STUDENT_NOTES')
|
||||
|
||||
def __init__(self, tab_dict=None):
|
||||
super(NotesTab, self).__init__(
|
||||
name=tab_dict['name'],
|
||||
tab_id=self.type,
|
||||
link_func=link_reverse_func(self.type),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
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 EdxNotesTab(AuthenticatedCourseTab):
|
||||
"""
|
||||
A tab for the course student notes.
|
||||
"""
|
||||
type = 'edxnotes'
|
||||
|
||||
def can_display(self, course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
|
||||
return settings.FEATURES.get('ENABLE_EDXNOTES')
|
||||
|
||||
def __init__(self, tab_dict=None):
|
||||
super(EdxNotesTab, self).__init__(
|
||||
name=tab_dict['name'] if tab_dict else _('Notes'),
|
||||
tab_id=self.type,
|
||||
link_func=link_reverse_func(self.type),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, tab_dict, raise_error=True):
|
||||
return super(EdxNotesTab, cls).validate(tab_dict, raise_error) and need_name(tab_dict, raise_error)
|
||||
|
||||
|
||||
class InstructorTab(StaffTab):
|
||||
"""
|
||||
A tab for the course instructors.
|
||||
"""
|
||||
type = 'instructor'
|
||||
|
||||
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'),
|
||||
)
|
||||
|
||||
|
||||
class CcxCoachTab(CourseTab):
|
||||
"""
|
||||
A tab for the custom course coaches.
|
||||
"""
|
||||
type = 'ccx_coach'
|
||||
|
||||
def __init__(self, tab_dict=None): # pylint: disable=unused-argument
|
||||
super(CcxCoachTab, self).__init__(
|
||||
name=_('CCX Coach'),
|
||||
tab_id=self.type,
|
||||
link_func=link_reverse_func('ccx_coach_dashboard'),
|
||||
)
|
||||
|
||||
def can_display(self, course, settings, *args, **kw):
|
||||
"""
|
||||
Since we don't get the user here, we use a thread local defined in the ccx
|
||||
overrides to get it, then use the course to get the coach role and find out if
|
||||
the user is one.
|
||||
"""
|
||||
user_is_coach = False
|
||||
if settings.FEATURES.get('CUSTOM_COURSES_EDX', False) and course.enable_ccx:
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from student.roles import CourseCcxCoachRole # pylint: disable=import-error
|
||||
from ccx.overrides import get_current_request # pylint: disable=import-error
|
||||
course_id = course.id.to_deprecated_string()
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
role = CourseCcxCoachRole(course_key)
|
||||
request = get_current_request()
|
||||
if request is not None:
|
||||
user_is_coach = role.has_user(request.user)
|
||||
super_can_display = super(CcxCoachTab, self).can_display(
|
||||
course, settings, *args, **kw
|
||||
)
|
||||
return user_is_coach and super_can_display
|
||||
return None
|
||||
tab_type.validate(tab_dict)
|
||||
return tab_type.create_tab(tab_dict=tab_dict)
|
||||
|
||||
|
||||
class CourseTabList(List):
|
||||
@@ -780,6 +215,9 @@ class CourseTabList(List):
|
||||
It is automatically created and can be retrieved through a CourseDescriptor object: course.tabs
|
||||
"""
|
||||
|
||||
# TODO: Ideally, we'd like for this list of tabs to be dynamically
|
||||
# generated by the tabs plugin code. For now, we're leaving it like this to
|
||||
# preserve backwards compatibility.
|
||||
@staticmethod
|
||||
def initialize_default(course):
|
||||
"""
|
||||
@@ -789,43 +227,47 @@ class CourseTabList(List):
|
||||
"""
|
||||
|
||||
course.tabs.extend([
|
||||
CoursewareTab(),
|
||||
CourseInfoTab(),
|
||||
CourseTab.load('courseware'),
|
||||
CourseTab.load('course_info')
|
||||
])
|
||||
|
||||
# Presence of syllabus tab is indicated by a course attribute
|
||||
if hasattr(course, 'syllabus_present') and course.syllabus_present:
|
||||
course.tabs.append(SyllabusTab())
|
||||
course.tabs.append(CourseTab.load('syllabus'))
|
||||
|
||||
# If the course has a discussion link specified, use that even if we feature
|
||||
# flag discussions off. Disabling that is mostly a server safety feature
|
||||
# at this point, and we don't need to worry about external sites.
|
||||
if course.discussion_link:
|
||||
discussion_tab = ExternalDiscussionTab(link_value=course.discussion_link)
|
||||
discussion_tab = CourseTab.load(
|
||||
'external_discussion', name=_('External Discussion'), link=course.discussion_link
|
||||
)
|
||||
else:
|
||||
discussion_tab = DiscussionTab()
|
||||
discussion_tab = CourseTab.load('discussion')
|
||||
|
||||
course.tabs.extend([
|
||||
TextbookTabs(),
|
||||
CourseTab.load('textbooks'),
|
||||
discussion_tab,
|
||||
WikiTab(),
|
||||
ProgressTab(),
|
||||
CourseTab.load('wiki'),
|
||||
CourseTab.load('progress'),
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def get_discussion(course):
|
||||
"""
|
||||
Returns the discussion tab for the given course. It can be either of type DiscussionTab
|
||||
or ExternalDiscussionTab. The returned tab object is self-aware of the 'link' that it corresponds to.
|
||||
Returns the discussion tab for the given course. It can be either of type 'discussion'
|
||||
or 'external_discussion'. The returned tab object is self-aware of the 'link' that it corresponds to.
|
||||
"""
|
||||
|
||||
# the discussion_link setting overrides everything else, even if there is a discussion tab in the course tabs
|
||||
if course.discussion_link:
|
||||
return ExternalDiscussionTab(link_value=course.discussion_link)
|
||||
return CourseTab.load(
|
||||
'external_discussion', name=_('External Discussion'), link=course.discussion_link
|
||||
)
|
||||
|
||||
# find one of the discussion tab types in the course tabs
|
||||
for tab in course.tabs:
|
||||
if isinstance(tab, DiscussionTab) or isinstance(tab, ExternalDiscussionTab):
|
||||
if tab.type == 'discussion' or tab.type == 'external_discussion':
|
||||
return tab
|
||||
return None
|
||||
|
||||
@@ -851,48 +293,23 @@ class CourseTabList(List):
|
||||
return next((tab for tab in tab_list if tab.tab_id == tab_id), None)
|
||||
|
||||
@staticmethod
|
||||
def iterate_displayable(
|
||||
course,
|
||||
settings,
|
||||
is_user_authenticated=True,
|
||||
is_user_staff=True,
|
||||
is_user_enrolled=False
|
||||
):
|
||||
def iterate_displayable(course, user=None, inline_collections=True):
|
||||
"""
|
||||
Generator method for iterating through all tabs that can be displayed for the given course and
|
||||
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, is_user_enrolled
|
||||
) and (not tab.is_hideable or not tab.is_hidden):
|
||||
if tab.is_enabled(course, user=user) and not (user and tab.is_hidden):
|
||||
if tab.is_collection:
|
||||
for item in tab.items(course):
|
||||
yield item
|
||||
# If rendering inline that add each item in the collection,
|
||||
# else just show the tab itself as long as it is not empty.
|
||||
if inline_collections:
|
||||
for item in tab.items(course):
|
||||
yield item
|
||||
elif len(list(tab.items(course))) > 0:
|
||||
yield tab
|
||||
else:
|
||||
yield tab
|
||||
instructor_tab = InstructorTab()
|
||||
if instructor_tab.can_display(course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
|
||||
yield instructor_tab
|
||||
ccx_coach_tab = CcxCoachTab()
|
||||
if ccx_coach_tab.can_display(course, settings, is_user_authenticated, is_user_staff, is_user_enrolled):
|
||||
yield ccx_coach_tab
|
||||
|
||||
@staticmethod
|
||||
def iterate_displayable_cms(
|
||||
course,
|
||||
settings
|
||||
):
|
||||
"""
|
||||
Generator method for iterating through all tabs that can be displayed for the given course
|
||||
with the provided settings.
|
||||
"""
|
||||
for tab in course.tabs:
|
||||
if tab.can_display(course, settings, is_user_authenticated=True, is_user_staff=True, is_user_enrolled=True):
|
||||
if tab.is_collection and not len(list(tab.items(course))):
|
||||
# do not yield collections that have no items
|
||||
continue
|
||||
yield tab
|
||||
|
||||
@classmethod
|
||||
def validate_tabs(cls, tabs):
|
||||
@@ -911,24 +328,20 @@ class CourseTabList(List):
|
||||
if len(tabs) < 2:
|
||||
raise InvalidTabsException("Expected at least two tabs. tabs: '{0}'".format(tabs))
|
||||
|
||||
if tabs[0].get('type') != CoursewareTab.type:
|
||||
if tabs[0].get('type') != 'courseware':
|
||||
raise InvalidTabsException(
|
||||
"Expected first tab to have type 'courseware'. tabs: '{0}'".format(tabs))
|
||||
|
||||
if tabs[1].get('type') != CourseInfoTab.type:
|
||||
if tabs[1].get('type') != 'course_info':
|
||||
raise InvalidTabsException(
|
||||
"Expected second tab to have type 'course_info'. tabs: '{0}'".format(tabs))
|
||||
|
||||
# the following tabs should appear only once
|
||||
for tab_type in [
|
||||
CoursewareTab.type,
|
||||
CourseInfoTab.type,
|
||||
NotesTab.type,
|
||||
TextbookTabs.type,
|
||||
PDFTextbookTabs.type,
|
||||
HtmlTextbookTabs.type,
|
||||
EdxNotesTab.type]:
|
||||
cls._validate_num_tabs_of_type(tabs, tab_type, 1)
|
||||
# TODO: don't import openedx capabilities from common
|
||||
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager
|
||||
for course_view_type in CourseViewTypeManager.get_course_view_types():
|
||||
if not course_view_type.allow_multiple:
|
||||
cls._validate_num_tabs_of_type(tabs, course_view_type.name, 1)
|
||||
|
||||
@staticmethod
|
||||
def _validate_num_tabs_of_type(tabs, tab_type, max_num):
|
||||
@@ -965,26 +378,15 @@ 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_dict) for tab_dict in values]
|
||||
tabs = []
|
||||
for tab_dict in values:
|
||||
tab = CourseTab.from_json(tab_dict)
|
||||
if tab:
|
||||
tabs.append(tab)
|
||||
return tabs
|
||||
|
||||
|
||||
#### Link Functions
|
||||
def link_reverse_func(reverse_name):
|
||||
"""
|
||||
Returns a function that takes in a course and reverse_url_func,
|
||||
and calls the reverse_url_func with the given reverse_name and course' ID.
|
||||
"""
|
||||
return lambda course, reverse_url_func: reverse_url_func(reverse_name, args=[course.id.to_deprecated_string()])
|
||||
|
||||
|
||||
def link_value_func(value):
|
||||
"""
|
||||
Returns a function takes in a course and reverse_url_func, and returns the given value.
|
||||
"""
|
||||
return lambda course, reverse_url_func: value
|
||||
|
||||
|
||||
#### Validators
|
||||
# Validators
|
||||
# A validator takes a dict and raises InvalidTabsException if required fields are missing or otherwise wrong.
|
||||
# (e.g. "is there a 'name' field?). Validators can assume that the type field is valid.
|
||||
def key_checker(expected_keys):
|
||||
|
||||
@@ -1,769 +0,0 @@
|
||||
"""Tests for Tab classes"""
|
||||
from mock import MagicMock
|
||||
import xmodule.tabs as tabs
|
||||
import unittest
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class TabTestCase(unittest.TestCase):
|
||||
"""Base class for Tab-related test cases."""
|
||||
def setUp(self):
|
||||
super(TabTestCase, self).setUp()
|
||||
|
||||
self.course = MagicMock()
|
||||
self.course.id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
self.fake_dict_tab = {'fake_key': 'fake_value'}
|
||||
self.settings = MagicMock()
|
||||
self.settings.FEATURES = {}
|
||||
self.reverse = lambda name, args: "name/{0}/args/{1}".format(name, ",".join(str(a) for a in args))
|
||||
self.books = None
|
||||
|
||||
def set_up_books(self, num_books):
|
||||
"""Initializes the textbooks in the course and adds the given number of books to each textbook"""
|
||||
self.books = [MagicMock() for _ in range(num_books)]
|
||||
for book_index, book in enumerate(self.books):
|
||||
book.title = 'Book{0}'.format(book_index)
|
||||
self.course.textbooks = self.books
|
||||
self.course.pdf_textbooks = self.books
|
||||
self.course.html_textbooks = self.books
|
||||
|
||||
def check_tab(
|
||||
self,
|
||||
tab_class,
|
||||
dict_tab,
|
||||
expected_link,
|
||||
expected_tab_id,
|
||||
expected_name='same',
|
||||
invalid_dict_tab=None,
|
||||
):
|
||||
"""
|
||||
Helper method to verify a tab class.
|
||||
|
||||
'tab_class' is the class of the tab that is being tested
|
||||
'dict_tab' is the raw dictionary value of the tab
|
||||
'expected_link' is the expected value for the hyperlink of the tab
|
||||
'expected_tab_id' is the expected value for the unique id of the tab
|
||||
'expected_name' is the expected value for the name of the tab
|
||||
'invalid_dict_tab' is an invalid dictionary value for the tab.
|
||||
Can be 'None' if the given tab class does not have any keys to validate.
|
||||
"""
|
||||
# create tab
|
||||
tab = tab_class(dict_tab)
|
||||
|
||||
# name is as expected
|
||||
self.assertEqual(tab.name, expected_name)
|
||||
|
||||
# link is as expected
|
||||
self.assertEqual(tab.link_func(self.course, self.reverse), expected_link)
|
||||
|
||||
# verify active page name
|
||||
self.assertEqual(tab.tab_id, expected_tab_id)
|
||||
|
||||
# validate tab
|
||||
self.assertTrue(tab.validate(dict_tab))
|
||||
if invalid_dict_tab:
|
||||
with self.assertRaises(tabs.InvalidTabsException):
|
||||
tab.validate(invalid_dict_tab)
|
||||
|
||||
# check get and set methods
|
||||
self.check_get_and_set_methods(tab)
|
||||
|
||||
# check to_json and from_json methods
|
||||
self.check_tab_json_methods(tab)
|
||||
|
||||
# check equality methods
|
||||
self.check_tab_equality(tab, dict_tab)
|
||||
|
||||
# return tab for any additional tests
|
||||
return tab
|
||||
|
||||
def check_tab_equality(self, tab, dict_tab):
|
||||
"""Tests the equality methods on the given tab"""
|
||||
self.assertEquals(tab, dict_tab) # test __eq__
|
||||
ne_dict_tab = dict_tab
|
||||
ne_dict_tab['type'] = 'fake_type'
|
||||
self.assertNotEquals(tab, ne_dict_tab) # test __ne__: incorrect type
|
||||
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"""
|
||||
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,
|
||||
for_enrolled_users_only=False
|
||||
):
|
||||
"""Checks can display results for various users"""
|
||||
if for_staff_only:
|
||||
self.assertEquals(
|
||||
expected_value,
|
||||
tab.can_display(
|
||||
self.course, self.settings, is_user_authenticated=True, is_user_staff=True, is_user_enrolled=True
|
||||
)
|
||||
)
|
||||
if for_authenticated_users_only:
|
||||
self.assertEquals(
|
||||
expected_value,
|
||||
tab.can_display(
|
||||
self.course, self.settings, is_user_authenticated=True, is_user_staff=False, is_user_enrolled=False
|
||||
)
|
||||
)
|
||||
if not for_staff_only and not for_authenticated_users_only and not for_enrolled_users_only:
|
||||
self.assertEquals(
|
||||
expected_value,
|
||||
tab.can_display(
|
||||
self.course, self.settings, is_user_authenticated=False, is_user_staff=False, is_user_enrolled=False
|
||||
)
|
||||
)
|
||||
if for_enrolled_users_only:
|
||||
self.assertEquals(
|
||||
expected_value,
|
||||
tab.can_display(
|
||||
self.course, self.settings, is_user_authenticated=True, is_user_staff=False, is_user_enrolled=True
|
||||
)
|
||||
)
|
||||
|
||||
def check_get_and_set_methods(self, tab):
|
||||
"""Test __getitem__ and __setitem__ calls"""
|
||||
self.assertEquals(tab['type'], tab.type)
|
||||
self.assertEquals(tab['tab_id'], tab.tab_id)
|
||||
with self.assertRaises(KeyError):
|
||||
_ = tab['invalid_key']
|
||||
|
||||
self.check_get_and_set_method_for_key(tab, 'name')
|
||||
self.check_get_and_set_method_for_key(tab, 'tab_id')
|
||||
with self.assertRaises(KeyError):
|
||||
tab['invalid_key'] = 'New Value'
|
||||
|
||||
def check_get_and_set_method_for_key(self, tab, key):
|
||||
"""Test __getitem__ and __setitem__ for the given key"""
|
||||
old_value = tab[key]
|
||||
new_value = 'New Value'
|
||||
tab[key] = new_value
|
||||
self.assertEquals(tab[key], new_value)
|
||||
tab[key] = old_value
|
||||
self.assertEquals(tab[key], old_value)
|
||||
|
||||
|
||||
class ProgressTestCase(TabTestCase):
|
||||
"""Test cases for Progress Tab."""
|
||||
|
||||
def check_progress_tab(self):
|
||||
"""Helper function for verifying the progress tab."""
|
||||
return self.check_tab(
|
||||
tab_class=tabs.ProgressTab,
|
||||
dict_tab={'type': tabs.ProgressTab.type, 'name': 'same'},
|
||||
expected_link=self.reverse('progress', args=[self.course.id.to_deprecated_string()]),
|
||||
expected_tab_id=tabs.ProgressTab.type,
|
||||
invalid_dict_tab=None,
|
||||
)
|
||||
|
||||
def test_progress(self):
|
||||
|
||||
self.course.hide_progress_tab = False
|
||||
tab = self.check_progress_tab()
|
||||
self.check_can_display_results(
|
||||
tab, for_staff_only=True, for_enrolled_users_only=True
|
||||
)
|
||||
|
||||
self.course.hide_progress_tab = True
|
||||
self.check_progress_tab()
|
||||
self.check_can_display_results(
|
||||
tab, for_staff_only=True, for_enrolled_users_only=True, expected_value=False
|
||||
)
|
||||
|
||||
|
||||
class WikiTestCase(TabTestCase):
|
||||
"""Test cases for Wiki Tab."""
|
||||
|
||||
def check_wiki_tab(self):
|
||||
"""Helper function for verifying the wiki tab."""
|
||||
return self.check_tab(
|
||||
tab_class=tabs.WikiTab,
|
||||
dict_tab={'type': tabs.WikiTab.type, 'name': 'same'},
|
||||
expected_link=self.reverse('course_wiki', args=[self.course.id.to_deprecated_string()]),
|
||||
expected_tab_id=tabs.WikiTab.type,
|
||||
invalid_dict_tab=self.fake_dict_tab,
|
||||
)
|
||||
|
||||
def test_wiki_enabled_and_public(self):
|
||||
"""
|
||||
Test wiki tab when Enabled setting is True and the wiki is open to
|
||||
the public.
|
||||
"""
|
||||
self.settings.WIKI_ENABLED = True
|
||||
self.course.allow_public_wiki_access = True
|
||||
tab = self.check_wiki_tab()
|
||||
self.check_can_display_results(tab)
|
||||
|
||||
def test_wiki_enabled_and_not_public(self):
|
||||
"""
|
||||
Test wiki when it is enabled but not open to the public
|
||||
"""
|
||||
self.settings.WIKI_ENABLED = True
|
||||
self.course.allow_public_wiki_access = False
|
||||
tab = self.check_wiki_tab()
|
||||
self.check_can_display_results(tab, for_enrolled_users_only=True, for_staff_only=True)
|
||||
|
||||
def test_wiki_enabled_false(self):
|
||||
"""Test wiki tab when Enabled setting is False"""
|
||||
|
||||
self.settings.WIKI_ENABLED = False
|
||||
tab = self.check_wiki_tab()
|
||||
self.check_can_display_results(tab, expected_value=False)
|
||||
|
||||
def test_wiki_visibility(self):
|
||||
"""Test toggling of visibility of wiki tab"""
|
||||
|
||||
wiki_tab = tabs.WikiTab()
|
||||
self.assertTrue(wiki_tab.is_hideable)
|
||||
wiki_tab.is_hidden = True
|
||||
self.assertTrue(wiki_tab['is_hidden'])
|
||||
self.check_tab_json_methods(wiki_tab)
|
||||
self.check_tab_equality(wiki_tab, wiki_tab.to_json())
|
||||
wiki_tab['is_hidden'] = False
|
||||
self.assertFalse(wiki_tab.is_hidden)
|
||||
|
||||
|
||||
class ExternalLinkTestCase(TabTestCase):
|
||||
"""Test cases for External Link Tab."""
|
||||
|
||||
def test_external_link(self):
|
||||
|
||||
link_value = 'link_value'
|
||||
tab = self.check_tab(
|
||||
tab_class=tabs.ExternalLinkTab,
|
||||
dict_tab={'type': tabs.ExternalLinkTab.type, 'name': 'same', 'link': link_value},
|
||||
expected_link=link_value,
|
||||
expected_tab_id=None,
|
||||
invalid_dict_tab=self.fake_dict_tab,
|
||||
)
|
||||
self.check_can_display_results(tab)
|
||||
self.check_get_and_set_method_for_key(tab, 'link')
|
||||
|
||||
|
||||
class StaticTabTestCase(TabTestCase):
|
||||
"""Test cases for Static Tab."""
|
||||
|
||||
def test_static_tab(self):
|
||||
|
||||
url_slug = 'schmug'
|
||||
|
||||
tab = self.check_tab(
|
||||
tab_class=tabs.StaticTab,
|
||||
dict_tab={'type': tabs.StaticTab.type, 'name': 'same', 'url_slug': url_slug},
|
||||
expected_link=self.reverse('static_tab', args=[self.course.id.to_deprecated_string(), url_slug]),
|
||||
expected_tab_id='static_tab_schmug',
|
||||
invalid_dict_tab=self.fake_dict_tab,
|
||||
)
|
||||
self.check_can_display_results(tab)
|
||||
self.check_get_and_set_method_for_key(tab, 'url_slug')
|
||||
|
||||
|
||||
class TextbooksTestCase(TabTestCase):
|
||||
"""Test cases for Textbook Tab."""
|
||||
|
||||
def setUp(self):
|
||||
super(TextbooksTestCase, self).setUp()
|
||||
|
||||
self.set_up_books(2)
|
||||
|
||||
self.dict_tab = MagicMock()
|
||||
self.course.tabs = [
|
||||
tabs.CoursewareTab(),
|
||||
tabs.CourseInfoTab(),
|
||||
tabs.TextbookTabs(),
|
||||
tabs.PDFTextbookTabs(),
|
||||
tabs.HtmlTextbookTabs(),
|
||||
]
|
||||
self.num_textbook_tabs = sum(1 for tab in self.course.tabs if isinstance(tab, tabs.TextbookTabsBase))
|
||||
self.num_textbooks = self.num_textbook_tabs * len(self.books)
|
||||
|
||||
def test_textbooks_enabled(self):
|
||||
|
||||
type_to_reverse_name = {'textbook': 'book', 'pdftextbook': 'pdf_book', 'htmltextbook': 'html_book'}
|
||||
|
||||
self.settings.FEATURES['ENABLE_TEXTBOOK'] = True
|
||||
num_textbooks_found = 0
|
||||
for tab in tabs.CourseTabList.iterate_displayable(self.course, self.settings):
|
||||
# verify all textbook type tabs
|
||||
if isinstance(tab, tabs.SingleTextbookTab):
|
||||
book_type, book_index = tab.tab_id.split("/", 1)
|
||||
expected_link = self.reverse(
|
||||
type_to_reverse_name[book_type],
|
||||
args=[self.course.id.to_deprecated_string(), book_index]
|
||||
)
|
||||
self.assertEqual(tab.link_func(self.course, self.reverse), expected_link)
|
||||
self.assertTrue(tab.name.startswith('Book{0}'.format(book_index)))
|
||||
num_textbooks_found = num_textbooks_found + 1
|
||||
self.assertEquals(num_textbooks_found, self.num_textbooks)
|
||||
|
||||
def test_textbooks_disabled(self):
|
||||
|
||||
self.settings.FEATURES['ENABLE_TEXTBOOK'] = False
|
||||
tab = tabs.TextbookTabs(self.dict_tab)
|
||||
self.check_can_display_results(tab, for_authenticated_users_only=True, expected_value=False)
|
||||
|
||||
|
||||
class GradingTestCase(TabTestCase):
|
||||
"""Test cases for Grading related Tabs."""
|
||||
|
||||
def check_grading_tab(self, tab_class, name, link_value):
|
||||
"""Helper function for verifying the grading tab."""
|
||||
return self.check_tab(
|
||||
tab_class=tab_class,
|
||||
dict_tab={'type': tab_class.type, 'name': name},
|
||||
expected_name=name,
|
||||
expected_link=self.reverse(link_value, args=[self.course.id.to_deprecated_string()]),
|
||||
expected_tab_id=tab_class.type,
|
||||
invalid_dict_tab=None,
|
||||
)
|
||||
|
||||
def test_grading_tabs(self):
|
||||
|
||||
peer_grading_tab = self.check_grading_tab(
|
||||
tabs.PeerGradingTab,
|
||||
'Peer grading',
|
||||
'peer_grading'
|
||||
)
|
||||
self.check_can_display_results(peer_grading_tab, for_authenticated_users_only=True)
|
||||
open_ended_grading_tab = self.check_grading_tab(
|
||||
tabs.OpenEndedGradingTab,
|
||||
'Open Ended Panel',
|
||||
'open_ended_notifications'
|
||||
)
|
||||
self.check_can_display_results(open_ended_grading_tab, for_authenticated_users_only=True)
|
||||
staff_grading_tab = self.check_grading_tab(
|
||||
tabs.StaffGradingTab,
|
||||
'Staff grading',
|
||||
'staff_grading'
|
||||
)
|
||||
self.check_can_display_results(staff_grading_tab, for_staff_only=True)
|
||||
|
||||
|
||||
class NotesTestCase(TabTestCase):
|
||||
"""Test cases for Notes Tab."""
|
||||
|
||||
def check_notes_tab(self):
|
||||
"""Helper function for verifying the notes tab."""
|
||||
return self.check_tab(
|
||||
tab_class=tabs.NotesTab,
|
||||
dict_tab={'type': tabs.NotesTab.type, 'name': 'same'},
|
||||
expected_link=self.reverse('notes', args=[self.course.id.to_deprecated_string()]),
|
||||
expected_tab_id=tabs.NotesTab.type,
|
||||
invalid_dict_tab=self.fake_dict_tab,
|
||||
)
|
||||
|
||||
def test_notes_tabs_enabled(self):
|
||||
self.settings.FEATURES['ENABLE_STUDENT_NOTES'] = True
|
||||
tab = self.check_notes_tab()
|
||||
self.check_can_display_results(tab, for_authenticated_users_only=True)
|
||||
|
||||
def test_notes_tabs_disabled(self):
|
||||
self.settings.FEATURES['ENABLE_STUDENT_NOTES'] = False
|
||||
tab = self.check_notes_tab()
|
||||
self.check_can_display_results(tab, expected_value=False)
|
||||
|
||||
|
||||
class SyllabusTestCase(TabTestCase):
|
||||
"""Test cases for Syllabus Tab."""
|
||||
|
||||
def check_syllabus_tab(self, expected_can_display_value):
|
||||
"""Helper function for verifying the syllabus tab."""
|
||||
|
||||
name = 'Syllabus'
|
||||
tab = self.check_tab(
|
||||
tab_class=tabs.SyllabusTab,
|
||||
dict_tab={'type': tabs.SyllabusTab.type, 'name': name},
|
||||
expected_name=name,
|
||||
expected_link=self.reverse('syllabus', args=[self.course.id.to_deprecated_string()]),
|
||||
expected_tab_id=tabs.SyllabusTab.type,
|
||||
invalid_dict_tab=None,
|
||||
)
|
||||
self.check_can_display_results(tab, expected_value=expected_can_display_value)
|
||||
|
||||
def test_syllabus_tab_enabled(self):
|
||||
self.course.syllabus_present = True
|
||||
self.check_syllabus_tab(True)
|
||||
|
||||
def test_syllabus_tab_disabled(self):
|
||||
self.course.syllabus_present = False
|
||||
self.check_syllabus_tab(False)
|
||||
|
||||
|
||||
class InstructorTestCase(TabTestCase):
|
||||
"""Test cases for Instructor Tab."""
|
||||
|
||||
def test_instructor_tab(self):
|
||||
name = 'Instructor'
|
||||
tab = self.check_tab(
|
||||
tab_class=tabs.InstructorTab,
|
||||
dict_tab={'type': tabs.InstructorTab.type, 'name': name},
|
||||
expected_name=name,
|
||||
expected_link=self.reverse('instructor_dashboard', args=[self.course.id.to_deprecated_string()]),
|
||||
expected_tab_id=tabs.InstructorTab.type,
|
||||
invalid_dict_tab=None,
|
||||
)
|
||||
self.check_can_display_results(tab, for_staff_only=True)
|
||||
|
||||
|
||||
class EdxNotesTestCase(TabTestCase):
|
||||
"""
|
||||
Test cases for Notes Tab.
|
||||
"""
|
||||
|
||||
def check_edxnotes_tab(self):
|
||||
"""
|
||||
Helper function for verifying the edxnotes tab.
|
||||
"""
|
||||
return self.check_tab(
|
||||
tab_class=tabs.EdxNotesTab,
|
||||
dict_tab={'type': tabs.EdxNotesTab.type, 'name': 'same'},
|
||||
expected_link=self.reverse('edxnotes', args=[self.course.id.to_deprecated_string()]),
|
||||
expected_tab_id=tabs.EdxNotesTab.type,
|
||||
invalid_dict_tab=self.fake_dict_tab,
|
||||
)
|
||||
|
||||
def test_edxnotes_tabs_enabled(self):
|
||||
"""
|
||||
Tests that edxnotes tab is shown when feature is enabled.
|
||||
"""
|
||||
self.settings.FEATURES['ENABLE_EDXNOTES'] = True
|
||||
tab = self.check_edxnotes_tab()
|
||||
self.check_can_display_results(tab, for_authenticated_users_only=True)
|
||||
|
||||
def test_edxnotes_tabs_disabled(self):
|
||||
"""
|
||||
Tests that edxnotes tab is not shown when feature is disabled.
|
||||
"""
|
||||
self.settings.FEATURES['ENABLE_EDXNOTES'] = False
|
||||
tab = self.check_edxnotes_tab()
|
||||
self.check_can_display_results(tab, expected_value=False)
|
||||
|
||||
|
||||
class KeyCheckerTestCase(unittest.TestCase):
|
||||
"""Test cases for KeyChecker class"""
|
||||
|
||||
def setUp(self):
|
||||
super(KeyCheckerTestCase, self).setUp()
|
||||
|
||||
self.valid_keys = ['a', 'b']
|
||||
self.invalid_keys = ['a', 'v', 'g']
|
||||
self.dict_value = {'a': 1, 'b': 2, 'c': 3}
|
||||
|
||||
def test_key_checker(self):
|
||||
|
||||
self.assertTrue(tabs.key_checker(self.valid_keys)(self.dict_value, raise_error=False))
|
||||
self.assertFalse(tabs.key_checker(self.invalid_keys)(self.dict_value, raise_error=False))
|
||||
with self.assertRaises(tabs.InvalidTabsException):
|
||||
tabs.key_checker(self.invalid_keys)(self.dict_value)
|
||||
|
||||
|
||||
class NeedNameTestCase(unittest.TestCase):
|
||||
"""Test cases for NeedName validator"""
|
||||
|
||||
def setUp(self):
|
||||
super(NeedNameTestCase, self).setUp()
|
||||
|
||||
self.valid_dict1 = {'a': 1, 'name': 2}
|
||||
self.valid_dict2 = {'name': 1}
|
||||
self.valid_dict3 = {'a': 1, 'name': 2, 'b': 3}
|
||||
self.invalid_dict = {'a': 1, 'b': 2}
|
||||
|
||||
def test_need_name(self):
|
||||
self.assertTrue(tabs.need_name(self.valid_dict1))
|
||||
self.assertTrue(tabs.need_name(self.valid_dict2))
|
||||
self.assertTrue(tabs.need_name(self.valid_dict3))
|
||||
with self.assertRaises(tabs.InvalidTabsException):
|
||||
tabs.need_name(self.invalid_dict)
|
||||
|
||||
|
||||
class TabListTestCase(TabTestCase):
|
||||
"""Base class for Test cases involving tab lists."""
|
||||
|
||||
def setUp(self):
|
||||
super(TabListTestCase, self).setUp()
|
||||
|
||||
# invalid tabs
|
||||
self.invalid_tabs = [
|
||||
# less than 2 tabs
|
||||
[{'type': tabs.CoursewareTab.type}],
|
||||
# missing course_info
|
||||
[{'type': tabs.CoursewareTab.type}, {'type': tabs.DiscussionTab.type, 'name': 'fake_name'}],
|
||||
# incorrect order
|
||||
[{'type': tabs.CourseInfoTab.type, 'name': 'fake_name'}, {'type': tabs.CoursewareTab.type}],
|
||||
# invalid type
|
||||
[{'type': tabs.CoursewareTab.type}, {'type': tabs.CourseInfoTab.type, 'name': 'fake_name'}, {'type': 'fake_type'}],
|
||||
]
|
||||
|
||||
# tab types that should appear only once
|
||||
unique_tab_types = [
|
||||
tabs.CourseInfoTab.type,
|
||||
tabs.CoursewareTab.type,
|
||||
tabs.NotesTab.type,
|
||||
tabs.TextbookTabs.type,
|
||||
tabs.PDFTextbookTabs.type,
|
||||
tabs.HtmlTextbookTabs.type,
|
||||
tabs.EdxNotesTab.type,
|
||||
]
|
||||
|
||||
for unique_tab_type in unique_tab_types:
|
||||
self.invalid_tabs.append([
|
||||
{'type': tabs.CoursewareTab.type},
|
||||
{'type': tabs.CourseInfoTab.type, 'name': 'fake_name'},
|
||||
# add the unique tab multiple times
|
||||
{'type': unique_tab_type},
|
||||
{'type': unique_tab_type},
|
||||
])
|
||||
|
||||
# valid tabs
|
||||
self.valid_tabs = [
|
||||
# empty list
|
||||
[],
|
||||
# all valid tabs
|
||||
[
|
||||
{'type': tabs.CoursewareTab.type},
|
||||
{'type': tabs.CourseInfoTab.type, 'name': 'fake_name'},
|
||||
{'type': tabs.WikiTab.type, 'name': 'fake_name'},
|
||||
{'type': tabs.DiscussionTab.type, 'name': 'fake_name'},
|
||||
{'type': tabs.ExternalLinkTab.type, 'name': 'fake_name', 'link': 'fake_link'},
|
||||
{'type': tabs.TextbookTabs.type},
|
||||
{'type': tabs.PDFTextbookTabs.type},
|
||||
{'type': tabs.HtmlTextbookTabs.type},
|
||||
{'type': tabs.ProgressTab.type, 'name': 'fake_name'},
|
||||
{'type': tabs.StaticTab.type, 'name': 'fake_name', 'url_slug': 'schlug'},
|
||||
{'type': tabs.PeerGradingTab.type},
|
||||
{'type': tabs.StaffGradingTab.type},
|
||||
{'type': tabs.OpenEndedGradingTab.type},
|
||||
{'type': tabs.NotesTab.type, 'name': 'fake_name'},
|
||||
{'type': tabs.SyllabusTab.type},
|
||||
{'type': tabs.EdxNotesTab.type, 'name': 'fake_name'},
|
||||
],
|
||||
# with external discussion
|
||||
[
|
||||
{'type': tabs.CoursewareTab.type},
|
||||
{'type': tabs.CourseInfoTab.type, 'name': 'fake_name'},
|
||||
{'type': tabs.ExternalDiscussionTab.type, 'name': 'fake_name', 'link': 'fake_link'}
|
||||
],
|
||||
]
|
||||
|
||||
self.all_valid_tab_list = tabs.CourseTabList().from_json(self.valid_tabs[1])
|
||||
|
||||
|
||||
class ValidateTabsTestCase(TabListTestCase):
|
||||
"""Test cases for validating tabs."""
|
||||
|
||||
def test_validate_tabs(self):
|
||||
tab_list = tabs.CourseTabList()
|
||||
for invalid_tab_list in self.invalid_tabs:
|
||||
with self.assertRaises(tabs.InvalidTabsException):
|
||||
tab_list.from_json(invalid_tab_list)
|
||||
|
||||
for valid_tab_list in self.valid_tabs:
|
||||
from_json_result = tab_list.from_json(valid_tab_list)
|
||||
self.assertEquals(len(from_json_result), len(valid_tab_list))
|
||||
|
||||
|
||||
class CourseTabListTestCase(TabListTestCase):
|
||||
"""Testing the generator method for iterating through displayable tabs"""
|
||||
|
||||
def test_initialize_default_without_syllabus(self):
|
||||
self.course.tabs = []
|
||||
self.course.syllabus_present = False
|
||||
tabs.CourseTabList.initialize_default(self.course)
|
||||
self.assertTrue(tabs.SyllabusTab() not in self.course.tabs)
|
||||
|
||||
def test_initialize_default_with_syllabus(self):
|
||||
self.course.tabs = []
|
||||
self.course.syllabus_present = True
|
||||
tabs.CourseTabList.initialize_default(self.course)
|
||||
self.assertTrue(tabs.SyllabusTab() in self.course.tabs)
|
||||
|
||||
def test_initialize_default_with_external_link(self):
|
||||
self.course.tabs = []
|
||||
self.course.discussion_link = "other_discussion_link"
|
||||
tabs.CourseTabList.initialize_default(self.course)
|
||||
self.assertTrue(tabs.ExternalDiscussionTab(link_value="other_discussion_link") in self.course.tabs)
|
||||
self.assertTrue(tabs.DiscussionTab() not in self.course.tabs)
|
||||
|
||||
def test_initialize_default_without_external_link(self):
|
||||
self.course.tabs = []
|
||||
self.course.discussion_link = ""
|
||||
tabs.CourseTabList.initialize_default(self.course)
|
||||
self.assertTrue(tabs.ExternalDiscussionTab() not in self.course.tabs)
|
||||
self.assertTrue(tabs.DiscussionTab() in self.course.tabs)
|
||||
|
||||
def test_iterate_displayable(self):
|
||||
# enable all tab types
|
||||
self.settings.FEATURES['ENABLE_TEXTBOOK'] = True
|
||||
self.settings.FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
|
||||
self.settings.FEATURES['ENABLE_STUDENT_NOTES'] = True
|
||||
self.settings.FEATURES['ENABLE_EDXNOTES'] = True
|
||||
self.course.hide_progress_tab = False
|
||||
|
||||
# create 1 book per textbook type
|
||||
self.set_up_books(1)
|
||||
|
||||
# initialize the course tabs to a list of all valid tabs
|
||||
self.course.tabs = self.all_valid_tab_list
|
||||
|
||||
# enumerate the tabs using the CMS call
|
||||
for i, tab in enumerate(tabs.CourseTabList.iterate_displayable_cms(
|
||||
self.course,
|
||||
self.settings,
|
||||
)):
|
||||
self.assertEquals(tab.type, self.course.tabs[i].type)
|
||||
|
||||
# enumerate the tabs and verify textbooks and the instructor tab
|
||||
for i, tab in enumerate(tabs.CourseTabList.iterate_displayable(
|
||||
self.course,
|
||||
self.settings,
|
||||
)):
|
||||
if getattr(tab, 'is_collection_item', False):
|
||||
# a collection item was found as a result of a collection tab
|
||||
self.assertTrue(getattr(self.course.tabs[i], 'is_collection', False))
|
||||
elif i == len(self.course.tabs):
|
||||
# the last tab must be the Instructor tab
|
||||
self.assertEquals(tab.type, tabs.InstructorTab.type)
|
||||
else:
|
||||
# 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"""
|
||||
self.course.tabs = self.all_valid_tab_list
|
||||
for tab in self.course.tabs:
|
||||
|
||||
# get tab by type
|
||||
self.assertEquals(tabs.CourseTabList.get_tab_by_type(self.course.tabs, tab.type), tab)
|
||||
|
||||
# get tab by id
|
||||
self.assertEquals(tabs.CourseTabList.get_tab_by_id(self.course.tabs, tab.tab_id), tab)
|
||||
|
||||
|
||||
class DiscussionLinkTestCase(TabTestCase):
|
||||
"""Test cases for discussion link tab."""
|
||||
|
||||
def setUp(self):
|
||||
super(DiscussionLinkTestCase, self).setUp()
|
||||
|
||||
self.tabs_with_discussion = [
|
||||
tabs.CoursewareTab(),
|
||||
tabs.CourseInfoTab(),
|
||||
tabs.DiscussionTab(),
|
||||
tabs.TextbookTabs(),
|
||||
]
|
||||
self.tabs_without_discussion = [
|
||||
tabs.CoursewareTab(),
|
||||
tabs.CourseInfoTab(),
|
||||
tabs.TextbookTabs(),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _reverse(course):
|
||||
"""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.to_deprecated_string()]:
|
||||
return "default_discussion_link"
|
||||
return reverse_discussion_link
|
||||
|
||||
def check_discussion(
|
||||
self, tab_list,
|
||||
expected_discussion_link,
|
||||
expected_can_display_value,
|
||||
discussion_link_in_course="",
|
||||
is_staff=True,
|
||||
is_enrolled=True,
|
||||
):
|
||||
"""Helper function to verify whether the discussion tab exists and can be displayed"""
|
||||
self.course.tabs = tab_list
|
||||
self.course.discussion_link = discussion_link_in_course
|
||||
discussion = tabs.CourseTabList.get_discussion(self.course)
|
||||
self.assertEquals(
|
||||
(
|
||||
discussion is not None and
|
||||
discussion.can_display(self.course, self.settings, True, is_staff, is_enrolled) and
|
||||
(discussion.link_func(self.course, self._reverse(self.course)) == expected_discussion_link)
|
||||
),
|
||||
expected_can_display_value
|
||||
)
|
||||
|
||||
def test_explicit_discussion_link(self):
|
||||
"""Test that setting discussion_link overrides everything else"""
|
||||
self.settings.FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
|
||||
self.check_discussion(
|
||||
tab_list=self.tabs_with_discussion,
|
||||
discussion_link_in_course="other_discussion_link",
|
||||
expected_discussion_link="other_discussion_link",
|
||||
expected_can_display_value=True,
|
||||
)
|
||||
|
||||
def test_discussions_disabled(self):
|
||||
"""Test that other cases return None with discussions disabled"""
|
||||
self.settings.FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
|
||||
for tab_list in [[], self.tabs_with_discussion, self.tabs_without_discussion]:
|
||||
self.check_discussion(
|
||||
tab_list=tab_list,
|
||||
expected_discussion_link=not None,
|
||||
expected_can_display_value=False,
|
||||
)
|
||||
|
||||
def test_tabs_with_discussion(self):
|
||||
"""Test a course with a discussion tab configured"""
|
||||
self.settings.FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
|
||||
self.check_discussion(
|
||||
tab_list=self.tabs_with_discussion,
|
||||
expected_discussion_link="default_discussion_link",
|
||||
expected_can_display_value=True,
|
||||
)
|
||||
|
||||
def test_tabs_without_discussion(self):
|
||||
"""Test a course with tabs configured but without a discussion tab"""
|
||||
self.settings.FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
|
||||
self.check_discussion(
|
||||
tab_list=self.tabs_without_discussion,
|
||||
expected_discussion_link=not None,
|
||||
expected_can_display_value=False,
|
||||
)
|
||||
|
||||
def test_tabs_enrolled_or_staff(self):
|
||||
self.settings.FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
|
||||
for is_enrolled, is_staff in [(True, False), (False, True)]:
|
||||
self.check_discussion(
|
||||
tab_list=self.tabs_with_discussion,
|
||||
expected_discussion_link="default_discussion_link",
|
||||
expected_can_display_value=True,
|
||||
is_enrolled=is_enrolled,
|
||||
is_staff=is_staff
|
||||
)
|
||||
|
||||
def test_tabs_not_enrolled_or_staff(self):
|
||||
self.settings.FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
|
||||
is_enrolled = is_staff = False
|
||||
self.check_discussion(
|
||||
tab_list=self.tabs_with_discussion,
|
||||
expected_discussion_link="default_discussion_link",
|
||||
expected_can_display_value=False,
|
||||
is_enrolled=is_enrolled,
|
||||
is_staff=is_staff
|
||||
)
|
||||
Reference in New Issue
Block a user