""" Test the about xblock """ import datetime from unittest import mock from unittest.mock import patch import ddt import pytz from ccx_keys.locator import CCXLocator from django.conf import settings from django.test.utils import override_settings from django.urls import reverse from milestones.tests.utils import MilestonesTestCaseMixin from waffle.testutils import override_switch from common.djangoapps.course_modes.models import CourseMode from edx_toggles.toggles.testutils import override_waffle_flag # lint-amnesty, pylint: disable=wrong-import-order from lms.djangoapps.ccx.tests.factories import CcxFactory from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML from openedx.features.course_experience.waffle import WAFFLE_NAMESPACE as COURSE_EXPERIENCE_WAFFLE_NAMESPACE from common.djangoapps.student.tests.factories import AdminFactory, CourseEnrollmentAllowedFactory, UserFactory from common.djangoapps.track.tests import EventTrackingTestCase from common.djangoapps.util.milestones_helpers import get_prerequisite_courses_display, set_prerequisite_courses from xmodule.course_module import ( CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE, COURSE_VISIBILITY_PRIVATE, COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE ) from xmodule.modulestore.tests.django_utils import ( TEST_DATA_MIXED_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase, SharedModuleStoreTestCase ) from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.utils import TEST_DATA_DIR from xmodule.modulestore.xml_importer import import_course_from_xml from .helpers import LoginEnrollmentTestCase # HTML for registration button REG_STR = "
" SHIB_ERROR_STR = "The currently logged-in user account does not have permission to enroll in this course." @ddt.ddt class AboutTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase, EventTrackingTestCase, MilestonesTestCaseMixin): """ Tests about xblock. """ @classmethod def setUpClass(cls): super().setUpClass() cls.course = CourseFactory.create() cls.course_without_about = CourseFactory.create(catalog_visibility=CATALOG_VISIBILITY_NONE) cls.course_with_about = CourseFactory.create(catalog_visibility=CATALOG_VISIBILITY_ABOUT) cls.purchase_course = CourseFactory.create(org='MITx', number='buyme', display_name='Course To Buy') cls.about = ItemFactory.create( category="about", parent_location=cls.course.location, data="OOGIE BLOOGIE", display_name="overview" ) cls.about = ItemFactory.create( category="about", parent_location=cls.course_without_about.location, data="WITHOUT ABOUT", display_name="overview" ) cls.about = ItemFactory.create( category="about", parent_location=cls.course_with_about.location, data="WITH ABOUT", display_name="overview" ) def setUp(self): super().setUp() self.course_mode = CourseMode( course_id=self.purchase_course.id, mode_slug=CourseMode.DEFAULT_MODE_SLUG, mode_display_name=CourseMode.DEFAULT_MODE_SLUG, min_price=10 ) self.course_mode.save() def test_anonymous_user(self): """ This test asserts that a non-logged in user can visit the course about page """ url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) self.assertContains(resp, "OOGIE BLOOGIE") # Check that registration button is present self.assertContains(resp, REG_STR) def test_logged_in(self): """ This test asserts that a logged-in user can visit the course about page """ self.setup_user() url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) self.assertContains(resp, "OOGIE BLOOGIE") def test_already_enrolled(self): """ Asserts that the end user sees the appropriate messaging when he/she visits the course about page, but is already enrolled """ self.setup_user() self.enroll(self.course, True) url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) self.assertContains(resp, "You are enrolled in this course") self.assertContains(resp, "View Course") @override_settings(COURSE_ABOUT_VISIBILITY_PERMISSION="see_about_page") def test_visible_about_page_settings(self): """ Verify that the About Page honors the permission settings in the course module """ url = reverse('about_course', args=[str(self.course_with_about.id)]) resp = self.client.get(url) self.assertContains(resp, "WITH ABOUT") url = reverse('about_course', args=[str(self.course_without_about.id)]) resp = self.client.get(url) assert resp.status_code == 404 @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True}) def test_logged_in_marketing(self): self.setup_user() url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) # should be redirected assert resp.status_code == 302 # follow this time, and check we're redirected to the course home page resp = self.client.get(url, follow=True) target_url = resp.redirect_chain[-1][0] course_home_url = reverse('openedx.course_experience.course_home', args=[str(self.course.id)]) assert target_url.endswith(course_home_url) @patch.dict(settings.FEATURES, {'ENABLE_COURSE_HOME_REDIRECT': False}) @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True}) def test_logged_in_marketing_without_course_home_redirect(self): """ Verify user is not redirected to course home page when ENABLE_COURSE_HOME_REDIRECT is set to False """ self.setup_user() url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) # should not be redirected self.assertContains(resp, "OOGIE BLOOGIE") @patch.dict(settings.FEATURES, {'ENABLE_COURSE_HOME_REDIRECT': True}) @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': False}) def test_logged_in_marketing_without_mktg_site(self): """ Verify user is not redirected to course home page when ENABLE_MKTG_SITE is set to False """ self.setup_user() url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) # should not be redirected self.assertContains(resp, "OOGIE BLOOGIE") @patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True}) def test_pre_requisite_course(self): pre_requisite_course = CourseFactory.create(org='edX', course='900', display_name='pre requisite course') course = CourseFactory.create(pre_requisite_courses=[str(pre_requisite_course.id)]) self.setup_user() url = reverse('about_course', args=[str(course.id)]) resp = self.client.get(url) assert resp.status_code == 200 pre_requisite_courses = get_prerequisite_courses_display(course) pre_requisite_course_about_url = reverse('about_course', args=[str(pre_requisite_courses[0]['key'])]) assert '{}'.format(pre_requisite_course_about_url, pre_requisite_courses[0]['display']) in resp.content.decode(resp.charset).strip('\n') # pylint: disable=line-too-long @patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True}) def test_about_page_unfulfilled_prereqs(self): pre_requisite_course = CourseFactory.create( org='edX', course='901', display_name='pre requisite course', ) pre_requisite_courses = [str(pre_requisite_course.id)] # for this failure to occur, the enrollment window needs to be in the past course = CourseFactory.create( org='edX', course='1000', # closed enrollment enrollment_start=datetime.datetime(2013, 1, 1), enrollment_end=datetime.datetime(2014, 1, 1), start=datetime.datetime(2013, 1, 1), end=datetime.datetime(2030, 1, 1), pre_requisite_courses=pre_requisite_courses, ) set_prerequisite_courses(course.id, pre_requisite_courses) self.setup_user() self.enroll(self.course, True) self.enroll(pre_requisite_course, True) url = reverse('about_course', args=[str(course.id)]) resp = self.client.get(url) assert resp.status_code == 200 pre_requisite_courses = get_prerequisite_courses_display(course) pre_requisite_course_about_url = reverse('about_course', args=[str(pre_requisite_courses[0]['key'])]) assert '{}'.format(pre_requisite_course_about_url, pre_requisite_courses[0]['display']) in resp.content.decode(resp.charset).strip('\n') # pylint: disable=line-too-long url = reverse('about_course', args=[str(pre_requisite_course.id)]) resp = self.client.get(url) assert resp.status_code == 200 @ddt.data( [COURSE_VISIBILITY_PRIVATE], [COURSE_VISIBILITY_PUBLIC_OUTLINE], [COURSE_VISIBILITY_PUBLIC], ) @ddt.unpack def test_about_page_public_view(self, course_visibility): """ Assert that anonymous or unenrolled users see View Course option when unenrolled access flag is set """ with mock.patch('xmodule.course_module.CourseBlock.course_visibility', course_visibility): with override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=True): url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) if course_visibility == COURSE_VISIBILITY_PUBLIC or course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE: # lint-amnesty, pylint: disable=consider-using-in self.assertContains(resp, "View Course") else: self.assertContains(resp, "Enroll Now") class AboutTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase): """ Tests for the course about page """ MODULESTORE = TEST_DATA_MIXED_MODULESTORE def setUp(self): """ Set up the tests """ super().setUp() # The following test course (which lives at common/test/data/2014) # is closed; we're testing that an about page still appears when # the course is already closed self.xml_course_id = self.store.make_course_key('edX', 'detached_pages', '2014') import_course_from_xml( self.store, 'test_user', TEST_DATA_DIR, source_dirs=['2014'], static_content_store=None, target_id=self.xml_course_id, raise_on_failure=True, create_if_not_present=True, ) # this text appears in that course's about page # common/test/data/2014/about/overview.html self.xml_data = "about page 463139" @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) def test_logged_in_xml(self): self.setup_user() url = reverse('about_course', args=[str(self.xml_course_id)]) resp = self.client.get(url) self.assertContains(resp, self.xml_data) @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) def test_anonymous_user_xml(self): url = reverse('about_course', args=[str(self.xml_course_id)]) resp = self.client.get(url) self.assertContains(resp, self.xml_data) class AboutWithCappedEnrollmentsTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase): """ This test case will check the About page when a course has a capped enrollment """ @classmethod def setUpClass(cls): super().setUpClass() cls.course = CourseFactory.create(metadata={"max_student_enrollments_allowed": 1}) cls.about = ItemFactory.create( category="about", parent_location=cls.course.location, data="OOGIE BLOOGIE", display_name="overview" ) def test_enrollment_cap(self): """ This test will make sure that enrollment caps are enforced """ self.setup_user() url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) self.assertContains(resp, '') self.enroll(self.course, verify=True) # pylint: disable=attribute-defined-outside-init # create a new account since the first account is already enrolled in the course self.email = 'foo_second@test.com' self.password = 'bar' self.username = 'test_second' self.create_account(self.username, self.email, self.password) self.activate_user(self.email) self.login(self.email, self.password) # Get the about page again and make sure that the page says that the course is full resp = self.client.get(url) self.assertContains(resp, "Course is full") # Try to enroll as well result = self.enroll(self.course) assert not result # Check that registration button is not present self.assertNotContains(resp, REG_STR) class AboutWithInvitationOnly(SharedModuleStoreTestCase): """ This test case will check the About page when a course is invitation only. """ @classmethod def setUpClass(cls): super().setUpClass() cls.course = CourseFactory.create(metadata={"invitation_only": True}) cls.about = ItemFactory.create( category="about", parent_location=cls.course.location, display_name="overview" ) def test_invitation_only(self): """ Test for user not logged in, invitation only course. """ url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) self.assertContains(resp, "Enrollment in this course is by invitation only") # Check that registration button is not present self.assertNotContains(resp, REG_STR) def test_invitation_only_but_allowed(self): """ Test for user logged in and allowed to enroll in invitation only course. """ # Course is invitation only, student is allowed to enroll and logged in user = UserFactory.create(username='allowed_student', password='test', email='allowed_student@test.com') CourseEnrollmentAllowedFactory(email=user.email, course_id=self.course.id) self.client.login(username=user.username, password='test') url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) self.assertContains(resp, "Enroll Now") # Check that registration button is present self.assertContains(resp, REG_STR) class AboutWithClosedEnrollment(ModuleStoreTestCase): """ This test case will check the About page for a course that has enrollment start/end set but it is currently outside of that period. """ def setUp(self): super().setUp() self.course = CourseFactory.create(metadata={"invitation_only": False}) # Setup enrollment period to be in future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) nextday = tomorrow + datetime.timedelta(days=1) self.course.enrollment_start = tomorrow self.course.enrollment_end = nextday self.course = self.update_course(self.course, self.user.id) self.about = ItemFactory.create( category="about", parent_location=self.course.location, display_name="overview" ) def test_closed_enrollmement(self): url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) self.assertContains(resp, "Enrollment is Closed") # Check that registration button is not present self.assertNotContains(resp, REG_STR) def test_course_price_is_not_visble_in_sidebar(self): url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) # course price is not visible ihe course_about page when the course # mode is not set to honor self.assertNotContains(resp, '$10') @ddt.ddt class AboutSidebarHTMLTestCase(SharedModuleStoreTestCase): """ This test case will check the About page for the content in the HTML sidebar. """ def setUp(self): super().setUp() self.course = CourseFactory.create() @ddt.data( ("", "", False), ("about_sidebar_html", "About Sidebar HTML Heading", False), ("about_sidebar_html", "", False), ("", "", True), ("about_sidebar_html", "About Sidebar HTML Heading", True), ("about_sidebar_html", "", True), ) @ddt.unpack def test_html_sidebar_enabled(self, itemfactory_display_name, itemfactory_data, waffle_switch_value): with override_switch( '{}.{}'.format( COURSE_EXPERIENCE_WAFFLE_NAMESPACE, ENABLE_COURSE_ABOUT_SIDEBAR_HTML ), active=waffle_switch_value ): if itemfactory_display_name: ItemFactory.create( category="about", parent_location=self.course.location, display_name=itemfactory_display_name, data=itemfactory_data, ) url = reverse('about_course', args=[str(self.course.id)]) resp = self.client.get(url) if waffle_switch_value and itemfactory_display_name and itemfactory_data: self.assertContains(resp, '
') self.assertContains(resp, itemfactory_data) else: self.assertNotContains(resp, '
') class CourseAboutTestCaseCCX(SharedModuleStoreTestCase, LoginEnrollmentTestCase): """ Test for unenrolled student tries to access ccx. Note: Only CCX coach can enroll a student in CCX. In sum self-registration not allowed. """ MODULESTORE = TEST_DATA_SPLIT_MODULESTORE @classmethod def setUpClass(cls): super().setUpClass() cls.course = CourseFactory.create() def setUp(self): super().setUp() # Create ccx coach account self.coach = coach = AdminFactory.create(password="test") self.client.login(username=coach.username, password="test") def test_redirect_to_dashboard_unenrolled_ccx(self): """ Assert that when unenrolled user tries to access CCX do not allow the user to self-register. Redirect them to their student dashboard """ # create ccx ccx = CcxFactory(course_id=self.course.id, coach=self.coach) ccx_locator = CCXLocator.from_course_locator(self.course.id, str(ccx.id)) self.setup_user() url = reverse('openedx.course_experience.course_home', args=[ccx_locator]) response = self.client.get(url) expected = reverse('dashboard') self.assertRedirects(response, expected, status_code=302, target_status_code=200)