Merge "Plugin-based panel group configuration"
This commit is contained in:
commit
d7f33e1fcb
|
@ -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'
|
|
@ -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