Merge "Converts syspanel.tenants to use DataTables and modal forms."

This commit is contained in:
Jenkins 2012-01-04 09:23:01 +00:00 committed by Gerrit Code Review
commit 30a60bd92b
13 changed files with 331 additions and 139 deletions

View File

@ -1,26 +1 @@
{% load i18n %}
<table class="zebra-striped">
<tr>
<th>{% trans "Id" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Enabled" %}</th>
<th>{% trans "Options" %}</th>
</tr>
{% for tenant in tenants %}
<tr class="{% cycle 'odd' 'even' %}">
<td>{{ tenant.id }}</td>
<td>{{ tenant.name }}</td>
<td>{{ tenant.description }}</td>
<td>{{ tenant.enabled }}</td>
<td id="actions">
<ul>
<li class="form">{% include "syspanel/tenants/_delete.html" with form=tenant_delete_form %}</li>
<li><a class="btn small" href="{% url horizon:syspanel:tenants:update tenant.id %}">{% trans "Edit" %}</a></li>
<li><a class="btn small" href="{% url horizon:syspanel:tenants:users tenant.id %}">{% trans "View Members" %}</a></li>
<li><a class="btn small" href="{% url horizon:syspanel:tenants:quotas tenant.id %}">{% trans "Modify Quotas" %}</a></li>
</ul>
</td>
</tr>
{% endfor %}
</table>
{{ table.render }}

View File

@ -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 @@
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "From here you can edit quotas (max limits) for the tenant {{ tenant_id }}." %}</p>
<p>{% blocktrans with tenant_id=tenant.id %}From here you can edit quotas (max limits) for the tenant {{ tenant_id }}.{% endblocktrans %}</p>
</div>
{% endblock %}

View File

@ -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 %}

View File

@ -10,5 +10,4 @@
{% block syspanel_main %}
{% include "syspanel/tenants/_list.html" %}
<a id="tenant_create_link" class="btn small" href="{% url horizon:syspanel:tenants:create %}">{% trans "Create New Tenant" %}</a>
{% endblock %}

View File

@ -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())

View File

@ -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)

View File

@ -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<tenant_id>[^/]+)/update/$', 'update', name='update'),
url(r'^$', IndexView.as_view(), name='index'),
url(r'^create$', CreateView.as_view(), name='create'),
url(r'^(?P<tenant_id>[^/]+)/update/$',
UpdateView.as_view(), name='update'),
url(r'^(?P<tenant_id>[^/]+)/users/$', 'users', name='users'),
url(r'^(?P<tenant_id>[^/]+)/quotas/$', 'quotas', name='quotas'))
url(r'^(?P<tenant_id>[^/]+)/quotas/$',
QuotasView.as_view(), name='quotas'))

View File

@ -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}

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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)