Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 ``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
.. 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
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
74 changes: 74 additions & 0 deletions openedx_filters/learning/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -1174,6 +1175,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

Expand Down Expand Up @@ -1283,6 +1288,75 @@ 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.

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.requested.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.
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,
InstructorDashboardTabsRequested,
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 TestInstructorDashboardTabsRequested(TestCase):
"""
Test class to verify standard behavior of the InstructorDashboardTabsRequested filter.

You'll find test suites for:
- InstructorDashboardTabsRequested
"""

def test_run_filter_returns_unchanged_tabs_when_no_pipeline(self):
"""
Test InstructorDashboardTabsRequested 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 = InstructorDashboardTabsRequested.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(
InstructorDashboardTabsRequested.filter_type,
"org.openedx.learning.instructor.dashboard.tabs.requested.v1",
)

def test_run_filter_with_pipeline_returning_dict_with_tabs(self):
"""
Test InstructorDashboardTabsRequested 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 = InstructorDashboardTabsRequested.run_filter(
tabs=tabs, user=user, course_key=course_key
)

self.assertEqual(result_tabs, modified_tabs)

@data(
(
InstructorDashboardTabsRequested.PreventTabsGeneration,
{
"message": "Custom tabs provided by plugin",
"tabs": [{"tab_id": "custom", "title": "Custom", "url": "/custom", "sort_order": 0}],
}
),
(
InstructorDashboardTabsRequested.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())
Loading