From 0c09016753e5e5eaf480644af8fd91c179a7d162 Mon Sep 17 00:00:00 2001 From: farhan Date: Fri, 28 Aug 2026 20:31:35 +0500 Subject: [PATCH] feat: add Course Feedback tab to new instructor dashboard Register an InstructorDashboardTabsRequested (tabs.requested.v1) pipeline step so the Feedback tab appears on the new frontend-base instructor dashboard. The existing AddFeedbackTab step only hooks the legacy render.started.v1 filter, which the new dashboard never fires. Part 1 of 2: this adds the tab entry; rendering its content still requires a frontend route component registered in the org.openedx.frontend.slot.instructorDashboard.routes.v1 slot (follow-up). Refs: openedx/xblocks-extra#62 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/feedback/extensions/filters.py | 49 ++++++++++++++++++++++ src/feedback/feedbacktests/test_filters.py | 49 +++++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/feedback/extensions/filters.py b/src/feedback/extensions/filters.py index 3c07eb4..fb9b8b0 100644 --- a/src/feedback/extensions/filters.py +++ b/src/feedback/extensions/filters.py @@ -7,6 +7,7 @@ from crum import get_current_request from django.conf import settings from django.template import Context, Template +from django.utils.translation import gettext as _ from openedx_filters import PipelineStep from web_fragments.fragment import Fragment @@ -27,6 +28,54 @@ BLOCK_CATEGORY = "feedback" TEMPLATE_CATEGORY = "feedback_instructor" +# Tab identifier used by both the backend tab entry and the frontend route slot. +FEEDBACK_TAB_ID = "feedback" +# Placed after the platform-defined tabs (highest core sort_order is 110 for Special Exams). +FEEDBACK_TAB_SORT_ORDER = 120 + + +class AddFeedbackTabToInstructorDashboard(PipelineStep): + """ + Add the Course Feedback tab to the new (frontend-base) instructor dashboard. + + The legacy dashboard is extended via the ``...render.started.v1`` filter (see + :class:`AddFeedbackTab`), which appends a server-rendered section. The new + frontend-base instructor dashboard instead builds its navigation from the + ``org.openedx.learning.instructor.dashboard.tabs.requested.v1`` filter, so the + legacy step never runs there. This step registers the tab for that dashboard. + + The tab content is rendered by a frontend plugin registered in the + ``org.openedx.frontend.slot.instructorDashboard.routes.v1`` slot for the + ``feedback`` tab id; without that plugin the tab renders the "Page Not Found" + fallback. + """ + + def run_filter(self, tabs, user, course_key): # pylint: disable=unused-argument, arguments-differ + """ + Append the Course Feedback tab to the instructor dashboard tabs list. + + Args: + tabs (list): List of tab dicts (each with tab_id, title, url, sort_order). + user (User): The requesting user (unused, kept for filter signature). + course_key (CourseKey): Course key for the instructor dashboard. + + Returns: + dict: The (possibly) modified ``tabs`` list under the ``tabs`` key. + """ + if not settings.FEATURES.get("ENABLE_FEEDBACK_INSTRUCTOR_VIEW", False): + return {"tabs": tabs} + + tabs.append( + { + "tab_id": FEEDBACK_TAB_ID, + "title": _("Course Feedback"), + "url": f"/instructor-dashboard/{course_key}/{FEEDBACK_TAB_ID}", + "sort_order": FEEDBACK_TAB_SORT_ORDER, + } + ) + + return {"tabs": tabs} + class AddFeedbackTab(PipelineStep): """Add forum_notifier tab to instructor dashboard by adding a new context with feedback data.""" diff --git a/src/feedback/feedbacktests/test_filters.py b/src/feedback/feedbacktests/test_filters.py index 6ee112a..ec8f19d 100644 --- a/src/feedback/feedbacktests/test_filters.py +++ b/src/feedback/feedbacktests/test_filters.py @@ -7,7 +7,12 @@ from django.test.utils import override_settings -from feedback.extensions.filters import AddFeedbackTab, load_xblock_answers +from feedback.extensions.filters import ( + FEEDBACK_TAB_ID, + AddFeedbackTab, + AddFeedbackTabToInstructorDashboard, + load_xblock_answers, +) class TestFilters(TestCase): @@ -89,6 +94,48 @@ def test_run_filter_disable(self): self.assertEqual(context, new_context) + @override_settings(FEATURES={"ENABLE_FEEDBACK_INSTRUCTOR_VIEW": True}) + def test_tabs_filter_adds_feedback_tab(self): + """ + The tabs.requested filter adds a single feedback tab when the feature is enabled. + + Expected result: + - A tab with tab_id "feedback" is appended, carrying a course-scoped url. + """ + tab_filter = AddFeedbackTabToInstructorDashboard(filter_type=Mock(), running_pipeline=Mock()) + + result = tab_filter.run_filter(tabs=[], user=Mock(), course_key="course-v1:test+1+1") + + tabs = result["tabs"] + self.assertEqual(1, len(tabs)) + self.assertEqual(FEEDBACK_TAB_ID, tabs[0]["tab_id"]) + self.assertIn("course-v1:test+1+1", tabs[0]["url"]) + self.assertIn("sort_order", tabs[0]) + + @override_settings(FEATURES={"ENABLE_FEEDBACK_INSTRUCTOR_VIEW": True}) + def test_tabs_filter_preserves_existing_tabs(self): + """ + The tabs.requested filter appends to, rather than replaces, existing tabs. + """ + tab_filter = AddFeedbackTabToInstructorDashboard(filter_type=Mock(), running_pipeline=Mock()) + existing = [{"tab_id": "course_info", "title": "Course Info", "url": "/x", "sort_order": 10}] + + result = tab_filter.run_filter(tabs=existing, user=Mock(), course_key="course-v1:test+1+1") + + tab_ids = [tab["tab_id"] for tab in result["tabs"]] + self.assertEqual(["course_info", FEEDBACK_TAB_ID], tab_ids) + + @override_settings(FEATURES={"ENABLE_FEEDBACK_INSTRUCTOR_VIEW": False}) + def test_tabs_filter_disabled(self): + """ + The tabs.requested filter is a no-op when the feature flag is disabled. + """ + tab_filter = AddFeedbackTabToInstructorDashboard(filter_type=Mock(), running_pipeline=Mock()) + + result = tab_filter.run_filter(tabs=[], user=Mock(), course_key="course-v1:test+1+1") + + self.assertEqual([], result["tabs"]) + @patch("feedback.extensions.filters.load_single_xblock") def test_load_xblock_answers(self, load_single_xblock_mock): request_mock = Mock()