admin workflow to add/edit project info and quotas

partially implements blueprint tenant-creation-workflow

Change-Id: Id3ab3593b35c5dd3e4b9e13513dd3dcbd0a79b9a
This commit is contained in:
Kelsey Tripp 2012-07-03 16:20:13 -07:00
parent 576262a142
commit cbb22a1811
10 changed files with 669 additions and 198 deletions

View File

@ -52,88 +52,3 @@ class AddUser(forms.SelfHandlingForm):
return True
except:
exceptions.handle(request, _('Unable to add user to project.'))
class CreateTenant(forms.SelfHandlingForm):
name = forms.CharField(label=_("Name"))
description = forms.CharField(
widget=forms.widgets.Textarea(),
label=_("Description"),
required=False)
enabled = forms.BooleanField(label=_("Enabled"), required=False,
initial=True)
def handle(self, request, data):
try:
LOG.info('Creating project with name "%s"' % data['name'])
project = api.tenant_create(request,
data['name'],
data['description'],
data['enabled'])
messages.success(request,
_('%s was successfully created.')
% data['name'])
return project
except:
exceptions.handle(request, _('Unable to create project.'))
class UpdateTenant(forms.SelfHandlingForm):
id = forms.CharField(label=_("ID"),
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
name = forms.CharField(label=_("Name"))
description = forms.CharField(
widget=forms.widgets.Textarea(),
label=_("Description"))
enabled = forms.BooleanField(required=False, label=_("Enabled"))
def handle(self, request, data):
try:
LOG.info('Updating project with id "%s"' % data['id'])
project = api.tenant_update(request,
data['id'],
data['name'],
data['description'],
data['enabled'])
messages.success(request,
_('%s was successfully updated.')
% data['name'])
return project
except:
exceptions.handle(request, _('Unable to update project.'))
class UpdateQuotas(forms.SelfHandlingForm):
tenant_id = forms.CharField(label=_("ID (name)"),
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
metadata_items = forms.IntegerField(label=_("Metadata Items"))
injected_files = forms.IntegerField(label=_("Injected Files"))
injected_file_content_bytes = forms.IntegerField(label=_("Injected File "
"Content Bytes"))
cores = forms.IntegerField(label=_("VCPUs"))
instances = forms.IntegerField(label=_("Instances"))
volumes = forms.IntegerField(label=_("Volumes"))
gigabytes = forms.IntegerField(label=_("Gigabytes"))
ram = forms.IntegerField(label=_("RAM (in MB)"))
floating_ips = forms.IntegerField(label=_("Floating IPs"))
def handle(self, request, data):
ifcb = data['injected_file_content_bytes']
try:
api.nova.tenant_quota_update(request,
data['tenant_id'],
metadata_items=data['metadata_items'],
injected_file_content_bytes=ifcb,
volumes=data['volumes'],
gigabytes=data['gigabytes'],
ram=data['ram'],
floating_ips=data['floating_ips'],
instances=data['instances'],
injected_files=data['injected_files'],
cores=data['cores'])
messages.success(request,
_('Quotas for %s were successfully updated.')
% data['tenant_id'])
return True
except:
exceptions.handle(request, _('Unable to update quotas.'))

View File

@ -2,6 +2,7 @@ import logging
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.http import urlencode
from horizon import api
from horizon import exceptions
@ -13,13 +14,6 @@ from ..users.tables import UsersTable
LOG = logging.getLogger(__name__)
class ModifyQuotasLink(tables.LinkAction):
name = "quotas"
verbose_name = _("Modify Quotas")
url = "horizon:syspanel:projects:quotas"
classes = ("ajax-modal", "btn-edit")
class ViewMembersLink(tables.LinkAction):
name = "users"
verbose_name = _("Modify Users")
@ -34,18 +28,31 @@ class UsageLink(tables.LinkAction):
classes = ("btn-stats",)
class EditLink(tables.LinkAction):
class CreateProject(tables.LinkAction):
name = "create"
verbose_name = _("Create Project")
url = "horizon:syspanel:projects:create"
classes = ("btn-launch", "ajax-modal",)
class UpdateProject(tables.LinkAction):
name = "update"
verbose_name = _("Edit Project")
url = "horizon:syspanel:projects:update"
classes = ("ajax-modal", "btn-edit")
class CreateLink(tables.LinkAction):
name = "create"
verbose_name = _("Create New Project")
url = "horizon:syspanel:projects:create"
classes = ("ajax-modal",)
class ModifyQuotas(tables.LinkAction):
name = "quotas"
verbose_name = "Modify Quotas"
url = "horizon:syspanel:projects:update"
classes = ("ajax-modal", "btn-edit")
def get_link_url(self, project):
step = 'update_quotas'
base_url = reverse(self.url, args=[project.id])
param = urlencode({"step": step})
return "?".join([base_url, param])
class DeleteTenantsAction(tables.DeleteAction):
@ -80,9 +87,10 @@ class TenantsTable(tables.DataTable):
class Meta:
name = "tenants"
verbose_name = _("Projects")
row_actions = (ViewMembersLink, EditLink, UsageLink, ModifyQuotasLink,
DeleteTenantsAction)
table_actions = (TenantFilterAction, CreateLink, DeleteTenantsAction)
row_actions = (ViewMembersLink, UpdateProject, UsageLink,
ModifyQuotas, DeleteTenantsAction)
table_actions = (TenantFilterAction, CreateProject,
DeleteTenantsAction)
class RemoveUserAction(tables.BatchAction):

View File

@ -6,6 +6,7 @@
{% include "horizon/common/_page_header.html" with title=_("Create Project") %}
{% endblock page_header %}
{% block main %}
{% include 'syspanel/projects/_create.html' %}
{% include 'horizon/common/_workflow.html' %}
{% endblock %}

View File

@ -1,11 +1,12 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}Update Project{% endblock %}
{% block title %}{% trans "Edit Project" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Update Project") %}
{% include "horizon/common/_page_header.html" with title=_("Edit Project") %}
{% endblock page_header %}
{% block main %}
{% include 'syspanel/projects/_update.html' %}
{% include 'horizon/common/_workflow.html' %}
{% endblock %}

View File

@ -16,11 +16,13 @@
from django import http
from django.core.urlresolvers import reverse
from mox import IgnoreArg, IsA
from mox import IsA
from horizon import api
from horizon import test
from .workflows import CreateProject, UpdateProject
from .views import QUOTA_FIELDS
INDEX_URL = reverse('horizon:syspanel:projects:index')
@ -36,49 +38,344 @@ class TenantsViewTests(test.BaseAdminViewTests):
self.assertTemplateUsed(res, 'syspanel/projects/index.html')
self.assertItemsEqual(res.context['table'].data, self.tenants.list())
def test_modify_quota(self):
tenant = self.tenants.first()
class CreateProjectWorkflowTests(test.BaseAdminViewTests):
def _get_project_info(self, project):
project_info = {"tenant_name": project.name,
"description": project.description,
"enabled": project.enabled}
return project_info
def _get_workflow_fields(self, project):
project_info = {"name": project.name,
"description": project.description,
"enabled": project.enabled}
return project_info
def _get_quota_info(self, quota):
quota_data = {}
for field in QUOTA_FIELDS:
quota_data[field] = int(getattr(quota, field, None))
return quota_data
def _get_workflow_data(self, project, quota):
project_info = self._get_workflow_fields(project)
quota_data = self._get_quota_info(quota)
project_info.update(quota_data)
return project_info
@test.create_stubs({api: ('tenant_quota_defaults',)})
def test_add_project_get(self):
quota = self.quotas.first()
quota_data = {"metadata_items": 1,
"injected_files": 1,
"injected_file_content_bytes": 1,
"cores": 1,
"instances": 1,
"volumes": 1,
"gigabytes": 1,
"ram": 1,
"floating_ips": 1}
self.mox.StubOutWithMock(api.keystone, 'tenant_get')
self.mox.StubOutWithMock(api.nova, 'tenant_quota_get')
self.mox.StubOutWithMock(api.nova, 'tenant_quota_update')
api.nova.tenant_quota_get(IgnoreArg(), tenant.id).AndReturn(quota)
api.nova.tenant_quota_update(IgnoreArg(), tenant.id, **quota_data)
api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \
.AndReturn(quota)
self.mox.ReplayAll()
url = reverse('horizon:syspanel:projects:quotas',
args=[self.tenant.id])
quota_data.update({"method": "UpdateQuotas",
"tenant_id": self.tenant.id})
res = self.client.post(url, quota_data)
url = reverse('horizon:syspanel:projects:create')
res = self.client.get(url)
self.assertTemplateUsed(res, 'syspanel/projects/create.html')
workflow = res.context['workflow']
self.assertEqual(res.context['workflow'].name, CreateProject.name)
step = workflow.get_step("createprojectinfoaction")
self.assertEqual(step.action.initial['ram'], quota.ram)
self.assertEqual(step.action.initial['injected_files'],
quota.injected_files)
self.assertQuerysetEqual(workflow.steps,
['<CreateProjectInfo: createprojectinfoaction>',
'<UpdateProjectQuota: update_quotas>'])
@test.create_stubs({api.keystone: ('tenant_create',),
api.nova: ('tenant_quota_update',)})
def test_add_project_post(self):
project = self.tenants.first()
quota = self.quotas.first()
project_details = self._get_project_info(project)
quota_data = self._get_quota_info(quota)
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
.AndReturn(project)
api.nova.tenant_quota_update(IsA(http.HttpRequest),
project.id,
**quota_data)
self.mox.ReplayAll()
workflow_data = self._get_workflow_data(project, quota)
url = reverse('horizon:syspanel:projects:create')
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_modify_users(self):
self.mox.StubOutWithMock(api.keystone, 'tenant_get')
self.mox.StubOutWithMock(api.keystone, 'user_list')
self.mox.StubOutWithMock(api.keystone, 'roles_for_user')
api.keystone.tenant_get(IgnoreArg(), self.tenant.id, admin=True) \
.AndReturn(self.tenant)
api.keystone.user_list(IsA(http.HttpRequest)) \
.AndReturn(self.users.list())
api.keystone.user_list(IsA(http.HttpRequest), self.tenant.id) \
.AndReturn([self.user])
api.keystone.roles_for_user(IsA(http.HttpRequest),
self.user.id,
self.tenant.id) \
.AndReturn(self.roles.list())
@test.create_stubs({api: ('tenant_quota_defaults',)})
def test_add_project_quota_defaults_error(self):
api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \
.AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
url = reverse('horizon:syspanel:projects:users',
args=(self.tenant.id,))
url = reverse('horizon:syspanel:projects:create')
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'syspanel/projects/users.html')
self.assertTemplateUsed(res, 'syspanel/projects/create.html')
self.assertContains(res, "Unable to retrieve default quota values")
@test.create_stubs({api.keystone: ('tenant_create',),
api.nova: ('tenant_quota_update',)})
def test_add_project_tenant_create_error(self):
project = self.tenants.first()
quota = self.quotas.first()
project_details = self._get_project_info(project)
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
.AndRaise(self.exceptions.keystone)
self.mox.ReplayAll()
workflow_data = self._get_workflow_data(project, quota)
url = reverse('horizon:syspanel:projects:create')
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.keystone: ('tenant_create',),
api.nova: ('tenant_quota_update',)})
def test_add_project_quota_update_error(self):
project = self.tenants.first()
quota = self.quotas.first()
project_details = self._get_project_info(project)
quota_data = self._get_quota_info(quota)
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
.AndReturn(project)
api.nova.tenant_quota_update(IsA(http.HttpRequest),
project.id,
**quota_data) \
.AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
workflow_data = self._get_workflow_data(project, quota)
url = reverse('horizon:syspanel:projects:create')
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_add_project_missing_field_error(self):
project = self.tenants.first()
quota = self.quotas.first()
workflow_data = self._get_workflow_data(project, quota)
workflow_data["name"] = ""
url = reverse('horizon:syspanel:projects:create')
res = self.client.post(url, workflow_data)
self.assertContains(res, "field is required")
class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
def _get_quota_info(self, quota):
quota_data = {}
for field in QUOTA_FIELDS:
quota_data[field] = int(getattr(quota, field, None))
return quota_data
@test.create_stubs({api: ('tenant_get',
'tenant_quota_get',)})
def test_update_project_get(self):
project = self.tenants.first()
quota = self.quotas.first()
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
.AndReturn(project)
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
.AndReturn(quota)
self.mox.ReplayAll()
url = reverse('horizon:syspanel:projects:update',
args=[self.tenant.id])
res = self.client.get(url)
self.assertTemplateUsed(res, 'syspanel/projects/update.html')
workflow = res.context['workflow']
self.assertEqual(res.context['workflow'].name, UpdateProject.name)
step = workflow.get_step("update_info")
self.assertEqual(step.action.initial['ram'], quota.ram)
self.assertEqual(step.action.initial['injected_files'],
quota.injected_files)
self.assertEqual(step.action.initial['name'], project.name)
self.assertEqual(step.action.initial['description'],
project.description)
self.assertQuerysetEqual(workflow.steps,
['<UpdateProjectInfo: update_info>',
'<UpdateProjectQuota: update_quotas>'])
@test.create_stubs({api: ('tenant_get',
'tenant_quota_get',
'tenant_update',
'tenant_quota_update',)})
def test_update_project_post(self):
project = self.tenants.first()
quota = self.quotas.first()
api.tenant_get(IsA(http.HttpRequest), project.id, admin=True) \
.AndReturn(project)
api.tenant_quota_get(IsA(http.HttpRequest), project.id) \
.AndReturn(quota)
# update some fields
project._info["name"] = "updated name"
project._info["description"] = "updated description"
quota.metadata_items = 444
quota.volumes = 444
updated_project = {"tenant_name": project._info["name"],
"tenant_id": project.id,
"description": project._info["description"],
"enabled": project.enabled}
updated_quota = self._get_quota_info(quota)
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
.AndReturn(project)
api.tenant_quota_update(IsA(http.HttpRequest),
project.id,
**updated_quota)
self.mox.ReplayAll()
# submit form data
workflow_data = {"name": project._info["name"],
"id": project.id,
"description": project._info["description"],
"enabled": project.enabled}
workflow_data.update(updated_quota)
url = reverse('horizon:syspanel:projects:update',
args=[self.tenant.id])
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('tenant_get',
'tenant_quota_get',)})
def test_update_project_get_error(self):
project = self.tenants.first()
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
.AndReturn(project)
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
.AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
url = reverse('horizon:syspanel:projects:update',
args=[self.tenant.id])
res = self.client.get(url)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('tenant_get',
'tenant_quota_get',
'tenant_update',)})
def test_update_project_tenant_update_error(self):
project = self.tenants.first()
quota = self.quotas.first()
api.tenant_get(IsA(http.HttpRequest), project.id, admin=True) \
.AndReturn(project)
api.tenant_quota_get(IsA(http.HttpRequest), project.id) \
.AndReturn(quota)
# update some fields
project._info["name"] = "updated name"
project._info["description"] = "updated description"
quota.metadata_items = '444'
quota.volumes = '444'
updated_project = {"tenant_name": project._info["name"],
"tenant_id": project.id,
"description": project._info["description"],
"enabled": project.enabled}
updated_quota = self._get_quota_info(quota)
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
.AndRaise(self.exceptions.keystone)
self.mox.ReplayAll()
# submit form data
workflow_data = {"name": project._info["name"],
"id": project.id,
"description": project._info["description"],
"enabled": project.enabled}
workflow_data.update(updated_quota)
url = reverse('horizon:syspanel:projects:update',
args=[self.tenant.id])
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('tenant_get',
'tenant_quota_get',
'tenant_update',
'tenant_quota_update',)})
def test_update_project_quota_update_error(self):
project = self.tenants.first()
quota = self.quotas.first()
# first set of calls for 'get' because the url takes an arg
api.tenant_get(IsA(http.HttpRequest), project.id, admin=True) \
.AndReturn(project)
api.tenant_quota_get(IsA(http.HttpRequest), project.id) \
.AndReturn(quota)
# update some fields
project._info["name"] = "updated name"
project._info["description"] = "updated description"
quota.metadata_items = '444'
quota.volumes = '444'
updated_project = {"tenant_name": project._info["name"],
"tenant_id": project.id,
"description": project._info["description"],
"enabled": project.enabled}
updated_quota = self._get_quota_info(quota)
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
.AndReturn(project)
api.tenant_quota_update(IsA(http.HttpRequest),
project.id,
**updated_quota) \
.AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
# submit form data
workflow_data = {"name": updated_project["tenant_name"],
"id": project.id,
"description": updated_project["description"],
"enabled": project.enabled}
workflow_data.update(updated_quota)
url = reverse('horizon:syspanel:projects:update',
args=[self.tenant.id])
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)

View File

@ -20,17 +20,16 @@
from django.conf.urls.defaults import patterns, url
from .views import (IndexView, CreateView, UpdateView, QuotasView, UsersView,
AddUserView, TenantUsageView)
from .views import (IndexView, UsersView,
AddUserView, TenantUsageView,
CreateProjectView, UpdateProjectView)
urlpatterns = patterns('',
url(r'^$', IndexView.as_view(), name='index'),
url(r'^create$', CreateView.as_view(), name='create'),
url(r'^create$', CreateProjectView.as_view(), name='create'),
url(r'^(?P<tenant_id>[^/]+)/update/$',
UpdateView.as_view(), name='update'),
url(r'^(?P<tenant_id>[^/]+)/quotas/$',
QuotasView.as_view(), name='quotas'),
UpdateProjectView.as_view(), name='update'),
url(r'^(?P<tenant_id>[^/]+)/usage/$',
TenantUsageView.as_view(), name='usage'),
url(r'^(?P<tenant_id>[^/]+)/users/$', UsersView.as_view(), name='users'),

View File

@ -29,13 +29,30 @@ from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import usage
from .forms import AddUser, CreateTenant, UpdateTenant, UpdateQuotas
from .tables import TenantsTable, TenantUsersTable, AddUsersTable
from horizon import workflows
from .forms import AddUser
from .tables import TenantsTable, TenantUsersTable, AddUsersTable
from .workflows import CreateProject, UpdateProject
LOG = logging.getLogger(__name__)
QUOTA_FIELDS = ("metadata_items",
"cores",
"instances",
"injected_files",
"injected_file_content_bytes",
"volumes",
"gigabytes",
"ram",
"floating_ips")
PROJECT_INFO_FIELDS = ("name",
"description",
"enabled")
class TenantContextMixin(object):
def get_object(self):
if not hasattr(self, "_object"):
@ -72,25 +89,6 @@ class IndexView(tables.DataTableView):
return tenants
class CreateView(forms.ModalFormView):
form_class = CreateTenant
template_name = 'syspanel/projects/create.html'
success_url = reverse_lazy('horizon:syspanel:projects:index')
class UpdateView(TenantContextMixin, forms.ModalFormView):
form_class = UpdateTenant
template_name = 'syspanel/projects/update.html'
success_url = reverse_lazy('horizon:syspanel:projects:index')
def get_initial(self):
project = self.get_object()
return {'id': project.id,
'name': project.name,
'description': project.description,
'enabled': project.enabled}
class UsersView(tables.MultiTableView):
table_classes = (TenantUsersTable, AddUsersTable)
template_name = 'syspanel/projects/users.html'
@ -165,32 +163,6 @@ class AddUserView(TenantContextMixin, forms.ModalFormView):
'role_id': getattr(default_role, "id", None)}
class QuotasView(TenantContextMixin, forms.ModalFormView):
form_class = UpdateQuotas
template_name = 'syspanel/projects/quotas.html'
success_url = reverse_lazy('horizon:syspanel:projects:index')
def get_initial(self):
try:
quotas = api.nova.tenant_quota_get(self.request,
self.kwargs['tenant_id'])
except:
exceptions.handle(self.request,
_("Unable to retrieve quota information."),
redirect=reverse(self.get_sucess_url))
return {
'tenant_id': self.kwargs['tenant_id'],
'metadata_items': quotas.metadata_items,
'injected_file_content_bytes': quotas.injected_file_content_bytes,
'volumes': quotas.volumes,
'gigabytes': quotas.gigabytes,
'ram': quotas.ram,
'floating_ips': quotas.floating_ips,
'instances': quotas.instances,
'injected_files': quotas.injected_files,
'cores': quotas.cores}
class TenantUsageView(usage.UsageView):
table_class = usage.TenantUsageTable
usage_class = usage.TenantUsage
@ -199,3 +171,52 @@ class TenantUsageView(usage.UsageView):
def get_data(self):
super(TenantUsageView, self).get_data()
return self.usage.get_instances()
class CreateProjectView(workflows.WorkflowView):
workflow_class = CreateProject
template_name = "syspanel/projects/create.html"
def get_initial(self):
initial = super(CreateProjectView, self).get_initial()
# get initial quota defaults
try:
quota_defaults = api.tenant_quota_defaults(self.request,
self.request.user.tenant_id)
for field in QUOTA_FIELDS:
initial[field] = getattr(quota_defaults, field, None)
except:
error_msg = _('Unable to retrieve default quota values.')
self.add_error_to_step(error_msg, 'update_quotas')
return initial
class UpdateProjectView(workflows.WorkflowView):
workflow_class = UpdateProject
template_name = "syspanel/projects/update.html"
def get_initial(self):
initial = super(UpdateProjectView, self).get_initial()
project_id = self.kwargs['tenant_id']
initial['project_id'] = project_id
try:
# get initial project info
project_info = api.tenant_get(self.request, project_id, admin=True)
for field in PROJECT_INFO_FIELDS:
initial[field] = getattr(project_info, field, None)
# get initial project quota
quota_data = api.tenant_quota_get(self.request, project_id)
for field in QUOTA_FIELDS:
initial[field] = getattr(quota_data, field, None)
except:
redirect = reverse("horizon:syspanel:projects:index")
exceptions.handle(self.request,
_('Unable to retrieve project details.'),
redirect=redirect)
return initial

View File

@ -0,0 +1,194 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django import forms
from django.utils.translation import ugettext as _
from horizon import api
from horizon import exceptions
from horizon import workflows
class UpdateProjectQuotaAction(workflows.Action):
ifcb_label = _("Injected File Content Bytes")
metadata_items = forms.IntegerField(min_value=0, label=_("Metadata Items"))
cores = forms.IntegerField(min_value=0, label=_("VCPUs"))
instances = forms.IntegerField(min_value=0, label=_("Instances"))
injected_files = forms.IntegerField(min_value=0, label=_("Injected Files"))
injected_file_content_bytes = forms.IntegerField(min_value=0,
label=ifcb_label)
volumes = forms.IntegerField(min_value=0, label=_("Volumes"))
gigabytes = forms.IntegerField(min_value=0, label=_("Gigabytes"))
ram = forms.IntegerField(min_value=0, label=_("RAM (MB)"))
floating_ips = forms.IntegerField(min_value=0, label=_("Floating IPs"))
class Meta:
name = _("Quota")
slug = 'update_quotas'
help_text = _("From here you can set quotas "
"(max limits) for the project.")
class UpdateProjectQuota(workflows.Step):
action_class = UpdateProjectQuotaAction
depends_on = ("project_id",)
contributes = ("metadata_items",
"cores",
"instances",
"injected_files",
"injected_file_content_bytes",
"volumes",
"gigabytes",
"ram",
"floating_ips")
class CreateProjectInfoAction(workflows.Action):
name = forms.CharField(label=_("Name"))
description = forms.CharField(
widget=forms.widgets.Textarea(),
label=_("Description"),
required=False)
enabled = forms.BooleanField(label=_("Enabled"),
required=False,
initial=True)
class Meta:
name = _("Project Info")
help_text = _("From here you can create a new "
"project to organize users.")
class CreateProjectInfo(workflows.Step):
action_class = CreateProjectInfoAction
contributes = ("project_id",
"name",
"description",
"enabled")
class CreateProject(workflows.Workflow):
slug = "add_project"
name = _("Add Project")
finalize_button_name = _("Finish")
success_message = _('Created new project "%s".')
failure_message = _('Unable to create project "%s".')
success_url = "horizon:syspanel:projects:index"
default_steps = (CreateProjectInfo,
UpdateProjectQuota)
def format_status_message(self, message):
return message % self.context.get('name', 'unknown project')
def handle(self, request, data):
# create the project
try:
desc = data['description']
response = api.keystone.tenant_create(request,
tenant_name=data['name'],
description=desc,
enabled=data['enabled'])
except:
exceptions.handle(request, ignore=True)
return False
# update the project quota
ifcb = data['injected_file_content_bytes']
try:
api.nova.tenant_quota_update(request,
response.id,
metadata_items=data['metadata_items'],
injected_file_content_bytes=ifcb,
volumes=data['volumes'],
gigabytes=data['gigabytes'],
ram=data['ram'],
floating_ips=data['floating_ips'],
instances=data['instances'],
injected_files=data['injected_files'],
cores=data['cores'])
return True
except:
exceptions.handle(request, _('Unable to set project quotas.'))
return True
class UpdateProjectInfoAction(CreateProjectInfoAction):
enabled = forms.BooleanField(required=False, label=_("Enabled"))
class Meta:
name = _("Project Info")
slug = 'update_info'
help_text = _("From here you can edit the project details.")
class UpdateProjectInfo(workflows.Step):
action_class = UpdateProjectInfoAction
depends_on = ("project_id",)
contributes = ("name",
"description",
"enabled")
class UpdateProject(workflows.Workflow):
slug = "update_project"
name = _("Edit Project")
finalize_button_name = _("Save")
success_message = _('Modified project "%s".')
failure_message = _('Unable to modify project "%s".')
success_url = "horizon:syspanel:projects:index"
default_steps = (UpdateProjectInfo,
UpdateProjectQuota)
def format_status_message(self, message):
return message % self.context.get('name', 'unknown project')
def handle(self, request, data):
# update project info
try:
api.tenant_update(request,
tenant_id=data['project_id'],
tenant_name=data['name'],
description=data['description'],
enabled=data['enabled'])
except:
exceptions.handle(request, ignore=True)
return False
# update the project quota
ifcb = data['injected_file_content_bytes']
try:
api.tenant_quota_update(request,
data['project_id'],
metadata_items=data['metadata_items'],
injected_file_content_bytes=ifcb,
volumes=data['volumes'],
gigabytes=data['gigabytes'],
ram=data['ram'],
floating_ips=data['floating_ips'],
instances=data['instances'],
injected_files=data['injected_files'],
cores=data['cores'])
return True
except:
exceptions.handle(request, _('Modified project information, but'
'unable to modify project quotas.'))
return True

View File

@ -26,6 +26,7 @@ from django.utils.encoding import force_unicode
from django.utils.importlib import import_module
from django.utils.translation import ugettext as _
from django.template.defaultfilters import linebreaks, safe
from django.forms.forms import NON_FIELD_ERRORS
from horizon import base
from horizon import exceptions
@ -163,6 +164,12 @@ class Action(forms.Form):
text += linebreaks(force_unicode(self.help_text))
return safe(text)
def add_error(self, message):
"""
Adds an error to the Action's Step based on API issues.
"""
self._get_errors()[NON_FIELD_ERRORS] = self.error_class([message])
def handle(self, request, context):
"""
Handles any requisite processing for this action. The method should
@ -418,6 +425,12 @@ class Step(object):
text += self.action.get_help_text()
return safe(text)
def add_error(self, message):
"""
Adds an error to the Step based on API issues.
"""
self.action.add_error(message)
class WorkflowMetaclass(type):
def __new__(mcs, name, bases, attrs):
@ -779,3 +792,14 @@ class Workflow(html.HTMLElement):
e.g. the path at which the workflow was requested.
"""
return self.request.get_full_path().partition('?')[0]
def add_error_to_step(self, message, slug):
"""
Adds an error to the workflow's Step with the
specifed slug based on API issues. This is useful
when you wish for API errors to appear as errors on
the form rather than using the messages framework.
"""
step = self.get_step(slug)
if step:
step.add_error(message)

View File

@ -54,6 +54,7 @@ class WorkflowView(generic.TemplateView):
template_name = None
context_object_name = "workflow"
ajax_template_name = 'horizon/common/_workflow.html'
step_errors = {}
def __init__(self):
if not self.workflow_class:
@ -100,9 +101,19 @@ class WorkflowView(generic.TemplateView):
template = self.template_name
return template
def add_error_to_step(self, error_msg, step):
self.step_errors[step] = error_msg
def set_workflow_step_errors(self, context):
workflow = context['workflow']
for step in self.step_errors:
error_msg = self.step_errors[step]
workflow.add_error_to_step(error_msg, step)
def get(self, request, *args, **kwargs):
""" Handler for HTTP GET requests. """
context = self.get_context_data(**kwargs)
self.set_workflow_step_errors(context)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):