# -*- coding: utf-8 -*- """Tests for course home page date summary blocks.""" from datetime import datetime, timedelta import ddt import waffle from django.contrib.messages.middleware import MessageMiddleware from django.test import RequestFactory from django.urls import reverse from freezegun import freeze_time from mock import patch from pytz import utc from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory from lms.djangoapps.courseware.courses import get_course_date_blocks from lms.djangoapps.courseware.date_summary import ( CertificateAvailableDate, CourseAssignmentDate, CourseEndDate, CourseExpiredDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate ) from lms.djangoapps.courseware.models import ( CourseDynamicUpgradeDeadlineConfiguration, DynamicUpgradeDeadlineConfiguration, OrgDynamicUpgradeDeadlineConfiguration ) from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.schedules.signals import CREATE_SCHEDULE_WAFFLE_FLAG from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from openedx.features.course_experience import ( DATE_WIDGET_V2_FLAG, UNIFIED_COURSE_TAB_FLAG, UPGRADE_DEADLINE_MESSAGE, CourseHomeMessages ) from student.tests.factories import TEST_PASSWORD, CourseEnrollmentFactory, UserFactory from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @ddt.ddt class CourseDateSummaryTest(SharedModuleStoreTestCase): """Tests for course date summary blocks.""" def setUp(self): super(CourseDateSummaryTest, self).setUp() SelfPacedConfiguration.objects.create(enable_course_home_improvements=True) def test_course_info_feature_flag(self): SelfPacedConfiguration(enable_course_home_improvements=False).save() course = create_course_run() user = create_user() CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) self.client.login(username=user.username, password=TEST_PASSWORD) url = reverse('info', args=(course.id,)) response = self.client.get(url) self.assertNotContains(response, 'date-summary', status_code=302) def test_course_home_logged_out(self): course = create_course_run() url = reverse('openedx.course_experience.course_home', args=(course.id,)) response = self.client.get(url) self.assertEqual(200, response.status_code) # Tests for which blocks are enabled def assert_block_types(self, course, user, expected_blocks): """Assert that the enabled block types for this course are as expected.""" blocks = get_course_date_blocks(course, user) self.assertEqual(len(blocks), len(expected_blocks)) self.assertEqual(set(type(b) for b in blocks), set(expected_blocks)) @ddt.data( # Verified enrollment with no photo-verification before course start ({}, {}, (CourseEndDate, CourseStartDate, TodaysDate, VerificationDeadlineDate)), # Verified enrollment with `approved` photo-verification after course end ({'days_till_start': -10, 'days_till_end': -5, 'days_till_upgrade_deadline': -6, 'days_till_verification_deadline': -5, }, {'verification_status': 'approved'}, (TodaysDate, CourseEndDate)), # Verified enrollment with `expired` photo-verification during course run ({'days_till_start': -10}, {'verification_status': 'expired'}, (TodaysDate, CourseEndDate, VerificationDeadlineDate)), # Verified enrollment with `approved` photo-verification during course run ({'days_till_start': -10, }, {'verification_status': 'approved'}, (TodaysDate, CourseEndDate)), # Verified enrollment with *NO* course end date ({'days_till_end': None}, {}, (CourseStartDate, TodaysDate, VerificationDeadlineDate)), # Verified enrollment with no photo-verification during course run ({'days_till_start': -1}, {}, (TodaysDate, CourseEndDate, VerificationDeadlineDate)), # Verification approved ({'days_till_start': -10, 'days_till_upgrade_deadline': -1, 'days_till_verification_deadline': 1, }, {'verification_status': 'approved'}, (TodaysDate, CourseEndDate)), # After upgrade deadline ({'days_till_start': -10, 'days_till_upgrade_deadline': -1}, {}, (TodaysDate, CourseEndDate, VerificationDeadlineDate)), # After verification deadline ({'days_till_start': -10, 'days_till_upgrade_deadline': -2, 'days_till_verification_deadline': -1}, {}, (TodaysDate, CourseEndDate, VerificationDeadlineDate)), ) @ddt.unpack def test_enabled_block_types(self, course_kwargs, user_kwargs, expected_blocks): course = create_course_run(**course_kwargs) user = create_user(**user_kwargs) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) self.assert_block_types(course, user, expected_blocks) @override_waffle_flag(DATE_WIDGET_V2_FLAG, active=True) def test_enabled_block_types_with_assignments(self): # pylint: disable=too-many-statements """ Creates a course with multiple subsections to test all of the different cases for assignment dates showing up. Mocks out calling the edx-when service and then validates the correct data is set and returned. """ course = create_course_run(days_till_start=-100) user = create_user() request = RequestFactory().request() request.user = user CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) now = datetime.now(utc) assignment_title_html = [''] with self.store.bulk_operations(course.id): section = ItemFactory.create(category='chapter', parent_location=course.location) subsection_1 = ItemFactory.create( category='sequential', display_name='Released', parent_location=section.location, start=now - timedelta(days=1), due=now + timedelta(days=6), graded=True, ) subsection_2 = ItemFactory.create( category='sequential', display_name='Not released', parent_location=section.location, start=now + timedelta(days=1), due=now + timedelta(days=7), graded=True, ) subsection_3 = ItemFactory.create( category='sequential', display_name='Third nearest assignment', parent_location=section.location, start=now + timedelta(days=1), due=now + timedelta(days=8), graded=True, ) subsection_4 = ItemFactory.create( category='sequential', display_name='Past due date', parent_location=section.location, start=now - timedelta(days=14), due=now - timedelta(days=7), graded=True, ) subsection_5 = ItemFactory.create( category='sequential', display_name='Not returned since we do not get non-graded subsections', parent_location=section.location, start=now + timedelta(days=1), due=now - timedelta(days=7), graded=False, ) subsection_6 = ItemFactory.create( category='sequential', display_name='No start date', parent_location=section.location, start=None, due=now + timedelta(days=9), graded=True, ) subsection_7 = ItemFactory.create( category='sequential', # Setting display name to None should set the assignment title to 'Assignment' display_name=None, parent_location=section.location, start=now - timedelta(days=14), due=now + timedelta(days=10), graded=True, ) dummy_subsection = ItemFactory.create(category='sequential', graded=True, due=now + timedelta(days=11)) # We are deleting this subsection right after creating it because we need to pass in a real # location object (dummy_subsection.location), but do not want this to exist inside of the modulestore with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course.id): self.store.delete_item(dummy_subsection.location, user.id) with patch('lms.djangoapps.courseware.courses.get_dates_for_course') as mock_get_dates: mock_get_dates.return_value = { (subsection_1.location, 'due'): subsection_1.due, (subsection_1.location, 'start'): subsection_1.start, (subsection_2.location, 'due'): subsection_2.due, (subsection_2.location, 'start'): subsection_2.start, (subsection_3.location, 'due'): subsection_3.due, (subsection_3.location, 'start'): subsection_3.start, (subsection_4.location, 'due'): subsection_4.due, (subsection_4.location, 'start'): subsection_4.start, (subsection_5.location, 'due'): subsection_5.due, (subsection_5.location, 'start'): subsection_5.start, (subsection_6.location, 'due'): subsection_6.due, (subsection_7.location, 'due'): subsection_7.due, (subsection_7.location, 'start'): subsection_7.start, # Adding this in for the case where we return a block that # doesn't actually exist in the modulestore. Should just be ignored. (dummy_subsection.location, 'due'): dummy_subsection.due, } # Standard widget case where we restrict the number of assignments. expected_blocks = ( TodaysDate, CourseAssignmentDate, CourseAssignmentDate, CourseEndDate, VerificationDeadlineDate ) blocks = get_course_date_blocks(course, user, request, num_assignments=2) self.assertEqual(len(blocks), len(expected_blocks)) self.assertEqual(set(type(b) for b in blocks), set(expected_blocks)) assignment_blocks = filter(lambda b: isinstance(b, CourseAssignmentDate), blocks) for assignment in assignment_blocks: assignment_title = str(assignment.title_html) or str(assignment.title) self.assertNotEqual(assignment_title, 'Third nearest assignment') self.assertNotEqual(assignment_title, 'Past due date') self.assertNotEqual(assignment_title, 'Not returned since we do not get non-graded subsections') # checking if it is _in_ the title instead of being the title since released assignments # are actually links. Unreleased assignments are just the string of the title. if 'Released' in assignment_title: for html_tag in assignment_title_html: self.assertIn(html_tag, assignment_title) elif assignment_title == 'Not released': for html_tag in assignment_title_html: self.assertNotIn(html_tag, assignment_title) # No restrictions on number of assignments to return expected_blocks = ( CourseStartDate, TodaysDate, CourseAssignmentDate, CourseAssignmentDate, CourseAssignmentDate, CourseAssignmentDate, CourseAssignmentDate, CourseAssignmentDate, CourseEndDate, VerificationDeadlineDate ) blocks = get_course_date_blocks(course, user, request, include_past_dates=True) self.assertEqual(len(blocks), len(expected_blocks)) self.assertEqual(set(type(b) for b in blocks), set(expected_blocks)) assignment_blocks = filter(lambda b: isinstance(b, CourseAssignmentDate), blocks) for assignment in assignment_blocks: assignment_title = str(assignment.title_html) or str(assignment.title) self.assertNotEqual(assignment_title, 'Not returned since we do not get non-graded subsections') # checking if it is _in_ the title instead of being the title since released assignments # are actually links. Unreleased assignments are just the string of the title. if 'Released' in assignment_title: for html_tag in assignment_title_html: self.assertIn(html_tag, assignment_title) elif assignment_title == 'Not released': for html_tag in assignment_title_html: self.assertNotIn(html_tag, assignment_title) elif assignment_title == 'Third nearest assignment': # It's still not released for html_tag in assignment_title_html: self.assertNotIn(html_tag, assignment_title) elif 'Past due date' in assignment_title: self.assertGreater(now, assignment.date) for html_tag in assignment_title_html: self.assertIn(html_tag, assignment_title) elif 'No start date' == assignment_title: # Can't determine if it is released so it does not get a link for html_tag in assignment_title_html: self.assertNotIn(html_tag, assignment_title) # This is the item with no display name where we set one ourselves. elif 'Assignment' in assignment_title: # Can't determine if it is released so it does not get a link for html_tag in assignment_title_html: self.assertIn(html_tag, assignment_title) @override_waffle_flag(DATE_WIDGET_V2_FLAG, active=True) def test_enabled_block_types_with_expired_course(self): course = create_course_run(days_till_start=-100) user = create_user() # These two lines are to trigger the course expired block to be rendered CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT) CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=utc)) expected_blocks = ( TodaysDate, CourseEndDate, CourseExpiredDate, VerifiedUpgradeDeadlineDate ) self.assert_block_types(course, user, expected_blocks) @ddt.data( # Course not started ({}, (CourseStartDate, TodaysDate, CourseEndDate)), # Course active ({'days_till_start': -1}, (TodaysDate, CourseEndDate)), # Course ended ({'days_till_start': -10, 'days_till_end': -5}, (TodaysDate, CourseEndDate)), ) @ddt.unpack def test_enabled_block_types_without_enrollment(self, course_kwargs, expected_blocks): course = create_course_run(**course_kwargs) user = create_user() self.assert_block_types(course, user, expected_blocks) def test_enabled_block_types_with_non_upgradeable_course_run(self): course = create_course_run(days_till_start=-10, days_till_verification_deadline=None) user = create_user() CourseMode.objects.get(course_id=course.id, mode_slug=CourseMode.VERIFIED).delete() CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT) self.assert_block_types(course, user, (TodaysDate, CourseEndDate)) def test_todays_date_block(self): """ Helper function to test that today's date block renders correctly and displays the correct time, accounting for daylight savings """ with freeze_time('2015-01-02'): course = create_course_run() user = create_user() block = TodaysDate(course, user) self.assertTrue(block.is_enabled) self.assertEqual(block.date, datetime.now(utc)) self.assertEqual(block.title, 'current_datetime') @ddt.data( 'info', 'openedx.course_experience.course_home', ) @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True) def test_todays_date_no_timezone(self, url_name): with freeze_time('2015-01-02'): course = create_course_run() user = create_user() self.client.login(username=user.username, password=TEST_PASSWORD) html_elements = [ '

Upcoming Dates

', '
Upcoming Dates', '
time til end (end date shown) {'weeks_to_complete': 4}, # Weeks to complete < time til end (end date not shown) ) @override_waffle_flag(DATE_WIDGET_V2_FLAG, active=True) def test_course_end_date_self_paced(self, cr_details): """ In self-paced courses, the end date will now only show up if the learner views the course within the course's weeks to complete (as defined in the course-discovery service). E.g. if the weeks to complete is 5 weeks and the course doesn't end for 10 weeks, there will be no end date, but if the course ends in 3 weeks, the end date will appear. """ now = datetime.now(utc) end_timedelta_number = 5 course = CourseFactory.create( start=now + timedelta(days=-7), end=now + timedelta(weeks=end_timedelta_number), self_paced=True) user = create_user() with patch('lms.djangoapps.courseware.date_summary.get_course_run_details') as mock_get_cr_details: mock_get_cr_details.return_value = cr_details block = CourseEndDate(course, user) self.assertEqual(block.title, 'Course End') if cr_details['weeks_to_complete'] > end_timedelta_number: self.assertEqual(block.date, course.end) else: self.assertIsNone(block.date) def test_ecommerce_checkout_redirect(self): """Verify the block link redirects to ecommerce checkout if it's enabled.""" sku = 'TESTSKU' configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True) course = create_course_run() user = create_user() course_mode = CourseMode.objects.get(course_id=course.id, mode_slug=CourseMode.VERIFIED) course_mode.sku = sku course_mode.save() CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) block = VerifiedUpgradeDeadlineDate(course, user) self.assertEqual(block.link, '{}?sku={}'.format(configuration.basket_checkout_page, sku)) ## CertificateAvailableDate @waffle.testutils.override_switch('certificates.auto_certificate_generation', True) def test_no_certificate_available_date(self): course = create_course_run(days_till_start=-1) user = create_user() CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT) block = CertificateAvailableDate(course, user) self.assertEqual(block.date, None) self.assertFalse(block.is_enabled) ## CertificateAvailableDate @waffle.testutils.override_switch('certificates.auto_certificate_generation', True) def test_no_certificate_available_date_for_self_paced(self): course = create_self_paced_course_run() verified_user = create_user() CourseEnrollmentFactory(course_id=course.id, user=verified_user, mode=CourseMode.VERIFIED) course.certificate_available_date = datetime.now(utc) + timedelta(days=7) course.save() block = CertificateAvailableDate(course, verified_user) self.assertNotEqual(block.date, None) self.assertFalse(block.is_enabled) def test_no_certificate_available_date_for_audit_course(self): """ Tests that Certificate Available Date is not visible in the course "Important Course Dates" section if the course only has audit mode. """ course = create_course_run() audit_user = create_user() # Enroll learner in the audit mode and verify the course only has 1 mode (audit) CourseEnrollmentFactory(course_id=course.id, user=audit_user, mode=CourseMode.AUDIT) CourseMode.objects.get(course_id=course.id, mode_slug=CourseMode.VERIFIED).delete() all_course_modes = CourseMode.modes_for_course(course.id) self.assertEqual(len(all_course_modes), 1) self.assertEqual(all_course_modes[0].slug, CourseMode.AUDIT) course.certificate_available_date = datetime.now(utc) + timedelta(days=7) course.save() # Verify Certificate Available Date is not enabled for learner. block = CertificateAvailableDate(course, audit_user) self.assertFalse(block.is_enabled) self.assertNotEqual(block.date, None) @waffle.testutils.override_switch('certificates.auto_certificate_generation', True) def test_certificate_available_date_defined(self): course = create_course_run() audit_user = create_user() CourseEnrollmentFactory(course_id=course.id, user=audit_user, mode=CourseMode.AUDIT) verified_user = create_user() CourseEnrollmentFactory(course_id=course.id, user=verified_user, mode=CourseMode.VERIFIED) course.certificate_available_date = datetime.now(utc) + timedelta(days=7) enable_course_certificates(course) CertificateAvailableDate(course, audit_user) for block in (CertificateAvailableDate(course, audit_user), CertificateAvailableDate(course, verified_user)): self.assertIsNotNone(course.certificate_available_date) self.assertEqual(block.date, course.certificate_available_date) self.assertTrue(block.is_enabled) ## VerificationDeadlineDate def test_no_verification_deadline(self): course = create_course_run(days_till_start=-1, days_till_verification_deadline=None) user = create_user() CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) block = VerificationDeadlineDate(course, user) self.assertFalse(block.is_enabled) def test_no_verified_enrollment(self): course = create_course_run(days_till_start=-1) user = create_user() CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT) block = VerificationDeadlineDate(course, user) self.assertFalse(block.is_enabled) def test_verification_deadline_date_upcoming(self): with freeze_time('2015-01-02'): course = create_course_run(days_till_start=-1) user = create_user() CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) block = VerificationDeadlineDate(course, user) self.assertEqual(block.css_class, 'verification-deadline-upcoming') self.assertEqual(block.title, 'Verification Deadline') self.assertEqual(block.date, datetime.now(utc) + timedelta(days=14)) self.assertEqual( block.description, 'You must successfully complete verification before this date to qualify for a Verified Certificate.' ) self.assertEqual(block.link_text, 'Verify My Identity') self.assertEqual(block.link, reverse('verify_student_verify_now', args=(course.id,))) def test_verification_deadline_date_retry(self): with freeze_time('2015-01-02'): course = create_course_run(days_till_start=-1) user = create_user(verification_status='denied') CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) block = VerificationDeadlineDate(course, user) self.assertEqual(block.css_class, 'verification-deadline-retry') self.assertEqual(block.title, 'Verification Deadline') self.assertEqual(block.date, datetime.now(utc) + timedelta(days=14)) self.assertEqual( block.description, 'You must successfully complete verification before this date to qualify for a Verified Certificate.' ) self.assertEqual(block.link_text, 'Retry Verification') self.assertEqual(block.link, reverse('verify_student_reverify')) def test_verification_deadline_date_denied(self): with freeze_time('2015-01-02'): course = create_course_run(days_till_start=-10, days_till_verification_deadline=-1) user = create_user(verification_status='denied') CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) block = VerificationDeadlineDate(course, user) self.assertEqual(block.css_class, 'verification-deadline-passed') self.assertEqual(block.title, 'Missed Verification Deadline') self.assertEqual(block.date, datetime.now(utc) + timedelta(days=-1)) self.assertEqual( block.description, "Unfortunately you missed this course's deadline for a successful verification." ) self.assertEqual(block.link_text, 'Learn More') self.assertEqual(block.link, '') @ddt.data( (-1, u'1 day ago - {date}'), (1, u'in 1 day - {date}') ) @ddt.unpack def test_render_date_string_past(self, delta, expected_date_string): with freeze_time('2015-01-02'): course = create_course_run(days_till_start=-10, days_till_verification_deadline=delta) user = create_user(verification_status='denied') CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) block = VerificationDeadlineDate(course, user) self.assertEqual(block.relative_datestring, expected_date_string) @ddt.ddt class TestDateAlerts(SharedModuleStoreTestCase): """ Unit tests for date alerts. """ def setUp(self): super(TestDateAlerts, self).setUp() with freeze_time('2017-07-01 09:00:00'): self.course = create_course_run(days_till_start=0) self.course.certificate_available_date = self.course.start + timedelta(days=21) enable_course_certificates(self.course) self.enrollment = CourseEnrollmentFactory(course_id=self.course.id, mode=CourseMode.AUDIT) self.request = RequestFactory().request() self.request.session = {} self.request.user = self.enrollment.user MessageMiddleware().process_request(self.request) @ddt.data( ['2017-01-01 09:00:00', u'in 6 months on