diff --git a/cms/envs/common.py b/cms/envs/common.py index 44c4ee4e2f..f4f6fd8b9c 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -256,6 +256,15 @@ IN_CONTEXT_DISCUSSION_ENABLED_DEFAULT = True # .. toggle_tickets: 'https://openedx.atlassian.net/browse/VAN-622' ENABLE_COPPA_COMPLIANCE = False +# .. toggle_name: ENABLE_DATES_COURSE_APP +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: Controls whether the Dates course app is surfaced via the course apps API/UI. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2026-02-02 +# .. toggle_tickets: https://github.com/openedx/platform-roadmap/issues/392 +ENABLE_DATES_COURSE_APP = False + ENABLE_JASMINE = False MARKETING_EMAILS_OPT_IN = False diff --git a/lms/djangoapps/course_home_api/dates/views.py b/lms/djangoapps/course_home_api/dates/views.py index 0467af8a69..64f8fc3442 100644 --- a/lms/djangoapps/course_home_api/dates/views.py +++ b/lms/djangoapps/course_home_api/dates/views.py @@ -19,6 +19,7 @@ from lms.djangoapps.courseware.context_processor import user_timezone_locale_pre from lms.djangoapps.courseware.courses import get_course_date_blocks from lms.djangoapps.courseware.date_summary import TodaysDate from lms.djangoapps.courseware.masquerade import setup_masquerade +from lms.djangoapps.courseware.tabs import DatesTab from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.features.content_type_gating.models import ContentTypeGatingConfig @@ -110,13 +111,19 @@ class DatesTabView(RetrieveAPIView): course_key=course_key, ) + course_date_blocks = ( + [block for block in blocks if not isinstance(block, TodaysDate)] + if DatesTab.is_enabled(course, request.user) + else [] + ) + # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] data = { 'has_ended': course.has_ended(), - 'course_date_blocks': [block for block in blocks if not isinstance(block, TodaysDate)], + 'course_date_blocks': course_date_blocks, 'learner_is_full_access': learner_is_full_access, 'user_timezone': user_timezone, } diff --git a/lms/djangoapps/course_home_api/outline/views.py b/lms/djangoapps/course_home_api/outline/views.py index 7c5307cba7..9d67a01c60 100644 --- a/lms/djangoapps/course_home_api/outline/views.py +++ b/lms/djangoapps/course_home_api/outline/views.py @@ -42,6 +42,7 @@ from lms.djangoapps.courseware.context_processor import user_timezone_locale_pre from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_info_section from lms.djangoapps.courseware.date_summary import TodaysDate from lms.djangoapps.courseware.masquerade import is_masquerading, setup_masquerade +from lms.djangoapps.courseware.tabs import DatesTab from lms.djangoapps.courseware.toggles import courseware_disable_navigation_sidebar_blocks_caching from lms.djangoapps.courseware.views.views import get_cert_data from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory @@ -249,7 +250,12 @@ class OutlineTabView(RetrieveAPIView): if show_enrolled: course_blocks = get_course_outline_block_tree(request, course_key_string, request.user) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) - dates_widget['course_date_blocks'] = [block for block in date_blocks if not isinstance(block, TodaysDate)] + course_date_blocks = ( + [block for block in date_blocks if not isinstance(block, TodaysDate)] + if DatesTab.is_enabled(course, request.user) + else [] + ) + dates_widget['course_date_blocks'] = course_date_blocks handouts_html = get_course_info_section(request, request.user, course, 'handouts') welcome_message_html = get_current_update_for_user(request, course) diff --git a/lms/djangoapps/courseware/plugins.py b/lms/djangoapps/courseware/plugins.py index f16423c769..f890512d2a 100644 --- a/lms/djangoapps/courseware/plugins.py +++ b/lms/djangoapps/courseware/plugins.py @@ -8,8 +8,10 @@ from django.utils.translation import gettext_noop as _ from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import modulestore +from xmodule.tabs import CourseTabList + +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview, CourseTab -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.course_apps.plugins import CourseApp from openedx.core.lib.courses import get_course_by_id @@ -65,6 +67,58 @@ class ProgressCourseApp(CourseApp): } +class DatesCourseApp(CourseApp): + """Course app stub for course dates.""" + + app_id = "dates" + name = _("Dates") + description = _("Provide learners a summary of important course dates.") + documentation_links = { + "learn_more_configuration": getattr(settings, "DATES_HELP_URL", ""), + } + + @classmethod + def is_available(cls, course_key: CourseKey) -> bool: # pylint: disable=unused-argument + """ + Dates app is available when explicitly enabled via settings. + """ + return settings.ENABLE_DATES_COURSE_APP + + @classmethod + def is_enabled(cls, course_key: CourseKey) -> bool: + """ + The dates course status is stored in the course block. + """ + course = get_course_by_id(course_key) + dates_tab = CourseTabList.get_tab_by_id(course.tabs, 'dates') + return bool(dates_tab and not dates_tab.is_hidden) + + @classmethod + def set_enabled(cls, course_key: CourseKey, enabled: bool, user: 'User') -> bool: + """ + The dates course enabled/disabled status is stored in the course block. + """ + course = get_course_by_id(course_key) + dates_tab = CourseTabList.get_tab_by_id(course.tabs, 'dates') + if enabled and dates_tab is None: + dates_tab = CourseTab.load("dates") + course.tabs.append(dates_tab) + if dates_tab is not None: + dates_tab.is_hidden = not enabled + modulestore().update_item(course, user.id) + return enabled + + @classmethod + def get_allowed_operations(cls, course_key: CourseKey, user: Optional[User] = None) -> Dict[str, bool]: # pylint: disable=unused-argument + """ + Returns the allowed operations for the app. + """ + return { + "enable": True, + "configure": True, + } + + class TextbooksCourseApp(CourseApp): """ Course app config for textbooks app. diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 2a67b6454e..56c1ca2e0e 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -307,6 +307,7 @@ class DatesTab(EnrolledTab): title = gettext_noop("Dates") priority = 30 view_name = "dates" + is_hideable = True def __init__(self, tab_dict): def link_func(course, _reverse_func): @@ -315,6 +316,13 @@ class DatesTab(EnrolledTab): tab_dict['link_func'] = link_func super().__init__(tab_dict) + @classmethod + def is_enabled(cls, course, user=None): + if not super().is_enabled(course, user=user): + return False + dates_tab = CourseTabList.get_tab_by_id(course.tabs, 'dates') + return bool(dates_tab and not dates_tab.is_hidden) + def get_course_tab_list(user, course): """ diff --git a/lms/djangoapps/courseware/tests/test_tabs.py b/lms/djangoapps/courseware/tests/test_tabs.py index 841b0ffe53..e332d0de96 100644 --- a/lms/djangoapps/courseware/tests/test_tabs.py +++ b/lms/djangoapps/courseware/tests/test_tabs.py @@ -870,6 +870,12 @@ class DatesTabTestCase(TabListTestCase): """Test cases for making sure no persisted dates tab is surfaced""" user = self.create_mock_user() self.course.tabs = self.all_valid_tab_list + + # Ensure hidden state from other tests does not affect this test's intent. + dates_tab = xmodule_tabs.CourseTabList.get_tab_by_id(self.course.tabs, 'dates') + assert dates_tab is not None + dates_tab.is_hidden = False + self.course.save() # Verify that there is a dates tab in the modulestore @@ -886,3 +892,21 @@ class DatesTabTestCase(TabListTestCase): if tab.type == 'dates': num_dates_tabs += 1 assert num_dates_tabs == 1 + + @patch('common.djangoapps.student.models.course_enrollment.CourseEnrollment.is_enrolled') + def test_dates_tab_respects_hide_flag(self, is_enrolled): + """Test that the dates tab respects the hide flag.""" + is_enrolled.return_value = True + user = self.create_mock_user(is_staff=False, is_enrolled=True) + self.course.tabs = self.all_valid_tab_list + dates_tab = xmodule_tabs.CourseTabList.get_tab_by_id(self.course.tabs, 'dates') + assert dates_tab is not None + + dates_tab.is_hidden = False + self.course.save() + tabs = get_course_tab_list(user, self.course) + assert any(tab.type == 'dates' for tab in tabs) + + dates_tab.is_hidden = True + tabs = get_course_tab_list(user, self.course) + assert not any(tab.type == 'dates' for tab in tabs) diff --git a/setup.py b/setup.py index eeb7b79f53..8cc1acc666 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ setup( "openedx.course_app": [ "calculator = lms.djangoapps.courseware.plugins:CalculatorCourseApp", "custom_pages = lms.djangoapps.courseware.plugins:CustomPagesCourseApp", + "dates = lms.djangoapps.courseware.plugins:DatesCourseApp", "discussion = openedx.core.djangoapps.discussions.plugins:DiscussionCourseApp", "edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotesCourseApp", "live = openedx.core.djangoapps.course_live.plugins:LiveCourseApp",