Merge "Add ability to manage image custom properties"
This commit is contained in:
commit
a0f7235278
@ -301,6 +301,20 @@ icon names are based on the default icon theme provided by Bootstrap.
|
||||
|
||||
Example: ``[{'text': 'Official', 'tenant': '27d0058849da47c896d205e2fc25a5e8', 'icon': 'icon-ok'}]``
|
||||
|
||||
``IMAGE_RESERVED_CUSTOM_PROPERTIES``
|
||||
------------------------------------
|
||||
|
||||
.. versionadded:: 2014.2(Juno)
|
||||
|
||||
Default: ``[]``
|
||||
|
||||
A list of image custom property keys that should not be displayed in the
|
||||
Image Custom Properties table.
|
||||
|
||||
This setting can be used in the case where a separate panel is used for
|
||||
managing a custom property or if a certain custom property should never be
|
||||
edited.
|
||||
|
||||
``OPENSTACK_ENABLE_PASSWORD_RETRIEVE``
|
||||
--------------------------------------
|
||||
|
||||
|
@ -34,13 +34,21 @@ from openstack_dashboard.api import base
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def glanceclient(request):
|
||||
class ImageCustomProperty(object):
|
||||
def __init__(self, image_id, key, val):
|
||||
self.image_id = image_id
|
||||
self.id = key
|
||||
self.key = key
|
||||
self.value = val
|
||||
|
||||
|
||||
def glanceclient(request, version='1'):
|
||||
url = base.url_for(request, 'image')
|
||||
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
|
||||
cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
|
||||
LOG.debug('glanceclient connection created using token "%s" and url "%s"'
|
||||
% (request.user.token.id, url))
|
||||
return glance_client.Client('1', url, token=request.user.token.id,
|
||||
return glance_client.Client(version, url, token=request.user.token.id,
|
||||
insecure=insecure, cacert=cacert)
|
||||
|
||||
|
||||
@ -58,6 +66,26 @@ def image_get(request, image_id):
|
||||
return image
|
||||
|
||||
|
||||
def image_get_properties(request, image_id, reserved=True):
|
||||
"""List all custom properties of an image."""
|
||||
image = glanceclient(request, '2').images.get(image_id)
|
||||
reserved_props = getattr(settings, 'IMAGE_RESERVED_CUSTOM_PROPERTIES', [])
|
||||
properties_list = []
|
||||
for key in image.keys():
|
||||
if reserved or key not in reserved_props:
|
||||
prop = ImageCustomProperty(image_id, key, image.get(key))
|
||||
properties_list.append(prop)
|
||||
return properties_list
|
||||
|
||||
|
||||
def image_get_property(request, image_id, key, reserved=True):
|
||||
"""Get a custom property of an image."""
|
||||
for prop in image_get_properties(request, image_id, reserved):
|
||||
if prop.key == key:
|
||||
return prop
|
||||
return None
|
||||
|
||||
|
||||
def image_list_detailed(request, marker=None, sort_dir='desc',
|
||||
sort_key='created_at', filters=None, paginate=False):
|
||||
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
|
||||
@ -121,3 +149,13 @@ def image_create(request, **kwargs):
|
||||
'purge_props': False})
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def image_update_properties(request, image_id, **kwargs):
|
||||
"""Add or update a custom property of an image."""
|
||||
return glanceclient(request, '2').images.update(image_id, None, **kwargs)
|
||||
|
||||
|
||||
def image_delete_properties(request, image_id, keys):
|
||||
"""Delete custom properties for an image."""
|
||||
return glanceclient(request, '2').images.update(image_id, keys)
|
||||
|
@ -0,0 +1,89 @@
|
||||
# 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.utils.translation import ugettext_lazy as _
|
||||
|
||||
from glanceclient import exc
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
|
||||
def str2bool(value):
|
||||
"""Convert a string value to boolean
|
||||
"""
|
||||
return value.lower() in ("yes", "true", "1")
|
||||
|
||||
|
||||
# Mapping of property names to type, used for converting input string value
|
||||
# before submitting.
|
||||
PROPERTY_TYPES = {'min_disk': long, 'min_ram': long, 'protected': str2bool}
|
||||
|
||||
|
||||
def convert_value(key, value):
|
||||
"""Convert the property value to the proper type if necessary.
|
||||
"""
|
||||
_type = PROPERTY_TYPES.get(key)
|
||||
if _type:
|
||||
return _type(value)
|
||||
return value
|
||||
|
||||
|
||||
class CreateProperty(forms.SelfHandlingForm):
|
||||
key = forms.CharField(max_length="255", label=_("Key"))
|
||||
value = forms.CharField(label=_("Value"))
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
api.glance.image_update_properties(request,
|
||||
self.initial['image_id'],
|
||||
**{data['key']: convert_value(data['key'], data['value'])})
|
||||
msg = _('Created custom property "%s".') % data['key']
|
||||
messages.success(request, msg)
|
||||
return True
|
||||
except exc.HTTPForbidden:
|
||||
msg = _('Unable to create image custom property. Property "%s" '
|
||||
'is read only.' % data['key'])
|
||||
exceptions.handle(request, msg)
|
||||
except exc.HTTPConflict:
|
||||
msg = _('Unable to create image custom property. Property "%s" '
|
||||
'already exists.' % data['key'])
|
||||
exceptions.handle(request, msg)
|
||||
except Exception:
|
||||
msg = _('Unable to create image custom '
|
||||
'property "%s".' % data['key'])
|
||||
exceptions.handle(request, msg)
|
||||
|
||||
|
||||
class EditProperty(forms.SelfHandlingForm):
|
||||
key = forms.CharField(widget=forms.widgets.HiddenInput)
|
||||
value = forms.CharField(label=_("Value"))
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
api.glance.image_update_properties(request,
|
||||
self.initial['image_id'],
|
||||
**{data['key']: convert_value(data['key'], data['value'])})
|
||||
msg = _('Saved custom property "%s".') % data['key']
|
||||
messages.success(request, msg)
|
||||
return True
|
||||
except exc.HTTPForbidden:
|
||||
msg = _('Unable to edit image custom property. Property "%s" '
|
||||
'is read only.' % data['key'])
|
||||
exceptions.handle(request, msg)
|
||||
except Exception:
|
||||
msg = _('Unable to edit image custom '
|
||||
'property "%s".' % data['key'])
|
||||
exceptions.handle(request, msg)
|
@ -0,0 +1,93 @@
|
||||
# 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.core.urlresolvers import reverse
|
||||
from django.utils import http
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
|
||||
# Most of the following image custom properties can be found in the glance
|
||||
# project at glance.api.v2.images.RequestDeserializer.
|
||||
|
||||
# Properties that cannot be edited
|
||||
READONLY_PROPERTIES = ['checksum', 'container_format', 'created_at', 'deleted',
|
||||
'deleted_at', 'direct_url', 'disk_format', 'file', 'id', 'is_public',
|
||||
'location', 'owner', 'schema', 'self', 'size',
|
||||
'status', 'tags', 'updated_at', 'virtual_size']
|
||||
|
||||
# Properties that cannot be deleted
|
||||
REQUIRED_PROPERTIES = ['checksum', 'container_format', 'created_at', 'deleted',
|
||||
'deleted_at', 'direct_url', 'disk_format', 'file', 'id', 'is_public',
|
||||
'location', 'min_disk', 'min_ram', 'name', 'owner', 'protected', 'schema',
|
||||
'self', 'size', 'status', 'tags', 'updated_at', 'virtual_size',
|
||||
'visibility']
|
||||
|
||||
|
||||
class PropertyDelete(tables.DeleteAction):
|
||||
data_type_singular = _("Property")
|
||||
data_type_plural = _("Properties")
|
||||
|
||||
def allowed(self, request, prop=None):
|
||||
if prop and prop.key in REQUIRED_PROPERTIES:
|
||||
return False
|
||||
return True
|
||||
|
||||
def delete(self, request, obj_ids):
|
||||
api.glance.image_delete_properties(request, self.table.kwargs['id'],
|
||||
[obj_ids])
|
||||
|
||||
|
||||
class PropertyCreate(tables.LinkAction):
|
||||
name = "create"
|
||||
verbose_name = _("Create")
|
||||
url = "horizon:admin:images:properties:create"
|
||||
classes = ("btn-create", "ajax-modal")
|
||||
|
||||
def get_link_url(self, custom_property=None):
|
||||
return reverse(self.url, args=[self.table.kwargs['id']])
|
||||
|
||||
|
||||
class PropertyEdit(tables.LinkAction):
|
||||
name = "edit"
|
||||
verbose_name = _("Edit")
|
||||
url = "horizon:admin:images:properties:edit"
|
||||
classes = ("btn-edit", "ajax-modal")
|
||||
|
||||
def allowed(self, request, prop=None):
|
||||
if prop and prop.key in READONLY_PROPERTIES:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_link_url(self, custom_property):
|
||||
return reverse(self.url, args=[self.table.kwargs['id'],
|
||||
http.urlquote(custom_property.key, '')])
|
||||
|
||||
|
||||
class PropertiesTable(tables.DataTable):
|
||||
key = tables.Column('key', verbose_name=_('Key'))
|
||||
value = tables.Column('value', verbose_name=_('Value'))
|
||||
|
||||
class Meta:
|
||||
name = "properties"
|
||||
verbose_name = _("Custom Properties")
|
||||
table_actions = (PropertyCreate, PropertyDelete)
|
||||
row_actions = (PropertyEdit, PropertyDelete)
|
||||
|
||||
def get_object_id(self, datum):
|
||||
return datum.key
|
||||
|
||||
def get_object_display(self, datum):
|
||||
return datum.key
|
102
openstack_dashboard/dashboards/admin/images/properties/tests.py
Normal file
102
openstack_dashboard/dashboards/admin/images/properties/tests.py
Normal file
@ -0,0 +1,102 @@
|
||||
# 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.core.urlresolvers import reverse
|
||||
from django import http
|
||||
|
||||
from mox import IsA # noqa
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
|
||||
class ImageCustomPropertiesTests(test.BaseAdminViewTests):
|
||||
|
||||
@test.create_stubs({api.glance: ('image_get',
|
||||
'image_get_properties'), })
|
||||
def test_list_properties(self):
|
||||
image = self.images.first()
|
||||
props = [api.glance.ImageCustomProperty(image.id, 'k1', 'v1')]
|
||||
api.glance.image_get(IsA(http.HttpRequest), image.id).AndReturn(image)
|
||||
api.glance.image_get_properties(IsA(http.HttpRequest),
|
||||
image.id, False).AndReturn(props)
|
||||
self.mox.ReplayAll()
|
||||
url = reverse('horizon:admin:images:properties:index', args=[image.id])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTemplateUsed(resp, "admin/images/properties/index.html")
|
||||
|
||||
@test.create_stubs({api.glance: ('image_update_properties',), })
|
||||
def test_property_create_post(self):
|
||||
image = self.images.first()
|
||||
create_url = reverse('horizon:admin:images:properties:create',
|
||||
args=[image.id])
|
||||
index_url = reverse('horizon:admin:images:properties:index',
|
||||
args=[image.id])
|
||||
api.glance.image_update_properties(IsA(http.HttpRequest),
|
||||
image.id, **{'k1': 'v1'})
|
||||
self.mox.ReplayAll()
|
||||
data = {'image_id': image.id,
|
||||
'key': 'k1',
|
||||
'value': 'v1'}
|
||||
resp = self.client.post(create_url, data)
|
||||
self.assertNoFormErrors(resp)
|
||||
self.assertMessageCount(success=1)
|
||||
self.assertRedirectsNoFollow(resp, index_url)
|
||||
|
||||
@test.create_stubs({api.glance: ('image_get',), })
|
||||
def test_property_create_get(self):
|
||||
image = self.images.first()
|
||||
create_url = reverse('horizon:admin:images:properties:create',
|
||||
args=[image.id])
|
||||
api.glance.image_get(IsA(http.HttpRequest), image.id).AndReturn(image)
|
||||
self.mox.ReplayAll()
|
||||
resp = self.client.get(create_url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTemplateUsed(resp, 'admin/images/properties/create.html')
|
||||
|
||||
@test.create_stubs({api.glance: ('image_update_properties',
|
||||
'image_get_property'), })
|
||||
def test_property_update_post(self):
|
||||
image = self.images.first()
|
||||
prop = api.glance.ImageCustomProperty(image.id, 'k1', 'v1')
|
||||
edit_url = reverse('horizon:admin:images:properties:edit',
|
||||
args=[image.id, prop.id])
|
||||
index_url = reverse('horizon:admin:images:properties:index',
|
||||
args=[image.id])
|
||||
api.glance.image_get_property(IsA(http.HttpRequest),
|
||||
image.id, 'k1', False).AndReturn(prop)
|
||||
api.glance.image_update_properties(IsA(http.HttpRequest),
|
||||
image.id, **{'k1': 'v2'})
|
||||
self.mox.ReplayAll()
|
||||
data = {'image_id': image.id,
|
||||
'key': 'k1',
|
||||
'value': 'v2'}
|
||||
resp = self.client.post(edit_url, data)
|
||||
self.assertNoFormErrors(resp)
|
||||
self.assertMessageCount(success=1)
|
||||
self.assertRedirectsNoFollow(resp, index_url)
|
||||
|
||||
@test.create_stubs({api.glance: ('image_get',
|
||||
'image_get_property'), })
|
||||
def test_property_update_get(self):
|
||||
image = self.images.first()
|
||||
prop = api.glance.ImageCustomProperty(image.id, 'k1', 'v1')
|
||||
edit_url = reverse('horizon:admin:images:properties:edit',
|
||||
args=[image.id, prop.id])
|
||||
api.glance.image_get(IsA(http.HttpRequest), image.id).AndReturn(image)
|
||||
api.glance.image_get_property(IsA(http.HttpRequest),
|
||||
image.id, 'k1', False).AndReturn(prop)
|
||||
self.mox.ReplayAll()
|
||||
resp = self.client.get(edit_url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTemplateUsed(resp, 'admin/images/properties/edit.html')
|
@ -0,0 +1,22 @@
|
||||
# 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.conf.urls import patterns # noqa
|
||||
from django.conf.urls import url # noqa
|
||||
|
||||
from openstack_dashboard.dashboards.admin.images.properties import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<key>[^/]+)/edit/$', views.EditView.as_view(), name='edit')
|
||||
)
|
@ -0,0 +1,89 @@
|
||||
# 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.core.urlresolvers import reverse
|
||||
from django.utils import http
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.admin.images.properties \
|
||||
import forms as project_forms
|
||||
from openstack_dashboard.dashboards.admin.images.properties \
|
||||
import tables as project_tables
|
||||
|
||||
|
||||
class PropertyMixin(object):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(PropertyMixin, self).get_context_data(**kwargs)
|
||||
try:
|
||||
context['image'] = api.glance.image_get(self.request,
|
||||
self.kwargs['id'])
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve image details."))
|
||||
if 'key' in self.kwargs:
|
||||
context['encoded_key'] = self.kwargs['key']
|
||||
context['key'] = http.urlunquote(self.kwargs['key'])
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("horizon:admin:images:properties:index",
|
||||
args=(self.kwargs["id"],))
|
||||
|
||||
|
||||
class IndexView(PropertyMixin, tables.DataTableView):
|
||||
table_class = project_tables.PropertiesTable
|
||||
template_name = 'admin/images/properties/index.html'
|
||||
|
||||
def get_data(self):
|
||||
try:
|
||||
image_id = self.kwargs['id']
|
||||
properties_list = api.glance.image_get_properties(self.request,
|
||||
image_id,
|
||||
False)
|
||||
properties_list.sort(key=lambda prop: (prop.key,))
|
||||
except Exception:
|
||||
properties_list = []
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve image custom properties list.'))
|
||||
return properties_list
|
||||
|
||||
|
||||
class CreateView(PropertyMixin, forms.ModalFormView):
|
||||
form_class = project_forms.CreateProperty
|
||||
template_name = 'admin/images/properties/create.html'
|
||||
|
||||
def get_initial(self):
|
||||
return {'image_id': self.kwargs['id']}
|
||||
|
||||
|
||||
class EditView(PropertyMixin, forms.ModalFormView):
|
||||
form_class = project_forms.EditProperty
|
||||
template_name = 'admin/images/properties/edit.html'
|
||||
|
||||
def get_initial(self):
|
||||
image_id = self.kwargs['id']
|
||||
key = http.urlunquote(self.kwargs['key'])
|
||||
try:
|
||||
prop = api.glance.image_get_property(self.request, image_id,
|
||||
key, False)
|
||||
except Exception:
|
||||
prop = None
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve image custom property.'))
|
||||
return {'image_id': image_id,
|
||||
'key': key,
|
||||
'value': prop.value if prop else ''}
|
@ -40,6 +40,13 @@ class AdminEditImage(project_tables.EditImage):
|
||||
return True
|
||||
|
||||
|
||||
class ViewCustomProperties(tables.LinkAction):
|
||||
name = "properties"
|
||||
verbose_name = _("View Custom Properties")
|
||||
url = "horizon:admin:images:properties:index"
|
||||
classes = ("btn-edit",)
|
||||
|
||||
|
||||
class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
@ -59,4 +66,4 @@ class AdminImagesTable(project_tables.ImagesTable):
|
||||
status_columns = ["status"]
|
||||
verbose_name = _("Images")
|
||||
table_actions = (AdminCreateImage, AdminDeleteImage)
|
||||
row_actions = (AdminEditImage, AdminDeleteImage)
|
||||
row_actions = (AdminEditImage, ViewCustomProperties, AdminDeleteImage)
|
||||
|
@ -0,0 +1,28 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}image_custom_property_create_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:admin:images:properties:create' image.id %}{% endblock %}
|
||||
|
||||
|
||||
{% block modal_id %}image_custom_property_create_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Create Image Custom Property" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
<p>{% trans 'Create a new custom property for an image.' %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create" %}" />
|
||||
<a href="{% url 'horizon:admin:images:properties:index' image.id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,28 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}custom_property_edit_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:admin:images:properties:edit' image.id encoded_key %}{% endblock %}
|
||||
|
||||
|
||||
{% block modal_id %}custom_property_edit_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Edit Custom Property Value" %}: {{ key }}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
<p>{% trans 'Update the custom property value for' %} "{{ key }}"</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Save" %}" />
|
||||
<a href="{% url 'horizon:admin:images:properties:index' image.id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Create Image Custom Property" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
<h2>{% trans "Image" %}: {{image.name}} </h2>
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include "admin/images/properties/_create.html" %}
|
||||
{% endblock %}
|
@ -0,0 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Edit Image Custom Property" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
<h2>{% trans "Image" %}: {{image.name}} </h2>
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include "admin/images/properties/_edit.html" %}
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Image Custom Properties" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Image Custom Properties: ")|add:image.name|default:_("Image Custom Properties:") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{{ table.render }}
|
||||
{% endblock %}
|
@ -16,9 +16,12 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf.urls import include # noqa
|
||||
from django.conf.urls import patterns # noqa
|
||||
from django.conf.urls import url # noqa
|
||||
|
||||
from openstack_dashboard.dashboards.admin.images.properties \
|
||||
import urls as properties_urls
|
||||
from openstack_dashboard.dashboards.admin.images import views
|
||||
|
||||
|
||||
@ -28,5 +31,7 @@ urlpatterns = patterns('openstack_dashboard.dashboards.admin.images.views',
|
||||
url(r'^(?P<image_id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(), name='update'),
|
||||
url(r'^(?P<image_id>[^/]+)/detail/$',
|
||||
views.DetailView.as_view(), name='detail')
|
||||
views.DetailView.as_view(), name='detail'),
|
||||
url(r'^(?P<id>[^/]+)/properties/',
|
||||
include(properties_urls, namespace='properties')),
|
||||
)
|
||||
|
@ -218,6 +218,11 @@ IMAGE_CUSTOM_PROPERTY_TITLES = {
|
||||
"image_type": _("Image Type")
|
||||
}
|
||||
|
||||
# The IMAGE_RESERVED_CUSTOM_PROPERTIES setting is used to specify which image
|
||||
# custom properties should not be displayed in the Image Custom Properties
|
||||
# table.
|
||||
IMAGE_RESERVED_CUSTOM_PROPERTIES = []
|
||||
|
||||
# OPENSTACK_ENDPOINT_TYPE specifies the endpoint type to use for the endpoints
|
||||
# in the Keystone service catalog. Use this setting when Horizon is running
|
||||
# external to the OpenStack environment. The default is 'publicURL'.
|
||||
|
Loading…
x
Reference in New Issue
Block a user