This also starts taking priority into account for all tabs, and not just dynamic tabs via the CourseTabPluginManager (see comments in the code for more detail)
402 lines
13 KiB
Python
402 lines
13 KiB
Python
"""
|
|
This module is essentially a broker to xmodule/tabs.py -- it was originally introduced to
|
|
perform some LMS-specific tab display gymnastics for the Entrance Exams feature
|
|
"""
|
|
|
|
|
|
import six
|
|
from django.conf import settings
|
|
from django.utils.translation import ugettext as _
|
|
from django.utils.translation import ugettext_noop
|
|
|
|
from lms.djangoapps.courseware.access import has_access
|
|
from lms.djangoapps.courseware.entrance_exams import user_can_skip_entrance_exam
|
|
from lms.djangoapps.course_home_api.toggles import course_home_mfe_dates_tab_is_active, course_home_mfe_outline_tab_is_active
|
|
from lms.djangoapps.course_home_api.utils import get_microfrontend_url
|
|
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
|
from openedx.features.course_experience import RELATIVE_DATES_FLAG, UNIFIED_COURSE_TAB_FLAG, default_course_url_name
|
|
from student.models import CourseEnrollment
|
|
from xmodule.tabs import CourseTab, CourseTabList, course_reverse_func_from_name_func, key_checker
|
|
|
|
|
|
class EnrolledTab(CourseTab):
|
|
"""
|
|
A base class for any view types that require a user to be enrolled.
|
|
"""
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
return user and user.is_authenticated and \
|
|
bool(CourseEnrollment.is_enrolled(user, course.id) or has_access(user, 'staff', course, course.id))
|
|
|
|
|
|
class CoursewareTab(EnrolledTab):
|
|
"""
|
|
The main courseware view.
|
|
"""
|
|
type = 'courseware'
|
|
title = ugettext_noop('Course')
|
|
priority = 10
|
|
view_name = 'courseware'
|
|
is_movable = False
|
|
is_default = False
|
|
supports_preview_menu = True
|
|
|
|
def __init__(self, tab_dict):
|
|
def link_func(course, reverse_func):
|
|
if course_home_mfe_outline_tab_is_active(course.id):
|
|
return get_microfrontend_url(course_key=course.id, view_name='home')
|
|
else:
|
|
reverse_name_func = lambda course: default_course_url_name(course.id)
|
|
url_func = course_reverse_func_from_name_func(reverse_name_func)
|
|
return url_func(course, reverse_func)
|
|
|
|
tab_dict['link_func'] = link_func
|
|
super().__init__(tab_dict)
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
"""
|
|
Returns true if this tab is enabled.
|
|
"""
|
|
# If this is the unified course tab then it is always enabled
|
|
if UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
|
|
return True
|
|
return super(CoursewareTab, cls).is_enabled(course, user)
|
|
|
|
|
|
class CourseInfoTab(CourseTab):
|
|
"""
|
|
The course info view.
|
|
"""
|
|
type = 'course_info'
|
|
title = ugettext_noop('Home')
|
|
priority = 20
|
|
view_name = 'info'
|
|
tab_id = 'info'
|
|
is_movable = False
|
|
is_default = False
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
return True
|
|
|
|
|
|
class SyllabusTab(EnrolledTab):
|
|
"""
|
|
A tab for the course syllabus.
|
|
"""
|
|
type = 'syllabus'
|
|
title = ugettext_noop('Syllabus')
|
|
priority = 30
|
|
view_name = 'syllabus'
|
|
allow_multiple = True
|
|
is_default = False
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
if not super(SyllabusTab, cls).is_enabled(course, user=user):
|
|
return False
|
|
return getattr(course, 'syllabus_present', False)
|
|
|
|
|
|
class ProgressTab(EnrolledTab):
|
|
"""
|
|
The course progress view.
|
|
"""
|
|
type = 'progress'
|
|
title = ugettext_noop('Progress')
|
|
priority = 40
|
|
view_name = 'progress'
|
|
is_hideable = True
|
|
is_default = False
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
if not super(ProgressTab, cls).is_enabled(course, user=user):
|
|
return False
|
|
return not course.hide_progress_tab
|
|
|
|
|
|
class TextbookTabsBase(CourseTab):
|
|
"""
|
|
Abstract class for textbook collection tabs classes.
|
|
"""
|
|
# Translators: 'Textbooks' refers to the tab in the course that leads to the course' textbooks
|
|
title = ugettext_noop("Textbooks")
|
|
is_collection = True
|
|
is_default = False
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
return user is None or user.is_authenticated
|
|
|
|
@classmethod
|
|
def items(cls, course):
|
|
"""
|
|
A generator for iterating through all the SingleTextbookTab book objects associated with this
|
|
collection of textbooks.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class TextbookTabs(TextbookTabsBase):
|
|
"""
|
|
A tab representing the collection of all textbook tabs.
|
|
"""
|
|
type = 'textbooks'
|
|
priority = None
|
|
view_name = 'book'
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
parent_is_enabled = super(TextbookTabs, cls).is_enabled(course, user)
|
|
return settings.FEATURES.get('ENABLE_TEXTBOOK') and parent_is_enabled
|
|
|
|
@classmethod
|
|
def items(cls, course):
|
|
for index, textbook in enumerate(course.textbooks):
|
|
yield SingleTextbookTab(
|
|
name=textbook.title,
|
|
tab_id='textbook/{0}'.format(index),
|
|
view_name=cls.view_name,
|
|
index=index
|
|
)
|
|
|
|
|
|
class PDFTextbookTabs(TextbookTabsBase):
|
|
"""
|
|
A tab representing the collection of all PDF textbook tabs.
|
|
"""
|
|
type = 'pdf_textbooks'
|
|
priority = None
|
|
view_name = 'pdf_book'
|
|
|
|
@classmethod
|
|
def items(cls, course):
|
|
for index, textbook in enumerate(course.pdf_textbooks):
|
|
yield SingleTextbookTab(
|
|
name=textbook['tab_title'],
|
|
tab_id='pdftextbook/{0}'.format(index),
|
|
view_name=cls.view_name,
|
|
index=index
|
|
)
|
|
|
|
|
|
class HtmlTextbookTabs(TextbookTabsBase):
|
|
"""
|
|
A tab representing the collection of all Html textbook tabs.
|
|
"""
|
|
type = 'html_textbooks'
|
|
priority = None
|
|
view_name = 'html_book'
|
|
|
|
@classmethod
|
|
def items(cls, course):
|
|
for index, textbook in enumerate(course.html_textbooks):
|
|
yield SingleTextbookTab(
|
|
name=textbook['tab_title'],
|
|
tab_id='htmltextbook/{0}'.format(index),
|
|
view_name=cls.view_name,
|
|
index=index
|
|
)
|
|
|
|
|
|
class LinkTab(CourseTab):
|
|
"""
|
|
Abstract class for tabs that contain external links.
|
|
"""
|
|
link_value = ''
|
|
|
|
def __init__(self, tab_dict=None, link=None):
|
|
self.link_value = tab_dict['link'] if tab_dict else link
|
|
|
|
def link_value_func(_course, _reverse_func):
|
|
""" Returns the link_value as the link. """
|
|
return self.link_value
|
|
|
|
self.type = tab_dict['type']
|
|
|
|
tab_dict['link_func'] = link_value_func
|
|
|
|
super(LinkTab, self).__init__(tab_dict)
|
|
|
|
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 is_enabled(cls, course, user=None):
|
|
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 = ugettext_noop('Discussion')
|
|
priority = None
|
|
is_default = False
|
|
|
|
@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):
|
|
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):
|
|
"""
|
|
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
|
|
priority = None
|
|
|
|
def __init__(self, name, tab_id, view_name, index):
|
|
def link_func(course, reverse_func, index=index):
|
|
""" Constructs a link for textbooks from a view name, a course, and an index. """
|
|
return reverse_func(view_name, args=[six.text_type(course.id), index])
|
|
|
|
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.')
|
|
|
|
|
|
class DatesTab(EnrolledTab):
|
|
"""
|
|
A tab representing the relevant dates for a course.
|
|
"""
|
|
type = "dates"
|
|
title = ugettext_noop(
|
|
"Dates") # We don't have the user in this context, so we don't want to translate it at this level.
|
|
priority = 50
|
|
view_name = "dates"
|
|
is_dynamic = True
|
|
|
|
def __init__(self, tab_dict):
|
|
def link_func(course, reverse_func):
|
|
if course_home_mfe_dates_tab_is_active(course.id):
|
|
return get_microfrontend_url(course_key=course.id, view_name=self.view_name)
|
|
else:
|
|
return reverse_func(self.view_name, args=[six.text_type(course.id)])
|
|
|
|
tab_dict['link_func'] = link_func
|
|
super(DatesTab, self).__init__(tab_dict)
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
"""Returns true if this tab is enabled."""
|
|
if not super().is_enabled(course, user=user):
|
|
return False
|
|
return RELATIVE_DATES_FLAG.is_enabled(course.id)
|
|
|
|
|
|
def get_course_tab_list(user, course):
|
|
"""
|
|
Retrieves the course tab list from xmodule.tabs and manipulates the set as necessary
|
|
"""
|
|
xmodule_tab_list = CourseTabList.iterate_displayable(course, user=user)
|
|
|
|
# Now that we've loaded the tabs for this course, perform the Entrance Exam work.
|
|
# If the user has to take an entrance exam, we'll need to hide away all but the
|
|
# "Courseware" tab. The tab is then renamed as "Entrance Exam".
|
|
course_tab_list = []
|
|
must_complete_ee = not user_can_skip_entrance_exam(user, course)
|
|
for tab in xmodule_tab_list:
|
|
if must_complete_ee:
|
|
# Hide all of the tabs except for 'Courseware'
|
|
# Rename 'Courseware' tab to 'Entrance Exam'
|
|
if tab.type != 'courseware':
|
|
continue
|
|
tab.name = _("Entrance Exam")
|
|
# TODO: LEARNER-611 - once the course_info tab is removed, remove this code
|
|
if UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id) and tab.type == 'course_info':
|
|
continue
|
|
if tab.type == 'static_tab' and tab.course_staff_only and \
|
|
not bool(user and has_access(user, 'staff', course, course.id)):
|
|
continue
|
|
# We had initially created a CourseTab.load() for dates that ended up
|
|
# persisting the dates tab tomodulestore on Course Run creation, but
|
|
# ignoring any static dates tab here we can fix forward without
|
|
# allowing the bug to continue to surface
|
|
if tab.type == 'dates':
|
|
continue
|
|
course_tab_list.append(tab)
|
|
|
|
# Add in any dynamic tabs, i.e. those that are not persisted
|
|
course_tab_list += _get_dynamic_tabs(course, user)
|
|
# Sorting here because although the CourseTabPluginManager.get_tab_types function
|
|
# does do sorting on priority, we only use it for getting the dynamic tabs.
|
|
# We can't switch this function to just use the CourseTabPluginManager without
|
|
# further investigation since CourseTabList.iterate_displayable returns
|
|
# Static Tabs that are not returned by the CourseTabPluginManager.
|
|
course_tab_list.sort(key=lambda tab: tab.priority or float('inf'))
|
|
return course_tab_list
|
|
|
|
|
|
def _get_dynamic_tabs(course, user):
|
|
"""
|
|
Returns the dynamic tab types for the current user.
|
|
|
|
Note: dynamic tabs are those that are not persisted in the course, but are
|
|
instead added dynamically based upon the user's role.
|
|
"""
|
|
dynamic_tabs = list()
|
|
for tab_type in CourseTabPluginManager.get_tab_types():
|
|
if getattr(tab_type, "is_dynamic", False):
|
|
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)
|
|
return dynamic_tabs
|