Make 'switch' between legacy and Angular Images

This patch follows on the example that the Containers set, providing
a 'switch' in the panel-enablement file that currently defaults to
'legacy' (Python-based Images panel) and allows for 'angular' (Angular-
based Images panel).

To be clear, this does NOT enable Angular Images.  It's just setting the
stage to do so at some point, or to allow deployers/devs to easily switch
between the two.

A switch both for HORIZON_CONFIG and for integration tests is necessary
due to the way integration tests operate.

Co-Authored-By: Timur Sufiev <tsufiev@mirantis.com>
Change-Id: I12cd33552218ed1082d2d9a2ae8982639a217a6a
Partially-Implements: blueprint angularize-images-table
This commit is contained in:
Matt Borland 2016-07-07 10:32:43 -06:00
parent de1a23267a
commit 20bc6e1516
24 changed files with 199 additions and 147 deletions

View File

@ -180,6 +180,18 @@ A dictionary containing classes of exceptions which Horizon's centralized
exception handling should be aware of. Based on these exception categories,
Horizon will handle the exception and display a message to the user.
``images_panel``
-----------
.. versionadded:: 10.0.0(Newton)
Default: ``legacy``
There are currently two panel types that may be specified: ``legacy`` and
``angular``. ``legacy`` will display the Python-based (server-side) Images
panel and ``angular`` will display the Angular-based (client-side) Images
panel.
``modal_backdrop``
------------------

View File

@ -16,16 +16,24 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import settings
from django.conf.urls import url
from openstack_dashboard.dashboards.admin.images import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<image_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),
url(r'^(?P<image_id>[^/]+)/detail/$',
views.DetailView.as_view(), name='detail')
]
if settings.HORIZON_CONFIG['images_panel'] == 'angular':
# New angular images
urlpatterns = [
url(r'^$', views.AngularIndexView.as_view(), name='index'),
url(r'^(?P<image_id>[^/]+)/detail/$',
views.AngularIndexView.as_view(), name='detail'),
]
else:
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<image_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),
url(r'^(?P<image_id>[^/]+)/detail/$',
views.DetailView.as_view(), name='detail')
]

View File

@ -23,6 +23,7 @@ from oslo_utils import units
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.views import generic
from horizon import exceptions
from horizon import messages
@ -40,6 +41,10 @@ from openstack_dashboard.dashboards.admin.images \
LOG = logging.getLogger(__name__)
class AngularIndexView(generic.TemplateView):
template_name = 'angular.html'
class IndexView(tables.DataTableView):
DEFAULT_FILTERS = {'is_public': None}
table_class = project_tables.AdminImagesTable

View File

@ -16,14 +16,23 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import settings
from django.conf.urls import url
from openstack_dashboard.dashboards.project.images.images import views
from openstack_dashboard.dashboards.project.images import views as imgviews
urlpatterns = [
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<image_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),
url(r'^(?P<image_id>[^/]+)/$', views.DetailView.as_view(), name='detail'),
]
if settings.HORIZON_CONFIG['images_panel'] == 'angular':
urlpatterns = [
url(r'^(?P<image_id>[^/]+)/$', imgviews.AngularIndexView.as_view(),
name='detail'),
]
else:
urlpatterns = [
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<image_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),
url(r'^(?P<image_id>[^/]+)/$', views.DetailView.as_view(),
name='detail'),
]

View File

@ -16,6 +16,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import settings
from django.conf.urls import include
from django.conf.urls import url
@ -26,8 +27,16 @@ from openstack_dashboard.dashboards.project.images.snapshots \
from openstack_dashboard.dashboards.project.images import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'', include(image_urls, namespace='images')),
url(r'', include(snapshot_urls, namespace='snapshots')),
]
if settings.HORIZON_CONFIG['images_panel'] == 'angular':
# New angular images
urlpatterns = [
url(r'^$', views.AngularIndexView.as_view(), name='index'),
url(r'', include(image_urls, namespace='images')),
url(r'', include(snapshot_urls, namespace='snapshots')),
]
else:
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'', include(image_urls, namespace='images')),
url(r'', include(snapshot_urls, namespace='snapshots')),
]

View File

@ -22,6 +22,7 @@ Views for managing Images and Snapshots.
"""
from django.utils.translation import ugettext_lazy as _
from django.views import generic
from horizon import exceptions
from horizon import messages
@ -34,6 +35,10 @@ from openstack_dashboard.dashboards.project.images.images \
import tables as images_tables
class AngularIndexView(generic.TemplateView):
template_name = 'angular.html'
class IndexView(tables.DataTableView):
table_class = images_tables.ImagesTable
template_name = 'project/images/index.html'

View File

@ -1276,11 +1276,6 @@ class InstanceTests(helpers.TestCase):
server.id,
"snapshot1").AndReturn(self.snapshots.first())
api.glance.image_list_detailed(IsA(http.HttpRequest),
marker=None,
paginate=True) \
.AndReturn([[], False, False])
self.mox.ReplayAll()
formData = {'instance_id': server.id,

View File

@ -1,23 +0,0 @@
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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 _
import horizon
class NGImages(horizon.Panel):
name = _("Images")
slug = 'ngimages'
permissions = ('openstack.services.image',)

View File

@ -1,22 +0,0 @@
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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 url
from openstack_dashboard.dashboards.project.ngimages import views
urlpatterns = [
url('', views.IndexView.as_view(), name='index'),
]

View File

@ -1,19 +0,0 @@
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.views import generic
class IndexView(generic.TemplateView):
template_name = 'angular.html'

View File

@ -1,3 +1,17 @@
# (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# 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.
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'images'
# The slug of the dashboard the PANEL associated with. Required.

View File

@ -1,30 +0,0 @@
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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.
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
# If you want the panel to show up without a panel group,
# use the panel group "default".
PANEL_GROUP = 'compute'
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'ngimages'
# If set to True, this settings file will not be added to the settings.
DISABLED = True
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.project.ngimages.panel.NGImages'

View File

@ -79,6 +79,7 @@ HORIZON_CONFIG = {
'js_spec_files': [],
'external_templates': [],
'plugins': [],
'images_panel': 'legacy',
'integration_tests_support': INTEGRATION_TESTS_SUPPORT
}

View File

@ -295,9 +295,25 @@
var path = $windowProvider.$get().STATIC_URL + 'app/core/images/';
$provide.constant('horizon.app.core.images.basePath', path);
$routeProvider.when('/project/ngimages/', {
$routeProvider.when('/project/images/:id/', {
redirectTo: goToAngularDetails
});
$routeProvider.when('/admin/images/:id/detail/', {
redirectTo: goToAngularDetails
});
$routeProvider.when('/project/images/', {
templateUrl: path + 'panel.html'
});
$routeProvider.when('/admin/images/', {
templateUrl: path + 'panel.html'
});
function goToAngularDetails(params) {
return 'project/ngdetails/OS::Glance::Image/' + params.id;
}
}
})();

View File

@ -57,6 +57,9 @@ IdentityGroup = [
]
ImageGroup = [
cfg.StrOpt('panel_type',
default='legacy',
help='type/version of images panel'),
cfg.StrOpt('http_image',
default='http://download.cirros-cloud.net/0.3.1/'
'cirros-0.3.1-x86_64-uec.tar.gz',

View File

@ -58,6 +58,18 @@ NOT_TEST_OBJECT_ERROR_MSG = "Decorator can be applied only on test" \
" classes and test methods."
def _get_skip_method(obj):
"""Make sure that we can decorate both methods and classes."""
if inspect.isclass(obj):
if not _is_test_cls(obj):
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
return _mark_class_skipped
else:
if not _is_test_method_name(obj.__name__):
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
return _mark_method_skipped
def services_required(*req_services):
"""Decorator for marking test's service requirements,
if requirements are not met in the configuration file
@ -85,15 +97,7 @@ def services_required(*req_services):
.
"""
def actual_decoration(obj):
# make sure that we can decorate method and classes as well
if inspect.isclass(obj):
if not _is_test_cls(obj):
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
skip_method = _mark_class_skipped
else:
if not _is_test_method_name(obj.__name__):
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
skip_method = _mark_method_skipped
skip_method = _get_skip_method(obj)
# get available services from configuration
avail_services = config.get_config().service_available
for req_service in req_services:
@ -105,6 +109,32 @@ def services_required(*req_services):
return actual_decoration
def _parse_compound_config_option_value(option_name):
"""Parses the value of a given config option where option's section name is
separated from option name by '.'.
"""
name_parts = option_name.split('.')
name_parts.reverse()
option = config.get_config()
while name_parts:
option = getattr(option, name_parts.pop())
return option
def config_option_required(option_key, required_value, message=None):
if message is None:
message = "%s option equal to '%s' is required for this test to work" \
" properly." % (option_key, required_value)
def actual_decoration(obj):
skip_method = _get_skip_method(obj)
option_value = _parse_compound_config_option_value(option_key)
if option_value != required_value:
obj = skip_method(obj, message)
return obj
return actual_decoration
def skip_because(**kwargs):
"""Decorator for skipping tests hitting known bugs
@ -120,14 +150,7 @@ def skip_because(**kwargs):
.
"""
def actual_decoration(obj):
if inspect.isclass(obj):
if not _is_test_cls(obj):
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
skip_method = _mark_class_skipped
else:
if not _is_test_method_name(obj.__name__):
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
skip_method = _mark_method_skipped
skip_method = _get_skip_method(obj)
bugs = kwargs.get("bugs")
if bugs and isinstance(bugs, collections.Iterable):
for bug in bugs:

View File

@ -35,6 +35,7 @@ maximize_browser=yes
[image]
# http accessible image (string value)
panel_type=legacy
http_image=http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-uec.tar.gz
images_list=cirros-0.3.4-x86_64-uec,
cirros-0.3.4-x86_64-uec-kernel,

View File

@ -9,6 +9,8 @@
# 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 selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.regions import forms
from openstack_dashboard.test.integration_tests.regions import tables
@ -250,3 +252,12 @@ class ImagesPage(basepage.BaseNavigationPage):
launch_instance.count.value = instance_count
launch_instance.submit()
return InstancesPage(self.driver, self.conf)
class ImagesPageNG(ImagesPage):
_resource_page_header_locator = (by.By.CSS_SELECTOR,
'hz-resource-panel hz-page-header h1')
@property
def header(self):
return self._get_element(*self._resource_page_header_locator)

View File

@ -15,14 +15,39 @@ from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestImagesBasic(helpers.TestCase):
"""Login as demo user"""
IMAGE_NAME = helpers.gen_random_resource_name("image")
@decorators.config_option_required('image.panel_type', 'legacy',
message="Angular Panels not tested")
class TestImagesLegacy(helpers.TestCase):
@property
def images_page(self):
return self.home_pg.go_to_compute_imagespage()
@decorators.config_option_required('image.panel_type', 'angular',
message="Legacy Panels not tested")
class TestImagesAngular(helpers.TestCase):
@property
def images_page(self):
# FIXME(tsufiev): had to return angularized version of Images Page
# object with the horrendous hack below because it's not so easy to
# wire into the Navigation machinery and tell it to return an '*NG'
# version of ImagesPage class if one adds '_ng' suffix to
# 'go_to_compute_imagespage()' method. Yet that's how it should work
# (or rewrite Navigation module completely).
from openstack_dashboard.test.integration_tests.pages.project.\
compute.imagespage import ImagesPageNG
self.home_pg.go_to_compute_imagespage()
return ImagesPageNG(self.driver, self.CONFIG)
def test_basic_image_browse(self):
images_page = self.images_page
self.assertEqual(images_page.header.text, 'Images')
class TestImagesBasic(TestImagesLegacy):
"""Login as demo user"""
IMAGE_NAME = helpers.gen_random_resource_name("image")
def image_create(self, local_file=None):
images_page = self.images_page
if local_file:
@ -227,14 +252,10 @@ class TestImagesBasic(helpers.TestCase):
self.image_delete(new_image_name)
class TestImagesAdvanced(helpers.TestCase):
class TestImagesAdvanced(TestImagesLegacy):
"""Login as demo user"""
IMAGE_NAME = helpers.gen_random_resource_name("image")
@property
def images_page(self):
return self.home_pg.go_to_compute_imagespage()
def test_create_volume_from_image(self):
"""This test case checks create volume from image functionality:
Steps:
@ -293,7 +314,7 @@ class TestImagesAdvanced(helpers.TestCase):
self.assertTrue(instances_page.is_instance_deleted(target_instance))
class TestImagesAdmin(helpers.AdminTestCase, TestImagesBasic):
class TestImagesAdmin(helpers.AdminTestCase, TestImagesLegacy):
"""Login as admin user"""
IMAGE_NAME = helpers.gen_random_resource_name("image")

View File

@ -96,6 +96,7 @@ HORIZON_CONFIG = {
'unauthorized': exceptions.UNAUTHORIZED},
'angular_modules': [],
'js_files': [],
'images_panel': 'legacy',
}
# Load the pluggable dashboard settings

View File

@ -0,0 +1,12 @@
---
prelude: >
The Image panel now may be configured to use
either the legacy or Angular code.
features:
- HORIZON_CONFIG now allows for a key 'images_panel' to be
specified as 'legacy' or 'angular' indicating the type of
panel to use.
- Integration tests for Image features may also be toggled
in openstack_dashboard/test/integration_tests/horizon.conf
using the 'panel_type' feature, either set to 'legacy' or
'angular' to match the enabled panel type.