Skip to content

Commit

Permalink
Make Horizon timezone-aware.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gabrielhurley committed Jun 22, 2012
1 parent 810bf63 commit 6174eae
Show file tree
Hide file tree
Showing 12 changed files with 72 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ def test_detail_view(self):
"7f2293ff3775</dd>", 1, 200)
self.assertContains(res, "<dd>Available</dd>", 1, 200)
self.assertContains(res, "<dd>40 GB</dd>", 1, 200)
self.assertContains(res, "<dd>04/01/12 at 10:30:00</dd>", 1, 200)
self.assertContains(res, "<a href=\"/nova/instances_and_volumes/"
"instances/1/detail\">server_1</a>", 1, 200)

Expand Down
23 changes: 10 additions & 13 deletions horizon/dashboards/nova/overview/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@

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
Expand All @@ -34,13 +35,12 @@

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

Expand All @@ -50,15 +50,14 @@ def test_usage(self):
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()
Expand All @@ -68,14 +67,13 @@ def test_usage_csv(self):
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()

Expand All @@ -84,15 +82,14 @@ def test_usage_exception(self):
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()

Expand Down
2 changes: 1 addition & 1 deletion horizon/dashboards/nova/templates/nova/overview/usage.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends 'nova/base.html' %}
{% load i18n parse_date sizeformat %}
{% load i18n %}
{% block title %}Instance Overview{% endblock %}

{% block page_header %}
Expand Down
13 changes: 6 additions & 7 deletions horizon/dashboards/syspanel/overview/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@

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
Expand All @@ -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'))
Expand All @@ -66,14 +66,13 @@ def test_usage(self):
@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"
Expand Down
6 changes: 3 additions & 3 deletions horizon/forms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
59 changes: 18 additions & 41 deletions horizon/templatetags/parse_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
4 changes: 4 additions & 0 deletions horizon/tests/testsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
DEBUG = True
TESTSERVER = 'http://testserver'

USE_I18N = True
USE_L10N = True
USE_TZ = True

DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3'}}

INSTALLED_APPS = (
Expand Down
2 changes: 1 addition & 1 deletion horizon/usage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
46 changes: 25 additions & 21 deletions horizon/usage/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,6 +16,12 @@
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

Expand All @@ -26,27 +33,23 @@ def __init__(self, request, tenant_id=None):

@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):
Expand Down Expand Up @@ -82,10 +85,11 @@ def get_usage_list(self, start, end):
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:
Expand Down Expand Up @@ -125,7 +129,7 @@ def get_usage_list(self, start, end):
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.)
Expand Down
2 changes: 2 additions & 0 deletions openstack_dashboard/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@
)
LANGUAGE_CODE = 'en'
USE_I18N = True
USE_L10N = True
USE_TZ = True

OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member'

Expand Down
2 changes: 1 addition & 1 deletion run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion tools/pip-requires
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6174eae

Please sign in to comment.