diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 48ca6e9ac63b..49c2c090e2b5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2554,7 +2554,6 @@ paths: * `"empty"`: no start date is specified * pacing: Course pacing. Possible values: instructor, self * user_timezone: User's chosen timezone setting (or null for browser default) - * can_view_legacy_courseware: Indicates whether the user is able to see the legacy courseware view * user_has_passing_grade: Whether or not the effective user's grade is equal to or above the courses minimum passing grade * course_exit_page_is_active: Flag for the learning mfe on whether or not the course exit page should display diff --git a/lms/djangoapps/ccx/tests/test_views.py b/lms/djangoapps/ccx/tests/test_views.py index 8d717bcc1748..723fba2406c9 100644 --- a/lms/djangoapps/ccx/tests/test_views.py +++ b/lms/djangoapps/ccx/tests/test_views.py @@ -17,7 +17,6 @@ from django.urls import resolve, reverse from django.utils.translation import gettext as _ from edx_django_utils.cache import RequestCache -from edx_toggles.toggles.testutils import override_waffle_flag from opaque_keys.edx.keys import CourseKey from pytz import UTC from xmodule.modulestore import ModuleStoreEnum @@ -41,7 +40,6 @@ from lms.djangoapps.courseware.tests.factories import StudentModuleFactory from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from lms.djangoapps.courseware.testutils import FieldOverrideTestMixin -from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND from lms.djangoapps.discussion.django_comment_client.utils import has_forum_access from lms.djangoapps.grades.api import task_compute_all_grades_for_course from lms.djangoapps.instructor.access import allow_access, list_with_level @@ -1219,12 +1217,8 @@ def test_load_student_dashboard(self): assert response.status_code == 200 assert re.search('Test CCX', response.content.decode('utf-8')) - @override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) def test_load_courseware(self): self.client.login(username=self.student.username, password=self.student_password) - response = self.client.get(reverse('courseware_section', kwargs={ - 'course_id': str(self.ccx_course_key), - 'chapter': 'chapter_x', - 'section': 'sequential_x1', - })) + sequence_key = self.ccx_course_key.make_usage_key('sequential', 'sequential_x1') + response = self.client.get(reverse('render_xblock', args=[str(sequence_key)])) assert response.status_code == 200 diff --git a/lms/djangoapps/course_home_api/course_metadata/views.py b/lms/djangoapps/course_home_api/course_metadata/views.py index e57234fb54f6..b5e072878de3 100644 --- a/lms/djangoapps/course_home_api/course_metadata/views.py +++ b/lms/djangoapps/course_home_api/course_metadata/views.py @@ -21,7 +21,6 @@ from lms.djangoapps.courseware.courses import check_course_access from lms.djangoapps.courseware.masquerade import setup_masquerade from lms.djangoapps.courseware.tabs import get_course_tab_list -from lms.djangoapps.courseware.toggles import courseware_mfe_is_visible class CourseHomeMetadataView(RetrieveAPIView): @@ -102,12 +101,6 @@ def get(self, request, *args, **kwargs): enrollment = CourseEnrollment.get_enrollment(request.user, course_key_string) user_is_enrolled = bool(enrollment and enrollment.is_active) - can_load_courseware = courseware_mfe_is_visible( - course_key=course_key, - is_global_staff=original_user_is_global_staff, - is_course_staff=original_user_is_staff - ) - # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] @@ -133,7 +126,7 @@ def get(self, request, *args, **kwargs): 'is_self_paced': getattr(course, 'self_paced', False), 'is_enrolled': user_is_enrolled, 'course_access': load_access.to_json(), - 'can_load_courseware': can_load_courseware, + 'can_load_courseware': True, # can be removed once the MFE no longer references this field 'celebrations': celebrations, 'user_timezone': user_timezone, 'can_view_certificate': certificates_viewable_for_course(course), diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index 5247f71c3b0f..f5818f7734e7 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -7,8 +7,9 @@ import json from collections import OrderedDict from datetime import timedelta -from unittest.mock import Mock +from unittest.mock import Mock, patch +from django.conf import settings from django.contrib import messages from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.test import TestCase @@ -18,6 +19,7 @@ from xblock.field_data import DictFieldData from common.djangoapps.edxmako.shortcuts import render_to_string +from lms.djangoapps.courseware import access_utils from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link from lms.djangoapps.courseware.masquerade import MasqueradeView @@ -451,3 +453,11 @@ def get_context_dict_from_string(data): sorted(json.loads(cleaned_data['metadata']).items(), key=lambda t: t[0]) ) return cleaned_data + + +def set_preview_mode(preview_mode: bool): + """ + A decorator to force the preview mode on or off. + """ + hostname = settings.FEATURES.get('PREVIEW_LMS_BASE') if preview_mode else None + return patch.object(access_utils, 'get_current_request_hostname', new=lambda: hostname) diff --git a/lms/djangoapps/courseware/tests/test_entrance_exam.py b/lms/djangoapps/courseware/tests/test_entrance_exam.py index 0dd7976c115e..ee07b3aa5f1b 100644 --- a/lms/djangoapps/courseware/tests/test_entrance_exam.py +++ b/lms/djangoapps/courseware/tests/test_entrance_exam.py @@ -3,10 +3,9 @@ """ -from unittest.mock import Mock, patch +from unittest.mock import patch from crum import set_current_request from django.urls import reverse -from edx_toggles.toggles.testutils import override_waffle_flag from milestones.tests.utils import MilestonesTestCaseMixin from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -21,11 +20,9 @@ from lms.djangoapps.courseware.model_data import FieldDataCache from lms.djangoapps.courseware.module_render import get_module, handle_xblock_callback, toc_for_course from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase -from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND from openedx.core.djangolib.testing.utils import get_mock_request -from openedx.features.course_experience import DISABLE_COURSE_OUTLINE_PAGE_FLAG, DISABLE_UNIFIED_COURSE_TAB_FLAG from common.djangoapps.student.models import CourseEnrollment -from common.djangoapps.student.tests.factories import AnonymousUserFactory, CourseEnrollmentFactory +from common.djangoapps.student.tests.factories import AnonymousUserFactory from common.djangoapps.student.tests.factories import InstructorFactory from common.djangoapps.student.tests.factories import RequestFactoryNoCsrf from common.djangoapps.student.tests.factories import StaffFactory @@ -40,7 +37,6 @@ ) -@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) @patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True}) class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTestCaseMixin): """ @@ -216,54 +212,6 @@ def setUp(self): ] ) - def test_view_redirect_if_entrance_exam_required(self): - """ - Unit Test: if entrance exam is required. Should return a redirect. - """ - url = reverse('courseware', kwargs={'course_id': str(self.course.id)}) - expected_url = reverse('courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.entrance_exam.location.block_id, - 'section': self.exam_1.location.block_id - }) - resp = self.client.get(url) - self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200) - - @patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': False}) - def test_entrance_exam_content_absence(self): - """ - Unit Test: If entrance exam is not enabled then page should be redirected with chapter contents. - """ - url = reverse('courseware', kwargs={'course_id': str(self.course.id)}) - expected_url = reverse('courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.chapter.location.block_id, - 'section': self.welcome.location.block_id - }) - resp = self.client.get(url) - self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200) - resp = self.client.get(expected_url) - self.assertNotContains(resp, 'Exam Vertical - Unit 1') - - def test_entrance_exam_content_presence(self): - """ - Unit Test: If entrance exam is enabled then its content e.g. problems should be loaded and redirection will - occur with entrance exam contents. - """ - url = reverse('courseware', kwargs={'course_id': str(self.course.id)}) - expected_url = reverse('courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.entrance_exam.location.block_id, - 'section': self.exam_1.location.block_id - }) - resp = self.client.get(url) - self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200) - resp = self.client.get(expected_url) - self.assertContains(resp, 'Exam Vertical - Unit 1') - def test_get_entrance_exam_content(self): """ test get entrance exam content method @@ -279,95 +227,6 @@ def test_get_entrance_exam_content(self): assert exam_chapter is None assert user_has_passed_entrance_exam(self.request.user, self.course) - def test_entrance_exam_requirement_message(self): - """ - Unit Test: entrance exam requirement message should be present in response - """ - url = reverse( - 'courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.entrance_exam.location.block_id, - 'section': self.exam_1.location.block_id, - } - ) - resp = self.client.get(url) - self.assertContains(resp, 'To access course materials, you must score') - - def test_entrance_exam_requirement_message_with_correct_percentage(self): - """ - Unit Test: entrance exam requirement message should be present in response - and percentage of required score should be rounded as expected - """ - minimum_score_pct = 29 - self.course.entrance_exam_minimum_score_pct = float(minimum_score_pct) / 100 - self.update_course(self.course, self.request.user.id) - - # answer the problem so it results in only 20% correct. - answer_entrance_exam_problem(self.course, self.request, self.problem_1, value=1, max_value=5) - - url = reverse( - 'courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.entrance_exam.location.block_id, - 'section': self.exam_1.location.block_id - } - ) - resp = self.client.get(url) - self.assertContains( - resp, - f'To access course materials, you must score {minimum_score_pct}% or higher', - ) - assert 'Your current score is 20%.' in resp.content.decode(resp.charset) - - def test_entrance_exam_requirement_message_hidden(self): - """ - Unit Test: entrance exam message should not be present outside the context of entrance exam subsection. - """ - # Login as staff to avoid redirect to entrance exam - self.client.logout() - staff_user = StaffFactory(course_key=self.course.id) - self.client.login(username=staff_user.username, password='test') - CourseEnrollment.enroll(staff_user, self.course.id) - - url = reverse( - 'courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.chapter.location.block_id, - 'section': self.chapter_subsection.location.block_id - } - ) - resp = self.client.get(url) - assert resp.status_code == 200 - self.assertNotContains(resp, 'To access course materials, you must score') - self.assertNotContains(resp, 'You have passed the entrance exam.') - - # TODO: LEARNER-71: Do we need to adjust or remove this test? - @override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True) - def test_entrance_exam_passed_message_and_course_content(self): - """ - Unit Test: exam passing message and rest of the course section should be present - when user achieves the entrance exam milestone/pass the exam. - """ - url = reverse( - 'courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.entrance_exam.location.block_id, - 'section': self.exam_1.location.block_id - } - ) - - answer_entrance_exam_problem(self.course, self.request, self.problem_1) - answer_entrance_exam_problem(self.course, self.request, self.problem_2) - - resp = self.client.get(url) - self.assertNotContains(resp, 'To access course materials, you must score') - self.assertContains(resp, 'Your score is 100%. You have passed the entrance exam.') - self.assertContains(resp, 'Lesson 1') - def test_entrance_exam_gating(self): """ Unit Test: test_entrance_exam_gating @@ -427,71 +286,6 @@ def test_entrance_exam_gating_for_staff(self): for toc_section in self.expected_unlocked_toc: assert toc_section in unlocked_toc - def test_courseware_page_access_without_passing_entrance_exam(self): - """ - Test courseware access page without passing entrance exam - """ - url = reverse( - 'courseware_chapter', - kwargs={'course_id': str(self.course.id), 'chapter': self.chapter.url_name} - ) - response = self.client.get(url) - expected_url = reverse('courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.entrance_exam.location.block_id, - 'section': self.exam_1.location.block_id - }) - self.assertRedirects(response, expected_url, status_code=302, target_status_code=200) - - @override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True) - def test_courseinfo_page_access_without_passing_entrance_exam(self): - """ - Test courseware access page without passing entrance exam - """ - url = reverse('info', args=[str(self.course.id)]) - response = self.client.get(url) - redirect_url = reverse('courseware', args=[str(self.course.id)]) - self.assertRedirects(response, redirect_url, status_code=302, target_status_code=302) - response = self.client.get(redirect_url) - exam_url = response.get('Location') - self.assertRedirects(response, exam_url) - - @patch('lms.djangoapps.courseware.entrance_exams.get_entrance_exam_content', Mock(return_value=None)) - def test_courseware_page_access_after_passing_entrance_exam(self): - """ - Test courseware access page after passing entrance exam - """ - self._assert_chapter_loaded(self.course, self.chapter) - - @patch('common.djangoapps.util.milestones_helpers.get_required_content', Mock(return_value=['a value'])) - def test_courseware_page_access_with_staff_user_without_passing_entrance_exam(self): - """ - Test courseware access page without passing entrance exam but with staff user - """ - self.logout() - staff_user = StaffFactory.create(course_key=self.course.id) - self.login(staff_user.email, 'test') - CourseEnrollmentFactory(user=staff_user, course_id=self.course.id) - self._assert_chapter_loaded(self.course, self.chapter) - - def test_courseware_page_access_with_staff_user_after_passing_entrance_exam(self): - """ - Test courseware access page after passing entrance exam but with staff user - """ - self.logout() - staff_user = StaffFactory.create(course_key=self.course.id) - self.login(staff_user.email, 'test') - CourseEnrollmentFactory(user=staff_user, course_id=self.course.id) - self._assert_chapter_loaded(self.course, self.chapter) - - @patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': False}) - def test_courseware_page_access_when_entrance_exams_disabled(self): - """ - Test courseware page access when ENTRANCE_EXAMS feature is disabled - """ - self._assert_chapter_loaded(self.course, self.chapter) - def test_can_skip_entrance_exam_with_anonymous_user(self): """ Test can_skip_entrance_exam method with anonymous user @@ -540,17 +334,6 @@ def test_entrance_exam_xblock_response(self): assert response.status_code == 200 self.assertContains(response, 'entrance_exam_passed') - def _assert_chapter_loaded(self, course, chapter): - """ - Asserts courseware chapter load successfully. - """ - url = reverse( - 'courseware_chapter', - kwargs={'course_id': str(course.id), 'chapter': chapter.url_name} - ) - response = self.client.get(url) - assert response.status_code == 200 - def _return_table_of_contents(self): """ Returns table of content for the entrance exam specific to this test diff --git a/lms/djangoapps/courseware/tests/test_masquerade.py b/lms/djangoapps/courseware/tests/test_masquerade.py index 2e34ebed79d4..6c10cc38dbbe 100644 --- a/lms/djangoapps/courseware/tests/test_masquerade.py +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -26,9 +26,10 @@ setup_masquerade, ) -from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase, MasqueradeMixin, masquerade_as_group_member +from lms.djangoapps.courseware.tests.helpers import ( + LoginEnrollmentTestCase, MasqueradeMixin, masquerade_as_group_member, set_preview_mode, +) from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin -from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, set_user_preference @@ -42,7 +43,6 @@ from xmodule.partitions.partitions import Group, UserPartition # lint-amnesty, pylint: disable=wrong-import-order -@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) class MasqueradeTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase, MasqueradeMixin): """ Base class for masquerade tests that sets up a test course and enrolls a user in the course. @@ -189,32 +189,6 @@ def ensure_masquerade_as_group_member(self, partition_id, group_id): assert 200 == masquerade_as_group_member(self.test_user, self.course, partition_id, group_id) -class NormalStudentVisibilityTest(MasqueradeTestCase): - """ - Verify the course displays as expected for a "normal" student (to ensure test setup is correct). - """ - - def create_user(self): - """ - Creates a normal student user. - """ - return UserFactory() - - @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) - def test_staff_debug_not_visible(self): - """ - Tests that staff debug control is not present for a student. - """ - self.verify_staff_debug_present(False) - - @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) - def test_show_answer_not_visible(self): - """ - Tests that "Show Answer" is not visible for a student. - """ - self.verify_show_answer_present(False) - - class StaffMasqueradeTestCase(MasqueradeTestCase): """ Base class for tests of the masquerade behavior for a staff member. @@ -285,6 +259,7 @@ def testMasqueradeCohortAvailable(self, target, expected): assert is_target_available == expected +@set_preview_mode(True) class TestStaffMasqueradeAsStudent(StaffMasqueradeTestCase): """ Check for staff being able to masquerade as student. diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index a9e1857a3ad2..1dd954f5e0c9 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -15,19 +15,15 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from common.djangoapps.student.tests.factories import GlobalStaffFactory -from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase -from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND +from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase, set_preview_mode from openedx.features.course_experience import DISABLE_COURSE_OUTLINE_PAGE_FLAG -from common.djangoapps.student.tests.factories import UserFactory -@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) +@set_preview_mode(True) class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase): """ Check that navigation state is saved properly. """ - STUDENT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] - @classmethod def setUpClass(cls): # pylint: disable=super-method-not-called @@ -71,18 +67,11 @@ def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called display_name='pdf_textbooks_tab', default_tab='progress') - cls.staff_user = GlobalStaffFactory() - cls.user = UserFactory() + cls.user = GlobalStaffFactory(password='test') def setUp(self): super().setUp() - - # Create student accounts and activate them. - for i in range(len(self.STUDENT_INFO)): - email, password = self.STUDENT_INFO[i] - username = f'u{i}' - self.create_account(username, email, password) - self.activate_user(email) + self.login(self.user.email, 'test') def assertTabActive(self, tabname, response): ''' Check if the progress tab is active in the tab set ''' @@ -106,10 +95,6 @@ def test_chrome_settings(self): - Accordion enabled, or disabled - Navigation tabs enabled, disabled, or redirected ''' - email, password = self.STUDENT_INFO[0] - self.login(email, password) - self.enroll(self.course, True) - test_data = ( ('tabs', False, True), ('none', False, False), @@ -143,9 +128,6 @@ def test_inactive_session_timeout(self): Verify that an inactive session times out and redirects to the login page """ - email, password = self.STUDENT_INFO[0] - self.login(email, password) - # make sure we can access courseware immediately resp = self.client.get(reverse('dashboard')) assert resp.status_code == 200 @@ -163,11 +145,6 @@ def test_redirects_first_time(self): Verify that the first time we click on the courseware tab we are redirected to the 'Welcome' section. """ - email, password = self.STUDENT_INFO[0] - self.login(email, password) - self.enroll(self.course, True) - self.enroll(self.test_course, True) - resp = self.client.get(reverse('courseware', kwargs={'course_id': str(self.course.id)})) self.assertRedirects(resp, reverse( @@ -180,11 +157,6 @@ def test_redirects_second_time(self): Verify the accordion remembers we've already visited the Welcome section and redirects correspondingly. """ - email, password = self.STUDENT_INFO[0] - self.login(email, password) - self.enroll(self.course, True) - self.enroll(self.test_course, True) - section_url = reverse( 'courseware_section', kwargs={ @@ -203,11 +175,6 @@ def test_accordion_state(self): """ Verify the accordion remembers which chapter you were last viewing. """ - email, password = self.STUDENT_INFO[0] - self.login(email, password) - self.enroll(self.course, True) - self.enroll(self.test_course, True) - # Now we directly navigate to a section in a chapter other than 'Overview'. section_url = reverse( 'courseware_section', @@ -230,11 +197,6 @@ def test_accordion_state(self): # TODO: LEARNER-71: Do we need to adjust or remove this test? @override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True) def test_incomplete_course(self): - email = self.staff_user.email - password = "test" - self.login(email, password) - self.enroll(self.test_course, True) - test_course_id = str(self.test_course.id) url = reverse( @@ -284,11 +246,6 @@ def test_proctoring_js_includes(self): courseware pages if either the FEATURE flag is turned off or the course is not proctored enabled """ - - email, password = self.STUDENT_INFO[0] - self.login(email, password) - self.enroll(self.test_course_proctored, True) - test_course_id = str(self.test_course_proctored.id) with patch.dict(settings.FEATURES, {'ENABLE_SPECIAL_EXAMS': False}): diff --git a/lms/djangoapps/courseware/tests/test_split_module.py b/lms/djangoapps/courseware/tests/test_split_module.py index fb447dcca11e..2c06f03ec4f4 100644 --- a/lms/djangoapps/courseware/tests/test_split_module.py +++ b/lms/djangoapps/courseware/tests/test_split_module.py @@ -5,19 +5,16 @@ from unittest.mock import MagicMock from django.urls import reverse -from edx_toggles.toggles.testutils import override_waffle_flag from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.partitions.partitions import Group, UserPartition from lms.djangoapps.courseware.model_data import FieldDataCache from lms.djangoapps.courseware.module_render import get_module_for_descriptor -from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory -@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) class SplitTestBase(ModuleStoreTestCase): """ Sets up a basic course and user for split test testing. @@ -118,12 +115,7 @@ def _check_split_test(self, user_tag): value=str(user_tag) ) - resp = self.client.get(reverse( - 'courseware_section', - kwargs={'course_id': str(self.course.id), - 'chapter': self.chapter.url_name, - 'section': self.sequential.url_name} - )) + resp = self.client.get(reverse('render_xblock', args=[str(self.sequential.location)])) unicode_content = resp.content.decode(resp.charset) # Assert we see the proper icon in the top display diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index fa850550464c..36b8bca2f91b 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -2,14 +2,13 @@ Tests courseware views.py """ - import html import itertools import json import re from datetime import datetime, timedelta from unittest.mock import MagicMock, PropertyMock, create_autospec, patch -from urllib.parse import urlencode +from urllib.parse import quote, urlencode from uuid import uuid4 import ddt @@ -26,15 +25,12 @@ from django.urls import reverse, reverse_lazy from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch from freezegun import freeze_time -from markupsafe import escape -from milestones.tests.utils import MilestonesTestCaseMixin from opaque_keys.edx.keys import CourseKey, UsageKey from pytz import UTC from rest_framework import status from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Scope, String -from xmodule.course_module import COURSE_VISIBILITY_PRIVATE, COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE from xmodule.data import CertificatesDisplayBehaviors from xmodule.graders import ShowCorrectness from xmodule.modulestore import ModuleStoreEnum @@ -71,19 +67,13 @@ ) from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.utils import EcommerceService -from lms.djangoapps.courseware import access_utils from lms.djangoapps.courseware.access_utils import check_course_open_for_learner from lms.djangoapps.courseware.model_data import FieldDataCache, set_score from lms.djangoapps.courseware.module_render import get_module, handle_xblock_callback from lms.djangoapps.courseware.tests.factories import StudentModuleFactory -from lms.djangoapps.courseware.tests.helpers import get_expiration_banner_text +from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin, get_expiration_banner_text, set_preview_mode from lms.djangoapps.courseware.testutils import RenderXBlockTestMixin -from lms.djangoapps.courseware.toggles import ( - COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW, - COURSEWARE_OPTIMIZED_RENDER_XBLOCK, - COURSEWARE_USE_LEGACY_FRONTEND, - courseware_mfe_is_advertised -) +from lms.djangoapps.courseware.toggles import COURSEWARE_OPTIMIZED_RENDER_XBLOCK from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT from lms.djangoapps.grades.config.waffle import waffle_switch as grades_waffle_switch @@ -97,18 +87,15 @@ from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.core.djangolib.testing.utils import get_mock_request -from openedx.core.lib.gating import api as gating_api from openedx.core.lib.url_utils import quote_slashes from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from openedx.features.course_experience import ( - COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, DISABLE_COURSE_OUTLINE_PAGE_FLAG, DISABLE_UNIFIED_COURSE_TAB_FLAG, ) from openedx.features.course_experience.tests.views.helpers import add_course_mode from openedx.features.course_experience.url_helpers import ( - ExperienceOption, get_courseware_url, get_learning_mfe_home_url, make_learning_mfe_courseware_url @@ -121,61 +108,38 @@ FEATURES_WITH_DISABLE_HONOR_CERTIFICATE['DISABLE_HONOR_CERTIFICATES'] = True -def _set_mfe_flag(activate_mfe: bool): - """ - A decorator/contextmanager to force the base courseware MFE flag on or off. - """ - return override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=(not activate_mfe)) - - -def _set_preview_mfe_flag(active: bool): - """ - A decorator/contextmanager to force the courseware MFE educator preview flag on or off. - """ - return override_waffle_flag(COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW, active=active) - - @ddt.ddt class TestJumpTo(ModuleStoreTestCase): """ Check the jumpto link for a course. """ @ddt.data( - (False, None, False), # not provided -> Active experience - (False, "blarfingar", False), # nonsense -> Active experience - (False, "legacy", False), # "legacy" -> Legacy experience - (False, "new", True), # "new" -> MFE experience - (True, None, True), # not provided -> Active experience - (True, "blarfingar", True), # nonsense -> Active experience - (True, "legacy", False), # "legacy" -> Legacy experience - (True, "new", True), # "new" -> MFE experience + (True, False), # preview -> Legacy experience + (False, True), # no preview -> MFE experience ) @ddt.unpack - def test_jump_to_legacy_vs_mfe(self, activate_mfe, experience_param, expect_mfe): + def test_jump_to_legacy_vs_mfe(self, preview_mode, expect_mfe): """ - Test that jump_to and jump_to_id correctly choose which courseware - frontend to redirect to, taking into account the '?experience=' query - param. + Test that jump_to and jump_to_id correctly choose which courseware frontend to redirect to. - Will be removed along with DEPR-109. + Can be removed when the MFE supports a preview mode. """ course = CourseFactory.create() chapter = ItemFactory.create(category='chapter', parent_location=course.location) - querystring = f"experience={experience_param}" if experience_param else "" if expect_mfe: expected_url = f'http://learning-mfe/course/{course.id}/{chapter.location}' else: expected_url = f'/courses/{course.id}/courseware/{chapter.url_name}/' - jumpto_url = f'/courses/{course.id}/jump_to/{chapter.location}?{querystring}' - with _set_mfe_flag(activate_mfe): + jumpto_url = f'/courses/{course.id}/jump_to/{chapter.location}' + with set_preview_mode(preview_mode): response = self.client.get(jumpto_url) assert response.status_code == 302 # Check the response URL, but chop off the querystring; we don't care here. assert response.url.split('?')[0] == expected_url - jumpto_id_url = f'/courses/{course.id}/jump_to_id/{chapter.url_name}?{querystring}' - with _set_mfe_flag(activate_mfe): + jumpto_id_url = f'/courses/{course.id}/jump_to_id/{chapter.url_name}' + with set_preview_mode(preview_mode): response = self.client.get(jumpto_id_url) assert response.status_code == 302 # Check the response URL, but chop off the querystring; we don't care here. @@ -187,25 +151,25 @@ def test_jump_to_legacy_vs_mfe(self, activate_mfe, experience_param, expect_mfe) (True, ModuleStoreEnum.Type.split), ) @ddt.unpack - def test_jump_to_invalid_location(self, activate_mfe, store_type): + def test_jump_to_invalid_location(self, preview_mode, store_type): """Confirm that invalid locations redirect back to a general course URL""" with self.store.default_store(store_type): course = CourseFactory.create() location = course.id.make_usage_key(None, 'NoSuchPlace') expected_redirect_url = ( - f'http://learning-mfe/course/{course.id}' - ) if activate_mfe else ( f'/courses/{course.id}/courseware?' + urlencode({'activate_block_id': str(course.location)}) + ) if preview_mode else ( + f'http://learning-mfe/course/{course.id}' ) # This is fragile, but unfortunately the problem is that within the LMS we # can't use the reverse calls from the CMS jumpto_url = f'/courses/{course.id}/jump_to/{location}' - with _set_mfe_flag(activate_mfe): + with set_preview_mode(preview_mode): response = self.client.get(jumpto_url) assert response.status_code == 302 assert response.url == expected_redirect_url - @_set_mfe_flag(activate_mfe=False) + @set_preview_mode(True) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_jump_to_legacy_from_sequence(self, store_type): with self.store.default_store(store_type): @@ -220,7 +184,7 @@ def test_jump_to_legacy_from_sequence(self, store_type): response = self.client.get(jumpto_url) self.assertRedirects(response, expected_redirect_url, status_code=302, target_status_code=302) - @_set_mfe_flag(activate_mfe=True) + @set_preview_mode(False) def test_jump_to_mfe_from_sequence(self): course = CourseFactory.create() chapter = ItemFactory.create(category='chapter', parent_location=course.location) @@ -233,7 +197,7 @@ def test_jump_to_mfe_from_sequence(self): assert response.status_code == 302 assert response.url == expected_redirect_url - @_set_mfe_flag(activate_mfe=False) + @set_preview_mode(True) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_jump_to_legacy_from_module(self, store_type): with self.store.default_store(store_type): @@ -261,7 +225,7 @@ def test_jump_to_legacy_from_module(self, store_type): response = self.client.get(jumpto_url) self.assertRedirects(response, expected_redirect_url, status_code=302, target_status_code=302) - @_set_mfe_flag(activate_mfe=True) + @set_preview_mode(False) def test_jump_to_mfe_from_module(self): course = CourseFactory.create() chapter = ItemFactory.create(category='chapter', parent_location=course.location) @@ -289,7 +253,7 @@ def test_jump_to_mfe_from_module(self): # The new courseware experience does not support this sort of course structure; # it assumes a simple course->chapter->sequence->unit->component tree. - @_set_mfe_flag(activate_mfe=False) + @set_preview_mode(True) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_jump_to_legacy_from_nested_module(self, store_type): with self.store.default_store(store_type): @@ -319,15 +283,15 @@ def test_jump_to_legacy_from_nested_module(self, store_type): (True, ModuleStoreEnum.Type.split), ) @ddt.unpack - def test_jump_to_id_invalid_location(self, activate_mfe, store_type): + def test_jump_to_id_invalid_location(self, preview_mode, store_type): with self.store.default_store(store_type): course = CourseFactory.create() jumpto_url = f'/courses/{course.id}/jump_to/NoSuchPlace' - with _set_mfe_flag(activate_mfe): + with set_preview_mode(preview_mode): response = self.client.get(jumpto_url) assert response.status_code == 404 - @_set_mfe_flag(activate_mfe=False) + @set_preview_mode(True) @ddt.data( (ModuleStoreEnum.Type.mongo, False, '1'), (ModuleStoreEnum.Type.mongo, True, '2'), @@ -366,16 +330,14 @@ def test_jump_to_legacy_for_learner_with_staff_only_content(self, store_type, is } ) expected_url += "?{}".format(urlencode({'activate_block_id': str(staff_only_vertical.location)})) - assert expected_url == get_courseware_url(usage_key, request, ExperienceOption.LEGACY) + assert expected_url == get_courseware_url(usage_key, request) -@ddt.ddt -@_set_mfe_flag(activate_mfe=False) +@set_preview_mode(True) class IndexQueryTestCase(ModuleStoreTestCase): """ Tests for query count. """ - CREATE_USER = False NUM_PROBLEMS = 20 def test_index_query_counts(self): @@ -390,11 +352,10 @@ def test_index_query_counts(self): for _ in range(self.NUM_PROBLEMS): ItemFactory.create(category='problem', parent_location=vertical.location) - self.user = UserFactory() - self.client.login(username=self.user.username, password=TEST_PASSWORD) + self.client.login(username=self.user.username, password=self.user_password) CourseEnrollment.enroll(self.user, course.id) - with self.assertNumQueries(206, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST): + with self.assertNumQueries(203, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST): with check_mongo_calls(3): url = reverse( 'courseware_section', @@ -408,7 +369,10 @@ def test_index_query_counts(self): assert response.status_code == 200 -class BaseViewsTestCase(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring +class BaseViewsTestCase(ModuleStoreTestCase, MasqueradeMixin): + """Base class for courseware tests""" + CREATE_USER = False + def setUp(self): super().setUp() self.course = CourseFactory.create(display_name='teꜱᴛ course', run="Testing_course") @@ -497,16 +461,14 @@ def _get_urls(self): # lint-amnesty, pylint: disable=missing-function-docstring @ddt.ddt -@_set_mfe_flag(activate_mfe=False) -class ViewsTestCase(BaseViewsTestCase): +@set_preview_mode(True) +class CoursewareIndexTestCase(BaseViewsTestCase): """ - Tests for views.py methods. + Tests for the courseware index view, used for instructor previews. """ - YESTERDAY = 'yesterday' - DATES = { - YESTERDAY: datetime.now(UTC) - timedelta(days=1), - None: None, - } + def setUp(self): + super().setUp() + self._create_global_staff_user() # this view needs staff permission def test_index_success(self): response = self._verify_index_response() @@ -520,23 +482,20 @@ def test_index_success(self): self.assertNotContains(response, self.problem.location.replace(branch=None, version_guid=None)) self.assertContains(response, self.problem2.location.replace(branch=None, version_guid=None)) + @set_preview_mode(True) def test_index_nonexistent_chapter(self): self._verify_index_response(expected_response_code=404, chapter_name='non-existent') def test_index_nonexistent_chapter_masquerade(self): - with patch('lms.djangoapps.courseware.views.index.setup_masquerade') as patch_masquerade: - masquerade = MagicMock(role='student') - patch_masquerade.return_value = (masquerade, self.user) - self._verify_index_response(expected_response_code=302, chapter_name='non-existent') + self.update_masquerade(username=self.user.username) + self._verify_index_response(expected_response_code=302, chapter_name='non-existent') def test_index_nonexistent_section(self): self._verify_index_response(expected_response_code=404, section_name='non-existent') def test_index_nonexistent_section_masquerade(self): - with patch('lms.djangoapps.courseware.views.index.setup_masquerade') as patch_masquerade: - masquerade = MagicMock(role='student') - patch_masquerade.return_value = (masquerade, self.user) - self._verify_index_response(expected_response_code=302, section_name='non-existent') + self.update_masquerade(username=self.user.username) + self._verify_index_response(expected_response_code=302, section_name='non-existent') def _verify_index_response(self, expected_response_code=200, chapter_name=None, section_name=None): """ @@ -555,25 +514,76 @@ def _verify_index_response(self, expected_response_code=200, chapter_name=None, assert response.status_code == expected_response_code return response - def test_index_no_visible_section_in_chapter(self): + def test_get_redirect_url(self): + # test the course location + assert '/courses/{course_key}/courseware?{activate_block_id}'.format( + course_key=str(self.course_key), + activate_block_id=urlencode({'activate_block_id': str(self.course.location)}) + ) == get_courseware_url(self.course.location) + # test a section location + assert '/courses/{course_key}/courseware/Chapter_1/Sequential_1/?{activate_block_id}'.format( + course_key=str(self.course_key), + activate_block_id=urlencode({'activate_block_id': str(self.section.location)}) + ) == get_courseware_url(self.section.location) - # reload the chapter from the store so its children information is updated - self.chapter = self.store.get_item(self.chapter.location) + def test_index_invalid_position(self): + request_url = '/'.join([ + '/courses', + str(self.course.id), + 'courseware', + self.chapter.location.block_id, + self.section.location.block_id, + 'f' + ]) + response = self.client.get(request_url) + assert response.status_code == 404 - # disable the visibility of the sections in the chapter - for section in self.chapter.get_children(): - section.visible_to_staff_only = True - self.store.update_item(section, ModuleStoreEnum.UserID.test) + def test_unicode_handling_in_url(self): + url_parts = [ + '/courses', + str(self.course.id), + 'courseware', + self.chapter.location.block_id, + self.section.location.block_id, + '1' + ] + for idx, val in enumerate(url_parts): + url_parts_copy = url_parts[:] + url_parts_copy[idx] = val + 'χ' + request_url = '/'.join(url_parts_copy) + response = self.client.get(request_url) + assert response.status_code == 404 - url = reverse( - 'courseware_chapter', - kwargs={'course_id': str(self.course.id), - 'chapter': str(self.chapter.location.block_id)}, + # TODO: TNL-6387: Remove test + @override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True) + def test_accordion(self): + """ + This needs a response_context, which is not included in the render_accordion's main method + returning a render_to_string, so we will render via the courseware URL in order to include + the needed context + """ + response = self.client.get( + reverse('courseware', args=[str(self.course.id)]), + follow=True ) - response = self.client.get(url) - assert response.status_code == 200 - self.assertNotContains(response, 'Problem 1') - self.assertNotContains(response, 'Problem 2') + test_responses = [ + '

Sequential 1 current section

', + '

Sequential 2

' + ] + for test in test_responses: + self.assertContains(response, test) + + +@ddt.ddt +class ViewsTestCase(BaseViewsTestCase): + """ + Tests for views.py methods. + """ + YESTERDAY = 'yesterday' + DATES = { + YESTERDAY: datetime.now(UTC) - timedelta(days=1), + None: None, + } def test_mfe_link_from_about_page(self): """ @@ -582,7 +592,6 @@ def test_mfe_link_from_about_page(self): with self.store.default_store(ModuleStoreEnum.Type.split): course = CourseFactory.create() CourseEnrollment.enroll(self.user, course.id) - assert self.client.login(username=self.user.username, password=TEST_PASSWORD) response = self.client.get(reverse('about_course', args=[str(course.id)])) self.assertContains(response, get_learning_mfe_home_url(course_key=course.id, url_fragment='home')) @@ -592,14 +601,8 @@ def _create_url_for_enroll_staff(self): creates the courseware url and enroll staff url """ # create the _next parameter - courseware_url = reverse( - 'courseware_section', - kwargs={ - 'course_id': str(self.course_key), - 'chapter': str(self.chapter.location.block_id), - 'section': str(self.section.location.block_id), - } - ) + courseware_url = make_learning_mfe_courseware_url(self.course.id, self.chapter.location, self.section.location) + courseware_url = quote(courseware_url, safe=':/') # create the url for enroll_staff view enroll_url = "{enroll_url}?next={courseware_url}".format( enroll_url=reverse('enroll_staff', kwargs={'course_id': str(self.course.id)}), @@ -617,14 +620,11 @@ def test_enroll_staff_redirection(self, data, enrollment): """ self._create_global_staff_user() courseware_url, enroll_url = self._create_url_for_enroll_staff() - response = self.client.post(enroll_url, data=data, follow=True) - assert response.status_code == 200 + response = self.client.post(enroll_url, data=data) # we were redirected to our current location - assert 302 in response.redirect_chain[0] - assert len(response.redirect_chain) == 1 if enrollment: - self.assertRedirects(response, courseware_url) + self.assertRedirects(response, courseware_url, fetch_redirect_response=False) else: self.assertRedirects(response, f'/courses/{str(self.course_key)}/about') @@ -681,22 +681,6 @@ def test_user_groups(self): type(mock_user).is_authenticated = PropertyMock(return_value=False) assert views.user_groups(mock_user) == [] - def test_get_redirect_url(self): - # test the course location - assert '/courses/{course_key}/courseware?{activate_block_id}'.format( - course_key=str(self.course_key), - activate_block_id=urlencode({'activate_block_id': str(self.course.location)}) - ) == get_courseware_url( - self.course.location, experience=ExperienceOption.LEGACY - ) - # test a section location - assert '/courses/{course_key}/courseware/Chapter_1/Sequential_1/?{activate_block_id}'.format( - course_key=str(self.course_key), - activate_block_id=urlencode({'activate_block_id': str(self.section.location)}) - ) == get_courseware_url( - self.section.location, experience=ExperienceOption.LEGACY - ) - def test_invalid_course_id(self): response = self.client.get('/courses/MITx/3.091X/') assert response.status_code == 404 @@ -705,36 +689,6 @@ def test_incomplete_course_id(self): response = self.client.get('/courses/MITx/') assert response.status_code == 404 - def test_index_invalid_position(self): - request_url = '/'.join([ - '/courses', - str(self.course.id), - 'courseware', - self.chapter.location.block_id, - self.section.location.block_id, - 'f' - ]) - assert self.client.login(username=self.user.username, password=TEST_PASSWORD) - response = self.client.get(request_url) - assert response.status_code == 404 - - def test_unicode_handling_in_url(self): - url_parts = [ - '/courses', - str(self.course.id), - 'courseware', - self.chapter.location.block_id, - self.section.location.block_id, - '1' - ] - assert self.client.login(username=self.user.username, password=TEST_PASSWORD) - for idx, val in enumerate(url_parts): - url_parts_copy = url_parts[:] - url_parts_copy[idx] = val + 'χ' - request_url = '/'.join(url_parts_copy) - response = self.client.get(request_url) - assert response.status_code == 404 - def test_jump_to_invalid(self): # TODO add a test for invalid location # TODO add a test for no data * @@ -1063,25 +1017,6 @@ def test_bypass_course_info(self): response = self.client.get(reverse('info', args=[course_id]), HTTP_REFERER='foo') assert response.status_code == 200 - # TODO: TNL-6387: Remove test - @override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True) - def test_accordion(self): - """ - This needs a response_context, which is not included in the render_accordion's main method - returning a render_to_string, so we will render via the courseware URL in order to include - the needed context - """ - response = self.client.get( - reverse('courseware', args=[str(self.course.id)]), - follow=True - ) - test_responses = [ - '

Sequential 1 current section

', - '

Sequential 2

' - ] - for test in test_responses: - self.assertContains(response, test) - # Patching 'lms.djangoapps.courseware.views.views.get_programs' would be ideal, # but for some unknown reason that patch doesn't seem to be applied. @@ -1126,7 +1061,6 @@ def test_200(self, mock_cache): # setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly @override_settings(TIME_ZONE_DISPLAYED_FOR_DEADLINES="UTC") -@_set_mfe_flag(activate_mfe=False) class BaseDueDateTests(ModuleStoreTestCase): """ Base class that verifies that due dates are rendered correctly on a page @@ -1162,8 +1096,7 @@ def set_up_course(self, **course_kwargs): def setUp(self): super().setUp() - self.user = UserFactory.create() - assert self.client.login(username=self.user.username, password='test') + assert self.client.login(username=self.user.username, password=self.user_password) self.time_with_tz = "2013-09-18 11:30:00+00:00" @@ -1216,6 +1149,7 @@ def get_response(self, course): # TODO: LEARNER-71: Delete entire TestAccordionDueDate class +@set_preview_mode(True) class TestAccordionDueDate(BaseDueDateTests): """ Test that the accordion page displays due dates correctly @@ -2504,7 +2438,7 @@ def student_view(self, context): # pylint: disable=unused-argument @ddt.ddt -@_set_mfe_flag(activate_mfe=False) +@set_preview_mode(True) class TestIndexView(ModuleStoreTestCase): """ Tests of the courseware.views.index view. @@ -2514,8 +2448,6 @@ def test_student_state(self): """ Verify that saved student state is loaded for xblocks rendered in the index view. """ - user = UserFactory() - with modulestore().default_store(ModuleStoreEnum.Type.split): course = CourseFactory.create() chapter = ItemFactory.create(parent_location=course.location, category='chapter') @@ -2528,16 +2460,16 @@ def test_student_state(self): for item in (section, vertical, block): StudentModuleFactory.create( - student=user, + student=self.user, course_id=course.id, module_state_key=item.scope_ids.usage_id, state=json.dumps({'state': str(item.scope_ids.usage_id)}) ) CourseOverview.load_from_module_store(course.id) - CourseEnrollmentFactory(user=user, course_id=course.id) + CourseEnrollmentFactory(user=self.user, course_id=course.id) - assert self.client.login(username=user.username, password='test') + assert self.client.login(username=self.user.username, password=self.user_password) response = self.client.get( reverse( 'courseware_section', @@ -2553,8 +2485,6 @@ def test_student_state(self): @XBlock.register_temp_plugin(ActivateIDCheckerBlock, 'id_checker') def test_activate_block_id(self): - user = UserFactory() - course = CourseFactory.create() with self.store.bulk_operations(course.id): chapter = ItemFactory.create(parent=course, category='chapter') @@ -2563,9 +2493,9 @@ def test_activate_block_id(self): ItemFactory.create(parent=vertical, category='id_checker', display_name="ID Checker") CourseOverview.load_from_module_store(course.id) - CourseEnrollmentFactory(user=user, course_id=course.id) + CourseEnrollmentFactory(user=self.user, course_id=course.id) - assert self.client.login(username=user.username, password='test') + assert self.client.login(username=self.user.username, password=self.user_password) response = self.client.get( reverse( 'courseware_section', @@ -2578,83 +2508,6 @@ def test_activate_block_id(self): ) self.assertContains(response, "Activate Block ID: test_block_id") - @ddt.data( - [False, COURSE_VISIBILITY_PRIVATE, CourseUserType.ANONYMOUS, False], - [False, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.ANONYMOUS, False], - [False, COURSE_VISIBILITY_PUBLIC, CourseUserType.ANONYMOUS, False], - [True, COURSE_VISIBILITY_PRIVATE, CourseUserType.ANONYMOUS, False], - [True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.ANONYMOUS, False], - [True, COURSE_VISIBILITY_PUBLIC, CourseUserType.ANONYMOUS, True], - - [False, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED, False], - [False, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.UNENROLLED, False], - [False, COURSE_VISIBILITY_PUBLIC, CourseUserType.UNENROLLED, False], - [True, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED, False], - [True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.UNENROLLED, False], - [True, COURSE_VISIBILITY_PUBLIC, CourseUserType.UNENROLLED, True], - - [False, COURSE_VISIBILITY_PRIVATE, CourseUserType.ENROLLED, True], - [True, COURSE_VISIBILITY_PRIVATE, CourseUserType.ENROLLED, True], - [True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.ENROLLED, True], - [True, COURSE_VISIBILITY_PUBLIC, CourseUserType.ENROLLED, True], - - [False, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED_STAFF, True], - [True, COURSE_VISIBILITY_PRIVATE, CourseUserType.UNENROLLED_STAFF, True], - [True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.UNENROLLED_STAFF, True], - [True, COURSE_VISIBILITY_PUBLIC, CourseUserType.UNENROLLED_STAFF, True], - - [False, COURSE_VISIBILITY_PRIVATE, CourseUserType.GLOBAL_STAFF, True], - [True, COURSE_VISIBILITY_PRIVATE, CourseUserType.GLOBAL_STAFF, True], - [True, COURSE_VISIBILITY_PUBLIC_OUTLINE, CourseUserType.GLOBAL_STAFF, True], - [True, COURSE_VISIBILITY_PUBLIC, CourseUserType.GLOBAL_STAFF, True], - ) - @ddt.unpack - def test_courseware_access(self, waffle_override, course_visibility, user_type, expected_course_content): - - course = CourseFactory(course_visibility=course_visibility) - with self.store.bulk_operations(course.id): - chapter = ItemFactory(parent=course, category='chapter') - section = ItemFactory(parent=chapter, category='sequential') - vertical = ItemFactory.create(parent=section, category='vertical', display_name="Vertical") - ItemFactory.create(parent=vertical, category='html', display_name='HTML block') - ItemFactory.create(parent=vertical, category='video', display_name='Video') - - self.create_user_for_course(course, user_type) - - url = reverse( - 'courseware_section', - kwargs={ - 'course_id': str(course.id), - 'chapter': chapter.url_name, # lint-amnesty, pylint: disable=no-member - 'section': section.url_name, # lint-amnesty, pylint: disable=no-member - } - ) - - with override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=waffle_override): - - response = self.client.get(url, follow=False) - assert response.status_code == (200 if expected_course_content else 302) - unicode_content = response.content.decode('utf-8') - if expected_course_content: - if user_type in (CourseUserType.ANONYMOUS, CourseUserType.UNENROLLED): - assert 'data-save-position="false"' in unicode_content - assert 'data-show-completion="false"' in unicode_content - assert 'xblock-public_view-sequential' in unicode_content - assert 'xblock-public_view-vertical' in unicode_content - assert 'xblock-public_view-html' in unicode_content - assert 'xblock-public_view-video' in unicode_content - if user_type == CourseUserType.ANONYMOUS and course_visibility == COURSE_VISIBILITY_PRIVATE: - assert 'To see course content' in unicode_content - if user_type == CourseUserType.UNENROLLED and course_visibility == COURSE_VISIBILITY_PRIVATE: - assert 'You must be enrolled' in unicode_content - else: - assert 'data-save-position="true"' in unicode_content - assert 'data-show-completion="true"' in unicode_content - assert 'xblock-student_view-sequential' in unicode_content - assert 'xblock-student_view-vertical' in unicode_content - assert 'xblock-student_view-html' in unicode_content - assert 'xblock-student_view-video' in unicode_content - @patch('lms.djangoapps.courseware.views.views.CourseTabView.course_open_for_learner_enrollment') @patch('openedx.core.djangoapps.util.user_messages.PageLevelMessages.register_warning_message') def test_courseware_messages_differentiate_for_anonymous_users( @@ -2739,7 +2592,7 @@ def test_should_show_enroll_button(self, course_open_for_self_enrollment, @ddt.ddt -@_set_mfe_flag(activate_mfe=False) +@set_preview_mode(True) class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin): """ Tests CompleteOnView is set up correctly in CoursewareIndex. @@ -2752,7 +2605,6 @@ def setup_course(self, default_store): # pylint:disable=attribute-defined-outside-init self.request_factory = RequestFactoryNoCsrf() - self.user = UserFactory() with modulestore().default_store(default_store): self.course = CourseFactory.create() @@ -2811,11 +2663,11 @@ def setup_course(self, default_store): CourseOverview.load_from_module_store(self.course.id) CourseEnrollmentFactory(user=self.user, course_id=self.course.id) + assert self.client.login(username=self.user.username, password=self.user_password) def test_completion_service_disabled(self): self.setup_course(ModuleStoreEnum.Type.split) - assert self.client.login(username=self.user.username, password='test') response = self.client.get(self.section_1_url) self.assertNotContains(response, 'data-mark-completed-on-view-after-delay') @@ -2828,7 +2680,6 @@ def test_completion_service_enabled(self): self.override_waffle_switch(True) self.setup_course(ModuleStoreEnum.Type.split) - assert self.client.login(username=self.user.username, password='test') response = self.client.get(self.section_1_url) self.assertContains(response, 'data-mark-completed-on-view-after-delay') @@ -2840,6 +2691,7 @@ def test_completion_service_enabled(self): content_type='application/json', ) request.user = self.user + request.session = {} response = handle_xblock_callback( request, str(self.course.id), @@ -2858,6 +2710,7 @@ def test_completion_service_enabled(self): content_type='application/json', ) request.user = self.user + request.session = {} response = handle_xblock_callback( request, str(self.course.id), @@ -2874,7 +2727,7 @@ def test_completion_service_enabled(self): @ddt.ddt -@_set_mfe_flag(activate_mfe=False) +@set_preview_mode(True) class TestIndexViewWithVerticalPositions(ModuleStoreTestCase): """ Test the index view to handle vertical positions. Confirms that first position is loaded @@ -2887,8 +2740,6 @@ def setUp(self): """ super().setUp() - self.user = UserFactory() - # create course with 3 positions self.course = CourseFactory.create() with self.store.bulk_operations(self.course.id): @@ -2901,7 +2752,7 @@ def setUp(self): CourseOverview.load_from_module_store(self.course.id) - self.client.login(username=self.user, password='test') + self.client.login(username=self.user, password=self.user_password) CourseEnrollmentFactory(user=self.user, course_id=self.course.id) def _get_course_vertical_by_position(self, input_position): @@ -2940,130 +2791,6 @@ def test_vertical_positions(self, input_position, expected_position): self._assert_correct_position(resp, expected_position) -@_set_mfe_flag(activate_mfe=False) -class TestIndexViewWithGating(ModuleStoreTestCase, MilestonesTestCaseMixin): - """ - Test the index view for a course with gated content - """ - - def setUp(self): - """ - Set up the initial test data - """ - super().setUp() - - self.user = UserFactory() - self.course = CourseFactory.create() - with self.store.bulk_operations(self.course.id): - self.course.enable_subsection_gating = True - self.course.save() - self.course = self.update_course(self.course, 0) - self.chapter = ItemFactory.create( - parent_location=self.course.location, category="chapter", display_name="Chapter", - ) - self.open_seq = ItemFactory.create( - parent_location=self.chapter.location, category='sequential', display_name="Open Sequential" - ) - ItemFactory.create(parent_location=self.open_seq.location, category='problem', display_name="Problem 1") - self.gated_seq = ItemFactory.create( - parent_location=self.chapter.location, category='sequential', display_name="Gated Sequential" - ) - ItemFactory.create(parent_location=self.gated_seq.location, category='problem', display_name="Problem 2") - - gating_api.add_prerequisite(self.course.id, self.open_seq.location) - gating_api.set_required_content(self.course.id, self.gated_seq.location, self.open_seq.location, 100) - - CourseEnrollmentFactory(user=self.user, course_id=self.course.id) - - def test_index_with_gated_sequential(self): - """ - Test index view with a gated sequential raises Http404 - """ - assert self.client.login(username=self.user.username, password='test') - response = self.client.get( - reverse( - 'courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.chapter.url_name, - 'section': self.gated_seq.url_name, - } - ) - ) - assert response.status_code == 200 - self.assertContains(response, "Content Locked") - - -@_set_mfe_flag(activate_mfe=False) -class TestIndexViewWithCourseDurationLimits(ModuleStoreTestCase): - """ - Test the index view for a course with course duration limits enabled. - """ - - def setUp(self): - """ - Set up the initial test data. - """ - super().setUp() - - self.user = UserFactory() - self.course = CourseFactory.create(start=datetime.now() - timedelta(weeks=1)) - with self.store.bulk_operations(self.course.id): - self.chapter = ItemFactory.create(parent_location=self.course.location, category="chapter") - self.sequential = ItemFactory.create(parent_location=self.chapter.location, category='sequential') - self.vertical = ItemFactory.create(parent_location=self.sequential.location, category="vertical") - - CourseEnrollmentFactory(user=self.user, course_id=self.course.id) - - def test_index_with_course_duration_limits(self): - """ - Test that the courseware contains the course expiration banner - when course_duration_limits are enabled. - """ - CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) - assert self.client.login(username=self.user.username, password='test') - add_course_mode(self.course, mode_slug=CourseMode.AUDIT) - add_course_mode(self.course) - response = self.client.get( - reverse( - 'courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.chapter.url_name, - 'section': self.sequential.url_name, - } - ) - ) - bannerText = get_expiration_banner_text(self.user, self.course) - # Banner is XBlock wrapper, so it is escaped in raw response. Since - # it's escaped, ignoring the whitespace with assertContains doesn't - # work. Instead we remove all whitespace to verify content is correct. - bannerText_no_spaces = escape(bannerText).replace(' ', '') - response_no_spaces = response.content.decode('utf-8').replace(' ', '') - assert bannerText_no_spaces in response_no_spaces - - def test_index_without_course_duration_limits(self): - """ - Test that the courseware does not contain the course expiration banner - when course_duration_limits are disabled. - """ - CourseDurationLimitConfig.objects.create(enabled=False) - assert self.client.login(username=self.user.username, password='test') - add_course_mode(self.course, upgrade_deadline_expired=False) - response = self.client.get( - reverse( - 'courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.chapter.url_name, - 'section': self.sequential.url_name, - } - ) - ) - bannerText = get_expiration_banner_text(self.user, self.course) - self.assertNotContains(response, bannerText, html=True) - - class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase, CompletionWaffleTestMixin): """ Tests for the courseware.render_xblock endpoint. @@ -3354,7 +3081,7 @@ def course_options(self): return options -@_set_mfe_flag(activate_mfe=False) +@set_preview_mode(True) class TestIndexViewCrawlerStudentStateWrites(SharedModuleStoreTestCase): """ Ensure that courseware index requests do not trigger student state writes. @@ -3377,7 +3104,7 @@ def setUpClass(cls): @classmethod def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called """Set up and enroll our fake user in the course.""" - cls.user = UserFactory() + cls.user = UserFactory(is_staff=True) CourseEnrollment.enroll(cls.user, cls.course.id) def setUp(self): @@ -3494,91 +3221,10 @@ def test_legacy_redirect(self): assert response.get('Location') == 'http://learning-mfe/course/course-v1:Org+Course+Run/dates?foo=b%24r' -class TestShowCoursewareMFE(TestCase): +class MFEUrlTests(TestCase): """ - Make sure we're showing the Courseware MFE link when appropriate. - - There are an unfortunate number of state permutations here since we have - the product of the following binary states: - - * user is global staff member - * user is member of the course team - * whether the course_key is an old Mongo style of key - * the COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW CourseWaffleFlag - * the COURSEWARE_USE_LEGACY_FRONTEND opt-out CourseWaffleFlag - - Giving us theoretically 2^5 = 32 states. >_< + Test url utility method """ - def test_permutations(self): - """Test every permutation""" - old_course_key = CourseKey.from_string("OpenEdX/Old/2020") - new_course_key = CourseKey.from_string("course-v1:OpenEdX+New+2020") - - # Old style course keys are never supported and should always return false... - old_mongo_combos = itertools.product( - [True, False], # is_global_staff - [True, False], # is_course_staff - [True, False], # preview_active (COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW) - [True, False], # redirect_active (not COURSEWARE_USE_LEGACY_FRONTEND) - ) - for is_global_staff, is_course_staff, preview_active, redirect_active in old_mongo_combos: - with _set_preview_mfe_flag(preview_active): - with _set_mfe_flag(redirect_active): - assert not courseware_mfe_is_advertised( - is_global_staff=is_global_staff, - is_course_staff=is_course_staff, - course_key=old_course_key, - ) - - # We've checked all old-style course keys now, so we can test only the - # new ones going forward. Now we check combinations of waffle flags and - # user permissions... - with _set_preview_mfe_flag(True): - with _set_mfe_flag(activate_mfe=True): - # (preview=on, redirect=on) - # Global and Course Staff can see the link. - assert courseware_mfe_is_advertised(new_course_key, True, True) - assert courseware_mfe_is_advertised(new_course_key, True, False) - assert courseware_mfe_is_advertised(new_course_key, False, True) - - # (Regular users would see the link, but they can't see the Legacy - # experience, so it doesn't matter.) - - with _set_mfe_flag(activate_mfe=False): - # (preview=on, redirect=off) - # Global and Course Staff can see the link. - assert courseware_mfe_is_advertised(new_course_key, True, True) - assert courseware_mfe_is_advertised(new_course_key, True, False) - assert courseware_mfe_is_advertised(new_course_key, False, True) - - # Regular users don't see the link. - assert not courseware_mfe_is_advertised(new_course_key, False, False) - - with _set_preview_mfe_flag(False): - with _set_mfe_flag(activate_mfe=True): - # (preview=off, redirect=on) - # Global staff see the link anyway - assert courseware_mfe_is_advertised(new_course_key, True, True) - assert courseware_mfe_is_advertised(new_course_key, True, False) - - # If redirect is active for their students, course staff see the link even - # if preview=off. - assert courseware_mfe_is_advertised(new_course_key, False, True) - - # (Regular users would see the link, but they can't see the Legacy - # experience, so it doesn't matter.) - - with _set_mfe_flag(activate_mfe=False): - # (preview=off, redirect=off) - # Global staff and course teams can NOT see the link - # because both rollout waffle flags are false. - assert not courseware_mfe_is_advertised(new_course_key, True, True) - assert not courseware_mfe_is_advertised(new_course_key, True, False) - assert not courseware_mfe_is_advertised(new_course_key, False, True) - - # Regular users don't see the link. - assert not courseware_mfe_is_advertised(new_course_key, False, False) - @override_settings(LEARNING_MICROFRONTEND_URL='https://learningmfe.openedx.org') def test_url_generation(self): course_key = CourseKey.from_string("course-v1:OpenEdX+MFE+2020") @@ -3606,82 +3252,24 @@ def test_url_generation(self): ) -@ddt.ddt -class MFERedirectTests(BaseViewsTestCase): # lint-amnesty, pylint: disable=missing-class-docstring +class PreviewTests(BaseViewsTestCase): + """ + Make sure we allow the Legacy view for course previews. + """ def test_learner_redirect(self): - # learners will be redirected when the waffle flag is set + # learners will be redirected by default lms_url, mfe_url, __ = self._get_urls() - assert self.client.get(lms_url).url == mfe_url - def test_staff_no_redirect(self): - lms_url, __, __ = self._get_urls() - - # course staff will redirect in an MFE-enabled course - and not redirect otherwise. - course_staff = UserFactory.create(is_staff=False) - CourseStaffRole(self.course_key).add_users(course_staff) - self.client.login(username=course_staff.username, password='test') - - with _set_mfe_flag(activate_mfe=False): - assert self.client.get(lms_url).status_code == 200 - assert self.client.get(lms_url).status_code == 302 - - # global staff will never be redirected - self._create_global_staff_user() - - with _set_mfe_flag(activate_mfe=False): - assert self.client.get(lms_url).status_code == 200 - assert self.client.get(lms_url).status_code == 200 - - def test_exam_no_redirect(self): - # exams will not redirect to the mfe, for the time being - self.section2.is_time_limited = True - self.store.update_item(self.section2, self.user.id) - - lms_url, __, __ = self._get_urls() - - assert self.client.get(lms_url).status_code == 200 - - -class PreviewRedirectTests(BaseViewsTestCase): - """ - Make sure we're redirecting to the Legacy view for course previews. - - The user should always be redirected to the Legacy view as long as they are - part of the two following groups: - - * user is global staff member - * user is member of the course team - """ - def test_staff_no_redirect(self): + def test_preview_no_redirect(self): __, __, preview_url = self._get_urls() - with patch.object(access_utils, 'get_current_request_hostname', - return_value=settings.FEATURES.get('PREVIEW_LMS_BASE', None)): - - # Previews will not redirect to the mfe,, for the time being. + with set_preview_mode(True): + # Previews will not redirect to the mfe course_staff = UserFactory.create(is_staff=False) CourseStaffRole(self.course_key).add_users(course_staff) self.client.login(username=course_staff.username, password='test') - - with _set_mfe_flag(activate_mfe=False): - assert self.client.get(preview_url).status_code == 200 - assert self.client.get(preview_url).status_code == 200 - - # global staff will never be redirected - self._create_global_staff_user() - with _set_mfe_flag(activate_mfe=False): - assert self.client.get(preview_url).status_code == 200 assert self.client.get(preview_url).status_code == 200 - def test_exam_no_redirect(self): - # exams will not redirect to the mfe, for the time being - self.section2.is_time_limited = True - self.store.update_item(self.section2, self.user.id) - - __, __, preview_url = self._get_urls() - - assert self.client.get(preview_url).status_code == 200 - class ContentOptimizationTestCase(ModuleStoreTestCase): """ diff --git a/lms/djangoapps/courseware/testutils.py b/lms/djangoapps/courseware/testutils.py index 4570de9199a4..fe510799d282 100644 --- a/lms/djangoapps/courseware/testutils.py +++ b/lms/djangoapps/courseware/testutils.py @@ -11,8 +11,9 @@ import ddt from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from lms.djangoapps.courseware.tests.helpers import set_preview_mode from lms.djangoapps.courseware.utils import is_mode_upsellable -from openedx.features.course_experience.url_helpers import get_courseware_url, ExperienceOption +from openedx.features.course_experience.url_helpers import get_courseware_url from common.djangoapps.student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory @@ -170,6 +171,7 @@ def verify_response(self, expected_response_code=200, url_params=None): ('html_block', 4), ) @ddt.unpack + @set_preview_mode(True) def test_courseware_html(self, block_name, mongo_calls): """ To verify that the removal of courseware chrome elements is working, @@ -184,10 +186,7 @@ def test_courseware_html(self, block_name, mongo_calls): self.setup_user(admin=True, enroll=True, login=True) with check_mongo_calls(mongo_calls): - url = get_courseware_url( - self.block_to_be_tested.location, - experience=ExperienceOption.LEGACY, - ) + url = get_courseware_url(self.block_to_be_tested.location) response = self.client.get(url) expected_elements = self.block_specific_chrome_html_elements + self.COURSEWARE_CHROME_HTML_ELEMENTS for chrome_element in expected_elements: diff --git a/lms/djangoapps/courseware/toggles.py b/lms/djangoapps/courseware/toggles.py index 4cea39e09c0d..63babaceacf4 100644 --- a/lms/djangoapps/courseware/toggles.py +++ b/lms/djangoapps/courseware/toggles.py @@ -3,7 +3,6 @@ """ from edx_toggles.toggles import LegacyWaffleFlagNamespace, SettingToggle -from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag @@ -11,35 +10,6 @@ WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='courseware') -# .. toggle_name: courseware.use_legacy_frontend -# .. toggle_implementation: CourseWaffleFlag -# .. toggle_default: False -# .. toggle_description: Waffle flag to direct learners to the legacy courseware experience - the default behavior -# directs to the new MFE-based courseware in frontend-app-learning. Supports the ability to globally flip back to -# the legacy courseware experience. -# .. toggle_use_cases: temporary, open_edx -# .. toggle_creation_date: 2021-06-03 -# .. toggle_target_removal_date: 2021-10-09 -# .. toggle_tickets: DEPR-109 -COURSEWARE_USE_LEGACY_FRONTEND = CourseWaffleFlag( - WAFFLE_FLAG_NAMESPACE, 'use_legacy_frontend', __name__ -) - -# .. toggle_name: courseware.microfrontend_course_team_preview -# .. toggle_implementation: CourseWaffleFlag -# .. toggle_default: False -# .. toggle_description: Waffle flag to display a link for the new learner experience to course teams without -# redirecting students. Supports staged rollout to course teams of a new micro-frontend-based implementation of the -# courseware page. -# .. toggle_use_cases: temporary, open_edx -# .. toggle_creation_date: 2020-03-09 -# .. toggle_target_removal_date: 2020-12-31 -# .. toggle_warnings: Also set settings.LEARNING_MICROFRONTEND_URL. -# .. toggle_tickets: DEPR-109 -COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW = CourseWaffleFlag( - WAFFLE_FLAG_NAMESPACE, 'microfrontend_course_team_preview', __name__ -) - # Waffle flag to enable the course exit page in the learning MFE. # # .. toggle_name: courseware.microfrontend_course_exit_page @@ -128,118 +98,25 @@ COURSES_INVITE_ONLY = SettingToggle('COURSES_INVITE_ONLY', default=False) -def courseware_mfe_is_active(course_key: CourseKey) -> bool: +def courseware_mfe_is_active() -> bool: """ Should we serve the Learning MFE as the canonical courseware experience? """ - #Avoid circular imports. - from lms.djangoapps.courseware.access_utils import in_preview_mode - # NO: Old Mongo courses are always served in the Legacy frontend, - # regardless of configuration. - if course_key.deprecated: - return False - # NO: MFE courseware can be disabled for users/courses/globally via this - # Waffle flag. - if COURSEWARE_USE_LEGACY_FRONTEND.is_enabled(course_key): - return False - # NO: Course preview doesn't work in the MFE - if in_preview_mode(): - return False - # OTHERWISE: MFE courseware experience is active by default. - return True - - -def courseware_mfe_is_visible( - course_key: CourseKey, - is_global_staff=False, - is_course_staff=False, -) -> bool: - """ - Can we see a course run's content in the Learning MFE? - """ - #Avoid circular imports. - from lms.djangoapps.courseware.access_utils import in_preview_mode - # DENY: Old Mongo courses don't work in the MFE. - if course_key.deprecated: - return False - # DENY: Course preview doesn't work in the MFE - if in_preview_mode(): - return False - # ALLOW: Where techincally possible, global staff may always see the MFE. - if is_global_staff: - return True - # ALLOW: If course team preview is enabled, then course staff may see their - # course in the MFE. - if is_course_staff and COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW.is_enabled(course_key): - return True - # OTHERWISE: The MFE is only visible if it's the active (ie canonical) experience. - return courseware_mfe_is_active(course_key) - - -def courseware_mfe_is_advertised( - course_key: CourseKey, - is_global_staff=False, - is_course_staff=False, -) -> bool: - """ - Should we invite the user to view a course run's content in the Learning MFE? - - This check is slightly different than `courseware_mfe_is_visible`, in that - we always *permit* global staff to view MFE content (assuming it's deployed), - but we do not shove the New Experience in their face if the preview isn't - enabled. - """ - #Avoid circular imports. - from lms.djangoapps.courseware.access_utils import in_preview_mode - # DENY: Old Mongo courses don't work in the MFE. - if course_key.deprecated: - return False - # DENY: Course preview doesn't work in the MFE - if in_preview_mode(): - return False - # ALLOW: Both global and course staff can see the MFE link if the course team - # preview is enabled. - is_staff = is_global_staff or is_course_staff - if is_staff and COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW.is_enabled(course_key): - return True - # OTHERWISE: The MFE is only advertised if it's the active (ie canonical) experience. - return courseware_mfe_is_active(course_key) - - -def courseware_legacy_is_visible( - course_key: CourseKey, - is_global_staff=False, -) -> bool: - """ - Can we see a course run's content in the Legacy frontend? - - Note: This function will always return True for Old Mongo courses, - since `courseware_mfe_is_active` will always return False for them. - """ - #Avoid circular imports. - from lms.djangoapps.courseware.access_utils import in_preview_mode - # ALLOW: Global staff may always see the Legacy experience. - if is_global_staff: - return True - # ALLOW: All course previews will be shown in Legacy experience - if in_preview_mode(): - return True - # OTHERWISE: Legacy is only visible if it's the active (ie canonical) experience. - # Note that Old Mongo courses are never the active experience, - # so we effectively always ALLOW them to be viewed in Legacy. - return not courseware_mfe_is_active(course_key) + from lms.djangoapps.courseware.access_utils import in_preview_mode # avoid a circular import + # We only use legacy views for the Studio "preview mode" feature these days, while everyone else gets the MFE + return not in_preview_mode() def course_exit_page_is_active(course_key): return ( - courseware_mfe_is_active(course_key) and + courseware_mfe_is_active() and COURSEWARE_MICROFRONTEND_COURSE_EXIT_PAGE.is_enabled(course_key) ) def courseware_mfe_progress_milestones_are_active(course_key): return ( - courseware_mfe_is_active(course_key) and + courseware_mfe_is_active() and COURSEWARE_MICROFRONTEND_PROGRESS_MILESTONES.is_enabled(course_key) ) diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index 63cc2b27a0ae..b789a8659842 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -64,10 +64,7 @@ from ..model_data import FieldDataCache from ..module_render import get_module_for_descriptor, toc_for_course from ..permissions import MASQUERADE_AS_STUDENT -from ..toggles import ( - courseware_legacy_is_visible, - courseware_mfe_is_advertised -) +from ..toggles import courseware_mfe_is_active from .views import CourseTabView log = logging.getLogger("edx.courseware.views.index") @@ -172,23 +169,11 @@ def _setup_masquerade_for_effective_user(self): def _redirect_to_learning_mfe(self): """ - Can the user access this sequence in Legacy courseware? If not, redirect to MFE. - - We specifically allow users to stay in the Legacy frontend for special - (ie timed/proctored) exams since they're not yet supported by the MFE. + Can the user access this sequence in the courseware MFE? If so, redirect to MFE. """ - # STAY: if the course run as a whole is visible in the Legacy experience. - if courseware_legacy_is_visible( - course_key=self.course_key, - is_global_staff=self.request.user.is_staff, - ): - return - # STAY: if we are in a special (ie proctored/timed) exam, which isn't yet - # supported on the MFE. - if getattr(self.section, 'is_time_limited', False): - return - # REDIRECT otherwise. - raise Redirect(self.microfrontend_url) + # If the MFE is active, prefer that + if courseware_mfe_is_active(): + raise Redirect(self.microfrontend_url) @property def microfrontend_url(self): @@ -497,16 +482,6 @@ def _create_courseware_context(self, request): if self.section.position and self.section.has_children: self._add_sequence_title_to_context(courseware_context) - # Courseware MFE link - if courseware_mfe_is_advertised( - is_global_staff=request.user.is_staff, - is_course_staff=staff_access, - course_key=self.course.id, - ): - courseware_context['microfrontend_link'] = self.microfrontend_url - else: - courseware_context['microfrontend_link'] = None - return courseware_context def _add_sequence_title_to_context(self, courseware_context): diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index bf5ada7dda06..e5028a3d67b2 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -119,7 +119,6 @@ from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG, course_home_url from openedx.features.course_experience.course_tools import CourseToolsPluginManager from openedx.features.course_experience.url_helpers import ( - ExperienceOption, get_courseware_url, get_learning_mfe_home_url, is_request_from_learning_mfe @@ -410,19 +409,10 @@ def jump_to(request, course_id, location): except InvalidKeyError as exc: raise Http404("Invalid course_key or usage_key") from exc - experience_param = request.GET.get("experience", "").lower() - if experience_param == "new": - experience = ExperienceOption.NEW - elif experience_param == "legacy": - experience = ExperienceOption.LEGACY - else: - experience = ExperienceOption.ACTIVE - try: redirect_url = get_courseware_url( usage_key=usage_key, request=request, - experience=experience, ) except (ItemNotFoundError, NoPathToItem): # We used to 404 here, but that's ultimately a bad experience. There are real world use cases where a user @@ -432,7 +422,6 @@ def jump_to(request, course_id, location): redirect_url = get_courseware_url( usage_key=course_location_from_key(course_key), request=request, - experience=experience, ) return redirect(redirect_url) diff --git a/lms/templates/preview_menu.html b/lms/templates/preview_menu.html index 95cf58edb216..8af4d245f28f 100644 --- a/lms/templates/preview_menu.html +++ b/lms/templates/preview_menu.html @@ -91,11 +91,6 @@ % endif
- % if microfrontend_link: - - ${_("View in the new experience")} - - % endif % if studio_url: DISCOUNT_PRICE' in block_content @@ -785,7 +788,6 @@ def test_discount_display(self): @override_settings(FIELD_OVERRIDE_PROVIDERS=( 'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride', )) -@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) class TestConditionalContentAccess(TestConditionalContent): """ Conditional Content allows course authors to run a/b tests on course content. We want to make sure that @@ -852,6 +854,7 @@ def test_access_based_on_conditional_content(self): # Make sure that all audit enrollments are gated regardless of if they see vertical a or vertical b _assert_block_is_gated( + self.store, block=self.block_a, user=self.student_audit_a, course=self.course, @@ -859,6 +862,7 @@ def test_access_based_on_conditional_content(self): request_factory=self.factory, ) _assert_block_is_gated( + self.store, block=self.block_b, user=self.student_audit_b, course=self.course, @@ -868,6 +872,7 @@ def test_access_based_on_conditional_content(self): # Make sure that all verified enrollments are not gated regardless of if they see vertical a or vertical b _assert_block_is_gated( + self.store, block=self.block_a, user=self.student_verified_a, course=self.course, @@ -875,6 +880,7 @@ def test_access_based_on_conditional_content(self): request_factory=self.factory, ) _assert_block_is_gated( + self.store, block=self.block_b, user=self.student_verified_b, course=self.course, @@ -886,7 +892,6 @@ def test_access_based_on_conditional_content(self): @override_settings(FIELD_OVERRIDE_PROVIDERS=( 'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride', )) -@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) class TestMessageDeduplication(ModuleStoreTestCase): """ Tests to verify that access denied messages isn't shown if multiple items in a row are denied. @@ -943,12 +948,13 @@ def test_single_denied(self): mode='audit' ) blocks_dict['graded_1'] = ItemFactory.create( - parent=blocks_dict['vertical'], + parent_location=blocks_dict['vertical'].location, category='problem', graded=True, metadata=METADATA, ) _assert_block_is_gated( + self.store, block=blocks_dict['graded_1'], user=self.user, course=course['course'], @@ -978,6 +984,7 @@ def test_double_denied(self): mode='audit' ) _assert_block_is_gated( + self.store, block=blocks_dict['graded_1'], user=self.user, course=course['course'], @@ -985,6 +992,7 @@ def test_double_denied(self): request_factory=self.request_factory, ) _assert_block_is_empty( + self.store, block=blocks_dict['graded_2'], user_id=self.user.id, course=course['course'], @@ -1025,6 +1033,7 @@ def test_many_denied(self): mode='audit' ) _assert_block_is_gated( + self.store, block=blocks_dict['graded_1'], user=self.user, course=course['course'], @@ -1032,18 +1041,21 @@ def test_many_denied(self): request_factory=self.request_factory, ) _assert_block_is_empty( + self.store, block=blocks_dict['graded_2'], user_id=self.user.id, course=course['course'], request_factory=self.request_factory, ) _assert_block_is_empty( + self.store, block=blocks_dict['graded_3'], user_id=self.user.id, course=course['course'], request_factory=self.request_factory, ) _assert_block_is_empty( + self.store, block=blocks_dict['graded_4'], user_id=self.user.id, course=course['course'], @@ -1077,6 +1089,7 @@ def test_alternate_denied(self): mode='audit' ) _assert_block_is_gated( + self.store, block=blocks_dict['graded_1'], user=self.user, course=course['course'], @@ -1084,6 +1097,7 @@ def test_alternate_denied(self): request_factory=self.request_factory, ) _assert_block_is_gated( + self.store, block=blocks_dict['ungraded_2'], user=self.user, course=course['course'], @@ -1091,6 +1105,7 @@ def test_alternate_denied(self): request_factory=self.request_factory, ) _assert_block_is_gated( + self.store, block=blocks_dict['graded_3'], user=self.user, course=course['course'], diff --git a/openedx/features/course_duration_limits/tests/test_course_expiration.py b/openedx/features/course_duration_limits/tests/test_course_expiration.py index 8f0211513fda..2db8c1d1a65b 100644 --- a/openedx/features/course_duration_limits/tests/test_course_expiration.py +++ b/openedx/features/course_duration_limits/tests/test_course_expiration.py @@ -9,7 +9,6 @@ from django.conf import settings from django.urls import reverse from django.utils.timezone import now -from edx_toggles.toggles.testutils import override_waffle_flag from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID @@ -25,7 +24,6 @@ from common.djangoapps.student.tests.factories import OrgStaffFactory from common.djangoapps.student.tests.factories import StaffFactory from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin -from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.course_date_signals.utils import MAX_DURATION, MIN_DURATION @@ -44,7 +42,6 @@ # pylint: disable=no-member @ddt.ddt -@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin): """Tests to verify the get_user_course_expiration_date function is working correctly""" def setUp(self): @@ -80,14 +77,7 @@ def tearDown(self): def get_courseware(self): """Returns a response from a GET on a courseware section""" - courseware_url = reverse( - 'courseware_section', - kwargs={ - 'course_id': str(self.course.id), - 'chapter': self.chapter.location.block_id, - 'section': self.sequential.location.block_id, - }, - ) + courseware_url = reverse('render_xblock', args=[str(self.sequential.location)]) return self.client.get(courseware_url, follow=True) def test_enrollment_mode(self): diff --git a/openedx/features/course_experience/tests/test_url_helpers.py b/openedx/features/course_experience/tests/test_url_helpers.py index 4728858cf0a2..aeb7d7ffaa4f 100644 --- a/openedx/features/course_experience/tests/test_url_helpers.py +++ b/openedx/features/course_experience/tests/test_url_helpers.py @@ -284,4 +284,4 @@ def test_get_courseware_url( path = url.split('?')[0] assert path == expected_path course_run = self.items[store_type]['course_run'] - mock_mfe_is_active.assert_called_once_with(course_run.id) + mock_mfe_is_active.assert_called_once() diff --git a/openedx/features/course_experience/tests/views/test_course_sock.py b/openedx/features/course_experience/tests/views/test_course_sock.py index 6b361f81851b..cdc80168ab1c 100644 --- a/openedx/features/course_experience/tests/views/test_course_sock.py +++ b/openedx/features/course_experience/tests/views/test_course_sock.py @@ -11,7 +11,7 @@ from common.djangoapps.course_modes.models import CourseMode from lms.djangoapps.commerce.models import CommerceConfiguration -from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND +from lms.djangoapps.courseware.tests.helpers import set_preview_mode from openedx.core.djangolib.markup import HTML from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory @@ -25,7 +25,7 @@ @ddt.ddt -@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) +@set_preview_mode(True) class TestCourseSockView(SharedModuleStoreTestCase): """ Tests for the course verification sock fragment view. @@ -47,7 +47,7 @@ def setUpClass(cls): def setUp(self): super().setUp() - self.user = UserFactory.create() + self.user = UserFactory.create(is_staff=True) # Enroll the user in the four courses CourseEnrollmentFactory.create(user=self.user, course_id=self.standard_course.id) diff --git a/openedx/features/course_experience/tests/views/test_masquerade.py b/openedx/features/course_experience/tests/views/test_masquerade.py index b366d5ef2d55..0e9660c0158d 100644 --- a/openedx/features/course_experience/tests/views/test_masquerade.py +++ b/openedx/features/course_experience/tests/views/test_masquerade.py @@ -5,8 +5,7 @@ from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag -from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin -from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND +from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin, set_preview_mode from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG from common.djangoapps.student.roles import CourseStaffRole from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory @@ -62,12 +61,12 @@ def get_group_id_by_course_mode_name(self, course_id, mode_name): return None +@set_preview_mode(True) class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase): """ Tests for the course verification upgrade messages while the user is being masqueraded. """ - @override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) @override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True) def test_masquerade_as_student(self): # Elevate the staff user to be student @@ -75,7 +74,6 @@ def test_masquerade_as_student(self): response = self.client.get(reverse('courseware', kwargs={'course_id': str(self.verified_course.id)})) self.assertContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False) - @override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) @override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True) def test_masquerade_as_verified_student(self): user_group_id = self.get_group_id_by_course_mode_name( @@ -87,7 +85,6 @@ def test_masquerade_as_verified_student(self): response = self.client.get(reverse('courseware', kwargs={'course_id': str(self.verified_course.id)})) self.assertNotContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False) - @override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) @override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True) def test_masquerade_as_masters_student(self): user_group_id = self.get_group_id_by_course_mode_name( diff --git a/openedx/features/course_experience/url_helpers.py b/openedx/features/course_experience/url_helpers.py index f3b374c1502f..0e52cee57982 100644 --- a/openedx/features/course_experience/url_helpers.py +++ b/openedx/features/course_experience/url_helpers.py @@ -4,7 +4,6 @@ Centralized in openedx/features/course_experience instead of lms/djangoapps/courseware because the Studio course outline may need these utilities. """ -from enum import Enum from typing import Optional from django.conf import settings @@ -22,24 +21,9 @@ User = get_user_model() -class ExperienceOption(Enum): - """ - Versions of the courseware experience that can be requested. - - `ACTIVE` indicates that the default experience (in the context of the - course run) should be used. - - To be removed as part of DEPR-109. - """ - ACTIVE = 'courseware-experience-active' - NEW = 'courseware-experience-new' - LEGACY = 'courseware-experience-legacy' - - def get_courseware_url( usage_key: UsageKey, request: Optional[HttpRequest] = None, - experience: ExperienceOption = ExperienceOption.ACTIVE, ) -> str: """ Return the URL to the canonical learning experience for a given block. @@ -56,12 +40,7 @@ def get_courseware_url( * ItemNotFoundError if no data at the `usage_key`. * NoPathToItem if we cannot build a path to the `usage_key`. """ - course_key = usage_key.course_key.replace(version_guid=None, branch=None) - if experience == ExperienceOption.NEW: - get_url_fn = _get_new_courseware_url - elif experience == ExperienceOption.LEGACY: - get_url_fn = _get_legacy_courseware_url - elif courseware_mfe_is_active(course_key): + if courseware_mfe_is_active(): get_url_fn = _get_new_courseware_url else: get_url_fn = _get_legacy_courseware_url diff --git a/openedx/tests/xblock_integration/test_recommender.py b/openedx/tests/xblock_integration/test_recommender.py index d245a1dfb15e..d312f8892573 100644 --- a/openedx/tests/xblock_integration/test_recommender.py +++ b/openedx/tests/xblock_integration/test_recommender.py @@ -14,17 +14,14 @@ from ddt import data, ddt from django.conf import settings from django.urls import reverse -from edx_toggles.toggles.testutils import override_waffle_flag from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from common.djangoapps.student.tests.factories import GlobalStaffFactory from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase -from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND from openedx.core.lib.url_utils import quote_slashes -@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) class TestRecommender(SharedModuleStoreTestCase, LoginEnrollmentTestCase): """ Check that Recommender state is saved properly @@ -68,14 +65,7 @@ def setUpClass(cls): display_name='recommender_second' ) - cls.course_url = reverse( - 'courseware_section', - kwargs={ - 'course_id': str(cls.course.id), - 'chapter': 'Overview', - 'section': 'Welcome', - } - ) + cls.course_url = reverse('render_xblock', args=[str(cls.section.location)]) cls.resource_urls = [ ( diff --git a/openedx/tests/xblock_integration/xblock_testcase.py b/openedx/tests/xblock_integration/xblock_testcase.py index 2f142b6bca11..a35d170eb237 100644 --- a/openedx/tests/xblock_integration/xblock_testcase.py +++ b/openedx/tests/xblock_integration/xblock_testcase.py @@ -48,14 +48,12 @@ from bs4 import BeautifulSoup from django.conf import settings from django.urls import reverse -from edx_toggles.toggles.testutils import override_waffle_flag from xblock.plugin import Plugin from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory import lms.djangoapps.lms_xblock.runtime from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase -from lms.djangoapps.courseware.toggles import COURSEWARE_USE_LEGACY_FRONTEND class XBlockEventTestMixin: @@ -273,14 +271,7 @@ def setUpClass(cls): ) cls.xblocks[xblock_config['urlname']] = xblock - scenario_url = str(reverse( - 'courseware_section', - kwargs={ - 'course_id': str(cls.course.id), - 'chapter': "ch_" + chapter_config['urlname'], - 'section': "sec_" + chapter_config['urlname'] - } - )) + scenario_url = reverse('render_xblock', args=[str(section.location)]) cls.scenario_urls[chapter_config['urlname']] = scenario_url @@ -337,7 +328,6 @@ def select_student(self, user_id): self.login(email, password) -@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True) class XBlockTestCase(XBlockStudentTestCaseMixin, XBlockScenarioTestCaseMixin, XBlockEventTestMixin,