Converts the dates on the dashboard, sidebar navigation, and important course dates to user specified time zone.
This commit is contained in:
@@ -5,7 +5,6 @@ Convenience methods for working with datetime objects
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext, ugettext
|
||||
from pytz import timezone, utc, UnknownTimeZoneError
|
||||
|
||||
@@ -63,15 +62,6 @@ def get_time_display(dtime, format_string=None, coerce_tz=None):
|
||||
return get_default_time_display(dtime)
|
||||
|
||||
|
||||
def get_formatted_time_zone(time_zone):
|
||||
"""
|
||||
Returns a formatted time zone (e.g. 'Asia/Tokyo (JST +0900)') for user account settings time zone drop down
|
||||
"""
|
||||
abbr = get_time_display(now(), '%Z', time_zone)
|
||||
offset = get_time_display(now(), '%z', time_zone)
|
||||
return "{name} ({abbr}, UTC{offset})".format(name=time_zone, abbr=abbr, offset=offset).replace("_", " ")
|
||||
|
||||
|
||||
def almost_same_datetime(dt1, dt2, allowed_delta=timedelta(minutes=1)):
|
||||
"""
|
||||
Returns true if these are w/in a minute of each other. (in case secs saved to db
|
||||
|
||||
@@ -9,8 +9,7 @@ import unittest
|
||||
import ddt
|
||||
from mock import patch
|
||||
from nose.tools import assert_equals, assert_false # pylint: disable=no-name-in-module
|
||||
from pytz import UTC
|
||||
|
||||
from pytz import utc
|
||||
from util.date_utils import (
|
||||
get_default_time_display, get_time_display, almost_same_datetime,
|
||||
strftime_localized,
|
||||
@@ -19,7 +18,7 @@ from util.date_utils import (
|
||||
|
||||
def test_get_default_time_display():
|
||||
assert_equals("", get_default_time_display(None))
|
||||
test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=UTC)
|
||||
test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=utc)
|
||||
assert_equals(
|
||||
"Mar 12, 1992 at 15:03 UTC",
|
||||
get_default_time_display(test_time))
|
||||
@@ -34,12 +33,12 @@ def test_get_dflt_time_disp_notz():
|
||||
|
||||
def test_get_time_disp_ret_empty():
|
||||
assert_equals("", get_time_display(None))
|
||||
test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=UTC)
|
||||
test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=utc)
|
||||
assert_equals("", get_time_display(test_time, ""))
|
||||
|
||||
|
||||
def test_get_time_display():
|
||||
test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=UTC)
|
||||
test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=utc)
|
||||
assert_equals("dummy text", get_time_display(test_time, 'dummy text'))
|
||||
assert_equals("Mar 12 1992", get_time_display(test_time, '%b %d %Y'))
|
||||
assert_equals("Mar 12 1992 UTC", get_time_display(test_time, '%b %d %Y %Z'))
|
||||
@@ -47,15 +46,15 @@ def test_get_time_display():
|
||||
|
||||
|
||||
def test_get_time_pass_through():
|
||||
test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=UTC)
|
||||
test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=utc)
|
||||
assert_equals("Mar 12, 1992 at 15:03 UTC", get_time_display(test_time))
|
||||
assert_equals("Mar 12, 1992 at 15:03 UTC", get_time_display(test_time, None))
|
||||
assert_equals("Mar 12, 1992 at 15:03 UTC", get_time_display(test_time, "%"))
|
||||
|
||||
|
||||
def test_get_time_display_coerce():
|
||||
test_time_standard = datetime(1992, 1, 12, 15, 3, 30, tzinfo=UTC)
|
||||
test_time_daylight = datetime(1992, 7, 12, 15, 3, 30, tzinfo=UTC)
|
||||
test_time_standard = datetime(1992, 1, 12, 15, 3, 30, tzinfo=utc)
|
||||
test_time_daylight = datetime(1992, 7, 12, 15, 3, 30, tzinfo=utc)
|
||||
assert_equals("Jan 12, 1992 at 07:03 PST",
|
||||
get_time_display(test_time_standard, None, coerce_tz="US/Pacific"))
|
||||
assert_equals("Jan 12, 1992 at 15:03 UTC",
|
||||
|
||||
@@ -10,11 +10,12 @@ from datetime import datetime, timedelta
|
||||
import dateutil.parser
|
||||
from math import exp
|
||||
|
||||
from django.utils.timezone import UTC
|
||||
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())
|
||||
DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=utc)
|
||||
|
||||
|
||||
def clean_course_key(course_key, padding_char):
|
||||
@@ -56,7 +57,7 @@ def has_course_started(start_date):
|
||||
start_date (datetime): The start datetime of the course in question.
|
||||
"""
|
||||
# TODO: This will throw if start_date is None... consider changing this behavior?
|
||||
return datetime.now(UTC()) > start_date
|
||||
return datetime.now(utc) > start_date
|
||||
|
||||
|
||||
def has_course_ended(end_date):
|
||||
@@ -68,7 +69,7 @@ def has_course_ended(end_date):
|
||||
Arguments:
|
||||
end_date (datetime): The end datetime of the course in question.
|
||||
"""
|
||||
return datetime.now(UTC()) > end_date if end_date is not None else False
|
||||
return datetime.now(utc) > end_date if end_date is not None else False
|
||||
|
||||
|
||||
def course_starts_within(start_date, look_ahead_days):
|
||||
@@ -80,7 +81,7 @@ def course_starts_within(start_date, look_ahead_days):
|
||||
start_date (datetime): The start datetime of the course in question.
|
||||
look_ahead_days (int): number of days to see in future for course start date.
|
||||
"""
|
||||
return datetime.now(UTC()) + timedelta(days=look_ahead_days) > start_date
|
||||
return datetime.now(utc) + timedelta(days=look_ahead_days) > start_date
|
||||
|
||||
|
||||
def course_start_date_is_default(start, advertised_start):
|
||||
@@ -95,30 +96,31 @@ 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, strftime_localized):
|
||||
def _datetime_to_string(date_time, format_string, time_zone, strftime_localized):
|
||||
"""
|
||||
Formats the given datetime with the given function and format string.
|
||||
|
||||
Adds UTC to the resulting string if the format is DATE_TIME or TIME.
|
||||
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
|
||||
"""
|
||||
# TODO: Is manually appending UTC really the right thing to do here? What if date_time isn't UTC?
|
||||
result = strftime_localized(date_time, format_string)
|
||||
result = strftime_localized(date_time.astimezone(time_zone), format_string)
|
||||
abbr = get_time_zone_abbr(time_zone, date_time)
|
||||
return (
|
||||
result + u" UTC" if format_string in ['DATE_TIME', 'TIME', 'DAY_AND_TIME']
|
||||
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, ugettext, strftime_localized):
|
||||
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 UTC.
|
||||
datetime in specified time zone.
|
||||
|
||||
Prefers .advertised_start, then falls back to .start.
|
||||
|
||||
@@ -126,6 +128,7 @@ def course_start_datetime_text(start_date, advertised_start, format_string, uget
|
||||
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
|
||||
@@ -138,12 +141,12 @@ def course_start_datetime_text(start_date, advertised_start, format_string, uget
|
||||
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, strftime_localized)
|
||||
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, strftime_localized)
|
||||
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
|
||||
@@ -151,7 +154,7 @@ def course_start_datetime_text(start_date, advertised_start, format_string, uget
|
||||
return _('TBD')
|
||||
|
||||
|
||||
def course_end_datetime_text(end_date, format_string, strftime_localized):
|
||||
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.
|
||||
|
||||
@@ -160,11 +163,12 @@ def course_end_datetime_text(end_date, format_string, strftime_localized):
|
||||
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, strftime_localized) if end_date is not None
|
||||
_datetime_to_string(end_date, format_string, time_zone, strftime_localized) if end_date is not None
|
||||
else ''
|
||||
)
|
||||
|
||||
@@ -220,10 +224,10 @@ def sorting_dates(start, advertised_start, announcement):
|
||||
try:
|
||||
start = dateutil.parser.parse(advertised_start)
|
||||
if start.tzinfo is None:
|
||||
start = start.replace(tzinfo=UTC())
|
||||
start = start.replace(tzinfo=utc)
|
||||
except (ValueError, AttributeError):
|
||||
start = start
|
||||
|
||||
now = datetime.now(UTC())
|
||||
now = datetime.now(utc)
|
||||
|
||||
return announcement, start, now
|
||||
|
||||
@@ -7,10 +7,10 @@ from cStringIO import StringIO
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from django.utils.timezone import UTC
|
||||
from lazy import lazy
|
||||
from lxml import etree
|
||||
from path import Path as path
|
||||
from pytz import utc
|
||||
from xblock.fields import Scope, List, String, Dict, Boolean, Integer, Float
|
||||
|
||||
from xmodule import course_metadata_utils
|
||||
@@ -106,7 +106,7 @@ class Textbook(object):
|
||||
# see if we already fetched this
|
||||
if toc_url in _cached_toc:
|
||||
(table_of_contents, timestamp) = _cached_toc[toc_url]
|
||||
age = datetime.now(UTC) - timestamp
|
||||
age = datetime.now(utc) - timestamp
|
||||
# expire every 10 minutes
|
||||
if age.seconds < 600:
|
||||
return table_of_contents
|
||||
@@ -1190,16 +1190,17 @@ 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"):
|
||||
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 UTC. Prefers .advertised_start,
|
||||
then falls back to .start
|
||||
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
|
||||
)
|
||||
@@ -1215,13 +1216,14 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
|
||||
self.advertised_start
|
||||
)
|
||||
|
||||
def end_datetime_text(self, format_string="SHORT_DATE"):
|
||||
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
|
||||
)
|
||||
|
||||
@@ -1256,7 +1258,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
|
||||
setting
|
||||
"""
|
||||
blackouts = self.get_discussion_blackout_datetimes()
|
||||
now = datetime.now(UTC())
|
||||
now = datetime.now(utc)
|
||||
for blackout in blackouts:
|
||||
if blackout["start"] <= now <= blackout["end"]:
|
||||
return False
|
||||
@@ -1384,7 +1386,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
|
||||
Returns:
|
||||
bool: False if the course has already started, True otherwise.
|
||||
"""
|
||||
return datetime.now(UTC()) <= self.start
|
||||
return datetime.now(utc) <= self.start
|
||||
|
||||
|
||||
class CourseSummary(object):
|
||||
|
||||
@@ -5,8 +5,7 @@ from collections import namedtuple
|
||||
from datetime import timedelta, datetime
|
||||
from unittest import TestCase
|
||||
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from pytz import timezone, utc
|
||||
from xmodule.block_metadata_utils import (
|
||||
url_name_for_block,
|
||||
display_name_with_default,
|
||||
@@ -31,7 +30,7 @@ from xmodule.modulestore.tests.utils import (
|
||||
)
|
||||
|
||||
|
||||
_TODAY = datetime.now(UTC())
|
||||
_TODAY = datetime.now(utc)
|
||||
_LAST_MONTH = _TODAY - timedelta(days=30)
|
||||
_LAST_WEEK = _TODAY - timedelta(days=7)
|
||||
_NEXT_WEEK = _TODAY + timedelta(days=7)
|
||||
@@ -107,14 +106,18 @@ class CourseMetadataUtilsTestCase(TestCase):
|
||||
else:
|
||||
raise ValueError("Invalid format string :" + format_string)
|
||||
|
||||
def nop_gettext(text):
|
||||
def noop_gettext(text):
|
||||
"""Dummy implementation of gettext, so we don't need Django."""
|
||||
return text
|
||||
|
||||
test_datetime = datetime(1945, 02, 06, 04, 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_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
|
||||
@@ -170,48 +173,73 @@ class CourseMetadataUtilsTestCase(TestCase):
|
||||
# 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', nop_gettext, mock_strftime_localized),
|
||||
(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', nop_gettext, mock_strftime_localized),
|
||||
(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', nop_gettext, mock_strftime_localized),
|
||||
(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', nop_gettext, mock_strftime_localized),
|
||||
(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', nop_gettext, mock_strftime_localized),
|
||||
(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', mock_strftime_localized),
|
||||
(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', mock_strftime_localized),
|
||||
(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, [
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
"""Tests the course modules and their functions"""
|
||||
import ddt
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fs.memoryfs import MemoryFS
|
||||
|
||||
from mock import Mock, patch
|
||||
import itertools
|
||||
|
||||
from fs.memoryfs import MemoryFS
|
||||
from mock import Mock, patch
|
||||
from pytz import timezone, utc
|
||||
from xblock.runtime import KvsFieldData, DictKeyValueStore
|
||||
|
||||
import xmodule.course_module
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
|
||||
ORG = 'test_org'
|
||||
COURSE = 'test_course'
|
||||
|
||||
NOW = datetime.strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00').replace(tzinfo=UTC())
|
||||
NOW = datetime.strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00').replace(tzinfo=utc)
|
||||
|
||||
_TODAY = datetime.now(UTC())
|
||||
_TODAY = datetime.now(utc)
|
||||
_LAST_WEEK = _TODAY - timedelta(days=7)
|
||||
_NEXT_WEEK = _TODAY + timedelta(days=7)
|
||||
|
||||
@@ -28,7 +28,7 @@ class CourseFieldsTestCase(unittest.TestCase):
|
||||
def test_default_start_date(self):
|
||||
self.assertEqual(
|
||||
xmodule.course_module.CourseFields.start.default,
|
||||
datetime(2030, 1, 1, tzinfo=UTC())
|
||||
datetime(2030, 1, 1, tzinfo=utc)
|
||||
)
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ class HasEndedMayCertifyTestCase(unittest.TestCase):
|
||||
self.assertFalse(self.future_noshow_certs.may_certify())
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class IsNewCourseTestCase(unittest.TestCase):
|
||||
"""Make sure the property is_new works on courses"""
|
||||
|
||||
@@ -224,6 +225,20 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
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])
|
||||
@@ -277,6 +292,20 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
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):
|
||||
|
||||
@@ -7,9 +7,10 @@ from datetime import datetime
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils.timezone import UTC
|
||||
from pytz import utc
|
||||
|
||||
from lazy import lazy
|
||||
from openedx.core.lib.time_zone_utils import get_time_zone_abbr
|
||||
from xmodule_django.models import CourseKeyField, LocationKeyField
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -72,43 +73,43 @@ class CustomCourseForEdX(models.Model):
|
||||
|
||||
def has_started(self):
|
||||
"""Return True if the CCX start date is in the past"""
|
||||
return datetime.now(UTC()) > self.start
|
||||
return datetime.now(utc) > self.start
|
||||
|
||||
def has_ended(self):
|
||||
"""Return True if the CCX due date is set and is in the past"""
|
||||
if self.due is None:
|
||||
return False
|
||||
|
||||
return datetime.now(UTC()) > self.due
|
||||
return datetime.now(utc) > self.due
|
||||
|
||||
def start_datetime_text(self, format_string="SHORT_DATE"):
|
||||
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 always expressed in UTC
|
||||
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, format_string)
|
||||
value = strftime(self.start.astimezone(time_zone), format_string)
|
||||
if format_string == 'DATE_TIME':
|
||||
value += u' UTC'
|
||||
value += ' ' + get_time_zone_abbr(time_zone, self.start)
|
||||
return value
|
||||
|
||||
def end_datetime_text(self, format_string="SHORT_DATE"):
|
||||
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 always expressed in UTC
|
||||
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, format_string)
|
||||
value = strftime(self.due.astimezone(time_zone), format_string)
|
||||
if format_string == 'DATE_TIME':
|
||||
value += u' UTC'
|
||||
value += ' ' + get_time_zone_abbr(time_zone, self.due)
|
||||
return value
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""
|
||||
tests for the models
|
||||
"""
|
||||
import ddt
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from django.utils.timezone import UTC
|
||||
from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
from pytz import timezone, utc
|
||||
from student.roles import CourseCcxCoachRole
|
||||
from student.tests.factories import (
|
||||
AdminFactory,
|
||||
@@ -23,6 +24,7 @@ from .factories import (
|
||||
from ..overrides import override_field_for_ccx
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@attr('shard_1')
|
||||
class TestCCX(ModuleStoreTestCase):
|
||||
"""Unit tests for the CustomCourseForEdX model
|
||||
@@ -65,7 +67,7 @@ class TestCCX(ModuleStoreTestCase):
|
||||
For this reason we test the difference between and make sure it is less
|
||||
than one second.
|
||||
"""
|
||||
expected = datetime.now(UTC())
|
||||
expected = datetime.now(utc)
|
||||
self.set_ccx_override('start', expected)
|
||||
actual = self.ccx.start # pylint: disable=no-member
|
||||
diff = expected - actual
|
||||
@@ -73,7 +75,7 @@ class TestCCX(ModuleStoreTestCase):
|
||||
|
||||
def test_ccx_start_caching(self):
|
||||
"""verify that caching the start property works to limit queries"""
|
||||
now = datetime.now(UTC())
|
||||
now = datetime.now(utc)
|
||||
self.set_ccx_override('start', now)
|
||||
with check_mongo_calls(1):
|
||||
# these statements are used entirely to demonstrate the
|
||||
@@ -90,7 +92,7 @@ class TestCCX(ModuleStoreTestCase):
|
||||
|
||||
def test_ccx_due_is_correct(self):
|
||||
"""verify that the due datetime for a ccx is correctly retrieved"""
|
||||
expected = datetime.now(UTC())
|
||||
expected = datetime.now(utc)
|
||||
self.set_ccx_override('due', expected)
|
||||
actual = self.ccx.due # pylint: disable=no-member
|
||||
diff = expected - actual
|
||||
@@ -98,7 +100,7 @@ class TestCCX(ModuleStoreTestCase):
|
||||
|
||||
def test_ccx_due_caching(self):
|
||||
"""verify that caching the due property works to limit queries"""
|
||||
expected = datetime.now(UTC())
|
||||
expected = datetime.now(utc)
|
||||
self.set_ccx_override('due', expected)
|
||||
with check_mongo_calls(1):
|
||||
# these statements are used entirely to demonstrate the
|
||||
@@ -110,7 +112,7 @@ class TestCCX(ModuleStoreTestCase):
|
||||
|
||||
def test_ccx_has_started(self):
|
||||
"""verify that a ccx marked as starting yesterday has started"""
|
||||
now = datetime.now(UTC())
|
||||
now = datetime.now(utc)
|
||||
delta = timedelta(1)
|
||||
then = now - delta
|
||||
self.set_ccx_override('start', then)
|
||||
@@ -118,7 +120,7 @@ class TestCCX(ModuleStoreTestCase):
|
||||
|
||||
def test_ccx_has_not_started(self):
|
||||
"""verify that a ccx marked as starting tomorrow has not started"""
|
||||
now = datetime.now(UTC())
|
||||
now = datetime.now(utc)
|
||||
delta = timedelta(1)
|
||||
then = now + delta
|
||||
self.set_ccx_override('start', then)
|
||||
@@ -126,7 +128,7 @@ class TestCCX(ModuleStoreTestCase):
|
||||
|
||||
def test_ccx_has_ended(self):
|
||||
"""verify that a ccx that has a due date in the past has ended"""
|
||||
now = datetime.now(UTC())
|
||||
now = datetime.now(utc)
|
||||
delta = timedelta(1)
|
||||
then = now - delta
|
||||
self.set_ccx_override('due', then)
|
||||
@@ -135,7 +137,7 @@ class TestCCX(ModuleStoreTestCase):
|
||||
def test_ccx_has_not_ended(self):
|
||||
"""verify that a ccx that has a due date in the future has not eneded
|
||||
"""
|
||||
now = datetime.now(UTC())
|
||||
now = datetime.now(utc)
|
||||
delta = timedelta(1)
|
||||
then = now + delta
|
||||
self.set_ccx_override('due', then)
|
||||
@@ -152,7 +154,7 @@ class TestCCX(ModuleStoreTestCase):
|
||||
}))
|
||||
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())
|
||||
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
|
||||
@@ -163,18 +165,34 @@ class TestCCX(ModuleStoreTestCase):
|
||||
}))
|
||||
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())
|
||||
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())
|
||||
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
|
||||
@@ -185,12 +203,28 @@ class TestCCX(ModuleStoreTestCase):
|
||||
}))
|
||||
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())
|
||||
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",
|
||||
}))
|
||||
|
||||
@@ -12,11 +12,12 @@ from django.utils.translation import ugettext_lazy
|
||||
from django.utils.translation import to_locale, get_language
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from lazy import lazy
|
||||
import pytz
|
||||
from pytz import utc
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification
|
||||
from openedx.core.lib.time_zone_utils import get_time_zone_abbr, get_user_time_zone
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
|
||||
@@ -64,6 +65,11 @@ class DateSummary(object):
|
||||
"""The text of the link."""
|
||||
return ''
|
||||
|
||||
@property
|
||||
def time_zone(self):
|
||||
"""The time zone to display in"""
|
||||
return get_user_time_zone(self.user)
|
||||
|
||||
def __init__(self, course, user):
|
||||
self.course = course
|
||||
self.user = user
|
||||
@@ -93,7 +99,7 @@ class DateSummary(object):
|
||||
if self.date is None:
|
||||
return ''
|
||||
locale = to_locale(get_language())
|
||||
delta = self.date - datetime.now(pytz.UTC)
|
||||
delta = self.date - datetime.now(utc)
|
||||
try:
|
||||
relative_date = format_timedelta(delta, locale=locale)
|
||||
# Babel doesn't have translations for Esperanto, so we get
|
||||
@@ -111,7 +117,7 @@ class DateSummary(object):
|
||||
date_format = _(u"{relative} ago - {absolute}") if date_has_passed else _(u"in {relative} - {absolute}")
|
||||
return date_format.format(
|
||||
relative=relative_date,
|
||||
absolute=self.date.strftime(self.date_format.encode('utf-8')).decode('utf-8'),
|
||||
absolute=self.date.astimezone(self.time_zone).strftime(self.date_format.encode('utf-8')).decode('utf-8'),
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -123,7 +129,7 @@ class DateSummary(object):
|
||||
future.
|
||||
"""
|
||||
if self.date is not None:
|
||||
return datetime.now(pytz.UTC) <= self.date
|
||||
return datetime.now(utc) <= self.date
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
@@ -143,7 +149,7 @@ class TodaysDate(DateSummary):
|
||||
|
||||
@property
|
||||
def date_format(self):
|
||||
return u'%b %d, %Y (%H:%M {utc})'.format(utc=_('UTC'))
|
||||
return u'%b %d, %Y (%H:%M {tz_abbr})'.format(tz_abbr=get_time_zone_abbr(self.time_zone))
|
||||
|
||||
# The date is shown in the title, no need to display it again.
|
||||
def get_context(self):
|
||||
@@ -153,12 +159,12 @@ class TodaysDate(DateSummary):
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
return datetime.now(pytz.UTC)
|
||||
return datetime.now(utc)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return _(u'Today is {date}').format(
|
||||
date=datetime.now(pytz.UTC).strftime(self.date_format.encode('utf-8')).decode('utf-8')
|
||||
date=self.date.astimezone(self.time_zone).strftime(self.date_format.encode('utf-8')).decode('utf-8')
|
||||
)
|
||||
|
||||
|
||||
@@ -187,7 +193,7 @@ class CourseEndDate(DateSummary):
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
if datetime.now(pytz.UTC) <= self.date:
|
||||
if datetime.now(utc) <= self.date:
|
||||
mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
|
||||
if is_active and CourseMode.is_eligible_for_certificate(mode):
|
||||
return _('To earn a certificate, you must complete all requirements before this date.')
|
||||
@@ -332,7 +338,7 @@ class VerificationDeadlineDate(DateSummary):
|
||||
Return True if a verification deadline exists, and has already passed.
|
||||
"""
|
||||
deadline = self.date
|
||||
return deadline is not None and deadline <= datetime.now(pytz.UTC)
|
||||
return deadline is not None and deadline <= datetime.now(utc)
|
||||
|
||||
def must_retry(self):
|
||||
"""Return True if the user must re-submit verification, False otherwise."""
|
||||
|
||||
@@ -4,9 +4,9 @@ from datetime import datetime, timedelta
|
||||
|
||||
import ddt
|
||||
from django.core.urlresolvers import reverse
|
||||
import freezegun
|
||||
from freezegun import freeze_time
|
||||
from nose.plugins.attrib import attr
|
||||
import pytz
|
||||
from pytz import utc
|
||||
|
||||
from commerce.models import CommerceConfiguration
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
@@ -21,6 +21,7 @@ from courseware.date_summary import (
|
||||
VerifiedUpgradeDeadlineDate,
|
||||
)
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
|
||||
@@ -50,7 +51,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
sku=None
|
||||
):
|
||||
"""Set up the course and user for this test."""
|
||||
now = datetime.now(pytz.UTC)
|
||||
now = datetime.now(utc)
|
||||
self.course = CourseFactory.create( # pylint: disable=attribute-defined-outside-init
|
||||
start=now + timedelta(days=days_till_start)
|
||||
)
|
||||
@@ -175,21 +176,49 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
|
||||
## TodaysDate
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
def test_todays_date(self):
|
||||
def _today_date_helper(self, expected_display_date):
|
||||
"""
|
||||
Helper function to test that today's date block renders correctly
|
||||
and displays the correct time, accounting for daylight savings
|
||||
"""
|
||||
self.setup_course_and_user()
|
||||
set_user_preference(self.user, "time_zone", "America/Los_Angeles")
|
||||
block = TodaysDate(self.course, self.user)
|
||||
self.assertTrue(block.is_enabled)
|
||||
self.assertEqual(block.date, datetime.now(pytz.UTC))
|
||||
self.assertEqual(block.title, 'Today is Jan 02, 2015 (00:00 UTC)')
|
||||
self.assertEqual(block.date, datetime.now(utc))
|
||||
self.assertEqual(block.title, 'Today is {date}'.format(date=expected_display_date))
|
||||
self.assertNotIn('date-summary-date', block.render())
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
@freeze_time('2015-11-01 08:59:00')
|
||||
def test_todays_date_time_zone_daylight(self):
|
||||
"""
|
||||
Test today's date block displays correctly during
|
||||
daylight savings hours
|
||||
"""
|
||||
self._today_date_helper('Nov 01, 2015 (01:59 PDT)')
|
||||
|
||||
@freeze_time('2015-11-01 09:00:00')
|
||||
def test_todays_date_time_zone_normal(self):
|
||||
"""
|
||||
Test today's date block displays correctly during
|
||||
normal daylight hours
|
||||
"""
|
||||
self._today_date_helper('Nov 01, 2015 (01:00 PST)')
|
||||
|
||||
@freeze_time('2015-01-02')
|
||||
def test_todays_date_render(self):
|
||||
self.setup_course_and_user()
|
||||
block = TodaysDate(self.course, self.user)
|
||||
self.assertIn('Jan 02, 2015', block.render())
|
||||
|
||||
@freeze_time('2015-01-02')
|
||||
def test_todays_date_render_time_zone(self):
|
||||
self.setup_course_and_user()
|
||||
set_user_preference(self.user, "time_zone", "America/Los_Angeles")
|
||||
block = TodaysDate(self.course, self.user)
|
||||
# Today is 'Jan 01, 2015' because of time zone offset
|
||||
self.assertIn('Jan 01, 2015', block.render())
|
||||
|
||||
## CourseStartDate
|
||||
|
||||
def test_course_start_date(self):
|
||||
@@ -197,12 +226,20 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
block = CourseStartDate(self.course, self.user)
|
||||
self.assertEqual(block.date, self.course.start)
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
@freeze_time('2015-01-02')
|
||||
def test_start_date_render(self):
|
||||
self.setup_course_and_user()
|
||||
block = CourseStartDate(self.course, self.user)
|
||||
self.assertIn('in 1 day - Jan 03, 2015', block.render())
|
||||
|
||||
@freeze_time('2015-01-02')
|
||||
def test_start_date_render_time_zone(self):
|
||||
self.setup_course_and_user()
|
||||
set_user_preference(self.user, "time_zone", "America/Los_Angeles")
|
||||
block = CourseStartDate(self.course, self.user)
|
||||
# Jan 02 is in 1 day because of time zone offset
|
||||
self.assertIn('in 1 day - Jan 02, 2015', block.render())
|
||||
|
||||
## CourseEndDate
|
||||
|
||||
def test_course_end_date_for_certificate_eligible_mode(self):
|
||||
@@ -231,11 +268,11 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
|
||||
## VerifiedUpgradeDeadlineDate
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
@freeze_time('2015-01-02')
|
||||
def test_verified_upgrade_deadline_date(self):
|
||||
self.setup_course_and_user(days_till_upgrade_deadline=1)
|
||||
block = VerifiedUpgradeDeadlineDate(self.course, self.user)
|
||||
self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=1))
|
||||
self.assertEqual(block.date, datetime.now(utc) + timedelta(days=1))
|
||||
self.assertEqual(block.link, reverse('verify_student_upgrade_and_verify', args=(self.course.id,)))
|
||||
|
||||
def test_without_upgrade_deadline(self):
|
||||
@@ -267,13 +304,13 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertFalse(block.is_enabled)
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
@freeze_time('2015-01-02')
|
||||
def test_verification_deadline_date_upcoming(self):
|
||||
self.setup_course_and_user(days_till_start=-1)
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertEqual(block.css_class, 'verification-deadline-upcoming')
|
||||
self.assertEqual(block.title, 'Verification Deadline')
|
||||
self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=14))
|
||||
self.assertEqual(block.date, datetime.now(utc) + timedelta(days=14))
|
||||
self.assertEqual(
|
||||
block.description,
|
||||
'You must successfully complete verification before this date to qualify for a Verified Certificate.'
|
||||
@@ -281,13 +318,13 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
self.assertEqual(block.link_text, 'Verify My Identity')
|
||||
self.assertEqual(block.link, reverse('verify_student_verify_now', args=(self.course.id,)))
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
@freeze_time('2015-01-02')
|
||||
def test_verification_deadline_date_retry(self):
|
||||
self.setup_course_and_user(days_till_start=-1, verification_status='denied')
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertEqual(block.css_class, 'verification-deadline-retry')
|
||||
self.assertEqual(block.title, 'Verification Deadline')
|
||||
self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=14))
|
||||
self.assertEqual(block.date, datetime.now(utc) + timedelta(days=14))
|
||||
self.assertEqual(
|
||||
block.description,
|
||||
'You must successfully complete verification before this date to qualify for a Verified Certificate.'
|
||||
@@ -295,7 +332,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
self.assertEqual(block.link_text, 'Retry Verification')
|
||||
self.assertEqual(block.link, reverse('verify_student_reverify'))
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
@freeze_time('2015-01-02')
|
||||
def test_verification_deadline_date_denied(self):
|
||||
self.setup_course_and_user(
|
||||
days_till_start=-10,
|
||||
@@ -305,7 +342,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertEqual(block.css_class, 'verification-deadline-passed')
|
||||
self.assertEqual(block.title, 'Missed Verification Deadline')
|
||||
self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=-1))
|
||||
self.assertEqual(block.date, datetime.now(utc) + timedelta(days=-1))
|
||||
self.assertEqual(
|
||||
block.description,
|
||||
"Unfortunately you missed this course's deadline for a successful verification."
|
||||
@@ -313,7 +350,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
self.assertEqual(block.link_text, 'Learn More')
|
||||
self.assertEqual(block.link, '')
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
@freeze_time('2015-01-02')
|
||||
@ddt.data(
|
||||
(-1, '1 day ago - Jan 01, 2015'),
|
||||
(1, 'in 1 day - Jan 03, 2015')
|
||||
@@ -327,3 +364,20 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
)
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertEqual(block.get_context()['date'], expected_date_string)
|
||||
|
||||
@freeze_time('2015-01-02')
|
||||
@ddt.data(
|
||||
# dates reflected from Jan 01, 2015 because of time zone offset
|
||||
(-1, '1 day ago - Dec 31, 2014'),
|
||||
(1, 'in 1 day - Jan 02, 2015')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_render_date_string_time_zone(self, delta, expected_date_string):
|
||||
self.setup_course_and_user(
|
||||
days_till_start=-10,
|
||||
verification_status='denied',
|
||||
days_till_verification_deadline=delta,
|
||||
)
|
||||
set_user_preference(self.user, "time_zone", "America/Los_Angeles")
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertEqual(block.get_context()['date'], expected_date_string)
|
||||
|
||||
@@ -26,6 +26,7 @@ from lang_pref import LANGUAGE_KEY
|
||||
from xblock.fragment import Fragment
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.lib.gating import api as gating_api
|
||||
from openedx.core.lib.time_zone_utils import get_user_time_zone
|
||||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
|
||||
from shoppingcart.models import CourseRegistrationCode
|
||||
from student.models import CourseEnrollment
|
||||
@@ -514,6 +515,7 @@ def render_accordion(request, course, table_of_contents):
|
||||
('course_id', unicode(course.id)),
|
||||
('csrf', csrf(request)['csrf_token']),
|
||||
('due_date_display_format', course.due_date_display_format),
|
||||
('time_zone', get_user_time_zone(request.user).zone),
|
||||
] + TEMPLATE_IMPORTS.items()
|
||||
)
|
||||
return render_to_string('courseware/accordion.html', context)
|
||||
|
||||
@@ -31,7 +31,7 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site, get_value as get_themed_value
|
||||
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
|
||||
from openedx.core.djangoapps.user_api.errors import UserNotFound
|
||||
from openedx.core.djangoapps.user_api.models import UserPreference
|
||||
from openedx.core.lib.time_zone_utils import TIME_ZONE_CHOICES
|
||||
from openedx.core.lib.edx_api_utils import get_edx_api_data
|
||||
from student.models import UserProfile
|
||||
from student.views import (
|
||||
@@ -449,7 +449,7 @@ def account_settings_context(request):
|
||||
}, 'preferred_language': {
|
||||
'options': all_languages(),
|
||||
}, 'time_zone': {
|
||||
'options': UserPreference.TIME_ZONE_CHOICES,
|
||||
'options': TIME_ZONE_CHOICES,
|
||||
'enabled': settings.FEATURES.get('ENABLE_TIME_ZONE_PREFERENCE'),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -129,6 +129,10 @@
|
||||
color: $alert-color;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle-name {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
|
||||
@@ -36,7 +36,7 @@ else:
|
||||
if section.get('due') is None:
|
||||
due_date = ''
|
||||
else:
|
||||
formatted_string = get_time_display(section['due'], due_date_display_format, coerce_tz=settings.TIME_ZONE_DISPLAYED_FOR_DEADLINES)
|
||||
formatted_string = get_time_display(section['due'], due_date_display_format, coerce_tz=time_zone)
|
||||
due_date = '' if len(formatted_string)==0 else _('due {date}').format(date=formatted_string)
|
||||
%>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from course_modes.models import CourseMode
|
||||
from course_modes.helpers import enrollment_mode_display
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.core.lib.time_zone_utils import get_user_time_zone
|
||||
from student.helpers import (
|
||||
VERIFY_STATUS_NEED_TO_VERIFY,
|
||||
VERIFY_STATUS_SUBMITTED,
|
||||
@@ -103,16 +104,17 @@ from student.helpers import (
|
||||
<span class="info-university">${course_overview.display_org_with_default} - </span>
|
||||
<span class="info-course-id">${course_overview.display_number_with_default}</span>
|
||||
<span class="info-date-block" data-tooltip="Hi">
|
||||
<% time_zone = get_user_time_zone(user) %>
|
||||
% if course_overview.has_ended():
|
||||
${_("Ended - {end_date}").format(end_date=course_overview.end_datetime_text("SHORT_DATE"))}
|
||||
${_("Ended - {end_date}").format(end_date=course_overview.end_datetime_text("SHORT_DATE", time_zone))}
|
||||
% elif course_overview.has_started():
|
||||
${_("Started - {start_date}").format(start_date=course_overview.start_datetime_text("SHORT_DATE"))}
|
||||
${_("Started - {start_date}").format(start_date=course_overview.start_datetime_text("SHORT_DATE", time_zone))}
|
||||
% elif course_overview.start_date_is_still_default: # Course start date TBD
|
||||
${_("Coming Soon")}
|
||||
% elif course_overview.starts_within(days=5): # hasn't started yet
|
||||
${_("Starts - {start_date}").format(start_date=course_overview.start_datetime_text("DAY_AND_TIME"))}
|
||||
${_("Starts - {start_date}").format(start_date=course_overview.start_datetime_text("DAY_AND_TIME", time_zone))}
|
||||
% else: # hasn't started yet
|
||||
${_("Starts - {start_date}").format(start_date=course_overview.start_datetime_text("SHORT_DATE"))}
|
||||
${_("Starts - {start_date}").format(start_date=course_overview.start_datetime_text("SHORT_DATE", time_zone))}
|
||||
% endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ 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
|
||||
@@ -359,15 +360,17 @@ class CourseOverview(TimeStampedModel):
|
||||
|
||||
return course_metadata_utils.course_starts_within(self.start, days)
|
||||
|
||||
def start_datetime_text(self, format_string="SHORT_DATE"):
|
||||
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 UTC. Prefers .advertised_start, then falls back to .start.
|
||||
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
|
||||
)
|
||||
@@ -383,13 +386,14 @@ class CourseOverview(TimeStampedModel):
|
||||
self.advertised_start,
|
||||
)
|
||||
|
||||
def end_datetime_text(self, format_string="SHORT_DATE"):
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@@ -8,9 +8,6 @@ from django.db.models.signals import post_delete, pre_save, post_save
|
||||
from django.dispatch import receiver
|
||||
from model_utils.models import TimeStampedModel
|
||||
|
||||
from pytz import common_timezones
|
||||
from util.date_utils import get_formatted_time_zone
|
||||
|
||||
from util.model_utils import get_changed_fields_dict, emit_setting_changed_event
|
||||
from xmodule_django.models import CourseKeyField
|
||||
|
||||
@@ -30,10 +27,6 @@ class UserPreference(models.Model):
|
||||
key = models.CharField(max_length=255, db_index=True, validators=[RegexValidator(KEY_REGEX)])
|
||||
value = models.TextField()
|
||||
|
||||
TIME_ZONE_CHOICES = [
|
||||
(tz, get_formatted_time_zone(tz)) for tz in common_timezones
|
||||
]
|
||||
|
||||
class Meta(object):
|
||||
unique_together = ("user", "key")
|
||||
|
||||
|
||||
@@ -396,7 +396,7 @@ def validate_user_preference_serializer(serializer, preference_key, preference_v
|
||||
})
|
||||
if preference_key == "time_zone" and preference_value not in common_timezones_set:
|
||||
developer_message = ugettext_noop(u"Value '{preference_value}' not valid for preference '{preference_key}': Not in timezone set.") # pylint: disable=line-too-long
|
||||
user_message = ugettext_noop(u"Value '{preference_value}' is not valid for user preference '{preference_key}'.")
|
||||
user_message = ugettext_noop(u"Value '{preference_value}' is not a valid time zone selection.")
|
||||
raise PreferenceValidationError({
|
||||
preference_key: {
|
||||
"developer_message": developer_message.format(
|
||||
|
||||
@@ -248,7 +248,7 @@ class TestPreferencesAPI(UserAPITestCase):
|
||||
"time_zone": {
|
||||
"developer_message": u"Value 'Asia/Africa' not valid for preference 'time_zone': Not in "
|
||||
u"timezone set.",
|
||||
"user_message": u"Value 'Asia/Africa' is not valid for user preference 'time_zone'."
|
||||
"user_message": u"Value 'Asia/Africa' is not a valid time zone selection."
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
94
openedx/core/lib/tests/test_time_zone_utils.py
Normal file
94
openedx/core/lib/tests/test_time_zone_utils.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Tests covering time zone utilities."""
|
||||
from freezegun import freeze_time
|
||||
from student.tests.factories import UserFactory
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
from openedx.core.lib.time_zone_utils import (
|
||||
get_formatted_time_zone, get_time_zone_abbr, get_time_zone_offset, get_user_time_zone
|
||||
)
|
||||
from pytz import timezone, utc
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
class TestTimeZoneUtils(TestCase):
|
||||
"""
|
||||
Tests the time zone utilities
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Sets up user for testing with time zone utils.
|
||||
"""
|
||||
super(TestTimeZoneUtils, self).setUp()
|
||||
|
||||
self.user = UserFactory.build()
|
||||
self.user.save()
|
||||
|
||||
def test_get_user_time_zone(self):
|
||||
"""
|
||||
Test to ensure get_user_time_zone() returns the correct time zone
|
||||
or UTC if user has not specified time zone.
|
||||
"""
|
||||
# User time zone should be UTC when no time zone has been chosen
|
||||
user_tz = get_user_time_zone(self.user)
|
||||
self.assertEqual(user_tz, utc)
|
||||
|
||||
# User time zone should change when user specifies time zone
|
||||
set_user_preference(self.user, 'time_zone', 'Asia/Tokyo')
|
||||
user_tz = get_user_time_zone(self.user)
|
||||
self.assertEqual(user_tz, timezone('Asia/Tokyo'))
|
||||
|
||||
def _formatted_time_zone_helper(self, time_zone_string):
|
||||
"""
|
||||
Helper function to return all info from get_formatted_time_zone()
|
||||
"""
|
||||
time_zone = timezone(time_zone_string)
|
||||
tz_str = get_formatted_time_zone(time_zone)
|
||||
tz_abbr = get_time_zone_abbr(time_zone)
|
||||
tz_offset = get_time_zone_offset(time_zone)
|
||||
|
||||
return {'str': tz_str, 'abbr': tz_abbr, 'offset': tz_offset}
|
||||
|
||||
def _assert_time_zone_info_equal(self, formatted_tz_info, expected_name, expected_abbr, expected_offset):
|
||||
"""
|
||||
Asserts that all formatted_tz_info is equal to the expected inputs
|
||||
"""
|
||||
self.assertEqual(formatted_tz_info['str'], '{name} ({abbr}, UTC{offset})'.format(name=expected_name,
|
||||
abbr=expected_abbr,
|
||||
offset=expected_offset))
|
||||
self.assertEqual(formatted_tz_info['abbr'], expected_abbr)
|
||||
self.assertEqual(formatted_tz_info['offset'], expected_offset)
|
||||
|
||||
@freeze_time("2015-02-09")
|
||||
def test_formatted_time_zone_without_dst(self):
|
||||
"""
|
||||
Test to ensure get_formatted_time_zone() returns full formatted string when no kwargs specified
|
||||
and returns just abbreviation or offset when specified
|
||||
"""
|
||||
tz_info = self._formatted_time_zone_helper('America/Los_Angeles')
|
||||
self._assert_time_zone_info_equal(tz_info, 'America/Los Angeles', 'PST', '-0800')
|
||||
|
||||
@freeze_time("2015-04-02")
|
||||
def test_formatted_time_zone_with_dst(self):
|
||||
"""
|
||||
Test to ensure get_formatted_time_zone() returns modified abbreviations and
|
||||
offsets during daylight savings time.
|
||||
"""
|
||||
tz_info = self._formatted_time_zone_helper('America/Los_Angeles')
|
||||
self._assert_time_zone_info_equal(tz_info, 'America/Los Angeles', 'PDT', '-0700')
|
||||
|
||||
@freeze_time("2015-11-01 08:59:00")
|
||||
def test_formatted_time_zone_ambiguous_before(self):
|
||||
"""
|
||||
Test to ensure get_formatted_time_zone() returns correct abbreviations and offsets
|
||||
during ambiguous time periods (e.g. when DST is about to start/end) before the change
|
||||
"""
|
||||
tz_info = self._formatted_time_zone_helper('America/Los_Angeles')
|
||||
self._assert_time_zone_info_equal(tz_info, 'America/Los Angeles', 'PDT', '-0700')
|
||||
|
||||
@freeze_time("2015-11-01 09:00:00")
|
||||
def test_formatted_time_zone_ambiguous_after(self):
|
||||
"""
|
||||
Test to ensure get_formatted_time_zone() returns correct abbreviations and offsets
|
||||
during ambiguous time periods (e.g. when DST is about to start/end) after the change
|
||||
"""
|
||||
tz_info = self._formatted_time_zone_helper('America/Los_Angeles')
|
||||
self._assert_time_zone_info_equal(tz_info, 'America/Los Angeles', 'PST', '-0800')
|
||||
63
openedx/core/lib/time_zone_utils.py
Normal file
63
openedx/core/lib/time_zone_utils.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Utilities related to timezones
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from pytz import common_timezones, timezone, utc
|
||||
|
||||
|
||||
def get_user_time_zone(user):
|
||||
"""
|
||||
Returns pytz time zone object of the user's time zone if available or UTC time zone if unavailable
|
||||
"""
|
||||
#TODO: exception for unknown timezones?
|
||||
time_zone = user.preferences.model.get_value(user, "time_zone")
|
||||
if time_zone is not None:
|
||||
return timezone(time_zone)
|
||||
return utc
|
||||
|
||||
|
||||
def _format_time_zone_string(time_zone, date_time, format_string):
|
||||
"""
|
||||
Returns a string, specified by format string, of the current date/time of the time zone.
|
||||
|
||||
:param time_zone: Pytz time zone object
|
||||
:param date_time: datetime object of date to convert
|
||||
:param format_string: A list of format codes can be found at:
|
||||
https://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
|
||||
"""
|
||||
return date_time.astimezone(time_zone).strftime(format_string)
|
||||
|
||||
|
||||
def get_time_zone_abbr(time_zone, date_time=None):
|
||||
"""
|
||||
Returns the time zone abbreviation (e.g. EST) of the time zone for given datetime
|
||||
"""
|
||||
date_time = datetime.now(utc) if date_time is None else date_time
|
||||
return _format_time_zone_string(time_zone, date_time, '%Z')
|
||||
|
||||
|
||||
def get_time_zone_offset(time_zone, date_time=None):
|
||||
"""
|
||||
Returns the time zone offset (e.g. -0800) of the time zone for given datetime
|
||||
"""
|
||||
date_time = datetime.now(utc) if date_time is None else date_time
|
||||
return _format_time_zone_string(time_zone, date_time, '%z')
|
||||
|
||||
|
||||
def get_formatted_time_zone(time_zone):
|
||||
"""
|
||||
Returns a formatted time zone (e.g. 'Asia/Tokyo (JST, UTC+0900)')
|
||||
|
||||
:param time_zone: Pytz time zone object
|
||||
"""
|
||||
tz_abbr = get_time_zone_abbr(time_zone)
|
||||
tz_offset = get_time_zone_offset(time_zone)
|
||||
|
||||
return "{name} ({abbr}, UTC{offset})".format(name=time_zone, abbr=tz_abbr, offset=tz_offset).replace("_", " ")
|
||||
|
||||
|
||||
TIME_ZONE_CHOICES = sorted(
|
||||
[(tz, get_formatted_time_zone(timezone(tz))) for tz in common_timezones],
|
||||
key=lambda tz_tuple: tz_tuple[1]
|
||||
)
|
||||
Reference in New Issue
Block a user