diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index e8472a01a5..3c94fbd65a 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -172,13 +172,13 @@ class LoginEnrollmentTestCase(TestCase): self.user = self.activate_user(self.email) self.login(self.email, self.password) - def assert_request_status_code(self, status_code, url, method="GET", **kwargs): + def assert_request_status_code(self, status_code, url, method="GET", **kwargs): # pylint: disable=unicode-format-string make_request = getattr(self.client, method.lower()) response = make_request(url, **kwargs) self.assertEqual( response.status_code, status_code, - "{method} request to {url} returned status code {actual}, " - "expected status code {expected}".format( + u"{method} request to {url} returned status code {actual}, " + u"expected status code {expected}".format( method=method, url=url, actual=response.status_code, expected=status_code ) @@ -355,7 +355,7 @@ def _create_mock_json_request(user, data, method='POST'): return request -def get_expiration_banner_text(user, course, language='en-us'): +def get_expiration_banner_text(user, course, language='en'): """ Get text for banner that messages user course expiration date for different tests that depend on it. @@ -367,19 +367,21 @@ def get_expiration_banner_text(user, course, language='en-us'): if upgrade_deadline is None or now() < upgrade_deadline: upgrade_deadline = enrollment.course_upgrade_deadline - language_is_es = language and language.split('-')[0].lower() == 'es' - if language_is_es: - formatted_expiration_date = strftime_localized(expiration_date, '%-d de %b. de %Y').lower() - else: - formatted_expiration_date = strftime_localized(expiration_date, '%b. %-d, %Y') - + date_string = u'{formatted_date_localized}' + formatted_expiration_date = date_string.format( + language=language, + formatted_date=expiration_date.strftime(u'%b %-d, %Y'), + formatted_date_localized=strftime_localized(expiration_date, u'%b %-d, %Y') + ) if upgrade_deadline: - if language_is_es: - formatted_upgrade_deadline = strftime_localized(upgrade_deadline, '%-d de %b. de %Y').lower() - else: - formatted_upgrade_deadline = strftime_localized(upgrade_deadline, '%b. %-d, %Y') + formatted_upgrade_deadline = date_string.format( + language=language, + formatted_date=upgrade_deadline.strftime(u'%b %-d, %Y'), + formatted_date_localized=strftime_localized(upgrade_deadline, u'%b %-d, %Y') + ) - bannerText = 'Audit Access Expires {expiration_date}
\ + bannerText = u'Audit Access Expires {expiration_date}
\ You lose all access to this course, including your progress, on {expiration_date}.\
Upgrade by {upgrade_deadline} to get unlimited access to the course as long as it exists\ on the site. Upgrade now to retain access past\ @@ -389,7 +391,7 @@ def get_expiration_banner_text(user, course, language='en-us'): upgrade_deadline=formatted_upgrade_deadline ) else: - bannerText = 'Audit Access Expires {expiration_date}
\ + bannerText = u'Audit Access Expires {expiration_date}
\ You lose all access to this course, including your progress, on {expiration_date}.\ '.format( expiration_date=formatted_expiration_date diff --git a/openedx/features/course_duration_limits/access.py b/openedx/features/course_duration_limits/access.py index a024475a23..46edf3b7bd 100644 --- a/openedx/features/course_duration_limits/access.py +++ b/openedx/features/course_duration_limits/access.py @@ -32,22 +32,19 @@ class AuditExpiredError(AccessError): """ def __init__(self, user, course, expiration_date): error_code = "audit_expired" - developer_message = "User {} had access to {} until {}".format(user, course, expiration_date) + developer_message = u"User {} had access to {} until {}".format(user, course, expiration_date) language = get_language() - if language and language.split('-')[0].lower() == 'es': - expiration_date = strftime_localized(expiration_date, '%-d de %b. de %Y').lower() - else: - expiration_date = strftime_localized(expiration_date, '%b. %-d, %Y') - user_message = _("Access expired on {expiration_date}").format(expiration_date=expiration_date) + expiration_date = strftime_localized(expiration_date, u'%b. %-d, %Y') + user_message = _(u"Access expired on {expiration_date}").format(expiration_date=expiration_date) try: course_name = CourseOverview.get_from_id(course.id).display_name_with_default - additional_context_user_message = _("Access to {course_name} expired on {expiration_date}").format( + additional_context_user_message = _(u"Access to {course_name} expired on {expiration_date}").format( course_name=course_name, expiration_date=expiration_date ) except CourseOverview.DoesNotExist: - additional_context_user_message = _("Access to the course you were looking" - " for expired on {expiration_date}").format( + additional_context_user_message = _(u"Access to the course you were looking" + u" for expired on {expiration_date}").format( expiration_date=expiration_date ) super(AuditExpiredError, self).__init__(error_code, developer_message, user_message, @@ -63,7 +60,6 @@ def get_user_course_expiration_date(user, course): - Course access duration is bounded by the min and max duration. - If course fields are missing, default course access duration to MIN_DURATION. """ - access_duration = MIN_DURATION if not CourseMode.verified_mode_for_course(course.id, include_expired=True): @@ -112,6 +108,12 @@ def check_course_expired(user, course): return ACCESS_GRANTED +def get_date_string(): + # Creating this method to allow unit testing an issue where this string was missing the unicode prefix + return u'{formatted_date_localized}' + + def generate_course_expired_message(user, course): """ Generate the message for the user course expiration date if it exists. @@ -125,9 +127,9 @@ def generate_course_expired_message(user, course): if is_masquerading_as_specific_student(user, course.id) and timezone.now() > expiration_date: upgrade_message = _('This learner does not have access to this course. ' - 'Their access expired on {expiration_date}.') + u'Their access expired on {expiration_date}.') return HTML(upgrade_message).format( - expiration_date=strftime_localized(expiration_date, '%b. %-d, %Y') + expiration_date=strftime_localized(expiration_date, u'%b. %-d, %Y') ) else: enrollment = CourseEnrollment.get_enrollment(user, course.id) @@ -140,12 +142,12 @@ def generate_course_expired_message(user, course): if (not upgrade_deadline) or (upgrade_deadline < now): upgrade_deadline = course_upgrade_deadline - expiration_message = _('{strong_open}Audit Access Expires {expiration_date}{strong_close}' - '{line_break}You lose all access to this course, including your progress, on ' - '{expiration_date}.') - upgrade_deadline_message = _('{line_break}Upgrade by {upgrade_deadline} to get unlimited access to the course ' - 'as long as it exists on the site. {a_open}Upgrade now{sronly_span_open} to ' - 'retain access past {expiration_date}{span_close}{a_close}') + expiration_message = _(u'{strong_open}Audit Access Expires {expiration_date}{strong_close}' + u'{line_break}You lose all access to this course, including your progress, on ' + u'{expiration_date}.') + upgrade_deadline_message = _(u'{line_break}Upgrade by {upgrade_deadline} to get unlimited access to the course ' + u'as long as it exists on the site. {a_open}Upgrade now{sronly_span_open} to ' + u'retain access past {expiration_date}{span_close}{a_close}') full_message = expiration_message if upgrade_deadline and now < upgrade_deadline: full_message += upgrade_deadline_message @@ -154,36 +156,37 @@ def generate_course_expired_message(user, course): using_upgrade_messaging = False language = get_language() - language_is_es = language and language.split('-')[0].lower() == 'es' - if language_is_es: - formatted_expiration_date = strftime_localized(expiration_date, '%-d de %b. de %Y').lower() - else: - formatted_expiration_date = strftime_localized(expiration_date, '%b. %-d, %Y') - + date_string = get_date_string() + formatted_expiration_date = date_string.format( + language=language, + formatted_date=expiration_date.strftime(u'%b %-d, %Y'), + formatted_date_localized=strftime_localized(expiration_date, u'%b %-d, %Y') + ) if using_upgrade_messaging: - if language_is_es: - formatted_upgrade_deadline = strftime_localized(upgrade_deadline, '%-d de %b. de %Y').lower() - else: - formatted_upgrade_deadline = strftime_localized(upgrade_deadline, '%b. %-d, %Y') + formatted_upgrade_deadline = date_string.format( + language=language, + formatted_date=upgrade_deadline.strftime(u'%b %-d, %Y'), + formatted_date_localized=strftime_localized(upgrade_deadline, u'%b %-d, %Y') + ) return HTML(full_message).format( - a_open=HTML('
').format( + a_open=HTML(u'').format( upgrade_link=verified_upgrade_deadline_link(user=user, course=course) ), sronly_span_open=HTML(''), span_close=HTML(''), a_close=HTML(''), - expiration_date=formatted_expiration_date, + expiration_date=HTML(formatted_expiration_date), strong_open=HTML(''), strong_close=HTML(''), line_break=HTML('
'), - upgrade_deadline=formatted_upgrade_deadline + upgrade_deadline=HTML(formatted_upgrade_deadline) ) else: return HTML(full_message).format( span_close=HTML(''), - expiration_date=formatted_expiration_date, + expiration_date=HTML(formatted_expiration_date), strong_open=HTML(''), strong_close=HTML(''), line_break=HTML('
'), diff --git a/openedx/features/course_duration_limits/tests/test_access.py b/openedx/features/course_duration_limits/tests/test_access.py index 5ae99f30d4..b248f71dd9 100644 --- a/openedx/features/course_duration_limits/tests/test_access.py +++ b/openedx/features/course_duration_limits/tests/test_access.py @@ -5,10 +5,8 @@ import itertools from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from django.test import RequestFactory from django.utils import timezone from courseware.models import DynamicUpgradeDeadlineConfiguration -from mock import patch from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from openedx.features.course_duration_limits.access import ( @@ -33,12 +31,11 @@ class TestAccess(CacheIsolationTestCase): @ddt.data( *itertools.product( - ['en-us', 'es-419'], itertools.product([None, -2, -1, 1, 2], repeat=2), ) ) @ddt.unpack - def test_generate_course_expired_message(self, language, offsets): + def test_generate_course_expired_message(self, offsets): now = timezone.now() schedule_offset, course_offset = offsets @@ -53,45 +50,40 @@ class TestAccess(CacheIsolationTestCase): course_upgrade_deadline = None def format_date(date): - if language.startswith('es-'): - return strftime_localized(date, '%-d de %b. de %Y').lower() - else: - return strftime_localized(date, '%b. %-d, %Y') + return strftime_localized(date, u'%b %-d, %Y') - patch_lang = patch('openedx.features.course_duration_limits.access.get_language', return_value=language) - with patch_lang: - enrollment = CourseEnrollmentFactory.create( - course__start=datetime(2018, 1, 1, tzinfo=UTC), - course__self_paced=True, - ) - CourseModeFactory.create( - course_id=enrollment.course.id, - mode_slug=CourseMode.VERIFIED, - expiration_datetime=course_upgrade_deadline, - ) - CourseModeFactory.create( - course_id=enrollment.course.id, - mode_slug=CourseMode.AUDIT, - ) - ScheduleFactory.create( - enrollment=enrollment, - upgrade_deadline=schedule_upgrade_deadline, - ) + enrollment = CourseEnrollmentFactory.create( + course__start=datetime(2018, 1, 1, tzinfo=UTC), + course__self_paced=True, + ) + CourseModeFactory.create( + course_id=enrollment.course.id, + mode_slug=CourseMode.VERIFIED, + expiration_datetime=course_upgrade_deadline, + ) + CourseModeFactory.create( + course_id=enrollment.course.id, + mode_slug=CourseMode.AUDIT, + ) + ScheduleFactory.create( + enrollment=enrollment, + upgrade_deadline=schedule_upgrade_deadline, + ) - duration_limit_upgrade_deadline = get_user_course_expiration_date(enrollment.user, enrollment.course) - self.assertIsNotNone(duration_limit_upgrade_deadline) + duration_limit_upgrade_deadline = get_user_course_expiration_date(enrollment.user, enrollment.course) + self.assertIsNotNone(duration_limit_upgrade_deadline) - message = generate_course_expired_message(enrollment.user, enrollment.course) + message = generate_course_expired_message(enrollment.user, enrollment.course) - self.assertIn(format_date(duration_limit_upgrade_deadline), message) + self.assertIn(format_date(duration_limit_upgrade_deadline), message) - soft_upgradeable = schedule_upgrade_deadline is not None and now < schedule_upgrade_deadline - upgradeable = course_upgrade_deadline is None or now < course_upgrade_deadline - has_upgrade_deadline = course_upgrade_deadline is not None + soft_upgradeable = schedule_upgrade_deadline is not None and now < schedule_upgrade_deadline + upgradeable = course_upgrade_deadline is None or now < course_upgrade_deadline + has_upgrade_deadline = course_upgrade_deadline is not None - if upgradeable and soft_upgradeable: - self.assertIn(format_date(schedule_upgrade_deadline), message) - elif upgradeable and has_upgrade_deadline: - self.assertIn(format_date(course_upgrade_deadline), message) - else: - self.assertNotIn("Upgrade by", message) + if upgradeable and soft_upgradeable: + self.assertIn(format_date(schedule_upgrade_deadline), message) + elif upgradeable and has_upgrade_deadline: + self.assertIn(format_date(course_upgrade_deadline), message) + else: + self.assertNotIn("Upgrade by", message) diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index 048ebcf8c4..bd4ab1884d 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -38,6 +38,7 @@ from lms.djangoapps.courseware.tests.factories import ( GlobalStaffFactory, ) from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.djangoapps.dark_lang.models import DarkLangConfig from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag from openedx.features.course_duration_limits.config import EXPERIMENT_ID @@ -525,10 +526,10 @@ class TestCourseHomePageAccess(CourseHomePageTestCase): response = self.client.get(url) - expiration_date = strftime_localized(course.start + timedelta(weeks=4), '%b. %-d, %Y') + expiration_date = strftime_localized(course.start + timedelta(weeks=4), u'%b. %-d, %Y') expected_params = QueryDict(mutable=True) course_name = CourseOverview.get_from_id(course.id).display_name_with_default - expected_params['access_response_error'] = 'Access to {run} expired on {expiration_date}'.format( + expected_params['access_response_error'] = u'Access to {run} expired on {expiration_date}'.format( run=course_name, expiration_date=expiration_date ) @@ -722,6 +723,46 @@ class TestCourseHomePageAccess(CourseHomePageTestCase): bannerText = get_expiration_banner_text(self.staff_user, self.course) self.assertNotContains(response, bannerText, html=True) + @mock.patch("util.date_utils.strftime_localized") + @mock.patch("openedx.features.course_duration_limits.access.get_date_string") + def test_course_expiration_banner_with_unicode(self, mock_strftime_localized, mock_get_date_string): + """ + Ensure that switching to other languages that have unicode in their + date representations will not cause the course home page to 404. + """ + fake_unicode_start_time = u"üñîçø∂é_ßtå®t_tîµé" + mock_strftime_localized.return_value = fake_unicode_start_time + date_string = u'{formatted_date_localized}' + mock_get_date_string.return_value = date_string + + config = CourseDurationLimitConfig( + course=CourseOverview.get_from_id(self.course.id), + enabled=True, + enabled_as_of=datetime(2018, 1, 1) + ) + config.save() + url = course_home_url(self.course) + user = self.create_user_for_course(self.course, CourseUserType.UNENROLLED) + CourseEnrollment.enroll(user, self.course.id) + + language = 'zh-cn' + DarkLangConfig( + released_languages=language, + changed_by=user, + enabled=True + ).save() + + response = self.client.get(url, HTTP_ACCEPT_LANGUAGE=language) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Language'], language) + + # Check that if the string is incorrectly not marked as unicode we still get the error + with mock.patch("openedx.features.course_duration_limits.access.get_date_string", + return_value=str(date_string)): + response = self.client.get(url, HTTP_ACCEPT_LANGUAGE=language) + self.assertEqual(response.status_code, 500) + @override_waffle_flag(COURSE_PRE_START_ACCESS_FLAG, active=True) @override_waffle_flag(ENABLE_COURSE_GOALS, active=True) def test_course_goals(self): @@ -846,7 +887,10 @@ class CourseHomeFragmentViewTests(ModuleStoreTestCase): url = EcommerceService().get_checkout_page_url(self.verified_mode.sku) self.assertIn('