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 ...@@ -10,8 +10,11 @@ from datetime import datetime, timedelta
import dateutil.parser import dateutil.parser
from math import exp from math import exp
from openedx.core.lib.time_zone_utils import get_time_zone_abbr
from pytz import utc from pytz import utc
from .fields import Date
DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=utc) DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=utc)
...@@ -93,6 +96,83 @@ def course_start_date_is_default(start, advertised_start): ...@@ -93,6 +96,83 @@ def course_start_date_is_default(start, advertised_start):
return advertised_start is None and start == DEFAULT_START_DATE 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): 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 Returns whether it is acceptable to show the student a certificate download
......
...@@ -197,11 +197,10 @@ class CourseFields(object): ...@@ -197,11 +197,10 @@ class CourseFields(object):
scope=Scope.settings, scope=Scope.settings,
) )
advertised_start = String( advertised_start = String(
display_name=_("Course Advertised Start"), display_name=_("Course Advertised Start Date"),
help=_( help=_(
"Enter the text that you want to use as the advertised starting time frame for the course, " "Enter the date you want to advertise as the course start date, if this date is different from the set "
"such as \"Winter 2018\". If you enter null for this value, the start date that you have set " "start date. To advertise the set start date, enter null."
"for this course is used."
), ),
scope=Scope.settings scope=Scope.settings
) )
...@@ -1212,6 +1211,21 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): ...@@ -1212,6 +1211,21 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
"""Return the course_id for this course""" """Return the course_id for this course"""
return self.location.course_key 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 @property
def start_date_is_still_default(self): def start_date_is_still_default(self):
""" """
...@@ -1223,6 +1237,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): ...@@ -1223,6 +1237,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
self.advertised_start 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): def get_discussion_blackout_datetimes(self):
""" """
Get a list of dicts with start and end fields with datetime values from Get a list of dicts with start and end fields with datetime values from
......
...@@ -5,7 +5,7 @@ from collections import namedtuple ...@@ -5,7 +5,7 @@ from collections import namedtuple
from datetime import timedelta, datetime from datetime import timedelta, datetime
from unittest import TestCase from unittest import TestCase
from pytz import utc from pytz import timezone, utc
from xmodule.block_metadata_utils import ( from xmodule.block_metadata_utils import (
url_name_for_block, url_name_for_block,
display_name_with_default, display_name_with_default,
...@@ -18,8 +18,11 @@ from xmodule.course_metadata_utils import ( ...@@ -18,8 +18,11 @@ from xmodule.course_metadata_utils import (
has_course_ended, has_course_ended,
DEFAULT_START_DATE, DEFAULT_START_DATE,
course_start_date_is_default, course_start_date_is_default,
course_start_datetime_text,
course_end_datetime_text,
may_certify_for_course, may_certify_for_course,
) )
from xmodule.fields import Date
from xmodule.modulestore.tests.utils import ( from xmodule.modulestore.tests.utils import (
MongoModulestoreBuilder, MongoModulestoreBuilder,
VersioningModulestoreBuilder, VersioningModulestoreBuilder,
...@@ -109,6 +112,12 @@ class CourseMetadataUtilsTestCase(TestCase): ...@@ -109,6 +112,12 @@ class CourseMetadataUtilsTestCase(TestCase):
test_datetime = datetime(1945, 2, 6, 4, 20, 00, tzinfo=utc) test_datetime = datetime(1945, 2, 6, 4, 20, 00, tzinfo=utc)
advertised_start_parsable = "2038-01-19 03:14:07" 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 FunctionTest = namedtuple('FunctionTest', 'function scenarios') # pylint: disable=invalid-name
TestScenario = namedtuple('TestScenario', 'arguments expected_return') # pylint: disable=invalid-name TestScenario = namedtuple('TestScenario', 'arguments expected_return') # pylint: disable=invalid-name
...@@ -160,6 +169,79 @@ class CourseMetadataUtilsTestCase(TestCase): ...@@ -160,6 +169,79 @@ class CourseMetadataUtilsTestCase(TestCase):
TestScenario((DEFAULT_START_DATE, advertised_start_parsable), False), TestScenario((DEFAULT_START_DATE, advertised_start_parsable), False),
TestScenario((DEFAULT_START_DATE, None), True), 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, [ FunctionTest(may_certify_for_course, [
TestScenario(('early_with_info', True, True), True), TestScenario(('early_with_info', True, True), True),
TestScenario(('early_no_info', False, False), True), TestScenario(('early_no_info', False, False), True),
......
...@@ -6,7 +6,7 @@ from datetime import datetime, timedelta ...@@ -6,7 +6,7 @@ from datetime import datetime, timedelta
import itertools import itertools
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from mock import Mock, patch from mock import Mock, patch
from pytz import utc from pytz import timezone, utc
from xblock.runtime import KvsFieldData, DictKeyValueStore from xblock.runtime import KvsFieldData, DictKeyValueStore
import xmodule.course_module import xmodule.course_module
...@@ -209,6 +209,36 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -209,6 +209,36 @@ class IsNewCourseTestCase(unittest.TestCase):
(xmodule.course_module.CourseFields.start.default, 'January 2014', 'January 2014', False, 'January 2014'), (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): def test_start_date_is_default(self):
for s in self.start_advertised_settings: for s in self.start_advertised_settings:
d = get_dummy_course(start=s[0], advertised_start=s[1]) d = get_dummy_course(start=s[0], advertised_start=s[1])
...@@ -246,6 +276,36 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -246,6 +276,36 @@ class IsNewCourseTestCase(unittest.TestCase):
descriptor = get_dummy_course(start='2012-12-31T12:00') descriptor = get_dummy_course(start='2012-12-31T12:00')
assert descriptor.is_newish is True 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): class DiscussionTopicsTestCase(unittest.TestCase):
def test_default_discussion_topics(self): def test_default_discussion_topics(self):
......
...@@ -12,6 +12,7 @@ from pytz import utc ...@@ -12,6 +12,7 @@ from pytz import utc
from lazy import lazy from lazy import lazy
from ccx_keys.locator import CCXLocator 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 openedx.core.djangoapps.xmodule_django.models import CourseKeyField, LocationKeyField
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -83,6 +84,36 @@ class CustomCourseForEdX(models.Model): ...@@ -83,6 +84,36 @@ class CustomCourseForEdX(models.Model):
return datetime.now(utc) > self.due 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 @property
def structure(self): def structure(self):
""" """
......
...@@ -4,12 +4,14 @@ tests for the models ...@@ -4,12 +4,14 @@ tests for the models
import ddt import ddt
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from mock import patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from pytz import utc from pytz import timezone, utc
from student.roles import CourseCcxCoachRole from student.roles import CourseCcxCoachRole
from student.tests.factories import ( from student.tests.factories import (
AdminFactory, AdminFactory,
) )
from util.tests.test_date_utils import fake_ugettext
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, ModuleStoreTestCase,
TEST_DATA_SPLIT_MODULESTORE TEST_DATA_SPLIT_MODULESTORE
...@@ -150,6 +152,95 @@ class TestCCX(ModuleStoreTestCase): ...@@ -150,6 +152,95 @@ class TestCCX(ModuleStoreTestCase):
"""verify that a ccx without a due date has not ended""" """verify that a ccx without a due date has not ended"""
self.assertFalse(self.ccx.has_ended()) # pylint: disable=no-member 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): def test_ccx_max_student_enrollment_correct(self):
""" """
Verify the override value for max_student_enrollments_allowed 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 ...@@ -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.credentials.utils import get_programs_credentials
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs import utils from openedx.core.djangoapps.programs import utils
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences
@login_required @login_required
...@@ -76,8 +75,7 @@ def program_details(request, program_id): ...@@ -76,8 +75,7 @@ def program_details(request, program_id):
'show_program_listing': programs_config.show_program_listing, 'show_program_listing': programs_config.show_program_listing,
'nav_hidden': True, 'nav_hidden': True,
'disable_courseware_js': True, 'disable_courseware_js': True,
'uses_pattern_library': True, 'uses_pattern_library': True
'user_preferences': get_user_preferences(request.user)
} }
return render_to_response('learner_dashboard/program_details.html', context) return render_to_response('learner_dashboard/program_details.html', context)
...@@ -332,7 +332,7 @@ class Order(models.Model): ...@@ -332,7 +332,7 @@ class Order(models.Model):
""" """
this function generates the csv file this function generates the csv file
""" """
course_names = [] course_info = []
csv_file = StringIO.StringIO() csv_file = StringIO.StringIO()
csv_writer = csv.writer(csv_file) csv_writer = csv.writer(csv_file)
csv_writer.writerow(['Course Name', 'Registration Code', 'URL']) csv_writer.writerow(['Course Name', 'Registration Code', 'URL'])
...@@ -340,15 +340,15 @@ class Order(models.Model): ...@@ -340,15 +340,15 @@ class Order(models.Model):
course_id = item.course_id course_id = item.course_id
course = get_course_by_id(item.course_id, depth=0) course = get_course_by_id(item.course_id, depth=0)
registration_codes = CourseRegistrationCode.objects.filter(course_id=course_id, order=self) 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: for registration_code in registration_codes:
redemption_url = reverse('register_code_redemption', args=[registration_code.code]) redemption_url = reverse('register_code_redemption', args=[registration_code.code])
url = '{base_url}{redemption_url}'.format(base_url=site_name, redemption_url=redemption_url) 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]) 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 send confirmation e-mail
""" """
...@@ -358,7 +358,8 @@ class Order(models.Model): ...@@ -358,7 +358,8 @@ class Order(models.Model):
joined_course_names = "" joined_course_names = ""
if self.recipient_email: if self.recipient_email:
recipient_list.append((self.recipient_name, self.recipient_email, 'email_recipient')) 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: if not is_order_type_business:
subject = _("Order Payment Confirmation") subject = _("Order Payment Confirmation")
...@@ -386,7 +387,7 @@ class Order(models.Model): ...@@ -386,7 +387,7 @@ class Order(models.Model):
'recipient_type': recipient[2], 'recipient_type': recipient[2],
'site_name': site_name, 'site_name': site_name,
'order_items': orderitems, 'order_items': orderitems,
'course_names': ", ".join(course_names), 'course_names': ", ".join([course_info[0] for course_info in courses_info]),
'dashboard_url': dashboard_url, 'dashboard_url': dashboard_url,
'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1], 'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1],
'order_placed_by': '{username} ({email})'.format( 'order_placed_by': '{username} ({email})'.format(
...@@ -476,13 +477,13 @@ class Order(models.Model): ...@@ -476,13 +477,13 @@ class Order(models.Model):
item.purchase_item() item.purchase_item()
csv_file = None csv_file = None
course_names = [] courses_info = []
if self.order_type == OrderTypes.BUSINESS: if self.order_type == OrderTypes.BUSINESS:
# #
# Generate the CSV file that contains all of the RegistrationCodes that have already been # Generate the CSV file that contains all of the RegistrationCodes that have already been
# generated when the purchase has transacted # 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: try:
pdf_file = self.generate_pdf_receipt(orderitems) pdf_file = self.generate_pdf_receipt(orderitems)
...@@ -493,7 +494,7 @@ class Order(models.Model): ...@@ -493,7 +494,7 @@ class Order(models.Model):
try: try:
self.send_confirmation_emails( self.send_confirmation_emails(
orderitems, self.order_type == OrderTypes.BUSINESS, 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 except Exception: # pylint: disable=broad-except
# Catch all exceptions here, since the Django view implicitly # Catch all exceptions here, since the Django view implicitly
......
...@@ -514,6 +514,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -514,6 +514,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
response, response,
unicode(course.id), unicode(course.id),
course.display_name, course.display_name,
course.start_datetime_text(),
courseware_url courseware_url
) )
...@@ -965,11 +966,12 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -965,11 +966,12 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
else: else:
self.assertFalse(displayed, msg="Expected '{req}' requirement to be hidden".format(req=req)) 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. """ """Check the course information on the page. """
response_dict = self._get_page_data(response) response_dict = self._get_page_data(response)
self.assertEqual(response_dict['course_key'], course_key) self.assertEqual(response_dict['course_key'], course_key)
self.assertEqual(response_dict['course_name'], display_name) self.assertEqual(response_dict['course_name'], display_name)
self.assertEqual(response_dict['course_start_date'], start_text)
self.assertEqual(response_dict['courseware_url'], url) self.assertEqual(response_dict['courseware_url'], url)
def _assert_user_details(self, response, full_name): def _assert_user_details(self, response, full_name):
...@@ -999,6 +1001,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -999,6 +1001,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
'full_name': pay_and_verify_div['data-full-name'], 'full_name': pay_and_verify_div['data-full-name'],
'course_key': pay_and_verify_div['data-course-key'], 'course_key': pay_and_verify_div['data-course-key'],
'course_name': pay_and_verify_div['data-course-name'], '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'], 'courseware_url': pay_and_verify_div['data-courseware-url'],
'course_mode_name': pay_and_verify_div['data-course-mode-name'], 'course_mode_name': pay_and_verify_div['data-course-mode-name'],
'course_mode_slug': pay_and_verify_div['data-course-mode-slug'], 'course_mode_slug': pay_and_verify_div['data-course-mode-slug'],
......
...@@ -5,23 +5,17 @@ ...@@ -5,23 +5,17 @@
'js/discovery/views/search_form', 'js/discovery/views/courses_listing', 'js/discovery/views/search_form', 'js/discovery/views/courses_listing',
'js/discovery/views/filter_bar', 'js/discovery/views/refine_sidebar'], 'js/discovery/views/filter_bar', 'js/discovery/views/refine_sidebar'],
function(Backbone, SearchState, Filters, SearchForm, CoursesListing, FilterBar, RefineSidebar) { function(Backbone, SearchState, Filters, SearchForm, CoursesListing, FilterBar, RefineSidebar) {
return function(meanings, searchQuery, userLanguage, userTimezone) { return function(meanings, searchQuery) {
var dispatcher = _.extend({}, Backbone.Events); var dispatcher = _.extend({}, Backbone.Events);
var search = new SearchState(); var search = new SearchState();
var filters = new Filters(); var filters = new Filters();
var listing = new CoursesListing({model: search.discovery});
var form = new SearchForm(); var form = new SearchForm();
var filterBar = new FilterBar({collection: filters}); var filterBar = new FilterBar({collection: filters});
var refineSidebar = new RefineSidebar({ var refineSidebar = new RefineSidebar({
collection: search.discovery.facetOptions, collection: search.discovery.facetOptions,
meanings: meanings 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) { dispatcher.listenTo(form, 'search', function(query) {
filters.reset(); filters.reset();
......
...@@ -4,19 +4,24 @@ ...@@ -4,19 +4,24 @@
'underscore', 'underscore',
'backbone', 'backbone',
'gettext', 'gettext',
'edx-ui-toolkit/js/utils/date-utils' 'date'
], function($, _, Backbone, gettext, DateUtils) { ], function($, _, Backbone, gettext, Date) {
'use strict'; 'use strict';
function formatDate(date, userLanguage, userTimezone) { function formatDate(date) {
var context; return dateUTC(date).toString('MMM dd, yyyy');
context = { }
datetime: date,
language: userLanguage, // Return a date object using UTC time instead of local time
timezone: userTimezone, function dateUTC(date) {
format: DateUtils.dateFormatEnum.shortDate return new Date(
}; date.getUTCFullYear(),
return DateUtils.localize(context); date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds()
);
} }
return Backbone.View.extend({ return Backbone.View.extend({
...@@ -31,26 +36,8 @@ ...@@ -31,26 +36,8 @@
render: function() { render: function() {
var data = _.clone(this.model.attributes); var data = _.clone(this.model.attributes);
var userLanguage = '', data.start = formatDate(new Date(data.start));
userTimezone = ''; data.enrollment_start = formatDate(new Date(data.enrollment_start));
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
);
this.$el.html(this.tpl(data)); this.$el.html(this.tpl(data));
return this; return this;
} }
......
...@@ -31,15 +31,12 @@ ...@@ -31,15 +31,12 @@
}, },
renderItems: function() { renderItems: function() {
/* eslint no-param-reassign: [2, { "props": true }] */
var latest = this.model.latest(); var latest = this.model.latest();
var items = latest.map(function(result) { var items = latest.map(function(result) {
result.userPreferences = this.model.userPreferences;
var item = new CourseCardView({model: result}); var item = new CourseCardView({model: result});
return item.render().el; return item.render().el;
}, this); }, this);
this.$list.append(items); this.$list.append(items);
/* eslint no-param-reassign: [2, { "props": false }] */
}, },
attachScrollHandler: function() { attachScrollHandler: function() {
......
...@@ -4,15 +4,14 @@ ...@@ -4,15 +4,14 @@
(function(define) { (function(define) {
'use strict'; 'use strict';
define([ define([
'backbone', 'backbone'
'edx-ui-toolkit/js/utils/date-utils'
], ],
function(Backbone, DateUtils) { function(Backbone) {
return Backbone.Model.extend({ return Backbone.Model.extend({
initialize: function(data) { initialize: function(data) {
if (data) { if (data) {
this.context = 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 @@ ...@@ -32,7 +31,7 @@
var enrolled_mode = _.findWhere(runModes, {is_enrolled: true}), var enrolled_mode = _.findWhere(runModes, {is_enrolled: true}),
openEnrollmentRunModes = this.getEnrollableRunModes(), openEnrollmentRunModes = this.getEnrollableRunModes(),
desiredRunMode; 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 (enrolled_mode) {
// If the learner is already enrolled in a run mode, return that one. // If the learner is already enrolled in a run mode, return that one.
desiredRunMode = enrolled_mode; desiredRunMode = enrolled_mode;
...@@ -65,44 +64,15 @@ ...@@ -65,44 +64,15 @@
}); });
}, },
formatDate: function(date, userPreferences) { setActiveRunMode: function(runMode) {
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;
if (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({ this.set({
certificate_url: runMode.certificate_url, certificate_url: runMode.certificate_url,
course_image_url: runMode.course_image_url || '', course_image_url: runMode.course_image_url || '',
course_key: runMode.course_key, course_key: runMode.course_key,
course_url: runMode.course_url || '', course_url: runMode.course_url || '',
display_name: this.context.display_name, display_name: this.context.display_name,
end_date: this.formatDate( end_date: runMode.end_date,
runMode.end_date,
userPreferences
),
enrollable_run_modes: this.getEnrollableRunModes(), enrollable_run_modes: this.getEnrollableRunModes(),
is_course_ended: runMode.is_course_ended, is_course_ended: runMode.is_course_ended,
is_enrolled: runMode.is_enrolled, is_enrolled: runMode.is_enrolled,
...@@ -111,12 +81,13 @@ ...@@ -111,12 +81,13 @@
marketing_url: runMode.marketing_url, marketing_url: runMode.marketing_url,
mode_slug: runMode.mode_slug, mode_slug: runMode.mode_slug,
run_key: runMode.run_key, run_key: runMode.run_key,
start_date: startDateString, start_date: runMode.start_date,
upcoming_run_modes: this.getUpcomingRunModes(), upcoming_run_modes: this.getUpcomingRunModes(),
upgrade_url: runMode.upgrade_url upgrade_url: runMode.upgrade_url
}); });
} }
}, },
setUnselected: function() { setUnselected: function() {
// Called to reset the model back to the unselected state. // Called to reset the model back to the unselected state.
var unselectedMode = this.getUnselectedRunMode(this.get('enrollable_run_modes')); var unselectedMode = this.getUnselectedRunMode(this.get('enrollable_run_modes'));
......
...@@ -33,8 +33,7 @@ ...@@ -33,8 +33,7 @@
this.options = options; this.options = options;
this.programModel = new Backbone.Model(this.options.programData); this.programModel = new Backbone.Model(this.options.programData);
this.courseCardCollection = new CourseCardCollection( this.courseCardCollection = new CourseCardCollection(
this.programModel.get('course_codes'), this.programModel.get('course_codes')
this.options.userPreferences
); );
this.render(); this.render();
}, },
......
...@@ -47,7 +47,7 @@ define([ ...@@ -47,7 +47,7 @@ define([
expect(this.view.$el.find('.course-name')).toContainHtml(data.org); 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.number);
expect(this.view.$el.find('.course-name')).toContainHtml(data.content.display_name); 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([ ...@@ -30,9 +30,8 @@ define([
context.run_modes[0].marketing_url context.run_modes[0].marketing_url
); );
expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key); expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key);
expect(view.$('.course-details .course-text .run-period').html()).toEqual( expect(view.$('.course-details .course-text .run-period').html())
context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date .toEqual(context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date);
);
}; };
beforeEach(function() { beforeEach(function() {
...@@ -93,15 +92,6 @@ define([ ...@@ -93,15 +92,6 @@ define([
validateCourseInfoDisplay(); 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() { it('should only show certificate status section if a certificate has been earned', function() {
var certUrl = 'sample-certificate'; var certUrl = 'sample-certificate';
......
...@@ -75,6 +75,7 @@ var edx = edx || {}; ...@@ -75,6 +75,7 @@ var edx = edx || {};
'payment-confirmation-step': { 'payment-confirmation-step': {
courseKey: el.data('course-key'), courseKey: el.data('course-key'),
courseName: el.data('course-name'), courseName: el.data('course-name'),
courseStartDate: el.data('course-start-date'),
coursewareUrl: el.data('courseware-url'), coursewareUrl: el.data('courseware-url'),
platformName: el.data('platform-name'), platformName: el.data('platform-name'),
requirements: el.data('requirements') requirements: el.data('requirements')
...@@ -93,6 +94,7 @@ var edx = edx || {}; ...@@ -93,6 +94,7 @@ var edx = edx || {};
}, },
'enrollment-confirmation-step': { 'enrollment-confirmation-step': {
courseName: el.data('course-name'), courseName: el.data('course-name'),
courseStartDate: el.data('course-start-date'),
coursewareUrl: el.data('courseware-url'), coursewareUrl: el.data('courseware-url'),
platformName: el.data('platform-name') platformName: el.data('platform-name')
} }
......
...@@ -23,6 +23,7 @@ var edx = edx || {}; ...@@ -23,6 +23,7 @@ var edx = edx || {};
defaultContext: function() { defaultContext: function() {
return { return {
courseName: '', courseName: '',
courseStartDate: '',
coursewareUrl: '', coursewareUrl: '',
platformName: '' platformName: ''
}; };
......
...@@ -16,6 +16,7 @@ var edx = edx || {}; ...@@ -16,6 +16,7 @@ var edx = edx || {};
return { return {
courseKey: '', courseKey: '',
courseName: '', courseName: '',
courseStartDate: '',
coursewareUrl: '', coursewareUrl: '',
platformName: '', platformName: '',
requirements: [] requirements: []
......
...@@ -20,9 +20,7 @@ ...@@ -20,9 +20,7 @@
<%static:require_module module_name="js/discovery/discovery_factory" class_name="DiscoveryFactory"> <%static:require_module module_name="js/discovery/discovery_factory" class_name="DiscoveryFactory">
DiscoveryFactory( DiscoveryFactory(
${course_discovery_meanings | n, dump_js_escaped_json}, ${course_discovery_meanings | n, dump_js_escaped_json},
getParameterByName('search_query'), getParameterByName('search_query')
"${user_language}",
"${user_timezone}"
); );
</%static:require_module> </%static:require_module>
</%block> </%block>
......
...@@ -16,7 +16,6 @@ from openedx.core.djangolib.js_utils import ( ...@@ -16,7 +16,6 @@ from openedx.core.djangolib.js_utils import (
ProgramDetailsFactory({ ProgramDetailsFactory({
programData: ${program_data | n, dump_js_escaped_json}, programData: ${program_data | n, dump_js_escaped_json},
urls: ${urls | n, dump_js_escaped_json}, urls: ${urls | n, dump_js_escaped_json},
userPreferences: ${user_preferences | n, dump_js_escaped_json},
}); });
</%static:require_module> </%static:require_module>
</%block> </%block>
......
...@@ -301,6 +301,26 @@ from openedx.core.lib.courses import course_image_url ...@@ -301,6 +301,26 @@ from openedx.core.lib.courses import course_image_url
<span class="course-registration-title">${_('Registration for:')}</span> <span class="course-registration-title">${_('Registration for:')}</span>
<span class="course-display-name">${ course.display_name | h }</span> <span class="course-display-name">${ course.display_name | h }</span>
</h3> </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> <hr>
<div class="three-col"> <div class="three-col">
% if item.status == "purchased": % if item.status == "purchased":
......
...@@ -34,6 +34,12 @@ from openedx.core.lib.courses import course_image_url ...@@ -34,6 +34,12 @@ from openedx.core.lib.courses import course_image_url
<div class="course-title"> <div class="course-title">
<h1> <h1>
${_("{course_name}").format(course_name=course.display_name) | h} ${_("{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> </h1>
</div> </div>
<hr> <hr>
......
...@@ -34,6 +34,11 @@ from openedx.core.lib.courses import course_image_url ...@@ -34,6 +34,11 @@ from openedx.core.lib.courses import course_image_url
<div class="course-title"> <div class="course-title">
<h1> <h1>
${course.display_name | h} ${course.display_name | h}
<span class="course-dates">
${course.start_datetime_text()}
-
${course.end_datetime_text()}
</span>
</h1> </h1>
</div> </div>
<hr> <hr>
......
...@@ -74,6 +74,10 @@ from openedx.core.lib.courses import course_image_url ...@@ -74,6 +74,10 @@ from openedx.core.lib.courses import course_image_url
<span class="course-registration-title">${_('Registration for:')}</span> <span class="course-registration-title">${_('Registration for:')}</span>
<span class="course-display-name">${ course.display_name }</span> <span class="course-display-name">${ course.display_name }</span>
</h3> </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> <hr>
<div class="three-col"> <div class="three-col">
<div class="col-1"> <div class="col-1">
......
...@@ -13,14 +13,16 @@ ...@@ -13,14 +13,16 @@
<thead> <thead>
<tr> <tr>
<th scope="col" ><%- gettext( "Course" ) %></th> <th scope="col" ><%- gettext( "Course" ) %></th>
<th scope="col" ></th> <th scope="col" ><%- gettext( "Status" ) %></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><%- courseName %></td> <td><%- courseName %></td>
<td></td> <td>
<%- _.sprintf( gettext( "Starts: %(start)s" ), { start: courseStartDate } ) %>
</td>
</tr> </tr>
</tbody> </tbody>
......
...@@ -65,6 +65,7 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView ...@@ -65,6 +65,7 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView
data-platform-name='${platform_name}' data-platform-name='${platform_name}'
data-course-key='${course_key}' data-course-key='${course_key}'
data-course-name='${course.display_name}' data-course-name='${course.display_name}'
data-course-start-date='${course.start_datetime_text()}'
data-courseware-url='${courseware_url}' data-courseware-url='${courseware_url}'
data-course-mode-name='${course_mode.name}' data-course-mode-name='${course_mode.name}'
data-course-mode-slug='${course_mode.slug}' data-course-mode-slug='${course_mode.slug}'
...@@ -123,3 +124,6 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView ...@@ -123,3 +124,6 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView
</section> </section>
</div> </div>
</%block> </%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 ...@@ -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.models.fields import BooleanField, DateTimeField, DecimalField, TextField, FloatField, IntegerField
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.template import defaultfilters from django.template import defaultfilters
from django.utils.translation import ugettext
from ccx_keys.locator import CCXLocator from ccx_keys.locator import CCXLocator
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
...@@ -17,7 +18,9 @@ from opaque_keys.edx.keys import CourseKey ...@@ -17,7 +18,9 @@ from opaque_keys.edx.keys import CourseKey
from config_models.models import ConfigurationModel from config_models.models import ConfigurationModel
from lms.djangoapps import django_comment_client from lms.djangoapps import django_comment_client
from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.models.course_details import CourseDetails
from pytz import utc
from static_replace.models import AssetBaseUrlConfig from static_replace.models import AssetBaseUrlConfig
from util.date_utils import strftime_localized
from xmodule import course_metadata_utils, block_metadata_utils from xmodule import course_metadata_utils, block_metadata_utils
from xmodule.course_module import CourseDescriptor, DEFAULT_START_DATE from xmodule.course_module import CourseDescriptor, DEFAULT_START_DATE
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
...@@ -356,6 +359,21 @@ class CourseOverview(TimeStampedModel): ...@@ -356,6 +359,21 @@ class CourseOverview(TimeStampedModel):
""" """
return course_metadata_utils.course_starts_within(self.start, days) 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 @property
def start_date_is_still_default(self): def start_date_is_still_default(self):
""" """
...@@ -367,6 +385,18 @@ class CourseOverview(TimeStampedModel): ...@@ -367,6 +385,18 @@ class CourseOverview(TimeStampedModel):
self.advertised_start, 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 @property
def sorting_score(self): def sorting_score(self):
""" """
......
...@@ -127,6 +127,10 @@ class CourseOverviewTestCase(ModuleStoreTestCase): ...@@ -127,6 +127,10 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
('clean_id', ('#',)), ('clean_id', ('#',)),
('has_ended', ()), ('has_ended', ()),
('has_started', ()), ('has_started', ()),
('start_datetime_text', ('SHORT_DATE',)),
('start_datetime_text', ('DATE_TIME',)),
('end_datetime_text', ('SHORT_DATE',)),
('end_datetime_text', ('DATE_TIME',)),
('may_certify', ()), ('may_certify', ()),
] ]
for method_name, method_args in methods_to_test: for method_name, method_args in methods_to_test:
......
...@@ -11,6 +11,7 @@ from django.core.cache import cache ...@@ -11,6 +11,7 @@ from django.core.cache import cache
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils import timezone
from django.utils.text import slugify from django.utils.text import slugify
import httpretty import httpretty
import mock import mock
...@@ -18,7 +19,6 @@ from nose.plugins.attrib import attr ...@@ -18,7 +19,6 @@ from nose.plugins.attrib import attr
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from edx_oauth2_provider.tests.factories import ClientFactory from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL from provider.constants import CONFIDENTIAL
from pytz import utc
from lms.djangoapps.certificates.api import MODES from lms.djangoapps.certificates.api import MODES
from lms.djangoapps.commerce.tests.test_utils import update_commerce_config from lms.djangoapps.commerce.tests.test_utils import update_commerce_config
...@@ -718,8 +718,8 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -718,8 +718,8 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.course = CourseFactory() self.course = CourseFactory()
self.course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1) self.course.start = timezone.now() - datetime.timedelta(days=1)
self.course.end = datetime.datetime.now(utc) + 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.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
self.organization = factories.Organization() self.organization = factories.Organization()
...@@ -739,15 +739,14 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -739,15 +739,14 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
course_image_url=course_overview.course_image_url, course_image_url=course_overview.course_image_url,
course_key=unicode(self.course.id), # pylint: disable=no-member course_key=unicode(self.course.id), # pylint: disable=no-member
course_url=reverse('course_root', args=[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'), 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_enrolled=False,
is_enrollment_open=True, is_enrollment_open=True,
marketing_url=MARKETING_URL, marketing_url=MARKETING_URL,
start_date=self.course.start.replace(tzinfo=utc), start_date=strftime_localized(self.course.start, 'SHORT_DATE'),
upgrade_url=None, upgrade_url=None,
advertised_start=None
), ),
**kwargs **kwargs
) )
...@@ -829,9 +828,8 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -829,9 +828,8 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
""" """
Verify that course enrollment status is reflected correctly. Verify that course enrollment status is reflected correctly.
""" """
self.course.enrollment_start = datetime.datetime.now(utc) - datetime.timedelta(days=start_offset) self.course.enrollment_start = timezone.now() - datetime.timedelta(days=start_offset)
self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=end_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 self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.ProgramDataExtender(self.program, self.user).extend() data = utils.ProgramDataExtender(self.program, self.user).extend()
...@@ -847,7 +845,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -847,7 +845,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
Regression test for ECOM-4973. 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 self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.ProgramDataExtender(self.program, self.user).extend() data = utils.ProgramDataExtender(self.program, self.user).extend()
...@@ -877,7 +875,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -877,7 +875,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
@ddt.data(-1, 0, 1) @ddt.data(-1, 0, 1)
def test_course_course_ended(self, days_offset): 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 self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.ProgramDataExtender(self.program, self.user).extend() data = utils.ProgramDataExtender(self.program, self.user).extend()
......
...@@ -6,9 +6,10 @@ from urlparse import urljoin ...@@ -6,9 +6,10 @@ from urlparse import urljoin
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils import timezone
from django.utils.text import slugify from django.utils.text import slugify
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from pytz import utc import pytz
from course_modes.models import CourseMode from course_modes.models import CourseMode
from lms.djangoapps.certificates import api as certificate_api from lms.djangoapps.certificates import api as certificate_api
...@@ -29,7 +30,7 @@ from util.organizations_helpers import get_organization_by_short_name ...@@ -29,7 +30,7 @@ from util.organizations_helpers import get_organization_by_short_name
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# The datetime module's strftime() methods require a year >= 1900. # 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): def get_programs(user, program_id=None):
...@@ -382,30 +383,27 @@ class ProgramDataExtender(object): ...@@ -382,30 +383,27 @@ class ProgramDataExtender(object):
run_mode['course_url'] = reverse('course_root', args=[self.course_key]) run_mode['course_url'] = reverse('course_root', args=[self.course_key])
def _attach_run_mode_end_date(self, run_mode): 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): def _attach_run_mode_enrollment_open_date(self, run_mode):
run_mode['enrollment_open_date'] = strftime_localized(self.enrollment_start, 'SHORT_DATE') run_mode['enrollment_open_date'] = strftime_localized(self.enrollment_start, 'SHORT_DATE')
def _attach_run_mode_is_course_ended(self, run_mode): def _attach_run_mode_is_course_ended(self, run_mode):
end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=utc) end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
run_mode['is_course_ended'] = end_date < datetime.datetime.now(utc) run_mode['is_course_ended'] = end_date < timezone.now()
def _attach_run_mode_is_enrolled(self, run_mode): def _attach_run_mode_is_enrolled(self, run_mode):
run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(self.user, self.course_key) run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(self.user, self.course_key)
def _attach_run_mode_is_enrollment_open(self, run_mode): def _attach_run_mode_is_enrollment_open(self, run_mode):
enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=utc) enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
run_mode['is_enrollment_open'] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end run_mode['is_enrollment_open'] = self.enrollment_start <= timezone.now() < enrollment_end
def _attach_run_mode_marketing_url(self, run_mode): def _attach_run_mode_marketing_url(self, run_mode):
run_mode['marketing_url'] = get_run_marketing_url(self.course_key, self.user) run_mode['marketing_url'] = get_run_marketing_url(self.course_key, self.user)
def _attach_run_mode_start_date(self, run_mode): def _attach_run_mode_start_date(self, run_mode):
run_mode['start_date'] = self.course_overview.start run_mode['start_date'] = self.course_overview.start_datetime_text()
def _attach_run_mode_advertised_start(self, run_mode):
run_mode['advertised_start'] = self.course_overview.advertised_start
def _attach_run_mode_upgrade_url(self, run_mode): def _attach_run_mode_upgrade_url(self, run_mode):
required_mode_slug = run_mode['mode_slug'] 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