Merge "Adds PanelGroup class and site customization hook."
This commit is contained in:
commit
6982e373f9
@ -40,3 +40,6 @@ Panel
|
|||||||
|
|
||||||
.. autoclass:: Panel
|
.. autoclass:: Panel
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: PanelGroup
|
||||||
|
:members:
|
||||||
|
@ -28,6 +28,28 @@ To override the OpenStack Logo image, replace the image at the directory path
|
|||||||
|
|
||||||
The dimensions should be ``width: 108px, height: 121px``.
|
The dimensions should be ``width: 108px, height: 121px``.
|
||||||
|
|
||||||
|
Modifying Existing Dashboards and Panels
|
||||||
|
========================================
|
||||||
|
|
||||||
|
If you wish to alter dashboards or panels which are not part of your codebase,
|
||||||
|
you can specify a custom python module which will be loaded after the entire
|
||||||
|
Horizon site has been initialized, but prior to the URLconf construction.
|
||||||
|
This allows for common site-customization requirements such as:
|
||||||
|
|
||||||
|
* Registering or unregistering panels from an existing dashboard.
|
||||||
|
* Changing the names of dashboards and panels.
|
||||||
|
* Re-ordering panels within a dashboard or panel group.
|
||||||
|
|
||||||
|
To specify the python module containing your modifications, add the key
|
||||||
|
``customization_module`` to your ``settings.HORIZON_CONFIG`` dictionary.
|
||||||
|
The value should be a string containing the path to your module in dotted
|
||||||
|
python path notation. Example::
|
||||||
|
|
||||||
|
HORIZON_CONFIG = {
|
||||||
|
"customization_module": "my_project.overrides"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Button Icons
|
Button Icons
|
||||||
============
|
============
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ methods like :func:`~horizon.register` and :func:`~horizon.unregister`.
|
|||||||
# should that fail.
|
# should that fail.
|
||||||
Horizon = None
|
Horizon = None
|
||||||
try:
|
try:
|
||||||
from horizon.base import Horizon, Dashboard, Panel, Workflow
|
from horizon.base import Horizon, Dashboard, Panel, PanelGroup
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
165
horizon/base.py
165
horizon/base.py
@ -22,6 +22,7 @@ Public APIs are made available through the :mod:`horizon` module and
|
|||||||
the classes contained therein.
|
the classes contained therein.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
import copy
|
import copy
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
@ -30,6 +31,7 @@ from django.conf import settings
|
|||||||
from django.conf.urls.defaults import patterns, url, include
|
from django.conf.urls.defaults import patterns, url, include
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.utils.importlib import import_module
|
from django.utils.importlib import import_module
|
||||||
from django.utils.module_loading import module_has_submodule
|
from django.utils.module_loading import module_has_submodule
|
||||||
@ -254,6 +256,49 @@ class Panel(HorizonComponent):
|
|||||||
return urlpatterns, self.slug, self.slug
|
return urlpatterns, self.slug, self.slug
|
||||||
|
|
||||||
|
|
||||||
|
class PanelGroup(object):
|
||||||
|
""" A container for a set of :class:`~horizon.Panel` classes.
|
||||||
|
|
||||||
|
When iterated, it will yield each of the ``Panel`` instances it
|
||||||
|
contains.
|
||||||
|
|
||||||
|
.. attribute:: slug
|
||||||
|
|
||||||
|
A unique string to identify this panel group. Required.
|
||||||
|
|
||||||
|
.. attribute:: name
|
||||||
|
|
||||||
|
A user-friendly name which will be used as the group heading in
|
||||||
|
places such as the navigation. Default: ``None``.
|
||||||
|
|
||||||
|
.. attribute:: panels
|
||||||
|
|
||||||
|
A list of panel module names which should be contained within this
|
||||||
|
grouping.
|
||||||
|
"""
|
||||||
|
def __init__(self, dashboard, slug=None, name=None, panels=None):
|
||||||
|
self.dashboard = dashboard
|
||||||
|
self.slug = slug or getattr(self, "slug", "default")
|
||||||
|
self.name = name or getattr(self, "name", None)
|
||||||
|
# Our panels must be mutable so it can be extended by others.
|
||||||
|
self.panels = list(panels or getattr(self, "panels", []))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<%s: %s>" % (self.__class__.__name__, self.slug)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
panel_instances = []
|
||||||
|
for name in self.panels:
|
||||||
|
try:
|
||||||
|
panel_instances.append(self.dashboard.get_panel(name))
|
||||||
|
except NotRegistered, e:
|
||||||
|
LOG.debug(e)
|
||||||
|
return iter(panel_instances)
|
||||||
|
|
||||||
|
|
||||||
class Dashboard(Registry, HorizonComponent):
|
class Dashboard(Registry, HorizonComponent):
|
||||||
""" A base class for defining Horizon dashboards.
|
""" A base class for defining Horizon dashboards.
|
||||||
|
|
||||||
@ -275,13 +320,18 @@ class Dashboard(Registry, HorizonComponent):
|
|||||||
|
|
||||||
.. attribute:: panels
|
.. attribute:: panels
|
||||||
|
|
||||||
The ``panels`` attribute can be either a list containing the name
|
The ``panels`` attribute can be either a flat list containing the name
|
||||||
of each panel **module** which should be loaded as part of this
|
of each panel **module** which should be loaded as part of this
|
||||||
dashboard, or a dictionary of tuples which define groups of panels
|
dashboard, or a list of :class:`~horizon.PanelGroup` classes which
|
||||||
as in the following example::
|
define groups of panels as in the following example::
|
||||||
|
|
||||||
|
class SystemPanels(horizon.PanelGroup):
|
||||||
|
slug = "syspanel"
|
||||||
|
name = _("System Panel")
|
||||||
|
panels = ('overview', 'instances', ...)
|
||||||
|
|
||||||
class Syspanel(horizon.Dashboard):
|
class Syspanel(horizon.Dashboard):
|
||||||
panels = {'System Panel': ('overview', 'instances', ...)}
|
panels = (SystemPanels,)
|
||||||
|
|
||||||
Automatically generated navigation will use the order of the
|
Automatically generated navigation will use the order of the
|
||||||
modules in this attribute.
|
modules in this attribute.
|
||||||
@ -354,6 +404,10 @@ class Dashboard(Registry, HorizonComponent):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Dashboard: %s>" % self.slug
|
return "<Dashboard: %s>" % self.slug
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Dashboard, self).__init__(*args, **kwargs)
|
||||||
|
self._panel_groups = None
|
||||||
|
|
||||||
def get_panel(self, panel):
|
def get_panel(self, panel):
|
||||||
"""
|
"""
|
||||||
Returns the specified :class:`~horizon.Panel` instance registered
|
Returns the specified :class:`~horizon.Panel` instance registered
|
||||||
@ -364,27 +418,36 @@ class Dashboard(Registry, HorizonComponent):
|
|||||||
def get_panels(self):
|
def get_panels(self):
|
||||||
"""
|
"""
|
||||||
Returns the :class:`~horizon.Panel` instances registered with this
|
Returns the :class:`~horizon.Panel` instances registered with this
|
||||||
dashboard in order.
|
dashboard in order, without any panel groupings.
|
||||||
"""
|
"""
|
||||||
|
all_panels = []
|
||||||
|
panel_groups = self.get_panel_groups()
|
||||||
|
for panel_group in panel_groups.values():
|
||||||
|
all_panels.extend(panel_group)
|
||||||
|
return all_panels
|
||||||
|
|
||||||
|
def get_panel_group(self, slug):
|
||||||
|
return self._panel_groups[slug]
|
||||||
|
|
||||||
|
def get_panel_groups(self):
|
||||||
registered = copy.copy(self._registry)
|
registered = copy.copy(self._registry)
|
||||||
if isinstance(self.panels, dict):
|
panel_groups = []
|
||||||
panels = {}
|
|
||||||
for heading, items in self.panels.iteritems():
|
# Gather our known panels
|
||||||
panels.setdefault(heading, [])
|
for panel_group in self._panel_groups.values():
|
||||||
for item in items:
|
for panel in panel_group:
|
||||||
panel = self._registered(item)
|
|
||||||
panels[heading].append(panel)
|
|
||||||
registered.pop(panel.__class__)
|
|
||||||
if len(registered):
|
|
||||||
panels.setdefault(_("Other"), []).extend(registered.values())
|
|
||||||
else:
|
|
||||||
panels = []
|
|
||||||
for item in self.panels:
|
|
||||||
panel = self._registered(item)
|
|
||||||
panels.append(panel)
|
|
||||||
registered.pop(panel.__class__)
|
registered.pop(panel.__class__)
|
||||||
panels.extend(registered.values())
|
panel_groups.append((panel_group.slug, panel_group))
|
||||||
return panels
|
|
||||||
|
# Deal with leftovers (such as add-on registrations)
|
||||||
|
if len(registered):
|
||||||
|
slugs = [panel.slug for panel in registered.values()]
|
||||||
|
new_group = PanelGroup(self,
|
||||||
|
slug="other",
|
||||||
|
name=_("Other"),
|
||||||
|
panels=slugs)
|
||||||
|
panel_groups.append((new_group.slug, new_group))
|
||||||
|
return SortedDict(panel_groups)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
""" Returns the default URL for this dashboard.
|
""" Returns the default URL for this dashboard.
|
||||||
@ -405,7 +468,6 @@ class Dashboard(Registry, HorizonComponent):
|
|||||||
def _decorated_urls(self):
|
def _decorated_urls(self):
|
||||||
urlpatterns = self._get_default_urlpatterns()
|
urlpatterns = self._get_default_urlpatterns()
|
||||||
|
|
||||||
self._autodiscover()
|
|
||||||
default_panel = None
|
default_panel = None
|
||||||
|
|
||||||
# Add in each panel's views except for the default view.
|
# Add in each panel's views except for the default view.
|
||||||
@ -437,14 +499,36 @@ class Dashboard(Registry, HorizonComponent):
|
|||||||
|
|
||||||
def _autodiscover(self):
|
def _autodiscover(self):
|
||||||
""" Discovers panels to register from the current dashboard module. """
|
""" Discovers panels to register from the current dashboard module. """
|
||||||
|
if getattr(self, "_autodiscover_complete", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
panels_to_discover = []
|
||||||
|
panel_groups = []
|
||||||
|
# If we have a flat iterable of panel names, wrap it again so
|
||||||
|
# we have a consistent structure for the next step.
|
||||||
|
if all([isinstance(i, basestring) for i in self.panels]):
|
||||||
|
self.panels = [self.panels]
|
||||||
|
|
||||||
|
# Now iterate our panel sets.
|
||||||
|
for panel_set in self.panels:
|
||||||
|
# Instantiate PanelGroup classes.
|
||||||
|
if not isinstance(panel_set, collections.Iterable) and \
|
||||||
|
issubclass(panel_set, PanelGroup):
|
||||||
|
panel_group = panel_set(self)
|
||||||
|
# Check for nested tuples, and convert them to PanelGroups
|
||||||
|
elif not isinstance(panel_set, PanelGroup):
|
||||||
|
panel_group = PanelGroup(self, panels=panel_set)
|
||||||
|
|
||||||
|
# Put our results into their appropriate places
|
||||||
|
panels_to_discover.extend(panel_group.panels)
|
||||||
|
panel_groups.append((panel_group.slug, panel_group))
|
||||||
|
|
||||||
|
self._panel_groups = SortedDict(panel_groups)
|
||||||
|
|
||||||
|
# Do the actual discovery
|
||||||
package = '.'.join(self.__module__.split('.')[:-1])
|
package = '.'.join(self.__module__.split('.')[:-1])
|
||||||
mod = import_module(package)
|
mod = import_module(package)
|
||||||
panels = []
|
for panel in panels_to_discover:
|
||||||
if isinstance(self.panels, dict):
|
|
||||||
[panels.extend(values) for values in self.panels.values()]
|
|
||||||
else:
|
|
||||||
panels = self.panels
|
|
||||||
for panel in panels:
|
|
||||||
try:
|
try:
|
||||||
before_import_registry = copy.copy(self._registry)
|
before_import_registry = copy.copy(self._registry)
|
||||||
import_module('.%s.panel' % panel, package)
|
import_module('.%s.panel' % panel, package)
|
||||||
@ -452,6 +536,7 @@ class Dashboard(Registry, HorizonComponent):
|
|||||||
self._registry = before_import_registry
|
self._registry = before_import_registry
|
||||||
if module_has_submodule(mod, panel):
|
if module_has_submodule(mod, panel):
|
||||||
raise
|
raise
|
||||||
|
self._autodiscover_complete = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register(cls, panel):
|
def register(cls, panel):
|
||||||
@ -646,7 +731,27 @@ class Site(Registry, HorizonComponent):
|
|||||||
""" Constructs the URLconf for Horizon from registered Dashboards. """
|
""" Constructs the URLconf for Horizon from registered Dashboards. """
|
||||||
urlpatterns = self._get_default_urlpatterns()
|
urlpatterns = self._get_default_urlpatterns()
|
||||||
self._autodiscover()
|
self._autodiscover()
|
||||||
# Add in each dashboard's views.
|
|
||||||
|
# Discover each dashboard's panels.
|
||||||
|
for dash in self._registry.values():
|
||||||
|
dash._autodiscover()
|
||||||
|
|
||||||
|
# Allow for override modules
|
||||||
|
config = getattr(settings, "HORIZON_CONFIG", {})
|
||||||
|
if config.get("customization_module", None):
|
||||||
|
customization_module = config["customization_module"]
|
||||||
|
bits = customization_module.split('.')
|
||||||
|
mod_name = bits.pop()
|
||||||
|
package = '.'.join(bits)
|
||||||
|
try:
|
||||||
|
before_import_registry = copy.copy(self._registry)
|
||||||
|
import_module('%s.%s' % (package, mod_name))
|
||||||
|
except:
|
||||||
|
self._registry = before_import_registry
|
||||||
|
if module_has_submodule(package, mod_name):
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Compile the dynamic urlconf.
|
||||||
for dash in self._registry.values():
|
for dash in self._registry.values():
|
||||||
urlpatterns += patterns('',
|
urlpatterns += patterns('',
|
||||||
url(r'^%s/' % dash.slug, include(dash._decorated_urls)))
|
url(r'^%s/' % dash.slug, include(dash._decorated_urls)))
|
||||||
|
@ -19,14 +19,25 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
import horizon
|
import horizon
|
||||||
|
|
||||||
|
|
||||||
|
class BasePanels(horizon.PanelGroup):
|
||||||
|
slug = "compute"
|
||||||
|
name = _("Manage Compute")
|
||||||
|
panels = ('overview',
|
||||||
|
'instances_and_volumes',
|
||||||
|
'images_and_snapshots',
|
||||||
|
'access_and_security')
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectStorePanels(horizon.PanelGroup):
|
||||||
|
slug = "object_store"
|
||||||
|
name = _("Object Store")
|
||||||
|
panels = ('containers',)
|
||||||
|
|
||||||
|
|
||||||
class Nova(horizon.Dashboard):
|
class Nova(horizon.Dashboard):
|
||||||
name = _("Project")
|
name = _("Project")
|
||||||
slug = "nova"
|
slug = "nova"
|
||||||
panels = {_("Manage Compute"): ('overview',
|
panels = (BasePanels, ObjectStorePanels)
|
||||||
'instances_and_volumes',
|
|
||||||
'access_and_security',
|
|
||||||
'images_and_snapshots'),
|
|
||||||
_("Object Store"): ('containers',)}
|
|
||||||
default_panel = 'overview'
|
default_panel = 'overview'
|
||||||
supports_tenants = True
|
supports_tenants = True
|
||||||
|
|
||||||
|
@ -19,12 +19,17 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
import horizon
|
import horizon
|
||||||
|
|
||||||
|
|
||||||
|
class SystemPanels(horizon.PanelGroup):
|
||||||
|
slug = "syspanel"
|
||||||
|
name = _("System Panel")
|
||||||
|
panels = ('overview', 'instances', 'services', 'flavors', 'images',
|
||||||
|
'projects', 'users', 'quotas',)
|
||||||
|
|
||||||
|
|
||||||
class Syspanel(horizon.Dashboard):
|
class Syspanel(horizon.Dashboard):
|
||||||
name = _("Admin")
|
name = _("Admin")
|
||||||
slug = "syspanel"
|
slug = "syspanel"
|
||||||
panels = {_("System Panel"): ('overview', 'instances', 'services',
|
panels = (SystemPanels,)
|
||||||
'flavors', 'images', 'projects', 'users',
|
|
||||||
'quotas',)}
|
|
||||||
default_panel = 'overview'
|
default_panel = 'overview'
|
||||||
roles = ('admin',)
|
roles = ('admin',)
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% for heading, panels in components.iteritems %}
|
{% for heading, panels in components.iteritems %}
|
||||||
{% with panels|can_haz_list:user as filtered_panels %}
|
{% with panels|can_haz_list:user as filtered_panels %}
|
||||||
{% if filtered_panels %}
|
{% if filtered_panels %}
|
||||||
<h4>{{ heading }}</h4>
|
{% if heading %}<h4>{{ heading }}</h4>{% endif %}
|
||||||
<ul class="main_nav">
|
<ul class="main_nav">
|
||||||
{% for panel in filtered_panels %}
|
{% for panel in filtered_panels %}
|
||||||
<li>
|
<li>
|
||||||
|
@ -16,9 +16,8 @@
|
|||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
|
|
||||||
from horizon.base import Horizon
|
from horizon.base import Horizon
|
||||||
|
|
||||||
@ -78,21 +77,20 @@ def horizon_dashboard_nav(context):
|
|||||||
if 'request' not in context:
|
if 'request' not in context:
|
||||||
return {}
|
return {}
|
||||||
dashboard = context['request'].horizon['dashboard']
|
dashboard = context['request'].horizon['dashboard']
|
||||||
if isinstance(dashboard.panels, dict):
|
panel_groups = dashboard.get_panel_groups()
|
||||||
panels = copy.copy(dashboard.get_panels())
|
non_empty_groups = []
|
||||||
else:
|
|
||||||
panels = {dashboard.name: dashboard.get_panels()}
|
|
||||||
|
|
||||||
for heading, items in panels.iteritems():
|
for group in panel_groups.values():
|
||||||
temp_panels = []
|
allowed_panels = []
|
||||||
for panel in items:
|
for panel in group:
|
||||||
if callable(panel.nav) and panel.nav(context):
|
if callable(panel.nav) and panel.nav(context):
|
||||||
temp_panels.append(panel)
|
allowed_panels.append(panel)
|
||||||
elif not callable(panel.nav) and panel.nav:
|
elif not callable(panel.nav) and panel.nav:
|
||||||
temp_panels.append(panel)
|
allowed_panels.append(panel)
|
||||||
panels[heading] = temp_panels
|
if allowed_panels:
|
||||||
non_empty_panels = dict([(k, v) for k, v in panels.items() if len(v) > 0])
|
non_empty_groups.append((group.name, allowed_panels))
|
||||||
return {'components': non_empty_panels,
|
|
||||||
|
return {'components': SortedDict(non_empty_groups),
|
||||||
'user': context['request'].user,
|
'user': context['request'].user,
|
||||||
'current': context['request'].horizon['panel'].slug,
|
'current': context['request'].horizon['panel'].slug,
|
||||||
'request': context['request']}
|
'request': context['request']}
|
||||||
|
@ -138,7 +138,7 @@ class HorizonTests(BaseHorizonTests):
|
|||||||
def test_dashboard(self):
|
def test_dashboard(self):
|
||||||
syspanel = horizon.get_dashboard("syspanel")
|
syspanel = horizon.get_dashboard("syspanel")
|
||||||
self.assertEqual(syspanel._registered_with, base.Horizon)
|
self.assertEqual(syspanel._registered_with, base.Horizon)
|
||||||
self.assertQuerysetEqual(syspanel.get_panels().values()[0],
|
self.assertQuerysetEqual(syspanel.get_panels(),
|
||||||
['<Panel: overview>',
|
['<Panel: overview>',
|
||||||
'<Panel: instances>',
|
'<Panel: instances>',
|
||||||
'<Panel: services>',
|
'<Panel: services>',
|
||||||
@ -148,12 +148,18 @@ class HorizonTests(BaseHorizonTests):
|
|||||||
'<Panel: users>',
|
'<Panel: users>',
|
||||||
'<Panel: quotas>'])
|
'<Panel: quotas>'])
|
||||||
self.assertEqual(syspanel.get_absolute_url(), "/syspanel/")
|
self.assertEqual(syspanel.get_absolute_url(), "/syspanel/")
|
||||||
|
|
||||||
# Test registering a module with a dashboard that defines panels
|
# Test registering a module with a dashboard that defines panels
|
||||||
# as a dictionary.
|
# as a dictionary.
|
||||||
syspanel.register(MyPanel)
|
syspanel.register(MyPanel)
|
||||||
self.assertQuerysetEqual(syspanel.get_panels()['Other'],
|
self.assertQuerysetEqual(syspanel.get_panel_groups()['other'],
|
||||||
['<Panel: myslug>'])
|
['<Panel: myslug>'])
|
||||||
|
|
||||||
|
# Test that panels defined as a tuple still return a PanelGroup
|
||||||
|
settings_dash = horizon.get_dashboard("settings")
|
||||||
|
self.assertQuerysetEqual(settings_dash.get_panel_groups().values(),
|
||||||
|
['<PanelGroup: default>'])
|
||||||
|
|
||||||
# Test registering a module with a dashboard that defines panels
|
# Test registering a module with a dashboard that defines panels
|
||||||
# as a tuple.
|
# as a tuple.
|
||||||
settings_dash = horizon.get_dashboard("settings")
|
settings_dash = horizon.get_dashboard("settings")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user