From a464a94655471909b9f6d9f5b287b7a309862a80 Mon Sep 17 00:00:00 2001 From: hutianhao Date: Fri, 23 Aug 2019 16:01:05 +0800 Subject: [PATCH] AgularJS pages display dates using Horizon's Settings Timezone Horizon's AngularJS pages (for example Images and Keypairs) display dates using browser's timezone now. This change makes AngularJS pages use Horizon's Settings Timezone instead of browser's timezone and if Timezone is not set under Settings, AngularJS pages will display dates in 'UTC' timezone. Closes-Bug: 1832768 Change-Id: Ibbed19600bfe6b13c43b9f09fa484cb78524b0d6 --- .../static/framework/util/filters/filters.js | 35 +++++++-- .../framework/util/filters/filters.spec.js | 24 +++++- .../util/timezones/timezone.service.js | 74 +++++++++++++++++++ .../util/timezones/timezone.service.spec.js | 52 +++++++++++++ horizon/static/framework/util/util.module.js | 1 + openstack_dashboard/api/rest/config.py | 16 ++++ 6 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 horizon/static/framework/util/timezones/timezone.service.js create mode 100644 horizon/static/framework/util/timezones/timezone.service.spec.js diff --git a/horizon/static/framework/util/filters/filters.js b/horizon/static/framework/util/filters/filters.js index 6a3af8d88b..609f5d8ba4 100644 --- a/horizon/static/framework/util/filters/filters.js +++ b/horizon/static/framework/util/filters/filters.js @@ -17,7 +17,7 @@ 'use strict'; angular - .module('horizon.framework.util.filters') + .module('horizon.framework.util.filters', ['ngCookies']) .filter('yesno', yesNoFilter) .filter('simpleDate', simpleDateFilter) .filter('mediumDate', mediumDateFilter) @@ -53,10 +53,18 @@ * @description * Evaluates given for display as a short date, returning '-' if empty. */ - simpleDateFilter.$inject = ['$filter']; - function simpleDateFilter($filter) { + simpleDateFilter.$inject = [ + '$cookies', + '$filter', + 'horizon.framework.util.timezones.service' + ]; + function simpleDateFilter($cookies, $filter, timeZoneService) { return function (input) { - return $filter('noValue')($filter('date')(input, 'short')); + var currentTimeZone = $cookies.get('django_timezone') || 'UTC'; + currentTimeZone = currentTimeZone.replace(/^"(.*)"$/, '$1'); + return timeZoneService.getTimeZoneOffset(currentTimeZone).then(function (timeZoneOffset) { + return $filter('noValue')($filter('date')(input, 'short', timeZoneOffset)); + }); }; } @@ -66,10 +74,23 @@ * @description * Evaluates given for display as a medium date, returning '-' if empty. */ - mediumDateFilter.$inject = ['$filter']; - function mediumDateFilter($filter) { + mediumDateFilter.$inject = [ + '$cookies', + '$filter', + 'horizon.framework.util.timezones.service' + ]; + function mediumDateFilter($cookies, $filter, timeZoneService) { return function (input) { - return $filter('noValue')($filter('date')(input, 'medium')); + /* + * For the input time, we need to add "Z" to fit iso8601 time format + * so the filter can confirm that the input time is in UTC timezone. + */ + input = input + 'Z'; + var currentTimeZone = $cookies.get('django_timezone') || 'UTC'; + currentTimeZone = currentTimeZone.replace(/^"(.*)"$/, '$1'); + return timeZoneService.getTimeZoneOffset(currentTimeZone).then(function (timeZoneOffset) { + return $filter('noValue')($filter('date')(input, 'medium', timeZoneOffset)); + }); }; } diff --git a/horizon/static/framework/util/filters/filters.spec.js b/horizon/static/framework/util/filters/filters.spec.js index 23a331d03b..2b98ea7882 100644 --- a/horizon/static/framework/util/filters/filters.spec.js +++ b/horizon/static/framework/util/filters/filters.spec.js @@ -59,11 +59,19 @@ })); it('returns blank if nothing', function () { - expect(simpleDateFilter()).toBe('-'); + simpleDateFilter().then(getResult); + + function getResult(result) { + expect(result).toBe('-'); + } }); it('returns the expected time', function() { - expect(simpleDateFilter('2016-06-24T04:19:07')).toBe('6/24/16 4:19 AM'); + simpleDateFilter().then(getResult); + + function getResult(result) { + expect(result).toBe('9/3/19 9:19 AM'); + } }); }); @@ -74,11 +82,19 @@ })); it('returns blank if nothing', function () { - expect(mediumDateFilter()).toBe('-'); + mediumDateFilter().then(getResult); + + function getResult(result) { + expect(result).toBe('-'); + } }); it('returns the expected time', function() { - expect(mediumDateFilter('2001-02-03T16:05:06')).toBe('Feb 3, 2001 4:05:06 PM'); + mediumDateFilter().then(getResult); + + function getResult(result) { + expect(result).toBe('Sep 3, 2019 9:19:07 AM'); + } }); }); diff --git a/horizon/static/framework/util/timezones/timezone.service.js b/horizon/static/framework/util/timezones/timezone.service.js new file mode 100644 index 0000000000..713ca18540 --- /dev/null +++ b/horizon/static/framework/util/timezones/timezone.service.js @@ -0,0 +1,74 @@ +/* + * Copyright 2019 99Cloud Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function () { + 'use strict'; + + angular + .module('horizon.framework.util.timezones', []) + .factory('horizon.framework.util.timezones.service', timeZoneService); + + timeZoneService.$inject = [ + '$q', + 'horizon.framework.util.http.service' + ]; + + /** + * @ngdoc service + * @name timeZoneService + * @param {Object} $q + * @param {Object} ApiService + * @description + * Horizon's AngularJS pages(for example Images and Keypairs) display dates + * using browser's timezone now. This service get timezone offset from + * Horizon's Settings and if Timezone is not set under Settings, AngularJS + * pages will display dates in 'UTC' timezone. + * @returns {Object} The service + */ + + function timeZoneService($q, ApiService) { + var service = { + getTimeZones: getTimeZones, + getTimeZoneOffset: getTimeZoneOffset + }; + + return service; + + ///////// + + function getTimeZones() { + return ApiService.get('/api/timezones/', {cache: true}); + } + + function getTimeZoneOffset(timezone) { + var deferred = $q.defer(); + + function onTimezonesLoaded(response) { + var offsetDict = response.data.timezone_dict; + timezone = timezone || 'UTC'; + deferred.resolve(offsetDict[timezone]); + } + + function onTimezonesFailure(message) { + deferred.reject(message); + } + + service.getTimeZones() + .then(onTimezonesLoaded, onTimezonesFailure); + + return deferred.promise; + } + } +}()); diff --git a/horizon/static/framework/util/timezones/timezone.service.spec.js b/horizon/static/framework/util/timezones/timezone.service.spec.js new file mode 100644 index 0000000000..51c4f21205 --- /dev/null +++ b/horizon/static/framework/util/timezones/timezone.service.spec.js @@ -0,0 +1,52 @@ +/* + * Copyright 2019 99Cloud Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function () { + 'use strict'; + + describe('horizon.framework.util.timezones.service', function () { + var service; + beforeEach(module('horizon.framework')); + beforeEach(inject(function($injector) { + service = $injector.get('horizon.framework.util.timezones.service'); + })); + + it('defines the service', function() { + expect(service).toBeDefined(); + }); + + describe('get timezone offset', function () { + + it('returns +0000(UTC offset) if nothing', function () { + function getResult(result) { + expect(result).toBe('+0000'); + } + + service.getTimeZoneOffset().then(getResult); + }); + + it('returns the timezone offset', function() { + + function getResult(result) { + expect(result).toBe('+0800'); + } + + service.getTimeZoneOffset('Asia/Shanghai').then(getResult); + + }); + }); + }); // end of horizon.framework.util.timezones +})(); + diff --git a/horizon/static/framework/util/util.module.js b/horizon/static/framework/util/util.module.js index 38695d220b..624f9419f7 100644 --- a/horizon/static/framework/util/util.module.js +++ b/horizon/static/framework/util/util.module.js @@ -27,6 +27,7 @@ 'horizon.framework.util.promise-toggle', 'horizon.framework.util.q', 'horizon.framework.util.tech-debt', + 'horizon.framework.util.timezones', 'horizon.framework.util.uuid', 'horizon.framework.util.workflow', 'horizon.framework.util.validators', diff --git a/openstack_dashboard/api/rest/config.py b/openstack_dashboard/api/rest/config.py index 61cdd3eb6c..b4668225f0 100644 --- a/openstack_dashboard/api/rest/config.py +++ b/openstack_dashboard/api/rest/config.py @@ -13,8 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime + from django.conf import settings +from django.http import JsonResponse from django.views import generic +import pytz from openstack_dashboard import api from openstack_dashboard.api.rest import urls @@ -55,3 +59,15 @@ class Settings(generic.View): in settings_allowed if k not in self.SPECIALS} plain_settings.update(self.SPECIALS) return plain_settings + + +@urls.register +class Timezones(generic.View): + """API for timezone service.""" + url_regex = r'timezones/$' + + @rest_utils.ajax() + def get(self, request): + zones = {tz: datetime.now(pytz.timezone(tz)).strftime('%z') + for tz in pytz.common_timezones} + return JsonResponse({'timezone_dict': zones})