Added a new attribute - display_choices

to Column class, for substituting the
display value of the statuses provided
by Nova to some more meaningful ones
in the instance table.

Fixes bug 997374

Change-Id: I18560868435b4cbc42670e3fc9c0bc83ebf9fda4
This commit is contained in:
Tihomir Trifonov 2012-05-15 19:56:02 +03:00
parent b0df3b73f0
commit 0d8273463d
5 changed files with 94 additions and 14 deletions

View File

@ -23,6 +23,7 @@ from django.utils.translation import ugettext_lazy as _
from horizon import api from horizon import api
from horizon import tables from horizon import tables
from horizon.templatetags import sizeformat from horizon.templatetags import sizeformat
from horizon.utils.filters import replace_underscores
from .tabs import InstanceDetailTabs, LogTab, VNCTab from .tabs import InstanceDetailTabs, LogTab, VNCTab
@ -216,10 +217,6 @@ def get_power_state(instance):
return POWER_STATES.get(getattr(instance, "OS-EXT-STS:power_state", 0), '') return POWER_STATES.get(getattr(instance, "OS-EXT-STS:power_state", 0), '')
def replace_underscores(string):
return string.replace("_", " ")
class InstancesTable(tables.DataTable): class InstancesTable(tables.DataTable):
TASK_STATUS_CHOICES = ( TASK_STATUS_CHOICES = (
(None, True), (None, True),
@ -231,6 +228,9 @@ class InstancesTable(tables.DataTable):
("paused", True), ("paused", True),
("error", False), ("error", False),
) )
TASK_DISPLAY_CHOICES = (
("image_snapshot", "Snapshotting"),
)
name = tables.Column("name", link="horizon:nova:instances_and_volumes:" \ name = tables.Column("name", link="horizon:nova:instances_and_volumes:" \
"instances:detail", "instances:detail",
verbose_name=_("Instance Name")) verbose_name=_("Instance Name"))
@ -245,7 +245,8 @@ class InstancesTable(tables.DataTable):
verbose_name=_("Task"), verbose_name=_("Task"),
filters=(title, replace_underscores), filters=(title, replace_underscores),
status=True, status=True,
status_choices=TASK_STATUS_CHOICES) status_choices=TASK_STATUS_CHOICES,
display_choices=TASK_DISPLAY_CHOICES)
state = tables.Column(get_power_state, state = tables.Column(get_power_state,
filters=(title, replace_underscores), filters=(title, replace_underscores),
verbose_name=_("Power State")) verbose_name=_("Power State"))

View File

@ -21,6 +21,7 @@
from django import http from django import http
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from mox import IsA, IgnoreArg from mox import IsA, IgnoreArg
from copy import deepcopy
from horizon import api from horizon import api
from horizon import test from horizon import test
@ -308,6 +309,47 @@ class InstanceViewTests(test.TestCase):
res = self.client.get(url) res = self.client.get(url)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
def test_create_instance_snapshot(self):
server = self.servers.first()
snapshot_server = deepcopy(server)
setattr(snapshot_server, 'OS-EXT-STS:task_state',
"IMAGE_SNAPSHOT")
self.mox.StubOutWithMock(api, 'server_get')
self.mox.StubOutWithMock(api, 'snapshot_create')
self.mox.StubOutWithMock(api, 'snapshot_list_detailed')
self.mox.StubOutWithMock(api, 'image_list_detailed')
self.mox.StubOutWithMock(api, 'volume_snapshot_list')
self.mox.StubOutWithMock(api, 'server_list')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'server_delete')
self.mox.StubOutWithMock(api, 'volume_list')
api.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
api.snapshot_create(IsA(http.HttpRequest),
server.id,
"snapshot1")
api.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
api.snapshot_list_detailed(IsA(http.HttpRequest)).AndReturn([])
api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([])
api.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn([snapshot_server])
api.flavor_list(IgnoreArg()).AndReturn(self.flavors.list())
self.mox.ReplayAll()
formData = {'instance_id': server.id,
'method': 'CreateSnapshot',
'tenant_id': server.tenant_id,
'name': 'snapshot1'}
url = reverse('horizon:nova:images_and_snapshots:snapshots:create',
args=[server.id])
redir_url = reverse('horizon:nova:images_and_snapshots:index')
res = self.client.post(url, formData)
self.assertRedirects(res, redir_url)
res = self.client.get(INDEX_URL)
self.assertContains(res, "<td class=\"status_unknown\">"
"Snapshotting</td>", 1)
def test_instance_update_get(self): def test_instance_update_get(self):
server = self.servers.first() server = self.servers.first()

View File

@ -26,7 +26,7 @@ from horizon.dashboards.nova.instances_and_volumes.instances.tables import (
TerminateInstance, EditInstance, ConsoleLink, LogLink, SnapshotLink, TerminateInstance, EditInstance, ConsoleLink, LogLink, SnapshotLink,
TogglePause, ToggleSuspend, RebootInstance, get_size, UpdateRow, TogglePause, ToggleSuspend, RebootInstance, get_size, UpdateRow,
get_ips, get_power_state) get_ips, get_power_state)
from horizon.utils.filters import replace_underscores
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -50,6 +50,9 @@ class SyspanelInstancesTable(tables.DataTable):
("active", True), ("active", True),
("error", False), ("error", False),
) )
TASK_DISPLAY_CHOICES = (
("image_snapshot", "Snapshotting"),
)
tenant = tables.Column("tenant_name", verbose_name=_("Tenant")) tenant = tables.Column("tenant_name", verbose_name=_("Tenant"))
# NOTE(gabriel): Commenting out the user column because all we have # NOTE(gabriel): Commenting out the user column because all we have
# is an ID, and correlating that at production scale using our current # is an ID, and correlating that at production scale using our current
@ -67,17 +70,18 @@ class SyspanelInstancesTable(tables.DataTable):
verbose_name=_("Size"), verbose_name=_("Size"),
classes=('nowrap-col',)) classes=('nowrap-col',))
status = tables.Column("status", status = tables.Column("status",
filters=(title,), filters=(title, replace_underscores),
verbose_name=_("Status"), verbose_name=_("Status"),
status=True, status=True,
status_choices=STATUS_CHOICES) status_choices=STATUS_CHOICES)
task = tables.Column("OS-EXT-STS:task_state", task = tables.Column("OS-EXT-STS:task_state",
verbose_name=_("Task"), verbose_name=_("Task"),
filters=(title,), filters=(title, replace_underscores),
status=True, status=True,
status_choices=TASK_STATUS_CHOICES) status_choices=TASK_STATUS_CHOICES,
display_choices=TASK_DISPLAY_CHOICES)
state = tables.Column(get_power_state, state = tables.Column(get_power_state,
filters=(title,), filters=(title, replace_underscores),
verbose_name=_("Power State")) verbose_name=_("Power State"))
class Meta: class Meta:

View File

@ -110,6 +110,11 @@ class Column(html.HTMLElement):
('off', False), ('off', False),
) )
.. attribute:: display_choices
A tuple of tuples representing the possible values to substitute
the data when displayed in the column cell.
.. attribute:: empty_value .. attribute:: empty_value
A string or callable to be used for cells which have no data. A string or callable to be used for cells which have no data.
@ -157,8 +162,8 @@ class Column(html.HTMLElement):
def __init__(self, transform, verbose_name=None, sortable=False, def __init__(self, transform, verbose_name=None, sortable=False,
link=None, hidden=False, attrs=None, status=False, link=None, hidden=False, attrs=None, status=False,
status_choices=None, empty_value=None, filters=None, status_choices=None, display_choices=None,
classes=None): empty_value=None, filters=None, classes=None):
self.classes = classes or getattr(self, "classes", []) self.classes = classes or getattr(self, "classes", [])
super(Column, self).__init__() super(Column, self).__init__()
self.attrs.update(attrs or {}) self.attrs.update(attrs or {})
@ -183,6 +188,7 @@ class Column(html.HTMLElement):
self.filters = filters or [] self.filters = filters or []
if status_choices: if status_choices:
self.status_choices = status_choices self.status_choices = status_choices
self.display_choices = display_choices
self.creation_counter = Column.creation_counter self.creation_counter = Column.creation_counter
Column.creation_counter += 1 Column.creation_counter += 1
@ -227,8 +233,16 @@ class Column(html.HTMLElement):
msg = termcolors.colorize(msg, **PALETTE['ERROR']) msg = termcolors.colorize(msg, **PALETTE['ERROR'])
LOG.warning(msg) LOG.warning(msg)
data = None data = None
for filter_func in self.filters: display_value = None
data = filter_func(data) if self.display_choices:
display_value = [display for (value, display) in
self.display_choices
if value.lower() == (data or '').lower()]
if display_value:
data = display_value[0]
else:
for filter_func in self.filters:
data = filter_func(data)
self.table._data_cache[self][datum_id] = data self.table._data_cache[self][datum_id] = data
return self.table._data_cache[self][datum_id] return self.table._data_cache[self][datum_id]

19
horizon/utils/filters.py Normal file
View File

@ -0,0 +1,19 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nebula, 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.
def replace_underscores(string):
return string.replace("_", " ")