Query a period of time for usage summary

Allows the user to pick a range of time
to query services usage data.

Fallback mode: the user will have to input
dates by hand if JavaScript is disabled.

Change-Id: I4cf4d4fd56fd450743b9d624dc066d6ed74df881
Fixes: bug #1102448
This commit is contained in:
Victoria Martinez de la Cruz 2013-04-02 14:23:43 -03:00 committed by Victoria Martínez de la Cruz
parent 699926413c
commit 142a6e97fe
9 changed files with 147 additions and 85 deletions

View File

@ -20,8 +20,6 @@
from django import forms
from django.forms.forms import NON_FIELD_ERRORS
from django.utils import dates
from django.utils import timezone
class SelfHandlingMixin(object):
@ -49,13 +47,11 @@ class SelfHandlingForm(SelfHandlingMixin, forms.Form):
class DateForm(forms.Form):
""" A simple form for selecting a start date. """
month = forms.ChoiceField(choices=dates.MONTHS.items())
year = forms.ChoiceField()
""" A simple form for selecting a range of time. """
start = forms.DateField(input_formats=("%Y-%m-%d",))
end = forms.DateField(input_formats=("%Y-%m-%d",))
def __init__(self, *args, **kwargs):
super(DateForm, self).__init__(*args, **kwargs)
years = [(year, year) for year
in xrange(2009, timezone.now().year + 1)]
years.reverse()
self.fields['year'].choices = years
self.fields['start'].widget.attrs['data-date-format'] = "yyyy-mm-dd"
self.fields['end'].widget.attrs['data-date-format'] = "yyyy-mm-dd"

View File

@ -10,6 +10,7 @@ horizon.forms = {
$volSize.val($option.data("size"));
});
},
handle_image_source: function() {
$("div.table_wrapper, #modal_wrapper").on("change", "select#id_image_source", function(evt) {
var $option = $(this).find("option:selected");
@ -19,6 +20,27 @@ horizon.forms = {
var $volSize = $form.find('input#id_size');
$volSize.val($option.data("size"));
});
},
datepicker: function() {
var startDate = $('input#id_start').datepicker()
.on('changeDate', function(ev) {
if (ev.date.valueOf() > endDate.date.valueOf()) {
var newDate = new Date(ev.date)
newDate.setDate(newDate.getDate() + 1);
endDate.setValue(newDate);
$('input#id_end')[0].focus();
}
startDate.hide();
}).data('datepicker');
var endDate = $('input#id_end').datepicker({
onRender: function(date) {
return date.valueOf() < startDate.date.valueOf() ? 'disabled' : '';
}
}).on('changeDate', function(ev) {
endDate.hide();
}).data('datepicker');
}
};
@ -78,6 +100,7 @@ horizon.addInitFunction(function () {
horizon.forms.handle_snapshot_source();
horizon.forms.handle_image_source();
horizon.forms.datepicker();
// Bind event handlers to confirm dangerous actions.
$("body").on("click", "form button.btn-danger", function (evt) {

View File

@ -2,18 +2,19 @@
<div class="usage_info_wrapper">
<form action="" method="get" id="date_form" class="form-horizontal">
<h3>{% trans "Select a month to query its usage" %}: </h3>
<div class="form-row">
{{ form.month }}
{{ form.year }}
<h3>{% trans "Select a period of time to query its usage" %}: </h3>
<div class="datepicker">
<span>{% trans "From" %}: {{ form.start }} </span>
<span>{% trans "To" %}: {{ form.end }} </span>
<button class="btn btn-small" type="submit">{% trans "Submit" %}</button>
<small>{% trans "The date should be in YYYY-mm-dd format." %}</small>
</div>
</form>
<p id="activity">
<span><strong>{% trans "Active Instances" %}:</strong> {{ usage.summary.instances|default:'-' }}</span>
<span><strong>{% trans "Active RAM" %}:</strong> {{ usage.summary.memory_mb|mbformat|default:'-' }}</span>
<span><strong>{% trans "This Month's VCPU-Hours" %}:</strong> {{ usage.summary.vcpu_hours|floatformat:2|default:'-' }}</span>
<span><strong>{% trans "This Month's GB-Hours" %}:</strong> {{ usage.summary.disk_gb_hours|floatformat:2|default:'-' }}</span>
<span><strong>{% trans "This Period's VCPU-Hours" %}:</strong> {{ usage.summary.vcpu_hours|floatformat:2|default:'-' }}</span>
<span><strong>{% trans "This Period's GB-Hours" %}:</strong> {{ usage.summary.disk_gb_hours|floatformat:2|default:'-' }}</span>
</p>
</div>

View File

@ -24,7 +24,6 @@ from django.core.urlresolvers import reverse
from django import http
from django.utils import timezone
from mox import Func
from mox import IsA
from horizon.templatetags.sizeformat import mbformat
@ -47,10 +46,14 @@ class UsageViewTests(test.BaseAdminViewTests):
api.keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn([self.tenants.list(), False])
api.nova.usage_list(IsA(http.HttpRequest),
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
Func(usage.almost_now)) \
.AndReturn([usage_obj])
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
datetime.datetime(now.year,
now.month,
now.day, 0, 0, 0, 0),
datetime.datetime(now.year,
now.month,
now.day, 23, 59, 59, 0)) \
.AndReturn([usage_obj])
api.nova.tenant_absolute_limits(IsA(http.HttpRequest)) \
.AndReturn(self.limits['absolute'])
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:admin:overview:index'))
@ -78,9 +81,13 @@ class UsageViewTests(test.BaseAdminViewTests):
api.keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn([self.tenants.list(), False])
api.nova.usage_list(IsA(http.HttpRequest),
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
Func(usage.almost_now)) \
.AndReturn(usage_obj)
datetime.datetime(now.year,
now.month,
now.day, 0, 0, 0, 0),
datetime.datetime(now.year,
now.month,
now.day, 23, 59, 59, 0)) \
.AndReturn(usage_obj)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute'])
self.mox.ReplayAll()

View File

@ -24,7 +24,6 @@ from django.core.urlresolvers import reverse
from django import http
from django.utils import timezone
from mox import Func
from mox import IsA
from openstack_dashboard import api
@ -42,8 +41,12 @@ class UsageViewTests(test.TestCase):
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id,
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
Func(usage.almost_now)) \
datetime.datetime(now.year,
now.month,
now.day, 0, 0, 0, 0),
datetime.datetime(now.year,
now.month,
now.day, 23, 59, 59, 0)) \
.AndReturn(usage_obj)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute'])
@ -60,8 +63,12 @@ class UsageViewTests(test.TestCase):
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id,
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
Func(usage.almost_now)) \
datetime.datetime(now.year,
now.month,
now.day, 0, 0, 0, 0),
datetime.datetime(now.year,
now.month,
now.day, 23, 59, 59, 0)) \
.AndRaise(exc)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute'])
@ -78,14 +85,13 @@ class UsageViewTests(test.TestCase):
usage_obj = api.nova.NovaUsage(self.usages.first())
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0)
end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0)
api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id,
timestamp,
Func(usage.almost_now)) \
.AndReturn(usage_obj)
start, end).AndReturn(usage_obj)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute'])
.AndReturn(self.limits['absolute'])
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index') +
@ -97,14 +103,14 @@ class UsageViewTests(test.TestCase):
now = timezone.now()
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0)
end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0)
api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id,
timestamp,
Func(usage.almost_now)) \
.AndRaise(self.exceptions.nova)
start, end).AndRaise(self.exceptions.nova)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute'])
.AndReturn(self.limits['absolute'])
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index'))
@ -116,12 +122,11 @@ class UsageViewTests(test.TestCase):
usage_obj = api.nova.NovaUsage(self.usages.first())
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0)
end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0)
api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id,
timestamp,
Func(usage.almost_now)) \
.AndReturn(usage_obj)
start, end).AndReturn(usage_obj)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
@ -135,14 +140,13 @@ class UsageViewTests(test.TestCase):
usage_obj = api.nova.NovaUsage(self.usages.first())
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0)
end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0)
api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id,
timestamp,
Func(usage.almost_now)) \
.AndReturn(usage_obj)
start, end).AndReturn(usage_obj)
api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\
.AndReturn(self.limits['absolute'])
.AndReturn(self.limits['absolute'])
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index'))

View File

@ -6,6 +6,7 @@
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
.datepicker {
top: 0;
left: 0;
@ -179,4 +180,4 @@
cursor: pointer;
width: 16px;
height: 16px;
}
}

View File

@ -724,6 +724,19 @@ form label {
width: 150px;
}
.datepicker {
margin-top: 10px;
}
.datepicker input {
width: 65px;
margin-right: 10px;
}
.datepicker .btn {
margin-right: 10px;
}
form.horizontal .form-field {
float: left;
}
@ -2102,4 +2115,4 @@ div.network {
#info_box h3 {font-size:9pt;line-height:20px;}
#info_box p {margin:0;font-size:9pt;line-height:14px;}
#info_box a {margin:0;font-size:9pt;line-height:14px;}
#info_box .error {color:darkred;}
#info_box .error {color:darkred;}

View File

@ -14,7 +14,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack_dashboard.usage.base import almost_now
from openstack_dashboard.usage.base import BaseUsage
from openstack_dashboard.usage.base import GlobalUsage
from openstack_dashboard.usage.base import ProjectUsage
@ -26,7 +25,6 @@ from openstack_dashboard.usage.views import UsageView
assert BaseUsage
assert ProjectUsage
assert GlobalUsage
assert almost_now
assert UsageView
assert BaseUsageTable
assert ProjectUsageTable

View File

@ -1,8 +1,8 @@
from __future__ import division
from calendar import monthrange
from csv import DictWriter
from csv import writer
import datetime
import logging
from StringIO import StringIO
@ -24,12 +24,6 @@ from openstack_dashboard.usage import quotas
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
@ -46,21 +40,14 @@ class BaseUsage(object):
return timezone.now()
@staticmethod
def get_start(year, month, day=1):
def get_start(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):
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_end(year, month, day):
end = datetime.datetime(year, month, day, 23, 59, 59)
return timezone.make_aware(end, timezone.utc)
def get_instances(self):
instance_list = []
@ -69,24 +56,50 @@ class BaseUsage(object):
def get_date_range(self):
if not hasattr(self, "start") or not hasattr(self, "end"):
args = (self.today.year, self.today.month)
args_start = args_end = (self.today.year,
self.today.month,
self.today.day)
form = self.get_form()
if form.is_valid():
args = (int(form.cleaned_data['year']),
int(form.cleaned_data['month']))
self.start = self.get_start(*args)
self.end = self.get_end(*args)
start = form.cleaned_data['start']
end = form.cleaned_data['end']
args_start = (start.year,
start.month,
start.day)
args_end = (end.year,
end.month,
end.day)
elif form.is_bound:
messages.error(self.request,
_("Invalid date format: "
"Using today as default."))
self.start = self.get_start(*args_start)
self.end = self.get_end(*args_end)
return self.start, self.end
def init_form(self):
today = datetime.date.today()
first = datetime.date(day=1, month=today.month, year=today.year)
if today.day in range(5):
self.end = first - datetime.timedelta(days=1)
self.start = datetime.date(day=1,
month=self.end.month,
year=self.end.year)
else:
self.end = today
self.start = first
return self.start, self.end
def get_form(self):
if not hasattr(self, 'form'):
if any(key in ['month', 'year'] for key in self.request.GET):
if any(key in ['start', 'end'] for key in self.request.GET):
# bound form
self.form = forms.DateForm(self.request.GET)
else:
# non-bound form
self.form = forms.DateForm(initial={'month': self.today.month,
'year': self.today.year})
init = self.init_form()
self.form = forms.DateForm(initial={'start': init[0],
'end': init[1]})
return self.form
def get_limits(self):
@ -100,7 +113,7 @@ class BaseUsage(object):
raise NotImplementedError("You must define a get_usage method.")
def summarize(self, start, end):
if start <= end <= self.today:
if start <= end and start <= 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)
@ -110,10 +123,14 @@ class BaseUsage(object):
except:
exceptions.handle(self.request,
_('Unable to retrieve usage information.'))
else:
messages.info(self.request,
_("You are viewing data for the future, "
"which may or may not exist."))
elif end < start:
messages.error(self.request,
_("Invalid time period. The end date should be "
"more recent than the start date."))
elif start > self.today:
messages.error(self.request,
_("Invalid time period. You are requesting "
"data from the future which may not exist."))
for project_usage in self.usage_list:
project_summary = project_usage.get_summary()
@ -130,11 +147,13 @@ class BaseUsage(object):
def csv_link(self):
form = self.get_form()
data = {}
if hasattr(form, "cleaned_data"):
data = form.cleaned_data
else:
data = {"month": self.today.month, "year": self.today.year}
return "?month=%s&year=%s&format=csv" % (data['month'], data['year'])
if not ('start' in data and 'end' in data):
data = {"start": self.today.date(), "end": self.today.date()}
return "?start=%s&end=%s&format=csv" % (data['start'],
data['end'])
class GlobalUsage(BaseUsage):