admin workflow to add/edit project info and quotas
partially implements blueprint tenant-creation-workflow Change-Id: Id3ab3593b35c5dd3e4b9e13513dd3dcbd0a79b9a
This commit is contained in:
parent
576262a142
commit
cbb22a1811
@ -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.'))
|
||||
|
@ -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):
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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)
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
|
194
horizon/dashboards/syspanel/projects/workflows.py
Normal file
194
horizon/dashboards/syspanel/projects/workflows.py
Normal 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
|
@ -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)
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user