Show discount deadline in a timezone-aware way
Also, fix it and the access expiration deadline to not hardcode the date presentation in an American way.
This commit is contained in:
@@ -6,9 +6,13 @@ Convenience methods for working with datetime objects
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.utils.translation import pgettext, ugettext
|
||||
import crum
|
||||
from django.utils.translation import get_language, pgettext, ugettext
|
||||
from pytz import UnknownTimeZoneError, timezone, utc
|
||||
|
||||
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
|
||||
|
||||
def get_default_time_display(dtime):
|
||||
"""
|
||||
@@ -212,6 +216,40 @@ def strftime_localized(dtime, format): # pylint: disable=redefined-builtin
|
||||
return formatted_date
|
||||
|
||||
|
||||
def strftime_localized_html(dtime, fmt):
|
||||
"""
|
||||
Returns an html string that can be further localized in browser by JS code
|
||||
|
||||
For example, a user's default timezone preference is "whatever the browser default is" which must be queried via
|
||||
Javascript. So we write out a UTC formatted date here, but tag it with enough information that dateutil_factory.js
|
||||
can then later update the DOM to use the proper formatting.
|
||||
|
||||
Arguments:
|
||||
dtime (datetime): Datetime to format
|
||||
fmt (str): One of the special enum values that strftime_localized accepts. Only SHORT_DATE supported now.
|
||||
"""
|
||||
locale_prefs = user_timezone_locale_prefs(crum.get_current_request())
|
||||
user_timezone = locale_prefs['user_timezone']
|
||||
language = get_language()
|
||||
|
||||
# Here we map the enums for strftime_localized into the enums used by date-utils.js when localizing on JS side.
|
||||
format_mapping = {
|
||||
'SHORT_DATE': 'shortDate',
|
||||
}
|
||||
assert fmt in format_mapping.keys(), 'format "%s" not yet supported in strftime_localized_html' % fmt
|
||||
|
||||
date_html = '<span class="localized-datetime" data-format="{format}" data-timezone="{user_timezone}" \
|
||||
data-datetime="{formatted_date}" data-language="{language}">{formatted_date_localized}</span>'
|
||||
|
||||
return HTML(date_html).format(
|
||||
language=language,
|
||||
user_timezone=user_timezone,
|
||||
format=format_mapping[fmt],
|
||||
formatted_date=dtime.isoformat(),
|
||||
formatted_date_localized=strftime_localized(dtime, fmt),
|
||||
)
|
||||
|
||||
|
||||
# In order to extract the strings below, we have to mark them with pgettext.
|
||||
# But we'll do the actual pgettext later, so use a no-op for now, and save the
|
||||
# real pgettext so we can assign it back to the global name later.
|
||||
|
||||
@@ -8,11 +8,13 @@ import unittest
|
||||
from datetime import datetime, timedelta, tzinfo
|
||||
|
||||
import ddt
|
||||
import six
|
||||
from markupsafe import Markup
|
||||
from mock import patch
|
||||
from pytz import utc
|
||||
|
||||
from common.djangoapps.util.date_utils import almost_same_datetime, get_default_time_display, get_time_display, strftime_localized
|
||||
from common.djangoapps.util.date_utils import (
|
||||
almost_same_datetime, get_default_time_display, get_time_display, strftime_localized, strftime_localized_html
|
||||
)
|
||||
|
||||
|
||||
def test_get_default_time_display():
|
||||
@@ -134,9 +136,7 @@ class StrftimeLocalizedTest(unittest.TestCase):
|
||||
(fmt, expected) = fmt_expected
|
||||
dtime = datetime(2013, 2, 14, 16, 41, 17)
|
||||
self.assertEqual(expected, strftime_localized(dtime, fmt))
|
||||
# strftime doesn't like Unicode, so do the work in UTF8.
|
||||
self.assertEqual(expected.encode('utf-8') if six.PY2 else expected,
|
||||
dtime.strftime(fmt.encode('utf-8') if six.PY2 else fmt))
|
||||
self.assertEqual(expected, dtime.strftime(fmt))
|
||||
|
||||
@ddt.data(
|
||||
("SHORT_DATE", "Feb 14, 2013"),
|
||||
@@ -211,3 +211,28 @@ class StrftimeLocalizedTest(unittest.TestCase):
|
||||
dtime = datetime(2013, 2, 14, 16, 41, 17)
|
||||
with self.assertRaises(ValueError):
|
||||
strftime_localized(dtime, fmt)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class StrftimeLocalizedHtmlTest(unittest.TestCase):
|
||||
"""
|
||||
Tests for strftime_localized_html.
|
||||
"""
|
||||
@ddt.data(
|
||||
None,
|
||||
'Africa/Casablanca',
|
||||
)
|
||||
def test_happy_path(self, timezone):
|
||||
dtime = datetime(2013, 2, 14, 16, 41, 17)
|
||||
with patch('common.djangoapps.util.date_utils.user_timezone_locale_prefs',
|
||||
return_value={'user_timezone': timezone}):
|
||||
html = strftime_localized_html(dtime, 'SHORT_DATE')
|
||||
self.assertIsInstance(html, Markup)
|
||||
self.assertRegex(html,
|
||||
'<span class="localized-datetime" data-format="shortDate" data-timezone="%s" ' % timezone +
|
||||
'\\s*data-datetime="2013-02-14T16:41:17" data-language="en">Feb 14, 2013</span>')
|
||||
|
||||
def test_invalid_format_string(self):
|
||||
dtime = datetime(2013, 2, 14, 16, 41, 17)
|
||||
with self.assertRaisesRegex(AssertionError, 'format "NOPE" not yet supported in strftime_localized_html'):
|
||||
strftime_localized_html(dtime, 'NOPE')
|
||||
|
||||
@@ -27,10 +27,9 @@ from lms.djangoapps.courseware.masquerade import setup_masquerade
|
||||
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.lib.url_utils import quote_slashes
|
||||
from openedx.features.course_duration_limits.access import EXPIRATION_DATE_FORMAT_STR
|
||||
from common.djangoapps.student.models import CourseEnrollment, Registration
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from common.djangoapps.util.date_utils import strftime_localized
|
||||
from common.djangoapps.util.date_utils import strftime_localized_html
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -423,19 +422,9 @@ def get_expiration_banner_text(user, course, language='en'):
|
||||
if upgrade_deadline is None or now() < upgrade_deadline:
|
||||
upgrade_deadline = enrollment.course_upgrade_deadline
|
||||
|
||||
date_string = u'<span class="localized-datetime" data-format="shortDate" data-timezone="None" \
|
||||
data-datetime="{formatted_date}" data-language="{language}">{formatted_date_localized}</span>'
|
||||
formatted_expiration_date = date_string.format(
|
||||
language=language,
|
||||
formatted_date=expiration_date.isoformat(),
|
||||
formatted_date_localized=strftime_localized(expiration_date, EXPIRATION_DATE_FORMAT_STR)
|
||||
)
|
||||
formatted_expiration_date = strftime_localized_html(expiration_date, 'SHORT_DATE')
|
||||
if upgrade_deadline:
|
||||
formatted_upgrade_deadline = date_string.format(
|
||||
language=language,
|
||||
formatted_date=upgrade_deadline.isoformat(),
|
||||
formatted_date_localized=strftime_localized(upgrade_deadline, EXPIRATION_DATE_FORMAT_STR)
|
||||
)
|
||||
formatted_upgrade_deadline = strftime_localized_html(upgrade_deadline, 'SHORT_DATE')
|
||||
|
||||
bannerText = u'<strong>Audit Access Expires {expiration_date}</strong><br>\
|
||||
You lose all access to this course, including your progress, on {expiration_date}.\
|
||||
|
||||
@@ -4,19 +4,16 @@ Contains code related to computing content gating course duration limits
|
||||
and course access based on these limits.
|
||||
"""
|
||||
|
||||
import crum
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import get_language
|
||||
from django.utils.translation import ugettext as _
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.util.date_utils import strftime_localized
|
||||
from common.djangoapps.util.date_utils import strftime_localized, strftime_localized_html
|
||||
from lms.djangoapps.courseware.access_response import AccessError
|
||||
from lms.djangoapps.courseware.access_utils import ACCESS_GRANTED
|
||||
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
|
||||
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
|
||||
from lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_specific_student
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
@@ -24,8 +21,6 @@ from openedx.core.djangoapps.course_date_signals.utils import get_expected_durat
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
|
||||
EXPIRATION_DATE_FORMAT_STR = '%b %-d, %Y'
|
||||
|
||||
|
||||
class AuditExpiredError(AccessError):
|
||||
"""
|
||||
@@ -34,7 +29,7 @@ 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)
|
||||
expiration_date = strftime_localized(expiration_date, EXPIRATION_DATE_FORMAT_STR)
|
||||
expiration_date = strftime_localized(expiration_date, 'SHORT_DATE')
|
||||
user_message = _('Access expired on {expiration_date}').format(expiration_date=expiration_date)
|
||||
try:
|
||||
course_name = course.display_name_with_default
|
||||
@@ -113,12 +108,6 @@ 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 '<span class="localized-datetime" data-format="shortDate" data-timezone="{user_timezone}" \
|
||||
data-datetime="{formatted_date}" data-language="{language}">{formatted_date_localized}</span>'
|
||||
|
||||
|
||||
def get_access_expiration_data(user, course):
|
||||
"""
|
||||
Create a dictionary of information about the access expiration for this user & course.
|
||||
@@ -165,14 +154,11 @@ def generate_course_expired_message(user, course):
|
||||
upgrade_deadline = expiration_data['upgrade_deadline']
|
||||
upgrade_url = expiration_data['upgrade_url']
|
||||
|
||||
user_timezone_locale = user_timezone_locale_prefs(crum.get_current_request())
|
||||
user_timezone = user_timezone_locale['user_timezone']
|
||||
|
||||
if masquerading_expired_course:
|
||||
upgrade_message = _('This learner does not have access to this course. '
|
||||
'Their access expired on {expiration_date}.')
|
||||
return HTML(upgrade_message).format(
|
||||
expiration_date=strftime_localized(expiration_date, EXPIRATION_DATE_FORMAT_STR)
|
||||
expiration_date=strftime_localized_html(expiration_date, 'SHORT_DATE')
|
||||
)
|
||||
else:
|
||||
expiration_message = _('{strong_open}Audit Access Expires {expiration_date}{strong_close}'
|
||||
@@ -188,21 +174,9 @@ def generate_course_expired_message(user, course):
|
||||
else:
|
||||
using_upgrade_messaging = False
|
||||
|
||||
language = get_language()
|
||||
date_string = get_date_string()
|
||||
formatted_expiration_date = date_string.format(
|
||||
language=language,
|
||||
user_timezone=user_timezone,
|
||||
formatted_date=expiration_date.isoformat(),
|
||||
formatted_date_localized=strftime_localized(expiration_date, EXPIRATION_DATE_FORMAT_STR)
|
||||
)
|
||||
formatted_expiration_date = strftime_localized_html(expiration_date, 'SHORT_DATE')
|
||||
if using_upgrade_messaging:
|
||||
formatted_upgrade_deadline = date_string.format(
|
||||
language=language,
|
||||
user_timezone=user_timezone,
|
||||
formatted_date=upgrade_deadline.isoformat(),
|
||||
formatted_date_localized=strftime_localized(upgrade_deadline, EXPIRATION_DATE_FORMAT_STR)
|
||||
)
|
||||
formatted_upgrade_deadline = strftime_localized_html(upgrade_deadline, 'SHORT_DATE')
|
||||
|
||||
return HTML(full_message).format(
|
||||
a_open=HTML('<a id="FBE_banner" href="{upgrade_link}">').format(upgrade_link=upgrade_url),
|
||||
|
||||
@@ -39,7 +39,7 @@ class TestAccess(CacheIsolationTestCase):
|
||||
|
||||
def assertDateInMessage(self, date, message):
|
||||
# First, check that the formatted version is in there
|
||||
self.assertIn(strftime_localized(date, '%b %-d, %Y'), message)
|
||||
self.assertIn(strftime_localized(date, 'SHORT_DATE'), message)
|
||||
|
||||
# But also that the machine-readable version is in there
|
||||
self.assertIn('data-datetime="%s"' % date.isoformat(), message)
|
||||
|
||||
@@ -21,6 +21,7 @@ from waffle.testutils import override_flag
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from common.djangoapps.util.date_utils import strftime_localized_html
|
||||
from lms.djangoapps.experiments.models import ExperimentData
|
||||
from lms.djangoapps.commerce.models import CommerceConfiguration
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
@@ -37,7 +38,6 @@ from lms.djangoapps.courseware.tests.helpers import get_expiration_banner_text
|
||||
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
|
||||
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.dark_lang.models import DarkLangConfig
|
||||
from openedx.core.djangoapps.django_comment_common.models import (
|
||||
FORUM_ROLE_ADMINISTRATOR,
|
||||
FORUM_ROLE_COMMUNITY_TA,
|
||||
@@ -414,19 +414,19 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
user = self.create_user_for_course(self.course, CourseUserType.ENROLLED)
|
||||
now_time = datetime.now(tz=UTC).strftime(u"%Y-%m-%d %H:%M:%S%z")
|
||||
ExperimentData.objects.create(
|
||||
user=user, experiment_id=REV1008_EXPERIMENT_ID, key=str(self.course), value=now_time
|
||||
user=user, experiment_id=REV1008_EXPERIMENT_ID, key=str(self.course.id), value=now_time
|
||||
)
|
||||
self.client.login(username=user.username, password=self.TEST_PASSWORD)
|
||||
url = course_home_url(self.course)
|
||||
response = self.client.get(url)
|
||||
discount_expiration_date = get_discount_expiration_date(user, self.course).strftime(u'%B %d')
|
||||
expiration_date = strftime_localized_html(get_discount_expiration_date(user, self.course), 'SHORT_DATE')
|
||||
upgrade_link = verified_upgrade_deadline_link(user=user, course=self.course)
|
||||
bannerText = u'''<div class="first-purchase-offer-banner" role="note">
|
||||
<span class="first-purchase-offer-banner-bold"><b>
|
||||
Upgrade by {discount_expiration_date} and save {percentage}% [{strikeout_price}]</b></span>
|
||||
<br>Use code <b>EDXWELCOME</b> at checkout! <a id="welcome" href="{upgrade_link}">Upgrade Now</a>
|
||||
</div>'''.format(
|
||||
discount_expiration_date=discount_expiration_date,
|
||||
discount_expiration_date=expiration_date,
|
||||
percentage=percentage,
|
||||
strikeout_price=HTML(format_strikeout_price(user, self.course)[0]),
|
||||
upgrade_link=upgrade_link
|
||||
@@ -577,7 +577,7 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
expiration_date = strftime_localized(course.start + timedelta(weeks=4) + timedelta(days=1), u'%b %-d, %Y')
|
||||
expiration_date = strftime_localized(course.start + timedelta(weeks=4) + timedelta(days=1), 'SHORT_DATE')
|
||||
expected_params = QueryDict(mutable=True)
|
||||
course_name = CourseOverview.get_from_id(course.id).display_name_with_default
|
||||
expected_params['access_response_error'] = u'Access to {run} expired on {expiration_date}'.format(
|
||||
@@ -799,46 +799,6 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
bannerText = get_expiration_banner_text(self.staff_user, self.course)
|
||||
self.assertNotContains(response, bannerText, html=True)
|
||||
|
||||
@mock.patch("common.djangoapps.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'<span class="localized-datetime" data-format="shortDate" \
|
||||
data-datetime="{formatted_date}" data-language="{language}">{formatted_date_localized}</span>'
|
||||
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, tzinfo=UTC)
|
||||
)
|
||||
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 = 'eo'
|
||||
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=date_string.encode('utf-8')):
|
||||
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):
|
||||
|
||||
@@ -64,7 +64,7 @@ def get_discount_expiration_date(user, course):
|
||||
|
||||
time_limit_start = None
|
||||
try:
|
||||
saw_banner = ExperimentData.objects.get(user=user, experiment_id=REV1008_EXPERIMENT_ID, key=str(course))
|
||||
saw_banner = ExperimentData.objects.get(user=user, experiment_id=REV1008_EXPERIMENT_ID, key=str(course.id))
|
||||
time_limit_start = parse_datetime(saw_banner.value)
|
||||
except ExperimentData.DoesNotExist:
|
||||
return None
|
||||
|
||||
@@ -42,7 +42,7 @@ class TestApplicability(ModuleStoreTestCase):
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
|
||||
now_time = datetime.now(tz=pytz.UTC).strftime(u"%Y-%m-%d %H:%M:%S%z")
|
||||
ExperimentData.objects.create(
|
||||
user=self.user, experiment_id=REV1008_EXPERIMENT_ID, key=str(self.course), value=now_time
|
||||
user=self.user, experiment_id=REV1008_EXPERIMENT_ID, key=str(self.course.id), value=now_time
|
||||
)
|
||||
|
||||
holdback_patcher = patch(
|
||||
|
||||
@@ -12,6 +12,7 @@ from edx_django_utils.cache import RequestCache
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from common.djangoapps.course_modes.models import format_course_price, get_course_prices
|
||||
from common.djangoapps.util.date_utils import strftime_localized_html
|
||||
from lms.djangoapps.experiments.models import ExperimentData
|
||||
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
@@ -184,7 +185,7 @@ def generate_offer_html(user, course):
|
||||
'<div class="first-purchase-offer-banner" role="note">'
|
||||
'<span class="first-purchase-offer-banner-bold"><b>'
|
||||
),
|
||||
discount_expiration_date=data['expiration_date'].strftime('%B %d'),
|
||||
discount_expiration_date=strftime_localized_html(data['expiration_date'], 'SHORT_DATE'),
|
||||
percentage=data['percentage'],
|
||||
span_close=HTML('</b></span>'),
|
||||
div_close=HTML('</div>'),
|
||||
|
||||
Reference in New Issue
Block a user