Merge "Adds PanelGroup class and site customization hook."

This commit is contained in:
Jenkins 2012-03-27 02:13:35 +00:00 committed by Gerrit Code Review
commit 6982e373f9
9 changed files with 206 additions and 56 deletions

View File

@ -40,3 +40,6 @@ Panel
.. autoclass:: Panel .. autoclass:: Panel
:members: :members:
.. autoclass:: PanelGroup
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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