From 8aeb460133d3351a2584463d3584fab6811798ef Mon Sep 17 00:00:00 2001 From: Dillon Dumesnil Date: Tue, 18 May 2021 16:31:05 -0400 Subject: [PATCH] feat: Update the course end date block display logic For self-paced courses, we have decided to switch to showing the end date as long as it is within 365 days rather than the expected duration of the course. --- lms/djangoapps/courseware/date_summary.py | 41 +++++++++++----- .../courseware/tests/test_date_summary.py | 48 ++++++++----------- .../tests/views/test_course_dates.py | 2 +- 3 files changed, 48 insertions(+), 43 deletions(-) diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index bd6bb1534b..22cce48a90 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -24,7 +24,6 @@ from lms.djangoapps.certificates.api import get_active_web_certificate from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link, can_show_verified_upgrade from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.services import IDVerificationService -from openedx.core.djangoapps.catalog.utils import get_course_run_details from openedx.core.djangoapps.certificates.api import can_show_certificate_available_date_field from openedx.core.djangolib.markup import HTML, Text from openedx.features.course_duration_limits.access import get_user_course_expiration_date @@ -280,7 +279,7 @@ class CourseStartDate(DateSummary): enrollment = CourseEnrollment.get_enrollment(self.user, self.course_id) if enrollment and self.course.end and enrollment.created > self.course.end: return ugettext_lazy('Enrollment Date') - return ugettext_lazy('Course Starts') + return ugettext_lazy('Course starts') def register_alerts(self, request, course): """ @@ -317,28 +316,44 @@ class CourseEndDate(DateSummary): Displays the end date of the course. """ css_class = 'end-date' - title = ugettext_lazy('Course End') + title = ugettext_lazy('Course ends') is_enabled = True @property def description(self): - if self.current_time <= self.date: + """ + Returns a description for what experience changes a learner encounters when the course end date passes. + Note that this currently contains 4 scenarios: + 1. End date is in the future and learner is enrolled in a certificate earning mode + 2. End date is in the future and learner is not enrolled at all or not enrolled + in a certificate earning mode + 3. End date is in the past + 4. End date does not exist (and now neither does the description) + """ + if self.date and self.current_time <= self.date: mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_id) if is_active and CourseMode.is_eligible_for_certificate(mode): - return _('To earn a certificate, you must complete all requirements before this date.') + return _('This course will be archived, which means you can review the course content ' + 'but can no longer participate in graded assignments or earn a certificate.') else: - return _('After this date, course content will be archived.') - return _('This course is archived, which means you can review course content but it is no longer active.') + return _('After the course ends, the course content will be archived and no longer active.') + elif self.date: + return _('This course is archived, which means you can review course content but it is no longer active.') + else: + return '' @property def date(self): + """ + Returns the course end date, if applicable. + For self-paced courses using Personalized Learner Schedules, the end date is only displayed + if it is within 365 days. + """ if self.course.self_paced and RELATIVE_DATES_FLAG.is_enabled(self.course_id): - weeks_to_complete = get_course_run_details(self.course.id, ['weeks_to_complete']).get('weeks_to_complete') - if weeks_to_complete: - course_duration = datetime.timedelta(weeks=weeks_to_complete) - if self.course.end and self.course.end < (self.current_time + course_duration): - return self.course.end - return None + one_year = datetime.timedelta(days=365) + if self.course.end and self.course.end < (self.current_time + one_year): + return self.course.end + return None return self.course.end diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index f39b78dc2b..d11c1bbe21 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -4,7 +4,6 @@ from datetime import datetime, timedelta -from unittest.mock import patch import crum import ddt import waffle # lint-amnesty, pylint: disable=invalid-django-waffle-import @@ -12,11 +11,11 @@ from django.contrib.messages.middleware import MessageMiddleware from django.test import RequestFactory from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag +from freezegun import freeze_time from pytz import utc 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.commerce.models import CommerceConfiguration from lms.djangoapps.course_home_api.toggles import COURSE_HOME_MICROFRONTEND, COURSE_HOME_MICROFRONTEND_DATES_TAB from lms.djangoapps.courseware.courses import get_course_date_blocks @@ -42,7 +41,6 @@ from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVer from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration -from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory # pylint: disable=unused-import from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from openedx.features.course_experience import ( @@ -114,7 +112,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): {'verification_status': 'expired'}, (TodaysDate, CourseEndDate, VerificationDeadlineDate)), # Verified enrollment with `approved` photo-verification during course run - ({'days_till_start': -10, }, + ({'days_till_start': -10}, {'verification_status': 'approved'}, (TodaysDate, CourseEndDate)), # Verified enrollment with *NO* course end date @@ -522,15 +520,16 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): user = create_user() CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) block = CourseEndDate(course, user) - assert block.description == 'To earn a certificate, you must complete all requirements before this date.' + assert block.description == ('This course will be archived, which means you can review the course content ' + 'but can no longer participate in graded assignments or earn a certificate.') def test_course_end_date_for_non_certificate_eligible_mode(self): course = create_course_run(days_till_start=-1) user = create_user() CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT) block = CourseEndDate(course, user) - assert block.description == 'After this date, course content will be archived.' - assert block.title == 'Course End' + assert block.description == 'After the course ends, the course content will be archived and no longer active.' + assert block.title == 'Course ends' def test_course_end_date_after_course(self): course = create_course_run(days_till_start=-2, days_till_end=-1) @@ -539,35 +538,26 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): block = CourseEndDate(course, user) assert block.description ==\ 'This course is archived, which means you can review course content but it is no longer active.' - assert block.title == 'Course End' + assert block.title == 'Course ends' - @ddt.data( - {'weeks_to_complete': 7}, # Weeks to complete > time til end (end date shown) - {'weeks_to_complete': 4}, # Weeks to complete < time til end (end date not shown) - ) + @ddt.data(300, 400) @override_waffle_flag(RELATIVE_DATES_FLAG, active=True) - def test_course_end_date_self_paced(self, cr_details): + def test_course_end_date_self_paced(self, days_till_end): """ - 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. + In self-paced courses, the end date will only show up if the learner + views the course within 365 days of the course end date. """ 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) + start=now + timedelta(days=-7), end=now + timedelta(days=days_till_end), self_paced=True) user = create_user() - self.make_request(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) - assert block.title == 'Course End' - if cr_details['weeks_to_complete'] > end_timedelta_number: - assert block.date == course.end - else: - assert block.date is None + block = CourseEndDate(course, user) + assert block.title == 'Course ends' + if 365 > days_till_end: + assert block.date == course.end + else: + assert block.date is None + assert block.description == '' def test_ecommerce_checkout_redirect(self): """Verify the block link redirects to ecommerce checkout if it's enabled.""" diff --git a/openedx/features/course_experience/tests/views/test_course_dates.py b/openedx/features/course_experience/tests/views/test_course_dates.py index af7867741c..3a5b794e33 100644 --- a/openedx/features/course_experience/tests/views/test_course_dates.py +++ b/openedx/features/course_experience/tests/views/test_course_dates.py @@ -41,7 +41,7 @@ class TestCourseDatesFragmentView(ModuleStoreTestCase): def test_course_dates_fragment(self): response = self.client.get(self.dates_fragment_url) - self.assertContains(response, 'Course End') + self.assertContains(response, 'Course ends') self.client.logout() response = self.client.get(self.dates_fragment_url)