Commit 82c612f1 by Kevin Kim Committed by GitHub

Merge pull request #12888 from edx/kkim/convert_date_tz

Convert Dates for Time Zone
parents 4bc81ac0 0bf8fc4b
......@@ -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, [
......
"""Tests the course modules and their functions"""
import ddt
import unittest
from datetime import datetime, timedelta
import itertools
from fs.memoryfs import MemoryFS
from mock import Mock, patch
import itertools
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
......
"""
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."
},
}
)
......
"""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')
"""
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]
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment