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:
lin-hua-cheng 2014-03-15 00:50:18 -07:00 committed by Akihiro Motoki
parent 79088c8eea
commit e1080cbe05
11 changed files with 277 additions and 76 deletions

View File

@ -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'

View File

@ -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):

View File

@ -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'

View File

@ -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'

View File

@ -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()

View File

@ -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'

View File

@ -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'

View 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()])

View File

@ -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,

View File

@ -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)