Revert "Merge pull request #14078 from edx/yro_remove-datetimetext-functions"

This reverts commit 8c0098812d, reversing
changes made to 5b6e2dd5ee.
This commit is contained in:
Calen Pennington
2016-12-22 09:29:33 -05:00
parent bd87061f1c
commit 47e21ca5b0
31 changed files with 524 additions and 143 deletions

View File

@@ -12,6 +12,7 @@ from pytz import utc
from lazy import lazy
from ccx_keys.locator import CCXLocator
from openedx.core.lib.time_zone_utils import get_time_zone_abbr
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, LocationKeyField
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
@@ -83,6 +84,36 @@ class CustomCourseForEdX(models.Model):
return datetime.now(utc) > self.due
def start_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""Returns the desired text representation of the CCX start datetime
The returned value is in specified time zone, defaulted to UTC.
"""
i18n = self.course.runtime.service(self.course, "i18n")
strftime = i18n.strftime
value = strftime(self.start.astimezone(time_zone), format_string)
if format_string == 'DATE_TIME':
value += ' ' + get_time_zone_abbr(time_zone, self.start)
return value
def end_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""Returns the desired text representation of the CCX due datetime
If the due date for the CCX is not set, the value returned is the empty
string.
The returned value is in specified time zone, defaulted to UTC.
"""
if self.due is None:
return ''
i18n = self.course.runtime.service(self.course, "i18n")
strftime = i18n.strftime
value = strftime(self.due.astimezone(time_zone), format_string)
if format_string == 'DATE_TIME':
value += ' ' + get_time_zone_abbr(time_zone, self.due)
return value
@property
def structure(self):
"""

View File

@@ -4,12 +4,14 @@ tests for the models
import ddt
import json
from datetime import datetime, timedelta
from mock import patch
from nose.plugins.attrib import attr
from pytz import utc
from pytz import timezone, utc
from student.roles import CourseCcxCoachRole
from student.tests.factories import (
AdminFactory,
)
from util.tests.test_date_utils import fake_ugettext
from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase,
TEST_DATA_SPLIT_MODULESTORE
@@ -150,6 +152,95 @@ class TestCCX(ModuleStoreTestCase):
"""verify that a ccx without a due date has not ended"""
self.assertFalse(self.ccx.has_ended()) # pylint: disable=no-member
# ensure that the expected localized format will be found by the i18n
# service
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"SHORT_DATE_FORMAT": "%b %d, %Y",
}))
def test_start_datetime_short_date(self):
"""verify that the start date for a ccx formats properly by default"""
start = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015"
self.set_ccx_override('start', start)
actual = self.ccx.start_datetime_text() # pylint: disable=no-member
self.assertEqual(expected, actual)
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"DATE_TIME_FORMAT": "%b %d, %Y at %H:%M",
}))
def test_start_datetime_date_time_format(self):
"""verify that the DATE_TIME format also works as expected"""
start = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015 at 12:00 UTC"
self.set_ccx_override('start', start)
actual = self.ccx.start_datetime_text('DATE_TIME') # pylint: disable=no-member
self.assertEqual(expected, actual)
@ddt.data((datetime(2015, 11, 1, 8, 59, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:59 PDT"),
(datetime(2015, 11, 1, 9, 00, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:00 PST"))
@ddt.unpack
def test_start_date_time_zone(self, start_date_time, expected_short_date, expected_date_time):
"""
verify that start date is correctly converted when time zone specified
during normal daylight hours and daylight savings hours
"""
time_zone = timezone('America/Los_Angeles')
self.set_ccx_override('start', start_date_time)
actual_short_date = self.ccx.start_datetime_text(time_zone=time_zone) # pylint: disable=no-member
actual_datetime = self.ccx.start_datetime_text('DATE_TIME', time_zone) # pylint: disable=no-member
self.assertEqual(expected_short_date, actual_short_date)
self.assertEqual(expected_date_time, actual_datetime)
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"SHORT_DATE_FORMAT": "%b %d, %Y",
}))
def test_end_datetime_short_date(self):
"""verify that the end date for a ccx formats properly by default"""
end = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015"
self.set_ccx_override('due', end)
actual = self.ccx.end_datetime_text() # pylint: disable=no-member
self.assertEqual(expected, actual)
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"DATE_TIME_FORMAT": "%b %d, %Y at %H:%M",
}))
def test_end_datetime_date_time_format(self):
"""verify that the DATE_TIME format also works as expected"""
end = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015 at 12:00 UTC"
self.set_ccx_override('due', end)
actual = self.ccx.end_datetime_text('DATE_TIME') # pylint: disable=no-member
self.assertEqual(expected, actual)
@ddt.data((datetime(2015, 11, 1, 8, 59, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:59 PDT"),
(datetime(2015, 11, 1, 9, 00, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:00 PST"))
@ddt.unpack
def test_end_datetime_time_zone(self, end_date_time, expected_short_date, expected_date_time):
"""
verify that end date is correctly converted when time zone specified
during normal daylight hours and daylight savings hours
"""
time_zone = timezone('America/Los_Angeles')
self.set_ccx_override('due', end_date_time)
actual_short_date = self.ccx.end_datetime_text(time_zone=time_zone) # pylint: disable=no-member
actual_datetime = self.ccx.end_datetime_text('DATE_TIME', time_zone) # pylint: disable=no-member
self.assertEqual(expected_short_date, actual_short_date)
self.assertEqual(expected_date_time, actual_datetime)
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"DATE_TIME_FORMAT": "%b %d, %Y at %H:%M",
}))
def test_end_datetime_no_due_date(self):
"""verify that without a due date, the end date is an empty string"""
expected = ''
actual = self.ccx.end_datetime_text() # pylint: disable=no-member
self.assertEqual(expected, actual)
actual = self.ccx.end_datetime_text('DATE_TIME') # pylint: disable=no-member
self.assertEqual(expected, actual)
def test_ccx_max_student_enrollment_correct(self):
"""
Verify the override value for max_student_enrollments_allowed

View File

@@ -12,7 +12,6 @@ from openedx.core.djangoapps.catalog.utils import get_programs as get_catalog_pr
from openedx.core.djangoapps.credentials.utils import get_programs_credentials
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs import utils
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences
@login_required
@@ -76,8 +75,7 @@ def program_details(request, program_id):
'show_program_listing': programs_config.show_program_listing,
'nav_hidden': True,
'disable_courseware_js': True,
'uses_pattern_library': True,
'user_preferences': get_user_preferences(request.user)
'uses_pattern_library': True
}
return render_to_response('learner_dashboard/program_details.html', context)

View File

@@ -332,7 +332,7 @@ class Order(models.Model):
"""
this function generates the csv file
"""
course_names = []
course_info = []
csv_file = StringIO.StringIO()
csv_writer = csv.writer(csv_file)
csv_writer.writerow(['Course Name', 'Registration Code', 'URL'])
@@ -340,15 +340,15 @@ class Order(models.Model):
course_id = item.course_id
course = get_course_by_id(item.course_id, depth=0)
registration_codes = CourseRegistrationCode.objects.filter(course_id=course_id, order=self)
course_names.append(course.display_name)
course_info.append((course.display_name, ' (' + course.start_datetime_text() + '-' + course.end_datetime_text() + ')'))
for registration_code in registration_codes:
redemption_url = reverse('register_code_redemption', args=[registration_code.code])
url = '{base_url}{redemption_url}'.format(base_url=site_name, redemption_url=redemption_url)
csv_writer.writerow([unicode(course.display_name).encode("utf-8"), registration_code.code, url])
return csv_file, course_names
return csv_file, course_info
def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, pdf_file, site_name, course_names):
def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, pdf_file, site_name, courses_info):
"""
send confirmation e-mail
"""
@@ -358,7 +358,8 @@ class Order(models.Model):
joined_course_names = ""
if self.recipient_email:
recipient_list.append((self.recipient_name, self.recipient_email, 'email_recipient'))
joined_course_names = " " + ", ".join(course_names)
courses_names_with_dates = [course_info[0] + course_info[1] for course_info in courses_info]
joined_course_names = " " + ", ".join(courses_names_with_dates)
if not is_order_type_business:
subject = _("Order Payment Confirmation")
@@ -386,7 +387,7 @@ class Order(models.Model):
'recipient_type': recipient[2],
'site_name': site_name,
'order_items': orderitems,
'course_names': ", ".join(course_names),
'course_names': ", ".join([course_info[0] for course_info in courses_info]),
'dashboard_url': dashboard_url,
'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1],
'order_placed_by': '{username} ({email})'.format(
@@ -476,13 +477,13 @@ class Order(models.Model):
item.purchase_item()
csv_file = None
course_names = []
courses_info = []
if self.order_type == OrderTypes.BUSINESS:
#
# Generate the CSV file that contains all of the RegistrationCodes that have already been
# generated when the purchase has transacted
#
csv_file, course_names = self.generate_registration_codes_csv(orderitems, site_name)
csv_file, courses_info = self.generate_registration_codes_csv(orderitems, site_name)
try:
pdf_file = self.generate_pdf_receipt(orderitems)
@@ -493,7 +494,7 @@ class Order(models.Model):
try:
self.send_confirmation_emails(
orderitems, self.order_type == OrderTypes.BUSINESS,
csv_file, pdf_file, site_name, course_names
csv_file, pdf_file, site_name, courses_info
)
except Exception: # pylint: disable=broad-except
# Catch all exceptions here, since the Django view implicitly

View File

@@ -514,6 +514,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
response,
unicode(course.id),
course.display_name,
course.start_datetime_text(),
courseware_url
)
@@ -965,11 +966,12 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
else:
self.assertFalse(displayed, msg="Expected '{req}' requirement to be hidden".format(req=req))
def _assert_course_details(self, response, course_key, display_name, url):
def _assert_course_details(self, response, course_key, display_name, start_text, url):
"""Check the course information on the page. """
response_dict = self._get_page_data(response)
self.assertEqual(response_dict['course_key'], course_key)
self.assertEqual(response_dict['course_name'], display_name)
self.assertEqual(response_dict['course_start_date'], start_text)
self.assertEqual(response_dict['courseware_url'], url)
def _assert_user_details(self, response, full_name):
@@ -999,6 +1001,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
'full_name': pay_and_verify_div['data-full-name'],
'course_key': pay_and_verify_div['data-course-key'],
'course_name': pay_and_verify_div['data-course-name'],
'course_start_date': pay_and_verify_div['data-course-start-date'],
'courseware_url': pay_and_verify_div['data-courseware-url'],
'course_mode_name': pay_and_verify_div['data-course-mode-name'],
'course_mode_slug': pay_and_verify_div['data-course-mode-slug'],

View File

@@ -5,23 +5,17 @@
'js/discovery/views/search_form', 'js/discovery/views/courses_listing',
'js/discovery/views/filter_bar', 'js/discovery/views/refine_sidebar'],
function(Backbone, SearchState, Filters, SearchForm, CoursesListing, FilterBar, RefineSidebar) {
return function(meanings, searchQuery, userLanguage, userTimezone) {
return function(meanings, searchQuery) {
var dispatcher = _.extend({}, Backbone.Events);
var search = new SearchState();
var filters = new Filters();
var listing = new CoursesListing({model: search.discovery});
var form = new SearchForm();
var filterBar = new FilterBar({collection: filters});
var refineSidebar = new RefineSidebar({
collection: search.discovery.facetOptions,
meanings: meanings
});
var listing;
var courseListingModel = search.discovery;
courseListingModel.userPreferences = {
userLanguage: userLanguage,
userTimezone: userTimezone
};
listing = new CoursesListing({model: courseListingModel});
dispatcher.listenTo(form, 'search', function(query) {
filters.reset();

View File

@@ -4,19 +4,24 @@
'underscore',
'backbone',
'gettext',
'edx-ui-toolkit/js/utils/date-utils'
], function($, _, Backbone, gettext, DateUtils) {
'date'
], function($, _, Backbone, gettext, Date) {
'use strict';
function formatDate(date, userLanguage, userTimezone) {
var context;
context = {
datetime: date,
language: userLanguage,
timezone: userTimezone,
format: DateUtils.dateFormatEnum.shortDate
};
return DateUtils.localize(context);
function formatDate(date) {
return dateUTC(date).toString('MMM dd, yyyy');
}
// Return a date object using UTC time instead of local time
function dateUTC(date) {
return new Date(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds()
);
}
return Backbone.View.extend({
@@ -31,26 +36,8 @@
render: function() {
var data = _.clone(this.model.attributes);
var userLanguage = '',
userTimezone = '';
if (this.model.userPreferences !== undefined) {
userLanguage = this.model.userPreferences.userLanguage;
userTimezone = this.model.userPreferences.userTimezone;
}
if (data.advertised_start !== undefined) {
data.start = data.advertised_start;
} else {
data.start = formatDate(
new Date(data.start),
userLanguage,
userTimezone
);
}
data.enrollment_start = formatDate(
new Date(data.enrollment_start),
userLanguage,
userTimezone
);
data.start = formatDate(new Date(data.start));
data.enrollment_start = formatDate(new Date(data.enrollment_start));
this.$el.html(this.tpl(data));
return this;
}

View File

@@ -31,15 +31,12 @@
},
renderItems: function() {
/* eslint no-param-reassign: [2, { "props": true }] */
var latest = this.model.latest();
var items = latest.map(function(result) {
result.userPreferences = this.model.userPreferences;
var item = new CourseCardView({model: result});
return item.render().el;
}, this);
this.$list.append(items);
/* eslint no-param-reassign: [2, { "props": false }] */
},
attachScrollHandler: function() {

View File

@@ -4,15 +4,14 @@
(function(define) {
'use strict';
define([
'backbone',
'edx-ui-toolkit/js/utils/date-utils'
'backbone'
],
function(Backbone, DateUtils) {
function(Backbone) {
return Backbone.Model.extend({
initialize: function(data) {
if (data) {
this.context = data;
this.setActiveRunMode(this.getRunMode(data.run_modes), data.user_preferences);
this.setActiveRunMode(this.getRunMode(data.run_modes));
}
},
@@ -32,7 +31,7 @@
var enrolled_mode = _.findWhere(runModes, {is_enrolled: true}),
openEnrollmentRunModes = this.getEnrollableRunModes(),
desiredRunMode;
// We populate our model by looking at the run modes.
// We populate our model by looking at the run modes.
if (enrolled_mode) {
// If the learner is already enrolled in a run mode, return that one.
desiredRunMode = enrolled_mode;
@@ -65,44 +64,15 @@
});
},
formatDate: function(date, userPreferences) {
var context,
userTimezone = '',
userLanguage = '';
if (userPreferences !== undefined) {
userTimezone = userPreferences.time_zone;
userLanguage = userPreferences['pref-lang'];
}
context = {
datetime: date,
timezone: userTimezone,
language: userLanguage,
format: DateUtils.dateFormatEnum.shortDate
};
return DateUtils.localize(context);
},
setActiveRunMode: function(runMode, userPreferences) {
var startDateString;
setActiveRunMode: function(runMode) {
if (runMode) {
if (runMode.advertised_start !== undefined && runMode.advertised_start !== 'None') {
startDateString = runMode.advertised_start;
} else {
startDateString = this.formatDate(
runMode.start_date,
userPreferences
);
}
this.set({
certificate_url: runMode.certificate_url,
course_image_url: runMode.course_image_url || '',
course_key: runMode.course_key,
course_url: runMode.course_url || '',
display_name: this.context.display_name,
end_date: this.formatDate(
runMode.end_date,
userPreferences
),
end_date: runMode.end_date,
enrollable_run_modes: this.getEnrollableRunModes(),
is_course_ended: runMode.is_course_ended,
is_enrolled: runMode.is_enrolled,
@@ -111,12 +81,13 @@
marketing_url: runMode.marketing_url,
mode_slug: runMode.mode_slug,
run_key: runMode.run_key,
start_date: startDateString,
start_date: runMode.start_date,
upcoming_run_modes: this.getUpcomingRunModes(),
upgrade_url: runMode.upgrade_url
});
}
},
setUnselected: function() {
// Called to reset the model back to the unselected state.
var unselectedMode = this.getUnselectedRunMode(this.get('enrollable_run_modes'));

View File

@@ -33,8 +33,7 @@
this.options = options;
this.programModel = new Backbone.Model(this.options.programData);
this.courseCardCollection = new CourseCardCollection(
this.programModel.get('course_codes'),
this.options.userPreferences
this.programModel.get('course_codes')
);
this.render();
},

View File

@@ -47,7 +47,7 @@ define([
expect(this.view.$el.find('.course-name')).toContainHtml(data.org);
expect(this.view.$el.find('.course-name')).toContainHtml(data.content.number);
expect(this.view.$el.find('.course-name')).toContainHtml(data.content.display_name);
expect(this.view.$el.find('.course-date').text().trim()).toEqual('Starts: Jan 1, 1970');
expect(this.view.$el.find('.course-date')).toContainHtml('Jan 01, 1970');
});
});
});

View File

@@ -30,9 +30,8 @@ define([
context.run_modes[0].marketing_url
);
expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key);
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date
);
expect(view.$('.course-details .course-text .run-period').html())
.toEqual(context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date);
};
beforeEach(function() {
@@ -93,15 +92,6 @@ define([
validateCourseInfoDisplay();
});
it('should show the course advertised start date', function() {
var advertisedStart = 'This is an advertised start';
context.run_modes[0].advertised_start = advertisedStart;
setupView(context, false);
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
advertisedStart + ' - ' + context.run_modes[0].end_date
);
});
it('should only show certificate status section if a certificate has been earned', function() {
var certUrl = 'sample-certificate';

View File

@@ -75,6 +75,7 @@ var edx = edx || {};
'payment-confirmation-step': {
courseKey: el.data('course-key'),
courseName: el.data('course-name'),
courseStartDate: el.data('course-start-date'),
coursewareUrl: el.data('courseware-url'),
platformName: el.data('platform-name'),
requirements: el.data('requirements')
@@ -93,6 +94,7 @@ var edx = edx || {};
},
'enrollment-confirmation-step': {
courseName: el.data('course-name'),
courseStartDate: el.data('course-start-date'),
coursewareUrl: el.data('courseware-url'),
platformName: el.data('platform-name')
}

View File

@@ -23,6 +23,7 @@ var edx = edx || {};
defaultContext: function() {
return {
courseName: '',
courseStartDate: '',
coursewareUrl: '',
platformName: ''
};

View File

@@ -16,6 +16,7 @@ var edx = edx || {};
return {
courseKey: '',
courseName: '',
courseStartDate: '',
coursewareUrl: '',
platformName: '',
requirements: []

View File

@@ -20,9 +20,7 @@
<%static:require_module module_name="js/discovery/discovery_factory" class_name="DiscoveryFactory">
DiscoveryFactory(
${course_discovery_meanings | n, dump_js_escaped_json},
getParameterByName('search_query'),
"${user_language}",
"${user_timezone}"
getParameterByName('search_query')
);
</%static:require_module>
</%block>

View File

@@ -16,7 +16,6 @@ from openedx.core.djangolib.js_utils import (
ProgramDetailsFactory({
programData: ${program_data | n, dump_js_escaped_json},
urls: ${urls | n, dump_js_escaped_json},
userPreferences: ${user_preferences | n, dump_js_escaped_json},
});
</%static:require_module>
</%block>

View File

@@ -301,6 +301,26 @@ from openedx.core.lib.courses import course_image_url
<span class="course-registration-title">${_('Registration for:')}</span>
<span class="course-display-name">${ course.display_name | h }</span>
</h3>
<p class="course-meta-info" aria-describedby="course-title">
<span class="course-dates-title">
<%
course_start_time = course.start_datetime_text()
course_end_time = course.end_datetime_text()
%>
% if course_start_time or course_end_time:
${_("Course Dates")}:
%endif
</span>
<span class="course-display-dates">
% if course_start_time:
${course_start_time}
%endif
-
% if course_end_time:
${course_end_time}
%endif
</span>
</p>
<hr>
<div class="three-col">
% if item.status == "purchased":

View File

@@ -34,6 +34,12 @@ from openedx.core.lib.courses import course_image_url
<div class="course-title">
<h1>
${_("{course_name}").format(course_name=course.display_name) | h}
<span class="course-dates">
${_("{start_date} - {end_date}").format(
start_date=course.start_datetime_text(),
end_date=course.end_datetime_text()
)}
</span>
</h1>
</div>
<hr>

View File

@@ -34,6 +34,11 @@ from openedx.core.lib.courses import course_image_url
<div class="course-title">
<h1>
${course.display_name | h}
<span class="course-dates">
${course.start_datetime_text()}
-
${course.end_datetime_text()}
</span>
</h1>
</div>
<hr>

View File

@@ -74,6 +74,10 @@ from openedx.core.lib.courses import course_image_url
<span class="course-registration-title">${_('Registration for:')}</span>
<span class="course-display-name">${ course.display_name }</span>
</h3>
<p class="course-meta-info" aria-describedby="course-title">
<span class="course-dates-title">${_('Course Dates:')}</span>
<span class="course-display-dates">${ course.start_datetime_text() } - ${ course.end_datetime_text() }</span>
</p>
<hr>
<div class="three-col">
<div class="col-1">

View File

@@ -13,14 +13,16 @@
<thead>
<tr>
<th scope="col" ><%- gettext( "Course" ) %></th>
<th scope="col" ></th>
<th scope="col" ><%- gettext( "Status" ) %></th>
</tr>
</thead>
<tbody>
<tr>
<td><%- courseName %></td>
<td></td>
<td>
<%- _.sprintf( gettext( "Starts: %(start)s" ), { start: courseStartDate } ) %>
</td>
</tr>
</tbody>

View File

@@ -65,6 +65,7 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView
data-platform-name='${platform_name}'
data-course-key='${course_key}'
data-course-name='${course.display_name}'
data-course-start-date='${course.start_datetime_text()}'
data-courseware-url='${courseware_url}'
data-course-mode-name='${course_mode.name}'
data-course-mode-slug='${course_mode.slug}'
@@ -123,3 +124,6 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView
</section>
</div>
</%block>
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
DateUtilFactory.transform(iterationKey=".localized-datetime");
</%static:require_module_async>