From e429c2140fccf4f188f953fbf22be6a0284234f4 Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 30 Apr 2026 14:05:31 -0600 Subject: [PATCH 01/14] feat: add filter for instructor mfe tabs --- openedx_filters/learning/filters.py | 81 +++++++++++++++++++ .../learning/tests/test_filters.py | 71 ++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index f75c27af..a1f8834b 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -1487,6 +1487,87 @@ def run_filter(cls, readonly_fields: set, user: Any) -> tuple[set, Any]: return (data["readonly_fields"], data["user"]) +class InstructorDashboardTabsGenerated(OpenEdxPublicFilter): + """ + Filter used to modify the instructor dashboard tabs generation process. + + Purpose: + This filter is triggered when instructor dashboard tabs are generated, allowing plugins + to add, modify, or remove tabs from the instructor dashboard in the MFE. + + Filter Type: + org.openedx.learning.instructor.dashboard.tabs.generated.v1 + + Trigger: + - Repository: openedx/edx-platform + - Path: lms/djangoapps/instructor/views/serializers_v2.py + - Function or Method: CourseInformationSerializerV2.get_tabs + """ + + filter_type = "org.openedx.learning.instructor.dashboard.tabs.generated.v1" + + class PreventTabsGeneration(OpenEdxFilterException): + """ + Raise to prevent the normal tabs generation process and optionally provide custom tabs. + + This exception is propagated to the instructor dashboard serializer and handled to stop + the normal tab generation process. Plugins can provide their own tabs list. + + Attributes: + message (str): error message for the exception. + tabs (list): optional custom tabs list to use instead. + """ + + def __init__(self, message: str, tabs: Optional[list] = None) -> None: + """ + Initialize the exception with the message and optional custom tabs. + + Arguments: + message (str): error message for the exception. + tabs (list): optional custom tabs list to use instead. + """ + super().__init__(message, tabs=tabs) + + @classmethod + def run_filter( + cls, + tabs: list, + course: Any, + user: Any, + course_key: CourseKey + ) -> list | None: + """ + Process the tabs list using the configured pipeline steps to modify instructor dashboard tabs. + + Arguments: + tabs (list): List of tab dictionaries containing tab_id, title, url, sort_order, etc. + course (CourseBlock): Course object. + user (User): Django User object (usually an instructor or staff member). + course_key (CourseKey): Course key for the instructor dashboard. + + Returns: + list | None: + List of tab dictionaries, possibly modified. + """ + data = super().run_pipeline( + tabs=tabs, + course=course, + user=user, + course_key=course_key + ) + + # Always return only the tabs, never the course object or other parameters + # The run_pipeline method may return all the original kwargs including the course object + if isinstance(data, dict) and "tabs" in data: + filtered_tabs = data["tabs"] + # Double-check that we're returning a list of dictionaries, not course objects + if isinstance(filtered_tabs, list): + return filtered_tabs + + # Fallback to original tabs if anything goes wrong + return tabs + + class GradeEventContextRequested(OpenEdxPublicFilter): """ Filter used to enrich the context for grade events. diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index 8ca9bd01..41b46691 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -26,6 +26,7 @@ GradeEventContextRequested, IDVPageURLRequested, InstructorDashboardRenderStarted, + InstructorDashboardTabsGenerated, ORASubmissionViewRenderStarted, RenderXBlockStarted, ScheduleQuerySetRequested, @@ -867,3 +868,73 @@ def test_filter_type(self): AccountSettingsReadOnlyFieldsRequested.filter_type, "org.openedx.learning.account.settings.read_only_fields.requested.v1", ) + + +@ddt +class TestInstructorDashboardTabsGenerated(TestCase): + """ + Test class to verify standard behavior of the InstructorDashboardTabsGenerated filter. + + You'll find test suites for: + - InstructorDashboardTabsGenerated + """ + + def test_run_filter_returns_unchanged_tabs_when_no_pipeline(self): + """ + Test InstructorDashboardTabsGenerated filter behavior under normal conditions. + + When no pipeline steps are configured, run_filter returns the original inputs unchanged. + + Expected behavior: + - The filter should return the tabs list, course, user, and course_key unchanged. + """ + tabs = [ + {"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0}, + {"tab_id": "instructor", "title": "Instructor", "url": "/instructor/123", "sort_order": 1}, + ] + course = Mock() + user = Mock() + course_key = Mock() + + result_tabs, result_course, result_user, result_course_key = InstructorDashboardTabsGenerated.run_filter( + tabs=tabs, course=course, user=user, course_key=course_key + ) + + self.assertEqual(result_tabs, tabs) + self.assertEqual(result_course, course) + self.assertEqual(result_user, user) + self.assertEqual(result_course_key, course_key) + + def test_filter_type(self): + """Test that the filter type is properly set.""" + self.assertEqual( + InstructorDashboardTabsGenerated.filter_type, + "org.openedx.learning.instructor.dashboard.tabs.generated.v1", + ) + + @data( + ( + InstructorDashboardTabsGenerated.PreventTabsGeneration, + { + "message": "Custom tabs provided by plugin", + "tabs": [{"tab_id": "custom", "title": "Custom", "url": "/custom", "sort_order": 0}], + } + ), + ( + InstructorDashboardTabsGenerated.PreventTabsGeneration, + { + "message": "Disable tab generation", + } + ), + ) + @unpack + def test_prevent_tabs_generation_exception(self, exception_class, attributes): + """ + Test that the PreventTabsGeneration exception can be initialized with required attributes. + + Expected behavior: + - The exception must have the attributes specified. + """ + exception = exception_class(**attributes) + + self.assertLessEqual(attributes.items(), exception.__dict__.items()) From c83797ca7ce9d88a05e3f250ee947d574dc3d1d4 Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 30 Apr 2026 15:30:21 -0600 Subject: [PATCH 02/14] chore: update test --- openedx_filters/learning/tests/test_filters.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index 41b46691..6ffe7db3 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -883,10 +883,10 @@ def test_run_filter_returns_unchanged_tabs_when_no_pipeline(self): """ Test InstructorDashboardTabsGenerated filter behavior under normal conditions. - When no pipeline steps are configured, run_filter returns the original inputs unchanged. - + When no pipeline steps are configured, run_filter returns the original tabs unchanged. + Expected behavior: - - The filter should return the tabs list, course, user, and course_key unchanged. + - The filter should return the tabs list unchanged. """ tabs = [ {"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0}, @@ -896,14 +896,11 @@ def test_run_filter_returns_unchanged_tabs_when_no_pipeline(self): user = Mock() course_key = Mock() - result_tabs, result_course, result_user, result_course_key = InstructorDashboardTabsGenerated.run_filter( + result_tabs = InstructorDashboardTabsGenerated.run_filter( tabs=tabs, course=course, user=user, course_key=course_key ) self.assertEqual(result_tabs, tabs) - self.assertEqual(result_course, course) - self.assertEqual(result_user, user) - self.assertEqual(result_course_key, course_key) def test_filter_type(self): """Test that the filter type is properly set.""" From a8f59bf38d99f3f12cd9239fa2d745e15a9ac5fb Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 30 Apr 2026 15:40:03 -0600 Subject: [PATCH 03/14] chore: improve coverage --- .../learning/tests/test_filters.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index 6ffe7db3..e2baeb62 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -909,6 +909,97 @@ def test_filter_type(self): "org.openedx.learning.instructor.dashboard.tabs.generated.v1", ) + def test_run_filter_with_pipeline_returning_dict_with_tabs(self): + """ + Test InstructorDashboardTabsGenerated filter when pipeline returns dict with tabs. + + Expected behavior: + - The filter should return the filtered tabs from the pipeline result. + """ + tabs = [ + {"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0}, + ] + modified_tabs = [ + {"tab_id": "custom", "title": "Custom Tab", "url": "/custom/123", "sort_order": 0}, + ] + course = Mock() + user = Mock() + course_key = Mock() + + with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: + mock_run_pipeline.return_value = {"tabs": modified_tabs, "course": course, "user": user, "course_key": course_key} + result_tabs = InstructorDashboardTabsGenerated.run_filter( + tabs=tabs, course=course, user=user, course_key=course_key + ) + + self.assertEqual(result_tabs, modified_tabs) + + def test_run_filter_with_pipeline_returning_dict_without_tabs_key(self): + """ + Test InstructorDashboardTabsGenerated filter when pipeline returns dict without tabs key. + + Expected behavior: + - The filter should return the original tabs as fallback. + """ + tabs = [ + {"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0}, + ] + course = Mock() + user = Mock() + course_key = Mock() + + with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: + mock_run_pipeline.return_value = {"course": course, "user": user, "course_key": course_key} + result_tabs = InstructorDashboardTabsGenerated.run_filter( + tabs=tabs, course=course, user=user, course_key=course_key + ) + + self.assertEqual(result_tabs, tabs) + + def test_run_filter_with_pipeline_returning_dict_with_non_list_tabs(self): + """ + Test InstructorDashboardTabsGenerated filter when pipeline returns dict with non-list tabs. + + Expected behavior: + - The filter should return the original tabs as fallback. + """ + tabs = [ + {"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0}, + ] + course = Mock() + user = Mock() + course_key = Mock() + + with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: + mock_run_pipeline.return_value = {"tabs": "not_a_list", "course": course, "user": user, "course_key": course_key} + result_tabs = InstructorDashboardTabsGenerated.run_filter( + tabs=tabs, course=course, user=user, course_key=course_key + ) + + self.assertEqual(result_tabs, tabs) + + def test_run_filter_with_pipeline_returning_non_dict(self): + """ + Test InstructorDashboardTabsGenerated filter when pipeline returns non-dict. + + Expected behavior: + - The filter should return the original tabs as fallback. + """ + tabs = [ + {"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0}, + ] + course = Mock() + user = Mock() + course_key = Mock() + + with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: + mock_run_pipeline.return_value = "not_a_dict" + result_tabs = InstructorDashboardTabsGenerated.run_filter( + tabs=tabs, course=course, user=user, course_key=course_key + ) + + self.assertEqual(result_tabs, tabs) + @data( ( InstructorDashboardTabsGenerated.PreventTabsGeneration, From 52131e5d5fbcfc0d8f33d6b1b8cc3d82b7786d8d Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 30 Apr 2026 19:30:06 -0600 Subject: [PATCH 04/14] chore: fixed test syntax --- openedx_filters/learning/tests/test_filters.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index e2baeb62..afaef871 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -927,7 +927,11 @@ def test_run_filter_with_pipeline_returning_dict_with_tabs(self): course_key = Mock() with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: - mock_run_pipeline.return_value = {"tabs": modified_tabs, "course": course, "user": user, "course_key": course_key} + mock_run_pipeline.return_value = { + "tabs": modified_tabs, "course": + course, "user": user, + "course_key": course_key + } result_tabs = InstructorDashboardTabsGenerated.run_filter( tabs=tabs, course=course, user=user, course_key=course_key ) @@ -971,7 +975,12 @@ def test_run_filter_with_pipeline_returning_dict_with_non_list_tabs(self): course_key = Mock() with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: - mock_run_pipeline.return_value = {"tabs": "not_a_list", "course": course, "user": user, "course_key": course_key} + mock_run_pipeline.return_value = { + "tabs": "not_a_list", + "course": course, + "user": user, + "course_key": course_key + } result_tabs = InstructorDashboardTabsGenerated.run_filter( tabs=tabs, course=course, user=user, course_key=course_key ) From 29721036d66341e420d4e225eaa493a9de15202e Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Mon, 4 May 2026 11:58:03 -0600 Subject: [PATCH 05/14] chore: address comments --- ...4_javier.ontiveros_instructor_app_tabs.rst | 3 + openedx_filters/learning/filters.py | 19 +---- .../learning/tests/test_filters.py | 71 ------------------- 3 files changed, 6 insertions(+), 87 deletions(-) create mode 100644 changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst diff --git a/changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst b/changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst new file mode 100644 index 00000000..ef0641e0 --- /dev/null +++ b/changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst @@ -0,0 +1,3 @@ +Added +~~~~~ +* Added new ``InstructorDashboardTabsGenerated`` filter to allow for dynamic generation of instructor dashboard tabs. (by @holaontiveros) diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index a1f8834b..00866207 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -1535,19 +1535,16 @@ def run_filter( course: Any, user: Any, course_key: CourseKey - ) -> list | None: + ) -> list: """ Process the tabs list using the configured pipeline steps to modify instructor dashboard tabs. - Arguments: tabs (list): List of tab dictionaries containing tab_id, title, url, sort_order, etc. course (CourseBlock): Course object. user (User): Django User object (usually an instructor or staff member). course_key (CourseKey): Course key for the instructor dashboard. - Returns: - list | None: - List of tab dictionaries, possibly modified. + list: Tab dictionaries, possibly modified by pipeline steps. """ data = super().run_pipeline( tabs=tabs, @@ -1555,17 +1552,7 @@ def run_filter( user=user, course_key=course_key ) - - # Always return only the tabs, never the course object or other parameters - # The run_pipeline method may return all the original kwargs including the course object - if isinstance(data, dict) and "tabs" in data: - filtered_tabs = data["tabs"] - # Double-check that we're returning a list of dictionaries, not course objects - if isinstance(filtered_tabs, list): - return filtered_tabs - - # Fallback to original tabs if anything goes wrong - return tabs + return data.get("tabs") or [] class GradeEventContextRequested(OpenEdxPublicFilter): diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index afaef871..a925a028 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -938,77 +938,6 @@ def test_run_filter_with_pipeline_returning_dict_with_tabs(self): self.assertEqual(result_tabs, modified_tabs) - def test_run_filter_with_pipeline_returning_dict_without_tabs_key(self): - """ - Test InstructorDashboardTabsGenerated filter when pipeline returns dict without tabs key. - - Expected behavior: - - The filter should return the original tabs as fallback. - """ - tabs = [ - {"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0}, - ] - course = Mock() - user = Mock() - course_key = Mock() - - with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: - mock_run_pipeline.return_value = {"course": course, "user": user, "course_key": course_key} - result_tabs = InstructorDashboardTabsGenerated.run_filter( - tabs=tabs, course=course, user=user, course_key=course_key - ) - - self.assertEqual(result_tabs, tabs) - - def test_run_filter_with_pipeline_returning_dict_with_non_list_tabs(self): - """ - Test InstructorDashboardTabsGenerated filter when pipeline returns dict with non-list tabs. - - Expected behavior: - - The filter should return the original tabs as fallback. - """ - tabs = [ - {"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0}, - ] - course = Mock() - user = Mock() - course_key = Mock() - - with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: - mock_run_pipeline.return_value = { - "tabs": "not_a_list", - "course": course, - "user": user, - "course_key": course_key - } - result_tabs = InstructorDashboardTabsGenerated.run_filter( - tabs=tabs, course=course, user=user, course_key=course_key - ) - - self.assertEqual(result_tabs, tabs) - - def test_run_filter_with_pipeline_returning_non_dict(self): - """ - Test InstructorDashboardTabsGenerated filter when pipeline returns non-dict. - - Expected behavior: - - The filter should return the original tabs as fallback. - """ - tabs = [ - {"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0}, - ] - course = Mock() - user = Mock() - course_key = Mock() - - with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: - mock_run_pipeline.return_value = "not_a_dict" - result_tabs = InstructorDashboardTabsGenerated.run_filter( - tabs=tabs, course=course, user=user, course_key=course_key - ) - - self.assertEqual(result_tabs, tabs) - @data( ( InstructorDashboardTabsGenerated.PreventTabsGeneration, From 63d8fe1f50f97a4c2abedb59961ef57ab210271f Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 7 May 2026 09:27:58 -0600 Subject: [PATCH 06/14] chore: applied comments Co-authored-by: Copilot --- openedx_filters/learning/filters.py | 9 +++------ openedx_filters/learning/tests/test_filters.py | 18 ++++++++---------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index 00866207..8b7e9ff2 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -1532,27 +1532,24 @@ def __init__(self, message: str, tabs: Optional[list] = None) -> None: def run_filter( cls, tabs: list, - course: Any, user: Any, course_key: CourseKey - ) -> list: + ) -> list | None: """ Process the tabs list using the configured pipeline steps to modify instructor dashboard tabs. Arguments: tabs (list): List of tab dictionaries containing tab_id, title, url, sort_order, etc. - course (CourseBlock): Course object. user (User): Django User object (usually an instructor or staff member). course_key (CourseKey): Course key for the instructor dashboard. Returns: - list: Tab dictionaries, possibly modified by pipeline steps. + list | None: Tab dictionaries, possibly modified by pipeline steps, or None if not provided. """ data = super().run_pipeline( tabs=tabs, - course=course, user=user, course_key=course_key ) - return data.get("tabs") or [] + return data.get("tabs") class GradeEventContextRequested(OpenEdxPublicFilter): diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index a925a028..0a902c60 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -892,13 +892,14 @@ def test_run_filter_returns_unchanged_tabs_when_no_pipeline(self): {"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0}, {"tab_id": "instructor", "title": "Instructor", "url": "/instructor/123", "sort_order": 1}, ] - course = Mock() user = Mock() course_key = Mock() - result_tabs = InstructorDashboardTabsGenerated.run_filter( - tabs=tabs, course=course, user=user, course_key=course_key - ) + with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: + mock_run_pipeline.return_value = {"tabs": tabs, "user": user, "course_key": course_key} + result_tabs = InstructorDashboardTabsGenerated.run_filter( + tabs=tabs, user=user, course_key=course_key + ) self.assertEqual(result_tabs, tabs) @@ -922,18 +923,15 @@ def test_run_filter_with_pipeline_returning_dict_with_tabs(self): modified_tabs = [ {"tab_id": "custom", "title": "Custom Tab", "url": "/custom/123", "sort_order": 0}, ] - course = Mock() user = Mock() course_key = Mock() with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: mock_run_pipeline.return_value = { - "tabs": modified_tabs, "course": - course, "user": user, - "course_key": course_key - } + "tabs": modified_tabs, "user": user, "course_key": course_key + } result_tabs = InstructorDashboardTabsGenerated.run_filter( - tabs=tabs, course=course, user=user, course_key=course_key + tabs=tabs, user=user, course_key=course_key ) self.assertEqual(result_tabs, modified_tabs) From 34dda1d955e7edcb2b149b122ab9adad6e8df937 Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 7 May 2026 09:41:44 -0600 Subject: [PATCH 07/14] chore: manually added changelog changes --- CHANGELOG.rst | 6 ++++-- openedx_filters/__init__.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 67e51ddf..88eec643 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,17 +31,19 @@ Change Log Unreleased ---------- +.. scriv-insert-here +[3.4.0] - 2026-05-07 Added ~~~~~ * Added django52 support. * Also releasing pending items. +* Added new ``InstructorDashboardTabsGenerated`` filter to allow for dynamic generation of instructor dashboard tabs. (by @holaontiveros) See the fragment files in the `changelog.d directory`_. .. _changelog.d directory: https://github.com/openedx/openedx-filters/tree/master/changelog.d -.. scriv-insert-here [3.3.0] - 2025-04-17 -------------------- @@ -58,7 +60,7 @@ Changed ~~~~~~~ * Added GradeEventContextRequested filter - + [3.1.0] - 2025-04-06 -------------------- diff --git a/openedx_filters/__init__.py b/openedx_filters/__init__.py index 2bdb72d7..bd86af1c 100644 --- a/openedx_filters/__init__.py +++ b/openedx_filters/__init__.py @@ -6,7 +6,7 @@ from openedx_filters.filters import * -__version__ = "3.3.0" +__version__ = "3.4.0" if sys.version_info < (3, 12): # pragma: no cover warnings.warn( From 7e5fe6a2b61bc1ceab09de3ee1e91c6ef3cda8b0 Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 7 May 2026 10:49:22 -0600 Subject: [PATCH 08/14] chore: address comments --- CHANGELOG.rst | 2 +- ...4_javier.ontiveros_instructor_app_tabs.rst | 2 +- openedx_filters/learning/filters.py | 130 +++++++++--------- .../learning/tests/test_filters.py | 25 ++-- 4 files changed, 80 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 88eec643..09aae760 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,7 +40,7 @@ Added * Added django52 support. * Also releasing pending items. -* Added new ``InstructorDashboardTabsGenerated`` filter to allow for dynamic generation of instructor dashboard tabs. (by @holaontiveros) +* Added new ``InstructorDashboardTabsRequested`` filter to allow for dynamic generation of instructor dashboard tabs. (by @holaontiveros) See the fragment files in the `changelog.d directory`_. .. _changelog.d directory: https://github.com/openedx/openedx-filters/tree/master/changelog.d diff --git a/changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst b/changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst index ef0641e0..b9fa335d 100644 --- a/changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst +++ b/changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst @@ -1,3 +1,3 @@ Added ~~~~~ -* Added new ``InstructorDashboardTabsGenerated`` filter to allow for dynamic generation of instructor dashboard tabs. (by @holaontiveros) +* Added new ``InstructorDashboardTabsRequested`` filter to allow for dynamic generation of instructor dashboard tabs. (by @holaontiveros) diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index 8b7e9ff2..034d1754 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -1283,6 +1283,71 @@ def run_filter(cls, context: dict[str, Any], template_name: str) -> tuple[dict[s return data.get("context"), data.get("template_name") +class InstructorDashboardTabsRequested(OpenEdxPublicFilter): + """ + Filter used to modify the instructor dashboard tabs generation process. + + Purpose: + This filter is triggered when instructor dashboard tabs are generated, allowing plugins + to add, modify, or remove tabs from the instructor dashboard in the MFE. + + Filter Type: + org.openedx.learning.instructor.dashboard.tabs.generated.v1 + + Trigger: + - Repository: openedx/edx-platform + - Path: lms/djangoapps/instructor/views/serializers_v2.py + - Function or Method: CourseInformationSerializerV2.get_tabs + """ + + filter_type = "org.openedx.learning.instructor.dashboard.tabs.requested.v1" + + class PreventTabsGeneration(OpenEdxFilterException): + """ + Raise to prevent the normal tabs generation process and optionally provide custom tabs. + + This exception is propagated to the instructor dashboard serializer and handled to stop + the normal tab generation process. Plugins can provide their own tabs list. + + Attributes: + message (str): error message for the exception. + tabs (list): optional custom tabs list to use instead. + """ + + def __init__(self, message: str, tabs: Optional[list] = None) -> None: + """ + Initialize the exception with the message and optional custom tabs. + + Arguments: + message (str): error message for the exception. + tabs (list): optional custom tabs list to use instead. + """ + super().__init__(message, tabs=tabs) + + @classmethod + def run_filter( + cls, + tabs: list, + user: Any, + course_key: CourseKey + ) -> list | None: + """ + Process the tabs list using the configured pipeline steps to modify instructor dashboard tabs. + Arguments: + tabs (list): List of tab dictionaries containing tab_id, title, url, sort_order, etc. + user (User): Django User object (usually an instructor or staff member). + course_key (CourseKey): Course key for the instructor dashboard. + Returns: + list | None: Tab dictionaries, possibly modified by pipeline steps, or None if not provided. + """ + data = super().run_pipeline( + tabs=tabs, + user=user, + course_key=course_key + ) + return data.get("tabs") + + class ORASubmissionViewRenderStarted(OpenEdxPublicFilter): """ Filter used to modify the submission view rendering process. @@ -1487,71 +1552,6 @@ def run_filter(cls, readonly_fields: set, user: Any) -> tuple[set, Any]: return (data["readonly_fields"], data["user"]) -class InstructorDashboardTabsGenerated(OpenEdxPublicFilter): - """ - Filter used to modify the instructor dashboard tabs generation process. - - Purpose: - This filter is triggered when instructor dashboard tabs are generated, allowing plugins - to add, modify, or remove tabs from the instructor dashboard in the MFE. - - Filter Type: - org.openedx.learning.instructor.dashboard.tabs.generated.v1 - - Trigger: - - Repository: openedx/edx-platform - - Path: lms/djangoapps/instructor/views/serializers_v2.py - - Function or Method: CourseInformationSerializerV2.get_tabs - """ - - filter_type = "org.openedx.learning.instructor.dashboard.tabs.generated.v1" - - class PreventTabsGeneration(OpenEdxFilterException): - """ - Raise to prevent the normal tabs generation process and optionally provide custom tabs. - - This exception is propagated to the instructor dashboard serializer and handled to stop - the normal tab generation process. Plugins can provide their own tabs list. - - Attributes: - message (str): error message for the exception. - tabs (list): optional custom tabs list to use instead. - """ - - def __init__(self, message: str, tabs: Optional[list] = None) -> None: - """ - Initialize the exception with the message and optional custom tabs. - - Arguments: - message (str): error message for the exception. - tabs (list): optional custom tabs list to use instead. - """ - super().__init__(message, tabs=tabs) - - @classmethod - def run_filter( - cls, - tabs: list, - user: Any, - course_key: CourseKey - ) -> list | None: - """ - Process the tabs list using the configured pipeline steps to modify instructor dashboard tabs. - Arguments: - tabs (list): List of tab dictionaries containing tab_id, title, url, sort_order, etc. - user (User): Django User object (usually an instructor or staff member). - course_key (CourseKey): Course key for the instructor dashboard. - Returns: - list | None: Tab dictionaries, possibly modified by pipeline steps, or None if not provided. - """ - data = super().run_pipeline( - tabs=tabs, - user=user, - course_key=course_key - ) - return data.get("tabs") - - class GradeEventContextRequested(OpenEdxPublicFilter): """ Filter used to enrich the context for grade events. diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index 0a902c60..9dd3c3fe 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -26,7 +26,7 @@ GradeEventContextRequested, IDVPageURLRequested, InstructorDashboardRenderStarted, - InstructorDashboardTabsGenerated, + InstructorDashboardTabsRequested, ORASubmissionViewRenderStarted, RenderXBlockStarted, ScheduleQuerySetRequested, @@ -871,17 +871,18 @@ def test_filter_type(self): @ddt -class TestInstructorDashboardTabsGenerated(TestCase): +class TestInstructorDashboardTabsRequested(TestCase): """ - Test class to verify standard behavior of the InstructorDashboardTabsGenerated filter. + Test class to verify standard behavior of the InstructorDashboardTabsRequested filter. You'll find test suites for: - - InstructorDashboardTabsGenerated +class TestInstructorDashboardTabsRequested(TestCase): + - InstructorDashboardTabsRequested """ def test_run_filter_returns_unchanged_tabs_when_no_pipeline(self): """ - Test InstructorDashboardTabsGenerated filter behavior under normal conditions. + Test InstructorDashboardTabsRequested filter behavior under normal conditions. When no pipeline steps are configured, run_filter returns the original tabs unchanged. @@ -897,7 +898,7 @@ def test_run_filter_returns_unchanged_tabs_when_no_pipeline(self): with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: mock_run_pipeline.return_value = {"tabs": tabs, "user": user, "course_key": course_key} - result_tabs = InstructorDashboardTabsGenerated.run_filter( + result_tabs = InstructorDashboardTabsRequested.run_filter( tabs=tabs, user=user, course_key=course_key ) @@ -906,13 +907,13 @@ def test_run_filter_returns_unchanged_tabs_when_no_pipeline(self): def test_filter_type(self): """Test that the filter type is properly set.""" self.assertEqual( - InstructorDashboardTabsGenerated.filter_type, - "org.openedx.learning.instructor.dashboard.tabs.generated.v1", + InstructorDashboardTabsRequested.filter_type, + "org.openedx.learning.instructor.dashboard.tabs.requested.v1", ) def test_run_filter_with_pipeline_returning_dict_with_tabs(self): """ - Test InstructorDashboardTabsGenerated filter when pipeline returns dict with tabs. + Test InstructorDashboardTabsRequested filter when pipeline returns dict with tabs. Expected behavior: - The filter should return the filtered tabs from the pipeline result. @@ -930,7 +931,7 @@ def test_run_filter_with_pipeline_returning_dict_with_tabs(self): mock_run_pipeline.return_value = { "tabs": modified_tabs, "user": user, "course_key": course_key } - result_tabs = InstructorDashboardTabsGenerated.run_filter( + result_tabs = InstructorDashboardTabsRequested.run_filter( tabs=tabs, user=user, course_key=course_key ) @@ -938,14 +939,14 @@ def test_run_filter_with_pipeline_returning_dict_with_tabs(self): @data( ( - InstructorDashboardTabsGenerated.PreventTabsGeneration, + InstructorDashboardTabsRequested.PreventTabsGeneration, { "message": "Custom tabs provided by plugin", "tabs": [{"tab_id": "custom", "title": "Custom", "url": "/custom", "sort_order": 0}], } ), ( - InstructorDashboardTabsGenerated.PreventTabsGeneration, + InstructorDashboardTabsRequested.PreventTabsGeneration, { "message": "Disable tab generation", } From 1d2df7f2893d7528aa77ed2e6bfe88ef207127ff Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 7 May 2026 10:52:50 -0600 Subject: [PATCH 09/14] chore: add better docstrings --- openedx_filters/learning/filters.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index 034d1754..a2ecf42b 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -1174,6 +1174,10 @@ class InstructorDashboardRenderStarted(OpenEdxPublicFilter): This filter is triggered when an instructor requests to view the dashboard, just before the page is rendered allowing the filter to act on the context and the template used to render the page. + There's a new version of this filter (org.openedx.learning.instructor.dashboard.tabs.requested.v1) + that applies to the instructor dashboard app, + but this filter will still be triggered for the legacy instructor dashboard. + Filter Type: org.openedx.learning.instructor.dashboard.render.started.v1 @@ -1291,6 +1295,10 @@ class InstructorDashboardTabsRequested(OpenEdxPublicFilter): This filter is triggered when instructor dashboard tabs are generated, allowing plugins to add, modify, or remove tabs from the instructor dashboard in the MFE. + There's an old version of this filter (org.openedx.learning.instructor.dashboard.render.started.v1) + that applies to the legacy instructor dashboard, + but this new filter is specifically designed to work with the instructor dashboard app and its tabs generation process. + Filter Type: org.openedx.learning.instructor.dashboard.tabs.generated.v1 From f7947a58bf7620ad7e03e9d847eb629c862f7ae5 Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 7 May 2026 10:54:45 -0600 Subject: [PATCH 10/14] chore: added DEPR reference --- openedx_filters/learning/filters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index a2ecf42b..f6b940c1 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -1166,6 +1166,7 @@ def run_filter(cls, serialized_courserun: dict[str, Any]) -> dict[str, Any] | No return data.get("serialized_courserun") +# DEPR-38432: This filter should be handled as part of mentioned deprecation ticket class InstructorDashboardRenderStarted(OpenEdxPublicFilter): """ Filter used to modify the instructor dashboard rendering process. From c902a2b67d8da37b1ce22c2028901208b3ad5107 Mon Sep 17 00:00:00 2001 From: Javier Ontiveros Date: Thu, 7 May 2026 11:13:42 -0600 Subject: [PATCH 11/14] Update openedx_filters/learning/filters.py Co-authored-by: Felipe Montoya --- openedx_filters/learning/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index f6b940c1..7145bb69 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -1301,7 +1301,7 @@ class InstructorDashboardTabsRequested(OpenEdxPublicFilter): but this new filter is specifically designed to work with the instructor dashboard app and its tabs generation process. Filter Type: - org.openedx.learning.instructor.dashboard.tabs.generated.v1 + org.openedx.learning.instructor.dashboard.tabs.requested.v1 Trigger: - Repository: openedx/edx-platform From cd38d82ab9a944cf864463edfe8408ea41b4fc33 Mon Sep 17 00:00:00 2001 From: Javier Ontiveros Date: Thu, 7 May 2026 11:14:07 -0600 Subject: [PATCH 12/14] Update openedx_filters/learning/tests/test_filters.py Co-authored-by: Felipe Montoya --- openedx_filters/learning/tests/test_filters.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index 9dd3c3fe..b537e871 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -876,7 +876,6 @@ class TestInstructorDashboardTabsRequested(TestCase): Test class to verify standard behavior of the InstructorDashboardTabsRequested filter. You'll find test suites for: -class TestInstructorDashboardTabsRequested(TestCase): - InstructorDashboardTabsRequested """ From e9758a75233909d73af79a57b4efd13cc33de160 Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 7 May 2026 11:15:08 -0600 Subject: [PATCH 13/14] chore: remove entry from changelod.d --- .../20260430_193324_javier.ontiveros_instructor_app_tabs.rst | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst diff --git a/changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst b/changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst deleted file mode 100644 index b9fa335d..00000000 --- a/changelog.d/20260430_193324_javier.ontiveros_instructor_app_tabs.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added -~~~~~ -* Added new ``InstructorDashboardTabsRequested`` filter to allow for dynamic generation of instructor dashboard tabs. (by @holaontiveros) From 1dc13c85b2794f47dd4984749f3daecd0622f3b0 Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Thu, 7 May 2026 14:19:37 -0600 Subject: [PATCH 14/14] chore: update line too long --- openedx_filters/learning/filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index 7145bb69..8ca1f2b5 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -1297,8 +1297,8 @@ class InstructorDashboardTabsRequested(OpenEdxPublicFilter): to add, modify, or remove tabs from the instructor dashboard in the MFE. There's an old version of this filter (org.openedx.learning.instructor.dashboard.render.started.v1) - that applies to the legacy instructor dashboard, - but this new filter is specifically designed to work with the instructor dashboard app and its tabs generation process. + that applies to the legacy instructor dashboard, but this new filter is specifically designed + to work with the instructor dashboard app and its tabs generation process. Filter Type: org.openedx.learning.instructor.dashboard.tabs.requested.v1