Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------------
Expand All @@ -58,7 +60,7 @@ Changed
~~~~~~~

* Added GradeEventContextRequested filter

[3.1.0] - 2025-04-06
--------------------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added
~~~~~
* Added new ``InstructorDashboardTabsGenerated`` filter to allow for dynamic generation of instructor dashboard tabs. (by @holaontiveros)
2 changes: 1 addition & 1 deletion openedx_filters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
65 changes: 65 additions & 0 deletions openedx_filters/learning/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,71 @@ def run_filter(cls, readonly_fields: set, user: Any) -> tuple[set, Any]:
return (data["readonly_fields"], data["user"])


class InstructorDashboardTabsGenerated(OpenEdxPublicFilter):
Comment thread
holaontiveros marked this conversation as resolved.
Outdated
"""
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")
Comment thread
holaontiveros marked this conversation as resolved.
Outdated


class GradeEventContextRequested(OpenEdxPublicFilter):
"""
Filter used to enrich the context for grade events.
Expand Down
95 changes: 95 additions & 0 deletions openedx_filters/learning/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
GradeEventContextRequested,
IDVPageURLRequested,
InstructorDashboardRenderStarted,
InstructorDashboardTabsGenerated,
ORASubmissionViewRenderStarted,
RenderXBlockStarted,
ScheduleQuerySetRequested,
Expand Down Expand Up @@ -867,3 +868,97 @@ 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 tabs unchanged.

Expected behavior:
- The filter should return the tabs list unchanged.
"""
tabs = [
{"tab_id": "courseware", "title": "Course", "url": "/course/123", "sort_order": 0},
{"tab_id": "instructor", "title": "Instructor", "url": "/instructor/123", "sort_order": 1},
]
user = Mock()
course_key = Mock()

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)

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",
)

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},
]
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, "user": user, "course_key": course_key
}
result_tabs = InstructorDashboardTabsGenerated.run_filter(
tabs=tabs, user=user, course_key=course_key
)

self.assertEqual(result_tabs, modified_tabs)

@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())