Merge pull request #8015 from edx/andya/add-tab-extensions

Add extensible course view types for edX platform
This commit is contained in:
Diana Huang
2015-06-02 17:08:45 -04:00
52 changed files with 1928 additions and 1902 deletions

View File

@@ -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),

View File

@@ -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)

View File

@@ -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.

View File

@@ -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
'''

View File

@@ -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.

View File

@@ -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):

View File

@@ -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
)