Converts the dates on the dashboard, sidebar navigation, and important course dates to user specified time zone.
This commit is contained in:
@@ -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