Converts all of Access & Security to use new panels, modals, views, etc.

Adds empty table message, multi-table CBV, improved testing facilities.

Fixes bug 905376. Fixes bug 905399.

Change-Id: Ib93a5b9d09c9b98b0a6365f7d468efb05e28e676
This commit is contained in:
Gabriel Hurley 2012-01-09 14:44:53 -08:00
parent 120b43bd36
commit 22d80f7ff8
44 changed files with 652 additions and 841 deletions

View File

@ -61,3 +61,13 @@ Actions
.. autoclass:: FilterAction .. autoclass:: FilterAction
:members: :members:
Class-Based Views
=================
Several class-based views are provided to make working with DataTables
easier in your UI.
.. autoclass:: DataTableView
.. autoclass:: MultiTableView

View File

@ -132,16 +132,9 @@ class SecurityGroup(APIResourceWrapper):
_attrs = ['id', 'name', 'description', 'tenant_id', 'rules'] _attrs = ['id', 'name', 'description', 'tenant_id', 'rules']
class SecurityGroupRule(APIResourceWrapper): class SecurityGroupRule(APIDictWrapper):
"""Simple wrapper around """ Simple wrapper for individual rules in a SecurityGroup. """
openstackx.extras.security_groups.SecurityGroupRule""" _attrs = ['ip_protocol', 'from_port', 'to_port', 'ip_range']
_attrs = ['id', 'parent_group_id', 'group_id', 'ip_protocol',
'from_port', 'to_port', 'groups', 'ip_ranges']
class SecurityGroupRule(APIResourceWrapper):
"""Simple wrapper around openstackx.extras.users.User"""
_attrs = ['id', 'name', 'description', 'tenant_id', 'security_group_rules']
def novaclient(request): def novaclient(request):

View File

@ -33,22 +33,6 @@ from horizon import forms
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class ReleaseFloatingIp(forms.SelfHandlingForm):
floating_ip_id = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
try:
LOG.info('Releasing Floating IP "%s"' % data['floating_ip_id'])
api.tenant_floating_ip_release(request, data['floating_ip_id'])
messages.info(request, _('Successfully released Floating IP: %s')
% data['floating_ip_id'])
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in ReleaseFloatingIp")
messages.error(request, _('Error releasing Floating IP '
'from tenant: %s') % e.message)
return shortcuts.redirect(request.build_absolute_uri())
class FloatingIpAssociate(forms.SelfHandlingForm): class FloatingIpAssociate(forms.SelfHandlingForm):
floating_ip_id = forms.CharField(widget=forms.HiddenInput()) floating_ip_id = forms.CharField(widget=forms.HiddenInput())
floating_ip = forms.CharField(widget=forms.TextInput( floating_ip = forms.CharField(widget=forms.TextInput(
@ -69,58 +53,11 @@ class FloatingIpAssociate(forms.SelfHandlingForm):
data['floating_ip_id']) data['floating_ip_id'])
LOG.info('Associating Floating IP "%s" with Instance "%s"' LOG.info('Associating Floating IP "%s" with Instance "%s"'
% (data['floating_ip'], data['instance_id'])) % (data['floating_ip'], data['instance_id']))
messages.info(request, _('Successfully associated Floating IP: \ messages.info(request, _('Successfully associated Floating IP \
%(ip)s with Instance: %(inst)s' %(ip)s with Instance: %(inst)s'
% {"ip": data['floating_ip'], % {"ip": data['floating_ip'],
"inst": data['instance_id']})) "inst": data['instance_id']}))
except novaclient_exceptions.ClientException, e: except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in FloatingIpAssociate") LOG.exception("ClientException in FloatingIpAssociate")
messages.error(request, _('Error associating Floating IP: %s') messages.error(request, _('Error associating Floating IP: %s') % e)
% e.message) return shortcuts.redirect('horizon:nova:access_and_security:index')
return shortcuts.redirect(
'horizon:nova:access_and_security:index')
class FloatingIpDisassociate(forms.SelfHandlingForm):
floating_ip_id = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
try:
fip = api.tenant_floating_ip_get(request, data['floating_ip_id'])
api.server_remove_floating_ip(request, fip.instance_id, fip.id)
LOG.info('Disassociating Floating IP "%s"'
% data['floating_ip_id'])
messages.info(request,
_('Successfully disassociated Floating IP: %s')
% data['floating_ip_id'])
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in FloatingIpAssociate")
messages.error(request, _('Error disassociating Floating IP: %s')
% e.message)
return shortcuts.redirect(
'horizon:nova:access_and_security:index')
class FloatingIpAllocate(forms.SelfHandlingForm):
tenant_id = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
try:
fip = api.tenant_floating_ip_allocate(request)
LOG.info('Allocating Floating IP "%s" to tenant "%s"'
% (fip.ip, data['tenant_id']))
messages.success(request,
_('Successfully allocated Floating IP "%(ip)s"\
to tenant "%(tenant)s"')
% {"ip": fip.ip, "tenant": data['tenant_id']})
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in FloatingIpAllocate")
messages.error(request, _('Error allocating Floating IP "%(ip)s"\
to tenant "%(tenant)s": %(msg)s') %
{"ip": fip.ip, "tenant": data['tenant_id'], "msg": e.message})
return shortcuts.redirect(
'horizon:nova:access_and_security:index')

View File

@ -0,0 +1,123 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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.
import logging
from django import shortcuts
from django.contrib import messages
from django.core.urlresolvers import reverse
from novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon import tables
LOG = logging.getLogger(__name__)
class AllocateIP(tables.Action):
name = "allocate"
verbose_name = _("Allocate IP To Tenant")
requires_input = False
def single(self, data_table, request, *args):
tenant_id = request.user.tenant_id
try:
fip = api.tenant_floating_ip_allocate(request)
LOG.info('Allocating Floating IP "%s" to tenant "%s".'
% (fip.ip, tenant_id))
messages.success(request, _('Successfully allocated Floating IP '
'"%(ip)s" to tenant "%(tenant)s".')
% {"ip": fip.ip, "tenant": tenant_id})
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in FloatingIpAllocate")
messages.error(request, _('Unable to allocate Floating IP '
'"%(ip)s" to tenant "%(tenant)s".')
% {"ip": fip.ip, "tenant": tenant_id})
return shortcuts.redirect('horizon:nova:access_and_security:index')
class ReleaseIP(tables.Action):
name = "release"
verbose_name = _("Release IP")
classes = ('danger',)
def handle(self, table, request, object_ids):
released = []
for obj_id in object_ids:
LOG.info('Releasing Floating IP "%s".' % obj_id)
try:
api.tenant_floating_ip_release(request, obj_id)
# Floating IP ids are returned from the API as integers
released.append(table.get_object_by_id(int(obj_id)))
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in ReleaseFloatingIp")
messages.error(request, _('Unable to release Floating IP '
'from tenant.'))
if released:
messages.info(request,
_('Successfully released floating IPs: %s.')
% ", ".join([ip.ip for ip in released]))
return shortcuts.redirect('horizon:nova:access_and_security:index')
class AssociateIP(tables.LinkAction):
name = "associate"
verbose_name = _("Associate IP")
url = "horizon:nova:access_and_security:floating_ips:associate"
attrs = {"class": "ajax-modal"}
def allowed(self, request, fip):
if fip.instance_id:
return False
return True
class DisassociateIP(tables.Action):
name = "disassociate"
verbose_name = _("Disassociate IP")
def allowed(self, request, fip):
if fip.instance_id:
return True
return False
def single(self, table, request, obj_id):
try:
fip = table.get_object_by_id(int(obj_id))
api.server_remove_floating_ip(request, fip.instance_id, fip.id)
LOG.info('Disassociating Floating IP "%s".' % obj_id)
messages.info(request,
_('Successfully disassociated Floating IP: %s')
% obj_id)
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in FloatingIpAssociate")
messages.error(request, _('Error disassociating Floating IP: %s')
% e.message)
return shortcuts.redirect('horizon:nova:access_and_security:index')
class FloatingIPsTable(tables.DataTable):
ip = tables.Column("ip", verbose_name=_("IP Address"))
instance = tables.Column("instance_id",
verbose_name=_("Instance"),
empty_value="-")
class Meta:
name = "floating_ips"
verbose_name = _("Floating IPs")
table_actions = (AllocateIP, ReleaseIP)
row_actions = (AssociateIP, DisassociateIP, ReleaseIP)

View File

@ -21,7 +21,6 @@
import datetime import datetime
from django import http from django import http
from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import redirect from django.shortcuts import redirect
from mox import IsA, IgnoreArg from mox import IsA, IgnoreArg
@ -33,7 +32,7 @@ from horizon.dashboards.nova.access_and_security.floating_ips.forms import \
FloatingIpAssociate FloatingIpAssociate
FLOATING_IPS_INDEX = reverse('horizon:nova:access_and_security:index') INDEX_URL = reverse('horizon:nova:access_and_security:index')
class FloatingIpViewTests(test.BaseViewTests): class FloatingIpViewTests(test.BaseViewTests):
@ -111,7 +110,7 @@ class FloatingIpViewTests(test.BaseViewTests):
'floating_ip': self.floating_ip.ip, 'floating_ip': self.floating_ip.ip,
'method': 'FloatingIpAssociate'}) 'method': 'FloatingIpAssociate'})
self.assertRedirects(res, FLOATING_IPS_INDEX) self.assertRedirects(res, INDEX_URL)
def test_associate_post_with_exception(self): def test_associate_post_with_exception(self):
server = self.server server = self.server
@ -152,70 +151,51 @@ class FloatingIpViewTests(test.BaseViewTests):
'method': 'FloatingIpAssociate'}) 'method': 'FloatingIpAssociate'})
self.assertRaises(novaclient_exceptions.ClientException) self.assertRaises(novaclient_exceptions.ClientException)
self.assertRedirects(res, FLOATING_IPS_INDEX) self.assertRedirects(res, INDEX_URL)
def test_disassociate(self):
res = self.client.get(
reverse('horizon:nova:access_and_security:floating_ips:disassociate',
args=[1]))
self.assertTemplateUsed(res,
'nova/access_and_security/floating_ips/associate.html')
def test_disassociate_post(self): def test_disassociate_post(self):
self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') self.mox.StubOutWithMock(api.nova, 'keypair_list')
api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
AndReturn(self.floating_ips)
self.mox.StubOutWithMock(api, 'security_group_list') self.mox.StubOutWithMock(api, 'security_group_list')
self.mox.StubOutWithMock(api, 'tenant_floating_ip_list')
self.mox.StubOutWithMock(api, 'tenant_floating_ip_get')
self.mox.StubOutWithMock(api, 'server_remove_floating_ip')
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
api.security_group_list(IsA(http.HttpRequest)).\ api.security_group_list(IsA(http.HttpRequest)).\
AndReturn(self.security_groups) AndReturn(self.security_groups)
self.mox.StubOutWithMock(api.nova, 'keypair_list') api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) AndReturn(self.floating_ips)
self.mox.StubOutWithMock(api, 'server_remove_floating_ip')
api.server_remove_floating_ip = self.mox.CreateMockAnything()
api.server_remove_floating_ip(IsA(http.HttpRequest), IsA(int), api.server_remove_floating_ip(IsA(http.HttpRequest), IsA(int),
IsA(int)).\ IsA(int)).\
AndReturn(None) AndReturn(None)
self.mox.StubOutWithMock(messages, 'info')
messages.info(IsA(http.HttpRequest), IsA(unicode))
self.mox.StubOutWithMock(api, 'tenant_floating_ip_get')
api.tenant_floating_ip_get = self.mox.CreateMockAnything()
api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\
AndReturn(self.floating_ip)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.post(
reverse('horizon:nova:access_and_security:floating_ips:disassociate', action = "floating_ips__disassociate__%s" % self.floating_ip.id
args=[1]), res = self.client.post(INDEX_URL, {"action": action})
{'floating_ip_id': self.floating_ip.id, self.assertRedirectsNoFollow(res, INDEX_URL)
'method': 'FloatingIpDisassociate'})
self.assertRedirects(res, FLOATING_IPS_INDEX)
def test_disassociate_post_with_exception(self): def test_disassociate_post_with_exception(self):
self.mox.StubOutWithMock(api, 'tenant_floating_ip_list') self.mox.StubOutWithMock(api.nova, 'keypair_list')
api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
AndReturn(self.floating_ips)
self.mox.StubOutWithMock(api, 'security_group_list') self.mox.StubOutWithMock(api, 'security_group_list')
self.mox.StubOutWithMock(api, 'tenant_floating_ip_list')
self.mox.StubOutWithMock(api, 'tenant_floating_ip_get')
self.mox.StubOutWithMock(api, 'server_remove_floating_ip')
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
api.security_group_list(IsA(http.HttpRequest)).\ api.security_group_list(IsA(http.HttpRequest)).\
AndReturn(self.security_groups) AndReturn(self.security_groups)
self.mox.StubOutWithMock(api.nova, 'keypair_list') api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) AndReturn(self.floating_ips)
self.mox.StubOutWithMock(api, 'server_remove_floating_ip')
exception = novaclient_exceptions.ClientException('ClientException', exception = novaclient_exceptions.ClientException('ClientException',
message='clientException') message='clientException')
api.server_remove_floating_ip(IsA(http.HttpRequest), api.server_remove_floating_ip(IsA(http.HttpRequest),
IsA(int), IsA(int),
IsA(int)).AndRaise(exception) IsA(int)).AndRaise(exception)
self.mox.StubOutWithMock(api, 'tenant_floating_ip_get')
api.tenant_floating_ip_get = self.mox.CreateMockAnything()
api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\
AndReturn(self.floating_ip)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.post(
reverse('horizon:nova:access_and_security:floating_ips:disassociate', action = "floating_ips__disassociate__%s" % self.floating_ip.id
args=[1]), res = self.client.post(INDEX_URL, {"action": action})
{'floating_ip_id': self.floating_ip.id,
'method': 'FloatingIpDisassociate'})
self.assertRaises(novaclient_exceptions.ClientException) self.assertRaises(novaclient_exceptions.ClientException)
self.assertRedirects(res, FLOATING_IPS_INDEX) self.assertRedirectsNoFollow(res, INDEX_URL)

View File

@ -20,10 +20,11 @@
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
from .views import AssociateView
urlpatterns = patterns(
'horizon.dashboards.nova.access_and_security.floating_ips.views', urlpatterns = patterns('',
url(r'^$', 'index', name='index'), url(r'^(?P<ip_id>[^/]+)/associate/$',
url(r'^(?P<ip_id>[^/]+)/associate/$', 'associate', name='associate'), AssociateView.as_view(),
url(r'^(?P<ip_id>[^/]+)/disassociate/$', 'disassociate', name='associate')
name='disassociate')) )

View File

@ -23,76 +23,36 @@ Views for managing Nova floating IPs.
""" """
import logging import logging
from django import template
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django import shortcuts
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from novaclient import exceptions as novaclient_exceptions
from horizon import api from horizon import api
from horizon.dashboards.nova.access_and_security.floating_ips.forms import \ from horizon import forms
(ReleaseFloatingIp, FloatingIpAssociate, from .forms import FloatingIpAssociate
FloatingIpDisassociate, FloatingIpAllocate)
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@login_required class AssociateView(forms.ModalFormView):
def index(request): form_class = FloatingIpAssociate
for f in (ReleaseFloatingIp, FloatingIpDisassociate, FloatingIpAllocate): template_name = 'nova/access_and_security/floating_ips/associate.html'
_unused, handled = f.maybe_handle(request) context_object_name = 'floating_ip'
if handled:
return handled
try:
floating_ips = api.tenant_floating_ip_list(request)
except novaclient_exceptions.ClientException, e:
floating_ips = []
LOG.exception("ClientException in floating ip index")
messages.error(request,
_('Error fetching floating ips: %s') % e.message)
allocate_form = FloatingIpAllocate(initial={
'tenant_id': request.user.tenant_id})
return shortcuts.render(request,
'nova/access_and_security/floating_ips/index.html', {
'allocate_form': allocate_form,
'disassociate_form': FloatingIpDisassociate(),
'floating_ips': floating_ips,
'release_form': ReleaseFloatingIp()})
def get_object(self, *args, **kwargs):
ip_id = kwargs['ip_id']
try:
return api.tenant_floating_ip_get(self.request, ip_id)
except Exception as e:
LOG.exception('Error fetching floating ip with id "%s".' % ip_id)
messages.error(self.request,
_('Unable to associate floating ip: %s') % e)
raise http.Http404("Floating IP %s not available." % ip_id)
@login_required def get_initial(self):
def associate(request, ip_id): instances = [(server.id, 'id: %s, name: %s' %
instancelist = [(server.id, 'id: %s, name: %s' % (server.id, server.name))
(server.id, server.name)) for server in api.server_list(self.request)]
for server in api.server_list(request)] return {'floating_ip_id': self.object.id,
'floating_ip': self.object.ip,
form, handled = FloatingIpAssociate().maybe_handle(request, initial={ 'instances': instances}
'floating_ip_id': ip_id,
'floating_ip': api.tenant_floating_ip_get(request, ip_id).ip,
'instances': instancelist})
if handled:
return handled
context = {'floating_ip_id': ip_id,
'form': form}
if request.is_ajax():
template = 'nova/access_and_security/floating_ips/_associate.html'
context['hide'] = True
else:
template = 'nova/access_and_security/floating_ips/associate.html'
return shortcuts.render(request, template, context)
@login_required
def disassociate(request, ip_id):
form, handled = FloatingIpDisassociate().maybe_handle(request)
if handled:
return handled
return shortcuts.render(request,
'nova/access_and_security/floating_ips/associate.html', {
'floating_ip_id': ip_id})

View File

@ -36,24 +36,7 @@ from horizon import forms
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class DeleteKeypair(forms.SelfHandlingForm):
keypair_id = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
try:
LOG.info('Deleting keypair "%s"' % data['keypair_id'])
api.keypair_delete(request, urlquote(data['keypair_id']))
messages.info(request, _('Successfully deleted keypair: %s')
% data['keypair_id'])
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in DeleteKeypair")
messages.error(request,
_('Error deleting keypair: %s') % e.message)
return shortcuts.redirect(request.build_absolute_uri())
class CreateKeypair(forms.SelfHandlingForm): class CreateKeypair(forms.SelfHandlingForm):
name = forms.CharField(max_length="20", name = forms.CharField(max_length="20",
label=_("Keypair Name"), label=_("Keypair Name"),
validators=[validators.validate_slug], validators=[validators.validate_slug],
@ -79,7 +62,6 @@ class CreateKeypair(forms.SelfHandlingForm):
class ImportKeypair(forms.SelfHandlingForm): class ImportKeypair(forms.SelfHandlingForm):
name = forms.CharField(max_length="20", label=_("Keypair Name"), name = forms.CharField(max_length="20", label=_("Keypair Name"),
validators=[validators.RegexValidator('\w+')]) validators=[validators.RegexValidator('\w+')])
public_key = forms.CharField(label=_("Public Key"), widget=forms.Textarea) public_key = forms.CharField(label=_("Public Key"), widget=forms.Textarea)

View File

@ -40,11 +40,13 @@ class KeyPairViewTests(test.BaseViewTests):
def test_delete_keypair(self): def test_delete_keypair(self):
KEYPAIR_ID = self.keypairs[0].name KEYPAIR_ID = self.keypairs[0].name
formData = {'method': 'DeleteKeypair', formData = {'action': 'keypairs__delete__%s' % KEYPAIR_ID}
'keypair_id': KEYPAIR_ID}
self.mox.StubOutWithMock(api, 'keypair_delete') self.mox.StubOutWithMock(api.nova, 'keypair_list')
api.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID)) self.mox.StubOutWithMock(api.nova, 'keypair_delete')
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
api.nova.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID))
self.mox.ReplayAll() self.mox.ReplayAll()
@ -54,15 +56,16 @@ class KeyPairViewTests(test.BaseViewTests):
def test_delete_keypair_exception(self): def test_delete_keypair_exception(self):
KEYPAIR_ID = self.keypairs[0].name KEYPAIR_ID = self.keypairs[0].name
formData = {'method': 'DeleteKeypair', formData = {'action': 'keypairs__delete__%s' % KEYPAIR_ID}
'keypair_id': KEYPAIR_ID,
}
self.mox.StubOutWithMock(api.nova, 'keypair_list')
self.mox.StubOutWithMock(api.nova, 'keypair_delete')
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
exception = novaclient_exceptions.ClientException('clientException', exception = novaclient_exceptions.ClientException('clientException',
message='clientException') message='clientException')
self.mox.StubOutWithMock(api, 'keypair_delete') api.nova.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID)) \
api.keypair_delete(IsA(http.HttpRequest), .AndRaise(exception)
unicode(KEYPAIR_ID)).AndRaise(exception)
self.mox.ReplayAll() self.mox.ReplayAll()

View File

@ -20,12 +20,10 @@
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
from .views import IndexView, CreateView, ImportView from .views import CreateView, ImportView
urlpatterns = patterns( urlpatterns = patterns('',
'horizon.dashboards.nova.access_and_security.keypairs.views',
url(r'^$', IndexView.as_view(), name='index'),
url(r'^create/$', CreateView.as_view(), name='create'), url(r'^create/$', CreateView.as_view(), name='create'),
url(r'^import/$', ImportView.as_view(), name='import'), url(r'^import/$', ImportView.as_view(), name='import'),
) )

View File

@ -23,38 +23,13 @@ Views for managing Nova keypairs.
""" """
import logging import logging
from django import http
from django import shortcuts
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext as _
from novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon import forms from horizon import forms
from horizon import tables from .forms import CreateKeypair, ImportKeypair
from .forms import CreateKeypair, DeleteKeypair, ImportKeypair
from .tables import KeypairsTable
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView):
table_class = KeypairsTable
template_name = 'nova/access_and_security/keypairs/index.html'
def get_data(self):
try:
keypairs = api.nova.keypair_list(self.request)
except Exception, e:
keypairs = []
LOG.exception("ClientException in keypair index")
messages.error(request,
_('Error fetching keypairs: %s') % e.message)
return keypairs
class CreateView(forms.ModalFormView): class CreateView(forms.ModalFormView):
form_class = CreateKeypair form_class = CreateKeypair
template_name = 'nova/access_and_security/keypairs/create.html' template_name = 'nova/access_and_security/keypairs/create.html'

View File

@ -56,27 +56,6 @@ class CreateGroup(forms.SelfHandlingForm):
e.message) e.message)
class DeleteGroup(forms.SelfHandlingForm):
tenant_id = forms.CharField(widget=forms.HiddenInput())
security_group_id = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
try:
LOG.info('Delete security_group: "%s"' % data)
security_group = api.security_group_delete(request,
data['security_group_id'])
messages.success(request,
_('Successfully deleted security_group: %s')
% data['security_group_id'])
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in DeleteGroup")
messages.error(request, _('Error deleting security group: %s')
% e.message)
return shortcuts.redirect(
'horizon:nova:access_and_security:index')
class AddRule(forms.SelfHandlingForm): class AddRule(forms.SelfHandlingForm):
ip_protocol = forms.ChoiceField(choices=[('tcp', 'tcp'), ip_protocol = forms.ChoiceField(choices=[('tcp', 'tcp'),
('udp', 'udp'), ('udp', 'udp'),
@ -108,25 +87,3 @@ class AddRule(forms.SelfHandlingForm):
messages.error(request, _('Error adding rule security group: %s') messages.error(request, _('Error adding rule security group: %s')
% e.message) % e.message)
return shortcuts.redirect(request.build_absolute_uri()) return shortcuts.redirect(request.build_absolute_uri())
class DeleteRule(forms.SelfHandlingForm):
security_group_rule_id = forms.CharField(widget=forms.HiddenInput())
tenant_id = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
security_group_rule_id = data['security_group_rule_id']
tenant_id = data['tenant_id']
try:
LOG.info('Delete security_group_rule: "%s"' % data)
security_group = api.security_group_rule_delete(
request,
security_group_rule_id)
messages.info(request, _('Successfully deleted rule: %s')
% security_group_rule_id)
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in DeleteRule")
messages.error(request, _('Error authorizing security group: %s')
% e.message)
return shortcuts.redirect(request.build_absolute_uri())

View File

@ -0,0 +1,124 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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.
import logging
from django import shortcuts
from django.contrib import messages
from django.core.urlresolvers import reverse
from novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon import tables
LOG = logging.getLogger(__name__)
class DeleteGroup(tables.Action):
name = "delete"
verbose_name = _("Delete")
verbose_name_plural = _("Delete Security Groups")
classes = ('danger',)
def allowed(self, request, security_group=None):
if not security_group:
return True
return security_group.name != 'default'
def handle(self, table, request, object_ids):
tenant_id = request.user.tenant_id
deleted = []
for obj_id in object_ids:
obj = table.get_object_by_id(int(obj_id))
if obj.name == "default":
messages.info(request, _("The default group can't be deleted"))
continue
try:
security_group = api.security_group_delete(request, obj_id)
deleted.append(obj)
LOG.info('Deleted security_group: "%s"' % obj.name)
except novaclient_exceptions.ClientException, e:
LOG.exception("Error deleting security group")
messages.error(request, _('Unable to delete group: %s')
% obj.name)
if deleted:
messages.success(request,
_('Successfully deleted security groups: %s')
% ", ".join([group.name for group in deleted]))
return shortcuts.redirect('horizon:nova:access_and_security:index')
class CreateGroup(tables.LinkAction):
name = "create"
verbose_name = _("Create Security Group")
url = "horizon:nova:access_and_security:security_groups:create"
attrs = {"class": "btn small ajax-modal"}
class EditRules(tables.LinkAction):
name = "edit_rules"
verbose_name = _("Edit Rules")
url = "horizon:nova:access_and_security:security_groups:edit_rules"
class SecurityGroupsTable(tables.DataTable):
name = tables.Column("name")
description = tables.Column("description")
class Meta:
name = "security_groups"
verbose_name = _("Security Groups")
table_actions = (CreateGroup, DeleteGroup)
row_actions = (EditRules, DeleteGroup)
class DeleteRule(tables.Action):
name = "delete"
verbose_name = _("Delete")
classes = ('danger',)
def single(self, table, request, obj_id):
tenant_id = request.user.tenant_id
obj = table.get_object_by_id(int(obj_id))
try:
LOG.info('Delete security_group_rule: "%s"' % obj_id)
security_group = api.security_group_rule_delete(request, obj_id)
messages.info(request, _('Successfully deleted rule: %s')
% obj_id)
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in DeleteRule")
messages.error(request, _('Error authorizing security group: %s')
% e.message)
return shortcuts.redirect('horizon:nova:access_and_security:'
'security_groups:edit_rules',
(obj.parent_group_id))
def get_cidr(rule):
return rule.ip_range['cidr']
class RulesTable(tables.DataTable):
protocol = tables.Column("ip_protocol", verbose_name=_("IP Protocol"))
from_port = tables.Column("from_port", verbose_name=_("From Port"))
to_port = tables.Column("to_port", verbose_name=_("To Port"))
cidr = tables.Column(get_cidr, verbose_name=_("CIDR"))
class Meta:
name = "rules"
verbose_name = _("Security Group Rules")
row_actions = (DeleteRule,)

View File

@ -27,9 +27,11 @@ from mox import IgnoreArg, IsA
from horizon import api from horizon import api
from horizon import test from horizon import test
from .tables import SecurityGroupsTable, RulesTable
SECGROUP_ID = '1'
SG_INDEX_URL = reverse('horizon:nova:access_and_security:index') SECGROUP_ID = '2'
INDEX_URL = reverse('horizon:nova:access_and_security:index')
SG_CREATE_URL = \ SG_CREATE_URL = \
reverse('horizon:nova:access_and_security:security_groups:create') reverse('horizon:nova:access_and_security:security_groups:create')
SG_EDIT_RULE_URL = \ SG_EDIT_RULE_URL = \
@ -41,10 +43,24 @@ class SecurityGroupsViewTests(test.BaseViewTests):
def setUp(self): def setUp(self):
super(SecurityGroupsViewTests, self).setUp() super(SecurityGroupsViewTests, self).setUp()
security_group = api.SecurityGroup(None) sg1 = api.SecurityGroup(None)
security_group.id = '1' sg1.id = 1
security_group.name = 'default' sg1.name = 'default'
self.security_groups = (security_group,)
sg2 = api.SecurityGroup(None)
sg2.id = 2
sg2.name = 'group_2'
rule = {'id': 1,
'ip_protocol': "tcp",
'from_port': "80",
'to_port': "80",
'parent_group_id': "2",
'ip_range': {'cidr': "0.0.0.0/32"}}
self.rules = [api.nova.SecurityGroupRule(rule)]
sg2.rules = self.rules
self.security_groups = (sg1, sg2)
def test_create_security_groups_get(self): def test_create_security_groups_get(self):
res = self.client.get(SG_CREATE_URL) res = self.client.get(SG_CREATE_URL)
@ -73,7 +89,7 @@ class SecurityGroupsViewTests(test.BaseViewTests):
res = self.client.post(SG_CREATE_URL, formData) res = self.client.post(SG_CREATE_URL, formData)
self.assertRedirectsNoFollow(res, SG_INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
def test_create_security_groups_post_exception(self): def test_create_security_groups_post_exception(self):
SECGROUP_NAME = 'fakegroup' SECGROUP_NAME = 'fakegroup'
@ -100,10 +116,9 @@ class SecurityGroupsViewTests(test.BaseViewTests):
'nova/access_and_security/security_groups/create.html') 'nova/access_and_security/security_groups/create.html')
def test_edit_rules_get(self): def test_edit_rules_get(self):
self.mox.StubOutWithMock(api, 'security_group_get') self.mox.StubOutWithMock(api, 'security_group_get')
api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndReturn( api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndReturn(
self.security_groups[0]) self.security_groups[1])
self.mox.ReplayAll() self.mox.ReplayAll()
@ -112,21 +127,21 @@ class SecurityGroupsViewTests(test.BaseViewTests):
self.assertTemplateUsed(res, self.assertTemplateUsed(res,
'nova/access_and_security/security_groups/edit_rules.html') 'nova/access_and_security/security_groups/edit_rules.html')
self.assertItemsEqual(res.context['security_group'].name, self.assertItemsEqual(res.context['security_group'].name,
self.security_groups[0].name) self.security_groups[1].name)
def test_edit_rules_get_exception(self): def test_edit_rules_get_exception(self):
exception = novaclient_exceptions.ClientException('ClientException', exception = novaclient_exceptions.ClientException('ClientException',
message='ClientException') message='ClientException')
self.mox.StubOutWithMock(api, 'security_group_get') self.mox.StubOutWithMock(api, 'security_group_get')
api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndRaise( api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID) \
exception) .AndRaise(exception)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(SG_EDIT_RULE_URL) res = self.client.get(SG_EDIT_RULE_URL)
self.assertRedirectsNoFollow(res, SG_INDEX_URL) self.assertRedirects(res, INDEX_URL)
def test_edit_rules_add_rule(self): def test_edit_rules_add_rule(self):
RULE_ID = '1' RULE_ID = '1'
@ -194,72 +209,62 @@ class SecurityGroupsViewTests(test.BaseViewTests):
def test_edit_rules_delete_rule(self): def test_edit_rules_delete_rule(self):
RULE_ID = '1' RULE_ID = '1'
formData = {'method': 'DeleteRule',
'tenant_id': self.TEST_TENANT,
'security_group_rule_id': RULE_ID,
}
self.mox.StubOutWithMock(api, 'security_group_rule_delete') self.mox.StubOutWithMock(api, 'security_group_rule_delete')
api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID) api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.post(SG_EDIT_RULE_URL, formData) form_data = {"action": "rules__delete__%s" % RULE_ID}
req = self.factory.post(SG_EDIT_RULE_URL, form_data)
table = RulesTable(req, self.rules)
handled = table.maybe_handle()
self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL) self.assertEqual(handled['location'], SG_EDIT_RULE_URL)
def test_edit_rules_delete_rule_exception(self): def test_edit_rules_delete_rule_exception(self):
exception = novaclient_exceptions.ClientException('ClientException',
message='ClientException')
RULE_ID = '1' RULE_ID = '1'
formData = {'method': 'DeleteRule',
'tenant_id': self.TEST_TENANT,
'security_group_rule_id': RULE_ID,
}
self.mox.StubOutWithMock(api, 'security_group_rule_delete') self.mox.StubOutWithMock(api, 'security_group_rule_delete')
api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID).\
AndRaise(exception)
self.mox.ReplayAll()
res = self.client.post(SG_EDIT_RULE_URL, formData)
self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL)
def test_delete_group(self):
formData = {'method': 'DeleteGroup',
'tenant_id': self.TEST_TENANT,
'security_group_id': SECGROUP_ID,
}
self.mox.StubOutWithMock(api, 'security_group_delete')
api.security_group_delete(IsA(http.HttpRequest), SECGROUP_ID)
self.mox.ReplayAll()
res = self.client.post(SG_INDEX_URL, formData)
self.assertRedirectsNoFollow(res, SG_INDEX_URL)
def test_delete_group_exception(self):
exception = novaclient_exceptions.ClientException('ClientException', exception = novaclient_exceptions.ClientException('ClientException',
message='ClientException') message='ClientException')
api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID) \
.AndRaise(exception)
formData = {'method': 'DeleteGroup', self.mox.ReplayAll()
'tenant_id': self.TEST_TENANT,
'security_group_id': SECGROUP_ID,
}
form_data = {"action": "rules__delete__%s" % RULE_ID}
req = self.factory.post(SG_EDIT_RULE_URL, form_data)
table = RulesTable(req, self.rules)
handled = table.maybe_handle()
self.assertEqual(handled['location'], SG_EDIT_RULE_URL)
def test_delete_group(self):
self.mox.StubOutWithMock(api, 'security_group_delete') self.mox.StubOutWithMock(api, 'security_group_delete')
api.security_group_delete(IsA(http.HttpRequest), SECGROUP_ID).\ api.security_group_delete(IsA(http.HttpRequest), '2')
self.mox.ReplayAll()
form_data = {"action": "security_groups__delete__%s" % '2'}
req = self.factory.post(INDEX_URL, form_data)
table = SecurityGroupsTable(req, self.security_groups)
handled = table.maybe_handle()
self.assertEqual(handled['location'], INDEX_URL)
def test_delete_group_exception(self):
self.mox.StubOutWithMock(api, 'security_group_delete')
exception = novaclient_exceptions.ClientException('ClientException',
message='ClientException')
api.security_group_delete(IsA(http.HttpRequest), '2').\
AndRaise(exception) AndRaise(exception)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.post(SG_INDEX_URL, formData) form_data = {"action": "security_groups__delete__%s" % '2'}
req = self.factory.post(INDEX_URL, form_data)
table = SecurityGroupsTable(req, self.security_groups)
handled = table.maybe_handle()
self.assertRedirectsNoFollow(res, SG_INDEX_URL) self.assertEqual(handled['location'], INDEX_URL)

View File

@ -20,10 +20,11 @@
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
from .views import CreateView, EditRulesView
urlpatterns = patterns(
'horizon.dashboards.nova.access_and_security.security_groups.views', urlpatterns = patterns('',
url(r'^$', 'index', name='index'), url(r'^create/$', CreateView.as_view(), name='create'),
url(r'^create/$', 'create', name='create'), url(r'^(?P<security_group_id>[^/]+)/edit_rules/$',
url(r'^(?P<security_group_id>[^/]+)/edit_rules/$', 'edit_rules', EditRulesView.as_view(),
name='edit_rules')) name='edit_rules'))

View File

@ -24,81 +24,67 @@ Views for managing Nova instances.
import logging import logging
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django import shortcuts from django import shortcuts
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from novaclient import exceptions as novaclient_exceptions from novaclient import exceptions as novaclient_exceptions
from horizon import api from horizon import api
from horizon.dashboards.nova.access_and_security.security_groups.forms import \ from horizon import forms
(CreateGroup, DeleteGroup, AddRule, DeleteRule) from horizon import tables
from .forms import CreateGroup, AddRule
from .tables import RulesTable
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@login_required class EditRulesView(tables.DataTableView):
def index(request): table_class = RulesTable
tenant_id = request.user.tenant_id template_name = 'nova/access_and_security/security_groups/edit_rules.html'
delete_form, handled = DeleteGroup.maybe_handle(request,
initial={'tenant_id': tenant_id})
form = CreateGroup(initial={'tenant_id': tenant_id})
if handled:
return handled
try: def get_data(self):
security_groups = api.security_group_list(request) security_group_id = self.kwargs['security_group_id']
except novaclient_exceptions.ClientException, e: try:
security_groups = [] self.object = api.security_group_get(self.request,
LOG.exception("ClientException in security_groups index") security_group_id)
messages.error(request, _('Error fetching security_groups: %s') rules = [api.nova.SecurityGroupRule(rule) for
% e.message) rule in self.object.rules]
except novaclient_exceptions.ClientException, e:
self.object = None
rules = []
LOG.exception("ClientException in security_groups rules edit")
messages.error(self.request,
_('Error getting security_group: %s') % e)
return rules
return shortcuts.render(request, def handle_form(self):
'nova/access_and_security/security_groups/index.html', { tenant_id = self.request.user.tenant_id
'security_groups': security_groups, initial = {'tenant_id': tenant_id,
'form': form, 'security_group_id': self.kwargs['security_group_id']}
'delete_form': delete_form}) return AddRule.maybe_handle(self.request, initial=initial)
def get(self, request, *args, **kwargs):
form, handled = self.handle_form()
if handled:
return handled
tables = self.get_tables()
if not self.object:
return shortcuts.redirect("horizon:nova:access_and_security:index")
context = self.get_context_data(**kwargs)
context['form'] = form
context['security_group'] = self.object
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
form, handled = self.handle_form()
if handled:
return handled
return super(EditRulesView, self).post(request, *args, **kwargs)
@login_required class CreateView(forms.ModalFormView):
def edit_rules(request, security_group_id): form_class = CreateGroup
tenant_id = request.user.tenant_id template_name = 'nova/access_and_security/security_groups/create.html'
add_form, handled = AddRule.maybe_handle(request,
initial={'tenant_id': tenant_id,
'security_group_id': security_group_id})
if handled:
return handled
delete_form, handled = DeleteRule.maybe_handle(request, def get_initial(self):
initial={'tenant_id': tenant_id, return {"tenant_id": self.request.user.tenant_id}
'security_group_id': security_group_id})
if handled:
return handled
try:
security_group = api.security_group_get(request, security_group_id)
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in security_groups rules edit")
messages.error(request, _('Error getting security_group: %s')
% e.message)
return shortcuts.redirect('horizon:nova:access_and_security:index')
return shortcuts.render(request,
'nova/access_and_security/security_groups/edit_rules.html', {
'security_group': security_group,
'delete_form': delete_form,
'form': add_form})
@login_required
def create(request):
tenant_id = request.user.tenant_id
form, handled = CreateGroup.maybe_handle(request,
initial={'tenant_id': tenant_id})
if handled:
return handled
return shortcuts.render(request,
'nova/access_and_security/security_groups/create.html', {
'form': form})

View File

@ -74,6 +74,7 @@ class AccessAndSecurityTests(test.BaseViewTests):
self.assertTemplateUsed(res, 'nova/access_and_security/index.html') self.assertTemplateUsed(res, 'nova/access_and_security/index.html')
self.assertItemsEqual(res.context['keypairs_table'].data, self.assertItemsEqual(res.context['keypairs_table'].data,
self.keypairs) self.keypairs)
self.assertItemsEqual(res.context['security_groups'], self.assertItemsEqual(res.context['security_groups_table'].data,
self.security_groups) self.security_groups)
self.assertItemsEqual(res.context['floating_ips'], self.floating_ips) self.assertItemsEqual(res.context['floating_ips_table'].data,
self.floating_ips)

View File

@ -20,17 +20,13 @@
from django.conf.urls.defaults import * from django.conf.urls.defaults import *
import horizon from .floating_ips import urls as fip_urls
from .keypairs import urls as keypair_urls
from .security_groups import urls as sec_group_urls
from .views import IndexView
from horizon.dashboards.nova.access_and_security.keypairs import urls \ urlpatterns = patterns('',
as keypair_urls url(r'^$', IndexView.as_view(), name='index'),
from horizon.dashboards.nova.access_and_security.floating_ips import urls \
as fip_urls
from horizon.dashboards.nova.access_and_security.security_groups import urls \
as sec_group_urls
urlpatterns = patterns('horizon.dashboards.nova.access_and_security',
url(r'^$', 'views.index', name='index'),
url(r'keypairs/', include(keypair_urls, namespace='keypairs')), url(r'keypairs/', include(keypair_urls, namespace='keypairs')),
url(r'floating_ips/', include(fip_urls, namespace='floating_ips')), url(r'floating_ips/', include(fip_urls, namespace='floating_ips')),
url(r'security_groups/', url(r'security_groups/',

View File

@ -22,86 +22,52 @@
""" """
Views for Instances and Volumes. Views for Instances and Volumes.
""" """
import datetime
import logging import logging
from django import http
from django import shortcuts
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from novaclient import exceptions as novaclient_exceptions from novaclient import exceptions as novaclient_exceptions
import openstackx.api.exceptions as api_exceptions
from horizon import api from horizon import api
from horizon import forms from horizon import tables
from horizon import test
from .keypairs.forms import DeleteKeypair
from .keypairs.tables import KeypairsTable from .keypairs.tables import KeypairsTable
from .security_groups.forms import CreateGroup, DeleteGroup from .floating_ips.tables import FloatingIPsTable
from .floating_ips.forms import (ReleaseFloatingIp, FloatingIpDisassociate, from .security_groups.tables import SecurityGroupsTable
FloatingIpAllocate)
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@login_required class IndexView(tables.MultiTableView):
def index(request): table_classes = (KeypairsTable, SecurityGroupsTable, FloatingIPsTable)
tenant_id = request.user.tenant_id template_name = 'nova/access_and_security/index.html'
for f in (CreateGroup, DeleteGroup, DeleteKeypair, ReleaseFloatingIp, def get_keypairs_data(self):
FloatingIpDisassociate, FloatingIpAllocate): try:
_unused, handled = f.maybe_handle(request) keypairs = api.nova.keypair_list(self.request)
if handled: except Exception, e:
return handled keypairs = []
LOG.exception("Exception in keypair index")
messages.error(self.request,
_('Keypair list is currently unavailable.'))
return keypairs
# NOTE(gabriel): This is all temporary until all tables def get_security_groups_data(self):
# in this view are converted to DataTables. try:
try: security_groups = api.security_group_list(self.request)
keypairs = api.nova.keypair_list(request) except novaclient_exceptions.ClientException, e:
except Exception, e: security_groups = []
keypairs = [] LOG.exception("ClientException in security_groups index")
LOG.exception("Exception in keypair index") messages.error(self.request,
messages.error(request, _('Error fetching security_groups: %s') % e)
_('Keypair list is currently unavailable.')) return security_groups
keypairs_table = KeypairsTable(request, keypairs)
handled = keypairs_table.maybe_handle()
if handled:
return handled
try: def get_floating_ips_data(self):
security_groups = api.security_group_list(request) try:
except novaclient_exceptions.ClientException, e: floating_ips = api.tenant_floating_ip_list(self.request)
security_groups = [] except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in security_groups index") floating_ips = []
messages.error(request, _('Error fetching security_groups: %s') LOG.exception("ClientException in floating ip index")
% e.message) messages.error(self.request,
try: _('Error fetching floating ips: %s') % e)
floating_ips = api.tenant_floating_ip_list(request) return floating_ips
except novaclient_exceptions.ClientException, e:
floating_ips = []
LOG.exception("ClientException in floating ip index")
messages.error(request,
_('Error fetching floating ips: %s') % e.message)
context = {'keypairs_table': keypairs_table,
'floating_ips': floating_ips,
'security_groups': security_groups,
'keypair_delete_form': DeleteKeypair(),
'disassociate_form': FloatingIpDisassociate(),
'release_form': ReleaseFloatingIp(),
'allocate_form': FloatingIpAllocate(initial={
'tenant_id': request.user.tenant_id}),
'sec_group_create_form': CreateGroup(
initial={'tenant_id': tenant_id}),
'sec_group_delete_form': DeleteGroup.maybe_handle(request,
initial={'tenant_id': tenant_id})}
if request.is_ajax():
template = 'nova/access_and_security/index_ajax.html'
context['hide'] = True
else:
template = 'nova/access_and_security/index.html'
return shortcuts.render(request, template, context)

View File

@ -1,8 +0,0 @@
{% load i18n %}
<form id="form_allocate" action="." method="post">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
<input class="btn small primary" type="submit" value="{% trans "Allocate IP" %}" />
</form>

View File

@ -2,9 +2,9 @@
{% load i18n %} {% load i18n %}
{% block form_id %}associate_floating_ip_form{% endblock %} {% block form_id %}associate_floating_ip_form{% endblock %}
{% block form_action %}{% url horizon:nova:access_and_security:floating_ips:associate floating_ip_id %}{% endblock %} {% block form_action %}{% url horizon:nova:access_and_security:floating_ips:associate floating_ip.id %}{% endblock %}
{% block modal-header %}Associate Floating IP{% endblock %} {% block modal-header %}{% trans "Associate Floating IP" %}{% endblock %}
{% block modal-body %} {% block modal-body %}
<div class="left"> <div class="left">

View File

@ -1,9 +0,0 @@
{% load i18n %}
<form id="form_release_{{ ip.id }}" class="form-delete" method="post">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
<input name="floating_ip_id" type="hidden" value="{{ ip.id }}" />
<input id="delete_{{ ip.id }}" class="btn small delete" title="Floating IP: {{ ip.id }} ({{ ip.ip }})" type="submit" value="{% trans "Disassociate with instance." %}" />
</form>

View File

@ -1,56 +0,0 @@
{% load i18n %}
<div class="table_title">
<h3>Floating IPs</h3>
<div class="table_actions">
{% include "nova/access_and_security/floating_ips/_allocate.html" with form=allocate_form %}
<div class="floating_ips table_search">
<form action="#">
<input class="span3" type="text">
</form>
</div>
<a class="inspect" href="#">{% trans "inspect" %}</a>
</div>
</div>
<table id="floating_ips" class="zebra-striped sortable">
<thead>
<tr>
<th></th>
<th>IP</th>
<th>Instance</th>
<th colspan="2">Actions</th>
</tr>
</thead>
{% for ip in floating_ips %}
<tr class="{% cycle 'odd' 'even' %}">
<td class="select">
<input type="checkbox" name="ip_{{ ip.id }}" value="ip_{{ ip.id }}" id="ip_select_{{ ip.id }}" />
</td>
<td>{{ip.ip }}</td>
<td>
{% if ip.fixed_ip %}
<ul>
<li><span>{% trans "Instance ID:" %}</span> {{ ip.instance_id }}</li>
<li><span>{% trans "Fixed IP:" %}</span> {{ ip.fixed_ip }}</li>
</ul>
{% else %}
None
{% endif %}
</td>
<td>
{% if ip.fixed_ip %}
{% include "nova/access_and_security/floating_ips/_disassociate.html" with form=disassociate_form %}
{% else %}
<a class="btn primary small ajax-modal" href="{% url horizon:nova:access_and_security:floating_ips:associate ip.id %}">{% trans "Associate to instance" %}</a>
{% endif %}
</td>
<td id="name_{{ ip.name }}" class="actions">
<a class="more-actions" href="#">More</a>
<ul>
<li class="form">{% include "nova/access_and_security/floating_ips/_release.html" with form=release_form %}</li>
</ul>
</td>
</tr>
{% endfor %}
</table>

View File

@ -1,9 +0,0 @@
{% load i18n %}
<form id="form_release_{{ ip.id }}" class="form-delete" method="post">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
<input name="floating_ip_id" type="hidden" value="{{ ip.id }}" />
<input id="delete_{{ ip.id }}" class="btn small delete" title="Floating IP: {{ ip.id }} ({{ ip.ip }})" type="submit" value="{% trans "Release" %}" />
</form>

View File

@ -1,22 +0,0 @@
{% extends 'nova/base.html' %}
{% load i18n %}
{% block title %}Floating IPs{% endblock %}
{% block page_header %}
{% url horizon:nova:access_and_security:floating_ips:index as refresh_link %}
{# to make searchable false, just remove it from the include statement #}
{% include "horizon/common/_page_header.html" with title=_("Floating IPs") refresh_link=refresh_link searchable="true" %}
{% endblock page_header %}
{% block dash_main %}
{% if floating_ips %}
{% include 'nova/access_and_security/floating_ips/_list.html' %}
{% else %}
<div class="alert-message block-message info">
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no floating ips assigned to your tenant." %}</p>
<div class="alert-actions">
{% include "nova/access_and_security/floating_ips/_allocate.html" with form=allocate_form %}
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -7,43 +7,15 @@
{% endblock page_header %} {% endblock page_header %}
{% block dash_main %} {% block dash_main %}
<div id="floating_ips"> <div id="floating_ips">
{% if floating_ips %} {{ floating_ips_table.render }}
{% include 'nova/access_and_security/floating_ips/_list.html' %}
{% else %}
<div class="alert-message block-message info">
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no floating ips assigned to your tenant." %}</p>
<div class="alert-actions">
{% include "nova/access_and_security/floating_ips/_allocate.html" with form=allocate_form %}
</div>
</div>
{% endif %}
</div> </div>
<div id="security_groups"> <div id="security_groups">
{% url horizon:nova:access_and_security:security_groups:create as create_sec_url %} {{ security_groups_table.render }}
{% if security_groups %}
{% include 'nova/access_and_security/security_groups/_list.html' %}
{% else %}
<div class="message_box info">
<h2>{% trans "Info" %}</h2>
<p>{% blocktrans %}There are currently no security groups. <a href='{{ create_sec_url }}'>Create A Security Group</a>{% endblocktrans %}</p>
</div>
{% endif %}
</div> </div>
<div id="keypairs"> <div id="keypairs">
{% if keypairs_table.data %} {{ keypairs_table.render }}
{{ keypairs_table.render }} </div>
{% else %}
<div class="alert-message block-message info">
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no keypairs." %}</p>
<div class="alert-actions">
<a id="keypairs_create_link" class="btn primary small" href="{% url horizon:nova:access_and_security:keypairs:create %}">{% trans "Create New Keypair" %}</a>
<a id="keypairs_import_link" class="btn small" href="{% url horizon:nova:access_and_security:keypairs:import %}">{% trans "Import Keypair" %}</a>
</div>
</div>
{% endif %}
</div>
{% endblock %} {% endblock %}

View File

@ -1,9 +0,0 @@
{% load i18n %}
<form id="form_delete_{{ keypair.name }}" class="form-delete" method="post">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
<input name="keypair_id" type="hidden" value="{{ keypair.name }}" />
<input id="delete_{{ keypair.name }}" class="btn small danger delete" title="Keypair: {{ keypair.name }}" type="submit" value="{% trans "Delete" %}" />
</form>

View File

@ -1,26 +0,0 @@
{% load i18n %}
<form id="import_keypair_form" action="{% block form_action %}{% endblock %}" method="post">
{% csrf_token %}
<fieldset>
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% for field in form.visible_fields %}
<div class="clearfix{% if field.errors %} error{% endif %}">
{{ field.label_tag }}
{% if field.errors %}
{% for error in field.errors %}
<span class="help-inline">{{ error }}</span>
{% endfor %}
{% endif %}
<span class="help-block">{{ field.help_text }}</span>
<div class="input">
{{ field }}
</div>
</div>
{% endfor %}
{% block modal-footer %}
<input type="submit" value="{% trans "Add Keypair" %}" />
{% endblock %}
</fieldset>
</form>

View File

@ -1,22 +0,0 @@
{% extends 'nova/base.html' %}
{% load i18n %}
{% block title %}Keypairs{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Keypairs") %}
{% endblock page_header %}
{% block dash_main %}
{% if table.data %}
{{ table.render }}
{% else %}
<div class="alert-message block-message info">
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no keypairs." %}</p>
<div class="alert-actions">
<a id="keypairs_create_link" class="btn primary small ajax-modal" href="{% url horizon:nova:access_and_security:keypairs:create %}">{% trans "Create New Keypair" %}</a>
<a id="keypairs_import_link" class="btn small ajax-modal" href="{% url horizon:nova:access_and_security:keypairs:import %}">{% trans "Import Keypair" %}</a>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,9 +0,0 @@
{% load i18n %}
<form id="form_delete_{{ security_group.id }}" class="form-delete" method="post">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
<input name="security_group_id" type="hidden" value="{{ security_group.id }}" />
<input id="delete_{{ security_group.id }}" class="btn small danger delete" title="Security Group: {{ security_group.id }}" type="submit" value="{% trans "Delete" %}" />
</form>

View File

@ -1,6 +1,5 @@
{% load i18n %} {% load i18n %}
<div id="security_group_rule_modal" class="{% block modal_class %}modal{% if hide %} hide {% else %} static_page{% endif %}{% endblock %}"> <div id="security_group_rule_modal" class="{% block modal_class %}modal{% if hide %} hide {% else %} static_page{% endif %}{% endblock %}">
<div class="modal-header"> <div class="modal-header">
{% if hide %}<a href="#" class="close">&times;</a>{% endif %} {% if hide %}<a href="#" class="close">&times;</a>{% endif %}
@ -8,35 +7,7 @@
</div> </div>
<div class="modal-body clearfix"> <div class="modal-body clearfix">
<div class="right"> <div class="right">
<h3> {% trans "Rules for Security Group" %} '{{security_group.name}}'</h3> {{ table.render }}
<table id="security_groups" class="zebra-striped">
<tr>
<th>{% trans "IP Protocol" %}</th>
<th>{% trans "From Port" %}</th>
<th>{% trans "To Port" %}</th>
<th>{% trans "CIDR" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
{% for rule in security_group.rules %}
<tr class="{% cycle 'odd' 'even' %}">
<td>{{ rule.ip_protocol }}</td>
<td>{{ rule.from_port }}</td>
<td>{{ rule.to_port }}</td>
<td>{{ rule.ip_range.cidr }}</td>
<td id="actions">
<ul>
<li class="form">{% include "nova/access_and_security/security_groups/_delete_rule.html" with form=delete_form %}</li>
</ul>
</td>
</tr>
{% empty %}
<tr>
<td colspan="100%">
{% trans "No rules for this security group" %}
</td>
</tr>
{% endfor %}
</table>
</div> </div>
<form id="edit_security_group_rule_form" action="{% url horizon:nova:access_and_security:security_groups:edit_rules security_group.id %}" method="post"> <form id="edit_security_group_rule_form" action="{% url horizon:nova:access_and_security:security_groups:edit_rules security_group.id %}" method="post">
{% csrf_token %} {% csrf_token %}
@ -50,6 +21,3 @@
</div> </div>
</form> </form>
</div> </div>
{% block modal-footer %}
{% endblock %}

View File

@ -1,49 +0,0 @@
{% load i18n %}
<div class="table_title">
<h3>{% trans "Security Groups" %}</h3>
<div class="table_actions">
<a id="security_groups_create_link" class="btn primary small" href="{{ create_sec_url }}">{% trans "Create Security Group" %}</a>
<div class="security_group table_search">
<form action="#">
<input class="span3" type="text">
</form>
</div>
<a class="inspect" href="#">{% trans "inspect" %}</a>
</div>
</div>
<table id="security_groups" class="zebra-striped sortable">
<thead>
<tr>
<th></th>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th colspan="2">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for security_group in security_groups %}
<tr class="{% cycle 'odd' 'even' %}">
<td class="select">
<input type="checkbox" name="security_group_{{ security_group.id }}" value="security_group_{{ security_group.id }}" id="security_group_select_{{ security_group.id }}" />
</td>
<td><a href="{% url horizon:nova:access_and_security:security_groups:edit_rules security_group.id %}">{{ security_group.name }}</a></td>
<td>{{ security_group.description }}</td>
<td>
<a class="btn primary small" href="{% url horizon:nova:access_and_security:security_groups:edit_rules security_group.id %}">{% trans "Edit Rules" %}</a>
</td>
<td id="name_{{security_group.name}}" class="actions">
{% if security_group.name != 'default' %}
<a class="more-actions" href="#">More</a>
<ul>
<li class="form">{% include "nova/access_and_security/security_groups/_delete.html" with form=delete_form %}</li>
</ul>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -7,5 +7,5 @@
{% endblock page_header %} {% endblock page_header %}
{% block dash_main %} {% block dash_main %}
{% include 'nova/access_and_security/security_groups/_create.html' with form=form %} {% include 'nova/access_and_security/security_groups/_create.html' %}
{% endblock %} {% endblock %}

View File

@ -1,23 +0,0 @@
{% extends 'nova/base.html' %}
{% load i18n %}
{% block title %}Security Groups{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Security Groups") %}
{% endblock page_header %}
{% block dash_main %}
{% if security_groups %}
{% url horizon:nova:access_and_security:security_groups:create as create_sec_url %}
{% include 'nova/access_and_security/security_groups/_list.html' %}
{% else %}
<div class="message_box info">
{% url horizon:nova:access_and_security:security_groups:create as dash_sec_url %}
<h2>{% trans "Info" %}</h2>
<p>{% blocktrans %}There are currently no security groups. <a href='{{ dash_sec_url }}' data-controls-modal="create_security_group_modal" data-backdrop="static">Create A Security Group</a>{% endblocktrans %}</p>
</div>
{% endif %}
{% include 'nova/access_and_security/security_groups/_create.html' with form=form hide=True%}
{% endblock %}

View File

@ -17,4 +17,4 @@
# Convenience imports for public API components. # Convenience imports for public API components.
from .actions import Action, LinkAction, FilterAction from .actions import Action, LinkAction, FilterAction
from .base import DataTable, Column from .base import DataTable, Column
from .views import DataTableView from .views import DataTableView, MultiTableView

View File

@ -30,6 +30,7 @@ class BaseAction(object):
handles_multiple = False handles_multiple = False
attrs = {} attrs = {}
name = None name = None
requires_input = False
def allowed(self, request, datum): def allowed(self, request, datum):
""" Determine whether this action is allowed for the current request. """ Determine whether this action is allowed for the current request.
@ -84,6 +85,11 @@ class Action(BaseAction):
The HTTP method for this action. Defaults to ``POST``. Other methods The HTTP method for this action. Defaults to ``POST``. Other methods
may or may not succeed currently. may or may not succeed currently.
.. attribute:: requires_input
Boolean value indicating whether or not this action can be taken
without any additional input (e.g. an object id). Defaults to ``True``.
At least one of the following methods must be defined: At least one of the following methods must be defined:
.. method:: single(self, data_table, request, object_id) .. method:: single(self, data_table, request, object_id)
@ -104,10 +110,11 @@ class Action(BaseAction):
into a list containing only the single object id. into a list containing only the single object id.
""" """
method = "POST" method = "POST"
requires_input = True
def __init__(self, verbose_name=None, verbose_name_plural=None, def __init__(self, verbose_name=None, verbose_name_plural=None,
single_func=None, multiple_func=None, handle_func=None, single_func=None, multiple_func=None, handle_func=None,
handles_multiple=False, attrs=None): handles_multiple=False, attrs=None, requires_input=True):
super(Action, self).__init__() super(Action, self).__init__()
self.name = unicode(getattr(self, 'name', self.__class__.__name__)) self.name = unicode(getattr(self, 'name', self.__class__.__name__))
verbose_name = verbose_name or self.name.title() verbose_name = verbose_name or self.name.title()
@ -121,6 +128,9 @@ class Action(BaseAction):
self.handles_multiple = getattr(self, self.handles_multiple = getattr(self,
"handles_multiple", "handles_multiple",
handles_multiple) handles_multiple)
self.requires_input = getattr(self,
"requires_input",
requires_input)
if attrs: if attrs:
self.attrs.update(attrs) self.attrs.update(attrs)

View File

@ -260,8 +260,9 @@ class Row(object):
for column in table.columns.values(): for column in table.columns.values():
if column.auto == "multi_select": if column.auto == "multi_select":
widget = forms.CheckboxInput(check_test=False) widget = forms.CheckboxInput(check_test=False)
# Convert value to string to avoid accidental type conversion
data = widget.render('object_ids', data = widget.render('object_ids',
table.get_object_id(datum)) str(table.get_object_id(datum)))
column._data_cache[datum] = data column._data_cache[datum] = data
elif column.auto == "actions": elif column.auto == "actions":
data = table.render_row_actions(datum) data = table.render_row_actions(datum)
@ -623,6 +624,10 @@ class DataTable(object):
context = template.RequestContext(self._meta.request, extra_context) context = template.RequestContext(self._meta.request, extra_context)
return table_template.render(context) return table_template.render(context)
def get_empty_message(self):
""" Returns the message to be displayed when there is no data. """
return _("No items to display.")
def get_object_by_id(self, lookup): def get_object_by_id(self, lookup):
""" """
Returns the data object from the table's dataset which matches Returns the data object from the table's dataset which matches
@ -717,7 +722,7 @@ class DataTable(object):
# See if we have a list of ids # See if we have a list of ids
obj_ids = obj_ids or self._meta.request.POST.getlist('object_ids') obj_ids = obj_ids or self._meta.request.POST.getlist('object_ids')
action = self.base_actions.get(action_name, None) action = self.base_actions.get(action_name, None)
if action and (obj_id or obj_ids): if action and (not action.requires_input or obj_id or obj_ids):
# Single handling is easy # Single handling is easy
if not action.handles_multiple: if not action.handles_multiple:
response = action.single(self, self._meta.request, obj_id) response = action.single(self, self._meta.request, obj_id)
@ -727,6 +732,9 @@ class DataTable(object):
obj_ids = [obj_id] obj_ids = [obj_id]
response = action.multiple(self, self._meta.request, obj_ids) response = action.multiple(self, self._meta.request, obj_ids)
return response return response
elif action and action.requires_input and not (obj_id or obj_ids):
messages.info(self._meta.request,
_("Please select a row before taking that action."))
return None return None
def maybe_handle(self): def maybe_handle(self):

View File

@ -17,7 +17,74 @@
from django.views import generic from django.views import generic
class DataTableView(generic.TemplateView): class MultiTableView(generic.TemplateView):
"""
A class-based generic view to handle the display and processing of
multiple :class:`~horizon.tables.DataTable` classes in a single view.
Three steps are required to use this view: set the ``table_classes``
attribute with a tuple of the desired
:class:`~horizon.tables.DataTable` classes;
define a ``get_{{ table_name }}_data`` method for each table class
which returns a set of data for that table; and specify a template for
the ``template_name`` attribute.
"""
def __init__(self, *args, **kwargs):
super(MultiTableView, self).__init__(*args, **kwargs)
self.table_classes = getattr(self, "table_classes", [])
self._data = {}
self._tables = {}
def get_data(self):
if not self._data:
for table in self.table_classes:
func_name = "get_%s_data" % table._meta.name
data_func = getattr(self, func_name, None)
if data_func is None:
cls_name = self.__class__.__name__
raise NotImplementedError("You must define a %s method "
"on %s." % (func_name, cls_name))
self._data[table._meta.name] = data_func()
return self._data
def get_tables(self):
if not self.table_classes:
raise AttributeError('You must specify a one or more DataTable '
'classes for the "table_classes" attribute '
'on %s.' % self.__class__.__name__)
if not self._tables:
for table in self.table_classes:
func_name = "get_%s_table" % table._meta.name
table_func = getattr(self, func_name, None)
data = self.get_data()[table._meta.name]
if table_func is None:
tbl = table(self.request, data)
else:
tbl = table_func(self, self.request, data)
self._tables[table._meta.name] = tbl
return self._tables
def get_context_data(self, **kwargs):
context = super(MultiTableView, self).get_context_data(**kwargs)
tables = self.get_tables()
for name, table in tables.items():
context["%s_table" % name] = table
return context
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
tables = self.get_tables().values()
for table in tables:
handled = table.maybe_handle()
if handled:
return handled
return self.get(request, *args, **kwargs)
class DataTableView(MultiTableView):
""" A class-based generic view to handle basic DataTable processing. """ A class-based generic view to handle basic DataTable processing.
Three steps are required to use this view: set the ``table_class`` Three steps are required to use this view: set the ``table_class``
@ -32,6 +99,10 @@ class DataTableView(generic.TemplateView):
raise NotImplementedError('You must define a "get_data" method on %s.' raise NotImplementedError('You must define a "get_data" method on %s.'
% self.__class__.__name__) % self.__class__.__name__)
def get_tables(self):
table = self.get_table()
return {table._meta.name: table}
def get_table(self): def get_table(self):
if not self.table_class: if not self.table_class:
raise AttributeError('You must specify a DataTable class for the ' raise AttributeError('You must specify a DataTable class for the '
@ -41,15 +112,7 @@ class DataTableView(generic.TemplateView):
self.table = self.table_class(self.request, self.get_data()) self.table = self.table_class(self.request, self.get_data())
return self.table return self.table
def get(self, request, *args, **kwargs): def get_context_data(self, **kwargs):
table = self.get_table() context = super(DataTableView, self).get_context_data(**kwargs)
context = self.get_context_data(**kwargs) context[self.context_object_name] = self.table
context[self.context_object_name] = table return context
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
table = self.get_table()
handled = table.maybe_handle()
if handled:
return handled
return self.get(request, *args, **kwargs)

View File

@ -3,22 +3,33 @@
<h3 class='table_title'>{{ table }}</h3> <h3 class='table_title'>{{ table }}</h3>
{{ table.render_table_actions }} {{ table.render_table_actions }}
</div> </div>
{% with columns=table.get_columns rows=table.get_rows %}
<table id="{{ table.name }}" class="zebra-striped"> <table id="{{ table.name }}" class="zebra-striped">
<thead> <thead>
<tr> <tr>
{% for column in table.get_columns %} {% for column in columns %}
<th class="{{ column.get_classes }}">{{ column }}</th> <th class="{{ column.get_classes }}">{{ column }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for row in table.get_rows %} {% for row in rows %}
<tr id="{{ row.id }}" class="{% cycle 'odd' 'even' %} {{ row.status_class }}"> <tr id="{{ row.id }}" class="{% cycle 'odd' 'even' %} {{ row.status_class }}">
{% for cell in row %} {% for cell in row %}
<td class="{{ cell.get_classes }}">{{ cell.value }}</td> <td class="{{ cell.get_classes }}">{{ cell.value }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
{% empty %}
<tr class="{% cycle 'odd' 'even' %} empty">
<td colspan="{{ table.get_columns|length }}">{{ table.get_empty_message }}</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot>
<tr>
<td colspan="{{ table.get_columns|length }}">Displaying {{ rows|length }} item{{ rows|pluralize }}</td>
</td>
</tfoot>
</table> </table>
{% endwith %}
</form> </form>

View File

@ -25,6 +25,8 @@ from django import shortcuts
from django import test as django_test from django import test as django_test
from django import template as django_template from django import template as django_template
from django.conf import settings from django.conf import settings
from django.contrib.messages.storage import default_storage
from django.test.client import RequestFactory
import httplib2 import httplib2
import mox import mox
@ -60,6 +62,14 @@ def utcnow():
utcnow.override_time = None utcnow.override_time = None
class RequestFactoryWithMessages(RequestFactory):
def post(self, *args, **kwargs):
req = super(RequestFactoryWithMessages, self).post(*args, **kwargs)
req.session = []
req._messages = default_storage(req)
return req
class TestCase(django_test.TestCase): class TestCase(django_test.TestCase):
TEST_STAFF_USER = 'staffUser' TEST_STAFF_USER = 'staffUser'
TEST_TENANT = '1' TEST_TENANT = '1'
@ -114,6 +124,7 @@ class TestCase(django_test.TestCase):
def setUp(self): def setUp(self):
self.mox = mox.Mox() self.mox = mox.Mox()
self.factory = RequestFactoryWithMessages()
def fake_conn_request(*args, **kwargs): def fake_conn_request(*args, **kwargs):
raise Exception("An external URI request tried to escape through " raise Exception("An external URI request tried to escape through "

View File

@ -17,7 +17,6 @@
from django import http from django import http
from django import shortcuts from django import shortcuts
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.client import RequestFactory
import horizon import horizon
from horizon import tables from horizon import tables
@ -110,13 +109,6 @@ class MyTable(tables.DataTable):
class DataTableTests(test.TestCase): class DataTableTests(test.TestCase):
def setUp(self):
super(DataTableTests, self).setUp()
self.factory = RequestFactory()
def tearDown(self):
super(DataTableTests, self).tearDown()
def test_table_instantiation(self): def test_table_instantiation(self):
""" Tests everything that happens when the table is instantiated. """ """ Tests everything that happens when the table is instantiated. """
self.table = MyTable(self.request, TEST_DATA) self.table = MyTable(self.request, TEST_DATA)
@ -372,6 +364,8 @@ class DataTableTests(test.TestCase):
('my_table', 'click', None)) ('my_table', 'click', None))
handled = self.table.maybe_handle() handled = self.table.maybe_handle()
self.assertEqual(handled, None) self.assertEqual(handled, None)
self.assertEqual(list(req._messages)[0].message,
"Please select a row before taking that action.")
# Filtering # Filtering
action_string = "my_table__filter__q" action_string = "my_table__filter__q"

View File

@ -343,6 +343,17 @@ table form {
vertical-align: middle; vertical-align: middle;
} }
#main_content table tr.empty td {
text-align: center;
}
#main_content table tfoot tr td {
border-top: 1px solid #DDD;
background-color: #F1F1F1;
font-size: 11px;
line-height: 14px;
}
.table_actions { .table_actions {
float: right; float: right;
min-width: 400px; min-width: 400px;

View File

@ -17,7 +17,14 @@ horizon.addInitFunction(function() {
} }
}); });
$('table.sortable').tablesorter(); $('table.sortable').each(function(index, table) {
var $table = $(table);
// Only trigger if we have actual data rows in the table.
// Calling on an empty table throws a javascript error.
if ($table.find('tbody tr').length) {
$table.tablesorter();
}
})
// Actions button dropdown behavior // Actions button dropdown behavior
$('.action.primary').mouseenter(function() { $('.action.primary').mouseenter(function() {

View File

@ -267,7 +267,7 @@ function run_tests {
echo "Running Horizon application tests" echo "Running Horizon application tests"
${command_wrapper} coverage erase ${command_wrapper} coverage erase
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test horizon --settings=horizon.tests.testsettings ${command_wrapper} coverage run $root/openstack-dashboard/manage.py test horizon --settings=horizon.tests.testsettings $testargs
# get results of the Horizon tests # get results of the Horizon tests
HORIZON_RESULT=$? HORIZON_RESULT=$?
@ -278,9 +278,9 @@ function run_tests {
cp $root/openstack-dashboard/local/local_settings.py.example $root/openstack-dashboard/local/local_settings.py cp $root/openstack-dashboard/local/local_settings.py.example $root/openstack-dashboard/local/local_settings.py
if [ $selenium -eq 1 ]; then if [ $selenium -eq 1 ]; then
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test dashboard --with-selenium --with-cherrypyliveserver ${command_wrapper} coverage run $root/openstack-dashboard/manage.py test dashboard --with-selenium --with-cherrypyliveserver $testargs
else else
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test dashboard ${command_wrapper} coverage run $root/openstack-dashboard/manage.py test dashboard $testargs
fi fi
# get results of the openstack-dashboard tests # get results of the openstack-dashboard tests
DASHBOARD_RESULT=$? DASHBOARD_RESULT=$?