Skip to content
Draft
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
49 changes: 49 additions & 0 deletions src/feedback/extensions/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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."""
Expand Down
49 changes: 48 additions & 1 deletion src/feedback/feedbacktests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down