test: Drop testing for legacy courseware UI

This has all been replaced by the learning MFE and will be removed from
the platform in subsequent commits.

For masquerade testing, the page no longer renders content and so
shouldn't be a part of this test.  The render_xblock url is what is used
by the MFE so we're still testing that the course-wide content is being
loaded correctly for content served by the learning MFE.
This commit is contained in:
Feanil Patel
2025-05-22 11:09:48 -04:00
parent 0121a0ad9e
commit 3520b6b8e1
4 changed files with 2 additions and 929 deletions

View File

@@ -244,51 +244,6 @@ class TestMasqueradeOptionsNoContentGroups(StaffMasqueradeTestCase):
assert is_target_available == expected
# These tests are testing a capability of the old courseware page. We have to not
# force redirect to the new MFE in order to be able to load the old pages which are
# being tested by this page.
#
# This is a temporary change, until we can remove the old courseware pages
# all together.
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
class TestStaffMasqueradeAsStudent(StaffMasqueradeTestCase):
"""
Check for staff being able to masquerade as student.
"""
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_staff_debug_with_masquerade(self, mock_redirect):
"""
Tests that staff debug control is not visible when masquerading as a student.
"""
# Verify staff initially can see staff debug
self.verify_staff_debug_present(True)
# Toggle masquerade to student
self.update_masquerade(role='student')
self.verify_staff_debug_present(False)
# Toggle masquerade back to staff
self.update_masquerade(role='staff')
self.verify_staff_debug_present(True)
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_show_answer_for_staff(self, mock_redirect):
"""
Tests that "Show Answer" is not visible when masquerading as a student.
"""
# Verify that staff initially can see "Show Answer".
self.verify_show_answer_present(True)
# Toggle masquerade to student
self.update_masquerade(role='student')
self.verify_show_answer_present(False)
# Toggle masquerade back to staff
self.update_masquerade(role='staff')
self.verify_show_answer_present(True)
@ddt.ddt
class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmissionTestMixin):
"""

View File

@@ -1,285 +0,0 @@
"""
This test file will run through some LMS test scenarios regarding access and navigation of the LMS
"""
import time
from unittest.mock import patch
from django.conf import settings
from django.test.utils import override_settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
from common.djangoapps.student.tests.factories import GlobalStaffFactory
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
from openedx.features.course_experience import DISABLE_COURSE_OUTLINE_PAGE_FLAG
from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url
class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Check that navigation state is saved properly.
"""
@classmethod
def setUpClass(cls):
# pylint: disable=super-method-not-called
with super().setUpClassAndTestData():
cls.test_course = CourseFactory.create()
cls.test_course_proctored = CourseFactory.create()
cls.course = CourseFactory.create()
@classmethod
def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called
cls.chapter0 = BlockFactory.create(parent=cls.course,
display_name='Overview')
cls.chapter9 = BlockFactory.create(parent=cls.course,
display_name='factory_chapter')
cls.section0 = BlockFactory.create(parent=cls.chapter0,
display_name='Welcome')
cls.section9 = BlockFactory.create(parent=cls.chapter9,
display_name='factory_section')
cls.unit0 = BlockFactory.create(parent=cls.section0,
display_name='New Unit 0')
cls.chapterchrome = BlockFactory.create(parent=cls.course,
display_name='Chrome')
cls.chromelesssection = BlockFactory.create(parent=cls.chapterchrome,
display_name='chromeless',
chrome='none')
cls.accordionsection = BlockFactory.create(parent=cls.chapterchrome,
display_name='accordion',
chrome='accordion')
cls.tabssection = BlockFactory.create(parent=cls.chapterchrome,
display_name='tabs',
chrome='tabs')
cls.defaultchromesection = BlockFactory.create(
parent=cls.chapterchrome,
display_name='defaultchrome',
)
cls.fullchromesection = BlockFactory.create(parent=cls.chapterchrome,
display_name='fullchrome',
chrome='accordion,tabs')
cls.tabtest = BlockFactory.create(parent=cls.chapterchrome,
display_name='pdf_textbooks_tab',
default_tab='progress')
cls.user = GlobalStaffFactory(password='test')
def setUp(self):
super().setUp()
self.login(self.user.email, 'test')
self.patcher = patch(
'lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
self.mock_redirect = self.patcher.start()
def tearDown(self):
self.patcher.stop()
super().tearDown()
def assertTabActive(self, tabname, response):
''' Check if the progress tab is active in the tab set '''
for line in response.content.decode('utf-8').split('\n'):
if tabname in line and 'active' in line:
return
raise AssertionError(f"assertTabActive failed: {tabname} not active")
def assertTabInactive(self, tabname, response): # lint-amnesty, pylint: disable=useless-return
''' Check if the progress tab is active in the tab set '''
for line in response.content.decode('utf-8').split('\n'):
if tabname in line and 'active' in line:
raise AssertionError("assertTabInactive failed: " + tabname + " active")
return
# TODO: LEARNER-71: Do we need to adjust or remove this test?
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
def test_chrome_settings(self):
'''
Test settings for disabling and modifying navigation chrome in the courseware:
- Accordion enabled, or disabled
- Navigation tabs enabled, disabled, or redirected
'''
test_data = (
('tabs', False, True),
('none', False, False),
('accordion', True, False),
('fullchrome', True, True),
)
for (displayname, accordion, tabs) in test_data:
response = self.client.get(reverse('courseware_section', kwargs={
'course_id': str(self.course.id),
'chapter': 'Chrome',
'section': displayname,
}))
assert ('course-tabs' in response.content.decode('utf-8')) == tabs
assert ('course-navigation' in response.content.decode('utf-8')) == accordion
self.assertTabInactive('progress', response)
self.assertTabActive(make_learning_mfe_courseware_url(self.course.id), response)
response = self.client.get(reverse('courseware_section', kwargs={
'course_id': str(self.course.id),
'chapter': 'Chrome',
'section': 'pdf_textbooks_tab',
}))
self.assertTabActive('progress', response)
self.assertTabInactive(make_learning_mfe_courseware_url(self.course.id), response)
@override_settings(SESSION_INACTIVITY_TIMEOUT_IN_SECONDS=1)
def test_inactive_session_timeout(self):
"""
Verify that an inactive session times out and redirects to the
login page
"""
# make sure we can access courseware immediately
resp = self.client.get(reverse('dashboard'))
assert resp.status_code == 200
# then wait a bit and see if we get timed out
time.sleep(2)
resp = self.client.get(reverse('dashboard'))
# re-request, and we should get a redirect to login page
self.assertRedirects(resp, settings.LOGIN_REDIRECT_URL + '?next=' + reverse('dashboard'))
def test_redirects_first_time(self):
"""
Verify that the first time we click on the courseware tab we are
redirected to the 'Welcome' section.
"""
resp = self.client.get(reverse('courseware',
kwargs={'course_id': str(self.course.id)}))
self.assertRedirects(resp, reverse(
'courseware_section', kwargs={'course_id': str(self.course.id),
'chapter': 'Overview',
'section': 'Welcome'}))
def test_redirects_second_time(self):
"""
Verify the accordion remembers we've already visited the Welcome section
and redirects correspondingly.
"""
section_url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': 'Overview',
'section': 'Welcome',
},
)
self.client.get(section_url)
resp = self.client.get(
reverse('courseware', kwargs={'course_id': str(self.course.id)}),
)
self.assertRedirects(resp, section_url)
def test_accordion_state(self):
"""
Verify the accordion remembers which chapter you were last viewing.
"""
# Now we directly navigate to a section in a chapter other than 'Overview'.
section_url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': 'factory_chapter',
'section': 'factory_section',
}
)
self.assert_request_status_code(200, section_url)
# And now hitting the courseware tab should redirect to 'factory_chapter'
url = reverse(
'courseware',
kwargs={'course_id': str(self.course.id)}
)
resp = self.client.get(url)
self.assertRedirects(resp, section_url)
# 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):
test_course_id = str(self.test_course.id)
url = reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
response = self.assert_request_status_code(200, url)
self.assertContains(response, "No content has been added to this course")
section = BlockFactory.create(
parent_location=self.test_course.location,
display_name='New Section'
)
url = reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
response = self.assert_request_status_code(200, url)
self.assertNotContains(response, "No content has been added to this course")
self.assertContains(response, "New Section")
subsection = BlockFactory.create(
parent_location=section.location,
display_name='New Subsection',
)
url = reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
response = self.assert_request_status_code(200, url)
self.assertContains(response, "New Subsection")
self.assertNotContains(response, "sequence-nav")
BlockFactory.create(
parent_location=subsection.location,
display_name='New Unit',
)
url = reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
self.assert_request_status_code(302, url)
def test_proctoring_js_includes(self):
"""
Make sure that proctoring JS does not get included on
courseware pages if either the FEATURE flag is turned off
or the course is not proctored enabled
"""
test_course_id = str(self.test_course_proctored.id)
with patch.dict(settings.FEATURES, {'ENABLE_SPECIAL_EXAMS': False}):
url = reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
resp = self.client.get(url)
self.assertNotContains(resp, '/static/js/lms-proctoring.js')
with patch.dict(settings.FEATURES, {'ENABLE_SPECIAL_EXAMS': True}):
url = reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
resp = self.client.get(url)
self.assertNotContains(resp, '/static/js/lms-proctoring.js')
# now set up a course which is proctored enabled
self.test_course_proctored.enable_proctored_exams = True
self.test_course_proctored.save()
modulestore().update_item(self.test_course_proctored, self.user.id)
resp = self.client.get(url)
self.assertContains(resp, '/static/js/lms-proctoring.js')

View File

@@ -63,7 +63,7 @@ class TestViewAuth(EnterpriseTestConsentRequired, ModuleStoreTestCase, LoginEnro
Check that non-staff don't have access to dark urls.
"""
names = ['courseware', 'progress']
names = ['progress']
urls = self._reverse_urls(names, course)
urls.extend([
reverse('book', kwargs={'course_id': str(course.id),
@@ -106,10 +106,6 @@ class TestViewAuth(EnterpriseTestConsentRequired, ModuleStoreTestCase, LoginEnro
)
self.assert_request_status_code(302, url)
# The courseware url should redirect, not 200
url = self._reverse_urls(['courseware'], course)[0]
self.assert_request_status_code(302, url)
def login(self, user): # lint-amnesty, pylint: disable=arguments-differ
return super().login(user.email, self.TEST_PASSWORD)
@@ -164,60 +160,6 @@ class TestViewAuth(EnterpriseTestConsentRequired, ModuleStoreTestCase, LoginEnro
self.org_staff_user = OrgStaffFactory(course_key=self.course.id)
self.org_instructor_user = OrgInstructorFactory(course_key=self.course.id)
def test_redirection_unenrolled(self):
"""
Verify unenrolled student is redirected to the 'about' section of the chapter
instead of the 'Welcome' section after clicking on the courseware tab.
"""
self.login(self.unenrolled_user)
response = self.client.get(reverse('courseware',
kwargs={'course_id': str(self.course.id)}))
self.assertRedirects(
response,
reverse(
'about_course',
args=[str(self.course.id)]
)
)
def test_redirection_enrolled(self):
"""
Verify enrolled student is redirected to the 'Welcome' section of
the chapter after clicking on the courseware tab.
"""
self.login(self.enrolled_user)
response = self.client.get(
reverse(
'courseware',
kwargs={'course_id': str(self.course.id)}
)
)
self.assertRedirects(
response,
reverse(
'courseware_section',
kwargs={'course_id': str(self.course.id),
'chapter': self.overview_chapter.url_name,
'section': self.welcome_section.url_name}
),
fetch_redirect_response=False, # just sends us on to MFE
)
def test_redirection_missing_enterprise_consent(self):
"""
Verify that enrolled students are redirected to the Enterprise consent
URL if a linked Enterprise Customer requires data sharing consent
and it has not yet been provided.
"""
self.login(self.enrolled_user)
url = reverse(
'courseware',
kwargs={'course_id': str(self.course.id)}
)
self.verify_consent_required(self.client, url, status_code=302) # lint-amnesty, pylint: disable=no-value-for-parameter
def test_instructor_page_access_nonstaff(self):
"""
Verify non-staff cannot load the instructor

View File

@@ -109,6 +109,7 @@ from openedx.features.course_experience import (
from openedx.features.course_experience.tests.views.helpers import add_course_mode
from openedx.features.course_experience.url_helpers import (
get_learning_mfe_home_url,
get_courseware_url,
make_learning_mfe_courseware_url
)
from openedx.features.enterprise_support.tests.factories import (
@@ -248,42 +249,6 @@ class TestJumpTo(ModuleStoreTestCase):
assert response.status_code == 404
class IndexQueryTestCase(ModuleStoreTestCase):
"""
Tests for query count.
"""
NUM_PROBLEMS = 20
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
def test_index_query_counts(self, mock_redirect):
# TODO: decrease query count as part of REVO-28
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
with self.store.default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()
with self.store.bulk_operations(course.id):
chapter = BlockFactory.create(category='chapter', parent_location=course.location)
section = BlockFactory.create(category='sequential', parent_location=chapter.location)
vertical = BlockFactory.create(category='vertical', parent_location=section.location)
for _ in range(self.NUM_PROBLEMS):
BlockFactory.create(category='problem', parent_location=vertical.location)
self.client.login(username=self.user.username, password=self.user_password)
CourseEnrollment.enroll(self.user, course.id)
with self.assertNumQueries(152, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
with check_mongo_calls(3):
url = reverse(
'courseware_section',
kwargs={
'course_id': str(course.id),
'chapter': str(chapter.location.block_id),
'section': str(section.location.block_id),
}
)
response = self.client.get(url)
assert response.status_code == 200
class BaseViewsTestCase(ModuleStoreTestCase, MasqueradeMixin):
"""Base class for courseware tests"""
CREATE_USER = False
@@ -357,108 +322,6 @@ class BaseViewsTestCase(ModuleStoreTestCase, MasqueradeMixin):
assert self.client.login(username=self.global_staff.username, password=TEST_PASSWORD)
@ddt.ddt
class CoursewareIndexTestCase(BaseViewsTestCase):
"""
Tests for the courseware index view, used for instructor previews.
"""
def setUp(self):
super().setUp()
self._create_global_staff_user() # this view needs staff permission
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
def test_index_success(self, mock_redirect):
response = self._verify_index_response()
self.assertContains(response, self.problem2.location.replace(branch=None, version_guid=None))
# re-access to the main course page redirects to last accessed view.
url = reverse('courseware', kwargs={'course_id': str(self.course_key)})
response = self.client.get(url)
assert response.status_code == 302
response = self.client.get(response.url)
self.assertNotContains(response, self.problem.location.replace(branch=None, version_guid=None))
self.assertContains(response, self.problem2.location.replace(branch=None, version_guid=None))
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):
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):
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):
"""
Verifies the response when the courseware index page is accessed with
the given chapter and section names.
"""
url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course_key),
'chapter': str(self.chapter.location.block_id) if chapter_name is None else chapter_name,
'section': str(self.section2.location.block_id) if section_name is None else section_name,
}
)
response = self.client.get(url)
assert response.status_code == expected_response_code
return response
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
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
# TODO: TNL-6387: Remove test
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
def test_accordion(self, mock_redirect):
"""
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)
@ddt.ddt
class ViewsTestCase(BaseViewsTestCase):
"""
@@ -1046,56 +909,6 @@ class TestProgressDueDate(BaseDueDateTests):
return self.client.get(reverse('progress', args=[str(course.id)]))
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
class TestAccordionDueDate(BaseDueDateTests):
"""
Test that the accordion page displays due dates correctly
"""
__test__ = True
def setUp(self):
super().setUp()
self.patcher = patch(
'lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
self.mock_redirect = self.patcher.start()
def tearDown(self):
self.patcher.stop()
super().tearDown()
def get_response(self, course):
""" Returns the HTML for the accordion """
return self.client.get(
reverse('courseware', args=[str(course.id)]),
follow=True
)
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
def test_backwards_compatibility(self):
super().test_backwards_compatibility()
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
def test_defaults(self):
super().test_defaults()
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
def test_format_date(self):
super().test_format_date()
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
def test_format_invalid(self):
super().test_format_invalid()
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
@override_waffle_flag(DISABLE_COURSE_OUTLINE_PAGE_FLAG, active=True)
def test_format_none(self):
super().test_format_none()
class StartDateTests(ModuleStoreTestCase):
"""
Test that start dates are properly localized and displayed on the student
@@ -2275,72 +2088,6 @@ class TestIndexView(ModuleStoreTestCase):
"""
Tests of the courseware.views.index view.
"""
@XBlock.register_temp_plugin(ViewCheckerBlock, 'view_checker')
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
def test_student_state(self, mock_redirect):
"""
Verify that saved student state is loaded for xblocks rendered in the index view.
"""
with modulestore().default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()
chapter = BlockFactory.create(parent_location=course.location, category='chapter')
section = BlockFactory.create(parent_location=chapter.location, category='view_checker',
display_name="Sequence Checker")
vertical = BlockFactory.create(parent_location=section.location, category='view_checker',
display_name="Vertical Checker")
block = BlockFactory.create(parent_location=vertical.location, category='view_checker',
display_name="Block Checker")
for item in (section, vertical, block):
StudentModuleFactory.create(
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=self.user, course_id=course.id)
assert self.client.login(username=self.user.username, password=self.user_password)
response = self.client.get(
reverse(
'courseware_section',
kwargs={
'course_id': str(course.id),
'chapter': chapter.url_name,
'section': section.url_name,
}
)
)
# Trigger the assertions embedded in the ViewCheckerBlocks
self.assertContains(response, "ViewCheckerPassed", count=3)
@XBlock.register_temp_plugin(ActivateIDCheckerBlock, 'id_checker')
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
def test_activate_block_id(self, mock_redirect):
course = CourseFactory.create()
with self.store.bulk_operations(course.id):
chapter = BlockFactory.create(parent=course, category='chapter')
section = BlockFactory.create(parent=chapter, category='sequential', display_name="Sequence")
vertical = BlockFactory.create(parent=section, category='vertical', display_name="Vertical")
BlockFactory.create(parent=vertical, category='id_checker', display_name="ID Checker")
CourseOverview.load_from_module_store(course.id)
CourseEnrollmentFactory(user=self.user, course_id=course.id)
assert self.client.login(username=self.user.username, password=self.user_password)
response = self.client.get(
reverse(
'courseware_section',
kwargs={
'course_id': str(course.id),
'chapter': chapter.url_name,
'section': section.url_name,
}
) + '?activate_block_id=test_block_id'
)
self.assertContains(response, "Activate Block ID: test_block_id")
@patch('lms.djangoapps.courseware.views.views.CourseTabView.course_open_for_learner_enrollment')
@patch('openedx.core.djangoapps.util.user_messages.PageLevelMessages.register_warning_message')
@@ -2425,207 +2172,6 @@ class TestIndexView(ModuleStoreTestCase):
assert views.CourseTabView.course_open_for_learner_enrollment(course) == expected_should_show_enroll_button
@ddt.ddt
class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin):
"""
Tests CompleteOnView is set up correctly in CoursewareIndex.
"""
def setup_course(self, default_store):
"""
Set up course content for modulestore.
"""
# pylint:disable=attribute-defined-outside-init
self.request_factory = RequestFactoryNoCsrf()
with modulestore().default_store(default_store):
self.course = CourseFactory.create()
with self.store.bulk_operations(self.course.id):
self.chapter = BlockFactory.create(
parent_location=self.course.location, category='chapter', display_name='Week 1'
)
self.section_1 = BlockFactory.create(
parent_location=self.chapter.location, category='sequential', display_name='Lesson 1'
)
self.vertical_1 = BlockFactory.create(
parent_location=self.section_1.location, category='vertical', display_name='Subsection 1'
)
self.html_1_1 = BlockFactory.create(
parent_location=self.vertical_1.location, category='html', display_name="HTML 1_1"
)
self.problem_1 = BlockFactory.create(
parent_location=self.vertical_1.location, category='problem', display_name="Problem 1"
)
self.html_1_2 = BlockFactory.create(
parent_location=self.vertical_1.location, category='html', display_name="HTML 1_2"
)
self.section_2 = BlockFactory.create(
parent_location=self.chapter.location, category='sequential', display_name='Lesson 2'
)
self.vertical_2 = BlockFactory.create(
parent_location=self.section_2.location, category='vertical', display_name='Subsection 2'
)
self.video_2 = BlockFactory.create(
parent_location=self.vertical_2.location, category='video', display_name="Video 2"
)
self.problem_2 = BlockFactory.create(
parent_location=self.vertical_2.location, category='problem', display_name="Problem 2"
)
self.section_1_url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.chapter.url_name,
'section': self.section_1.url_name,
}
)
self.section_2_url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': self.chapter.url_name,
'section': self.section_2.url_name,
}
)
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)
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
def test_completion_service_disabled(self, mock_redirect):
self.setup_course(ModuleStoreEnum.Type.split)
response = self.client.get(self.section_1_url)
self.assertNotContains(response, 'data-mark-completed-on-view-after-delay')
response = self.client.get(self.section_2_url)
self.assertNotContains(response, 'data-mark-completed-on-view-after-delay')
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
def test_completion_service_enabled(self, mock_redirect):
self.override_waffle_switch(True)
self.setup_course(ModuleStoreEnum.Type.split)
response = self.client.get(self.section_1_url)
self.assertContains(response, 'data-mark-completed-on-view-after-delay')
self.assertContains(response, 'data-mark-completed-on-view-after-delay', count=2)
request = self.request_factory.post(
'/',
data=json.dumps({"completion": 1}),
content_type='application/json',
)
request.user = self.user
request.session = {}
response = handle_xblock_callback(
request,
str(self.course.id),
quote_slashes(str(self.html_1_1.scope_ids.usage_id)),
'publish_completion',
)
assert json.loads(response.content.decode('utf-8')) == {'result': 'ok'}
response = self.client.get(self.section_1_url)
self.assertContains(response, 'data-mark-completed-on-view-after-delay')
self.assertContains(response, 'data-mark-completed-on-view-after-delay', count=1)
request = self.request_factory.post(
'/',
data=json.dumps({"completion": 1}),
content_type='application/json',
)
request.user = self.user
request.session = {}
response = handle_xblock_callback(
request,
str(self.course.id),
quote_slashes(str(self.html_1_2.scope_ids.usage_id)),
'publish_completion',
)
assert json.loads(response.content.decode('utf-8')) == {'result': 'ok'}
response = self.client.get(self.section_1_url)
self.assertNotContains(response, 'data-mark-completed-on-view-after-delay')
response = self.client.get(self.section_2_url)
self.assertNotContains(response, 'data-mark-completed-on-view-after-delay')
@ddt.ddt
class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
"""
Test the index view to handle vertical positions. Confirms that first position is loaded
if input position is non-positive or greater than number of positions available.
"""
def setUp(self):
"""
Set up initial test data
"""
super().setUp()
# create course with 3 positions
self.course = CourseFactory.create()
with self.store.bulk_operations(self.course.id):
self.chapter = BlockFactory.create(parent_location=self.course.location, category='chapter')
self.section = BlockFactory.create(parent_location=self.chapter.location, category='sequential',
display_name="Sequence")
BlockFactory.create(parent_location=self.section.location, category='vertical', display_name="Vertical1")
BlockFactory.create(parent_location=self.section.location, category='vertical', display_name="Vertical2")
BlockFactory.create(parent_location=self.section.location, category='vertical', display_name="Vertical3")
CourseOverview.load_from_module_store(self.course.id)
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):
"""
Returns client response to input position.
"""
return self.client.get(
reverse(
'courseware_position',
kwargs={
'course_id': str(self.course.id),
'chapter': self.chapter.url_name,
'section': self.section.url_name,
'position': input_position,
}
)
)
def _assert_correct_position(self, response, expected_position):
"""
Asserts that the expected position and the position in the response are the same
"""
self.assertContains(response, f'data-position="{expected_position}"')
@ddt.data(("-1", 1), ("0", 1), ("-0", 1), ("2", 2), ("5", 1))
@ddt.unpack
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
def test_vertical_positions(self, input_position, expected_position, mock_redirect):
"""
Tests the following cases:
* Load first position when negative position inputted.
* Load first position when 0/-0 position inputted.
* Load given position when 0 < input_position <= num_positions_available.
* Load first position when positive position > num_positions_available.
"""
resp = self._get_course_vertical_by_position(input_position)
self._assert_correct_position(resp, expected_position)
@ddt.ddt
class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase, CompletionWaffleTestMixin):
"""
@@ -3007,89 +2553,6 @@ class TestRenderXBlockSelfPaced(TestRenderXBlock): # lint-amnesty, pylint: disa
return options
class TestIndexViewCrawlerStudentStateWrites(SharedModuleStoreTestCase):
"""
Ensure that courseware index requests do not trigger student state writes.
This is to prevent locking issues that have caused latency spikes in the
courseware_studentmodule table when concurrent requests each try to update
the same rows for sequence, section, and course positions.
"""
@classmethod
def setUpClass(cls):
"""Set up the simplest course possible."""
# setUpClassAndTestData() already calls setUpClass on SharedModuleStoreTestCase
# pylint: disable=super-method-not-called
with super().setUpClassAndTestData():
cls.course = CourseFactory.create()
with cls.store.bulk_operations(cls.course.id):
cls.chapter = BlockFactory.create(category='chapter', parent_location=cls.course.location)
cls.section = BlockFactory.create(category='sequential', parent_location=cls.chapter.location)
cls.vertical = BlockFactory.create(category='vertical', parent_location=cls.section.location)
@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(is_staff=True)
CourseEnrollment.enroll(cls.user, cls.course.id)
def setUp(self):
"""Do the client login."""
super().setUp()
self.client.login(username=self.user.username, password=TEST_PASSWORD)
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
def test_write_by_default(self, mock_redirect):
"""By default, always write student state, regardless of user agent."""
with patch('lms.djangoapps.courseware.model_data.UserStateCache.set_many') as patched_state_client_set_many:
# Simulate someone using Chrome
self._load_courseware('Mozilla/5.0 AppleWebKit/537.36')
assert patched_state_client_set_many.called
patched_state_client_set_many.reset_mock()
# Common crawler user agent
self._load_courseware('edX-downloader/0.1')
assert patched_state_client_set_many.called
@patch('lms.djangoapps.courseware.views.index.CoursewareIndex._redirect_to_learning_mfe', return_value=None)
def test_writes_with_config(self, mock_redirect):
"""Test state writes (or lack thereof) based on config values."""
CrawlersConfig.objects.create(known_user_agents='edX-downloader,crawler_foo', enabled=True)
with patch('lms.djangoapps.courseware.model_data.UserStateCache.set_many') as patched_state_client_set_many:
# Exact matching of crawler user agent
self._load_courseware('crawler_foo')
assert not patched_state_client_set_many.called
# Partial matching of crawler user agent
self._load_courseware('edX-downloader/0.1')
assert not patched_state_client_set_many.called
# Simulate an actual browser hitting it (we should write)
self._load_courseware('Mozilla/5.0 AppleWebKit/537.36')
assert patched_state_client_set_many.called
# Disabling the crawlers config should revert us to default behavior
CrawlersConfig.objects.create(enabled=False)
# Disabling the violation because pylint just can't see that we'll get the mock_redirect param passed in via the
# patch.
self.test_write_by_default() # pylint: disable=no-value-for-parameter
def _load_courseware(self, user_agent):
"""Helper to load the actual courseware page."""
url = reverse(
'courseware_section',
kwargs={
'course_id': str(self.course.id),
'chapter': str(self.chapter.location.block_id),
'section': str(self.section.location.block_id),
}
)
response = self.client.get(url, HTTP_USER_AGENT=user_agent)
# Make sure we get back an actual 200, and aren't redirected because we
# messed up the setup somehow (e.g. didn't enroll properly)
assert response.status_code == 200
class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ModuleStoreTestCase):
"""
Ensure that the Enterprise Data Consent redirects are in place only when consent is required.
@@ -3112,7 +2575,6 @@ class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ModuleStoreTestCa
course_id = str(self.course.id)
for url in (
reverse("courseware", kwargs=dict(course_id=course_id)),
reverse("progress", kwargs=dict(course_id=course_id)),
reverse("student_progress", kwargs=dict(course_id=course_id, student_id=str(self.user.id))),
):
@@ -3343,7 +2805,6 @@ class TestCourseWideResources(ModuleStoreTestCase):
"""
@ddt.data(
('courseware', 'course_id', False, True),
('progress', 'course_id', False, False),
('instructor_dashboard', 'course_id', True, False),
('forum_form_discussion', 'course_id', False, False),