Converts the dates on the dashboard, sidebar navigation, and important course dates to user specified time zone.

This commit is contained in:
Kevin Kim
2016-06-27 14:25:11 +00:00
parent 4bc81ac016
commit 0bf8fc4b44
21 changed files with 444 additions and 135 deletions

View File

@@ -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
)

View File

@@ -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")

View File

@@ -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(

View File

@@ -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."
},
}
)

View 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')

View 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]
)