Commit 127095b9 by Kevin Kim

Add endpoint for commonly used country time zones

parent 194c5096
......@@ -93,3 +93,8 @@ class PreferenceUpdateError(PreferenceRequestError):
def __init__(self, developer_message, user_message=None):
self.developer_message = developer_message
self.user_message = user_message
class CountryCodeError(ValueError):
"""There was a problem with the country code"""
pass
......@@ -29,6 +29,10 @@ urlpatterns = patterns(
user_api_views.UpdateEmailOptInPreference.as_view(),
name="preferences_email_opt_in"
),
url(
r'^v1/preferences/time_zones/$',
user_api_views.CountryTimeZoneListView.as_view(),
),
)
if settings.FEATURES.get('ENABLE_COMBINED_LOGIN_REGISTRATION'):
......
......@@ -7,22 +7,22 @@ from eventtracking import tracker
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django_countries import countries
from django.db import IntegrityError
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_noop
from pytz import common_timezones, common_timezones_set, country_timezones
from student.models import User, UserProfile
from request_cache import get_request_or_stub
from ..errors import (
UserAPIInternalError, UserAPIRequestError, UserNotFound, UserNotAuthorized,
PreferenceValidationError, PreferenceUpdateError
PreferenceValidationError, PreferenceUpdateError, CountryCodeError
)
from ..helpers import intercept_errors
from ..models import UserOrgTag, UserPreference
from ..serializers import UserSerializer, RawUserPreferenceSerializer
from pytz import common_timezones_set
log = logging.getLogger(__name__)
......@@ -417,3 +417,21 @@ def _create_preference_update_error(preference_key, preference_value, error):
key=preference_key, value=preference_value
),
)
def get_country_time_zones(country_code=None):
"""
Returns a list of time zones commonly used in given country
or list of all time zones, if country code is None.
Arguments:
country_code (str): ISO 3166-1 Alpha-2 country code
Raises:
CountryCodeError: the given country code is invalid
"""
if country_code is None:
return common_timezones
if country_code.upper() in set(countries.alt_codes):
return country_timezones(country_code)
raise CountryCodeError
......@@ -7,7 +7,7 @@ import ddt
import unittest
from mock import patch
from nose.plugins.attrib import attr
from pytz import UTC
from pytz import common_timezones, utc
from django.conf import settings
from django.contrib.auth.models import User
......@@ -21,11 +21,22 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from ...accounts.api import create_account
from ...errors import UserNotFound, UserNotAuthorized, PreferenceValidationError, PreferenceUpdateError
from ...errors import (
UserNotFound,
UserNotAuthorized,
PreferenceValidationError,
PreferenceUpdateError,
CountryCodeError,
)
from ...models import UserProfile, UserOrgTag
from ...preferences.api import (
get_user_preference, get_user_preferences, set_user_preference, update_user_preferences, delete_user_preference,
update_email_opt_in
get_user_preference,
get_user_preferences,
set_user_preference,
update_user_preferences,
delete_user_preference,
update_email_opt_in,
get_country_time_zones,
)
......@@ -407,7 +418,7 @@ class UpdateEmailOptInTests(ModuleStoreTestCase):
# Set year of birth
user = User.objects.get(username=self.USERNAME)
profile = UserProfile.objects.get(user=user)
year_of_birth = datetime.datetime.now(UTC).year - age
year_of_birth = datetime.datetime.now(utc).year - age
profile.year_of_birth = year_of_birth
profile.save()
......@@ -431,6 +442,26 @@ class UpdateEmailOptInTests(ModuleStoreTestCase):
return True
@ddt.ddt
class CountryTimeZoneTest(TestCase):
"""
Test cases to validate country code api functionality
"""
@ddt.data(('NZ', ['Pacific/Auckland', 'Pacific/Chatham']),
(None, common_timezones))
@ddt.unpack
def test_get_country_time_zones(self, country_code, expected_time_zones):
"""Verify that list of common country time zones are returned"""
country_time_zones = get_country_time_zones(country_code)
self.assertEqual(country_time_zones, expected_time_zones)
def test_country_code_errors(self):
"""Verify that country code error is raised for invalid country code"""
with self.assertRaises(CountryCodeError):
get_country_time_zones('AA')
def get_expected_validation_developer_message(preference_key, preference_value):
"""
Returns the expected dict of validation messages for the specified key.
......
......@@ -3,6 +3,8 @@ Django REST Framework serializers for the User API application
"""
from django.contrib.auth.models import User
from rest_framework import serializers
from openedx.core.lib.time_zone_utils import get_display_time_zone
from student.models import UserProfile
from .models import UserPreference
......@@ -81,3 +83,23 @@ class ReadOnlyFieldsSerializerMixin(object):
"""
all_fields = getattr(cls.Meta, 'fields', tuple())
return tuple(set(all_fields) - set(cls.get_read_only_fields()))
class CountryTimeZoneSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer that generates a list of common time zones for a country
"""
time_zone = serializers.SerializerMethodField()
description = serializers.SerializerMethodField()
def get_time_zone(self, time_zone_name):
"""
Returns inputted time zone name
"""
return time_zone_name
def get_description(self, time_zone_name):
"""
Returns the display version of time zone [e.g. US/Pacific (PST, UTC-0800)]
"""
return get_display_time_zone(time_zone_name)
......@@ -15,11 +15,12 @@ from django.test.client import RequestFactory
from django.test.testcases import TransactionTestCase
from django.test.utils import override_settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from pytz import UTC
from pytz import common_timezones_set, UTC
from social.apps.django_app.default.models import UserSocialAuth
from django_comment_common import models
from openedx.core.lib.api.test_utils import ApiTestCase, TEST_API_KEY
from openedx.core.lib.time_zone_utils import get_display_time_zone
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from student.tests.factories import UserFactory
from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin
......@@ -1963,3 +1964,40 @@ class UpdateEmailOptInTestCase(UserAPITestCase, SharedModuleStoreTestCase):
self.assertHttpBadRequest(response)
with self.assertRaises(UserOrgTag.DoesNotExist):
UserOrgTag.objects.get(user=self.user, org=self.course.id.org, key="email-optin")
@ddt.ddt
class CountryTimeZoneListViewTest(UserApiTestCase):
"""
Test cases covering the list viewing behavior for country time zones
"""
ALL_TIME_ZONES_URI = "/user_api/v1/preferences/time_zones/"
COUNTRY_TIME_ZONES_URI = "/user_api/v1/preferences/time_zones/?country_code=cA"
@ddt.data(ALL_TIME_ZONES_URI, COUNTRY_TIME_ZONES_URI)
def test_options(self, country_uri):
""" Verify that following options are allowed """
self.assertAllowedMethods(country_uri, ['OPTIONS', 'GET', 'HEAD'])
@ddt.data(ALL_TIME_ZONES_URI, COUNTRY_TIME_ZONES_URI)
def test_methods_not_allowed(self, country_uri):
""" Verify that put, patch, and delete are not allowed """
unallowed_methods = ['put', 'patch', 'delete']
for unallowed_method in unallowed_methods:
self.assertHttpMethodNotAllowed(self.request_with_auth(unallowed_method, country_uri))
def _assert_time_zone_is_valid(self, time_zone_info):
""" Asserts that the time zone is a valid pytz time zone """
time_zone_name = time_zone_info['time_zone']
self.assertIn(time_zone_name, common_timezones_set)
self.assertEqual(time_zone_info['description'], get_display_time_zone(time_zone_name))
@ddt.data((ALL_TIME_ZONES_URI, 432),
(COUNTRY_TIME_ZONES_URI, 27))
@ddt.unpack
def test_get_basic(self, country_uri, expected_count):
""" Verify that correct time zone info is returned """
results = self.get_json(country_uri)
self.assertEqual(len(results), expected_count)
for time_zone_info in results:
self._assert_time_zone_is_valid(time_zone_info)
......@@ -31,7 +31,7 @@ from student.cookies import set_logged_in_cookies
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.lib.api.authentication import SessionAuthenticationAllowInactiveUser
from util.json_request import JsonResponse
from .preferences.api import update_email_opt_in
from .preferences.api import get_country_time_zones, update_email_opt_in
from .helpers import FormDescription, shim_student_view, require_post_params
from .models import UserPreference, UserProfile
from .accounts import (
......@@ -39,7 +39,7 @@ from .accounts import (
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH
)
from .accounts.api import check_account_exists
from .serializers import UserSerializer, UserPreferenceSerializer
from .serializers import CountryTimeZoneSerializer, UserSerializer, UserPreferenceSerializer
class LoginSessionView(APIView):
......@@ -1036,3 +1036,37 @@ class UpdateEmailOptInPreference(APIView):
email_opt_in = request.data['email_opt_in'].lower() == 'true'
update_email_opt_in(request.user, org, email_opt_in)
return HttpResponse(status=status.HTTP_200_OK)
class CountryTimeZoneListView(generics.ListAPIView):
"""
**Use Cases**
Retrieves a list of all time zones, by default, or common time zones for country, if given
The country is passed in as its ISO 3166-1 Alpha-2 country code as an
optional 'country_code' argument. The country code is also case-insensitive.
**Example Requests**
GET /user_api/v1/preferences/time_zones/
GET /user_api/v1/preferences/time_zones/?country_code=FR
**Example GET Response**
If the request is successful, an HTTP 200 "OK" response is returned along with a
list of time zone dictionaries for all time zones or just for time zones commonly
used in a country, if given.
Each time zone dictionary contains the following values.
* time_zone: The name of the time zone.
* description: The display version of the time zone
"""
serializer_class = CountryTimeZoneSerializer
paginator = None
def get_queryset(self):
country_code = self.request.GET.get('country_code', None)
return get_country_time_zones(country_code)
......@@ -3,7 +3,10 @@ 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
get_display_time_zone,
get_time_zone_abbr,
get_time_zone_offset,
get_user_time_zone,
)
from pytz import timezone, utc
from unittest import TestCase
......@@ -36,59 +39,59 @@ class TestTimeZoneUtils(TestCase):
user_tz = get_user_time_zone(self.user)
self.assertEqual(user_tz, timezone('Asia/Tokyo'))
def _formatted_time_zone_helper(self, time_zone_string):
def _display_time_zone_helper(self, time_zone_string):
"""
Helper function to return all info from get_formatted_time_zone()
Helper function to return all info from get_display_time_zone()
"""
tz_str = get_display_time_zone(time_zone_string)
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):
def _assert_time_zone_info_equal(self, display_tz_info, expected_name, expected_abbr, expected_offset):
"""
Asserts that all formatted_tz_info is equal to the expected inputs
Asserts that all display_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)
self.assertEqual(display_tz_info['str'], '{name} ({abbr}, UTC{offset})'.format(name=expected_name,
abbr=expected_abbr,
offset=expected_offset))
self.assertEqual(display_tz_info['abbr'], expected_abbr)
self.assertEqual(display_tz_info['offset'], expected_offset)
@freeze_time("2015-02-09")
def test_formatted_time_zone_without_dst(self):
def test_display_time_zone_without_dst(self):
"""
Test to ensure get_formatted_time_zone() returns full formatted string when no kwargs specified
Test to ensure get_display_time_zone() returns full display string when no kwargs specified
and returns just abbreviation or offset when specified
"""
tz_info = self._formatted_time_zone_helper('America/Los_Angeles')
tz_info = self._display_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):
def test_display_time_zone_with_dst(self):
"""
Test to ensure get_formatted_time_zone() returns modified abbreviations and
Test to ensure get_display_time_zone() returns modified abbreviations and
offsets during daylight savings time.
"""
tz_info = self._formatted_time_zone_helper('America/Los_Angeles')
tz_info = self._display_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):
def test_display_time_zone_ambiguous_before(self):
"""
Test to ensure get_formatted_time_zone() returns correct abbreviations and offsets
Test to ensure get_display_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')
tz_info = self._display_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):
def test_display_time_zone_ambiguous_after(self):
"""
Test to ensure get_formatted_time_zone() returns correct abbreviations and offsets
Test to ensure get_display_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')
tz_info = self._display_time_zone_helper('America/Los_Angeles')
self._assert_time_zone_info_equal(tz_info, 'America/Los Angeles', 'PST', '-0800')
......@@ -43,12 +43,13 @@ def get_time_zone_offset(time_zone, date_time=None):
return _format_time_zone_string(time_zone, date_time, '%z')
def get_formatted_time_zone(time_zone):
def get_display_time_zone(time_zone_name):
"""
Returns a formatted time zone (e.g. 'Asia/Tokyo (JST, UTC+0900)')
Returns a formatted display time zone (e.g. 'Asia/Tokyo (JST, UTC+0900)')
:param time_zone: Pytz time zone object
:param time_zone_name (str): Name of Pytz time zone
"""
time_zone = timezone(time_zone_name)
tz_abbr = get_time_zone_abbr(time_zone)
tz_offset = get_time_zone_offset(time_zone)
......@@ -56,6 +57,6 @@ def get_formatted_time_zone(time_zone):
TIME_ZONE_CHOICES = sorted(
[(tz, get_formatted_time_zone(timezone(tz))) for tz in common_timezones],
[(tz, get_display_time_zone(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