This was the "outline tab" view of the course. Preceded by the
course info view, succeeded by the MFE outline tab.
In addition to the course home view itself, this drops related
features:
- Legacy version of Course Goals (MFE has a newer implementation)
- Course home in-course search (MFE has no search)
The old course info view and course about views survive for now.
This also drops a few now-unused feature toggles:
- course_experience.latest_update
- course_experience.show_upgrade_msg_on_course_home
- course_experience.upgrade_deadline_message
- course_home.course_home_use_legacy_frontend
With this change, just the progress and courseware tabs are still
supported in legacy form, if you opt-in with waffle flags. The
outline and dates tabs are offered only by the MFE.
AA-798
(This is identical to previous commit be5c1a6, just reintroduced
now that the e2e tests have been fixed)
392 lines
12 KiB
Python
392 lines
12 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
|
|
"""
|
|
|
|
|
|
from django.conf import settings
|
|
from django.utils.translation import gettext as _
|
|
from django.utils.translation import gettext_noop
|
|
from xmodule.tabs import CourseTab, CourseTabList, key_checker
|
|
|
|
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_progress_tab_is_active
|
|
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
|
from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG, default_course_url
|
|
from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url
|
|
from common.djangoapps.student.models import CourseEnrollment
|
|
|
|
|
|
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 = gettext_noop('Course')
|
|
priority = 11
|
|
view_name = 'courseware'
|
|
is_movable = False
|
|
is_default = False
|
|
supports_preview_menu = True
|
|
|
|
def __init__(self, tab_dict):
|
|
def link_func(course, _reverse_func):
|
|
return default_course_url(course.id)
|
|
|
|
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 DISABLE_UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
|
|
return super().is_enabled(course, user)
|
|
# If this is the unified course tab then it is always enabled
|
|
return True
|
|
|
|
|
|
class CourseInfoTab(CourseTab):
|
|
"""
|
|
The course info view.
|
|
"""
|
|
type = 'course_info'
|
|
title = gettext_noop('Home')
|
|
priority = 10
|
|
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 = gettext_noop('Syllabus')
|
|
priority = 80
|
|
view_name = 'syllabus'
|
|
allow_multiple = True
|
|
is_default = False
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
if not super().is_enabled(course, user=user):
|
|
return False
|
|
return getattr(course, 'syllabus_present', False)
|
|
|
|
|
|
class ProgressTab(EnrolledTab):
|
|
"""
|
|
The course progress view.
|
|
"""
|
|
type = 'progress'
|
|
title = gettext_noop('Progress')
|
|
priority = 20
|
|
view_name = 'progress'
|
|
is_hideable = True
|
|
is_default = False
|
|
|
|
def __init__(self, tab_dict):
|
|
def link_func(course, reverse_func):
|
|
if course_home_mfe_progress_tab_is_active(course.id):
|
|
return get_learning_mfe_home_url(course_key=course.id, url_fragment=self.view_name)
|
|
else:
|
|
return reverse_func(self.view_name, args=[str(course.id)])
|
|
|
|
tab_dict['link_func'] = link_func
|
|
super().__init__(tab_dict) # pylint: disable=super-with-arguments
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
if not super().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 = gettext_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 = 200
|
|
view_name = 'book'
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
parent_is_enabled = super().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=f'textbook/{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 = 201
|
|
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=f'pdftextbook/{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 = 202
|
|
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=f'htmltextbook/{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().__init__(tab_dict)
|
|
|
|
def __getitem__(self, key):
|
|
if key == 'link':
|
|
return self.link_value
|
|
else:
|
|
return super().__getitem__(key)
|
|
|
|
def __setitem__(self, key, value):
|
|
if key == 'link':
|
|
self.link_value = value
|
|
else:
|
|
super().__setitem__(key, value)
|
|
|
|
def to_json(self):
|
|
to_json_val = super().to_json()
|
|
to_json_val.update({'link': self.link_value})
|
|
return to_json_val
|
|
|
|
def __eq__(self, other):
|
|
if not super().__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 = gettext_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().validate(tab_dict, raise_error) and
|
|
key_checker(['link'])(tab_dict, raise_error))
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
if not super().is_enabled(course, user=user):
|
|
return False
|
|
# Course Overview objects don't have this attribute so avoid the error for now and figure
|
|
# out a better long-term solution
|
|
return hasattr(course, 'discussion_link') and course.discussion_link
|
|
|
|
|
|
class ExternalLinkCourseTab(LinkTab):
|
|
"""
|
|
A course tab containing an external link.
|
|
"""
|
|
type = 'external_link'
|
|
priority = 110
|
|
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().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=[str(course.id), index])
|
|
|
|
tab_dict = {}
|
|
tab_dict['name'] = name
|
|
tab_dict['tab_id'] = tab_id
|
|
tab_dict['link_func'] = link_func
|
|
super().__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"
|
|
# We don't have the user in this context, so we don't want to translate it at this level.
|
|
title = gettext_noop("Dates")
|
|
priority = 30
|
|
view_name = "dates"
|
|
|
|
def __init__(self, tab_dict):
|
|
def link_func(course, _reverse_func):
|
|
return get_learning_mfe_home_url(course_key=course.id, url_fragment='dates')
|
|
|
|
tab_dict['link_func'] = link_func
|
|
super().__init__(tab_dict)
|
|
|
|
|
|
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")
|
|
tab.title = _("Entrance Exam")
|
|
# TODO: LEARNER-611 - once the course_info tab is removed, remove this code
|
|
if not DISABLE_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
|
|
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 = []
|
|
for tab_type in CourseTabPluginManager.get_tab_types():
|
|
if getattr(tab_type, "is_dynamic", False):
|
|
tab = tab_type({})
|
|
if tab.is_enabled(course, user=user):
|
|
dynamic_tabs.append(tab)
|
|
dynamic_tabs.sort(key=lambda dynamic_tab: dynamic_tab.name)
|
|
return dynamic_tabs
|