diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index b7bdd3e35b..43e5eb2444 100644 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -539,7 +539,7 @@ create a file ``openstack_dashboard/local/enabled/_50_tuskar.py`` with:: } Pluggable Settings for Panels -================================= +============================= Panels customization can be made by providing a custom python module that contains python code to add or remove panel to/from the dashboard. This @@ -625,3 +625,59 @@ following content:: PANEL_DASHBOARD = 'admin' PANEL_GROUP = 'admin' DEFAULT_PANEL = 'instances' + +Pluggable Settings for Panel Groups +=================================== + +To organize the panels created from the pluggable settings, there is also +a way to create panel group though configuration file. This creates an empty +panel group to act as placeholder for the panels that can be created later. + +The default location for the panel group configuration files is +``openstack_dashboard/enabled``, with another directory, +``openstack_dashboard/local/enabled`` for local overrides. Both sets of files +will be loaded, but the settings in ``openstack_dashboard/local/enabled`` will +overwrite the default ones. The settings are applied in alphabetical order of +the filenames. If the same panel has configuration files in ``enabled`` and +``local/enabled``, the local name will be used. Note, that since names of +python modules can't start with a digit, the files are usually named with a +leading underscore and a number, so that you can control their order easily. + +When writing configuration files to create panels and panels group, make sure +that the panel group configuration file is loaded first because the panel +configuration might be referencing it. This can be achieved by providing a file +name that will go before the panel configuration file when the files are sorted +alphabetically. + +The files contain following keys: + +``PANEL_GROUP`` +------------- + +The name of the panel group to be added to ``HORIZON_CONFIG``. Required. + +``PANEL_GROUP_NAME`` +------------- + +The display name of the PANEL_GROUP. Required. + +``PANEL_GROUP_DASHBOARD`` +------------- + +The name of the dashboard the ``PANEL_GROUP`` associated with. Required. + +``DISABLED`` +------------ + +If set to ``True``, this panel configuration will be skipped. + +Examples +-------- + +To add a new panel group to the Admin dashboard, create a file +``openstack_dashboard/local/enabled/_90_admin_add_panel_group.py`` with the +following content:: + + PANEL_GROUP = 'plugin_panel_group' + PANEL_GROUP_NAME = 'Plugin Panel Group' + PANEL_GROUP_DASHBOARD = 'admin' diff --git a/horizon/base.py b/horizon/base.py index 0248e71ffc..39593d93f0 100644 --- a/horizon/base.py +++ b/horizon/base.py @@ -798,53 +798,96 @@ class Site(Registry, HorizonComponent): and make changes to the dashboard accordingly. It supports adding, removing and setting default panels on the - dashboard. + dashboard. It also support registering a panel group. """ panel_customization = self._conf.get("panel_customization", []) for config in panel_customization: + if config.get('PANEL'): + self._process_panel_configuration(config) + elif config.get('PANEL_GROUP'): + self._process_panel_group_configuration(config) + else: + LOG.warning("Skipping %s because it doesn't have PANEL or " + "PANEL_GROUP defined.", config.__name__) + + def _process_panel_configuration(self, config): + """Add, remove and set default panels on the dashboard.""" + try: dashboard = config.get('PANEL_DASHBOARD') if not dashboard: LOG.warning("Skipping %s because it doesn't have " "PANEL_DASHBOARD defined.", config.__name__) - continue - try: - panel_slug = config.get('PANEL') - dashboard_cls = self.get_dashboard(dashboard) - panel_group = config.get('PANEL_GROUP') - default_panel = config.get('DEFAULT_PANEL') + return + panel_slug = config.get('PANEL') + dashboard_cls = self.get_dashboard(dashboard) + panel_group = config.get('PANEL_GROUP') + default_panel = config.get('DEFAULT_PANEL') - # Set the default panel - if default_panel: - dashboard_cls.default_panel = default_panel + # Set the default panel + if default_panel: + dashboard_cls.default_panel = default_panel - # Remove the panel - if config.get('REMOVE_PANEL', False): - for panel in dashboard_cls.get_panels(): - if panel_slug == panel.slug: - dashboard_cls.unregister(panel.__class__) - elif config.get('ADD_PANEL', None): - # Add the panel to the dashboard - panel_path = config['ADD_PANEL'] - mod_path, panel_cls = panel_path.rsplit(".", 1) - try: - mod = import_module(mod_path) - except ImportError: - LOG.warning("Could not load panel: %s", mod_path) - continue + # Remove the panel + if config.get('REMOVE_PANEL', False): + for panel in dashboard_cls.get_panels(): + if panel_slug == panel.slug: + dashboard_cls.unregister(panel.__class__) + elif config.get('ADD_PANEL', None): + # Add the panel to the dashboard + panel_path = config['ADD_PANEL'] + mod_path, panel_cls = panel_path.rsplit(".", 1) + try: + mod = import_module(mod_path) + except ImportError: + LOG.warning("Could not load panel: %s", mod_path) + return - panel = getattr(mod, panel_cls) - dashboard_cls.register(panel) - if panel_group: - dashboard_cls.get_panel_group(panel_group).\ - panels.append(panel.slug) - else: - panels = list(dashboard_cls.panels) - panels.append(panel) - dashboard_cls.panels = tuple(panels) - except Exception as e: - LOG.warning('Could not process panel %(panel)s: %(exc)s', - {'panel': panel_slug, 'exc': e}) + panel = getattr(mod, panel_cls) + dashboard_cls.register(panel) + if panel_group: + dashboard_cls.get_panel_group(panel_group).\ + panels.append(panel.slug) + else: + panels = list(dashboard_cls.panels) + panels.append(panel) + dashboard_cls.panels = tuple(panels) + except Exception as e: + LOG.warning('Could not process panel %(panel)s: %(exc)s', + {'panel': panel_slug, 'exc': e}) + + def _process_panel_group_configuration(self, config): + """Adds a panel group to the dashboard.""" + panel_group_slug = config.get('PANEL_GROUP') + try: + dashboard = config.get('PANEL_GROUP_DASHBOARD') + if not dashboard: + LOG.warning("Skipping %s because it doesn't have " + "PANEL_GROUP_DASHBOARD defined.", config.__name__) + return + dashboard_cls = self.get_dashboard(dashboard) + + panel_group_name = config.get('PANEL_GROUP_NAME') + if not panel_group_name: + LOG.warning("Skipping %s because it doesn't have " + "PANEL_GROUP_NAME defined.", config.__name__) + return + # Create the panel group class + panel_group = type(panel_group_slug, + (PanelGroup, ), + {'slug': panel_group_slug, + 'name': panel_group_name},) + # Add the panel group to dashboard + panels = list(dashboard_cls.panels) + panels.append(panel_group) + dashboard_cls.panels = tuple(panels) + # Trigger the autodiscovery to completely load the new panel group + dashboard_cls._autodiscover_complete = False + dashboard_cls._autodiscover() + except Exception as e: + LOG.warning('Could not process panel group %(panel_group)s: ' + '%(exc)s', + {'panel_group': panel_group_slug, 'exc': e}) class HorizonSite(Site): diff --git a/openstack_dashboard/enabled/_80_admin_add_panel_group.py.example b/openstack_dashboard/enabled/_80_admin_add_panel_group.py.example new file mode 100644 index 0000000000..b0fd2d1b75 --- /dev/null +++ b/openstack_dashboard/enabled/_80_admin_add_panel_group.py.example @@ -0,0 +1,6 @@ +# The name of the panel group to be added to HORIZON_CONFIG. Required. +PANEL_GROUP = 'plugin_panel_group' +# The display name of the PANEL_GROUP. Required. +PANEL_GROUP_NAME = 'Plugin Panel Group' +# The name of the dashboard the PANEL_GROUP associated with. Required. +PANEL_GROUP_DASHBOARD = 'admin' diff --git a/openstack_dashboard/enabled/_90_admin_add_panel_to_group.py.example b/openstack_dashboard/enabled/_90_admin_add_panel_to_group.py.example new file mode 100644 index 0000000000..e9394611c2 --- /dev/null +++ b/openstack_dashboard/enabled/_90_admin_add_panel_to_group.py.example @@ -0,0 +1,10 @@ +# The name of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'plugin_panel' +# The name of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The name of the panel group the PANEL is associated with. +PANEL_GROUP = 'plugin_panel_group' + +# Python panel class of the PANEL to be added. +ADD_PANEL = \ + 'openstack_dashboard.test.test_panels.plugin_panel.panel.PluginPanel' diff --git a/openstack_dashboard/test/helpers.py b/openstack_dashboard/test/helpers.py index af7077b6a0..bd8ee4e6a0 100644 --- a/openstack_dashboard/test/helpers.py +++ b/openstack_dashboard/test/helpers.py @@ -25,6 +25,7 @@ from django.conf import settings from django.contrib.auth.middleware import AuthenticationMiddleware # noqa from django.contrib.messages.storage import default_storage # noqa from django.core.handlers import wsgi +from django.core import urlresolvers from django import http from django.test.client import RequestFactory # noqa from django.utils.importlib import import_module # noqa @@ -47,6 +48,8 @@ import mox from openstack_auth import user from openstack_auth import utils +from horizon import base +from horizon import conf from horizon import middleware from horizon.test import helpers as horizon_helpers @@ -415,3 +418,54 @@ def my_custom_sort(flavor): 'm1.massive': 2, } return sort_order[flavor.name] + + +class PluginTestCase(TestCase): + """The ``PluginTestCase`` class is for use with tests which deal with the + pluggable dashboard and panel configuration, it takes care of backing up + and restoring the Horizon configuration. + """ + def setUp(self): + super(PluginTestCase, self).setUp() + self.old_horizon_config = conf.HORIZON_CONFIG + conf.HORIZON_CONFIG = conf.LazySettings() + base.Horizon._urls() + # Trigger discovery, registration, and URLconf generation if it + # hasn't happened yet. + self.client.get("/") + # Store our original dashboards + self._discovered_dashboards = base.Horizon._registry.keys() + # Gather up and store our original panels for each dashboard + self._discovered_panels = {} + for dash in self._discovered_dashboards: + panels = base.Horizon._registry[dash]._registry.keys() + self._discovered_panels[dash] = panels + + def tearDown(self): + super(PluginTestCase, self).tearDown() + conf.HORIZON_CONFIG = self.old_horizon_config + # Destroy our singleton and re-create it. + base.HorizonSite._instance = None + del base.Horizon + base.Horizon = base.HorizonSite() + # Reload the convenience references to Horizon stored in __init__ + reload(import_module("horizon")) + # Re-register our original dashboards and panels. + # This is necessary because autodiscovery only works on the first + # import, and calling reload introduces innumerable additional + # problems. Manual re-registration is the only good way for testing. + for dash in self._discovered_dashboards: + base.Horizon.register(dash) + for panel in self._discovered_panels[dash]: + dash.register(panel) + self._reload_urls() + + def _reload_urls(self): + """Clears out the URL caches, reloads the root urls module, and + re-triggers the autodiscovery mechanism for Horizon. Allows URLs + to be re-calculated after registering new dashboards. Useful + only for testing and should never be used on a live site. + """ + urlresolvers.clear_url_caches() + reload(import_module(settings.ROOT_URLCONF)) + base.Horizon._urls() diff --git a/openstack_dashboard/test/test_plugins/panel_group_config/_10_admin_add_panel_group.py b/openstack_dashboard/test/test_plugins/panel_group_config/_10_admin_add_panel_group.py new file mode 100644 index 0000000000..b0fd2d1b75 --- /dev/null +++ b/openstack_dashboard/test/test_plugins/panel_group_config/_10_admin_add_panel_group.py @@ -0,0 +1,6 @@ +# The name of the panel group to be added to HORIZON_CONFIG. Required. +PANEL_GROUP = 'plugin_panel_group' +# The display name of the PANEL_GROUP. Required. +PANEL_GROUP_NAME = 'Plugin Panel Group' +# The name of the dashboard the PANEL_GROUP associated with. Required. +PANEL_GROUP_DASHBOARD = 'admin' diff --git a/openstack_dashboard/test/test_plugins/panel_group_config/_20_admin_add_panel_to_group.py b/openstack_dashboard/test/test_plugins/panel_group_config/_20_admin_add_panel_to_group.py new file mode 100644 index 0000000000..e9394611c2 --- /dev/null +++ b/openstack_dashboard/test/test_plugins/panel_group_config/_20_admin_add_panel_to_group.py @@ -0,0 +1,10 @@ +# The name of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'plugin_panel' +# The name of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The name of the panel group the PANEL is associated with. +PANEL_GROUP = 'plugin_panel_group' + +# Python panel class of the PANEL to be added. +ADD_PANEL = \ + 'openstack_dashboard.test.test_panels.plugin_panel.panel.PluginPanel' diff --git a/openstack_dashboard/test/test_plugins/panel_group_config/__init__.py b/openstack_dashboard/test/test_plugins/panel_group_config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/test/test_plugins/panel_group_tests.py b/openstack_dashboard/test/test_plugins/panel_group_tests.py new file mode 100644 index 0000000000..b7180d92c3 --- /dev/null +++ b/openstack_dashboard/test/test_plugins/panel_group_tests.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 copy + +from django.conf import settings +from django.test.utils import override_settings + +import horizon + +from openstack_dashboard.test import helpers as test +from openstack_dashboard.test.test_panels.plugin_panel \ + import panel as plugin_panel +import openstack_dashboard.test.test_plugins.panel_group_config +from openstack_dashboard.utils import settings as util_settings + + +PANEL_GROUP_SLUG = 'plugin_panel_group' +HORIZON_CONFIG = copy.deepcopy(settings.HORIZON_CONFIG) +INSTALLED_APPS = list(settings.INSTALLED_APPS) + +util_settings.update_dashboards([ + openstack_dashboard.test.test_plugins.panel_group_config, +], HORIZON_CONFIG, INSTALLED_APPS) + + +@override_settings(HORIZON_CONFIG=HORIZON_CONFIG, + INSTALLED_APPS=INSTALLED_APPS) +class PanelGroupPluginTests(test.PluginTestCase): + def test_add_panel_group(self): + dashboard = horizon.get_dashboard("admin") + self.assertIsNotNone(dashboard.get_panel_group(PANEL_GROUP_SLUG)) + + def test_add_panel(self): + dashboard = horizon.get_dashboard("admin") + self.assertIn(plugin_panel.PluginPanel, + [p.__class__ for p in dashboard.get_panels()]) diff --git a/openstack_dashboard/test/test_plugins/panel_tests.py b/openstack_dashboard/test/test_plugins/panel_tests.py index 525485464c..112395162b 100644 --- a/openstack_dashboard/test/test_plugins/panel_tests.py +++ b/openstack_dashboard/test/test_plugins/panel_tests.py @@ -15,13 +15,9 @@ import copy from django.conf import settings -from django.core import urlresolvers from django.test.utils import override_settings -from django.utils.importlib import import_module # noqa import horizon -from horizon import base -from horizon import conf from openstack_dashboard.dashboards.admin.info import panel as info_panel from openstack_dashboard.test import helpers as test @@ -31,7 +27,7 @@ import openstack_dashboard.test.test_plugins.panel_config from openstack_dashboard.utils import settings as util_settings -HORIZON_CONFIG = copy.copy(settings.HORIZON_CONFIG) +HORIZON_CONFIG = copy.deepcopy(settings.HORIZON_CONFIG) INSTALLED_APPS = list(settings.INSTALLED_APPS) util_settings.update_dashboards([ @@ -41,36 +37,7 @@ util_settings.update_dashboards([ @override_settings(HORIZON_CONFIG=HORIZON_CONFIG, INSTALLED_APPS=INSTALLED_APPS) -class PanelPluginTests(test.TestCase): - - def setUp(self): - super(PanelPluginTests, self).setUp() - self.old_horizon_config = conf.HORIZON_CONFIG - conf.HORIZON_CONFIG = conf.LazySettings() - base.Horizon._urls() - # Trigger discovery, registration, and URLconf generation if it - # hasn't happened yet. - self.client.get("/") - - def tearDown(self): - super(PanelPluginTests, self).tearDown() - conf.HORIZON_CONFIG = self.old_horizon_config - # Destroy our singleton and re-create it. - base.HorizonSite._instance = None - del base.Horizon - base.Horizon = base.HorizonSite() - self._reload_urls() - - def _reload_urls(self): - """Clears out the URL caches, reloads the root urls module, and - re-triggers the autodiscovery mechanism for Horizon. Allows URLs - to be re-calculated after registering new dashboards. Useful - only for testing and should never be used on a live site. - """ - urlresolvers.clear_url_caches() - reload(import_module(settings.ROOT_URLCONF)) - base.Horizon._urls() - +class PanelPluginTests(test.PluginTestCase): def test_add_panel(self): dashboard = horizon.get_dashboard("admin") self.assertIn(plugin_panel.PluginPanel, diff --git a/openstack_dashboard/utils/settings.py b/openstack_dashboard/utils/settings.py index 99293865ac..c80bdb6fdf 100644 --- a/openstack_dashboard/utils/settings.py +++ b/openstack_dashboard/utils/settings.py @@ -44,12 +44,13 @@ def import_dashboard_config(modules): if hasattr(submodule, 'DASHBOARD'): dashboard = submodule.DASHBOARD config[dashboard].update(submodule.__dict__) - elif hasattr(submodule, 'PANEL'): + elif (hasattr(submodule, 'PANEL') + or hasattr(submodule, 'PANEL_GROUP')): config[submodule.__name__] = submodule.__dict__ - #_update_panels(config, submodule) else: logging.warning("Skipping %s because it doesn't have DASHBOARD" - " or PANEL defined.", submodule.__name__) + ", PANEL or PANEL_GROUP defined.", + submodule.__name__) return sorted(config.iteritems(), key=lambda c: c[1]['__name__'].rsplit('.', 1)) @@ -98,7 +99,7 @@ def update_dashboards(modules, horizon_config, installed_apps): apps.extend(config.get('ADD_INSTALLED_APPS', [])) if config.get('DEFAULT', False): horizon_config['default_dashboard'] = dashboard - elif config.get('PANEL'): + elif config.get('PANEL') or config.get('PANEL_GROUP'): panel_customization.append(config) horizon_config['panel_customization'] = panel_customization horizon_config['dashboards'] = tuple(dashboards)