Merge pull request #8367 from edx/diana/merge-course-view-and-tab

Refactor and merge CourseViewType and CourseTab.
This commit is contained in:
Diana Huang
2015-06-08 16:08:27 -04:00
28 changed files with 442 additions and 514 deletions

View File

@@ -7,12 +7,12 @@ from django.utils.translation import ugettext as _
from courseware.access import has_access
from courseware.entrance_exams import user_must_complete_entrance_exam
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager, CourseViewType, StaticTab
from openedx.core.lib.course_tabs import CourseTabPluginManager
from student.models import CourseEnrollment
from xmodule.tabs import CourseTab, CourseTabList, key_checker
class EnrolledCourseViewType(CourseViewType):
class EnrolledTab(CourseTab):
"""
A base class for any view types that require a user to be enrolled.
"""
@@ -23,22 +23,22 @@ class EnrolledCourseViewType(CourseViewType):
return CourseEnrollment.is_enrolled(user, course.id) or has_access(user, 'staff', course, course.id)
class CoursewareViewType(EnrolledCourseViewType):
class CoursewareTab(EnrolledTab):
"""
The main courseware view.
"""
name = 'courseware'
type = 'courseware'
title = _('Courseware')
priority = 10
view_name = 'courseware'
is_movable = False
class CourseInfoViewType(CourseViewType):
class CourseInfoTab(CourseTab):
"""
The course info view.
"""
name = 'course_info'
type = 'course_info'
title = _('Course Info')
priority = 20
view_name = 'info'
@@ -50,27 +50,28 @@ class CourseInfoViewType(CourseViewType):
return True
class SyllabusCourseViewType(EnrolledCourseViewType):
class SyllabusTab(EnrolledTab):
"""
A tab for the course syllabus.
"""
name = 'syllabus'
type = 'syllabus'
title = _('Syllabus')
priority = 30
view_name = 'syllabus'
allow_multiple = True
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
if not super(SyllabusCourseViewType, cls).is_enabled(course, user=user):
if not super(SyllabusTab, cls).is_enabled(course, user=user):
return False
return getattr(course, 'syllabus_present', False)
class ProgressCourseViewType(EnrolledCourseViewType):
class ProgressTab(EnrolledTab):
"""
The course progress view.
"""
name = 'progress'
type = 'progress'
title = _('Progress')
priority = 40
view_name = 'progress'
@@ -78,12 +79,12 @@ class ProgressCourseViewType(EnrolledCourseViewType):
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
if not super(ProgressCourseViewType, cls).is_enabled(course, user=user):
if not super(ProgressTab, cls).is_enabled(course, user=user):
return False
return not course.hide_progress_tab
class TextbookCourseViewsBase(CourseViewType):
class TextbookTabsBase(CourseTab):
"""
Abstract class for textbook collection tabs classes.
"""
@@ -104,17 +105,17 @@ class TextbookCourseViewsBase(CourseViewType):
raise NotImplementedError()
class TextbookCourseViews(TextbookCourseViewsBase):
class TextbookTabs(TextbookTabsBase):
"""
A tab representing the collection of all textbook tabs.
"""
name = 'textbooks'
type = 'textbooks'
priority = None
view_name = 'book'
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
parent_is_enabled = super(TextbookCourseViews, cls).is_enabled(course, user)
parent_is_enabled = super(TextbookTabs, cls).is_enabled(course, user)
return settings.FEATURES.get('ENABLE_TEXTBOOK') and parent_is_enabled
@classmethod
@@ -128,11 +129,11 @@ class TextbookCourseViews(TextbookCourseViewsBase):
)
class PDFTextbookCourseViews(TextbookCourseViewsBase):
class PDFTextbookTabs(TextbookTabsBase):
"""
A tab representing the collection of all PDF textbook tabs.
"""
name = 'pdf_textbooks'
type = 'pdf_textbooks'
priority = None
view_name = 'pdf_book'
@@ -147,11 +148,11 @@ class PDFTextbookCourseViews(TextbookCourseViewsBase):
)
class HtmlTextbookCourseViews(TextbookCourseViewsBase):
class HtmlTextbookTabs(TextbookTabsBase):
"""
A tab representing the collection of all Html textbook tabs.
"""
name = 'html_textbooks'
type = 'html_textbooks'
priority = None
view_name = 'html_book'
@@ -166,89 +167,6 @@ class HtmlTextbookCourseViews(TextbookCourseViewsBase):
)
class StaticCourseViewType(CourseViewType):
"""
The view type that shows a static tab.
"""
name = 'static_tab'
is_default = False # A static tab is never added to a course by default
allow_multiple = True
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
"""
Static tabs are viewable to everyone, even anonymous users.
"""
return True
@classmethod
def validate(cls, tab_dict, raise_error=True):
"""
Ensures that the specified tab_dict is valid.
"""
return (super(StaticCourseViewType, cls).validate(tab_dict, raise_error)
and key_checker(['name', 'url_slug'])(tab_dict, raise_error))
@classmethod
def create_tab(cls, tab_dict):
"""
Returns the tab that will be shown to represent an instance of a view.
"""
return StaticTab(tab_dict)
class ExternalDiscussionCourseViewType(EnrolledCourseViewType):
"""
A course view links to an external discussion service.
"""
name = 'external_discussion'
# Translators: 'Discussion' refers to the tab in the courseware that leads to the discussion forums
title = _('Discussion')
priority = None
@classmethod
def create_tab(cls, tab_dict):
"""
Returns the tab that will be shown to represent an instance of a view.
"""
return LinkTab(tab_dict, cls.title)
@classmethod
def validate(cls, tab_dict, raise_error=True):
""" Validate that the tab_dict for this course view has the necessary information to render. """
return (super(ExternalDiscussionCourseViewType, cls).validate(tab_dict, raise_error) and
key_checker(['link'])(tab_dict, raise_error))
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
if not super(ExternalDiscussionCourseViewType, cls).is_enabled(course, user=user):
return False
return course.discussion_link
class ExternalLinkCourseViewType(EnrolledCourseViewType):
"""
A course view containing an external link.
"""
name = 'external_link'
priority = None
is_default = False # An external link tab is not added to a course by default
@classmethod
def create_tab(cls, tab_dict):
"""
Returns the tab that will be shown to represent an instance of a view.
"""
return LinkTab(tab_dict)
@classmethod
def validate(cls, tab_dict, raise_error=True):
""" Validate that the tab_dict for this course view has the necessary information to render. """
return (super(ExternalLinkCourseViewType, cls).validate(tab_dict, raise_error) and
key_checker(['link', 'name'])(tab_dict, raise_error))
class LinkTab(CourseTab):
"""
Abstract class for tabs that contain external links.
@@ -264,11 +182,9 @@ class LinkTab(CourseTab):
self.type = tab_dict['type']
super(LinkTab, self).__init__(
name=tab_dict['name'] if tab_dict else name,
tab_id=None,
link_func=link_value_func,
)
tab_dict['link_func'] = link_value_func
super(LinkTab, self).__init__(tab_dict)
def __getitem__(self, key):
if key == 'link':
@@ -292,6 +208,49 @@ class LinkTab(CourseTab):
return False
return self.link_value == other.get('link')
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
return True
class ExternalDiscussionCourseTab(LinkTab):
"""
A course tab that links to an external discussion service.
"""
type = 'external_discussion'
# Translators: 'Discussion' refers to the tab in the courseware that leads to the discussion forums
title = _('Discussion')
priority = None
@classmethod
def validate(cls, tab_dict, raise_error=True):
""" Validate that the tab_dict for this course tab has the necessary information to render. """
return (super(ExternalDiscussionCourseTab, cls).validate(tab_dict, raise_error) and
key_checker(['link'])(tab_dict, raise_error))
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
if not super(ExternalDiscussionCourseTab, cls).is_enabled(course, user=user):
return False
return course.discussion_link
class ExternalLinkCourseTab(LinkTab):
"""
A course tab containing an external link.
"""
type = 'external_link'
priority = None
is_default = False # An external link tab is not added to a course by default
allow_multiple = True
@classmethod
def validate(cls, tab_dict, raise_error=True):
""" Validate that the tab_dict for this course tab has the necessary information to render. """
return (super(ExternalLinkCourseTab, cls).validate(tab_dict, raise_error) and
key_checker(['link', 'name'])(tab_dict, raise_error))
class SingleTextbookTab(CourseTab):
"""
@@ -308,7 +267,11 @@ class SingleTextbookTab(CourseTab):
""" Constructs a link for textbooks from a view name, a course, and an index. """
return reverse_func(view_name, args=[unicode(course.id), index])
super(SingleTextbookTab, self).__init__(name, tab_id, link_func)
tab_dict = dict()
tab_dict['name'] = name
tab_dict['tab_id'] = tab_id
tab_dict['link_func'] = link_func
super(SingleTextbookTab, self).__init__(tab_dict)
def to_json(self):
raise NotImplementedError('SingleTextbookTab should not be serialized.')
@@ -347,9 +310,9 @@ def _get_dynamic_tabs(course, user):
instead added dynamically based upon the user's role.
"""
dynamic_tabs = list()
for tab_type in CourseViewTypeManager.get_course_view_types():
for tab_type in CourseTabPluginManager.get_tab_types():
if getattr(tab_type, "is_dynamic", False):
tab = tab_type.create_tab(dict())
tab = tab_type(dict())
if tab.is_enabled(course, user=user):
dynamic_tabs.append(tab)
dynamic_tabs.sort(key=lambda dynamic_tab: dynamic_tab.name)

View File

@@ -11,8 +11,8 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.courses import get_course_by_id
from courseware.tabs import (
get_course_tab_list, CoursewareViewType, CourseInfoViewType, ProgressCourseViewType,
StaticCourseViewType, ExternalDiscussionCourseViewType, ExternalLinkCourseViewType
get_course_tab_list, CoursewareTab, CourseInfoTab, ProgressTab,
ExternalDiscussionCourseTab, ExternalLinkCourseTab
)
from courseware.tests.helpers import get_request_for_user, LoginEnrollmentTestCase
from courseware.tests.factories import InstructorFactory, StaffFactory
@@ -85,7 +85,7 @@ class TabTestCase(ModuleStoreTestCase):
Can be 'None' if the given tab class does not have any keys to validate.
"""
# create tab
tab = tab_class.create_tab(tab_dict=dict_tab)
tab = tab_class(tab_dict=dict_tab)
# name is as expected
self.assertEqual(tab.name, expected_name)
@@ -475,17 +475,17 @@ class TabListTestCase(TabTestCase):
# invalid tabs
self.invalid_tabs = [
# less than 2 tabs
[{'type': CoursewareViewType.name}],
[{'type': CoursewareTab.type}],
# missing course_info
[{'type': CoursewareViewType.name}, {'type': 'discussion', 'name': 'fake_name'}],
[{'type': CoursewareTab.type}, {'type': 'discussion', 'name': 'fake_name'}],
# incorrect order
[{'type': CourseInfoViewType.name, 'name': 'fake_name'}, {'type': CoursewareViewType.name}],
[{'type': CourseInfoTab.type, 'name': 'fake_name'}, {'type': CoursewareTab.type}],
]
# tab types that should appear only once
unique_tab_types = [
CoursewareViewType.name,
CourseInfoViewType.name,
CoursewareTab.type,
CourseInfoTab.type,
'textbooks',
'pdf_textbooks',
'html_textbooks',
@@ -493,8 +493,8 @@ class TabListTestCase(TabTestCase):
for unique_tab_type in unique_tab_types:
self.invalid_tabs.append([
{'type': CoursewareViewType.name},
{'type': CourseInfoViewType.name, 'name': 'fake_name'},
{'type': CoursewareTab.type},
{'type': CourseInfoTab.type, 'name': 'fake_name'},
# add the unique tab multiple times
{'type': unique_tab_type},
{'type': unique_tab_type},
@@ -502,26 +502,27 @@ class TabListTestCase(TabTestCase):
# valid tabs
self.valid_tabs = [
# empty list
# any empty list is valid because a default list of tabs will be
# generated to replace the empty list.
[],
# all valid tabs
[
{'type': CoursewareViewType.name},
{'type': CourseInfoViewType.name, 'name': 'fake_name'},
{'type': CoursewareTab.type},
{'type': CourseInfoTab.type, 'name': 'fake_name'},
{'type': 'discussion', 'name': 'fake_name'},
{'type': ExternalLinkCourseViewType.name, 'name': 'fake_name', 'link': 'fake_link'},
{'type': ExternalLinkCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'},
{'type': 'textbooks'},
{'type': 'pdf_textbooks'},
{'type': 'html_textbooks'},
{'type': ProgressCourseViewType.name, 'name': 'fake_name'},
{'type': StaticCourseViewType.name, 'name': 'fake_name', 'url_slug': 'schlug'},
{'type': ProgressTab.type, 'name': 'fake_name'},
{'type': xmodule_tabs.StaticTab.type, 'name': 'fake_name', 'url_slug': 'schlug'},
{'type': 'syllabus'},
],
# with external discussion
[
{'type': CoursewareViewType.name},
{'type': CourseInfoViewType.name, 'name': 'fake_name'},
{'type': ExternalDiscussionCourseViewType.name, 'name': 'fake_name', 'link': 'fake_link'}
{'type': CoursewareTab.type},
{'type': CourseInfoTab.type, 'name': 'fake_name'},
{'type': ExternalDiscussionCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'}
],
]
@@ -550,8 +551,8 @@ class ValidateTabsTestCase(TabListTestCase):
tab_list = xmodule_tabs.CourseTabList()
self.assertEquals(
len(tab_list.from_json([
{'type': CoursewareViewType.name},
{'type': CourseInfoViewType.name, 'name': 'fake_name'},
{'type': CoursewareTab.type},
{'type': CourseInfoTab.type, 'name': 'fake_name'},
{'type': 'no_such_type'}
])),
2
@@ -660,10 +661,10 @@ class ProgressTestCase(TabTestCase):
def check_progress_tab(self):
"""Helper function for verifying the progress tab."""
return self.check_tab(
tab_class=ProgressCourseViewType,
dict_tab={'type': ProgressCourseViewType.name, 'name': 'same'},
tab_class=ProgressTab,
dict_tab={'type': ProgressTab.type, 'name': 'same'},
expected_link=self.reverse('progress', args=[self.course.id.to_deprecated_string()]),
expected_tab_id=ProgressCourseViewType.name,
expected_tab_id=ProgressTab.type,
invalid_dict_tab=None,
)
@@ -692,8 +693,8 @@ class StaticTabTestCase(TabTestCase):
url_slug = 'schmug'
tab = self.check_tab(
tab_class=StaticCourseViewType,
dict_tab={'type': StaticCourseViewType.name, 'name': 'same', 'url_slug': url_slug},
tab_class=xmodule_tabs.StaticTab,
dict_tab={'type': xmodule_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,

View File

@@ -1150,9 +1150,9 @@ def notification_image_for_tab(course_tab, user, course):
"""
tab_notification_handlers = {
StaffGradingTab.name: open_ended_notifications.staff_grading_notifications,
PeerGradingTab.name: open_ended_notifications.peer_grading_notifications,
OpenEndedGradingTab.name: open_ended_notifications.combined_notifications
StaffGradingTab.type: open_ended_notifications.staff_grading_notifications,
PeerGradingTab.type: open_ended_notifications.peer_grading_notifications,
OpenEndedGradingTab.type: open_ended_notifications.combined_notifications
}
if course_tab.name in tab_notification_handlers: