Skip to content

Commit b968eed

Browse files
authored
feat: add optional API for hiding the dates tab (#37923)
1 parent d9293af commit b968eed

7 files changed

Lines changed: 112 additions & 3 deletions

File tree

cms/envs/common.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,15 @@
256256
# .. toggle_tickets: 'https://openedx.atlassian.net/browse/VAN-622'
257257
ENABLE_COPPA_COMPLIANCE = False
258258

259+
# .. toggle_name: ENABLE_DATES_COURSE_APP
260+
# .. toggle_implementation: DjangoSetting
261+
# .. toggle_default: False
262+
# .. toggle_description: Controls whether the Dates course app is surfaced via the course apps API/UI.
263+
# .. toggle_use_cases: open_edx
264+
# .. toggle_creation_date: 2026-02-02
265+
# .. toggle_tickets: https://github.com/openedx/platform-roadmap/issues/392
266+
ENABLE_DATES_COURSE_APP = False
267+
259268
ENABLE_JASMINE = False
260269

261270
MARKETING_EMAILS_OPT_IN = False

lms/djangoapps/course_home_api/dates/views.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from lms.djangoapps.courseware.courses import get_course_date_blocks
2020
from lms.djangoapps.courseware.date_summary import TodaysDate
2121
from lms.djangoapps.courseware.masquerade import setup_masquerade
22+
from lms.djangoapps.courseware.tabs import DatesTab
2223
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
2324
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
2425

@@ -110,13 +111,19 @@ def get(self, request, *args, **kwargs):
110111
course_key=course_key,
111112
)
112113

114+
course_date_blocks = (
115+
[block for block in blocks if not isinstance(block, TodaysDate)]
116+
if DatesTab.is_enabled(course, request.user)
117+
else []
118+
)
119+
113120
# User locale settings
114121
user_timezone_locale = user_timezone_locale_prefs(request)
115122
user_timezone = user_timezone_locale['user_timezone']
116123

117124
data = {
118125
'has_ended': course.has_ended(),
119-
'course_date_blocks': [block for block in blocks if not isinstance(block, TodaysDate)],
126+
'course_date_blocks': course_date_blocks,
120127
'learner_is_full_access': learner_is_full_access,
121128
'user_timezone': user_timezone,
122129
}

lms/djangoapps/course_home_api/outline/views.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_info_section
4343
from lms.djangoapps.courseware.date_summary import TodaysDate
4444
from lms.djangoapps.courseware.masquerade import is_masquerading, setup_masquerade
45+
from lms.djangoapps.courseware.tabs import DatesTab
4546
from lms.djangoapps.courseware.toggles import courseware_disable_navigation_sidebar_blocks_caching
4647
from lms.djangoapps.courseware.views.views import get_cert_data
4748
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
@@ -249,7 +250,12 @@ def get(self, request, *args, **kwargs): # pylint: disable=too-many-statements
249250
if show_enrolled:
250251
course_blocks = get_course_outline_block_tree(request, course_key_string, request.user)
251252
date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1)
252-
dates_widget['course_date_blocks'] = [block for block in date_blocks if not isinstance(block, TodaysDate)]
253+
course_date_blocks = (
254+
[block for block in date_blocks if not isinstance(block, TodaysDate)]
255+
if DatesTab.is_enabled(course, request.user)
256+
else []
257+
)
258+
dates_widget['course_date_blocks'] = course_date_blocks
253259

254260
handouts_html = get_course_info_section(request, request.user, course, 'handouts')
255261
welcome_message_html = get_current_update_for_user(request, course)

lms/djangoapps/courseware/plugins.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from opaque_keys.edx.keys import CourseKey
99

1010
from xmodule.modulestore.django import modulestore
11+
from xmodule.tabs import CourseTabList
12+
13+
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview, CourseTab
1114

12-
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
1315
from openedx.core.djangoapps.course_apps.plugins import CourseApp
1416
from openedx.core.lib.courses import get_course_by_id
1517

@@ -65,6 +67,58 @@ def get_allowed_operations(cls, course_key: CourseKey, user: Optional[User] = No
6567
}
6668

6769

70+
class DatesCourseApp(CourseApp):
71+
"""Course app stub for course dates."""
72+
73+
app_id = "dates"
74+
name = _("Dates")
75+
description = _("Provide learners a summary of important course dates.")
76+
documentation_links = {
77+
"learn_more_configuration": getattr(settings, "DATES_HELP_URL", ""),
78+
}
79+
80+
@classmethod
81+
def is_available(cls, course_key: CourseKey) -> bool: # pylint: disable=unused-argument
82+
"""
83+
Dates app is available when explicitly enabled via settings.
84+
"""
85+
return settings.ENABLE_DATES_COURSE_APP
86+
87+
@classmethod
88+
def is_enabled(cls, course_key: CourseKey) -> bool:
89+
"""
90+
The dates course status is stored in the course block.
91+
"""
92+
course = get_course_by_id(course_key)
93+
dates_tab = CourseTabList.get_tab_by_id(course.tabs, 'dates')
94+
return bool(dates_tab and not dates_tab.is_hidden)
95+
96+
@classmethod
97+
def set_enabled(cls, course_key: CourseKey, enabled: bool, user: 'User') -> bool:
98+
"""
99+
The dates course enabled/disabled status is stored in the course block.
100+
"""
101+
course = get_course_by_id(course_key)
102+
dates_tab = CourseTabList.get_tab_by_id(course.tabs, 'dates')
103+
if enabled and dates_tab is None:
104+
dates_tab = CourseTab.load("dates")
105+
course.tabs.append(dates_tab)
106+
if dates_tab is not None:
107+
dates_tab.is_hidden = not enabled
108+
modulestore().update_item(course, user.id)
109+
return enabled
110+
111+
@classmethod
112+
def get_allowed_operations(cls, course_key: CourseKey, user: Optional[User] = None) -> Dict[str, bool]: # pylint: disable=unused-argument
113+
"""
114+
Returns the allowed operations for the app.
115+
"""
116+
return {
117+
"enable": True,
118+
"configure": True,
119+
}
120+
121+
68122
class TextbooksCourseApp(CourseApp):
69123
"""
70124
Course app config for textbooks app.

lms/djangoapps/courseware/tabs.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ class DatesTab(EnrolledTab):
307307
title = gettext_noop("Dates")
308308
priority = 30
309309
view_name = "dates"
310+
is_hideable = True
310311

311312
def __init__(self, tab_dict):
312313
def link_func(course, _reverse_func):
@@ -315,6 +316,13 @@ def link_func(course, _reverse_func):
315316
tab_dict['link_func'] = link_func
316317
super().__init__(tab_dict)
317318

319+
@classmethod
320+
def is_enabled(cls, course, user=None):
321+
if not super().is_enabled(course, user=user):
322+
return False
323+
dates_tab = CourseTabList.get_tab_by_id(course.tabs, 'dates')
324+
return bool(dates_tab and not dates_tab.is_hidden)
325+
318326

319327
def get_course_tab_list(user, course):
320328
"""

lms/djangoapps/courseware/tests/test_tabs.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,12 @@ def test_singular_dates_tab(self):
870870
"""Test cases for making sure no persisted dates tab is surfaced"""
871871
user = self.create_mock_user()
872872
self.course.tabs = self.all_valid_tab_list
873+
874+
# Ensure hidden state from other tests does not affect this test's intent.
875+
dates_tab = xmodule_tabs.CourseTabList.get_tab_by_id(self.course.tabs, 'dates')
876+
assert dates_tab is not None
877+
dates_tab.is_hidden = False
878+
873879
self.course.save()
874880

875881
# Verify that there is a dates tab in the modulestore
@@ -886,3 +892,21 @@ def test_singular_dates_tab(self):
886892
if tab.type == 'dates':
887893
num_dates_tabs += 1
888894
assert num_dates_tabs == 1
895+
896+
@patch('common.djangoapps.student.models.course_enrollment.CourseEnrollment.is_enrolled')
897+
def test_dates_tab_respects_hide_flag(self, is_enrolled):
898+
"""Test that the dates tab respects the hide flag."""
899+
is_enrolled.return_value = True
900+
user = self.create_mock_user(is_staff=False, is_enrolled=True)
901+
self.course.tabs = self.all_valid_tab_list
902+
dates_tab = xmodule_tabs.CourseTabList.get_tab_by_id(self.course.tabs, 'dates')
903+
assert dates_tab is not None
904+
905+
dates_tab.is_hidden = False
906+
self.course.save()
907+
tabs = get_course_tab_list(user, self.course)
908+
assert any(tab.type == 'dates' for tab in tabs)
909+
910+
dates_tab.is_hidden = True
911+
tabs = get_course_tab_list(user, self.course)
912+
assert not any(tab.type == 'dates' for tab in tabs)

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"openedx.course_app": [
8787
"calculator = lms.djangoapps.courseware.plugins:CalculatorCourseApp",
8888
"custom_pages = lms.djangoapps.courseware.plugins:CustomPagesCourseApp",
89+
"dates = lms.djangoapps.courseware.plugins:DatesCourseApp",
8990
"discussion = openedx.core.djangoapps.discussions.plugins:DiscussionCourseApp",
9091
"edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotesCourseApp",
9192
"live = openedx.core.djangoapps.course_live.plugins:LiveCourseApp",

0 commit comments

Comments
 (0)