diff --git a/cms/envs/common.py b/cms/envs/common.py index ceaa86756caa..28c788839db0 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -255,6 +255,15 @@ # .. 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 0467af8a6956..64f8fc34428e 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.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 @@ def get(self, request, *args, **kwargs): 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 7c5307cba764..9d67a01c6021 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.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 @@ def get(self, request, *args, **kwargs): # pylint: disable=too-many-statements 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 f16423c7695b..9274afee874e 100644 --- a/lms/djangoapps/courseware/plugins.py +++ b/lms/djangoapps/courseware/plugins.py @@ -65,6 +65,51 @@ def get_allowed_operations(cls, course_key: CourseKey, user: Optional[User] = No } +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. + """ + return not CourseOverview.get_from_id(course_key).hide_dates_tab + + @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) + course.hide_dates_tab = 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 2a67b6454e42..c993078b92ea 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,12 @@ def link_func(course, _reverse_func): 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 + return not getattr(course, 'hide_dates_tab', False) + 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 841b0ffe5362..f2af5feed643 100644 --- a/lms/djangoapps/courseware/tests/test_tabs.py +++ b/lms/djangoapps/courseware/tests/test_tabs.py @@ -870,6 +870,7 @@ def test_singular_dates_tab(self): """Test cases for making sure no persisted dates tab is surfaced""" user = self.create_mock_user() self.course.tabs = self.all_valid_tab_list + self.course.hide_dates_tab = False self.course.save() # Verify that there is a dates tab in the modulestore @@ -886,3 +887,16 @@ def test_singular_dates_tab(self): 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): + tab = DatesTab({'type': DatesTab.type, 'name': 'dates'}) + + is_enrolled.return_value = True + user = self.create_mock_user(is_staff=False, is_enrolled=True) + + self.course.hide_dates_tab = False + assert self.is_tab_enabled(tab, self.course, user) + + self.course.hide_dates_tab = True + assert not self.is_tab_enabled(tab, self.course, user) diff --git a/lms/envs/common.py b/lms/envs/common.py index 0419633f583e..8c72e83b4605 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2783,6 +2783,7 @@ # .. setting_description: Content to replace spam posts with CONTENT_FOR_SPAM_POSTS = "" + # .. toggle_name: ENABLE_AUTHN_RESET_PASSWORD_HIBP_POLICY # .. toggle_implementation: DjangoSetting # .. toggle_default: False diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index c5b453f82e6c..7e3378af40b1 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -943,6 +943,13 @@ def hide_progress_tab(self): """ return self._original_course.hide_progress_tab + @property + def hide_dates_tab(self): + """ + TODO: move this to the model. + """ + return self._original_course.hide_dates_tab + @property def edxnotes(self): """ diff --git a/setup.py b/setup.py index eeb7b79f534d..8cc1acc666e7 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ "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", diff --git a/xmodule/course_block.py b/xmodule/course_block.py index 17dc4d877be7..0c22c49e39d7 100644 --- a/xmodule/course_block.py +++ b/xmodule/course_block.py @@ -727,6 +727,13 @@ class CourseFields: # lint-amnesty, pylint: disable=missing-class-docstring deprecated=True ) + hide_dates_tab = Boolean( + display_name=_("Hide Dates Tab"), + help=_("Allows hiding of the dates tab."), + scope=Scope.settings, + deprecated=True + ) + display_organization = String( display_name=_("Course Organization Display String"), help=_(