Plugin-based panel group configuration
Panels can be added through plugin configuration, but it is broken if the user can only use existing panel groups. This adds the capability to create an empty panel group that can be used by panel configuration to add itself to the group. Change-Id: I28d6072fc05b3a4ef5a41cd68337a7864c560525 Closes-Bug: #1287981
This commit is contained in:
parent
79088c8eea
commit
e1080cbe05
@ -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'
|
||||
|
115
horizon/base.py
115
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):
|
||||
|
@ -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'
|
@ -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'
|
@ -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()
|
||||
|
@ -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'
|
@ -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'
|
48
openstack_dashboard/test/test_plugins/panel_group_tests.py
Normal file
48
openstack_dashboard/test/test_plugins/panel_group_tests.py
Normal file
@ -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()])
|
@ -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,
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user