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
: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']
class SecurityGroupRule(APIResourceWrapper):
"""Simple wrapper around
openstackx.extras.security_groups.SecurityGroupRule"""
_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']
class SecurityGroupRule(APIDictWrapper):
""" Simple wrapper for individual rules in a SecurityGroup. """
_attrs = ['ip_protocol', 'from_port', 'to_port', 'ip_range']
def novaclient(request):

View File

@ -33,22 +33,6 @@ from horizon import forms
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):
floating_ip_id = forms.CharField(widget=forms.HiddenInput())
floating_ip = forms.CharField(widget=forms.TextInput(
@ -69,58 +53,11 @@ class FloatingIpAssociate(forms.SelfHandlingForm):
data['floating_ip_id'])
LOG.info('Associating Floating IP "%s" with Instance "%s"'
% (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": data['floating_ip'],
"inst": data['instance_id']}))
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in FloatingIpAssociate")
messages.error(request, _('Error associating Floating IP: %s')
% e.message)
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')
messages.error(request, _('Error associating Floating IP: %s') % e)
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
from django import http
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from mox import IsA, IgnoreArg
@ -33,7 +32,7 @@ from horizon.dashboards.nova.access_and_security.floating_ips.forms import \
FloatingIpAssociate
FLOATING_IPS_INDEX = reverse('horizon:nova:access_and_security:index')
INDEX_URL = reverse('horizon:nova:access_and_security:index')
class FloatingIpViewTests(test.BaseViewTests):
@ -111,7 +110,7 @@ class FloatingIpViewTests(test.BaseViewTests):
'floating_ip': self.floating_ip.ip,
'method': 'FloatingIpAssociate'})
self.assertRedirects(res, FLOATING_IPS_INDEX)
self.assertRedirects(res, INDEX_URL)
def test_associate_post_with_exception(self):
server = self.server
@ -152,70 +151,51 @@ class FloatingIpViewTests(test.BaseViewTests):
'method': 'FloatingIpAssociate'})
self.assertRaises(novaclient_exceptions.ClientException)
self.assertRedirects(res, FLOATING_IPS_INDEX)
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')
self.assertRedirects(res, INDEX_URL)
def test_disassociate_post(self):
self.mox.StubOutWithMock(api, 'tenant_floating_ip_list')
api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
AndReturn(self.floating_ips)
self.mox.StubOutWithMock(api.nova, 'keypair_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)).\
AndReturn(self.security_groups)
self.mox.StubOutWithMock(api.nova, 'keypair_list')
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
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),
IsA(int)).\
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()
res = self.client.post(
reverse('horizon:nova:access_and_security:floating_ips:disassociate',
args=[1]),
{'floating_ip_id': self.floating_ip.id,
'method': 'FloatingIpDisassociate'})
self.assertRedirects(res, FLOATING_IPS_INDEX)
action = "floating_ips__disassociate__%s" % self.floating_ip.id
res = self.client.post(INDEX_URL, {"action": action})
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_disassociate_post_with_exception(self):
self.mox.StubOutWithMock(api, 'tenant_floating_ip_list')
api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
AndReturn(self.floating_ips)
self.mox.StubOutWithMock(api.nova, 'keypair_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)).\
AndReturn(self.security_groups)
self.mox.StubOutWithMock(api.nova, 'keypair_list')
api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
api.tenant_floating_ip_list(IsA(http.HttpRequest)).\
AndReturn(self.floating_ips)
self.mox.StubOutWithMock(api, 'server_remove_floating_ip')
exception = novaclient_exceptions.ClientException('ClientException',
message='clientException')
api.server_remove_floating_ip(IsA(http.HttpRequest),
IsA(int),
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()
res = self.client.post(
reverse('horizon:nova:access_and_security:floating_ips:disassociate',
args=[1]),
{'floating_ip_id': self.floating_ip.id,
'method': 'FloatingIpDisassociate'})
action = "floating_ips__disassociate__%s" % self.floating_ip.id
res = self.client.post(INDEX_URL, {"action": action})
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 .views import AssociateView
urlpatterns = patterns(
'horizon.dashboards.nova.access_and_security.floating_ips.views',
url(r'^$', 'index', name='index'),
url(r'^(?P<ip_id>[^/]+)/associate/$', 'associate', name='associate'),
url(r'^(?P<ip_id>[^/]+)/disassociate/$', 'disassociate',
name='disassociate'))
urlpatterns = patterns('',
url(r'^(?P<ip_id>[^/]+)/associate/$',
AssociateView.as_view(),
name='associate')
)

View File

@ -23,76 +23,36 @@ Views for managing Nova floating IPs.
"""
import logging
from django import template
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 novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon.dashboards.nova.access_and_security.floating_ips.forms import \
(ReleaseFloatingIp, FloatingIpAssociate,
FloatingIpDisassociate, FloatingIpAllocate)
from horizon import forms
from .forms import FloatingIpAssociate
LOG = logging.getLogger(__name__)
@login_required
def index(request):
for f in (ReleaseFloatingIp, FloatingIpDisassociate, FloatingIpAllocate):
_unused, handled = f.maybe_handle(request)
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()})
class AssociateView(forms.ModalFormView):
form_class = FloatingIpAssociate
template_name = 'nova/access_and_security/floating_ips/associate.html'
context_object_name = 'floating_ip'
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 associate(request, ip_id):
instancelist = [(server.id, 'id: %s, name: %s' %
(server.id, server.name))
for server in api.server_list(request)]
form, handled = FloatingIpAssociate().maybe_handle(request, initial={
'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})
def get_initial(self):
instances = [(server.id, 'id: %s, name: %s' %
(server.id, server.name))
for server in api.server_list(self.request)]
return {'floating_ip_id': self.object.id,
'floating_ip': self.object.ip,
'instances': instances}

View File

@ -36,24 +36,7 @@ from horizon import forms
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):
name = forms.CharField(max_length="20",
label=_("Keypair Name"),
validators=[validators.validate_slug],
@ -79,7 +62,6 @@ class CreateKeypair(forms.SelfHandlingForm):
class ImportKeypair(forms.SelfHandlingForm):
name = forms.CharField(max_length="20", label=_("Keypair Name"),
validators=[validators.RegexValidator('\w+')])
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):
KEYPAIR_ID = self.keypairs[0].name
formData = {'method': 'DeleteKeypair',
'keypair_id': KEYPAIR_ID}
formData = {'action': 'keypairs__delete__%s' % KEYPAIR_ID}
self.mox.StubOutWithMock(api, 'keypair_delete')
api.keypair_delete(IsA(http.HttpRequest), unicode(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)
api.nova.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID))
self.mox.ReplayAll()
@ -54,15 +56,16 @@ class KeyPairViewTests(test.BaseViewTests):
def test_delete_keypair_exception(self):
KEYPAIR_ID = self.keypairs[0].name
formData = {'method': 'DeleteKeypair',
'keypair_id': KEYPAIR_ID,
}
formData = {'action': 'keypairs__delete__%s' % 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',
message='clientException')
self.mox.StubOutWithMock(api, 'keypair_delete')
api.keypair_delete(IsA(http.HttpRequest),
unicode(KEYPAIR_ID)).AndRaise(exception)
api.nova.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID)) \
.AndRaise(exception)
self.mox.ReplayAll()

View File

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

View File

@ -23,38 +23,13 @@ Views for managing Nova keypairs.
"""
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 tables
from .forms import CreateKeypair, DeleteKeypair, ImportKeypair
from .tables import KeypairsTable
from .forms import CreateKeypair, ImportKeypair
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):
form_class = CreateKeypair
template_name = 'nova/access_and_security/keypairs/create.html'

View File

@ -56,27 +56,6 @@ class CreateGroup(forms.SelfHandlingForm):
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):
ip_protocol = forms.ChoiceField(choices=[('tcp', 'tcp'),
('udp', 'udp'),
@ -108,25 +87,3 @@ class AddRule(forms.SelfHandlingForm):
messages.error(request, _('Error adding rule security group: %s')
% e.message)
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 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 = \
reverse('horizon:nova:access_and_security:security_groups:create')
SG_EDIT_RULE_URL = \
@ -41,10 +43,24 @@ class SecurityGroupsViewTests(test.BaseViewTests):
def setUp(self):
super(SecurityGroupsViewTests, self).setUp()
security_group = api.SecurityGroup(None)
security_group.id = '1'
security_group.name = 'default'
self.security_groups = (security_group,)
sg1 = api.SecurityGroup(None)
sg1.id = 1
sg1.name = 'default'
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):
res = self.client.get(SG_CREATE_URL)
@ -73,7 +89,7 @@ class SecurityGroupsViewTests(test.BaseViewTests):
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):
SECGROUP_NAME = 'fakegroup'
@ -100,10 +116,9 @@ class SecurityGroupsViewTests(test.BaseViewTests):
'nova/access_and_security/security_groups/create.html')
def test_edit_rules_get(self):
self.mox.StubOutWithMock(api, 'security_group_get')
api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndReturn(
self.security_groups[0])
self.security_groups[1])
self.mox.ReplayAll()
@ -112,21 +127,21 @@ class SecurityGroupsViewTests(test.BaseViewTests):
self.assertTemplateUsed(res,
'nova/access_and_security/security_groups/edit_rules.html')
self.assertItemsEqual(res.context['security_group'].name,
self.security_groups[0].name)
self.security_groups[1].name)
def test_edit_rules_get_exception(self):
exception = novaclient_exceptions.ClientException('ClientException',
message='ClientException')
self.mox.StubOutWithMock(api, 'security_group_get')
api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID).AndRaise(
exception)
api.security_group_get(IsA(http.HttpRequest), SECGROUP_ID) \
.AndRaise(exception)
self.mox.ReplayAll()
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):
RULE_ID = '1'
@ -194,72 +209,62 @@ class SecurityGroupsViewTests(test.BaseViewTests):
def test_edit_rules_delete_rule(self):
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')
api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID)
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):
exception = novaclient_exceptions.ClientException('ClientException',
message='ClientException')
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')
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',
message='ClientException')
api.security_group_rule_delete(IsA(http.HttpRequest), RULE_ID) \
.AndRaise(exception)
formData = {'method': 'DeleteGroup',
'tenant_id': self.TEST_TENANT,
'security_group_id': SECGROUP_ID,
}
self.mox.ReplayAll()
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')
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)
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 .views import CreateView, EditRulesView
urlpatterns = patterns(
'horizon.dashboards.nova.access_and_security.security_groups.views',
url(r'^$', 'index', name='index'),
url(r'^create/$', 'create', name='create'),
url(r'^(?P<security_group_id>[^/]+)/edit_rules/$', 'edit_rules',
urlpatterns = patterns('',
url(r'^create/$', CreateView.as_view(), name='create'),
url(r'^(?P<security_group_id>[^/]+)/edit_rules/$',
EditRulesView.as_view(),
name='edit_rules'))

View File

@ -24,81 +24,67 @@ Views for managing Nova instances.
import logging
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 novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon.dashboards.nova.access_and_security.security_groups.forms import \
(CreateGroup, DeleteGroup, AddRule, DeleteRule)
from horizon import forms
from horizon import tables
from .forms import CreateGroup, AddRule
from .tables import RulesTable
LOG = logging.getLogger(__name__)
@login_required
def index(request):
tenant_id = request.user.tenant_id
delete_form, handled = DeleteGroup.maybe_handle(request,
initial={'tenant_id': tenant_id})
form = CreateGroup(initial={'tenant_id': tenant_id})
if handled:
return handled
class EditRulesView(tables.DataTableView):
table_class = RulesTable
template_name = 'nova/access_and_security/security_groups/edit_rules.html'
try:
security_groups = api.security_group_list(request)
except novaclient_exceptions.ClientException, e:
security_groups = []
LOG.exception("ClientException in security_groups index")
messages.error(request, _('Error fetching security_groups: %s')
% e.message)
def get_data(self):
security_group_id = self.kwargs['security_group_id']
try:
self.object = api.security_group_get(self.request,
security_group_id)
rules = [api.nova.SecurityGroupRule(rule) for
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,
'nova/access_and_security/security_groups/index.html', {
'security_groups': security_groups,
'form': form,
'delete_form': delete_form})
def handle_form(self):
tenant_id = self.request.user.tenant_id
initial = {'tenant_id': tenant_id,
'security_group_id': self.kwargs['security_group_id']}
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
def edit_rules(request, security_group_id):
tenant_id = request.user.tenant_id
add_form, handled = AddRule.maybe_handle(request,
initial={'tenant_id': tenant_id,
'security_group_id': security_group_id})
if handled:
return handled
class CreateView(forms.ModalFormView):
form_class = CreateGroup
template_name = 'nova/access_and_security/security_groups/create.html'
delete_form, handled = DeleteRule.maybe_handle(request,
initial={'tenant_id': 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})
def get_initial(self):
return {"tenant_id": self.request.user.tenant_id}

View File

@ -74,6 +74,7 @@ class AccessAndSecurityTests(test.BaseViewTests):
self.assertTemplateUsed(res, 'nova/access_and_security/index.html')
self.assertItemsEqual(res.context['keypairs_table'].data,
self.keypairs)
self.assertItemsEqual(res.context['security_groups'],
self.assertItemsEqual(res.context['security_groups_table'].data,
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 *
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 \
as keypair_urls
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'),
urlpatterns = patterns('',
url(r'^$', IndexView.as_view(), name='index'),
url(r'keypairs/', include(keypair_urls, namespace='keypairs')),
url(r'floating_ips/', include(fip_urls, namespace='floating_ips')),
url(r'security_groups/',

View File

@ -22,86 +22,52 @@
"""
Views for Instances and Volumes.
"""
import datetime
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
import openstackx.api.exceptions as api_exceptions
from horizon import api
from horizon import forms
from horizon import test
from .keypairs.forms import DeleteKeypair
from horizon import tables
from .keypairs.tables import KeypairsTable
from .security_groups.forms import CreateGroup, DeleteGroup
from .floating_ips.forms import (ReleaseFloatingIp, FloatingIpDisassociate,
FloatingIpAllocate)
from .floating_ips.tables import FloatingIPsTable
from .security_groups.tables import SecurityGroupsTable
LOG = logging.getLogger(__name__)
@login_required
def index(request):
tenant_id = request.user.tenant_id
class IndexView(tables.MultiTableView):
table_classes = (KeypairsTable, SecurityGroupsTable, FloatingIPsTable)
template_name = 'nova/access_and_security/index.html'
for f in (CreateGroup, DeleteGroup, DeleteKeypair, ReleaseFloatingIp,
FloatingIpDisassociate, FloatingIpAllocate):
_unused, handled = f.maybe_handle(request)
if handled:
return handled
def get_keypairs_data(self):
try:
keypairs = api.nova.keypair_list(self.request)
except Exception, e:
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
# in this view are converted to DataTables.
try:
keypairs = api.nova.keypair_list(request)
except Exception, e:
keypairs = []
LOG.exception("Exception in keypair index")
messages.error(request,
_('Keypair list is currently unavailable.'))
keypairs_table = KeypairsTable(request, keypairs)
handled = keypairs_table.maybe_handle()
if handled:
return handled
def get_security_groups_data(self):
try:
security_groups = api.security_group_list(self.request)
except novaclient_exceptions.ClientException, e:
security_groups = []
LOG.exception("ClientException in security_groups index")
messages.error(self.request,
_('Error fetching security_groups: %s') % e)
return security_groups
try:
security_groups = api.security_group_list(request)
except novaclient_exceptions.ClientException, e:
security_groups = []
LOG.exception("ClientException in security_groups index")
messages.error(request, _('Error fetching security_groups: %s')
% e.message)
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)
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)
def get_floating_ips_data(self):
try:
floating_ips = api.tenant_floating_ip_list(self.request)
except novaclient_exceptions.ClientException, e:
floating_ips = []
LOG.exception("ClientException in floating ip index")
messages.error(self.request,
_('Error fetching floating ips: %s') % e)
return floating_ips

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 %}
{% 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 %}
<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 %}
{% block dash_main %}
<div id="floating_ips">
{% 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 %}
{{ floating_ips_table.render }}
</div>
<div id="security_groups">
{% url horizon:nova:access_and_security:security_groups:create as create_sec_url %}
{% 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 %}
{{ security_groups_table.render }}
</div>
<div id="keypairs">
{% if keypairs_table.data %}
{{ keypairs_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" 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>
<div id="keypairs">
{{ keypairs_table.render }}
</div>
{% 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 %}
<div id="security_group_rule_modal" class="{% block modal_class %}modal{% if hide %} hide {% else %} static_page{% endif %}{% endblock %}">
<div class="modal-header">
{% if hide %}<a href="#" class="close">&times;</a>{% endif %}
@ -8,35 +7,7 @@
</div>
<div class="modal-body clearfix">
<div class="right">
<h3> {% trans "Rules for Security Group" %} '{{security_group.name}}'</h3>
<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>
{{ table.render }}
</div>
<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 %}
@ -50,6 +21,3 @@
</div>
</form>
</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 %}
{% 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 %}

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.
from .actions import Action, LinkAction, FilterAction
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
attrs = {}
name = None
requires_input = False
def allowed(self, request, datum):
""" 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
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:
.. method:: single(self, data_table, request, object_id)
@ -104,10 +110,11 @@ class Action(BaseAction):
into a list containing only the single object id.
"""
method = "POST"
requires_input = True
def __init__(self, verbose_name=None, verbose_name_plural=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__()
self.name = unicode(getattr(self, 'name', self.__class__.__name__))
verbose_name = verbose_name or self.name.title()
@ -121,6 +128,9 @@ class Action(BaseAction):
self.handles_multiple = getattr(self,
"handles_multiple",
handles_multiple)
self.requires_input = getattr(self,
"requires_input",
requires_input)
if attrs:
self.attrs.update(attrs)

View File

@ -260,8 +260,9 @@ class Row(object):
for column in table.columns.values():
if column.auto == "multi_select":
widget = forms.CheckboxInput(check_test=False)
# Convert value to string to avoid accidental type conversion
data = widget.render('object_ids',
table.get_object_id(datum))
str(table.get_object_id(datum)))
column._data_cache[datum] = data
elif column.auto == "actions":
data = table.render_row_actions(datum)
@ -623,6 +624,10 @@ class DataTable(object):
context = template.RequestContext(self._meta.request, extra_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):
"""
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
obj_ids = obj_ids or self._meta.request.POST.getlist('object_ids')
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
if not action.handles_multiple:
response = action.single(self, self._meta.request, obj_id)
@ -727,6 +732,9 @@ class DataTable(object):
obj_ids = [obj_id]
response = action.multiple(self, self._meta.request, obj_ids)
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
def maybe_handle(self):

View File

@ -17,7 +17,74 @@
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.
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.'
% self.__class__.__name__)
def get_tables(self):
table = self.get_table()
return {table._meta.name: table}
def get_table(self):
if not self.table_class:
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())
return self.table
def get(self, request, *args, **kwargs):
table = self.get_table()
context = self.get_context_data(**kwargs)
context[self.context_object_name] = table
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)
def get_context_data(self, **kwargs):
context = super(DataTableView, self).get_context_data(**kwargs)
context[self.context_object_name] = self.table
return context

View File

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

View File

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

View File

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

View File

@ -343,6 +343,17 @@ table form {
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 {
float: right;
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
$('.action.primary').mouseenter(function() {

View File

@ -267,7 +267,7 @@ function run_tests {
echo "Running Horizon application tests"
${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
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
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
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test dashboard
${command_wrapper} coverage run $root/openstack-dashboard/manage.py test dashboard $testargs
fi
# get results of the openstack-dashboard tests
DASHBOARD_RESULT=$?