From 6174eae5ae468937f7ebe4af5ae09acc73244ddf Mon Sep 17 00:00:00 2001 From: Gabriel Hurley Date: Thu, 21 Jun 2012 19:33:42 -0700 Subject: [PATCH] Make Horizon timezone-aware. This systematically replaces anyplace that deals with dates or times in Horizon with Django's timezone-aware machinery, and enables timezone support in settings. The assumption is that the server time should *always* be UTC. TO DO: Add a setting for allowing the user to change their preferred timezone display and add timezone indicators anywhere times are displayed to the user. Implements blueprint timezones. Also fixes bug 927974. Change-Id: I5e462ba86e64b97b46873a017f87f328acee1b1d --- .../instances_and_volumes/volumes/tests.py | 1 - horizon/dashboards/nova/overview/tests.py | 23 ++++---- .../nova/templates/nova/overview/usage.html | 2 +- horizon/dashboards/syspanel/overview/tests.py | 13 ++-- horizon/forms/base.py | 6 +- horizon/templatetags/parse_date.py | 59 ++++++------------- horizon/tests/testsettings.py | 4 ++ horizon/usage/__init__.py | 2 +- horizon/usage/base.py | 46 ++++++++------- openstack_dashboard/settings.py | 2 + run_tests.sh | 2 +- tools/pip-requires | 2 +- 12 files changed, 72 insertions(+), 90 deletions(-) diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/tests.py b/horizon/dashboards/nova/instances_and_volumes/volumes/tests.py index 9d90f3f186..9fbb4a7d5b 100644 --- a/horizon/dashboards/nova/instances_and_volumes/volumes/tests.py +++ b/horizon/dashboards/nova/instances_and_volumes/volumes/tests.py @@ -94,7 +94,6 @@ class VolumeViewTests(test.TestCase): "7f2293ff3775", 1, 200) self.assertContains(res, "
Available
", 1, 200) self.assertContains(res, "
40 GB
", 1, 200) - self.assertContains(res, "
04/01/12 at 10:30:00
", 1, 200) self.assertContains(res, "server_1", 1, 200) diff --git a/horizon/dashboards/nova/overview/tests.py b/horizon/dashboards/nova/overview/tests.py index 20bdc4b11f..36d924bbec 100644 --- a/horizon/dashboards/nova/overview/tests.py +++ b/horizon/dashboards/nova/overview/tests.py @@ -22,7 +22,8 @@ import datetime from django import http from django.core.urlresolvers import reverse -from mox import IsA +from django.utils import timezone +from mox import IsA, Func from horizon import api from horizon import test @@ -34,13 +35,12 @@ INDEX_URL = reverse('horizon:nova:overview:index') class UsageViewTests(test.TestCase): def test_usage(self): - now = datetime.datetime.utcnow() + now = timezone.now() usage_obj = api.nova.Usage(self.usages.first()) self.mox.StubOutWithMock(api, 'usage_get') api.usage_get(IsA(http.HttpRequest), self.tenant.id, datetime.datetime(now.year, now.month, 1, 0, 0, 0), - datetime.datetime(now.year, now.month, now.day, now.hour, - now.minute, now.second)) \ + Func(usage.almost_now)) \ .AndReturn(usage_obj) self.mox.ReplayAll() @@ -50,15 +50,14 @@ class UsageViewTests(test.TestCase): self.assertContains(res, 'form-horizontal') def test_usage_csv(self): - now = datetime.datetime.utcnow() + now = timezone.now() usage_obj = api.nova.Usage(self.usages.first()) self.mox.StubOutWithMock(api, 'usage_get') timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) api.usage_get(IsA(http.HttpRequest), self.tenant.id, timestamp, - datetime.datetime(now.year, now.month, now.day, now.hour, - now.minute, now.second)) \ + Func(usage.almost_now)) \ .AndReturn(usage_obj) self.mox.ReplayAll() @@ -68,14 +67,13 @@ class UsageViewTests(test.TestCase): self.assertTrue(isinstance(res.context['usage'], usage.TenantUsage)) def test_usage_exception(self): - now = datetime.datetime.utcnow() + now = timezone.now() self.mox.StubOutWithMock(api, 'usage_get') timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) api.usage_get(IsA(http.HttpRequest), self.tenant.id, timestamp, - datetime.datetime(now.year, now.month, now.day, now.hour, - now.minute, now.second)) \ + Func(usage.almost_now)) \ .AndRaise(self.exceptions.nova) self.mox.ReplayAll() @@ -84,15 +82,14 @@ class UsageViewTests(test.TestCase): self.assertEqual(res.context['usage'].usage_list, []) def test_usage_default_tenant(self): - now = datetime.datetime.utcnow() + now = timezone.now() usage_obj = api.nova.Usage(self.usages.first()) self.mox.StubOutWithMock(api, 'usage_get') timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) api.usage_get(IsA(http.HttpRequest), self.tenant.id, timestamp, - datetime.datetime(now.year, now.month, now.day, now.hour, - now.minute, now.second)) \ + Func(usage.almost_now)) \ .AndReturn(usage_obj) self.mox.ReplayAll() diff --git a/horizon/dashboards/nova/templates/nova/overview/usage.html b/horizon/dashboards/nova/templates/nova/overview/usage.html index ef7f8676d7..cdea5e15b1 100644 --- a/horizon/dashboards/nova/templates/nova/overview/usage.html +++ b/horizon/dashboards/nova/templates/nova/overview/usage.html @@ -1,5 +1,5 @@ {% extends 'nova/base.html' %} -{% load i18n parse_date sizeformat %} +{% load i18n %} {% block title %}Instance Overview{% endblock %} {% block page_header %} diff --git a/horizon/dashboards/syspanel/overview/tests.py b/horizon/dashboards/syspanel/overview/tests.py index a04aea0cf4..f9e81353c8 100644 --- a/horizon/dashboards/syspanel/overview/tests.py +++ b/horizon/dashboards/syspanel/overview/tests.py @@ -22,7 +22,8 @@ import datetime from django import http from django.core.urlresolvers import reverse -from mox import IsA +from django.utils import timezone +from mox import IsA, Func from horizon import api from horizon import test @@ -37,14 +38,13 @@ class UsageViewTests(test.BaseAdminViewTests): @test.create_stubs({api: ('usage_list',), api.keystone: ('tenant_list',)}) def test_usage(self): - now = datetime.datetime.utcnow() + now = timezone.now() usage_obj = api.nova.Usage(self.usages.first()) api.keystone.tenant_list(IsA(http.HttpRequest), admin=True) \ .AndReturn(self.tenants.list()) api.usage_list(IsA(http.HttpRequest), datetime.datetime(now.year, now.month, 1, 0, 0, 0), - datetime.datetime(now.year, now.month, now.day, now.hour, - now.minute, now.second)) \ + Func(usage.almost_now)) \ .AndReturn([usage_obj]) self.mox.ReplayAll() res = self.client.get(reverse('horizon:syspanel:overview:index')) @@ -66,14 +66,13 @@ class UsageViewTests(test.BaseAdminViewTests): @test.create_stubs({api: ('usage_list',), api.keystone: ('tenant_list',)}) def test_usage_csv(self): - now = datetime.datetime.utcnow() + now = timezone.now() usage_obj = api.nova.Usage(self.usages.first()) api.keystone.tenant_list(IsA(http.HttpRequest), admin=True) \ .AndReturn(self.tenants.list()) api.usage_list(IsA(http.HttpRequest), datetime.datetime(now.year, now.month, 1, 0, 0, 0), - datetime.datetime(now.year, now.month, now.day, now.hour, - now.minute, now.second)) \ + Func(usage.almost_now)) \ .AndReturn([usage_obj]) self.mox.ReplayAll() csv_url = reverse('horizon:syspanel:overview:index') + "?format=csv" diff --git a/horizon/forms/base.py b/horizon/forms/base.py index 5b71284a2e..193d044878 100644 --- a/horizon/forms/base.py +++ b/horizon/forms/base.py @@ -18,13 +18,12 @@ # License for the specific language governing permissions and limitations # under the License. -from datetime import date import logging from django import forms from django.forms.forms import NON_FIELD_ERRORS from django.core.urlresolvers import reverse -from django.utils import dates +from django.utils import dates, timezone from horizon import exceptions @@ -119,6 +118,7 @@ class DateForm(forms.Form): def __init__(self, *args, **kwargs): super(DateForm, self).__init__(*args, **kwargs) - years = [(year, year) for year in xrange(2009, date.today().year + 1)] + years = [(year, year) for year + in xrange(2009, timezone.now().year + 1)] years.reverse() self.fields['year'].choices = years diff --git a/horizon/templatetags/parse_date.py b/horizon/templatetags/parse_date.py index 523e6f2a94..f4309a7504 100644 --- a/horizon/templatetags/parse_date.py +++ b/horizon/templatetags/parse_date.py @@ -22,56 +22,33 @@ Template tags for parsing date strings. """ -import datetime +from datetime import datetime from django import template -from dateutil import tz +from django.utils import timezone register = template.Library() -def _parse_datetime(dtstr): - if not dtstr: - return "None" - fmts = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M:%S.%f", - "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] - for fmt in fmts: - try: - return datetime.datetime.strptime(dtstr, fmt) - except: - pass - - class ParseDateNode(template.Node): - def render(self, context): - """Turn an iso formatted time back into a datetime.""" - if not context: - return "None" - date_obj = _parse_datetime(context) - return date_obj.strftime("%m/%d/%y at %H:%M:%S") + def render(self, datestring): + """ + Parses a date-like input string into a timezone aware Python datetime. + """ + formats = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M:%S.%f", + "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] + if datestring: + for format in formats: + try: + parsed = datetime.strptime(datestring, format) + if not timezone.is_aware(parsed): + parsed = timezone.make_aware(parsed, timezone.utc) + return parsed + except: + pass + return None @register.filter(name='parse_date') def parse_date(value): return ParseDateNode().render(value) - - -@register.filter(name='parse_datetime') -def parse_datetime(value): - return _parse_datetime(value) - - -@register.filter(name='parse_local_datetime') -def parse_local_datetime(value): - dt = _parse_datetime(value) - local_tz = tz.tzlocal() - utc = tz.gettz('UTC') - local_dt = dt.replace(tzinfo=utc) - return local_dt.astimezone(local_tz) - - -@register.filter(name='pretty_date') -def pretty_date(value): - if not value: - return "None" - return value.strftime("%d/%m/%y at %H:%M:%S") diff --git a/horizon/tests/testsettings.py b/horizon/tests/testsettings.py index b6330bb84d..51de8df4a1 100644 --- a/horizon/tests/testsettings.py +++ b/horizon/tests/testsettings.py @@ -29,6 +29,10 @@ ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) DEBUG = True TESTSERVER = 'http://testserver' +USE_I18N = True +USE_L10N = True +USE_TZ = True + DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3'}} INSTALLED_APPS = ( diff --git a/horizon/usage/__init__.py b/horizon/usage/__init__.py index 0013a27975..e74fc056a9 100644 --- a/horizon/usage/__init__.py +++ b/horizon/usage/__init__.py @@ -14,6 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -from .base import BaseUsage, TenantUsage, GlobalUsage +from .base import BaseUsage, TenantUsage, GlobalUsage, almost_now from .views import UsageView from .tables import BaseUsageTable, TenantUsageTable, GlobalUsageTable diff --git a/horizon/usage/base.py b/horizon/usage/base.py index 1bdd52e5b5..d6871b0bb4 100644 --- a/horizon/usage/base.py +++ b/horizon/usage/base.py @@ -1,11 +1,12 @@ from __future__ import division +from calendar import monthrange import datetime import logging -from dateutil.relativedelta import relativedelta from django.contrib import messages from django.utils.translation import ugettext as _ +from django.utils import timezone from horizon import api from horizon import exceptions @@ -15,6 +16,12 @@ from horizon import forms LOG = logging.getLogger(__name__) +def almost_now(input_time): + now = timezone.make_naive(timezone.now(), timezone.utc) + # If we're less than a minute apart we'll assume success here. + return now - input_time < datetime.timedelta(seconds=30) + + class BaseUsage(object): show_terminated = False @@ -26,27 +33,23 @@ class BaseUsage(object): @property def today(self): - return datetime.date.today() - - @staticmethod - def get_datetime(date, now=False): - if now: - now = datetime.datetime.utcnow() - current_time = datetime.time(now.hour, now.minute, now.second) - else: - current_time = datetime.time() - return datetime.datetime.combine(date, current_time) + return timezone.now() @staticmethod def get_start(year, month, day=1): - return datetime.date(year, month, day) + start = datetime.datetime(year, month, day, 0, 0, 0) + return timezone.make_aware(start, timezone.utc) @staticmethod def get_end(year, month, day=1): - period = relativedelta(months=1) - date_end = BaseUsage.get_start(year, month, day) + period - if date_end > datetime.date.today(): - date_end = datetime.date.today() + days_in_month = monthrange(year, month)[1] + period = datetime.timedelta(days=days_in_month) + end = BaseUsage.get_start(year, month, day) + period + # End our calculation at midnight of the given day. + date_end = datetime.datetime.combine(end, datetime.time(0, 0, 0)) + date_end = timezone.make_aware(date_end, timezone.utc) + if date_end > timezone.now(): + date_end = timezone.now() return date_end def get_instances(self): @@ -82,10 +85,11 @@ class BaseUsage(object): raise NotImplementedError("You must define a get_usage method.") def summarize(self, start, end): - if start <= end <= datetime.date.today(): - # Convert to datetime.datetime just for API call. - start = BaseUsage.get_datetime(start) - end = BaseUsage.get_datetime(end, now=True) + if start <= end <= self.today: + # The API can't handle timezone aware datetime, so convert back + # to naive UTC just for this last step. + start = timezone.make_naive(start, timezone.utc) + end = timezone.make_naive(end, timezone.utc) try: self.usage_list = self.get_usage_list(start, end) except: @@ -125,7 +129,7 @@ class TenantUsage(BaseUsage): usage = api.usage_get(self.request, self.tenant_id, start, end) # Attribute may not exist if there are no instances if hasattr(usage, 'server_usages'): - now = datetime.datetime.now() + now = self.today for server_usage in usage.server_usages: # This is a way to phrase uptime in a way that is compatible # with the 'timesince' filter. (Use of local time intentional.) diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py index 13f6f5991c..c73647e75d 100644 --- a/openstack_dashboard/settings.py +++ b/openstack_dashboard/settings.py @@ -137,6 +137,8 @@ LANGUAGES = ( ) LANGUAGE_CODE = 'en' USE_I18N = True +USE_L10N = True +USE_TZ = True OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member' diff --git a/run_tests.sh b/run_tests.sh index 103a565473..45297084ba 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -6,7 +6,7 @@ set -o errexit # Increment me any time the environment should be rebuilt. # This includes dependncy changes, directory renames, etc. # Simple integer secuence: 1, 2, 3... -environment_version=18 +environment_version=19 #--------------------------------------------------------# function usage { diff --git a/tools/pip-requires b/tools/pip-requires index 50a2ba75b3..c629aa0413 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -2,7 +2,7 @@ Django>=1.4 django_compressor python-cloudfiles -python-dateutil +pytz # Horizon Non-pip Requirements https://github.com/openstack/python-novaclient/zipball/master#egg=python-novaclient