diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index 9cbb63595e..460a403204 100755 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -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`` ------------------ diff --git a/openstack_dashboard/dashboards/admin/images/urls.py b/openstack_dashboard/dashboards/admin/images/urls.py index 2a907c9e7a..d19b970669 100644 --- a/openstack_dashboard/dashboards/admin/images/urls.py +++ b/openstack_dashboard/dashboards/admin/images/urls.py @@ -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[^/]+)/update/$', - views.UpdateView.as_view(), name='update'), - url(r'^(?P[^/]+)/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[^/]+)/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[^/]+)/update/$', + views.UpdateView.as_view(), name='update'), + url(r'^(?P[^/]+)/detail/$', + views.DetailView.as_view(), name='detail') + ] diff --git a/openstack_dashboard/dashboards/admin/images/views.py b/openstack_dashboard/dashboards/admin/images/views.py index 35a0dfcd3e..554e7765e0 100644 --- a/openstack_dashboard/dashboards/admin/images/views.py +++ b/openstack_dashboard/dashboards/admin/images/views.py @@ -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 diff --git a/openstack_dashboard/dashboards/project/images/images/urls.py b/openstack_dashboard/dashboards/project/images/images/urls.py index 7e63484b60..8d01d1a1b5 100644 --- a/openstack_dashboard/dashboards/project/images/images/urls.py +++ b/openstack_dashboard/dashboards/project/images/images/urls.py @@ -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[^/]+)/update/$', - views.UpdateView.as_view(), name='update'), - url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), -] +if settings.HORIZON_CONFIG['images_panel'] == 'angular': + urlpatterns = [ + url(r'^(?P[^/]+)/$', imgviews.AngularIndexView.as_view(), + name='detail'), + ] +else: + urlpatterns = [ + url(r'^create/$', views.CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/update/$', + views.UpdateView.as_view(), name='update'), + url(r'^(?P[^/]+)/$', views.DetailView.as_view(), + name='detail'), + ] diff --git a/openstack_dashboard/dashboards/project/images/urls.py b/openstack_dashboard/dashboards/project/images/urls.py index 71ded710e0..fd9e574c99 100644 --- a/openstack_dashboard/dashboards/project/images/urls.py +++ b/openstack_dashboard/dashboards/project/images/urls.py @@ -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')), + ] diff --git a/openstack_dashboard/dashboards/project/images/views.py b/openstack_dashboard/dashboards/project/images/views.py index 621f6d9855..7ed07644e6 100644 --- a/openstack_dashboard/dashboards/project/images/views.py +++ b/openstack_dashboard/dashboards/project/images/views.py @@ -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' diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index b22c8627fb..ca089fab95 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -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, diff --git a/openstack_dashboard/dashboards/project/ngimages/__init__.py b/openstack_dashboard/dashboards/project/ngimages/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openstack_dashboard/dashboards/project/ngimages/panel.py b/openstack_dashboard/dashboards/project/ngimages/panel.py deleted file mode 100644 index 93e1833d7a..0000000000 --- a/openstack_dashboard/dashboards/project/ngimages/panel.py +++ /dev/null @@ -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',) diff --git a/openstack_dashboard/dashboards/project/ngimages/urls.py b/openstack_dashboard/dashboards/project/ngimages/urls.py deleted file mode 100644 index 82ed6966f8..0000000000 --- a/openstack_dashboard/dashboards/project/ngimages/urls.py +++ /dev/null @@ -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'), -] diff --git a/openstack_dashboard/dashboards/project/ngimages/views.py b/openstack_dashboard/dashboards/project/ngimages/views.py deleted file mode 100644 index e072aeab92..0000000000 --- a/openstack_dashboard/dashboards/project/ngimages/views.py +++ /dev/null @@ -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' diff --git a/openstack_dashboard/enabled/_1050_project_images_panel.py b/openstack_dashboard/enabled/_1050_project_images_panel.py index 503357788b..88e1a825d6 100644 --- a/openstack_dashboard/enabled/_1050_project_images_panel.py +++ b/openstack_dashboard/enabled/_1050_project_images_panel.py @@ -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. diff --git a/openstack_dashboard/enabled/_1051_project_ng_images_panel.py b/openstack_dashboard/enabled/_1051_project_ng_images_panel.py deleted file mode 100644 index a92ec15b08..0000000000 --- a/openstack_dashboard/enabled/_1051_project_ng_images_panel.py +++ /dev/null @@ -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' diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py index 682e5f8729..64dbab27df 100644 --- a/openstack_dashboard/settings.py +++ b/openstack_dashboard/settings.py @@ -79,6 +79,7 @@ HORIZON_CONFIG = { 'js_spec_files': [], 'external_templates': [], 'plugins': [], + 'images_panel': 'legacy', 'integration_tests_support': INTEGRATION_TESTS_SUPPORT } diff --git a/openstack_dashboard/static/app/core/images/images.module.js b/openstack_dashboard/static/app/core/images/images.module.js index 113809d8b4..2aea051880 100644 --- a/openstack_dashboard/static/app/core/images/images.module.js +++ b/openstack_dashboard/static/app/core/images/images.module.js @@ -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; + } } })(); diff --git a/openstack_dashboard/static/app/core/images/details/drawer.controller.js b/openstack_dashboard/static/app/core/images/summary.controller.js similarity index 100% rename from openstack_dashboard/static/app/core/images/details/drawer.controller.js rename to openstack_dashboard/static/app/core/images/summary.controller.js diff --git a/openstack_dashboard/static/app/core/images/details/drawer.controller.spec.js b/openstack_dashboard/static/app/core/images/summary.controller.spec.js similarity index 100% rename from openstack_dashboard/static/app/core/images/details/drawer.controller.spec.js rename to openstack_dashboard/static/app/core/images/summary.controller.spec.js diff --git a/openstack_dashboard/test/integration_tests/config.py b/openstack_dashboard/test/integration_tests/config.py index bd95cde291..2bcb6abb4d 100644 --- a/openstack_dashboard/test/integration_tests/config.py +++ b/openstack_dashboard/test/integration_tests/config.py @@ -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', diff --git a/openstack_dashboard/test/integration_tests/decorators.py b/openstack_dashboard/test/integration_tests/decorators.py index 14c1a1664a..a787aebf82 100644 --- a/openstack_dashboard/test/integration_tests/decorators.py +++ b/openstack_dashboard/test/integration_tests/decorators.py @@ -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: diff --git a/openstack_dashboard/test/integration_tests/horizon.conf b/openstack_dashboard/test/integration_tests/horizon.conf index 31ccc705f4..b225dff9fb 100644 --- a/openstack_dashboard/test/integration_tests/horizon.conf +++ b/openstack_dashboard/test/integration_tests/horizon.conf @@ -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, diff --git a/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py b/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py index 42117d44da..2bb7b83844 100644 --- a/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py +++ b/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py @@ -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) diff --git a/openstack_dashboard/test/integration_tests/tests/test_images.py b/openstack_dashboard/test/integration_tests/tests/test_images.py index ec264d4f93..9e40ca25f3 100644 --- a/openstack_dashboard/test/integration_tests/tests/test_images.py +++ b/openstack_dashboard/test/integration_tests/tests/test_images.py @@ -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") diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index fdd0a4afc0..cecabc2571 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -96,6 +96,7 @@ HORIZON_CONFIG = { 'unauthorized': exceptions.UNAUTHORIZED}, 'angular_modules': [], 'js_files': [], + 'images_panel': 'legacy', } # Load the pluggable dashboard settings diff --git a/releasenotes/notes/image-panel-switch-38e9d3716451f9e3.yaml b/releasenotes/notes/image-panel-switch-38e9d3716451f9e3.yaml new file mode 100644 index 0000000000..a52a50e082 --- /dev/null +++ b/releasenotes/notes/image-panel-switch-38e9d3716451f9e3.yaml @@ -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.