""" Tests courseware views.py """ import html import itertools import json from datetime import datetime, timedelta from uuid import uuid4 from unittest.mock import MagicMock, PropertyMock, call, create_autospec, patch from urllib.parse import quote, urlencode import ddt from completion.test_utils import CompletionWaffleTestMixin from crum import set_current_request from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.http import Http404, HttpResponseBadRequest from django.test import RequestFactory, TestCase from django.test.client import Client 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 markupsafe import escape from milestones.tests.utils import MilestonesTestCaseMixin from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator from pytz import UTC, utc from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Scope, String import lms.djangoapps.courseware.views.views as views from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory from freezegun import freeze_time # lint-amnesty, pylint: disable=wrong-import-order from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import ( CertificateGenerationConfiguration, CertificateStatuses, CertificateWhitelist ) from lms.djangoapps.certificates.tests.factories import CertificateInvalidationFactory, GeneratedCertificateFactory from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.utils import EcommerceService 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 GlobalStaffFactory, RequestFactoryNoCsrf, StudentModuleFactory from lms.djangoapps.courseware.tests.helpers import get_expiration_banner_text from lms.djangoapps.courseware.testutils import RenderXBlockTestMixin from lms.djangoapps.courseware.toggles import ( COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW, COURSEWARE_OPTIMIZED_RENDER_XBLOCK, REDIRECT_TO_COURSEWARE_MICROFRONTEND, ) from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient from lms.djangoapps.courseware.views.index import show_courseware_mfe_link from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag 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 from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.services import IDVerificationService from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.crawlers.models import CrawlersConfig 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, RELATIVE_DATES_FLAG ) from openedx.features.course_experience.tests.views.helpers import add_course_mode from openedx.features.course_experience.url_helpers import get_learning_mfe_courseware_url, get_legacy_courseware_url from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.roles import CourseStaffRole from common.djangoapps.student.tests.factories import TEST_PASSWORD, AdminFactory, CourseEnrollmentFactory, UserFactory from common.djangoapps.util.tests.test_date_utils import fake_pgettext, fake_ugettext from common.djangoapps.util.url import reload_django_url_config from common.djangoapps.util.views import ensure_valid_course_key from xmodule.course_module import COURSE_VISIBILITY_PRIVATE, COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE from xmodule.graders import ShowCorrectness from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ( TEST_DATA_MIXED_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE, CourseUserType, ModuleStoreTestCase, SharedModuleStoreTestCase ) from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES FEATURES_WITH_DISABLE_HONOR_CERTIFICATE = settings.FEATURES.copy() FEATURES_WITH_DISABLE_HONOR_CERTIFICATE['DISABLE_HONOR_CERTIFICATES'] = True @ddt.ddt class TestJumpTo(ModuleStoreTestCase): """ Check the jumpto link for a course. """ MODULESTORE = TEST_DATA_MIXED_MODULESTORE def setUp(self): super().setUp() # Use toy course from XML self.course_key = CourseKey.from_string('edX/toy/2012_Fall') def test_jumpto_invalid_location(self): location = self.course_key.make_usage_key(None, 'NoSuchPlace') # This is fragile, but unfortunately the problem is that within the LMS we # can't use the reverse calls from the CMS jumpto_url = '{}/{}/jump_to/{}'.format('/courses', str(self.course_key), str(location)) response = self.client.get(jumpto_url) assert response.status_code == 404 def test_jumpto_from_section(self): course = CourseFactory.create() chapter = ItemFactory.create(category='chapter', parent_location=course.location) section = ItemFactory.create(category='sequential', parent_location=chapter.location) expected = '/courses/{course_id}/courseware/{chapter_id}/{section_id}/?{activate_block_id}'.format( course_id=str(course.id), chapter_id=chapter.url_name, section_id=section.url_name, activate_block_id=urlencode({'activate_block_id': str(section.location)}) ) jumpto_url = '{}/{}/jump_to/{}'.format( '/courses', str(course.id), str(section.location), ) response = self.client.get(jumpto_url) self.assertRedirects(response, expected, status_code=302, target_status_code=302) def test_jumpto_from_module(self): course = CourseFactory.create() chapter = ItemFactory.create(category='chapter', parent_location=course.location) section = ItemFactory.create(category='sequential', parent_location=chapter.location) vertical1 = ItemFactory.create(category='vertical', parent_location=section.location) vertical2 = ItemFactory.create(category='vertical', parent_location=section.location) module1 = ItemFactory.create(category='html', parent_location=vertical1.location) module2 = ItemFactory.create(category='html', parent_location=vertical2.location) expected = '/courses/{course_id}/courseware/{chapter_id}/{section_id}/1?{activate_block_id}'.format( course_id=str(course.id), chapter_id=chapter.url_name, section_id=section.url_name, activate_block_id=urlencode({'activate_block_id': str(module1.location)}) ) jumpto_url = '{}/{}/jump_to/{}'.format( '/courses', str(course.id), str(module1.location), ) response = self.client.get(jumpto_url) self.assertRedirects(response, expected, status_code=302, target_status_code=302) expected = '/courses/{course_id}/courseware/{chapter_id}/{section_id}/2?{activate_block_id}'.format( course_id=str(course.id), chapter_id=chapter.url_name, section_id=section.url_name, activate_block_id=urlencode({'activate_block_id': str(module2.location)}) ) jumpto_url = '{}/{}/jump_to/{}'.format( '/courses', str(course.id), str(module2.location), ) response = self.client.get(jumpto_url) self.assertRedirects(response, expected, status_code=302, target_status_code=302) def test_jumpto_from_nested_module(self): course = CourseFactory.create() chapter = ItemFactory.create(category='chapter', parent_location=course.location) section = ItemFactory.create(category='sequential', parent_location=chapter.location) vertical = ItemFactory.create(category='vertical', parent_location=section.location) nested_section = ItemFactory.create(category='sequential', parent_location=vertical.location) nested_vertical1 = ItemFactory.create(category='vertical', parent_location=nested_section.location) # put a module into nested_vertical1 for completeness ItemFactory.create(category='html', parent_location=nested_vertical1.location) nested_vertical2 = ItemFactory.create(category='vertical', parent_location=nested_section.location) module2 = ItemFactory.create(category='html', parent_location=nested_vertical2.location) # internal position of module2 will be 1_2 (2nd item withing 1st item) expected = '/courses/{course_id}/courseware/{chapter_id}/{section_id}/1?{activate_block_id}'.format( course_id=str(course.id), chapter_id=chapter.url_name, section_id=section.url_name, activate_block_id=urlencode({'activate_block_id': str(module2.location)}) ) jumpto_url = '{}/{}/jump_to/{}'.format( '/courses', str(course.id), str(module2.location), ) response = self.client.get(jumpto_url) self.assertRedirects(response, expected, status_code=302, target_status_code=302) def test_jumpto_id_invalid_location(self): location = BlockUsageLocator(CourseLocator('edX', 'toy', 'NoSuchPlace', deprecated=True), None, None, deprecated=True) jumpto_url = '{}/{}/jump_to_id/{}'.format('/courses', str(self.course_key), str(location)) response = self.client.get(jumpto_url) assert response.status_code == 404 @ddt.data( (False, '1'), (True, '2') ) @ddt.unpack def test_jump_to_for_learner_with_staff_only_content(self, is_staff_user, position): """ Test for checking correct position in redirect_url for learner when a course has staff-only units. """ course = CourseFactory.create() request = RequestFactory().get('/') request.user = UserFactory(is_staff=is_staff_user, username="staff") request.session = {} course_key = CourseKey.from_string(str(course.id)) chapter = ItemFactory.create(category='chapter', parent_location=course.location) section = ItemFactory.create(category='sequential', parent_location=chapter.location) __ = ItemFactory.create(category='vertical', parent_location=section.location) staff_only_vertical = ItemFactory.create(category='vertical', parent_location=section.location, metadata=dict(visible_to_staff_only=True)) __ = ItemFactory.create(category='vertical', parent_location=section.location) usage_key = UsageKey.from_string(str(staff_only_vertical.location)).replace(course_key=course_key) expected_url = reverse( 'courseware_position', kwargs={ 'course_id': str(course.id), 'chapter': chapter.url_name, 'section': section.url_name, 'position': position, } ) expected_url += "?{}".format(urlencode({'activate_block_id': str(staff_only_vertical.location)})) assert expected_url == get_legacy_courseware_url(usage_key, request) @ddt.ddt class IndexQueryTestCase(ModuleStoreTestCase): """ Tests for query count. """ CREATE_USER = False NUM_PROBLEMS = 20 @ddt.data( (ModuleStoreEnum.Type.mongo, 10, 171), (ModuleStoreEnum.Type.split, 4, 167), ) @ddt.unpack def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): # 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(store_type): course = CourseFactory.create() with self.store.bulk_operations(course.id): chapter = ItemFactory.create(category='chapter', parent_location=course.location) section = ItemFactory.create(category='sequential', parent_location=chapter.location) vertical = ItemFactory.create(category='vertical', parent_location=section.location) 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) CourseEnrollment.enroll(self.user, course.id) with self.assertNumQueries(expected_mysql_query_count, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with check_mongo_calls(expected_mongo_query_count): 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): # lint-amnesty, pylint: disable=missing-class-docstring def setUp(self): super().setUp() self.course = CourseFactory.create(display_name='teꜱᴛ course', run="Testing_course") with self.store.bulk_operations(self.course.id): self.chapter = ItemFactory.create( category='chapter', parent_location=self.course.location, display_name="Chapter 1", ) self.section = ItemFactory.create( category='sequential', parent_location=self.chapter.location, due=datetime(2013, 9, 18, 11, 30, 00), display_name='Sequential 1', format='Homework' ) self.vertical = ItemFactory.create( category='vertical', parent_location=self.section.location, display_name='Vertical 1', ) self.problem = ItemFactory.create( category='problem', parent_location=self.vertical.location, display_name='Problem 1', ) self.section2 = ItemFactory.create( category='sequential', parent_location=self.chapter.location, display_name='Sequential 2', ) self.vertical2 = ItemFactory.create( category='vertical', parent_location=self.section2.location, display_name='Vertical 2', ) self.problem2 = ItemFactory.create( category='problem', parent_location=self.vertical2.location, display_name='Problem 2', ) self.course_key = self.course.id # Set profile country to Åland Islands to check Unicode characters does not raise error self.user = UserFactory(username='dummy', profile__country='AX') self.date = datetime(2013, 1, 22, tzinfo=UTC) self.enrollment = CourseEnrollment.enroll(self.user, self.course_key) self.enrollment.created = self.date self.enrollment.save() chapter = 'Overview' self.chapter_url = '{}/{}/{}'.format('/courses', self.course_key, chapter) self.org = "ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ" self.org_html = "

'+Stark/Industries+'

" assert self.client.login(username=self.user.username, password=TEST_PASSWORD) # refresh the course from the modulestore so that it has children self.course = modulestore().get_course(self.course.id) def _create_global_staff_user(self): """ Create global staff user and log them in """ self.global_staff = GlobalStaffFactory.create() # pylint: disable=attribute-defined-outside-init assert self.client.login(username=self.global_staff.username, password=TEST_PASSWORD) @ddt.ddt class ViewsTestCase(BaseViewsTestCase): """ Tests for views.py methods. """ YESTERDAY = 'yesterday' DATES = { YESTERDAY: datetime.now(UTC) - timedelta(days=1), None: None, } def test_index_success(self): response = self._verify_index_response() self.assertContains(response, self.problem2.location) # 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) self.assertContains(response, self.problem2.location) 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') 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') 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_no_visible_section_in_chapter(self): # reload the chapter from the store so its children information is updated self.chapter = self.store.get_item(self.chapter.location) # 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) url = reverse( 'courseware_chapter', kwargs={'course_id': str(self.course.id), 'chapter': str(self.chapter.location.block_id)}, ) response = self.client.get(url) assert response.status_code == 200 self.assertNotContains(response, 'Problem 1') self.assertNotContains(response, 'Problem 2') def _create_url_for_enroll_staff(self): """ creates the courseware url and enroll staff url """ # create the _next parameter courseware_url = reverse( 'courseware_section', kwargs={ 'course_id': str(self.course_key), 'chapter': str(self.chapter.location.block_id), 'section': str(self.section.location.block_id), } ) # 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)}), courseware_url=courseware_url ) return courseware_url, enroll_url @ddt.data( ({'enroll': "Enroll"}, True), ({'dont_enroll': "Don't enroll"}, False)) @ddt.unpack def test_enroll_staff_redirection(self, data, enrollment): """ Verify unenrolled staff is redirected to correct url. """ 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 # 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) else: self.assertRedirects(response, '/courses/{}/about'.format(str(self.course_key))) def test_enroll_staff_with_invalid_data(self): """ If we try to post with an invalid data pattern, then we'll redirected to course about page. """ self._create_global_staff_user() __, enroll_url = self._create_url_for_enroll_staff() response = self.client.post(enroll_url, data={'test': "test"}) assert response.status_code == 302 self.assertRedirects(response, '/courses/{}/about'.format(str(self.course_key))) def assert_enrollment_link_present(self, is_anonymous): """ Prepare ecommerce checkout data and assert if the ecommerce link is contained in the response. Arguments: is_anonymous(bool): Tell the method to use an anonymous user or the logged in one. _id(bool): Tell the method to either expect an id in the href or not. """ sku = 'TEST123' configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True) course = CourseFactory.create() CourseModeFactory(mode_slug=CourseMode.PROFESSIONAL, course_id=course.id, sku=sku, min_price=1) if is_anonymous: self.client.logout() else: assert self.client.login(username=self.user.username, password=TEST_PASSWORD) # Construct the link according the following scenarios and verify its presence in the response: # (1) shopping cart is enabled and the user is not logged in # (2) shopping cart is enabled and the user is logged in href = ''.format( uri_stem=configuration.basket_checkout_page, sku=sku, ) # Generate the course about page content response = self.client.get(reverse('about_course', args=[str(course.id)])) self.assertContains(response, href) @ddt.data(True, False) def test_ecommerce_checkout(self, is_anonymous): if not is_anonymous: self.assert_enrollment_link_present(is_anonymous=is_anonymous) else: assert EcommerceService().is_enabled(AnonymousUser()) is False def test_user_groups(self): # deprecated function mock_user = MagicMock() 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_legacy_courseware_url(self.course.location) # pylint: disable=line-too-long # 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_legacy_courseware_url(self.section.location) # pylint: disable=line-too-long def test_invalid_course_id(self): response = self.client.get('/courses/MITx/3.091X/') assert response.status_code == 404 def test_incomplete_course_id(self): response = self.client.get('/courses/MITx/') assert response.status_code == 404 def test_index_invalid_position(self): request_url = '/'.join([ '/courses', str(self.course.id), 'courseware', self.chapter.location.block_id, self.section.location.block_id, 'f' ]) assert self.client.login(username=self.user.username, password=TEST_PASSWORD) response = self.client.get(request_url) assert response.status_code == 404 def test_unicode_handling_in_url(self): url_parts = [ '/courses', str(self.course.id), 'courseware', self.chapter.location.block_id, self.section.location.block_id, '1' ] assert self.client.login(username=self.user.username, password=TEST_PASSWORD) for idx, val in enumerate(url_parts): url_parts_copy = url_parts[:] url_parts_copy[idx] = val + 'χ' request_url = '/'.join(url_parts_copy) response = self.client.get(request_url) assert response.status_code == 404 def test_jump_to_invalid(self): # TODO add a test for invalid location # TODO add a test for no data * response = self.client.get(reverse('jump_to', args=['foo/bar/baz', 'baz'])) assert response.status_code == 404 def verify_end_date(self, course_id, expected_end_text=None): """ Visits the about page for `course_id` and tests that both the text "Classes End", as well as the specified `expected_end_text`, is present on the page. If `expected_end_text` is None, verifies that the about page *does not* contain the text "Classes End". """ result = self.client.get(reverse('about_course', args=[str(course_id)])) if expected_end_text is not None: self.assertContains(result, "Classes End") self.assertContains(result, expected_end_text) else: self.assertNotContains(result, "Classes End") def test_submission_history_accepts_valid_ids(self): # log into a staff account admin = AdminFactory() assert self.client.login(username=admin.username, password='test') url = reverse('submission_history', kwargs={ 'course_id': str(self.course_key), 'learner_identifier': 'dummy', 'location': str(self.problem.location), }) response = self.client.get(url) # Tests that we do not get an "Invalid x" response when passing correct arguments to view self.assertNotContains(response, 'Invalid') def test_submission_history_xss(self): # log into a staff account admin = AdminFactory() assert self.client.login(username=admin.username, password='test') # try it with an existing user and a malicious location url = reverse('submission_history', kwargs={ 'course_id': str(self.course_key), 'learner_identifier': 'dummy', 'location': '' }) response = self.client.get(url) self.assertNotContains(response, '', 'location': 'dummy' }) response = self.client.get(url) self.assertNotContains(response, '', '', '') def test_progress_page_xss_prevent(self, malicious_code): """ Test that XSS attack is prevented """ resp = self._get_student_progress_page() # Test that malicious code does not appear in html self.assertNotContains(resp, malicious_code) def test_pure_ungraded_xblock(self): ItemFactory.create(category='acid', parent_location=self.vertical.location) self._get_progress_page() @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_student_progress_with_valid_and_invalid_id(self, default_store): """ Check that invalid 'student_id' raises Http404 for both old mongo and split mongo courses. """ # Create new course with respect to 'default_store' # Enroll student into course self.course = CourseFactory.create(default_store=default_store) # lint-amnesty, pylint: disable=attribute-defined-outside-init CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=CourseMode.HONOR) # Invalid Student Ids (Integer and Non-int) invalid_student_ids = [ 991021, 'azU3N_8$', ] for invalid_id in invalid_student_ids: resp = self.client.get( reverse('student_progress', args=[str(self.course.id), invalid_id]) ) assert resp.status_code == 404 # Assert that valid 'student_id' returns 200 status self._get_student_progress_page() @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_unenrolled_student_progress_for_credit_course(self, default_store): """ Test that student progress page does not break while checking for an unenrolled student. Scenario: When instructor checks the progress of a student who is not enrolled in credit course. It should return 200 response. """ # Create a new course, a user which will not be enrolled in course, admin user for staff access course = CourseFactory.create(default_store=default_store) admin = AdminFactory.create() assert self.client.login(username=admin.username, password='test') # Create and enable Credit course CreditCourse.objects.create(course_key=course.id, enabled=True) # Configure a credit provider for the course CreditProvider.objects.create( provider_id="ASU", enable_integration=True, provider_url="https://credit.example.com/request" ) requirements = [{ "namespace": "grade", "name": "grade", "display_name": "Grade", "criteria": {"min_grade": 0.52}, }] # Add a single credit requirement (final grade) set_credit_requirements(course.id, requirements) self._get_student_progress_page() def test_non_ascii_grade_cutoffs(self): self._get_progress_page() def test_generate_cert_config(self): resp = self._get_progress_page() self.assertNotContains(resp, 'Request Certificate') # Enable the feature, but do not enable it for this course CertificateGenerationConfiguration(enabled=True).save() resp = self._get_progress_page() self.assertNotContains(resp, 'Request Certificate') # Enable certificate generation for this course certs_api.set_cert_generation_enabled(self.course.id, True) resp = self._get_progress_page() self.assertNotContains(resp, 'Request Certificate') @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) def test_view_certificate_for_unverified_student(self): """ If user has already generated a certificate, it should be visible in case of user being unverified too. """ GeneratedCertificateFactory.create( user=self.user, course_id=self.course.id, status=CertificateStatuses.downloadable, mode='verified' ) # Enable the feature, but do not enable it for this course CertificateGenerationConfiguration(enabled=True).save() # Enable certificate generation for this course certs_api.set_cert_generation_enabled(self.course.id, True) CourseEnrollment.enroll(self.user, self.course.id, mode="verified") # Check that the user is unverified assert not IDVerificationService.user_is_verified(self.user) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} resp = self._get_progress_page() self.assertNotContains(resp, "Certificate unavailable") self.assertContains(resp, "Your certificate is available") @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) def test_view_certificate_link(self): """ If certificate web view is enabled then certificate web view button should appear for user who certificate is available/generated """ certificate = GeneratedCertificateFactory.create( user=self.user, course_id=self.course.id, status=CertificateStatuses.downloadable, download_url="http://www.example.com/certificate.pdf", mode='honor' ) # Enable the feature, but do not enable it for this course CertificateGenerationConfiguration(enabled=True).save() # Enable certificate generation for this course certs_api.set_cert_generation_enabled(self.course.id, True) # Course certificate configurations certificates = [ { 'id': 1, 'name': 'Name 1', 'description': 'Description 1', 'course_title': 'course_title_1', 'signatories': [], 'version': 1, 'is_active': True } ] self.course.certificates = {'certificates': certificates} self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} resp = self._get_progress_page() self.assertContains(resp, "View Certificate") self.assertContains(resp, "earned a certificate for this course") cert_url = certs_api.get_certificate_url(course_id=self.course.id, uuid=certificate.verify_uuid) self.assertContains(resp, cert_url) # when course certificate is not active certificates[0]['is_active'] = False self.store.update_item(self.course, self.user.id) resp = self._get_progress_page() self.assertNotContains(resp, "View Your Certificate") self.assertNotContains(resp, "You can now view your certificate") self.assertContains(resp, "Your certificate is available") self.assertContains(resp, "earned a certificate for this course.") @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False}) def test_view_certificate_link_hidden(self): """ If certificate web view is disabled then certificate web view button should not appear for user who certificate is available/generated """ GeneratedCertificateFactory.create( user=self.user, course_id=self.course.id, status=CertificateStatuses.downloadable, download_url="http://www.example.com/certificate.pdf", mode='honor' ) # Enable the feature, but do not enable it for this course CertificateGenerationConfiguration(enabled=True).save() # Enable certificate generation for this course certs_api.set_cert_generation_enabled(self.course.id, True) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} resp = self._get_progress_page() self.assertContains(resp, "Download Your Certificate") @ddt.data( (True, 54), (False, 54), ) @ddt.unpack def test_progress_queries_paced_courses(self, self_paced, query_count): """Test that query counts remain the same for self-paced and instructor-paced courses.""" # TODO: decrease query count as part of REVO-28 ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) self.setup_course(self_paced=self_paced) with self.assertNumQueries(query_count, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1): self._get_progress_page() @patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False}) @ddt.data( (False, 61, 44), (True, 54, 39) ) @ddt.unpack def test_progress_queries(self, enable_waffle, initial, subsequent): ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) self.setup_course() with override_waffle_switch(grades_waffle_switch(ASSUME_ZERO_GRADE_IF_ABSENT), active=enable_waffle): with self.assertNumQueries( initial, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST ), check_mongo_calls(1): self._get_progress_page() # subsequent accesses to the progress page require fewer queries. for _ in range(2): with self.assertNumQueries( subsequent, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST ), check_mongo_calls(1): self._get_progress_page() @ddt.data( *itertools.product( ( CourseMode.AUDIT, CourseMode.HONOR, CourseMode.VERIFIED, CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.CREDIT_MODE ), (True, False) ) ) @ddt.unpack def test_show_certificate_request_button(self, course_mode, user_verified): """Verify that the Request Certificate is not displayed in audit mode.""" CertificateGenerationConfiguration(enabled=True).save() certs_api.set_cert_generation_enabled(self.course.id, True) CourseEnrollment.enroll(self.user, self.course.id, mode=course_mode) with patch( 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' ) as user_verify: user_verify.return_value = user_verified with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summary = { 'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} } resp = self._get_progress_page() cert_button_hidden = course_mode is CourseMode.AUDIT or \ course_mode in CourseMode.VERIFIED_MODES and not user_verified assert cert_button_hidden == ('Request Certificate' not in resp.content.decode('utf-8')) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) def test_page_with_invalidated_certificate_with_html_view(self): """ Verify that for html certs if certificate is marked as invalidated than re-generate button should not appear on progress page. """ generated_certificate = self.generate_certificate( "http://www.example.com/certificate.pdf", "honor" ) # Course certificate configurations certificates = [ { 'id': 1, 'name': 'dummy', 'description': 'dummy description', 'course_title': 'dummy title', 'signatories': [], 'version': 1, 'is_active': True } ] self.course.certificates = {'certificates': certificates} self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summary = { 'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} } resp = self._get_progress_page() self.assertContains(resp, "View Certificate") self.assert_invalidate_certificate(generated_certificate) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) def test_page_with_whitelisted_certificate_with_html_view(self): """ Verify that for white listed user the view certificate is appearing on dashboard """ generated_certificate = self.generate_certificate( "http://www.example.com/certificate.pdf", "honor" ) # Course certificate configurations certificates = [ { 'id': 1, 'name': 'dummy', 'description': 'dummy description', 'course_title': 'dummy title', 'signatories': [], 'version': 1, 'is_active': True } ] self.course.certificates = {'certificates': certificates} self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) CertificateWhitelist.objects.create( user=self.user, course_id=self.course.id, whitelist=True ) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = False course_grade.summary = { 'grade': 'Fail', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} } resp = self._get_progress_page() self.assertContains(resp, "View Certificate") self.assert_invalidate_certificate(generated_certificate) def test_page_with_invalidated_certificate_with_pdf(self): """ Verify that for pdf certs if certificate is marked as invalidated than re-generate button should not appear on progress page. """ generated_certificate = self.generate_certificate( "http://www.example.com/certificate.pdf", "honor" ) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} resp = self._get_progress_page() self.assertContains(resp, 'Download Your Certificate') self.assert_invalidate_certificate(generated_certificate) @ddt.data( *itertools.product( ( CourseMode.AUDIT, CourseMode.HONOR, CourseMode.VERIFIED, CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.CREDIT_MODE ) ) ) @ddt.unpack def test_progress_with_course_duration_limits(self, course_mode): """ Verify that expired banner message appears on progress page, if learner is enrolled in audit mode. """ CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) user = UserFactory.create() assert self.client.login(username=user.username, password='test') add_course_mode(self.course, mode_slug=CourseMode.AUDIT) add_course_mode(self.course) CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=course_mode) response = self._get_progress_page() bannerText = get_expiration_banner_text(user, self.course) if course_mode == CourseMode.AUDIT: self.assertContains(response, bannerText, html=True) else: self.assertNotContains(response, bannerText, html=True) @ddt.data( *itertools.product( ( CourseMode.AUDIT, CourseMode.HONOR, CourseMode.VERIFIED, CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.CREDIT_MODE ) ) ) @ddt.unpack def test_progress_without_course_duration_limits(self, course_mode): """ Verify that expired banner message never appears on progress page, regardless of course_mode """ CourseDurationLimitConfig.objects.create(enabled=False) user = UserFactory.create() assert self.client.login(username=user.username, password='test') CourseModeFactory.create( course_id=self.course.id, mode_slug=course_mode ) CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=course_mode) response = self._get_progress_page() bannerText = get_expiration_banner_text(user, self.course) self.assertNotContains(response, bannerText, html=True) @patch('lms.djangoapps.courseware.views.views.is_course_passed', PropertyMock(return_value=True)) @override_settings(FEATURES=FEATURES_WITH_DISABLE_HONOR_CERTIFICATE) @ddt.data(CourseMode.AUDIT, CourseMode.HONOR) def test_message_for_ineligible_mode(self, course_mode): """ Verify that message appears on progress page, if learner is enrolled in an ineligible mode. """ user = UserFactory.create() assert self.client.login(username=user.username, password='test') CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=course_mode) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} response = self._get_progress_page() expected_message = ('You are enrolled in the {mode} track for this course. ' 'The {mode} track does not include a certificate.').format(mode=course_mode) self.assertContains(response, expected_message) def test_invalidated_cert_data(self): """ Verify that invalidated cert data is returned if cert is invalidated. """ generated_certificate = self.generate_certificate( "http://www.example.com/certificate.pdf", "honor" ) CertificateInvalidationFactory.create( generated_certificate=generated_certificate, invalidated_by=self.user ) # Invalidate user certificate generated_certificate.invalidate() response = views.get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) assert response.cert_status == 'invalidated' assert response.title == 'Your certificate has been invalidated' @override_settings(FEATURES=FEATURES_WITH_DISABLE_HONOR_CERTIFICATE) def test_downloadable_get_cert_data(self): """ Verify that downloadable cert data is returned if cert is downloadable even when DISABLE_HONOR_CERTIFICATES feature flag is turned ON. """ self.generate_certificate( "http://www.example.com/certificate.pdf", "honor" ) response = views.get_cert_data( self.user, self.course, CourseMode.HONOR, MagicMock(passed=True) ) assert response.cert_status == 'downloadable' assert response.title == 'Your certificate is available' def test_generating_get_cert_data(self): """ Verify that generating cert data is returned if cert is generating. """ self.generate_certificate( "http://www.example.com/certificate.pdf", "honor" ) with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', return_value=self.mock_certificate_downloadable_status(is_generating=True)): response = views.get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) assert response.cert_status == 'generating' assert response.title == "We're working on it..." def test_unverified_get_cert_data(self): """ Verify that unverified cert data is returned if cert is unverified. """ self.generate_certificate( "http://www.example.com/certificate.pdf", "honor" ) with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', return_value=self.mock_certificate_downloadable_status(is_unverified=True)): response = views.get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) assert response.cert_status == 'unverified' assert response.title == 'Certificate unavailable' def test_request_get_cert_data(self): """ Verify that requested cert data is returned if cert is to be requested. """ self.generate_certificate( "http://www.example.com/certificate.pdf", "honor" ) with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', return_value=self.mock_certificate_downloadable_status()): response = views.get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) assert response.cert_status == 'requesting' assert response.title == 'Congratulations, you qualified for a certificate!' def test_earned_but_not_available_get_cert_data(self): """ Verify that earned but not available cert data is returned if cert has been earned, but isn't available. """ self.generate_certificate( "http://www.example.com/certificate.pdf", "verified" ) with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', return_value=self.mock_certificate_downloadable_status(earned_but_not_available=True)): response = views.get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) assert response.cert_status == 'earned_but_not_available' assert response.title == 'Your certificate will be available soon!' def assert_invalidate_certificate(self, certificate): """ Dry method to mark certificate as invalid. And assert the response. """ CertificateInvalidationFactory.create( generated_certificate=certificate, invalidated_by=self.user ) # Invalidate user certificate certificate.invalidate() resp = self._get_progress_page() self.assertNotContains(resp, 'Request Certificate') self.assertContains(resp, 'Your certificate has been invalidated') self.assertContains(resp, 'Please contact your course team if you have any questions.') self.assertNotContains(resp, 'View Your Certificate') self.assertNotContains(resp, 'Download Your Certificate') def generate_certificate(self, url, mode): """ Dry method to generate certificate. """ generated_certificate = GeneratedCertificateFactory.create( user=self.user, course_id=self.course.id, status=CertificateStatuses.downloadable, download_url=url, mode=mode ) CertificateGenerationConfiguration(enabled=True).save() certs_api.set_cert_generation_enabled(self.course.id, True) return generated_certificate def mock_certificate_downloadable_status( self, is_downloadable=False, is_generating=False, is_unverified=False, uuid=None, download_url=None, earned_but_not_available=None, ): """Dry method to mock certificate downloadable status response.""" return { 'is_downloadable': is_downloadable, 'is_generating': is_generating, 'is_unverified': is_unverified, 'download_url': uuid, 'uuid': download_url, 'earned_but_not_available': earned_but_not_available, } @ddt.ddt class ProgressPageShowCorrectnessTests(ProgressPageBaseTests): """ Tests that verify that the progress page works correctly when displaying subsections where correctness is hidden. """ # Constants used in the test data NOW = datetime.now(UTC) DAY_DELTA = timedelta(days=1) YESTERDAY = 'yesterday' TODAY = 'today' TOMORROW = 'tomorrow' GRADER_TYPE = 'Homework' DATES = { YESTERDAY: NOW - DAY_DELTA, TODAY: NOW, TOMORROW: NOW + DAY_DELTA, None: None, } def setUp(self): super().setUp() self.staff_user = UserFactory.create(is_staff=True) def setup_course(self, show_correctness='', due_date=None, graded=False, **course_options): # lint-amnesty, pylint: disable=arguments-differ """ Set up course with a subsection with the given show_correctness, due_date, and graded settings. """ # Use a simple grading policy course_options['grading_policy'] = { "GRADER": [{ "type": self.GRADER_TYPE, "min_count": 2, "drop_count": 0, "short_label": "HW", "weight": 1.0 }], "GRADE_CUTOFFS": { 'A': .9, 'B': .33 } } self.create_course(**course_options) metadata = dict( show_correctness=show_correctness, ) if due_date is not None: metadata['due'] = due_date if graded: metadata['graded'] = True metadata['format'] = self.GRADER_TYPE with self.store.bulk_operations(self.course.id): self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location, display_name="Section 1") self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location, display_name="Subsection 1", metadata=metadata) self.vertical = ItemFactory.create(category='vertical', parent_location=self.section.location) CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=CourseMode.HONOR) def add_problem(self): """ Add a problem to the subsection """ problem_xml = MultipleChoiceResponseXMLFactory().build_xml( question_text='The correct answer is Choice 1', choices=[True, False], choice_names=['choice_0', 'choice_1'] ) self.problem = ItemFactory.create(category='problem', parent_location=self.vertical.location, # lint-amnesty, pylint: disable=attribute-defined-outside-init data=problem_xml, display_name='Problem 1') # Re-fetch the course from the database self.course = self.store.get_course(self.course.id) # lint-amnesty, pylint: disable=attribute-defined-outside-init def answer_problem(self, value=1, max_value=1): """ Submit the given score to the problem on behalf of the user """ # Get the module for the problem, as viewed by the user field_data_cache = FieldDataCache.cache_for_descriptor_descendents( self.course.id, self.user, self.course, depth=2 ) self.addCleanup(set_current_request, None) module = get_module( self.user, get_mock_request(self.user), self.problem.scope_ids.usage_id, field_data_cache, ) # Submit the given score/max_score to the problem xmodule grade_dict = {'value': value, 'max_value': max_value, 'user_id': self.user.id} module.system.publish(self.problem, 'grade', grade_dict) def assert_progress_page_show_grades(self, response, show_correctness, due_date, graded, show_grades, score, max_score, avg): # lint-amnesty, pylint: disable=unused-argument """ Ensures that grades and scores are shown or not shown on the progress page as required. """ expected_score = f"
{score}/{max_score}
" percent = score / float(max_score) # Test individual problem scores if show_grades: # If grades are shown, we should be able to see the current problem scores. self.assertContains(response, expected_score) if graded: expected_summary_text = "Problem Scores:" else: expected_summary_text = "Practice Scores:" else: # If grades are hidden, we should not be able to see the current problem scores. self.assertNotContains(response, expected_score) if graded: expected_summary_text = "Problem scores are hidden" else: expected_summary_text = "Practice scores are hidden" if show_correctness == ShowCorrectness.PAST_DUE and due_date: expected_summary_text += ' until the due date.' else: expected_summary_text += '.' # Ensure that expected text is present self.assertContains(response, expected_summary_text) # Test overall sequential score if graded and max_score > 0: percentageString = f"{percent:.0%}" if max_score > 0 else "" template = ' ({0:.3n}/{1:.3n}) {2}' expected_grade_summary = template.format(float(score), float(max_score), percentageString) if show_grades: self.assertContains(response, expected_grade_summary) else: self.assertNotContains(response, expected_grade_summary) @ddt.data( ('', None, False), ('', None, True), (ShowCorrectness.ALWAYS, None, False), (ShowCorrectness.ALWAYS, None, True), (ShowCorrectness.ALWAYS, YESTERDAY, False), (ShowCorrectness.ALWAYS, YESTERDAY, True), (ShowCorrectness.ALWAYS, TODAY, False), (ShowCorrectness.ALWAYS, TODAY, True), (ShowCorrectness.ALWAYS, TOMORROW, False), (ShowCorrectness.ALWAYS, TOMORROW, True), (ShowCorrectness.NEVER, None, False), (ShowCorrectness.NEVER, None, True), (ShowCorrectness.NEVER, YESTERDAY, False), (ShowCorrectness.NEVER, YESTERDAY, True), (ShowCorrectness.NEVER, TODAY, False), (ShowCorrectness.NEVER, TODAY, True), (ShowCorrectness.NEVER, TOMORROW, False), (ShowCorrectness.NEVER, TOMORROW, True), (ShowCorrectness.PAST_DUE, None, False), (ShowCorrectness.PAST_DUE, None, True), (ShowCorrectness.PAST_DUE, YESTERDAY, False), (ShowCorrectness.PAST_DUE, YESTERDAY, True), (ShowCorrectness.PAST_DUE, TODAY, False), (ShowCorrectness.PAST_DUE, TODAY, True), (ShowCorrectness.PAST_DUE, TOMORROW, False), (ShowCorrectness.PAST_DUE, TOMORROW, True), ) @ddt.unpack def test_progress_page_no_problem_scores(self, show_correctness, due_date_name, graded): """ Test that "no problem scores are present" for a course with no problems, regardless of the various show correctness settings. """ self.setup_course(show_correctness=show_correctness, due_date=self.DATES[due_date_name], graded=graded) resp = self._get_progress_page() # Test that no problem scores are present self.assertContains(resp, 'No problem scores in this section') @ddt.data( ('', None, False, True), ('', None, True, True), (ShowCorrectness.ALWAYS, None, False, True), (ShowCorrectness.ALWAYS, None, True, True), (ShowCorrectness.ALWAYS, YESTERDAY, False, True), (ShowCorrectness.ALWAYS, YESTERDAY, True, True), (ShowCorrectness.ALWAYS, TODAY, False, True), (ShowCorrectness.ALWAYS, TODAY, True, True), (ShowCorrectness.ALWAYS, TOMORROW, False, True), (ShowCorrectness.ALWAYS, TOMORROW, True, True), (ShowCorrectness.NEVER, None, False, False), (ShowCorrectness.NEVER, None, True, False), (ShowCorrectness.NEVER, YESTERDAY, False, False), (ShowCorrectness.NEVER, YESTERDAY, True, False), (ShowCorrectness.NEVER, TODAY, False, False), (ShowCorrectness.NEVER, TODAY, True, False), (ShowCorrectness.NEVER, TOMORROW, False, False), (ShowCorrectness.NEVER, TOMORROW, True, False), (ShowCorrectness.PAST_DUE, None, False, True), (ShowCorrectness.PAST_DUE, None, True, True), (ShowCorrectness.PAST_DUE, YESTERDAY, False, True), (ShowCorrectness.PAST_DUE, YESTERDAY, True, True), (ShowCorrectness.PAST_DUE, TODAY, False, True), (ShowCorrectness.PAST_DUE, TODAY, True, True), (ShowCorrectness.PAST_DUE, TOMORROW, False, False), (ShowCorrectness.PAST_DUE, TOMORROW, True, False), ) @ddt.unpack def test_progress_page_hide_scores_from_learner(self, show_correctness, due_date_name, graded, show_grades): """ Test that problem scores are hidden on progress page when correctness is not available to the learner, and that they are visible when it is. """ due_date = self.DATES[due_date_name] self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded) self.add_problem() self.client.login(username=self.user.username, password='test') resp = self._get_progress_page() # Ensure that expected text is present self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 0, 1, 0) # Submit answers to the problem, and re-fetch the progress page self.answer_problem() resp = self._get_progress_page() # Test that the expected text is still present. self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 1, 1, .5) @ddt.data( ('', None, False, True), ('', None, True, True), (ShowCorrectness.ALWAYS, None, False, True), (ShowCorrectness.ALWAYS, None, True, True), (ShowCorrectness.ALWAYS, YESTERDAY, False, True), (ShowCorrectness.ALWAYS, YESTERDAY, True, True), (ShowCorrectness.ALWAYS, TODAY, False, True), (ShowCorrectness.ALWAYS, TODAY, True, True), (ShowCorrectness.ALWAYS, TOMORROW, False, True), (ShowCorrectness.ALWAYS, TOMORROW, True, True), (ShowCorrectness.NEVER, None, False, False), (ShowCorrectness.NEVER, None, True, False), (ShowCorrectness.NEVER, YESTERDAY, False, False), (ShowCorrectness.NEVER, YESTERDAY, True, False), (ShowCorrectness.NEVER, TODAY, False, False), (ShowCorrectness.NEVER, TODAY, True, False), (ShowCorrectness.NEVER, TOMORROW, False, False), (ShowCorrectness.NEVER, TOMORROW, True, False), (ShowCorrectness.PAST_DUE, None, False, True), (ShowCorrectness.PAST_DUE, None, True, True), (ShowCorrectness.PAST_DUE, YESTERDAY, False, True), (ShowCorrectness.PAST_DUE, YESTERDAY, True, True), (ShowCorrectness.PAST_DUE, TODAY, False, True), (ShowCorrectness.PAST_DUE, TODAY, True, True), (ShowCorrectness.PAST_DUE, TOMORROW, False, True), (ShowCorrectness.PAST_DUE, TOMORROW, True, True), ) @ddt.unpack def test_progress_page_hide_scores_from_staff(self, show_correctness, due_date_name, graded, show_grades): """ Test that problem scores are hidden from staff viewing a learner's progress page only if show_correctness=never. """ due_date = self.DATES[due_date_name] self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded) self.add_problem() # Login as a course staff user to view the student progress page. self.client.login(username=self.staff_user.username, password='test') resp = self._get_student_progress_page() # Ensure that expected text is present self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 0, 1, 0) # Submit answers to the problem, and re-fetch the progress page self.answer_problem() resp = self._get_student_progress_page() # Test that the expected text is still present. self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 1, 1, .5) class VerifyCourseKeyDecoratorTests(TestCase): """ Tests for the ensure_valid_course_key decorator. """ def setUp(self): super().setUp() self.request = RequestFactoryNoCsrf().get("foo") self.valid_course_id = "edX/test/1" self.invalid_course_id = "edX/" def test_decorator_with_valid_course_id(self): mocked_view = create_autospec(views.course_about) view_function = ensure_valid_course_key(mocked_view) view_function(self.request, course_id=self.valid_course_id) assert mocked_view.called def test_decorator_with_invalid_course_id(self): mocked_view = create_autospec(views.course_about) view_function = ensure_valid_course_key(mocked_view) self.assertRaises(Http404, view_function, self.request, course_id=self.invalid_course_id) assert not mocked_view.called class GenerateUserCertTests(ModuleStoreTestCase): """ Tests for the view function Generated User Certs """ def setUp(self): super().setUp() self.student = UserFactory() self.course = CourseFactory.create( org='edx', number='verified', end=datetime.now(), display_name='Verified Course', grade_cutoffs={'cutoff': 0.75, 'Pass': 0.5}, self_paced=True ) self.enrollment = CourseEnrollment.enroll(self.student, self.course.id, mode='honor') assert self.client.login(username=self.student, password=TEST_PASSWORD) self.url = reverse('generate_user_cert', kwargs={'course_id': str(self.course.id)}) def test_user_with_out_passing_grades(self): # If user has no grading then json will return failed message and badrequest code resp = self.client.post(self.url) self.assertContains( resp, "Your certificate will be available when you pass the course.", status_code=HttpResponseBadRequest.status_code, ) @patch('lms.djangoapps.courseware.views.views.is_course_passed', return_value=True) @override_settings(CERT_QUEUE='certificates', LMS_SEGMENT_KEY="foobar") def test_user_with_passing_grade(self, mock_is_course_passed): # lint-amnesty, pylint: disable=unused-argument # If user has above passing grading then json will return cert generating message and # status valid code # mocking xqueue and Segment analytics analytics_patcher = patch('lms.djangoapps.courseware.views.views.segment') mock_tracker = analytics_patcher.start() self.addCleanup(analytics_patcher.stop) with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue: mock_send_to_queue.return_value = (0, "Successfully queued") resp = self.client.post(self.url) assert resp.status_code == 200 # Verify Google Analytics event fired after generating certificate mock_tracker.track.assert_called_once_with( self.student.id, 'edx.bi.user.certificate.generate', { 'category': 'certificates', 'label': str(self.course.id) }, ) mock_tracker.reset_mock() def test_user_with_passing_existing_generating_cert(self): # If user has passing grade but also has existing generating cert # then json will return cert generating message with bad request code GeneratedCertificateFactory.create( user=self.student, course_id=self.course.id, status=CertificateStatuses.generating, mode='verified' ) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summary = {'grade': 'Pass', 'percent': 0.75} resp = self.client.post(self.url) self.assertContains(resp, "Certificate is being created.", status_code=HttpResponseBadRequest.status_code) @override_settings(CERT_QUEUE='certificates', LMS_SEGMENT_KEY="foobar") def test_user_with_passing_existing_downloadable_cert(self): # If user has already downloadable certificate # then json will return cert generating message with bad request code GeneratedCertificateFactory.create( user=self.student, course_id=self.course.id, status=CertificateStatuses.downloadable, mode='verified' ) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summay = {'grade': 'Pass', 'percent': 0.75} resp = self.client.post(self.url) self.assertContains( resp, "Certificate has already been created.", status_code=HttpResponseBadRequest.status_code, ) def test_user_with_non_existing_course(self): # If try to access a course with valid key pattern then it will return # bad request code with course is not valid message resp = self.client.post('/courses/def/abc/in_valid/generate_user_cert') self.assertContains(resp, "Course is not valid", status_code=HttpResponseBadRequest.status_code) def test_user_with_invalid_course_id(self): # If try to access a course with invalid key pattern then 404 will return resp = self.client.post('/courses/def/generate_user_cert') assert resp.status_code == 404 def test_user_without_login_return_error(self): # If user try to access without login should see a bad request status code with message self.client.logout() resp = self.client.post(self.url) self.assertContains( resp, "You must be signed in to {platform_name} to create a certificate.".format( platform_name=settings.PLATFORM_NAME ), status_code=HttpResponseBadRequest.status_code, ) class ActivateIDCheckerBlock(XBlock): """ XBlock for checking for an activate_block_id entry in the render context. """ # We don't need actual children to test this. has_children = False def student_view(self, context): """ A student view that displays the activate_block_id context variable. """ result = Fragment() if 'activate_block_id' in context: result.add_content("Activate Block ID: {block_id}

".format(block_id=context['activate_block_id'])) return result class ViewCheckerBlock(XBlock): """ XBlock for testing user state in views. """ has_children = True state = String(scope=Scope.user_state) position = 0 def student_view(self, context): # pylint: disable=unused-argument """ A student_view that asserts that the ``state`` field for this block matches the block's usage_id. """ msg = f"{self.state} != {self.scope_ids.usage_id}" assert self.state == str(self.scope_ids.usage_id), msg fragments = self.runtime.render_children(self) result = Fragment( content="

ViewCheckerPassed: {}

\n{}".format( str(self.scope_ids.usage_id), "\n".join(fragment.content for fragment in fragments), ) ) return result @ddt.ddt class TestIndexView(ModuleStoreTestCase): """ Tests of the courseware.views.index view. """ @XBlock.register_temp_plugin(ViewCheckerBlock, 'view_checker') @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_student_state(self, default_store): """ Verify that saved student state is loaded for xblocks rendered in the index view. """ user = UserFactory() with modulestore().default_store(default_store): course = CourseFactory.create() chapter = ItemFactory.create(parent=course, category='chapter') section = ItemFactory.create(parent=chapter, category='view_checker', display_name="Sequence Checker") vertical = ItemFactory.create(parent=section, category='view_checker', display_name="Vertical Checker") block = ItemFactory.create(parent=vertical, category='view_checker', display_name="Block Checker") for item in (section, vertical, block): StudentModuleFactory.create( student=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) assert self.client.login(username=user.username, password='test') 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') 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') section = ItemFactory.create(parent=chapter, category='sequential', display_name="Sequence") vertical = ItemFactory.create(parent=section, category='vertical', display_name="Vertical") 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) assert self.client.login(username=user.username, password='test') 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") @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( self, patch_register_warning_message, patch_course_open_for_learner_enrollment ): """ Tests that the anonymous user case for the register_user_access_warning_messages returns different messaging based on the possibility of enrollment """ course = CourseFactory() user = self.create_user_for_course(course, CourseUserType.ANONYMOUS) request = RequestFactory().get('/') request.user = user patch_course_open_for_learner_enrollment.return_value = False views.CourseTabView.register_user_access_warning_messages(request, course) open_for_enrollment_message = patch_register_warning_message.mock_calls[0][1][1] patch_register_warning_message.reset_mock() patch_course_open_for_learner_enrollment.return_value = True views.CourseTabView.register_user_access_warning_messages(request, course) closed_to_enrollment_message = patch_register_warning_message.mock_calls[0][1][1] assert open_for_enrollment_message != closed_to_enrollment_message @patch('openedx.core.djangoapps.util.user_messages.PageLevelMessages.register_warning_message') def test_courseware_messages_masters_only(self, patch_register_warning_message): with patch( 'lms.djangoapps.courseware.views.views.CourseTabView.course_open_for_learner_enrollment' ) as patch_course_open_for_learner_enrollment: course = CourseFactory() user = self.create_user_for_course(course, CourseUserType.UNENROLLED) request = RequestFactory().get('/') request.user = user button_html = '' patch_course_open_for_learner_enrollment.return_value = False views.CourseTabView.register_user_access_warning_messages(request, course) # pull message out of the calls to the mock so that # we can make finer grained assertions than mock provides message = patch_register_warning_message.mock_calls[0][1][1] assert button_html not in message patch_register_warning_message.reset_mock() patch_course_open_for_learner_enrollment.return_value = True views.CourseTabView.register_user_access_warning_messages(request, course) # pull message out of the calls to the mock so that # we can make finer grained assertions than mock provides message = patch_register_warning_message.mock_calls[0][1][1] assert button_html in message @ddt.data( [True, True, True, False, ], [False, True, True, False, ], [True, False, True, False, ], [True, True, False, False, ], [False, False, True, False, ], [True, False, False, True, ], [False, True, False, False, ], [False, False, False, False, ], ) @ddt.unpack def test_should_show_enroll_button(self, course_open_for_self_enrollment, invitation_only, is_masters_only, expected_should_show_enroll_button): with patch('lms.djangoapps.courseware.views.views.course_open_for_self_enrollment') \ as patch_course_open_for_self_enrollment, \ patch('common.djangoapps.course_modes.models.CourseMode.is_masters_only') \ as patch_is_masters_only: course = CourseFactory() patch_course_open_for_self_enrollment.return_value = course_open_for_self_enrollment patch_is_masters_only.return_value = is_masters_only course.invitation_only = invitation_only 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() self.user = UserFactory() with modulestore().default_store(default_store): self.course = CourseFactory.create() with self.store.bulk_operations(self.course.id): self.chapter = ItemFactory.create( parent_location=self.course.location, category='chapter', display_name='Week 1' ) self.section_1 = ItemFactory.create( parent_location=self.chapter.location, category='sequential', display_name='Lesson 1' ) self.vertical_1 = ItemFactory.create( parent_location=self.section_1.location, category='vertical', display_name='Subsection 1' ) self.html_1_1 = ItemFactory.create( parent_location=self.vertical_1.location, category='html', display_name="HTML 1_1" ) self.problem_1 = ItemFactory.create( parent_location=self.vertical_1.location, category='problem', display_name="Problem 1" ) self.html_1_2 = ItemFactory.create( parent_location=self.vertical_1.location, category='html', display_name="HTML 1_2" ) self.section_2 = ItemFactory.create( parent_location=self.chapter.location, category='sequential', display_name='Lesson 2' ) self.vertical_2 = ItemFactory.create( parent_location=self.section_2.location, category='vertical', display_name='Subsection 2' ) self.video_2 = ItemFactory.create( parent_location=self.vertical_2.location, category='video', display_name="Video 2" ) self.problem_2 = ItemFactory.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) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_completion_service_disabled(self, default_store): self.setup_course(default_store) 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') response = self.client.get(self.section_2_url) self.assertNotContains(response, 'data-mark-completed-on-view-after-delay') @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_completion_service_enabled(self, default_store): self.override_waffle_switch(True) self.setup_course(default_store) 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') 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 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 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() self.user = UserFactory() # create course with 3 positions self.course = CourseFactory.create() with self.store.bulk_operations(self.course.id): self.chapter = ItemFactory.create(parent=self.course, category='chapter') self.section = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Sequence") ItemFactory.create(parent=self.section, category='vertical', display_name="Vertical1") ItemFactory.create(parent=self.section, category='vertical', display_name="Vertical2") ItemFactory.create(parent=self.section, category='vertical', display_name="Vertical3") CourseOverview.load_from_module_store(self.course.id) self.client.login(username=self.user, password='test') 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 def test_vertical_positions(self, input_position, expected_position): """ 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) 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.store.update_item(self.course, 0) self.chapter = ItemFactory.create(parent=self.course, category="chapter", display_name="Chapter") self.open_seq = ItemFactory.create( parent=self.chapter, category='sequential', display_name="Open Sequential" ) ItemFactory.create(parent=self.open_seq, category='problem', display_name="Problem 1") self.gated_seq = ItemFactory.create( parent=self.chapter, category='sequential', display_name="Gated Sequential" ) ItemFactory.create(parent=self.gated_seq, 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") 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=self.course, category="chapter") self.sequential = ItemFactory.create(parent=self.chapter, category='sequential') self.vertical = ItemFactory.create(parent=self.sequential, 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. This class overrides the get_response method, which is used by the tests defined in RenderXBlockTestMixin. """ def setUp(self): reload_django_url_config() super().setUp() def test_render_xblock_with_invalid_usage_key(self): """ Test XBlockRendering with invalid usage key """ response = self.get_response(usage_key='some_invalid_usage_key') self.assertContains(response, 'Page not found', status_code=404) def get_response(self, usage_key, url_encoded_params=None): """ Overridable method to get the response from the endpoint that is being tested. """ url = reverse('render_xblock', kwargs={'usage_key_string': str(usage_key)}) if url_encoded_params: url += '?' + url_encoded_params return self.client.get(url) def test_render_xblock_with_completion_service_disabled(self): """ Test that render_xblock does not set up the CompletionOnViewService. """ self.setup_course(ModuleStoreEnum.Type.split) self.setup_user(admin=True, enroll=True, login=True) response = self.get_response(usage_key=self.html_block.location) assert response.status_code == 200 self.assertContains(response, 'data-enable-completion-on-view-service="false"') self.assertNotContains(response, 'data-mark-completed-on-view-after-delay') def test_render_xblock_with_completion_service_enabled(self): """ Test that render_xblock sets up the CompletionOnViewService for relevant xblocks. """ self.override_waffle_switch(True) self.setup_course(ModuleStoreEnum.Type.split) self.setup_user(admin=False, enroll=True, login=True) response = self.get_response(usage_key=self.html_block.location) assert response.status_code == 200 self.assertContains(response, 'data-enable-completion-on-view-service="true"') self.assertContains(response, 'data-mark-completed-on-view-after-delay') request = RequestFactoryNoCsrf().post( '/', data=json.dumps({"completion": 1}), content_type='application/json', ) request.user = self.user response = handle_xblock_callback( request, str(self.course.id), quote_slashes(str(self.html_block.location)), 'publish_completion', ) assert response.status_code == 200 assert json.loads(response.content.decode('utf-8')) == {'result': 'ok'} response = self.get_response(usage_key=self.html_block.location) assert response.status_code == 200 self.assertContains(response, 'data-enable-completion-on-view-service="false"') self.assertNotContains(response, 'data-mark-completed-on-view-after-delay') response = self.get_response(usage_key=self.problem_block.location) assert response.status_code == 200 self.assertContains(response, 'data-enable-completion-on-view-service="false"') self.assertNotContains(response, 'data-mark-completed-on-view-after-delay') def test_rendering_descendant_of_gated_sequence(self): """ Test that we redirect instead of rendering what should be gated content, for things that are gated at the sequence level. """ with self.store.default_store(ModuleStoreEnum.Type.split): # pylint:disable=attribute-defined-outside-init self.course = CourseFactory.create(**self.course_options()) self.chapter = ItemFactory.create(parent=self.course, category='chapter') self.sequence = ItemFactory.create( parent=self.chapter, category='sequential', display_name='Sequence', is_time_limited=True, ) self.vertical_block = ItemFactory.create( parent=self.sequence, category='vertical', display_name="Vertical", ) self.html_block = ItemFactory.create( parent=self.vertical_block, category='html', data="

Test HTML Content

" ) self.problem_block = ItemFactory.create( parent=self.vertical_block, category='problem', display_name='Problem' ) CourseOverview.load_from_module_store(self.course.id) self.setup_user(admin=False, enroll=True, login=True) # Problem and Vertical response should both redirect to the Sequential # (where useful messaging would be). seq_url = reverse('render_xblock', kwargs={'usage_key_string': str(self.sequence.location)}) for block in [self.problem_block, self.vertical_block]: response = self.get_response(usage_key=block.location) assert response.status_code == 302 assert response.get('Location') == seq_url # The Sequence itself 200s (or we risk infinite redirect loops). assert self.get_response(usage_key=self.sequence.location).status_code == 200 class TestRenderXBlockSelfPaced(TestRenderXBlock): # lint-amnesty, pylint: disable=test-inherits-tests """ Test rendering XBlocks for a self-paced course. Relies on the query count assertions in the tests defined by RenderXBlockMixin. """ def setUp(self): # lint-amnesty, pylint: disable=useless-super-delegation super().setUp() def course_options(self): options = super().course_options() options['self_paced'] = True 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 = ItemFactory.create(category='chapter', parent_location=cls.course.location) cls.section = ItemFactory.create(category='sequential', parent_location=cls.chapter.location) cls.vertical = ItemFactory.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() 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) def test_write_by_default(self): """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 def test_writes_with_config(self): """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) self.test_write_by_default() 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. """ def setUp(self): super().setUp() self.user = UserFactory.create() assert self.client.login(username=self.user.username, password='test') self.course = CourseFactory.create() CourseOverview.load_from_module_store(self.course.id) CourseEnrollmentFactory(user=self.user, course_id=self.course.id) @patch('openedx.features.enterprise_support.api.enterprise_customer_for_request') def test_consent_required(self, mock_enterprise_customer_for_request): """ Test that enterprise data sharing consent is required when enabled for the various courseware views. """ # ENT-924: Temporary solution to replace sensitive SSO usernames. mock_enterprise_customer_for_request.return_value = None 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))), ): self.verify_consent_required(self.client, url) # lint-amnesty, pylint: disable=no-value-for-parameter @ddt.ddt class AccessUtilsTestCase(ModuleStoreTestCase): """ Test access utilities """ @ddt.data( (1, False), (-1, True) ) @ddt.unpack @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) def test_is_course_open_for_learner(self, start_date_modifier, expected_value): staff_user = AdminFactory() start_date = datetime.now(UTC) + timedelta(days=start_date_modifier) course = CourseFactory.create(start=start_date) assert bool(check_course_open_for_learner(staff_user, course)) == expected_value @ddt.ddt class DatesTabTestCase(ModuleStoreTestCase): """ Ensure that the dates page renders with the correct data for both a verified and audit learner """ def setUp(self): super().setUp() now = datetime.now(utc) self.course = CourseFactory.create(start=now + timedelta(days=-1), self_paced=True) self.course.end = now + timedelta(days=3) ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) CourseModeFactory(course_id=self.course.id, mode_slug=CourseMode.AUDIT) CourseModeFactory( course_id=self.course.id, mode_slug=CourseMode.VERIFIED, expiration_datetime=now + timedelta(days=1) ) VerificationDeadline.objects.create( course_key=self.course.id, deadline=now + timedelta(days=2) ) self.user = UserFactory() self.client.login(username=self.user.username, password=TEST_PASSWORD) ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2017, 1, 1)) def _get_response(self, course): """ Returns the HTML for the dates page """ return self.client.get(reverse('dates', args=[str(course.id)])) def test_tab_redirects_if_not_logged_in(self): self.client.logout() response = self._get_response(self.course) assert response.status_code == 302 assert '/login?next=/courses/' in response.url def test_tab_redirects_if_not_enrolled_and_not_staff(self): response = self._get_response(self.course) assert response.status_code == 302 # Beginning of redirect URL assert '/courses/' in response.url # End of redirect URL assert '/course/' in response.url # Now check staff users can see self.user.is_staff = True self.user.save() response = self._get_response(self.course) assert response.status_code == 200 # Enrolled users can also see self.client.logout() enrolled_user = UserFactory() CourseEnrollmentFactory(course_id=self.course.id, user=enrolled_user, mode=CourseMode.VERIFIED) self.client.login(username=enrolled_user.username, password=TEST_PASSWORD) response = self._get_response(self.course) assert response.status_code == 200 @override_experiment_waffle_flag(RELATIVE_DATES_FLAG, active=True) @patch('edx_django_utils.monitoring.set_custom_attribute') def test_defaults(self, mock_set_custom_attribute): enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode=CourseMode.VERIFIED) now = datetime.now(utc) with self.store.bulk_operations(self.course.id): section = ItemFactory.create(category='chapter', parent_location=self.course.location) subsection = ItemFactory.create( category='sequential', display_name='Released', parent_location=section.location, start=now - timedelta(days=1), due=now + timedelta(days=1), # Setting this to tomorrow so it'll show the 'Due Next' pill graded=True, format='Homework', ) vertical = ItemFactory.create(category='vertical', parent_location=subsection.location) ItemFactory.create(category='problem', parent_location=vertical.location, has_score=True) with patch('lms.djangoapps.courseware.views.views.get_enrollment') as mock_get_enrollment: mock_get_enrollment.return_value = { 'mode': enrollment.mode } response = self._get_response(self.course) self.assertContains(response, subsection.display_name) # Show the Verification Deadline for verified only self.assertContains(response, 'Verification Deadline') # Make sure pill exists for today's date self.assertContains(response, '

') # Make sure pill exists for next due assignment self.assertContains(response, '
') # No pills for verified enrollments self.assertNotContains(response, '
') # Make sure the assignment type is rendered self.assertContains(response, 'Homework:') enrollment.delete() enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode=CourseMode.AUDIT) mock_get_enrollment.return_value = { 'mode': enrollment.mode } expected_calls = [ call('course_id', str(self.course.id)), call('user_id', self.user.id), call('is_staff', self.user.is_staff), ] response = self._get_response(self.course) mock_set_custom_attribute.assert_has_calls(expected_calls, any_order=True) self.assertContains(response, subsection.display_name) # Don't show the Verification Deadline for audit self.assertNotContains(response, 'Verification Deadline') # Pill doesn't exist for assignment due tomorrow self.assertNotContains(response, '
') # Should have verified pills for audit enrollments self.assertContains(response, '
') # Make sure the assignment type is rendered self.assertContains(response, 'Homework:') @override_experiment_waffle_flag(RELATIVE_DATES_FLAG, active=True) def test_reset_deadlines_banner_displays(self): CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode=CourseMode.VERIFIED) now = datetime.now(utc) with self.store.bulk_operations(self.course.id): section = ItemFactory.create(category='chapter', parent_location=self.course.location) ItemFactory.create( category='sequential', display_name='Released', parent_location=section.location, start=now - timedelta(days=1), due=now - timedelta(days=1), # Setting this to tomorrow so it'll show the 'Due Next' pill graded=True, ) response = self._get_response(self.course) self.assertContains(response, 'div class="banner-cta-text"') class TestShowCoursewareMFE(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 REDIRECT_TO_COURSEWARE_MICROFRONTEND ExperimentWaffleFlag Giving us theoretically 2^5 = 32 states. >_< """ def test_permuations(self): """Test every permutation""" old_course_key = CourseKey.from_string("OpenEdX/Old/2020") new_course_key = CourseKey.from_string("course-v1:OpenEdX+New+2020") global_staff_user = UserFactory(username="global_staff", is_staff=True) regular_user = UserFactory(username="normal", is_staff=False) # Old style course keys are never supported and should always return false... old_mongo_combos = itertools.product( [regular_user, global_staff_user], # User (is global staff) [True, False], # is_course_staff [True, False], # preview_active (COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW) [True, False], # redirect_active (REDIRECT_TO_COURSEWARE_MICROFRONTEND) ) for user, is_course_staff, preview_active, redirect_active in old_mongo_combos: with override_waffle_flag(COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW, preview_active): with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, active=redirect_active): assert show_courseware_mfe_link(user, is_course_staff, old_course_key) is False # 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 override_waffle_flag(COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW, True): with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, active=True): # (preview=on, redirect=on) # Global and Course Staff can see the link. assert show_courseware_mfe_link(global_staff_user, True, new_course_key) assert show_courseware_mfe_link(global_staff_user, False, new_course_key) assert show_courseware_mfe_link(regular_user, True, new_course_key) # (Regular users would see the link, but they can't see the Legacy # experience, so it doesn't matter.) with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, active=False): # (preview=on, redirect=off) # Global and Course Staff can see the link. assert show_courseware_mfe_link(global_staff_user, True, new_course_key) assert show_courseware_mfe_link(global_staff_user, False, new_course_key) assert show_courseware_mfe_link(regular_user, True, new_course_key) # Regular users don't see the link. assert not show_courseware_mfe_link(regular_user, False, new_course_key) with override_waffle_flag(COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW, False): with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, active=True): # (preview=off, redirect=on) # Global staff see the link anyway assert show_courseware_mfe_link(global_staff_user, True, new_course_key) assert show_courseware_mfe_link(global_staff_user, False, new_course_key) # If redirect is active for their students, course staff see the link even # if preview=off. assert show_courseware_mfe_link(regular_user, True, new_course_key) # (Regular users would see the link, but they can't see the Legacy # experience, so it doesn't matter.) with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, active=False): # (preview=off, redirect=off) # Global staff see the link anyway assert show_courseware_mfe_link(global_staff_user, True, new_course_key) assert show_courseware_mfe_link(global_staff_user, False, new_course_key) # Course teams can NOT see the link because both rollout waffle flags are false. assert not show_courseware_mfe_link(regular_user, True, new_course_key) # Regular users don't see the link. assert not show_courseware_mfe_link(regular_user, False, new_course_key) @override_settings(LEARNING_MICROFRONTEND_URL='https://learningmfe.openedx.org') def test_url_generation(self): course_key = CourseKey.from_string("course-v1:OpenEdX+MFE+2020") section_key = UsageKey.from_string("block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction") unit_id = "block-v1:OpenEdX+MFE+2020+type@vertical+block@Getting_To_Know_You" assert get_learning_mfe_courseware_url(course_key) == ( 'https://learningmfe.openedx.org' '/course/course-v1:OpenEdX+MFE+2020' ) assert get_learning_mfe_courseware_url(course_key, section_key, '') == ( 'https://learningmfe.openedx.org' '/course/course-v1:OpenEdX+MFE+2020' '/block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction' ) assert get_learning_mfe_courseware_url(course_key, section_key, unit_id) == ( 'https://learningmfe.openedx.org' '/course/course-v1:OpenEdX+MFE+2020' '/block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction' '/block-v1:OpenEdX+MFE+2020+type@vertical+block@Getting_To_Know_You' ) @ddt.ddt class MFERedirectTests(BaseViewsTestCase): # lint-amnesty, pylint: disable=missing-class-docstring MODULESTORE = TEST_DATA_SPLIT_MODULESTORE def _get_urls(self): # lint-amnesty, pylint: disable=missing-function-docstring lms_url = reverse( 'courseware_section', kwargs={ 'course_id': str(self.course_key), 'chapter': str(self.chapter.location.block_id), 'section': str(self.section2.location.block_id), } ) mfe_url = '{}/course/{}/{}'.format( settings.LEARNING_MICROFRONTEND_URL, self.course_key, self.section2.location ) return lms_url, mfe_url def test_learner_redirect(self): # learners will be redirected when the waffle flag is set lms_url, mfe_url = self._get_urls() with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, active=True): assert self.client.get(lms_url).url == mfe_url def test_staff_no_redirect(self): lms_url, mfe_url = self._get_urls() # lint-amnesty, pylint: disable=unused-variable # course staff will not redirect course_staff = UserFactory.create(is_staff=False) CourseStaffRole(self.course_key).add_users(course_staff) self.client.login(username=course_staff.username, password='test') assert self.client.get(lms_url).status_code == 200 with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, active=True): assert self.client.get(lms_url).status_code == 200 # global staff will never be redirected self._create_global_staff_user() assert self.client.get(lms_url).status_code == 200 with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, active=True): 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, mfe_url = self._get_urls() # lint-amnesty, pylint: disable=unused-variable with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, active=True): assert self.client.get(lms_url).status_code == 200 class ContentOptimizationTestCase(ModuleStoreTestCase): """ Test our ability to make browser optimizations based on XBlock content. """ def setUp(self): super().setUp() self.math_html_usage_keys = [] with self.store.default_store(ModuleStoreEnum.Type.split): self.course = CourseFactory.create(display_name='teꜱᴛ course', run="Testing_course") with self.store.bulk_operations(self.course.id): chapter = ItemFactory.create( category='chapter', parent_location=self.course.location, display_name="Chapter 1", ) section = ItemFactory.create( category='sequential', parent_location=chapter.location, due=datetime(2013, 9, 18, 11, 30, 00), display_name='Sequential 1', format='Homework' ) self.math_vertical = ItemFactory.create( category='vertical', parent_location=section.location, display_name='Vertical with Mathjax HTML', ) self.no_math_vertical = ItemFactory.create( category='vertical', parent_location=section.location, display_name='Vertical with No Mathjax HTML', ) MATHJAX_TAG_PAIRS = [ (r"\(", r"\)"), (r"\[", r"\]"), ("[mathjaxinline]", "[/mathjaxinline]"), ("[mathjax]", "[/mathjax]"), ] for (i, (start_tag, end_tag)) in enumerate(MATHJAX_TAG_PAIRS): math_html_block = ItemFactory.create( category='html', parent_location=self.math_vertical.location, display_name=f"HTML With Mathjax {i}", data=f"

Hello Math! {start_tag}x^2 + y^2{end_tag}

", ) self.math_html_usage_keys.append(math_html_block.location) self.html_without_mathjax = ItemFactory.create( category='html', parent_location=self.no_math_vertical.location, display_name="HTML Without Mathjax", data="

I talk about mathjax, but I have no actual Math!

", ) self.course_key = self.course.id self.user = UserFactory(username='staff_user', profile__country='AX', is_staff=True) self.date = datetime(2013, 1, 22, tzinfo=UTC) self.enrollment = CourseEnrollment.enroll(self.user, self.course_key) self.enrollment.created = self.date self.enrollment.save() @override_waffle_flag(COURSEWARE_OPTIMIZED_RENDER_XBLOCK, True) def test_mathjax_detection(self): self.client.login(username=self.user.username, password=TEST_PASSWORD) # Check the HTML blocks with Math for usage_key in self.math_html_usage_keys: url = reverse("render_xblock", kwargs={'usage_key_string': str(usage_key)}) response = self.client.get(url) assert response.status_code == 200 assert b"MathJax.Hub.Config" in response.content # Check the one without Math... url = reverse("render_xblock", kwargs={ 'usage_key_string': str(self.html_without_mathjax.location) }) response = self.client.get(url) assert response.status_code == 200 assert b"MathJax.Hub.Config" not in response.content # The containing vertical should still return MathJax (for now) url = reverse("render_xblock", kwargs={ 'usage_key_string': str(self.no_math_vertical.location) }) response = self.client.get(url) assert response.status_code == 200 assert b"MathJax.Hub.Config" in response.content @override_waffle_flag(COURSEWARE_OPTIMIZED_RENDER_XBLOCK, False) def test_mathjax_detection_disabled(self): """Check that we can disable optimizations.""" self.client.login(username=self.user.username, password=TEST_PASSWORD) url = reverse("render_xblock", kwargs={ 'usage_key_string': str(self.html_without_mathjax.location) }) response = self.client.get(url) assert response.status_code == 200 assert b"MathJax.Hub.Config" in response.content