diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_list.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_list.html
index bedaf7a1f..99f86d427 100644
--- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_list.html
+++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_list.html
@@ -1,26 +1 @@
-{% load i18n %}
-
-
- {% trans "Id" %} |
- {% trans "Name" %} |
- {% trans "Description" %} |
- {% trans "Enabled" %} |
- {% trans "Options" %} |
-
- {% for tenant in tenants %}
-
- {{ tenant.id }} |
- {{ tenant.name }} |
- {{ tenant.description }} |
- {{ tenant.enabled }} |
-
-
- |
-
- {% endfor %}
-
+{{ table.render }}
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_quotas.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_quotas.html
index 52c80092e..5bee2b78e 100644
--- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_quotas.html
+++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_quotas.html
@@ -2,7 +2,7 @@
{% load i18n %}
{% block form_id %}quota_update_form{% endblock %}
-{% block form_action %}{% url horizon:syspanel:tenants:quotas tenant_id %}{% endblock %}
+{% block form_action %}{% url horizon:syspanel:tenants:quotas tenant.id %}{% endblock %}
{% block modal-header %}{% trans "Update Quota" %}{% endblock %}
@@ -14,7 +14,7 @@
{% trans "Description" %}:
-
{% trans "From here you can edit quotas (max limits) for the tenant {{ tenant_id }}." %}
+
{% blocktrans with tenant_id=tenant.id %}From here you can edit quotas (max limits) for the tenant {{ tenant_id }}.{% endblocktrans %}
{% endblock %}
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update.html
index fdf353295..2d957c9e3 100644
--- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update.html
+++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/_update.html
@@ -2,7 +2,7 @@
{% load i18n %}
{% block form_id %}{% endblock %}
-{% block form_action %}{% url horizon:syspanel:tenants:update tenant_id %}{% endblock %}
+{% block form_action %}{% url horizon:syspanel:tenants:update tenant.id %}{% endblock %}
{% block modal-header %}{% endblock %}
diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/index.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/index.html
index d0bf128ef..32333e603 100644
--- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/index.html
+++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/index.html
@@ -10,5 +10,4 @@
{% block syspanel_main %}
{% include "syspanel/tenants/_list.html" %}
- {% trans "Create New Tenant" %}
{% endblock %}
diff --git a/horizon/horizon/dashboards/syspanel/tenants/forms.py b/horizon/horizon/dashboards/syspanel/tenants/forms.py
index 27dfb5675..08163d375 100644
--- a/horizon/horizon/dashboards/syspanel/tenants/forms.py
+++ b/horizon/horizon/dashboards/syspanel/tenants/forms.py
@@ -167,21 +167,3 @@ class UpdateQuotas(forms.SelfHandlingForm):
messages.error(request,
_('Unable to update quotas: %s') % e.message)
return shortcuts.redirect('horizon:syspanel:tenants:index')
-
-
-class DeleteTenant(forms.SelfHandlingForm):
- tenant_id = forms.CharField(required=True)
-
- def handle(self, request, data):
- tenant_id = data['tenant_id']
- try:
- api.tenant_delete(request, tenant_id)
- messages.info(request, _('Successfully deleted tenant %(tenant)s.')
- % {"tenant": tenant_id})
- except Exception, e:
- LOG.exception("Error deleting tenant")
- if not hasattr(e, 'message'):
- e.message = str(e)
- messages.error(request,
- _("Error deleting tenant: %s") % e.message)
- return shortcuts.redirect(request.build_absolute_uri())
diff --git a/horizon/horizon/dashboards/syspanel/tenants/tables.py b/horizon/horizon/dashboards/syspanel/tenants/tables.py
new file mode 100644
index 000000000..4a3c16b15
--- /dev/null
+++ b/horizon/horizon/dashboards/syspanel/tenants/tables.py
@@ -0,0 +1,93 @@
+import logging
+
+from django import shortcuts
+from django.contrib import messages
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import tables
+
+
+LOG = logging.getLogger(__name__)
+
+
+class ModifyQuotasLink(tables.LinkAction):
+ name = "quotas"
+ verbose_name = _("Modify Quotas")
+ url = "horizon:syspanel:tenants:quotas"
+ attrs = {"class": "ajax-modal"}
+
+
+class ViewMembersLink(tables.LinkAction):
+ name = "users"
+ verbose_name = _("View Members")
+ url = "horizon:syspanel:tenants:users"
+
+
+class EditLink(tables.LinkAction):
+ name = "update"
+ verbose_name = _("Edit")
+ url = "horizon:syspanel:tenants:update"
+ attrs = {"class": "ajax-modal"}
+
+
+class CreateLink(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create New Tenant")
+ url = "horizon:syspanel:tenants:create"
+ attrs = {"class": "ajax-modal btn small"}
+
+
+class DeleteTenantsAction(tables.Action):
+ name = "delete"
+ verbose_name = _("Delete")
+ verbose_name_plural = _("Delete Tenants")
+ classes = ("danger",)
+
+ def handle(self, data_table, request, object_ids):
+ failures = 0
+ deleted = []
+ for obj_id in object_ids:
+ LOG.info('Deleting tenant with id "%s"' % obj_id)
+ try:
+ api.keystone.tenant_delete(request, obj_id)
+ deleted.append(obj_id)
+ except Exception, e:
+ failures += 1
+ messages.error(request, _("Error deleting tenant: %s") % e)
+ LOG.exception("Error deleting tenant.")
+ if failures:
+ messages.info(request, _("Deleted the following tenant: %s")
+ % ", ".join(deleted))
+ else:
+ messages.success(request, _("Successfully deleted tenant: %s")
+ % ", ".join(deleted))
+ return shortcuts.redirect('horizon:syspanel:tenants:index')
+
+
+class TenantFilterAction(tables.FilterAction):
+ def filter(self, table, tenants, filter_string):
+ """ Really naive case-insensitive search. """
+ # FIXME(gabriel): This should be smarter. Written for demo purposes.
+ q = filter_string.lower()
+
+ def comp(tenant):
+ if q in tenant.name.lower():
+ return True
+ return False
+
+ return filter(comp, tenants)
+
+
+class TenantsTable(tables.DataTable):
+ id = tables.Column('id', verbose_name=_('Id'))
+ name = tables.Column('name', verbose_name=_('Name'))
+ description = tables.Column("description", verbose_name=_('Description'))
+ enabled = tables.Column('enabled', verbose_name=_('Enabled'), status=True)
+
+ class Meta:
+ name = "tenants"
+ verbose_name = _("Tenants")
+ row_actions = (EditLink, ViewMembersLink, ModifyQuotasLink,
+ DeleteTenantsAction)
+ table_actions = (TenantFilterAction, CreateLink, DeleteTenantsAction)
diff --git a/horizon/horizon/dashboards/syspanel/tenants/urls.py b/horizon/horizon/dashboards/syspanel/tenants/urls.py
index 1e43b874f..158371767 100644
--- a/horizon/horizon/dashboards/syspanel/tenants/urls.py
+++ b/horizon/horizon/dashboards/syspanel/tenants/urls.py
@@ -20,10 +20,14 @@
from django.conf.urls.defaults import patterns, url
+from .views import IndexView, CreateView, UpdateView, QuotasView
+
urlpatterns = patterns('horizon.dashboards.syspanel.tenants.views',
- url(r'^$', 'index', name='index'),
- url(r'^create$', 'create', name='create'),
- url(r'^(?P[^/]+)/update/$', 'update', name='update'),
+ url(r'^$', IndexView.as_view(), name='index'),
+ url(r'^create$', CreateView.as_view(), name='create'),
+ url(r'^(?P[^/]+)/update/$',
+ UpdateView.as_view(), name='update'),
url(r'^(?P[^/]+)/users/$', 'users', name='users'),
- url(r'^(?P[^/]+)/quotas/$', 'quotas', name='quotas'))
+ url(r'^(?P[^/]+)/quotas/$',
+ QuotasView.as_view(), name='quotas'))
diff --git a/horizon/horizon/dashboards/syspanel/tenants/views.py b/horizon/horizon/dashboards/syspanel/tenants/views.py
index 7f00980c5..935c8a6d5 100644
--- a/horizon/horizon/dashboards/syspanel/tenants/views.py
+++ b/horizon/horizon/dashboards/syspanel/tenants/views.py
@@ -21,6 +21,7 @@
import logging
from django import shortcuts
+from django import http
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@@ -28,77 +29,64 @@ from django.utils.translation import ugettext as _
from keystoneclient import exceptions as api_exceptions
from horizon import api
-from horizon.dashboards.syspanel.tenants.forms import (AddUser, RemoveUser,
- CreateTenant, UpdateTenant, UpdateQuotas, DeleteTenant)
+from horizon import forms
+from horizon import tables
+from .forms import (AddUser, RemoveUser, CreateTenant, UpdateTenant,
+ UpdateQuotas)
+from .tables import TenantsTable
LOG = logging.getLogger(__name__)
-@login_required
-def index(request):
- form, handled = DeleteTenant.maybe_handle(request)
- if handled:
- return handled
+class IndexView(tables.DataTableView):
+ table_class = TenantsTable
+ template_name = 'syspanel/tenants/index.html'
- tenant_delete_form = DeleteTenant()
-
- tenants = []
- try:
- tenants = api.tenant_list(request)
- except api_exceptions.AuthorizationFailure, e:
- LOG.exception("Unauthorized attempt to list tenants.")
- messages.error(request, _('Unable to get tenant info: %s') % e.message)
- except Exception, e:
- LOG.exception('Exception while getting tenant list')
- if not hasattr(e, 'message'):
- e.message = str(e)
- messages.error(request, _('Unable to get tenant info: %s') % e.message)
-
- tenants.sort(key=lambda x: x.id, reverse=True)
- return shortcuts.render(request,
- 'syspanel/tenants/index.html', {
- 'tenants': tenants,
- 'tenant_delete_form': tenant_delete_form})
-
-
-@login_required
-def create(request):
- form, handled = CreateTenant.maybe_handle(request)
- if handled:
- return handled
-
- return shortcuts.render(request,
- 'syspanel/tenants/create.html', {
- 'form': form})
-
-
-@login_required
-def update(request, tenant_id):
- form, handled = UpdateTenant.maybe_handle(request)
- if handled:
- return handled
-
- if request.method == 'GET':
+ def get_data(self):
+ tenants = []
try:
- tenant = api.tenant_get(request, tenant_id)
- form = UpdateTenant(initial={'id': tenant.id,
- 'name': tenant.name,
- 'description': tenant.description,
- 'enabled': tenant.enabled})
- except api_exceptions.ApiException, e:
+ tenants = api.tenant_list(self.request)
+ except api_exceptions.AuthorizationFailure, e:
+ LOG.exception("Unauthorized attempt to list tenants.")
+ messages.error(self.request, _('Unable to get tenant info: %s')
+ % e.message)
+ except Exception, e:
+ LOG.exception('Exception while getting tenant list')
+ if not hasattr(e, 'message'):
+ e.message = str(e)
+ messages.error(self.request, _('Unable to get tenant info: %s')
+ % e.message)
+ tenants.sort(key=lambda x: x.id, reverse=True)
+ return tenants
+
+
+class CreateView(forms.ModalFormView):
+ form_class = CreateTenant
+ template_name = 'syspanel/tenants/create.html'
+
+
+class UpdateView(forms.ModalFormView):
+ form_class = UpdateTenant
+ template_name = 'syspanel/tenants/update.html'
+ context_object_name = 'tenant'
+
+ def get_object(self, tenant_id):
+ try:
+ return api.tenant_get(self.request, tenant_id)
+ except Exception as e:
LOG.exception('Error fetching tenant with id "%s"' % tenant_id)
- messages.error(request,
- _('Unable to update tenant: %s') % e.message)
- return shortcuts.redirect('horizon:syspanel:tenants:index')
+ messages.error(request, _('Unable to update tenant: %s')
+ % e.message)
+ raise http.Http404("Tenant with ID %s not found." % tenant_id)
- return shortcuts.render(request,
- 'syspanel/tenants/update.html', {
- 'tenant_id': tenant_id,
- 'form': form})
+ def get_initial(self):
+ return {'id': self.object.id,
+ 'name': self.object.name,
+ 'description': self.object.description,
+ 'enabled': self.object.enabled}
-@login_required
def users(request, tenant_id):
for f in (AddUser, RemoveUser,):
form, handled = f.maybe_handle(request)
@@ -121,30 +109,25 @@ def users(request, tenant_id):
'new_users': new_users})
-@login_required
-def quotas(request, tenant_id):
- for f in (UpdateQuotas,):
- form, handled = f.maybe_handle(request)
- if handled:
- return handled
+class QuotasView(forms.ModalFormView):
+ form_class = UpdateQuotas
+ template_name = 'syspanel/tenants/quotas.html'
+ context_object_name = 'tenant'
- quotas = api.admin_api(request).quota_sets.get(tenant_id)
- quota_set = {
- 'tenant_id': quotas.id,
- 'metadata_items': quotas.metadata_items,
- 'injected_file_content_bytes': quotas.injected_file_content_bytes,
- 'volumes': quotas.volumes,
- 'gigabytes': quotas.gigabytes,
- 'ram': int(quotas.ram),
- 'floating_ips': quotas.floating_ips,
- 'instances': quotas.instances,
- 'injected_files': quotas.injected_files,
- 'cores': quotas.cores,
- }
- form = UpdateQuotas(initial=quota_set)
+ def get_object(self, tenant_id):
+ return api.tenant_get(self.request, tenant_id)
- return shortcuts.render(request,
- 'syspanel/tenants/quotas.html', {
- 'form': form,
- 'tenant_id': tenant_id,
- 'quotas': quotas})
+ def get_initial(self):
+ admin_api = api.admin_api(self.request)
+ quotas = admin_api.quota_sets.get(self.kwargs['tenant_id'])
+ return {
+ 'tenant_id': quotas.id,
+ 'metadata_items': quotas.metadata_items,
+ 'injected_file_content_bytes': quotas.injected_file_content_bytes,
+ 'volumes': quotas.volumes,
+ 'gigabytes': quotas.gigabytes,
+ 'ram': int(quotas.ram),
+ 'floating_ips': quotas.floating_ips,
+ 'instances': quotas.instances,
+ 'injected_files': quotas.injected_files,
+ 'cores': quotas.cores}
diff --git a/horizon/horizon/forms/__init__.py b/horizon/horizon/forms/__init__.py
new file mode 100644
index 000000000..fd7994af1
--- /dev/null
+++ b/horizon/horizon/forms/__init__.py
@@ -0,0 +1,24 @@
+# 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.
+
+# FIXME(gabriel): Legacy imports from django-openstack.forms
+# for API compatibility.
+from django.forms import *
+from django.forms import widgets
+
+# Convenience imports for public API components.
+from .base import SelfHandlingForm, SelectDateWidget, DateForm
+from .views import ModalFormView
diff --git a/horizon/horizon/forms.py b/horizon/horizon/forms/base.py
similarity index 98%
rename from horizon/horizon/forms.py
rename to horizon/horizon/forms/base.py
index 03af24d35..4306e8866 100644
--- a/horizon/horizon/forms.py
+++ b/horizon/horizon/forms/base.py
@@ -192,7 +192,8 @@ class SelfHandlingForm(Form):
converted to messages.
"""
- if cls.__name__ != request.POST.get('method'):
+ if request.method != 'POST' or \
+ cls.__name__ != request.POST.get('method'):
return cls._instantiate(request, *args, **kwargs), None
if request.FILES:
diff --git a/horizon/horizon/forms/views.py b/horizon/horizon/forms/views.py
new file mode 100644
index 000000000..cddfc3704
--- /dev/null
+++ b/horizon/horizon/forms/views.py
@@ -0,0 +1,75 @@
+# 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 os
+
+from django.views import generic
+
+
+class ModalFormView(generic.TemplateView):
+ form_class = None
+ initial = {}
+ context_form_name = "form"
+ context_object_name = "object"
+
+ def get_template_names(self):
+ if self.request.is_ajax():
+ if not hasattr(self, "ajax_template_name"):
+ # Transform standard template name to ajax name (leading "_")
+ bits = list(os.path.split(self.template_name))
+ bits[1] = "".join(("_", bits[1]))
+ self.ajax_template_name = os.path.join(*bits)
+ template = self.ajax_template_name
+ else:
+ template = self.template_name
+ return template
+
+ def get_object(self, *args, **kwargs):
+ return None
+
+ def get_initial(self):
+ return self.initial
+
+ def get_form_kwargs(self):
+ kwargs = {'initial': self.get_initial()}
+ return kwargs
+
+ def maybe_handle(self):
+ if not self.form_class:
+ raise AttributeError('You must specify a SelfHandlingForm class '
+ 'for the "form_class" attribute on %s.'
+ % self.__class__.__name__)
+ if not hasattr(self, "form"):
+ form = self.form_class
+ kwargs = self.get_form_kwargs()
+ self.form, self.handled = form.maybe_handle(self.request, **kwargs)
+ return self.form, self.handled
+
+ def get(self, request, *args, **kwargs):
+ self.object = self.get_object(*args, **kwargs)
+ form, handled = self.maybe_handle()
+ if handled:
+ return handled
+ context = self.get_context_data(**kwargs)
+ context[self.context_form_name] = form
+ context[self.context_object_name] = self.object
+ if self.request.is_ajax():
+ context['hide'] = True
+ return self.render_to_response(context)
+
+ def post(self, request, *args, **kwargs):
+ """ Placeholder to allow POST; handled the same as GET. """
+ return self.get(self, request, *args, **kwargs)
diff --git a/horizon/horizon/tables/__init__.py b/horizon/horizon/tables/__init__.py
index 33e24a7ce..903527f55 100644
--- a/horizon/horizon/tables/__init__.py
+++ b/horizon/horizon/tables/__init__.py
@@ -15,5 +15,6 @@
# under the License.
# Convenience imports for public API components.
-from .base import DataTable, Column
from .actions import Action, LinkAction, FilterAction
+from .base import DataTable, Column
+from .views import DataTableView
diff --git a/horizon/horizon/tables/views.py b/horizon/horizon/tables/views.py
new file mode 100644
index 000000000..22cf0a506
--- /dev/null
+++ b/horizon/horizon/tables/views.py
@@ -0,0 +1,55 @@
+# 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.
+
+from django.views import generic
+
+
+class DataTableView(generic.TemplateView):
+ """ A class-based generic view to handle basic DataTable processing.
+
+ Three steps are required to use this view: set the ``table_class``
+ attribute with the desired :class:`~horizon.tables.DataTable` class;
+ define a ``get_data`` method which returns a set of data for the
+ table; and specify a template for the ``template_name`` attribute.
+ """
+ table_class = None
+ context_object_name = 'table'
+
+ def get_data(self):
+ raise NotImplementedError('You must define a "get_data" method on %s.'
+ % self.__class__.__name__)
+
+ def get_table(self):
+ if not self.table_class:
+ raise AttributeError('You must specify a DataTable class for the '
+ '"table_class" attribute on %s.'
+ % self.__class__.__name__)
+ if not hasattr(self, "table"):
+ 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)