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
This commit is contained in:
parent
810bf6340d
commit
6174eae5ae
@ -94,7 +94,6 @@ class VolumeViewTests(test.TestCase):
|
||||
"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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends 'nova/base.html' %}
|
||||
{% load i18n parse_date sizeformat %}
|
||||
{% load i18n %}
|
||||
{% block title %}Instance Overview{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
class ParseDateNode(template.Node):
|
||||
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"]
|
||||
for fmt in fmts:
|
||||
if datestring:
|
||||
for format in formats:
|
||||
try:
|
||||
return datetime.datetime.strptime(dtstr, fmt)
|
||||
parsed = datetime.strptime(datestring, format)
|
||||
if not timezone.is_aware(parsed):
|
||||
parsed = timezone.make_aware(parsed, timezone.utc)
|
||||
return parsed
|
||||
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")
|
||||
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")
|
||||
|
@ -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 = (
|
||||
|
@ -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
|
||||
|
@ -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.)
|
||||
|
@ -137,6 +137,8 @@ LANGUAGES = (
|
||||
)
|
||||
LANGUAGE_CODE = 'en'
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member'
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user