diff --git a/lms/djangoapps/bulk_email/tests/test_course_optout.py b/lms/djangoapps/bulk_email/tests/test_course_optout.py index 9c4b986fe606..a71b7fd832e8 100644 --- a/lms/djangoapps/bulk_email/tests/test_course_optout.py +++ b/lms/djangoapps/bulk_email/tests/test_course_optout.py @@ -13,12 +13,14 @@ from edx_ace.message import Message from edx_ace.policy import PolicyResult from edx_ace.recipient import Recipient +from edx_toggles.toggles.testutils import override_waffle_flag from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory from lms.djangoapps.bulk_email.api import get_unsubscribed_link from lms.djangoapps.bulk_email.models import BulkEmailFlag from lms.djangoapps.bulk_email.policies import CourseEmailOptout +from lms.djangoapps.instructor.toggles import LEGACY_INSTRUCTOR_DASHBOARD from xmodule.modulestore.tests.django_utils import ( ModuleStoreTestCase, # lint-amnesty, pylint: disable=wrong-import-order ) @@ -26,6 +28,10 @@ @patch('lms.djangoapps.bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message', autospec=True)) # lint-amnesty, pylint: disable=line-too-long +# Tests for legacy views. When DEPR-38432 is picked up, these tests will require the following changes: +# Either remove or leave the specific parts that reference the legacy instructor dashboard, +# and remove the override_waffle_flag for LEGACY_INSTRUCTOR_DASHBOARD. +@override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) class TestOptoutCourseEmails(ModuleStoreTestCase): """ Test that optouts are referenced in sending course email. diff --git a/lms/djangoapps/bulk_email/tests/test_email.py b/lms/djangoapps/bulk_email/tests/test_email.py index 9f2c6779d0a2..96ec03d5058e 100644 --- a/lms/djangoapps/bulk_email/tests/test_email.py +++ b/lms/djangoapps/bulk_email/tests/test_email.py @@ -16,6 +16,7 @@ from django.test.utils import override_settings from django.urls import reverse from django.utils.translation import get_language +from edx_toggles.toggles.testutils import override_waffle_flag from markupsafe import escape from common.djangoapps.course_modes.models import CourseMode @@ -29,6 +30,7 @@ ) from lms.djangoapps.bulk_email.messages import ACEEmail from lms.djangoapps.bulk_email.tasks import _get_course_email_context, _get_source_address +from lms.djangoapps.instructor.toggles import LEGACY_INSTRUCTOR_DASHBOARD from lms.djangoapps.instructor_task.subtasks import update_subtask_status from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort from openedx.core.djangoapps.course_groups.models import CourseCohort @@ -62,6 +64,10 @@ def mock_update_subtask_status(entry_id, current_task_id, new_subtask_status): return mock_update_subtask_status +# Tests for legacy views. When DEPR-38432 is picked up, these tests will require the following changes: +# Either remove or leave the specific parts that reference the legacy instructor dashboard, +# and remove the override_waffle_flag for LEGACY_INSTRUCTOR_DASHBOARD. +@override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) class EmailSendFromDashboardTestCase(SharedModuleStoreTestCase): """ Test that emails send correctly. diff --git a/lms/djangoapps/bulk_email/tests/test_signals.py b/lms/djangoapps/bulk_email/tests/test_signals.py index 6089f117f819..abb299ad30f0 100644 --- a/lms/djangoapps/bulk_email/tests/test_signals.py +++ b/lms/djangoapps/bulk_email/tests/test_signals.py @@ -9,12 +9,14 @@ from django.core import mail from django.core.management import call_command from django.urls import reverse +from edx_toggles.toggles.testutils import override_waffle_flag from opaque_keys.edx.keys import CourseKey from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory from lms.djangoapps.bulk_email.models import BulkEmailFlag, Optout from lms.djangoapps.bulk_email.signals import force_optout_all +from lms.djangoapps.instructor.toggles import LEGACY_INSTRUCTOR_DASHBOARD from xmodule.modulestore.tests.django_utils import ( ModuleStoreTestCase, # lint-amnesty, pylint: disable=wrong-import-order ) @@ -22,6 +24,10 @@ @patch('lms.djangoapps.bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message', autospec=True)) # lint-amnesty, pylint: disable=line-too-long +# Tests for legacy views. When DEPR-38432 is picked up, these tests will require the following changes: +# Either remove or leave the specific parts that reference the legacy instructor dashboard, +# and remove the override_waffle_flag for LEGACY_INSTRUCTOR_DASHBOARD. +@override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) class TestOptoutCourseEmailsBySignal(ModuleStoreTestCase): """ Tests that the force_optout_all signal receiver opts the user out of course emails diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 8e3df22d56a1..9a412526f276 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -8,6 +8,7 @@ import pytz from django.urls import reverse +from edx_toggles.toggles.testutils import override_waffle_flag from common.djangoapps.student.tests.factories import ( BetaTesterFactory, @@ -21,6 +22,7 @@ ) from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.tests.helpers import CourseAccessTestMixin, LoginEnrollmentTestCase +from lms.djangoapps.instructor.toggles import LEGACY_INSTRUCTOR_DASHBOARD from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -92,7 +94,11 @@ def _check_staff(self, course): for index in range(len(course.textbooks)) ]) for url in urls: - self.assert_request_status_code(200, url) + # Instructor dashboard returns 302 (MFE redirect) by default + if 'instructor' in url: + self.assert_request_status_code(302, url) + else: + self.assert_request_status_code(200, url) # The student progress tab is not accessible to a student # before launch, so the instructor view-as-student feature @@ -109,6 +115,58 @@ def _check_staff(self, course): ) self.assert_request_status_code(302, url) + def _check_staff_legacy(self, course): + """ + Check that access is right for staff in course with legacy instructor dashboard enabled. + """ + names = ['about_course', 'instructor_dashboard', 'progress'] + urls = self._reverse_urls(names, course) + urls.extend([ + reverse('book', kwargs={'course_id': str(course.id), + 'book_index': index}) + for index in range(len(course.textbooks)) + ]) + for url in urls: + # With legacy flag enabled, all URLs return 200 (instructor dashboard skips MFE redirect) + self.assert_request_status_code(200, url) + + # The student progress tab behavior is affected by legacy flag in normal scenarios + url = reverse( + 'student_progress', + kwargs={ + 'course_id': str(course.id), + 'student_id': self.enrolled_user.id, + } + ) + self.assert_request_status_code(200, url) + + def _check_staff_legacy_dark_launch(self, course): + """ + Check staff access during dark launch with legacy instructor dashboard enabled. + In dark launch scenarios, student progress URL still returns 302 even with legacy flag. + """ + names = ['about_course', 'instructor_dashboard', 'progress'] + urls = self._reverse_urls(names, course) + urls.extend([ + reverse('book', kwargs={'course_id': str(course.id), + 'book_index': index}) + for index in range(len(course.textbooks)) + ]) + for url in urls: + # With legacy flag enabled, all URLs return 200 (instructor dashboard skips MFE redirect) + self.assert_request_status_code(200, url) + + # In dark launch scenarios, student progress URL still returns 302 even with legacy flag + # because course access restrictions take precedence + url = reverse( + 'student_progress', + kwargs={ + 'course_id': str(course.id), + 'student_id': self.enrolled_user.id, + } + ) + self.assert_request_status_code(302, url) + def login(self, user): # lint-amnesty, pylint: disable=arguments-differ return super().login(user.email, self.TEST_PASSWORD) @@ -186,7 +244,7 @@ def test_staff_course_access(self): # Now should be able to get to self.course, but not self.test_course url = reverse('instructor_dashboard', kwargs={'course_id': str(self.course.id)}) - self.assert_request_status_code(200, url) + self.assert_request_status_code(302, url) url = reverse('instructor_dashboard', kwargs={'course_id': str(self.test_course.id)}) self.assert_request_status_code(404, url) @@ -200,7 +258,7 @@ def test_instructor_course_access(self): # Now should be able to get to self.course, but not self.test_course url = reverse('instructor_dashboard', kwargs={'course_id': str(self.course.id)}) - self.assert_request_status_code(200, url) + self.assert_request_status_code(302, url) url = reverse('instructor_dashboard', kwargs={'course_id': str(self.test_course.id)}) self.assert_request_status_code(404, url) @@ -212,10 +270,10 @@ def test_org_staff_access(self): """ self.login(self.org_staff_user) url = reverse('instructor_dashboard', kwargs={'course_id': str(self.course.id)}) - self.assert_request_status_code(200, url) + self.assert_request_status_code(302, url) url = reverse('instructor_dashboard', kwargs={'course_id': str(self.test_course.id)}) - self.assert_request_status_code(200, url) + self.assert_request_status_code(302, url) url = reverse('instructor_dashboard', kwargs={'course_id': str(self.other_org_course.id)}) self.assert_request_status_code(404, url) @@ -227,10 +285,10 @@ def test_org_instructor_access(self): """ self.login(self.org_instructor_user) url = reverse('instructor_dashboard', kwargs={'course_id': str(self.course.id)}) - self.assert_request_status_code(200, url) + self.assert_request_status_code(302, url) url = reverse('instructor_dashboard', kwargs={'course_id': str(self.test_course.id)}) - self.assert_request_status_code(200, url) + self.assert_request_status_code(302, url) url = reverse('instructor_dashboard', kwargs={'course_id': str(self.other_org_course.id)}) self.assert_request_status_code(404, url) @@ -242,12 +300,94 @@ def test_global_staff_access(self): self.login(self.global_staff_user) # and now should be able to load both + urls = [reverse('instructor_dashboard', kwargs={'course_id': str(self.course.id)}), + reverse('instructor_dashboard', kwargs={'course_id': str(self.test_course.id)})] + + for url in urls: + self.assert_request_status_code(302, url) + + # Legacy instructor dashboard tests (with waffle flag enabled, expect 200 responses) + + @override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) + def test_staff_course_access_legacy(self): + """ + Verify staff can load the legacy instructor dashboard (expects 200 response). + """ + self.login(self.staff_user) + + url = reverse('instructor_dashboard', kwargs={'course_id': str(self.course.id)}) + self.assert_request_status_code(200, url) + + url = reverse('instructor_dashboard', kwargs={'course_id': str(self.test_course.id)}) + self.assert_request_status_code(404, url) + + @override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) + def test_instructor_course_access_legacy(self): + """ + Verify instructor can load the legacy instructor dashboard (expects 200 response). + """ + self.login(self.instructor_user) + + url = reverse('instructor_dashboard', kwargs={'course_id': str(self.course.id)}) + self.assert_request_status_code(200, url) + + url = reverse('instructor_dashboard', kwargs={'course_id': str(self.test_course.id)}) + self.assert_request_status_code(404, url) + + @override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) + def test_org_staff_access_legacy(self): + """ + Verify org staff can load the legacy instructor dashboard (expects 200 response). + """ + self.login(self.org_staff_user) + url = reverse('instructor_dashboard', kwargs={'course_id': str(self.course.id)}) + self.assert_request_status_code(200, url) + + url = reverse('instructor_dashboard', kwargs={'course_id': str(self.test_course.id)}) + self.assert_request_status_code(200, url) + + url = reverse('instructor_dashboard', kwargs={'course_id': str(self.other_org_course.id)}) + self.assert_request_status_code(404, url) + + @override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) + def test_org_instructor_access_legacy(self): + """ + Verify org instructor can load the legacy instructor dashboard (expects 200 response). + """ + self.login(self.org_instructor_user) + url = reverse('instructor_dashboard', kwargs={'course_id': str(self.course.id)}) + self.assert_request_status_code(200, url) + + url = reverse('instructor_dashboard', kwargs={'course_id': str(self.test_course.id)}) + self.assert_request_status_code(200, url) + + url = reverse('instructor_dashboard', kwargs={'course_id': str(self.other_org_course.id)}) + self.assert_request_status_code(404, url) + + @override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) + def test_global_staff_access_legacy(self): + """ + Verify the global staff user can access the legacy instructor dashboard (expects 200 response). + """ + self.login(self.global_staff_user) + urls = [reverse('instructor_dashboard', kwargs={'course_id': str(self.course.id)}), reverse('instructor_dashboard', kwargs={'course_id': str(self.test_course.id)})] for url in urls: self.assert_request_status_code(200, url) + @override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) + def test_staff_method_legacy(self): + """ + Test the _check_staff_legacy helper method with legacy flag enabled (expects 200 response). + """ + self.login(self.staff_user) + self.enroll(self.course, True) + + # Test the _check_staff_legacy method which includes instructor dashboard checks + self._check_staff_legacy(self.course) + @patch.dict('lms.djangoapps.courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False}) def test_dark_launch_enrolled_student(self): """ @@ -355,6 +495,52 @@ def test_enrollment_period(self): self.login(self.global_staff_user) assert self.enroll(self.course) + @override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) + @patch.dict('lms.djangoapps.courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False}) + def test_dark_launch_instructor_legacy(self): + """ + Make sure that before course start instructors can access the + page for their course with legacy instructor dashboard enabled. + """ + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + self.course.start = tomorrow + self.test_course.start = tomorrow + self.course = self.update_course(self.course, self.user.id) + self.test_course = self.update_course(self.test_course, self.user.id) + + self.login(self.instructor_user) + # Enroll in the classes---can't see courseware otherwise. + self.enroll(self.course, True) + self.enroll(self.test_course, True) + + # should now be able to get to everything for self.course + self._check_staff_legacy_dark_launch(self.course) + self._check_non_staff_dark(self.test_course) + + @override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) + @patch.dict('lms.djangoapps.courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False}) + def test_dark_launch_global_staff_legacy(self): + """ + Make sure that before course start staff can access + course pages with legacy instructor dashboard enabled. + """ + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + + self.course.start = tomorrow + self.test_course.start = tomorrow + self.course = self.update_course(self.course, self.user.id) + self.test_course = self.update_course(self.test_course, self.user.id) + + self.login(self.global_staff_user) + self.enroll(self.course, True) + self.enroll(self.test_course, True) + + # and now should be able to load both + self._check_staff_legacy_dark_launch(self.course) + self._check_staff_legacy_dark_launch(self.test_course) + class TestBetatesterAccess(ModuleStoreTestCase, CourseAccessTestMixin): """ diff --git a/lms/djangoapps/instructor/tests/test_certificates.py b/lms/djangoapps/instructor/tests/test_certificates.py index 9b0299e6e9fe..170d4f9fb89a 100644 --- a/lms/djangoapps/instructor/tests/test_certificates.py +++ b/lms/djangoapps/instructor/tests/test_certificates.py @@ -14,6 +14,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.test.utils import override_settings from django.urls import reverse +from edx_toggles.toggles.testutils import override_waffle_flag from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.tests.factories import GlobalStaffFactory, InstructorFactory, UserFactory @@ -24,6 +25,7 @@ CertificateInvalidationFactory, GeneratedCertificateFactory, ) +from lms.djangoapps.instructor.toggles import LEGACY_INSTRUCTOR_DASHBOARD from xmodule.modulestore.tests.django_utils import ( SharedModuleStoreTestCase, # lint-amnesty, pylint: disable=wrong-import-order ) @@ -31,6 +33,10 @@ @ddt.ddt +# Tests for legacy views. When DEPR-38432 is picked up, these tests will require the following changes: +# Either remove or leave the specific parts that reference the legacy instructor dashboard, +# and remove the override_waffle_flag for LEGACY_INSTRUCTOR_DASHBOARD. +@override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) class CertificateTaskViewTests(SharedModuleStoreTestCase): """Tests for the certificate panel of the instructor dash. """ @@ -153,6 +159,10 @@ def test_certificate_regeneration_status_handling(self, cert_status, expected_st @ddt.ddt +# Tests for legacy views. When DEPR-XXXX is picked up, these tests will require the following changes: +# Either remove or leave the specific parts that reference the legacy instructor dashboard, +# and remove the override_waffle_flag for LEGACY_INSTRUCTOR_DASHBOARD. +@override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) class CertificatesInstructorDashTest(SharedModuleStoreTestCase): """Tests for the certificate panel of the instructor dash. """ @@ -336,6 +346,10 @@ def _assert_enable_certs_button(self, is_enabled): @override_settings(CERT_QUEUE='certificates') @ddt.ddt +# Tests for legacy views. When DEPR-XXXX is picked up, these tests will require the following changes: +# Either remove or leave the specific parts that reference the legacy instructor dashboard, +# and remove the override_waffle_flag for LEGACY_INSTRUCTOR_DASHBOARD. +@override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) class CertificatesInstructorApiTest(SharedModuleStoreTestCase): """Tests for the certificates end-points in the instructor dash API. """ @classmethod diff --git a/lms/djangoapps/instructor/tests/test_email.py b/lms/djangoapps/instructor/tests/test_email.py index 07fc9c9410d1..97360cb67737 100644 --- a/lms/djangoapps/instructor/tests/test_email.py +++ b/lms/djangoapps/instructor/tests/test_email.py @@ -7,6 +7,7 @@ from django.urls import reverse +from edx_toggles.toggles.testutils import override_waffle_flag from opaque_keys.edx.keys import CourseKey from common.djangoapps.student.tests.factories import AdminFactory @@ -16,6 +17,7 @@ is_bulk_email_enabled_for_course, is_bulk_email_feature_enabled, ) +from lms.djangoapps.instructor.toggles import LEGACY_INSTRUCTOR_DASHBOARD from xmodule.modulestore.tests.django_utils import ( # lint-amnesty, pylint: disable=wrong-import-order TEST_DATA_MIXED_MODULESTORE, SharedModuleStoreTestCase, @@ -23,6 +25,10 @@ from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order +# Tests for legacy views. When DEPR-38432 is picked up, these tests will require the following changes: +# Either remove or leave the specific parts that reference the legacy instructor dashboard, +# and remove the override_waffle_flag for LEGACY_INSTRUCTOR_DASHBOARD. +@override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) class TestNewInstructorDashboardEmailViewMongoBacked(SharedModuleStoreTestCase): """ Check for email view on the new instructor dashboard diff --git a/lms/djangoapps/instructor/tests/test_filters.py b/lms/djangoapps/instructor/tests/test_filters.py index 4f1324531a59..3c143e47497f 100644 --- a/lms/djangoapps/instructor/tests/test_filters.py +++ b/lms/djangoapps/instructor/tests/test_filters.py @@ -6,11 +6,13 @@ from django.http import HttpResponse from django.test import override_settings from django.urls import reverse +from edx_toggles.toggles.testutils import override_waffle_flag from openedx_filters import PipelineStep from openedx_filters.learning.filters import InstructorDashboardRenderStarted from rest_framework import status from common.djangoapps.student.tests.factories import AdminFactory, CourseAccessRoleFactory +from lms.djangoapps.instructor.toggles import LEGACY_INSTRUCTOR_DASHBOARD from openedx.core.djangolib.testing.utils import skip_unless_lms from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -86,6 +88,10 @@ def run_filter(self, context, template_name): # pylint: disable=arguments-diffe @skip_unless_lms +# Tests for legacy views. When DEPR-38432 is picked up, these tests will require the following changes: +# Either remove or leave the specific parts that reference the legacy instructor dashboard, +# and remove the override_waffle_flag for LEGACY_INSTRUCTOR_DASHBOARD. +@override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) class InstructorDashboardFiltersTest(ModuleStoreTestCase): """ Tests for the Open edX Filters associated with the instructor dashboard rendering process. diff --git a/lms/djangoapps/instructor/tests/test_proctoring.py b/lms/djangoapps/instructor/tests/test_proctoring.py index 173e2a8a7c05..8df3f64bfe08 100644 --- a/lms/djangoapps/instructor/tests/test_proctoring.py +++ b/lms/djangoapps/instructor/tests/test_proctoring.py @@ -10,9 +10,11 @@ from django.urls import reverse from edx_proctoring.api import create_exam from edx_proctoring.backends.tests.test_backend import TestBackendProvider +from edx_toggles.toggles.testutils import override_waffle_flag from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole from common.djangoapps.student.tests.factories import AdminFactory +from lms.djangoapps.instructor.toggles import LEGACY_INSTRUCTOR_DASHBOARD from xmodule.modulestore.tests.django_utils import ( SharedModuleStoreTestCase, # lint-amnesty, pylint: disable=wrong-import-order ) @@ -21,6 +23,10 @@ @patch.dict(settings.FEATURES, {'ENABLE_SPECIAL_EXAMS': True}) @ddt.ddt +# Tests for legacy views. When DEPR-38432 is picked up, these tests will require the following changes: +# Either remove or leave the specific parts that reference the legacy instructor dashboard, +# and remove the override_waffle_flag for LEGACY_INSTRUCTOR_DASHBOARD. +@override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) class TestProctoringDashboardViews(SharedModuleStoreTestCase): """ Check for Proctoring view on the new instructor dashboard diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py index 8e616423929e..deac31a182f0 100644 --- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py +++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py @@ -32,7 +32,7 @@ from lms.djangoapps.courseware.tests.factories import StudentModuleFactory from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from lms.djangoapps.grades.config.waffle import WRITABLE_GRADEBOOK -from lms.djangoapps.instructor.toggles import DATA_DOWNLOAD_V2 +from lms.djangoapps.instructor.toggles import DATA_DOWNLOAD_V2, LEGACY_INSTRUCTOR_DASHBOARD from lms.djangoapps.instructor.views.gradebook_api import calculate_page_info from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted from openedx.core.djangoapps.discussions.config.waffle import ( @@ -60,6 +60,10 @@ def intercept_renderer(path, context): @ddt.ddt +# Tests for legacy views. When DEPR-38432 is picked up, these tests will require the following changes: +# Either remove or leave the specific parts that reference the legacy instructor dashboard, +# and remove the override_waffle_flag for LEGACY_INSTRUCTOR_DASHBOARD. +@override_waffle_flag(LEGACY_INSTRUCTOR_DASHBOARD, active=True) class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssTestMixin): """ Tests for the instructor dashboard (not legacy). diff --git a/lms/djangoapps/instructor/toggles.py b/lms/djangoapps/instructor/toggles.py index 340079868722..03f9094c021c 100644 --- a/lms/djangoapps/instructor/toggles.py +++ b/lms/djangoapps/instructor/toggles.py @@ -28,6 +28,18 @@ # .. toggle_tickets: PROD-1740 OPTIMISED_IS_SMALL_COURSE = WaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.optimised_is_small_course', __name__) +# .. toggle_name: instructor.legacy_instructor_dashboard +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: Waffle flag to enable the legacy instructor experience +# .. toggle_use_cases: opt_out, temporary +# .. toggle_creation_date: 2026-04-20 +# .. toggle_target_removal_date: 2026-11-01 +# .. toggle_tickets: https://github.com/openedx/openedx-platform/issues/38432 +LEGACY_INSTRUCTOR_DASHBOARD = WaffleFlag( + f'{WAFFLE_FLAG_NAMESPACE}.legacy_instructor_dashboard', __name__ +) + def data_download_v2_is_enabled(): """ @@ -38,3 +50,10 @@ def data_download_v2_is_enabled(): def use_optimised_is_small_course(): return OPTIMISED_IS_SMALL_COURSE.is_enabled() + + +def legacy_instructor_dashboard(): + """ + Check if legacy instructor dashboard experience is enabled. + """ + return LEGACY_INSTRUCTOR_DASHBOARD.is_enabled() diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 70f9b8ef00ed..8388e3d67338 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -10,6 +10,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.http import Http404, HttpResponseRedirect, HttpResponseServerError +from django.shortcuts import redirect from django.urls import reverse from django.utils.html import escape from django.utils.translation import gettext as _ @@ -58,7 +59,7 @@ from xmodule.tabs import CourseTab # lint-amnesty, pylint: disable=wrong-import-order from .. import permissions -from ..toggles import data_download_v2_is_enabled +from ..toggles import data_download_v2_is_enabled, legacy_instructor_dashboard from .tools import get_units_with_due_date, title_or_url log = logging.getLogger(__name__) @@ -144,6 +145,13 @@ def instructor_dashboard_2(request, course_id): # lint-amnesty, pylint: disable if not request.user.has_perm(permissions.VIEW_DASHBOARD, course_key): raise Http404() + # With new instructor dashboard we need to redirect them to it instead of rendering the old one, + # but we still want to check if they have access to view the dashboard before redirecting. + if not legacy_instructor_dashboard(): + return redirect(get_instructor_dashboard_url(course_key)) + + # WHEN DEPR-38432 is picked up the legacy dashboard may be removed + sections = [] if access['staff']: sections_content = [ @@ -817,3 +825,11 @@ def is_ecommerce_course(course_key): """ sku_count = len([mode.sku for mode in CourseMode.modes_for_course(course_key) if mode.sku]) return sku_count > 0 + + +def get_instructor_dashboard_url(course_key: CourseKey) -> str: + """ + Gets instructor microfrontend URL for the current course locator. + """ + mfe_base_url = settings.INSTRUCTOR_MICROFRONTEND_URL + return f'{mfe_base_url}/{course_key}/course_info'