Commit f0aa24ef by Calen Pennington Committed by GitHub

Merge pull request #14205 from cpennington/revert-14078

Revert "Merge pull request #14078 from edx/yro_remove-datetimetext-fu…
parents bd87061f 47e21ca5
......@@ -10,8 +10,11 @@ from datetime import datetime, timedelta
import dateutil.parser
from math import exp
from openedx.core.lib.time_zone_utils import get_time_zone_abbr
from pytz import utc
from .fields import Date
DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=utc)
......@@ -93,6 +96,83 @@ def course_start_date_is_default(start, advertised_start):
return advertised_start is None and start == DEFAULT_START_DATE
def _datetime_to_string(date_time, format_string, time_zone, strftime_localized):
"""
Formats the given datetime with the given function and format string.
Adds time zone abbreviation to the resulting string if the format is DATE_TIME or TIME.
Arguments:
date_time (datetime): the datetime to be formatted
format_string (str): the date format type, as passed to strftime
time_zone (pytz time zone): the time zone to convert to
strftime_localized ((datetime, str) -> str): a nm localized string
formatting function
"""
result = strftime_localized(date_time.astimezone(time_zone), format_string)
abbr = get_time_zone_abbr(time_zone, date_time)
return (
result + ' ' + abbr if format_string in ['DATE_TIME', 'TIME', 'DAY_AND_TIME']
else result
)
def course_start_datetime_text(start_date, advertised_start, format_string, time_zone, ugettext, strftime_localized):
"""
Calculates text to be shown to user regarding a course's start
datetime in specified time zone.
Prefers .advertised_start, then falls back to .start.
Arguments:
start_date (datetime): the course's start datetime
advertised_start (str): the course's advertised start date
format_string (str): the date format type, as passed to strftime
time_zone (pytz time zone): the time zone to convert to
ugettext ((str) -> str): a text localization function
strftime_localized ((datetime, str) -> str): a localized string
formatting function
"""
if advertised_start is not None:
# TODO: This will return an empty string if advertised_start == ""... consider changing this behavior?
try:
# from_json either returns a Date, returns None, or raises a ValueError
parsed_advertised_start = Date().from_json(advertised_start)
if parsed_advertised_start is not None:
# In the Django implementation of strftime_localized, if
# the year is <1900, _datetime_to_string will raise a ValueError.
return _datetime_to_string(parsed_advertised_start, format_string, time_zone, strftime_localized)
except ValueError:
pass
return advertised_start.title()
elif start_date != DEFAULT_START_DATE:
return _datetime_to_string(start_date, format_string, time_zone, strftime_localized)
else:
_ = ugettext
# Translators: TBD stands for 'To Be Determined' and is used when a course
# does not yet have an announced start date.
return _('TBD')
def course_end_datetime_text(end_date, format_string, time_zone, strftime_localized):
"""
Returns a formatted string for a course's end date or datetime.
If end_date is None, an empty string will be returned.
Arguments:
end_date (datetime): the end datetime of a course
format_string (str): the date format type, as passed to strftime
time_zone (pytz time zone): the time zone to convert to
strftime_localized ((datetime, str) -> str): a localized string
formatting function
"""
return (
_datetime_to_string(end_date, format_string, time_zone, strftime_localized) if end_date is not None
else ''
)
def may_certify_for_course(certificates_display_behavior, certificates_show_before_end, has_ended):
"""
Returns whether it is acceptable to show the student a certificate download
......
......@@ -197,11 +197,10 @@ class CourseFields(object):
scope=Scope.settings,
)
advertised_start = String(
display_name=_("Course Advertised Start"),
display_name=_("Course Advertised Start Date"),
help=_(
"Enter the text that you want to use as the advertised starting time frame for the course, "
"such as \"Winter 2018\". If you enter null for this value, the start date that you have set "
"for this course is used."
"Enter the date you want to advertise as the course start date, if this date is different from the set "
"start date. To advertise the set start date, enter null."
),
scope=Scope.settings
)
......@@ -1212,6 +1211,21 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
"""Return the course_id for this course"""
return self.location.course_key
def start_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""
Returns the desired text corresponding the course's start date and time in specified time zone, defaulted
to UTC. Prefers .advertised_start, then falls back to .start
"""
i18n = self.runtime.service(self, "i18n")
return course_metadata_utils.course_start_datetime_text(
self.start,
self.advertised_start,
format_string,
time_zone,
i18n.ugettext,
i18n.strftime
)
@property
def start_date_is_still_default(self):
"""
......@@ -1223,6 +1237,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
self.advertised_start
)
def end_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""
Returns the end date or date_time for the course formatted as a string.
"""
return course_metadata_utils.course_end_datetime_text(
self.end,
format_string,
time_zone,
self.runtime.service(self, "i18n").strftime
)
def get_discussion_blackout_datetimes(self):
"""
Get a list of dicts with start and end fields with datetime values from
......
......@@ -5,7 +5,7 @@ from collections import namedtuple
from datetime import timedelta, datetime
from unittest import TestCase
from pytz import utc
from pytz import timezone, utc
from xmodule.block_metadata_utils import (
url_name_for_block,
display_name_with_default,
......@@ -18,8 +18,11 @@ from xmodule.course_metadata_utils import (
has_course_ended,
DEFAULT_START_DATE,
course_start_date_is_default,
course_start_datetime_text,
course_end_datetime_text,
may_certify_for_course,
)
from xmodule.fields import Date
from xmodule.modulestore.tests.utils import (
MongoModulestoreBuilder,
VersioningModulestoreBuilder,
......@@ -109,6 +112,12 @@ class CourseMetadataUtilsTestCase(TestCase):
test_datetime = datetime(1945, 2, 6, 4, 20, 00, tzinfo=utc)
advertised_start_parsable = "2038-01-19 03:14:07"
advertised_start_bad_date = "215-01-01 10:10:10"
advertised_start_unparsable = "This coming fall"
time_zone_normal_parsable = "2016-03-27 00:59:00"
time_zone_normal_datetime = datetime(2016, 3, 27, 00, 59, 00, tzinfo=utc)
time_zone_daylight_parsable = "2016-03-27 01:00:00"
time_zone_daylight_datetime = datetime(2016, 3, 27, 1, 00, 00, tzinfo=utc)
FunctionTest = namedtuple('FunctionTest', 'function scenarios') # pylint: disable=invalid-name
TestScenario = namedtuple('TestScenario', 'arguments expected_return') # pylint: disable=invalid-name
......@@ -160,6 +169,79 @@ class CourseMetadataUtilsTestCase(TestCase):
TestScenario((DEFAULT_START_DATE, advertised_start_parsable), False),
TestScenario((DEFAULT_START_DATE, None), True),
]),
FunctionTest(course_start_datetime_text, [
# Test parsable advertised start date.
# Expect start datetime to be parsed and formatted back into a string.
TestScenario(
(DEFAULT_START_DATE, advertised_start_parsable, 'DATE_TIME',
utc, noop_gettext, mock_strftime_localized),
mock_strftime_localized(Date().from_json(advertised_start_parsable), 'DATE_TIME') + " UTC"
),
# Test un-parsable advertised start date.
# Expect date parsing to throw a ValueError, and the advertised
# start to be returned in Title Case.
TestScenario(
(test_datetime, advertised_start_unparsable, 'DATE_TIME',
utc, noop_gettext, mock_strftime_localized),
advertised_start_unparsable.title()
),
# Test parsable advertised start date from before January 1, 1900.
# Expect mock_strftime_localized to throw a ValueError, and the
# advertised start to be returned in Title Case.
TestScenario(
(test_datetime, advertised_start_bad_date, 'DATE_TIME',
utc, noop_gettext, mock_strftime_localized),
advertised_start_bad_date.title()
),
# Test without advertised start date, but with a set start datetime.
# Expect formatted datetime to be returned.
TestScenario(
(test_datetime, None, 'SHORT_DATE', utc, noop_gettext, mock_strftime_localized),
mock_strftime_localized(test_datetime, 'SHORT_DATE')
),
# Test without advertised start date and with default start datetime.
# Expect TBD to be returned.
TestScenario(
(DEFAULT_START_DATE, None, 'SHORT_DATE', utc, noop_gettext, mock_strftime_localized),
'TBD'
),
# Test correctly formatted start datetime is returned during normal daylight hours
TestScenario(
(DEFAULT_START_DATE, time_zone_normal_parsable, 'DATE_TIME',
timezone('Europe/Paris'), noop_gettext, mock_strftime_localized),
"DATE_TIME " + "2016-03-27 01:59:00 CET"
),
# Test correctly formatted start datetime is returned during daylight savings hours
TestScenario(
(DEFAULT_START_DATE, time_zone_daylight_parsable, 'DATE_TIME',
timezone('Europe/Paris'), noop_gettext, mock_strftime_localized),
"DATE_TIME " + "2016-03-27 03:00:00 CEST"
)
]),
FunctionTest(course_end_datetime_text, [
# Test with a set end datetime.
# Expect formatted datetime to be returned.
TestScenario(
(test_datetime, 'TIME', utc, mock_strftime_localized),
mock_strftime_localized(test_datetime, 'TIME') + " UTC"
),
# Test with default end datetime.
# Expect empty string to be returned.
TestScenario(
(None, 'TIME', utc, mock_strftime_localized),
""
),
# Test correctly formatted end datetime is returned during normal daylight hours
TestScenario(
(time_zone_normal_datetime, 'TIME', timezone('Europe/Paris'), mock_strftime_localized),
"TIME " + "2016-03-27 01:59:00 CET"
),
# Test correctly formatted end datetime is returned during daylight savings hours
TestScenario(
(time_zone_daylight_datetime, 'TIME', timezone('Europe/Paris'), mock_strftime_localized),
"TIME " + "2016-03-27 03:00:00 CEST"
)
]),
FunctionTest(may_certify_for_course, [
TestScenario(('early_with_info', True, True), True),
TestScenario(('early_no_info', False, False), True),
......
......@@ -6,7 +6,7 @@ from datetime import datetime, timedelta
import itertools
from fs.memoryfs import MemoryFS
from mock import Mock, patch
from pytz import utc
from pytz import timezone, utc
from xblock.runtime import KvsFieldData, DictKeyValueStore
import xmodule.course_module
......@@ -209,6 +209,36 @@ class IsNewCourseTestCase(unittest.TestCase):
(xmodule.course_module.CourseFields.start.default, 'January 2014', 'January 2014', False, 'January 2014'),
]
@patch('xmodule.course_metadata_utils.datetime.now')
def test_start_date_text(self, gmtime_mock):
gmtime_mock.return_value = NOW
for s in self.start_advertised_settings:
d = get_dummy_course(start=s[0], advertised_start=s[1])
print "Checking start=%s advertised=%s" % (s[0], s[1])
self.assertEqual(d.start_datetime_text(), s[2])
@patch('xmodule.course_metadata_utils.datetime.now')
def test_start_date_time_text(self, gmtime_mock):
gmtime_mock.return_value = NOW
for setting in self.start_advertised_settings:
course = get_dummy_course(start=setting[0], advertised_start=setting[1])
print "Checking start=%s advertised=%s" % (setting[0], setting[1])
self.assertEqual(course.start_datetime_text("DATE_TIME"), setting[4])
@ddt.data(("2015-11-01T08:59", 'Nov 01, 2015', u'Nov 01, 2015 at 01:59 PDT'),
("2015-11-01T09:00", 'Nov 01, 2015', u'Nov 01, 2015 at 01:00 PST'))
@ddt.unpack
def test_start_date_time_zone(self, course_date, expected_short_date, expected_date_time):
"""
Test that start datetime text correctly formats datetimes
for normal daylight hours and daylight savings hours
"""
time_zone = timezone('America/Los_Angeles')
course = get_dummy_course(start=course_date, advertised_start=course_date)
self.assertEqual(course.start_datetime_text(time_zone=time_zone), expected_short_date)
self.assertEqual(course.start_datetime_text("DATE_TIME", time_zone), expected_date_time)
def test_start_date_is_default(self):
for s in self.start_advertised_settings:
d = get_dummy_course(start=s[0], advertised_start=s[1])
......@@ -246,6 +276,36 @@ class IsNewCourseTestCase(unittest.TestCase):
descriptor = get_dummy_course(start='2012-12-31T12:00')
assert descriptor.is_newish is True
def test_end_date_text(self):
# No end date set, returns empty string.
d = get_dummy_course('2012-12-02T12:00')
self.assertEqual('', d.end_datetime_text())
d = get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
self.assertEqual('Sep 04, 2014', d.end_datetime_text())
def test_end_date_time_text(self):
# No end date set, returns empty string.
course = get_dummy_course('2012-12-02T12:00')
self.assertEqual('', course.end_datetime_text("DATE_TIME"))
course = get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
self.assertEqual('Sep 04, 2014 at 12:00 UTC', course.end_datetime_text("DATE_TIME"))
@ddt.data(("2015-11-01T08:59", 'Nov 01, 2015', u'Nov 01, 2015 at 01:59 PDT'),
("2015-11-01T09:00", 'Nov 01, 2015', u'Nov 01, 2015 at 01:00 PST'))
@ddt.unpack
def test_end_date_time_zone(self, course_date, expected_short_date, expected_date_time):
"""
Test that end datetime text correctly formats datetimes
for normal daylight hours and daylight savings hours
"""
time_zone = timezone('America/Los_Angeles')
course = get_dummy_course(course_date, end=course_date)
self.assertEqual(course.end_datetime_text(time_zone=time_zone), expected_short_date)
self.assertEqual(course.end_datetime_text("DATE_TIME", time_zone), expected_date_time)
class DiscussionTopicsTestCase(unittest.TestCase):
def test_default_discussion_topics(self):
......
......@@ -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):
"""
......
......@@ -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
......
......@@ -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)
......@@ -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
......
......@@ -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'],
......
......@@ -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();
......
......@@ -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;
}
......
......@@ -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() {
......
......@@ -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'));
......
......@@ -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();
},
......
......@@ -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');
});
});
});
......@@ -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';
......
......@@ -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')
}
......
......@@ -23,6 +23,7 @@ var edx = edx || {};
defaultContext: function() {
return {
courseName: '',
courseStartDate: '',
coursewareUrl: '',
platformName: ''
};
......
......@@ -16,6 +16,7 @@ var edx = edx || {};
return {
courseKey: '',
courseName: '',
courseStartDate: '',
coursewareUrl: '',
platformName: '',
requirements: []
......
......@@ -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>
......
......@@ -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>
......
......@@ -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":
......
......@@ -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>
......
......@@ -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>
......
......@@ -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">
......
......@@ -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>
......
......@@ -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>
......@@ -9,6 +9,7 @@ from django.db import models, transaction
from django.db.models.fields import BooleanField, DateTimeField, DecimalField, TextField, FloatField, IntegerField
from django.db.utils import IntegrityError
from django.template import defaultfilters
from django.utils.translation import ugettext
from ccx_keys.locator import CCXLocator
from model_utils.models import TimeStampedModel
......@@ -17,7 +18,9 @@ from opaque_keys.edx.keys import CourseKey
from config_models.models import ConfigurationModel
from lms.djangoapps import django_comment_client
from openedx.core.djangoapps.models.course_details import CourseDetails
from pytz import utc
from static_replace.models import AssetBaseUrlConfig
from util.date_utils import strftime_localized
from xmodule import course_metadata_utils, block_metadata_utils
from xmodule.course_module import CourseDescriptor, DEFAULT_START_DATE
from xmodule.error_module import ErrorDescriptor
......@@ -356,6 +359,21 @@ class CourseOverview(TimeStampedModel):
"""
return course_metadata_utils.course_starts_within(self.start, days)
def start_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""
Returns the desired text corresponding to the course's start date and
time in the specified time zone, or utc if no time zone given.
Prefers .advertised_start, then falls back to .start.
"""
return course_metadata_utils.course_start_datetime_text(
self.start,
self.advertised_start,
format_string,
time_zone,
ugettext,
strftime_localized
)
@property
def start_date_is_still_default(self):
"""
......@@ -367,6 +385,18 @@ class CourseOverview(TimeStampedModel):
self.advertised_start,
)
def end_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""
Returns the end date or datetime for the course formatted as a string.
"""
return course_metadata_utils.course_end_datetime_text(
self.end,
format_string,
time_zone,
strftime_localized
)
@property
def sorting_score(self):
"""
......
......@@ -127,6 +127,10 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
('clean_id', ('#',)),
('has_ended', ()),
('has_started', ()),
('start_datetime_text', ('SHORT_DATE',)),
('start_datetime_text', ('DATE_TIME',)),
('end_datetime_text', ('SHORT_DATE',)),
('end_datetime_text', ('DATE_TIME',)),
('may_certify', ()),
]
for method_name, method_args in methods_to_test:
......
......@@ -11,6 +11,7 @@ from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.utils import override_settings
from django.utils import timezone
from django.utils.text import slugify
import httpretty
import mock
......@@ -18,7 +19,6 @@ from nose.plugins.attrib import attr
from opaque_keys.edx.keys import CourseKey
from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from pytz import utc
from lms.djangoapps.certificates.api import MODES
from lms.djangoapps.commerce.tests.test_utils import update_commerce_config
......@@ -718,8 +718,8 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.course = CourseFactory()
self.course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1)
self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1)
self.course.start = timezone.now() - datetime.timedelta(days=1)
self.course.end = timezone.now() + datetime.timedelta(days=1)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
self.organization = factories.Organization()
......@@ -739,15 +739,14 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
course_image_url=course_overview.course_image_url,
course_key=unicode(self.course.id), # pylint: disable=no-member
course_url=reverse('course_root', args=[self.course.id]), # pylint: disable=no-member
end_date=self.course.end.replace(tzinfo=utc),
end_date=strftime_localized(self.course.end, 'SHORT_DATE'),
enrollment_open_date=strftime_localized(utils.DEFAULT_ENROLLMENT_START_DATE, 'SHORT_DATE'),
is_course_ended=self.course.end < datetime.datetime.now(utc),
is_course_ended=self.course.end < timezone.now(),
is_enrolled=False,
is_enrollment_open=True,
marketing_url=MARKETING_URL,
start_date=self.course.start.replace(tzinfo=utc),
start_date=strftime_localized(self.course.start, 'SHORT_DATE'),
upgrade_url=None,
advertised_start=None
),
**kwargs
)
......@@ -829,9 +828,8 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
"""
Verify that course enrollment status is reflected correctly.
"""
self.course.enrollment_start = datetime.datetime.now(utc) - datetime.timedelta(days=start_offset)
self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=end_offset)
self.course.enrollment_start = timezone.now() - datetime.timedelta(days=start_offset)
self.course.enrollment_end = timezone.now() - datetime.timedelta(days=end_offset)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.ProgramDataExtender(self.program, self.user).extend()
......@@ -847,7 +845,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
Regression test for ECOM-4973.
"""
self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=1)
self.course.enrollment_end = timezone.now() - datetime.timedelta(days=1)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.ProgramDataExtender(self.program, self.user).extend()
......@@ -877,7 +875,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
@ddt.data(-1, 0, 1)
def test_course_course_ended(self, days_offset):
self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=days_offset)
self.course.end = timezone.now() + datetime.timedelta(days=days_offset)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.ProgramDataExtender(self.program, self.user).extend()
......
......@@ -6,9 +6,10 @@ from urlparse import urljoin
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils import timezone
from django.utils.text import slugify
from opaque_keys.edx.keys import CourseKey
from pytz import utc
import pytz
from course_modes.models import CourseMode
from lms.djangoapps.certificates import api as certificate_api
......@@ -29,7 +30,7 @@ from util.organizations_helpers import get_organization_by_short_name
log = logging.getLogger(__name__)
# The datetime module's strftime() methods require a year >= 1900.
DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=utc)
DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=pytz.UTC)
def get_programs(user, program_id=None):
......@@ -382,30 +383,27 @@ class ProgramDataExtender(object):
run_mode['course_url'] = reverse('course_root', args=[self.course_key])
def _attach_run_mode_end_date(self, run_mode):
run_mode['end_date'] = self.course_overview.end
run_mode['end_date'] = self.course_overview.end_datetime_text()
def _attach_run_mode_enrollment_open_date(self, run_mode):
run_mode['enrollment_open_date'] = strftime_localized(self.enrollment_start, 'SHORT_DATE')
def _attach_run_mode_is_course_ended(self, run_mode):
end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=utc)
run_mode['is_course_ended'] = end_date < datetime.datetime.now(utc)
end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
run_mode['is_course_ended'] = end_date < timezone.now()
def _attach_run_mode_is_enrolled(self, run_mode):
run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(self.user, self.course_key)
def _attach_run_mode_is_enrollment_open(self, run_mode):
enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=utc)
run_mode['is_enrollment_open'] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end
enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
run_mode['is_enrollment_open'] = self.enrollment_start <= timezone.now() < enrollment_end
def _attach_run_mode_marketing_url(self, run_mode):
run_mode['marketing_url'] = get_run_marketing_url(self.course_key, self.user)
def _attach_run_mode_start_date(self, run_mode):
run_mode['start_date'] = self.course_overview.start
def _attach_run_mode_advertised_start(self, run_mode):
run_mode['advertised_start'] = self.course_overview.advertised_start
run_mode['start_date'] = self.course_overview.start_datetime_text()
def _attach_run_mode_upgrade_url(self, run_mode):
required_mode_slug = run_mode['mode_slug']
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment