feat!: drop legacy courseware tab access for learners
The only way to access the legacy courseware is now through the Studio preview feature (and at some point, when the MFE supports a preview mode, we can then remove even that). This drops the courseware.use_legacy_frontend waffle.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -17,7 +17,6 @@ from django.test.utils import override_settings
|
||||
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.tabs import get_course_tab_list
|
||||
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 @@ class TestStudentViewsWithCCX(ModuleStoreTestCase):
|
||||
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
|
||||
|
||||
@@ -21,7 +21,6 @@ from lms.djangoapps.courseware.context_processor import user_timezone_locale_pre
|
||||
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 @@ class CourseHomeMetadataView(RetrieveAPIView):
|
||||
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 @@ class CourseHomeMetadataView(RetrieveAPIView):
|
||||
'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),
|
||||
|
||||
@@ -7,8 +7,9 @@ import ast
|
||||
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 django.utils.timezone import now
|
||||
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)
|
||||
|
||||
@@ -3,10 +3,9 @@ Tests use cases related to LMS Entrance Exam behavior, such as gated content acc
|
||||
"""
|
||||
|
||||
|
||||
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.entrance_exams import (
|
||||
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 @@ from common.djangoapps.util.milestones_helpers import (
|
||||
)
|
||||
|
||||
|
||||
@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 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
|
||||
]
|
||||
)
|
||||
|
||||
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 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
|
||||
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 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
|
||||
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 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
|
||||
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
|
||||
|
||||
@@ -26,9 +26,10 @@ from lms.djangoapps.courseware.masquerade import (
|
||||
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.modulestore.tests.factories import CourseFactory, ItemFactory # li
|
||||
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 @@ class MasqueradeTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase, Mas
|
||||
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 @@ class TestMasqueradeOptionsNoContentGroups(StaffMasqueradeTestCase):
|
||||
assert is_target_available == expected
|
||||
|
||||
|
||||
@set_preview_mode(True)
|
||||
class TestStaffMasqueradeAsStudent(StaffMasqueradeTestCase):
|
||||
"""
|
||||
Check for staff being able to masquerade as student.
|
||||
|
||||
@@ -15,19 +15,15 @@ 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 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 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
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 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
- 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 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
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 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
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 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
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 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
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 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
# 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 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
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}):
|
||||
|
||||
@@ -5,19 +5,16 @@ Test for split test XModule
|
||||
|
||||
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 @@ class SplitTestBase(ModuleStoreTestCase):
|
||||
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
|
||||
|
||||
@@ -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.test.utils import override_settings
|
||||
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.certificates.tests.factories import (
|
||||
)
|
||||
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.api import set_credit_requirements
|
||||
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 = settings.FEATURES.copy()
|
||||
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 @@ class TestJumpTo(ModuleStoreTestCase):
|
||||
(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 @@ class TestJumpTo(ModuleStoreTestCase):
|
||||
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 @@ class TestJumpTo(ModuleStoreTestCase):
|
||||
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 @@ class TestJumpTo(ModuleStoreTestCase):
|
||||
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 @@ class TestJumpTo(ModuleStoreTestCase):
|
||||
|
||||
# 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 @@ class TestJumpTo(ModuleStoreTestCase):
|
||||
(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 @@ class TestJumpTo(ModuleStoreTestCase):
|
||||
}
|
||||
)
|
||||
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 @@ class IndexQueryTestCase(ModuleStoreTestCase):
|
||||
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 @@ class IndexQueryTestCase(ModuleStoreTestCase):
|
||||
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 @@ class BaseViewsTestCase(ModuleStoreTestCase): # lint-amnesty, pylint: disable=m
|
||||
|
||||
|
||||
@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 @@ class ViewsTestCase(BaseViewsTestCase):
|
||||
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 @@ class ViewsTestCase(BaseViewsTestCase):
|
||||
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 = [
|
||||
'<p class="accordion-display-name">Sequential 1 <span class="sr">current section</span></p>',
|
||||
'<p class="accordion-display-name">Sequential 2 </p>'
|
||||
]
|
||||
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 @@ class ViewsTestCase(BaseViewsTestCase):
|
||||
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 @@ class ViewsTestCase(BaseViewsTestCase):
|
||||
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 @@ class ViewsTestCase(BaseViewsTestCase):
|
||||
"""
|
||||
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 @@ class ViewsTestCase(BaseViewsTestCase):
|
||||
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 @@ class ViewsTestCase(BaseViewsTestCase):
|
||||
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 @@ class ViewsTestCase(BaseViewsTestCase):
|
||||
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 = [
|
||||
'<p class="accordion-display-name">Sequential 1 <span class="sr">current section</span></p>',
|
||||
'<p class="accordion-display-name">Sequential 2 </p>'
|
||||
]
|
||||
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 @@ class TestProgramMarketingView(SharedModuleStoreTestCase):
|
||||
|
||||
# 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 @@ class BaseDueDateTests(ModuleStoreTestCase):
|
||||
|
||||
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 @@ class TestProgressDueDate(BaseDueDateTests):
|
||||
|
||||
|
||||
# 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 @@ class ViewCheckerBlock(XBlock):
|
||||
|
||||
|
||||
@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 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
"""
|
||||
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 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
|
||||
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 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
|
||||
@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 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
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 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
)
|
||||
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 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
|
||||
|
||||
@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 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
|
||||
# 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 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
|
||||
|
||||
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 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
|
||||
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 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
|
||||
content_type='application/json',
|
||||
)
|
||||
request.user = self.user
|
||||
request.session = {}
|
||||
response = handle_xblock_callback(
|
||||
request,
|
||||
str(self.course.id),
|
||||
@@ -2858,6 +2710,7 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
|
||||
content_type='application/json',
|
||||
)
|
||||
request.user = self.user
|
||||
request.session = {}
|
||||
response = handle_xblock_callback(
|
||||
request,
|
||||
str(self.course.id),
|
||||
@@ -2874,7 +2727,7 @@ class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin
|
||||
|
||||
|
||||
@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 @@ class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
|
||||
"""
|
||||
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 @@ class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
|
||||
|
||||
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 @@ class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
|
||||
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 @@ class TestRenderXBlockSelfPaced(TestRenderXBlock): # lint-amnesty, pylint: disa
|
||||
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 @@ class TestIndexViewCrawlerStudentStateWrites(SharedModuleStoreTestCase):
|
||||
@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 @@ class DatesTabTestCase(TestCase):
|
||||
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 @@ class TestShowCoursewareMFE(TestCase):
|
||||
)
|
||||
|
||||
|
||||
@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):
|
||||
"""
|
||||
|
||||
@@ -11,8 +11,9 @@ from urllib.parse import urlencode
|
||||
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 @@ class RenderXBlockTestMixin(MasqueradeMixin, metaclass=ABCMeta):
|
||||
('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 @@ class RenderXBlockTestMixin(MasqueradeMixin, metaclass=ABCMeta):
|
||||
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:
|
||||
|
||||
@@ -3,7 +3,6 @@ Toggles for courseware in-course experience.
|
||||
"""
|
||||
|
||||
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 @@ from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
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 @@ COURSEWARE_OPTIMIZED_RENDER_XBLOCK = CourseWaffleFlag(
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
@@ -64,10 +64,7 @@ from ..masquerade import check_content_start_date_for_masquerade_user, setup_mas
|
||||
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 @@ class CoursewareIndex(View):
|
||||
|
||||
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 @@ class CoursewareIndex(View):
|
||||
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):
|
||||
|
||||
@@ -119,7 +119,6 @@ from openedx.features.course_duration_limits.access import generate_course_expir
|
||||
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)
|
||||
|
||||
@@ -91,11 +91,6 @@ show_preview_menu = course and can_masquerade and supports_preview_menu
|
||||
</div>
|
||||
% endif
|
||||
<div style="flex-shrink: 1; text-align: center;">
|
||||
% if microfrontend_link:
|
||||
<a class="btn btn-primary" style="border: solid 1px white;" href="${microfrontend_link}">
|
||||
${_("View in the new experience")}
|
||||
</a>
|
||||
% endif
|
||||
% if studio_url:
|
||||
<a
|
||||
class="btn btn-primary view-in-studio"
|
||||
|
||||
@@ -102,7 +102,6 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract-
|
||||
pacing = serializers.CharField()
|
||||
user_timezone = serializers.CharField()
|
||||
show_calculator = serializers.BooleanField()
|
||||
can_view_legacy_courseware = serializers.BooleanField()
|
||||
can_access_proctored_exams = serializers.BooleanField()
|
||||
notes = serializers.DictField()
|
||||
marketing_url = serializers.CharField()
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
},
|
||||
"show_calculator": false,
|
||||
"original_user_is_staff": true,
|
||||
"can_view_legacy_courseware": true,
|
||||
"is_staff": true,
|
||||
"course_access": {
|
||||
"has_access": true,
|
||||
@@ -200,9 +199,6 @@
|
||||
"$.body.original_user_is_staff": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.can_view_legacy_courseware": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.is_staff": {
|
||||
"match": "type"
|
||||
},
|
||||
|
||||
@@ -41,10 +41,7 @@ from lms.djangoapps.courseware.masquerade import (
|
||||
)
|
||||
from lms.djangoapps.courseware.models import LastSeenCoursewareTimezone
|
||||
from lms.djangoapps.courseware.module_render import get_module_by_usage_id
|
||||
from lms.djangoapps.courseware.toggles import (
|
||||
courseware_legacy_is_visible,
|
||||
course_exit_page_is_active,
|
||||
)
|
||||
from lms.djangoapps.courseware.toggles import course_exit_page_is_active
|
||||
from lms.djangoapps.courseware.views.views import get_cert_data
|
||||
from lms.djangoapps.gating.api import get_entrance_exam_score, get_entrance_exam_usage_key
|
||||
from lms.djangoapps.grades.api import CourseGradeFactory
|
||||
@@ -94,10 +91,6 @@ class CoursewareMeta:
|
||||
self.request.user = self.effective_user
|
||||
self.enrollment_object = CourseEnrollment.get_enrollment(self.effective_user, self.course_key,
|
||||
select_related=['celebration', 'user__celebration'])
|
||||
self.can_view_legacy_courseware = courseware_legacy_is_visible(
|
||||
course_key=course_key,
|
||||
is_global_staff=self.original_user_is_global_staff,
|
||||
)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.overview, name)
|
||||
@@ -448,7 +441,6 @@ class CoursewareInformation(RetrieveAPIView):
|
||||
* pacing: Course pacing. Possible values: instructor, self
|
||||
* user_timezone: User's chosen timezone setting (or null for browser default)
|
||||
* can_load_course: Whether the user can view the course (AccessResponse object)
|
||||
* 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
|
||||
|
||||
@@ -12,7 +12,6 @@ from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth import get_user_model
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from pyquery import PyQuery as pq
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
TEST_DATA_MONGO_AMNESTY_MODULESTORE, ModuleStoreTestCase, SharedModuleStoreTestCase,
|
||||
@@ -30,7 +29,6 @@ from common.djangoapps.student.tests.factories import OrgStaffFactory
|
||||
from common.djangoapps.student.tests.factories import StaffFactory
|
||||
from lms.djangoapps.courseware.module_render import load_single_xblock
|
||||
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.django_comment_common.models import (
|
||||
FORUM_ROLE_ADMINISTRATOR,
|
||||
@@ -59,7 +57,7 @@ User = get_user_model()
|
||||
|
||||
|
||||
@patch("crum.get_current_request")
|
||||
def _get_content_from_fragment(block, user_id, course, request_factory, mock_get_current_request):
|
||||
def _get_content_from_fragment(_store, block, user_id, course, request_factory, mock_get_current_request):
|
||||
"""
|
||||
Returns the content from the rendered fragment of a block
|
||||
Arguments:
|
||||
@@ -84,29 +82,30 @@ def _get_content_from_fragment(block, user_id, course, request_factory, mock_get
|
||||
return frag.content
|
||||
|
||||
|
||||
def _get_content_from_lms_index(block, user_id, course, request_factory):
|
||||
def _get_content_from_lms_index(store, block, user_id, _course, _request_factory):
|
||||
"""
|
||||
Returns the content from the lms index view of the block
|
||||
Arguments:
|
||||
block: some sort of xblock descriptor, must implement .scope_ids.usage_id
|
||||
user_id (int): id of user
|
||||
course_id (CourseLocator): id of course
|
||||
"""
|
||||
client = Client()
|
||||
client.login(username=User.objects.get(id=user_id).username, password=TEST_PASSWORD)
|
||||
|
||||
# Re-load the block from the store to ensure that its `parent` field is filled out correctly
|
||||
block = store.get_item(block.location)
|
||||
|
||||
page_content = client.get(
|
||||
reverse('courseware', kwargs={'course_id': block.scope_ids.usage_id.course_key}),
|
||||
reverse('render_xblock', args=[str(block.parent)]) + '?recheck_access=1',
|
||||
follow=True,
|
||||
)
|
||||
page = pq(page_content.content)
|
||||
seq_contents = page('#seq_contents_0').html()
|
||||
seq = pq(seq_contents)
|
||||
block_contents = seq(f'[data-id="{block.scope_ids.usage_id}"]')
|
||||
|
||||
page = pq(page_content.content)
|
||||
block_contents = page(f'[data-id="{block.scope_ids.usage_id}"]')
|
||||
return block_contents.html()
|
||||
|
||||
|
||||
def _assert_block_is_gated(block, is_gated, user, course, request_factory, has_upgrade_link=True):
|
||||
def _assert_block_is_gated(store, block, is_gated, user, course, request_factory, has_upgrade_link=True):
|
||||
"""
|
||||
Asserts that a block in a specific course is gated for a specific user
|
||||
Arguments:
|
||||
@@ -118,7 +117,7 @@ def _assert_block_is_gated(block, is_gated, user, course, request_factory, has_u
|
||||
checkout_link = '#' if has_upgrade_link else None
|
||||
for content_getter in (_get_content_from_fragment, _get_content_from_lms_index):
|
||||
with patch.object(ContentTypeGatingPartition, '_get_checkout_link', return_value=checkout_link):
|
||||
content = content_getter(block, user.id, course, request_factory) # pylint: disable=no-value-for-parameter
|
||||
content = content_getter(store, block, user.id, course, request_factory) # pylint: disable=no-value-for-parameter
|
||||
if is_gated:
|
||||
assert 'content-paywall' in content
|
||||
if has_upgrade_link:
|
||||
@@ -148,7 +147,7 @@ def _assert_block_is_gated(block, is_gated, user, course, request_factory, has_u
|
||||
assert 'student_view_data' in course_api_block
|
||||
|
||||
|
||||
def _assert_block_is_empty(block, user_id, course, request_factory):
|
||||
def _assert_block_is_empty(store, block, user_id, course, request_factory):
|
||||
"""
|
||||
Asserts that a block in a specific course is empty for a specific user
|
||||
Arguments:
|
||||
@@ -157,7 +156,7 @@ def _assert_block_is_empty(block, user_id, course, request_factory):
|
||||
user_id (int): id of user
|
||||
course_id (CourseLocator): id of course
|
||||
"""
|
||||
content = _get_content_from_lms_index(block, user_id, course, request_factory)
|
||||
content = _get_content_from_lms_index(store, block, user_id, course, request_factory)
|
||||
assert content is None
|
||||
|
||||
|
||||
@@ -165,7 +164,6 @@ def _assert_block_is_empty(block, user_id, course, request_factory):
|
||||
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
|
||||
'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride',
|
||||
))
|
||||
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
|
||||
class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pylint: disable=missing-class-docstring
|
||||
|
||||
PROBLEM_TYPES = ['problem', 'openassessment', 'drag-and-drop-v2', 'done', 'edx_sga']
|
||||
@@ -465,6 +463,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
@ddt.unpack
|
||||
def test_access_to_problems(self, prob_type, is_gated):
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=self.blocks_dict[prob_type],
|
||||
user=self.users['audit'],
|
||||
course=self.course,
|
||||
@@ -472,6 +471,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
request_factory=self.factory,
|
||||
)
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=self.blocks_dict[prob_type],
|
||||
user=self.users['verified'],
|
||||
course=self.course,
|
||||
@@ -487,6 +487,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
# Verify that graded, has_score and weight must all be true for a component to be gated
|
||||
block = self.graded_score_weight_blocks[(graded, has_score, weight)]
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=block,
|
||||
user=self.audit_user,
|
||||
course=self.course,
|
||||
@@ -522,6 +523,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
All users should have access to non-problem component types, the 'html' components test that.
|
||||
"""
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=self.courses[course]['blocks'][component_type],
|
||||
user=self.users[user_track],
|
||||
course=self.courses[course]['course'],
|
||||
@@ -535,6 +537,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
the user will continue to see gated content, but the upgrade messaging will be removed.
|
||||
"""
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=self.courses['expired_upgrade_deadline']['blocks']['problem'],
|
||||
user=self.users['audit'],
|
||||
course=self.courses['expired_upgrade_deadline']['course'],
|
||||
@@ -599,6 +602,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
|
||||
# assert that course team members have access to graded content
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=self.blocks_dict['problem'],
|
||||
user=user,
|
||||
course=self.course,
|
||||
@@ -626,6 +630,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
mode='audit',
|
||||
)
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=self.blocks_dict['problem'],
|
||||
user=user,
|
||||
course=self.course,
|
||||
@@ -650,6 +655,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
graded, has_score, weight = True, True, 1
|
||||
block = self.graded_score_weight_blocks[(graded, has_score, weight)]
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=block,
|
||||
user=user,
|
||||
course=self.course,
|
||||
@@ -758,6 +764,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
self.update_masquerade(username=user.username)
|
||||
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=self.blocks_dict['problem'],
|
||||
user=user,
|
||||
course=self.course,
|
||||
@@ -772,12 +779,8 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
def test_discount_display(self):
|
||||
|
||||
with patch.object(ContentTypeGatingPartition, '_get_checkout_link', return_value='#'):
|
||||
block_content = _get_content_from_lms_index(
|
||||
block=self.blocks_dict['problem'],
|
||||
user_id=self.audit_user.id,
|
||||
course=self.course,
|
||||
request_factory=self.factory,
|
||||
)
|
||||
block_content = _get_content_from_lms_index(self.store, self.blocks_dict['problem'], self.audit_user.id,
|
||||
self.course, self.factory)
|
||||
|
||||
assert '<span>DISCOUNT_PRICE</span>' in block_content
|
||||
|
||||
@@ -785,7 +788,6 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase, MasqueradeMixin): # pyli
|
||||
@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 @@ class TestConditionalContentAccess(TestConditionalContent):
|
||||
|
||||
# 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 @@ class TestConditionalContentAccess(TestConditionalContent):
|
||||
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 @@ class TestConditionalContentAccess(TestConditionalContent):
|
||||
|
||||
# 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 @@ class TestConditionalContentAccess(TestConditionalContent):
|
||||
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 @@ class TestConditionalContentAccess(TestConditionalContent):
|
||||
@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 @@ class TestMessageDeduplication(ModuleStoreTestCase):
|
||||
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 @@ class TestMessageDeduplication(ModuleStoreTestCase):
|
||||
mode='audit'
|
||||
)
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=blocks_dict['graded_1'],
|
||||
user=self.user,
|
||||
course=course['course'],
|
||||
@@ -985,6 +992,7 @@ class TestMessageDeduplication(ModuleStoreTestCase):
|
||||
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 @@ class TestMessageDeduplication(ModuleStoreTestCase):
|
||||
mode='audit'
|
||||
)
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=blocks_dict['graded_1'],
|
||||
user=self.user,
|
||||
course=course['course'],
|
||||
@@ -1032,18 +1041,21 @@ class TestMessageDeduplication(ModuleStoreTestCase):
|
||||
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 @@ class TestMessageDeduplication(ModuleStoreTestCase):
|
||||
mode='audit'
|
||||
)
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=blocks_dict['graded_1'],
|
||||
user=self.user,
|
||||
course=course['course'],
|
||||
@@ -1084,6 +1097,7 @@ class TestMessageDeduplication(ModuleStoreTestCase):
|
||||
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 @@ class TestMessageDeduplication(ModuleStoreTestCase):
|
||||
request_factory=self.request_factory,
|
||||
)
|
||||
_assert_block_is_gated(
|
||||
self.store,
|
||||
block=blocks_dict['graded_3'],
|
||||
user=self.user,
|
||||
course=course['course'],
|
||||
|
||||
@@ -9,7 +9,6 @@ import ddt
|
||||
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 OrgInstructorFactory
|
||||
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 @@ from openedx.features.course_experience.tests.views.helpers import add_course_mo
|
||||
|
||||
# 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 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
|
||||
|
||||
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):
|
||||
|
||||
@@ -284,4 +284,4 @@ class GetCoursewareUrlTests(SharedModuleStoreTestCase):
|
||||
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()
|
||||
|
||||
@@ -11,7 +11,7 @@ from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
|
||||
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 @@ TEST_VERIFICATION_SOCK_LOCATOR = '<div class="verification-sock"'
|
||||
|
||||
|
||||
@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 @@ class TestCourseSockView(SharedModuleStoreTestCase):
|
||||
|
||||
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)
|
||||
|
||||
@@ -5,8 +5,7 @@ Tests for masquerading functionality on course_experience
|
||||
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 @@ class MasqueradeTestBase(SharedModuleStoreTestCase, MasqueradeMixin):
|
||||
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 @@ class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
|
||||
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 @@ class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
|
||||
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(
|
||||
|
||||
@@ -4,7 +4,6 @@ Helper functions for logic related to learning (courseare & course home) URLs.
|
||||
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 @@ from xmodule.modulestore.search import navigation_index, path_to_location # lin
|
||||
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
|
||||
|
||||
@@ -14,17 +14,14 @@ import simplejson as json
|
||||
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 @@ class TestRecommender(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
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 = [
|
||||
(
|
||||
|
||||
@@ -48,14 +48,12 @@ import pytz
|
||||
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 @@ class XBlockScenarioTestCaseMixin:
|
||||
)
|
||||
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 @@ class XBlockStudentTestCaseMixin:
|
||||
self.login(email, password)
|
||||
|
||||
|
||||
@override_waffle_flag(COURSEWARE_USE_LEGACY_FRONTEND, active=True)
|
||||
class XBlockTestCase(XBlockStudentTestCaseMixin,
|
||||
XBlockScenarioTestCaseMixin,
|
||||
XBlockEventTestMixin,
|
||||
|
||||
Reference in New Issue
Block a user