Angularize metadata update modals

Update table action to use new angular modal dialog.
Remove "Metadata Update" views.

Implemenents: blueprint angularize-metadata-update-modals
Change-Id: I7b3d2eab8c0830dc44a45901981eb0e71009c038
This commit is contained in:
Szymon Wroblewski 2015-05-19 16:14:59 +02:00 committed by Szymon Wróblewski
parent f030262521
commit 3c2ce866df
25 changed files with 51 additions and 721 deletions

View File

@ -1,48 +0,0 @@
/*
*
* 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.
*/
(function () {
'use strict';
angular
.module('horizon.framework.util.tech-debt')
.controller('hzModalFormUpdateMetadataController', hzModalFormUpdateMetadataController);
hzModalFormUpdateMetadataController.$inject = ['$scope', '$window'];
function hzModalFormUpdateMetadataController($scope, $window) {
var ctrl = this;
ctrl.tree = null;
ctrl.available = $window.available_metadata.namespaces;
ctrl.existing = $window.existing_metadata;
ctrl.saveMetadata = function () {
var metadata = [];
angular.forEach(ctrl.tree.getExisting(), function (value, key) {
metadata.push({
key: key,
value: value
});
});
ctrl.metadata = angular.toJson(metadata);
};
}
}());

View File

@ -1,29 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_name %}metadataForm{% endblock %}
{% block form_validation %}novalidate{% endblock %}
{% block ng_controller %}hzModalFormUpdateMetadataController as metadataCtrl{% endblock %}
{% block modal-body %}
<metadata-tree available="metadataCtrl.available"
existing="metadataCtrl.existing"
model="metadataCtrl.tree"></metadata-tree>
<script type="text/javascript">
var existing_metadata = JSON.parse('{{ existing_metadata|escapejs }}');
var available_metadata = JSON.parse('{{ available_metadata|escapejs }}');
</script>
{% endblock %}
{% block modal-footer %}
<div>
<input class="btn btn-primary pull-right"
ng-disabled="metadataForm.$invalid"
ng-click="metadataCtrl.saveMetadata()" type="submit"
value="{% trans "Save" %}"/>
<a class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
<input type="hidden" name="metadata" ng-value="metadataCtrl.metadata"
ng-model="metadataCtrl.metadata">
</div>
{% endblock %}

View File

@ -17,9 +17,5 @@ AGGREGATES_CREATE_URL = 'horizon:admin:aggregates:create'
AGGREGATES_CREATE_VIEW_TEMPLATE = 'admin/aggregates/create.html' AGGREGATES_CREATE_VIEW_TEMPLATE = 'admin/aggregates/create.html'
AGGREGATES_MANAGE_HOSTS_URL = 'horizon:admin:aggregates:manage_hosts' AGGREGATES_MANAGE_HOSTS_URL = 'horizon:admin:aggregates:manage_hosts'
AGGREGATES_MANAGE_HOSTS_TEMPLATE = 'admin/aggregates/manage_hosts.html' AGGREGATES_MANAGE_HOSTS_TEMPLATE = 'admin/aggregates/manage_hosts.html'
AGGREGATES_UPDATE_METADATA_URL = 'horizon:admin:aggregates:update_metadata'
AGGREGATES_UPDATE_METADATA_TEMPLATE = 'admin/aggregates/update_metadata.html'
AGGREGATES_UPDATE_METADATA_SUBTEMPLATE = \
'admin/aggregates/_update_metadata.html'
AGGREGATES_UPDATE_URL = 'horizon:admin:aggregates:update' AGGREGATES_UPDATE_URL = 'horizon:admin:aggregates:update'
AGGREGATES_UPDATE_VIEW_TEMPLATE = 'admin/aggregates/update.html' AGGREGATES_UPDATE_VIEW_TEMPLATE = 'admin/aggregates/update.html'

View File

@ -10,8 +10,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions from horizon import exceptions
@ -49,32 +47,3 @@ class UpdateAggregateForm(forms.SelfHandlingForm):
exceptions.handle(request, exceptions.handle(request,
_('Unable to update the aggregate.')) _('Unable to update the aggregate.'))
return True return True
class UpdateMetadataForm(forms.SelfHandlingForm):
def handle(self, request, data):
id = self.initial['id']
old_metadata = self.initial['metadata']
try:
new_metadata = json.loads(self.data['metadata'])
metadata = dict(
(item['key'], str(item['value']))
for item in new_metadata
)
for key in old_metadata:
if key not in metadata:
metadata[key] = None
api.nova.aggregate_set_metadata(request, id, metadata)
message = _('Metadata successfully updated.')
messages.success(request, message)
except Exception:
msg = _('Unable to update the aggregate metadata.')
exceptions.handle(request, msg)
return False
return True

View File

@ -62,9 +62,19 @@ class ManageHostsAction(tables.LinkAction):
class UpdateMetadataAction(tables.LinkAction): class UpdateMetadataAction(tables.LinkAction):
name = "update-metadata" name = "update-metadata"
verbose_name = _("Update Metadata") verbose_name = _("Update Metadata")
url = constants.AGGREGATES_UPDATE_METADATA_URL ajax = False
classes = ("ajax-modal",)
icon = "pencil" icon = "pencil"
attrs = {"ng-controller": "MetadataModalHelperController as modal"}
def __init__(self, attrs=None, **kwargs):
kwargs['preempt'] = True
super(UpdateMetadataAction, self).__init__(attrs, **kwargs)
def get_link_url(self, datum):
image_id = self.table.get_object_id(datum)
self.attrs['ng-click'] = (
"modal.openMetadataModal('aggregate', '%s', true)" % image_id)
return "javascript:void(0);"
class UpdateAggregateAction(tables.LinkAction): class UpdateAggregateAction(tables.LinkAction):

View File

@ -1,11 +0,0 @@
{% extends 'horizon/common/_modal_form_update_metadata.html' %}
{% load i18n %}
{% load url from future %}
{% block title %}{% trans "Update Aggregate Metadata" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Update Aggregate Metadata") %}
{% endblock page_header %}
{% block form_action %}{% url 'horizon:admin:aggregates:update_metadata' id %}{% endblock %}
{% block modal-header %}{% trans "Update Aggregate Metadata" %}{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Aggregate Metadata" %}{% endblock %}
{% block main %}
{% include 'admin/aggregates/_update_metadata.html' %}
{% endblock %}

View File

@ -10,8 +10,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
import mock import mock
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -451,82 +449,3 @@ class ManageHostsTests(test.BaseAdminViewTests):
form_data, form_data,
addAggregate=False, addAggregate=False,
cleanAggregates=True) cleanAggregates=True)
class HostAggregateMetadataTests(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('aggregate_get',),
api.glance: ('metadefs_namespace_list',
'metadefs_namespace_get')})
def test_host_aggregate_metadata_get(self):
aggregate = self.aggregates.first()
api.nova.aggregate_get(
IsA(http.HttpRequest),
str(aggregate.id)
).AndReturn(aggregate)
namespaces = self.metadata_defs.list()
api.glance.metadefs_namespace_list(
IsA(http.HttpRequest),
filters={'resource_types': ['OS::Nova::Aggregate']}
).AndReturn((namespaces, False, False))
for namespace in namespaces:
api.glance.metadefs_namespace_get(
IsA(http.HttpRequest),
namespace.namespace,
'OS::Nova::Aggregate'
).AndReturn(namespace)
self.mox.ReplayAll()
res = self.client.get(
reverse(constants.AGGREGATES_UPDATE_METADATA_URL,
args=[aggregate.id]))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(
res,
constants.AGGREGATES_UPDATE_METADATA_TEMPLATE
)
self.assertTemplateUsed(
res,
constants.AGGREGATES_UPDATE_METADATA_SUBTEMPLATE
)
self.assertContains(res, 'namespace_1')
self.assertContains(res, 'namespace_2')
self.assertContains(res, 'namespace_3')
self.assertContains(res, 'namespace_4')
@test.create_stubs({api.nova: ('aggregate_get', 'aggregate_set_metadata')})
def test_host_aggregate_metadata_update(self):
aggregate = self.aggregates.first()
aggregate.metadata = {'key': 'test_key', 'value': 'test_value'}
api.nova.aggregate_get(
IsA(http.HttpRequest),
str(aggregate.id)
).AndReturn(aggregate)
api.nova.aggregate_set_metadata(
IsA(http.HttpRequest),
str(aggregate.id),
{'value': None, 'key': None, 'test_key': 'test_value'}
).AndReturn(None)
self.mox.ReplayAll()
form_data = {"metadata": json.dumps([aggregate.metadata])}
res = self.client.post(
reverse(constants.AGGREGATES_UPDATE_METADATA_URL,
args=(aggregate.id,)), form_data)
self.assertEqual(res.status_code, 302)
self.assertNoFormErrors(res)
self.assertMessageCount(success=1)
self.assertRedirectsNoFollow(
res,
reverse(constants.AGGREGATES_INDEX_URL)
)

View File

@ -25,8 +25,6 @@ urlpatterns = patterns(
views.CreateView.as_view(), name='create'), views.CreateView.as_view(), name='create'),
url(r'^(?P<id>[^/]+)/update/$', url(r'^(?P<id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'), views.UpdateView.as_view(), name='update'),
url(r'^(?P<id>[^/]+)/update_metadata/$',
views.UpdateMetadataView.as_view(), name='update_metadata'),
url(r'^(?P<id>[^/]+)/manage_hosts/$', url(r'^(?P<id>[^/]+)/manage_hosts/$',
views.ManageHostsView.as_view(), name='manage_hosts'), views.ManageHostsView.as_view(), name='manage_hosts'),
) )

View File

@ -10,16 +10,12 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions from horizon import exceptions
from horizon import forms from horizon import forms
from horizon import tables from horizon import tables
from horizon.utils import memoized
from horizon import workflows from horizon import workflows
from openstack_dashboard import api from openstack_dashboard import api
@ -101,55 +97,6 @@ class UpdateView(forms.ModalFormView):
return self._object return self._object
class UpdateMetadataView(forms.ModalFormView):
template_name = constants.AGGREGATES_UPDATE_METADATA_TEMPLATE
form_class = aggregate_forms.UpdateMetadataForm
success_url = reverse_lazy(constants.AGGREGATES_INDEX_URL)
page_title = _("Update Aggregate Metadata")
def get_initial(self):
aggregate = self.get_object()
return {'id': self.kwargs["id"], 'metadata': aggregate.metadata}
def get_context_data(self, **kwargs):
context = super(UpdateMetadataView, self).get_context_data(**kwargs)
aggregate = self.get_object()
context['existing_metadata'] = json.dumps(aggregate.metadata)
resource_type = 'OS::Nova::Aggregate'
namespaces = []
try:
# metadefs_namespace_list() returns a tuple with list as 1st elem
namespaces = [
api.glance.metadefs_namespace_get(self.request, x.namespace,
resource_type)
for x in api.glance.metadefs_namespace_list(
self.request,
filters={'resource_types': [resource_type]}
)[0]
]
except Exception:
msg = _('Unable to retrieve available metadata for aggregate.')
exceptions.handle(self.request, msg)
context['available_metadata'] = json.dumps({'namespaces': namespaces})
context['id'] = self.kwargs['id']
return context
@memoized.memoized_method
def get_object(self):
aggregate_id = self.kwargs['id']
try:
return api.nova.aggregate_get(self.request, aggregate_id)
except Exception:
msg = _('Unable to retrieve the aggregate to be '
'updated.')
exceptions.handle(
self.request, msg, redirect=reverse(INDEX_URL))
class ManageHostsView(workflows.WorkflowView): class ManageHostsView(workflows.WorkflowView):
template_name = constants.AGGREGATES_MANAGE_HOSTS_TEMPLATE template_name = constants.AGGREGATES_MANAGE_HOSTS_TEMPLATE
workflow_class = aggregate_workflows.ManageAggregateHostsWorkflow workflow_class = aggregate_workflows.ManageAggregateHostsWorkflow

View File

@ -16,6 +16,3 @@ FLAVORS_CREATE_URL = 'horizon:admin:flavors:create'
FLAVORS_CREATE_VIEW_TEMPLATE = 'admin/flavors/create.html' FLAVORS_CREATE_VIEW_TEMPLATE = 'admin/flavors/create.html'
FLAVORS_UPDATE_URL = 'horizon:admin:flavors:update' FLAVORS_UPDATE_URL = 'horizon:admin:flavors:update'
FLAVORS_UPDATE_VIEW_TEMPLATE = 'admin/flavors/update.html' FLAVORS_UPDATE_VIEW_TEMPLATE = 'admin/flavors/update.html'
FLAVORS_UPDATE_METADATA_URL = 'horizon:admin:flavors:update_metadata'
FLAVORS_UPDATE_METADATA_TEMPLATE = 'admin/flavors/update_metadata.html'
FLAVORS_UPDATE_METADATA_SUBTEMPLATE = 'admin/flavors/_update_metadata.html'

View File

@ -1,50 +0,0 @@
# 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 json
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
class UpdateMetadataForm(forms.SelfHandlingForm):
def handle(self, request, data):
id = self.initial['id']
old_metadata = self.initial['metadata']
try:
new_metadata = json.loads(self.data['metadata'])
metadata = dict(
(item['key'], str(item['value']))
for item in new_metadata
)
api.nova.flavor_extra_set(request, id, metadata)
remove_keys = [key for key in old_metadata if key not in metadata]
api.nova.flavor_extra_delete(request, id, remove_keys)
message = _('Metadata successfully updated.')
messages.success(request, message)
except Exception:
exceptions.handle(request,
_('Unable to update the flavor metadata.'))
return False
return True

View File

@ -66,11 +66,29 @@ class UpdateFlavor(tables.LinkAction):
class UpdateMetadata(tables.LinkAction): class UpdateMetadata(tables.LinkAction):
url = "horizon:admin:flavors:update_metadata"
name = "update_metadata" name = "update_metadata"
verbose_name = _("Update Metadata") verbose_name = _("Update Metadata")
classes = ("ajax-modal",) ajax = False
icon = "pencil" icon = "pencil"
attrs = {"ng-controller": "MetadataModalHelperController as modal"}
def __init__(self, **kwargs):
kwargs['preempt'] = True
super(UpdateMetadata, self).__init__(**kwargs)
def get_link_url(self, datum):
obj_id = self.table.get_object_id(datum)
self.attrs['ng-click'] = (
"modal.openMetadataModal('flavor', '%s', true)" % obj_id)
return "javascript:void(0);"
class UpdateMetadataColumn(tables.Column):
def get_link_url(self, datum):
obj_id = self.table.get_object_id(datum)
self.link_attrs['ng-click'] = (
"modal.openMetadataModal('flavor', '%s', true)" % obj_id)
return "javascript:void(0);"
class ModifyAccess(tables.LinkAction): class ModifyAccess(tables.LinkAction):
@ -138,12 +156,13 @@ class FlavorsTable(tables.DataTable):
verbose_name=_("Public"), verbose_name=_("Public"),
empty_value=False, empty_value=False,
filters=(filters.yesno, filters.capfirst)) filters=(filters.yesno, filters.capfirst))
extra_specs = tables.Column(get_extra_specs, extra_specs = UpdateMetadataColumn(
verbose_name=_("Metadata"), get_extra_specs,
link="horizon:admin:flavors:update_metadata", verbose_name=_("Metadata"),
link_classes=("ajax-modal",), link=True,
empty_value=False, empty_value=False,
filters=(filters.yesno, filters.capfirst)) filters=(filters.yesno, filters.capfirst),
link_attrs={'ng-controller': 'MetadataModalHelperController as modal'})
class Meta(object): class Meta(object):
name = "flavors" name = "flavors"

View File

@ -1,11 +0,0 @@
{% extends 'horizon/common/_modal_form_update_metadata.html' %}
{% load i18n %}
{% load url from future %}
{% block title %}{% trans "Update Flavor Metadata" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Update Flavor Metadata") %}
{% endblock page_header %}
{% block form_action %}{% url 'horizon:admin:flavors:update_metadata' id %}{% endblock %}
{% block modal-header %}{% trans "Update Flavor Metadata" %}{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Flavor Metadata" %}{% endblock %}
{% block main %}
{% include 'admin/flavors/_update_metadata.html' %}
{% endblock %}

View File

@ -10,8 +10,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django import http from django import http
from mox3.mox import IsA # noqa from mox3.mox import IsA # noqa
@ -764,117 +762,3 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
data = {'eph_gb': -1} data = {'eph_gb': -1}
self.generic_update_flavor_invalid_data_form_fails(override_data=data, self.generic_update_flavor_invalid_data_form_fails(override_data=data,
error_msg=error) error_msg=error)
class FlavorUpdateMetadataViewTest(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('flavor_get_extras',),
api.glance: ('metadefs_namespace_list',
'metadefs_namespace_get')})
def test_flavor_metadata_get(self):
# <Flavor: m1.metadata>
flavor = self.flavors.list()[3]
namespaces = self.metadata_defs.list()
api.nova.flavor_get_extras(
IsA(http.HttpRequest),
flavor.id
).AndReturn([flavor.extra_specs])
api.glance.metadefs_namespace_list(
IsA(http.HttpRequest),
filters={
'resource_types': ['OS::Nova::Flavor']
}
).AndReturn((namespaces, False, False))
for namespace in namespaces:
api.glance.metadefs_namespace_get(
IsA(http.HttpRequest),
namespace.namespace,
'OS::Nova::Flavor'
).AndReturn(namespace)
self.mox.ReplayAll()
res = self.client.get(
reverse(
constants.FLAVORS_UPDATE_METADATA_URL,
kwargs={'id': flavor.id}
)
)
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(
res,
constants.FLAVORS_UPDATE_METADATA_TEMPLATE
)
self.assertTemplateUsed(
res,
constants.FLAVORS_UPDATE_METADATA_SUBTEMPLATE
)
self.assertContains(res, 'namespace_1')
self.assertContains(res, 'namespace_2')
self.assertContains(res, 'namespace_3')
self.assertContains(res, 'namespace_4')
@test.create_stubs({api.nova: ('flavor_get_extras',
'flavor_extra_set',
'flavor_extra_delete')})
def test_flavor_metadata_update(self):
# <Flavor: m1.metadata>
flavor = self.flavors.list()[3]
api.nova.flavor_get_extras(
IsA(http.HttpRequest),
flavor.id
).AndReturn([flavor.extra_specs])
api.nova.flavor_extra_set(
IsA(http.HttpRequest),
flavor.id,
{'key_mock': 'value_mock'}
).AndReturn(None)
api.nova.flavor_extra_delete(
IsA(http.HttpRequest),
flavor.id,
[]
).AndReturn(None)
self.mox.ReplayAll()
metadata = [{'value': 'value_mock', 'key': 'key_mock'}]
formData = {'metadata': json.dumps(metadata)}
res = self.client.post(
reverse(
constants.FLAVORS_UPDATE_METADATA_URL,
kwargs={'id': flavor.id}
),
formData
)
self.assertEqual(res.status_code, 302)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
self.assertMessageCount(success=1)
@test.create_stubs({api.nova: ('flavor_get_extras',)})
def test_flavor_metadata_get_get_extras_fails(self):
# <Flavor: m1.metadata>
flavor = self.flavors.list()[3]
api.nova.flavor_get_extras(
IsA(http.HttpRequest),
flavor.id
).AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
res = self.client.get(
reverse(
constants.FLAVORS_UPDATE_METADATA_URL,
kwargs={'id': flavor.id}
)
)
self.assertEqual(res.status_code, 302)
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
self.assertMessageCount(error=1)

View File

@ -26,7 +26,5 @@ urlpatterns = patterns(
'openstack_dashboard.dashboards.admin.flavors.views', 'openstack_dashboard.dashboards.admin.flavors.views',
url(r'^$', views.IndexView.as_view(), name='index'), url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'), url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<id>[^/]+)/update_metadata/$',
views.UpdateMetadataView.as_view(), name='update_metadata'),
url(r'^(?P<id>[^/]+)/update/$', views.UpdateView.as_view(), name='update'), url(r'^(?P<id>[^/]+)/update/$', views.UpdateView.as_view(), name='update'),
) )

View File

@ -16,22 +16,15 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions from horizon import exceptions
from horizon import forms
from horizon import tables from horizon import tables
from horizon.utils import memoized
from horizon import workflows from horizon import workflows
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.flavors \
import forms as project_forms
from openstack_dashboard.dashboards.admin.flavors \ from openstack_dashboard.dashboards.admin.flavors \
import tables as project_tables import tables as project_tables
from openstack_dashboard.dashboards.admin.flavors \ from openstack_dashboard.dashboards.admin.flavors \
@ -88,54 +81,3 @@ class UpdateView(workflows.WorkflowView):
'disk_gb': flavor.disk, 'disk_gb': flavor.disk,
'swap_mb': flavor.swap or 0, 'swap_mb': flavor.swap or 0,
'eph_gb': getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral', None)} 'eph_gb': getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral', None)}
class UpdateMetadataView(forms.ModalFormView):
template_name = "admin/flavors/update_metadata.html"
form_class = project_forms.UpdateMetadataForm
success_url = reverse_lazy('horizon:admin:flavors:index')
page_title = _("Update Flavor Metadata")
def get_initial(self):
extra_specs_dict = self.get_object()
return {'id': self.kwargs["id"], 'metadata': extra_specs_dict}
def get_context_data(self, **kwargs):
context = super(UpdateMetadataView, self).get_context_data(**kwargs)
extra_specs_dict = self.get_object()
context['existing_metadata'] = json.dumps(extra_specs_dict)
resource_type = 'OS::Nova::Flavor'
namespaces = []
try:
# metadefs_namespace_list() returns a tuple with list as 1st elem
namespaces = [
api.glance.metadefs_namespace_get(self.request, x.namespace,
resource_type)
for x in api.glance.metadefs_namespace_list(
self.request,
filters={'resource_types': [resource_type]}
)[0]
]
except Exception:
msg = _('Unable to retrieve available metadata for flavors.')
exceptions.handle(self.request, msg)
context['available_metadata'] = json.dumps({'namespaces': namespaces})
context['id'] = self.kwargs['id']
return context
@memoized.memoized_method
def get_object(self):
flavor_id = self.kwargs['id']
try:
extra_specs = api.nova.flavor_get_extras(self.request, flavor_id)
return dict((i.key, i.value) for i in extra_specs)
except Exception:
msg = _('Unable to retrieve the flavor metadata.')
exceptions.handle(self.request, msg,
redirect=reverse(INDEX_URL))

View File

@ -16,16 +16,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images.images \ from openstack_dashboard.dashboards.project.images.images \
import forms as images_forms import forms as images_forms
@ -36,32 +26,3 @@ class AdminCreateImageForm(images_forms.CreateImageForm):
class AdminUpdateImageForm(images_forms.UpdateImageForm): class AdminUpdateImageForm(images_forms.UpdateImageForm):
pass pass
class UpdateMetadataForm(forms.SelfHandlingForm):
def handle(self, request, data):
id = self.initial['id']
old_metadata = self.initial['metadata']
try:
new_metadata = json.loads(self.data['metadata'])
metadata = dict(
(item['key'], str(item['value']))
for item in new_metadata
)
remove_props = [key for key in old_metadata if key not in metadata]
api.glance.image_update_properties(request,
id,
remove_props,
**metadata)
message = _('Metadata successfully updated.')
messages.success(request, message)
except Exception:
exceptions.handle(request,
_('Unable to update the image metadata.'))
return False
return True

View File

@ -41,11 +41,21 @@ class AdminEditImage(project_tables.EditImage):
class UpdateMetadata(tables.LinkAction): class UpdateMetadata(tables.LinkAction):
url = "horizon:admin:images:update_metadata"
name = "update_metadata" name = "update_metadata"
verbose_name = _("Update Metadata") verbose_name = _("Update Metadata")
classes = ("ajax-modal",) ajax = False
icon = "pencil" icon = "pencil"
attrs = {"ng-controller": "MetadataModalHelperController as modal"}
def __init__(self, attrs=None, **kwargs):
kwargs['preempt'] = True
super(UpdateMetadata, self).__init__(attrs, **kwargs)
def get_link_url(self, datum):
image_id = self.table.get_object_id(datum)
self.attrs['ng-click'] = (
"modal.openMetadataModal('image', '%s', true)" % image_id)
return "javascript:void(0);"
class UpdateRow(tables.Row): class UpdateRow(tables.Row):

View File

@ -1 +0,0 @@
{% extends 'horizon/common/_modal_form_update_metadata.html' %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Image Metadata" %}{% endblock %}
{% block main %}
{% include 'admin/images/_update_metadata.html' %}
{% endblock %}

View File

@ -12,8 +12,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django import http from django import http
@ -26,10 +24,6 @@ from openstack_dashboard.test import helpers as test
from openstack_dashboard.dashboards.admin.images import tables from openstack_dashboard.dashboards.admin.images import tables
IMAGE_METADATA_URL = reverse('horizon:admin:images:update_metadata',
kwargs={
"id": "007e7d55-fe1e-4c5c-bf08-44b4a4964822"})
class ImageCreateViewTest(test.BaseAdminViewTests): class ImageCreateViewTest(test.BaseAdminViewTests):
@test.create_stubs({api.glance: ('image_list_detailed',)}) @test.create_stubs({api.glance: ('image_list_detailed',)})
@ -138,64 +132,6 @@ class ImagesViewTest(test.BaseAdminViewTests):
1) 1)
self.assertContains(res, 'test_tenant', 2, 200) self.assertContains(res, 'test_tenant', 2, 200)
@test.create_stubs({api.glance: ('image_get',
'metadefs_namespace_list',
'metadefs_namespace_get')})
def test_images_metadata_get(self):
image = self.images.first()
api.glance.image_get(
IsA(http.HttpRequest),
image.id
).AndReturn(image)
namespaces = self.metadata_defs.list()
api.glance.metadefs_namespace_list(IsA(http.HttpRequest), filters={
'resource_types': ['OS::Glance::Image']}).AndReturn(
(namespaces, False, False))
for namespace in namespaces:
api.glance.metadefs_namespace_get(
IsA(http.HttpRequest),
namespace.namespace,
'OS::Glance::Image'
).AndReturn(namespace)
self.mox.ReplayAll()
res = self.client.get(IMAGE_METADATA_URL)
self.assertTemplateUsed(res, 'admin/images/update_metadata.html')
self.assertContains(res, 'namespace_1')
self.assertContains(res, 'namespace_2')
self.assertContains(res, 'namespace_3')
self.assertContains(res, 'namespace_4')
@test.create_stubs({api.glance: ('image_get', 'image_update_properties')})
def test_images_metadata_update(self):
image = self.images.first()
api.glance.image_get(
IsA(http.HttpRequest),
image.id
).AndReturn(image)
api.glance.image_update_properties(
IsA(http.HttpRequest), image.id, ['image_type'],
hw_machine_type='mock_value').AndReturn(None)
self.mox.ReplayAll()
metadata = [{"value": "mock_value", "key": "hw_machine_type"}]
formData = {"metadata": json.dumps(metadata)}
res = self.client.post(IMAGE_METADATA_URL, formData)
self.assertNoFormErrors(res)
self.assertMessageCount(success=1)
self.assertRedirectsNoFollow(
res, reverse('horizon:admin:images:index')
)
@override_settings(API_RESULT_PAGE_SIZE=2) @override_settings(API_RESULT_PAGE_SIZE=2)
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',),
api.keystone: ('tenant_list',)}) api.keystone: ('tenant_list',)})

View File

@ -28,8 +28,6 @@ urlpatterns = patterns(
url(r'^create/$', views.CreateView.as_view(), name='create'), url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<image_id>[^/]+)/update/$', url(r'^(?P<image_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'), views.UpdateView.as_view(), name='update'),
url(r'^(?P<id>[^/]+)/update_metadata/$',
views.UpdateMetadataView.as_view(), name='update_metadata'),
url(r'^(?P<image_id>[^/]+)/detail/$', url(r'^(?P<image_id>[^/]+)/detail/$',
views.DetailView.as_view(), name='detail') views.DetailView.as_view(), name='detail')
) )

View File

@ -16,21 +16,16 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
import logging import logging
from oslo_utils import units from oslo_utils import units
import six
from django import conf
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions from horizon import exceptions
from horizon import forms
from horizon import tables from horizon import tables
from horizon.utils import memoized
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images.images import views from openstack_dashboard.dashboards.project.images.images import views
@ -149,81 +144,3 @@ class DetailView(views.DetailView):
context["url"] = reverse('horizon:admin:images:index') context["url"] = reverse('horizon:admin:images:index')
context["actions"] = table.render_row_actions(context["image"]) context["actions"] = table.render_row_actions(context["image"])
return context return context
class UpdateMetadataView(forms.ModalFormView):
template_name = "admin/images/update_metadata.html"
modal_header = _("Update Image")
form_id = "update_image_form"
form_class = project_forms.UpdateMetadataForm
submit_url = "horizon:admin:images:update_metadata"
success_url = reverse_lazy('horizon:admin:images:index')
page_title = _("Update Image Metadata")
def get_initial(self):
image = self.get_object()
return {'id': self.kwargs["id"], 'metadata': image.properties}
def get_context_data(self, **kwargs):
context = super(UpdateMetadataView, self).get_context_data(**kwargs)
image = self.get_object()
reserved_props = getattr(conf.settings,
'IMAGE_RESERVED_CUSTOM_PROPERTIES', [])
image.properties = dict((k, v)
for (k, v) in six.iteritems(image.properties)
if k not in reserved_props)
context['existing_metadata'] = json.dumps(image.properties)
args = (self.kwargs['id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
resource_type = 'OS::Glance::Image'
namespaces = []
try:
# metadefs_namespace_list() returns a tuple with list as 1st elem
available_namespaces = [x.namespace for x in
api.glance.metadefs_namespace_list(
self.request,
filters={"resource_types":
[resource_type]}
)[0]]
for namespace in available_namespaces:
details = api.glance.metadefs_namespace_get(self.request,
namespace,
resource_type)
# Filter out reserved custom properties from namespace
if reserved_props:
if hasattr(details, 'properties'):
details.properties = dict(
(k, v)
for (k, v) in six.iteritems(details.properties)
if k not in reserved_props
)
if hasattr(details, 'objects'):
for obj in details.objects:
obj['properties'] = dict(
(k, v)
for (k, v) in six.iteritems(obj['properties'])
if k not in reserved_props
)
namespaces.append(details)
except Exception:
msg = _('Unable to retrieve available properties for image.')
exceptions.handle(self.request, msg)
context['available_metadata'] = json.dumps({'namespaces': namespaces})
context['id'] = self.kwargs['id']
return context
@memoized.memoized_method
def get_object(self):
image_id = self.kwargs['id']
try:
return api.glance.image_get(self.request, image_id)
except Exception:
msg = _('Unable to retrieve the image to be updated.')
exceptions.handle(self.request, msg,
redirect=reverse('horizon:admin:images:index'))