diff --git a/lms/djangoapps/instructor/tests/views/test_api_v2.py b/lms/djangoapps/instructor/tests/views/test_api_v2.py index e438b3583c12..a901f469f500 100644 --- a/lms/djangoapps/instructor/tests/views/test_api_v2.py +++ b/lms/djangoapps/instructor/tests/views/test_api_v2.py @@ -4,8 +4,8 @@ import json from textwrap import dedent from unittest.mock import MagicMock, patch +from unittest.mock import patch as patch_filter from uuid import uuid4 - from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient @@ -692,3 +692,37 @@ 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_filter("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 c345f7d67fd4..0d1d8596283d 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 b31b3126523f..487b820c1c74 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.1.0 +openedx-filters==3.3.0 # via # -r requirements/edx/kernel.in # edx-enterprise diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 10e05f216365..8bbf513e91b8 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.1.0 +openedx-filters==3.3.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 432599aac433..59bf1e31ff00 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.1.0 +openedx-filters==3.3.0 # via # -r requirements/edx/base.txt # edx-enterprise diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 3b000d1ca072..95edea1f9662 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.1.0 +openedx-filters==3.3.0 # via # -r requirements/edx/base.txt # edx-enterprise