""" 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"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, '
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