diff --git a/lms/djangoapps/instructor/tests/test_api_v2.py b/lms/djangoapps/instructor/tests/test_api_v2.py index 7db49a4651d1..27ffcb0efb2e 100644 --- a/lms/djangoapps/instructor/tests/test_api_v2.py +++ b/lms/djangoapps/instructor/tests/test_api_v2.py @@ -396,7 +396,7 @@ def test_instructor_sees_all_basic_tabs(self): tab_ids = [tab['tab_id'] for tab in instructor_tabs] expected_tabs = ['course_info', 'enrollments', 'course_team', 'grading', 'cohorts'] - assert tab_ids == expected_tabs + assert set(tab_ids) == set(expected_tabs) def test_researcher_sees_all_basic_tabs(self): """ diff --git a/lms/djangoapps/instructor/tests/views/test_api_v2.py b/lms/djangoapps/instructor/tests/views/test_api_v2.py index e438b3583c12..427364fd2296 100644 --- a/lms/djangoapps/instructor/tests/views/test_api_v2.py +++ b/lms/djangoapps/instructor/tests/views/test_api_v2.py @@ -692,3 +692,39 @@ def test_override_rejects_negative_score(self): format='json', ) assert response.status_code == status.HTTP_400_BAD_REQUEST + + +class CourseMetadataViewTestCase(ModuleStoreTestCase): + """ + Tests for GET /api/instructor/v2/courses/{course_id} with InstructorDashboardTabsRequested filter. + """ + def setUp(self): + super().setUp() + self.client = APIClient() + self.course = CourseFactory.create() + self.instructor = InstructorFactory.create(course_key=self.course.id) + self.client.force_authenticate(user=self.instructor) + + def test_tabs_filter_adds_custom_tab(self): + """Test that a filter can add a custom tab to the tabs list.""" + custom_tab = { + "tab_id": "custom", + "title": "Custom Tab", + "url": "/custom/123", + "sort_order": 999, + } + # Patch the filter to add a custom tab + with patch( + "openedx_filters.learning.filters.InstructorDashboardTabsRequested.run_filter" + ) as mock_filter: + def filter_side_effect(tabs, user, course_key): + return tabs + [custom_tab] + mock_filter.side_effect = filter_side_effect + + url = reverse("instructor_api_v2:course_metadata", kwargs={"course_id": str(self.course.id)}) + response = self.client.get(url) + assert response.status_code == status.HTTP_200_OK + data = response.json() + tab_ids = [tab["tab_id"] for tab in data["tabs"]] + required_tabs = {"course_info", "custom"} + assert required_tabs.issubset(set(tab_ids)), f"Missing required tabs: {required_tabs - set(tab_ids)}" diff --git a/lms/djangoapps/instructor/views/serializers_v2.py b/lms/djangoapps/instructor/views/serializers_v2.py index 1f8d400a6c59..ca3284d6f89f 100644 --- a/lms/djangoapps/instructor/views/serializers_v2.py +++ b/lms/djangoapps/instructor/views/serializers_v2.py @@ -13,6 +13,7 @@ from django.utils.html import escape from django.utils.translation import gettext as _ from edx_when.api import is_enabled_for_course +from openedx_filters.learning.filters import InstructorDashboardTabsRequested from rest_framework import serializers from common.djangoapps.course_modes.models import CourseMode @@ -305,6 +306,19 @@ def get_tabs(self, data): 'sort_order': 110, }) + try: + # .. filter_implemented_name: InstructorDashboardTabsRequested + # .. filter_type: org.openedx.learning.instructor.dashboard.tabs.requested.v1 + filtered_tabs = InstructorDashboardTabsRequested.run_filter( + tabs=tabs, + user=request.user, + course_key=course_key + ) + return filtered_tabs if filtered_tabs is not None else tabs + except InstructorDashboardTabsRequested.PreventTabsGeneration as exc: + # Plugin provided custom tabs or prevented tab generation + custom_tabs = getattr(exc, 'tabs', []) + # We provide the tabs in a specific order based on how it was # historically presented in the frontend. The frontend can use # this info or choose to ignore the ordering. @@ -322,8 +336,7 @@ def get_tabs(self, data): 'special_exams', ] order_index = {tab: i for i, tab in enumerate(tabs_order)} - tabs = sorted(tabs, key=lambda x: order_index.get(x['tab_id'], float("inf"))) - return tabs + return sorted(custom_tabs, key=lambda x: order_index.get(x['tab_id'], float("inf"))) def get_course_id(self, data): """Get course ID as string.""" diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index b60565318e48..583767bfcc90 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -861,7 +861,7 @@ openedx-events==11.2.0 # openedx-authz # openedx-core # ora2 -openedx-filters==3.3.0 +openedx-filters==3.4.0 # via # -r requirements/edx/kernel.in # edx-enterprise diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index c6fa858b6356..348272da12e6 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1413,7 +1413,7 @@ openedx-events==11.2.0 # openedx-authz # openedx-core # ora2 -openedx-filters==3.3.0 +openedx-filters==3.4.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 0a5de9ac771c..07c9a84daedb 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1041,7 +1041,7 @@ openedx-events==11.2.0 # openedx-authz # openedx-core # ora2 -openedx-filters==3.3.0 +openedx-filters==3.4.0 # via # -r requirements/edx/base.txt # edx-enterprise diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index eb46087641e6..fb97fa43d7f6 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1081,7 +1081,7 @@ openedx-events==11.2.0 # openedx-authz # openedx-core # ora2 -openedx-filters==3.3.0 +openedx-filters==3.4.0 # via # -r requirements/edx/base.txt # edx-enterprise