diff --git a/common/lib/xmodule/xmodule/course_metadata_utils.py b/common/lib/xmodule/xmodule/course_metadata_utils.py index e0a2e996ed..726104ff54 100644 --- a/common/lib/xmodule/xmodule/course_metadata_utils.py +++ b/common/lib/xmodule/xmodule/course_metadata_utils.py @@ -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 diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 95af81a9b9..6bcbeef621 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -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 diff --git a/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py b/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py index 06014a9bd7..05df588a7e 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +++ b/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py @@ -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), diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index 35c9322017..72a4d8d176 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -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): diff --git a/lms/djangoapps/ccx/models.py b/lms/djangoapps/ccx/models.py index 8f8d596cbb..0b10b79d89 100644 --- a/lms/djangoapps/ccx/models.py +++ b/lms/djangoapps/ccx/models.py @@ -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): """ diff --git a/lms/djangoapps/ccx/tests/test_models.py b/lms/djangoapps/ccx/tests/test_models.py index 5fdf7fed0e..f4091e4210 100644 --- a/lms/djangoapps/ccx/tests/test_models.py +++ b/lms/djangoapps/ccx/tests/test_models.py @@ -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 diff --git a/lms/djangoapps/learner_dashboard/views.py b/lms/djangoapps/learner_dashboard/views.py index 556489bbd7..a67bbb20ce 100644 --- a/lms/djangoapps/learner_dashboard/views.py +++ b/lms/djangoapps/learner_dashboard/views.py @@ -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) diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index dc4de3e044..9509a8362a 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -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 diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 9add06d8b2..36fb41caa5 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -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'], diff --git a/lms/static/js/discovery/discovery_factory.js b/lms/static/js/discovery/discovery_factory.js index 2bc58fac8d..575bca1d3e 100644 --- a/lms/static/js/discovery/discovery_factory.js +++ b/lms/static/js/discovery/discovery_factory.js @@ -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(); diff --git a/lms/static/js/discovery/views/course_card.js b/lms/static/js/discovery/views/course_card.js index f67ca4bf8e..761e4f07ec 100644 --- a/lms/static/js/discovery/views/course_card.js +++ b/lms/static/js/discovery/views/course_card.js @@ -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; } diff --git a/lms/static/js/discovery/views/courses_listing.js b/lms/static/js/discovery/views/courses_listing.js index da313678ce..62f5672722 100644 --- a/lms/static/js/discovery/views/courses_listing.js +++ b/lms/static/js/discovery/views/courses_listing.js @@ -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() { diff --git a/lms/static/js/learner_dashboard/models/course_card_model.js b/lms/static/js/learner_dashboard/models/course_card_model.js index 2652d600c4..0ed6ea78b1 100644 --- a/lms/static/js/learner_dashboard/models/course_card_model.js +++ b/lms/static/js/learner_dashboard/models/course_card_model.js @@ -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')); diff --git a/lms/static/js/learner_dashboard/views/program_details_view.js b/lms/static/js/learner_dashboard/views/program_details_view.js index 6536375c7e..74effe5a08 100644 --- a/lms/static/js/learner_dashboard/views/program_details_view.js +++ b/lms/static/js/learner_dashboard/views/program_details_view.js @@ -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(); }, diff --git a/lms/static/js/spec/discovery/views/course_card_spec.js b/lms/static/js/spec/discovery/views/course_card_spec.js index 228035dca0..ab093672ec 100644 --- a/lms/static/js/spec/discovery/views/course_card_spec.js +++ b/lms/static/js/spec/discovery/views/course_card_spec.js @@ -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'); }); }); }); diff --git a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js index 9754f70d8c..5f89de6298 100644 --- a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js @@ -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'; diff --git a/lms/static/js/verify_student/pay_and_verify.js b/lms/static/js/verify_student/pay_and_verify.js index ba4ec01a90..fed1aa72cf 100644 --- a/lms/static/js/verify_student/pay_and_verify.js +++ b/lms/static/js/verify_student/pay_and_verify.js @@ -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') } diff --git a/lms/static/js/verify_student/views/enrollment_confirmation_step_view.js b/lms/static/js/verify_student/views/enrollment_confirmation_step_view.js index 3fda84f90a..e71976b765 100644 --- a/lms/static/js/verify_student/views/enrollment_confirmation_step_view.js +++ b/lms/static/js/verify_student/views/enrollment_confirmation_step_view.js @@ -23,6 +23,7 @@ var edx = edx || {}; defaultContext: function() { return { courseName: '', + courseStartDate: '', coursewareUrl: '', platformName: '' }; diff --git a/lms/static/js/verify_student/views/payment_confirmation_step_view.js b/lms/static/js/verify_student/views/payment_confirmation_step_view.js index 501b84d3e6..cba61eebc6 100644 --- a/lms/static/js/verify_student/views/payment_confirmation_step_view.js +++ b/lms/static/js/verify_student/views/payment_confirmation_step_view.js @@ -16,6 +16,7 @@ var edx = edx || {}; return { courseKey: '', courseName: '', + courseStartDate: '', coursewareUrl: '', platformName: '', requirements: [] diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 94f48ea018..994ef7e6d7 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -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') ); diff --git a/lms/templates/learner_dashboard/program_details.html b/lms/templates/learner_dashboard/program_details.html index b31f71e37a..8af6668543 100644 --- a/lms/templates/learner_dashboard/program_details.html +++ b/lms/templates/learner_dashboard/program_details.html @@ -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}, }); diff --git a/lms/templates/shoppingcart/receipt.html b/lms/templates/shoppingcart/receipt.html index 4ae016b773..08e213e3c8 100644 --- a/lms/templates/shoppingcart/receipt.html +++ b/lms/templates/shoppingcart/receipt.html @@ -301,6 +301,26 @@ from openedx.core.lib.courses import course_image_url ${_('Registration for:')} ${ course.display_name | h } +

+ + <% + 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 + + + % if course_start_time: + ${course_start_time} + %endif + - + % if course_end_time: + ${course_end_time} + %endif + +


% if item.status == "purchased": diff --git a/lms/templates/shoppingcart/registration_code_receipt.html b/lms/templates/shoppingcart/registration_code_receipt.html index cd85e78b61..df9359ba21 100644 --- a/lms/templates/shoppingcart/registration_code_receipt.html +++ b/lms/templates/shoppingcart/registration_code_receipt.html @@ -34,6 +34,12 @@ from openedx.core.lib.courses import course_image_url

${_("{course_name}").format(course_name=course.display_name) | h} + + ${_("{start_date} - {end_date}").format( + start_date=course.start_datetime_text(), + end_date=course.end_datetime_text() + )} +


diff --git a/lms/templates/shoppingcart/registration_code_redemption.html b/lms/templates/shoppingcart/registration_code_redemption.html index 266db3b955..c2c3213db7 100644 --- a/lms/templates/shoppingcart/registration_code_redemption.html +++ b/lms/templates/shoppingcart/registration_code_redemption.html @@ -34,6 +34,11 @@ from openedx.core.lib.courses import course_image_url

${course.display_name | h} + + ${course.start_datetime_text()} + - + ${course.end_datetime_text()} +


diff --git a/lms/templates/shoppingcart/shopping_cart.html b/lms/templates/shoppingcart/shopping_cart.html index 6bc5c277c6..6775236220 100644 --- a/lms/templates/shoppingcart/shopping_cart.html +++ b/lms/templates/shoppingcart/shopping_cart.html @@ -74,6 +74,10 @@ from openedx.core.lib.courses import course_image_url ${_('Registration for:')} ${ course.display_name } +

+ ${_('Course Dates:')} + ${ course.start_datetime_text() } - ${ course.end_datetime_text() } +


diff --git a/lms/templates/verify_student/enrollment_confirmation_step.underscore b/lms/templates/verify_student/enrollment_confirmation_step.underscore index 8131bca06c..55d26ed69a 100644 --- a/lms/templates/verify_student/enrollment_confirmation_step.underscore +++ b/lms/templates/verify_student/enrollment_confirmation_step.underscore @@ -13,14 +13,16 @@ <%- gettext( "Course" ) %> - + <%- gettext( "Status" ) %> <%- courseName %> - + + <%- _.sprintf( gettext( "Starts: %(start)s" ), { start: courseStartDate } ) %> + diff --git a/lms/templates/verify_student/pay_and_verify.html b/lms/templates/verify_student/pay_and_verify.html index bb91fd62c7..a60f7a4ae6 100644 --- a/lms/templates/verify_student/pay_and_verify.html +++ b/lms/templates/verify_student/pay_and_verify.html @@ -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
+<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> + DateUtilFactory.transform(iterationKey=".localized-datetime"); + diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index 82adac6150..d9cede9f58 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -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): """ diff --git a/openedx/core/djangoapps/content/course_overviews/tests.py b/openedx/core/djangoapps/content/course_overviews/tests.py index bd60498f7e..2aebf28053 100644 --- a/openedx/core/djangoapps/content/course_overviews/tests.py +++ b/openedx/core/djangoapps/content/course_overviews/tests.py @@ -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: diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 4e42b8c6ef..0d38bfe702 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -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() diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index abe4e30f05..6bfd5c1bf1 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -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']