08e6ed7392
We should use ugettext_lazy to let users in different languages get correct translation.Though in views or similar places we can use ugettext with no problems, we should make code style consistent. This also fix the issues that the django proxy objects appear on the launching instance page instead of translated strings. Now the translated quota strings are handled correctly. Fixes: bug #1178722 Change-Id: I76f0d31e6546d70e5ece7e86a06f7eefd04861d2
789 lines
28 KiB
Python
789 lines
28 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright 2012 Nebula, Inc.
|
|
#
|
|
# 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.
|
|
|
|
"""
|
|
Contains the core classes and functionality that makes Horizon what it is.
|
|
This module is considered internal, and should not be relied on directly.
|
|
|
|
Public APIs are made available through the :mod:`horizon` module and
|
|
the classes contained therein.
|
|
"""
|
|
|
|
import collections
|
|
import copy
|
|
import inspect
|
|
import logging
|
|
import os
|
|
|
|
from django.conf import settings
|
|
from django.conf.urls.defaults import patterns, url, include
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
from django.core.urlresolvers import reverse
|
|
from django.utils.datastructures import SortedDict
|
|
from django.utils.functional import SimpleLazyObject
|
|
from django.utils.importlib import import_module
|
|
from django.utils.module_loading import module_has_submodule
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from horizon import loaders
|
|
from horizon import conf
|
|
from horizon.decorators import require_auth, require_perms, _current_component
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def _decorate_urlconf(urlpatterns, decorator, *args, **kwargs):
|
|
for pattern in urlpatterns:
|
|
if getattr(pattern, 'callback', None):
|
|
pattern._callback = decorator(pattern.callback, *args, **kwargs)
|
|
if getattr(pattern, 'url_patterns', []):
|
|
_decorate_urlconf(pattern.url_patterns, decorator, *args, **kwargs)
|
|
|
|
|
|
class NotRegistered(Exception):
|
|
pass
|
|
|
|
|
|
class HorizonComponent(object):
|
|
def __init__(self):
|
|
super(HorizonComponent, self).__init__()
|
|
if not self.slug:
|
|
raise ImproperlyConfigured('Every %s must have a slug.'
|
|
% self.__class__)
|
|
|
|
def __unicode__(self):
|
|
name = getattr(self, 'name', u"Unnamed %s" % self.__class__.__name__)
|
|
return unicode(name)
|
|
|
|
def _get_default_urlpatterns(self):
|
|
package_string = '.'.join(self.__module__.split('.')[:-1])
|
|
if getattr(self, 'urls', None):
|
|
try:
|
|
mod = import_module('.%s' % self.urls, package_string)
|
|
except ImportError:
|
|
mod = import_module(self.urls)
|
|
urlpatterns = mod.urlpatterns
|
|
else:
|
|
# Try importing a urls.py from the dashboard package
|
|
if module_has_submodule(import_module(package_string), 'urls'):
|
|
urls_mod = import_module('.urls', package_string)
|
|
urlpatterns = urls_mod.urlpatterns
|
|
else:
|
|
urlpatterns = patterns('')
|
|
return urlpatterns
|
|
|
|
|
|
class Registry(object):
|
|
def __init__(self):
|
|
self._registry = {}
|
|
if not getattr(self, '_registerable_class', None):
|
|
raise ImproperlyConfigured('Subclasses of Registry must set a '
|
|
'"_registerable_class" property.')
|
|
|
|
def _register(self, cls):
|
|
"""Registers the given class.
|
|
|
|
If the specified class is already registered then it is ignored.
|
|
"""
|
|
if not inspect.isclass(cls):
|
|
raise ValueError('Only classes may be registered.')
|
|
elif not issubclass(cls, self._registerable_class):
|
|
raise ValueError('Only %s classes or subclasses may be registered.'
|
|
% self._registerable_class.__name__)
|
|
|
|
if cls not in self._registry:
|
|
cls._registered_with = self
|
|
self._registry[cls] = cls()
|
|
|
|
return self._registry[cls]
|
|
|
|
def _unregister(self, cls):
|
|
"""Unregisters the given class.
|
|
|
|
If the specified class isn't registered, ``NotRegistered`` will
|
|
be raised.
|
|
"""
|
|
if not issubclass(cls, self._registerable_class):
|
|
raise ValueError('Only %s classes or subclasses may be '
|
|
'unregistered.' % self._registerable_class)
|
|
|
|
if cls not in self._registry.keys():
|
|
raise NotRegistered('%s is not registered' % cls)
|
|
|
|
del self._registry[cls]
|
|
|
|
return True
|
|
|
|
def _registered(self, cls):
|
|
if inspect.isclass(cls) and issubclass(cls, self._registerable_class):
|
|
found = self._registry.get(cls, None)
|
|
if found:
|
|
return found
|
|
else:
|
|
# Allow for fetching by slugs as well.
|
|
for registered in self._registry.values():
|
|
if registered.slug == cls:
|
|
return registered
|
|
class_name = self._registerable_class.__name__
|
|
if hasattr(self, "_registered_with"):
|
|
parent = self._registered_with._registerable_class.__name__
|
|
raise NotRegistered('%(type)s with slug "%(slug)s" is not '
|
|
'registered with %(parent)s "%(name)s".'
|
|
% {"type": class_name,
|
|
"slug": cls,
|
|
"parent": parent,
|
|
"name": self.slug})
|
|
else:
|
|
slug = getattr(cls, "slug", cls)
|
|
raise NotRegistered('%(type)s with slug "%(slug)s" is not '
|
|
'registered.' % {"type": class_name,
|
|
"slug": slug})
|
|
|
|
|
|
class Panel(HorizonComponent):
|
|
""" A base class for defining Horizon dashboard panels.
|
|
|
|
All Horizon dashboard panels should extend from this class. It provides
|
|
the appropriate hooks for automatically constructing URLconfs, and
|
|
providing permission-based access control.
|
|
|
|
.. attribute:: name
|
|
|
|
The name of the panel. This will be displayed in the
|
|
auto-generated navigation and various other places.
|
|
Default: ``''``.
|
|
|
|
.. attribute:: slug
|
|
|
|
A unique "short name" for the panel. The slug is used as
|
|
a component of the URL path for the panel. Default: ``''``.
|
|
|
|
.. attribute:: permissions
|
|
|
|
A list of permission names, all of which a user must possess in order
|
|
to access any view associated with this panel. This attribute
|
|
is combined cumulatively with any permissions required on the
|
|
``Dashboard`` class with which it is registered.
|
|
|
|
.. attribute:: urls
|
|
|
|
Path to a URLconf of views for this panel using dotted Python
|
|
notation. If no value is specified, a file called ``urls.py``
|
|
living in the same package as the ``panel.py`` file is used.
|
|
Default: ``None``.
|
|
|
|
.. attribute:: nav
|
|
.. method:: nav(context)
|
|
|
|
The ``nav`` attribute can be either boolean value or a callable
|
|
which accepts a ``RequestContext`` object as a single argument
|
|
to control whether or not this panel should appear in
|
|
automatically-generated navigation. Default: ``True``.
|
|
|
|
.. attribute:: index_url_name
|
|
|
|
The ``name`` argument for the URL pattern which corresponds to
|
|
the index view for this ``Panel``. This is the view that
|
|
:meth:`.Panel.get_absolute_url` will attempt to reverse.
|
|
"""
|
|
name = ''
|
|
slug = ''
|
|
urls = None
|
|
nav = True
|
|
index_url_name = "index"
|
|
|
|
def __repr__(self):
|
|
return "<Panel: %s>" % self.slug
|
|
|
|
def get_absolute_url(self):
|
|
""" Returns the default URL for this panel.
|
|
|
|
The default URL is defined as the URL pattern with ``name="index"`` in
|
|
the URLconf for this panel.
|
|
"""
|
|
try:
|
|
return reverse('horizon:%s:%s:%s' % (self._registered_with.slug,
|
|
self.slug,
|
|
self.index_url_name))
|
|
except Exception as exc:
|
|
# Logging here since this will often be called in a template
|
|
# where the exception would be hidden.
|
|
LOG.info("Error reversing absolute URL for %s: %s" % (self, exc))
|
|
raise
|
|
|
|
@property
|
|
def _decorated_urls(self):
|
|
urlpatterns = self._get_default_urlpatterns()
|
|
|
|
# Apply access controls to all views in the patterns
|
|
permissions = getattr(self, 'permissions', [])
|
|
_decorate_urlconf(urlpatterns, require_perms, permissions)
|
|
_decorate_urlconf(urlpatterns, _current_component, panel=self)
|
|
|
|
# Return the three arguments to django.conf.urls.defaults.include
|
|
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):
|
|
""" A base class for defining Horizon dashboards.
|
|
|
|
All Horizon dashboards should extend from this base class. It provides the
|
|
appropriate hooks for automatic discovery of :class:`~horizon.Panel`
|
|
modules, automatically constructing URLconfs, and providing
|
|
permission-based access control.
|
|
|
|
.. attribute:: name
|
|
|
|
The name of the dashboard. This will be displayed in the
|
|
auto-generated navigation and various other places.
|
|
Default: ``''``.
|
|
|
|
.. attribute:: slug
|
|
|
|
A unique "short name" for the dashboard. The slug is used as
|
|
a component of the URL path for the dashboard. Default: ``''``.
|
|
|
|
.. attribute:: panels
|
|
|
|
The ``panels`` attribute can be either a flat list containing the name
|
|
of each panel **module** which should be loaded as part of this
|
|
dashboard, or a list of :class:`~horizon.PanelGroup` classes which
|
|
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):
|
|
panels = (SystemPanels,)
|
|
|
|
Automatically generated navigation will use the order of the
|
|
modules in this attribute.
|
|
|
|
Default: ``[]``.
|
|
|
|
.. warning::
|
|
|
|
The values for this attribute should not correspond to the
|
|
:attr:`~.Panel.name` attributes of the ``Panel`` classes.
|
|
They should be the names of the Python modules in which the
|
|
``panel.py`` files live. This is used for the automatic
|
|
loading and registration of ``Panel`` classes much like
|
|
Django's ``ModelAdmin`` machinery.
|
|
|
|
Panel modules must be listed in ``panels`` in order to be
|
|
discovered by the automatic registration mechanism.
|
|
|
|
.. attribute:: default_panel
|
|
|
|
The name of the panel which should be treated as the default
|
|
panel for the dashboard, i.e. when you visit the root URL
|
|
for this dashboard, that's the panel that is displayed.
|
|
Default: ``None``.
|
|
|
|
.. attribute:: permissions
|
|
|
|
A list of permission names, all of which a user must possess in order
|
|
to access any panel registered with this dashboard. This attribute
|
|
is combined cumulatively with any permissions required on individual
|
|
:class:`~horizon.Panel` classes.
|
|
|
|
.. attribute:: urls
|
|
|
|
Optional path to a URLconf of additional views for this dashboard
|
|
which are not connected to specific panels. Default: ``None``.
|
|
|
|
.. attribute:: nav
|
|
|
|
Optional boolean to control whether or not this dashboard should
|
|
appear in automatically-generated navigation. Default: ``True``.
|
|
|
|
.. attribute:: supports_tenants
|
|
|
|
Optional boolean that indicates whether or not this dashboard includes
|
|
support for projects/tenants. If set to ``True`` this dashboard's
|
|
navigation will include a UI element that allows the user to select
|
|
project/tenant. Default: ``False``.
|
|
|
|
.. attribute:: public
|
|
|
|
Boolean value to determine whether this dashboard can be viewed
|
|
without being logged in. Defaults to ``False``.
|
|
"""
|
|
_registerable_class = Panel
|
|
name = ''
|
|
slug = ''
|
|
urls = None
|
|
panels = []
|
|
default_panel = None
|
|
nav = True
|
|
supports_tenants = False
|
|
public = False
|
|
|
|
def __repr__(self):
|
|
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):
|
|
"""
|
|
Returns the specified :class:`~horizon.Panel` instance registered
|
|
with this dashboard.
|
|
"""
|
|
return self._registered(panel)
|
|
|
|
def get_panels(self):
|
|
"""
|
|
Returns the :class:`~horizon.Panel` instances registered with this
|
|
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)
|
|
panel_groups = []
|
|
|
|
# Gather our known panels
|
|
for panel_group in self._panel_groups.values():
|
|
for panel in panel_group:
|
|
registered.pop(panel.__class__)
|
|
panel_groups.append((panel_group.slug, panel_group))
|
|
|
|
# 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):
|
|
""" Returns the default URL for this dashboard.
|
|
|
|
The default URL is defined as the URL pattern with ``name="index"``
|
|
in the URLconf for the :class:`~horizon.Panel` specified by
|
|
:attr:`~horizon.Dashboard.default_panel`.
|
|
"""
|
|
try:
|
|
return self._registered(self.default_panel).get_absolute_url()
|
|
except:
|
|
# Logging here since this will often be called in a template
|
|
# where the exception would be hidden.
|
|
LOG.exception("Error reversing absolute URL for %s." % self)
|
|
raise
|
|
|
|
@property
|
|
def _decorated_urls(self):
|
|
urlpatterns = self._get_default_urlpatterns()
|
|
|
|
default_panel = None
|
|
|
|
# Add in each panel's views except for the default view.
|
|
for panel in self._registry.values():
|
|
if panel.slug == self.default_panel:
|
|
default_panel = panel
|
|
continue
|
|
urlpatterns += patterns('',
|
|
url(r'^%s/' % panel.slug, include(panel._decorated_urls)))
|
|
# Now the default view, which should come last
|
|
if not default_panel:
|
|
raise NotRegistered('The default panel "%s" is not registered.'
|
|
% self.default_panel)
|
|
urlpatterns += patterns('',
|
|
url(r'', include(default_panel._decorated_urls)))
|
|
|
|
# Require login if not public.
|
|
if not self.public:
|
|
_decorate_urlconf(urlpatterns, require_auth)
|
|
# Apply access controls to all views in the patterns
|
|
permissions = getattr(self, 'permissions', [])
|
|
_decorate_urlconf(urlpatterns, require_perms, permissions)
|
|
_decorate_urlconf(urlpatterns, _current_component, dashboard=self)
|
|
|
|
# Return the three arguments to django.conf.urls.defaults.include
|
|
return urlpatterns, self.slug, self.slug
|
|
|
|
def _autodiscover(self):
|
|
""" 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])
|
|
mod = import_module(package)
|
|
for panel in panels_to_discover:
|
|
try:
|
|
before_import_registry = copy.copy(self._registry)
|
|
import_module('.%s.panel' % panel, package)
|
|
except:
|
|
self._registry = before_import_registry
|
|
if module_has_submodule(mod, panel):
|
|
raise
|
|
self._autodiscover_complete = True
|
|
|
|
@classmethod
|
|
def register(cls, panel):
|
|
""" Registers a :class:`~horizon.Panel` with this dashboard. """
|
|
panel_class = Horizon.register_panel(cls, panel)
|
|
# Support template loading from panel template directories.
|
|
panel_mod = import_module(panel.__module__)
|
|
panel_dir = os.path.dirname(panel_mod.__file__)
|
|
template_dir = os.path.join(panel_dir, "templates")
|
|
if os.path.exists(template_dir):
|
|
key = os.path.join(cls.slug, panel.slug)
|
|
loaders.panel_template_dirs[key] = template_dir
|
|
return panel_class
|
|
|
|
@classmethod
|
|
def unregister(cls, panel):
|
|
""" Unregisters a :class:`~horizon.Panel` from this dashboard. """
|
|
success = Horizon.unregister_panel(cls, panel)
|
|
if success:
|
|
# Remove the panel's template directory.
|
|
key = os.path.join(cls.slug, panel.slug)
|
|
if key in loaders.panel_template_dirs:
|
|
del loaders.panel_template_dirs[key]
|
|
return success
|
|
|
|
|
|
class Workflow(object):
|
|
def __init__(*args, **kwargs):
|
|
raise NotImplementedError()
|
|
|
|
|
|
try:
|
|
from django.utils.functional import empty
|
|
except ImportError:
|
|
#Django 1.3 fallback
|
|
empty = None
|
|
|
|
|
|
class LazyURLPattern(SimpleLazyObject):
|
|
def __iter__(self):
|
|
if self._wrapped is empty:
|
|
self._setup()
|
|
return iter(self._wrapped)
|
|
|
|
def __reversed__(self):
|
|
if self._wrapped is empty:
|
|
self._setup()
|
|
return reversed(self._wrapped)
|
|
|
|
|
|
class Site(Registry, HorizonComponent):
|
|
""" The overarching class which encompasses all dashboards and panels. """
|
|
|
|
# Required for registry
|
|
_registerable_class = Dashboard
|
|
|
|
name = "Horizon"
|
|
namespace = 'horizon'
|
|
slug = 'horizon'
|
|
urls = 'horizon.site_urls'
|
|
|
|
def __repr__(self):
|
|
return u"<Site: %s>" % self.slug
|
|
|
|
@property
|
|
def _conf(self):
|
|
return conf.HORIZON_CONFIG
|
|
|
|
@property
|
|
def dashboards(self):
|
|
return self._conf['dashboards']
|
|
|
|
@property
|
|
def default_dashboard(self):
|
|
return self._conf['default_dashboard']
|
|
|
|
def register(self, dashboard):
|
|
""" Registers a :class:`~horizon.Dashboard` with Horizon."""
|
|
return self._register(dashboard)
|
|
|
|
def unregister(self, dashboard):
|
|
""" Unregisters a :class:`~horizon.Dashboard` from Horizon. """
|
|
return self._unregister(dashboard)
|
|
|
|
def registered(self, dashboard):
|
|
return self._registered(dashboard)
|
|
|
|
def register_panel(self, dashboard, panel):
|
|
dash_instance = self.registered(dashboard)
|
|
return dash_instance._register(panel)
|
|
|
|
def unregister_panel(self, dashboard, panel):
|
|
dash_instance = self.registered(dashboard)
|
|
if not dash_instance:
|
|
raise NotRegistered("The dashboard %s is not registered."
|
|
% dashboard)
|
|
return dash_instance._unregister(panel)
|
|
|
|
def get_dashboard(self, dashboard):
|
|
""" Returns the specified :class:`~horizon.Dashboard` instance. """
|
|
return self._registered(dashboard)
|
|
|
|
def get_dashboards(self):
|
|
""" Returns an ordered tuple of :class:`~horizon.Dashboard` modules.
|
|
|
|
Orders dashboards according to the ``"dashboards"`` key in
|
|
``HORIZON_CONFIG`` or else returns all registered dashboards
|
|
in alphabetical order.
|
|
|
|
Any remaining :class:`~horizon.Dashboard` classes registered with
|
|
Horizon but not listed in ``HORIZON_CONFIG['dashboards']``
|
|
will be appended to the end of the list alphabetically.
|
|
"""
|
|
if self.dashboards:
|
|
registered = copy.copy(self._registry)
|
|
dashboards = []
|
|
for item in self.dashboards:
|
|
dashboard = self._registered(item)
|
|
dashboards.append(dashboard)
|
|
registered.pop(dashboard.__class__)
|
|
if len(registered):
|
|
extra = registered.values()
|
|
extra.sort()
|
|
dashboards.extend(extra)
|
|
return dashboards
|
|
else:
|
|
dashboards = self._registry.values()
|
|
dashboards.sort()
|
|
return dashboards
|
|
|
|
def get_default_dashboard(self):
|
|
""" Returns the default :class:`~horizon.Dashboard` instance.
|
|
|
|
If ``"default_dashboard"`` is specified in ``HORIZON_CONFIG``
|
|
then that dashboard will be returned. If not, the first dashboard
|
|
returned by :func:`~horizon.get_dashboards` will be returned.
|
|
"""
|
|
if self.default_dashboard:
|
|
return self._registered(self.default_dashboard)
|
|
elif len(self._registry):
|
|
return self.get_dashboards()[0]
|
|
else:
|
|
raise NotRegistered("No dashboard modules have been registered.")
|
|
|
|
def get_user_home(self, user):
|
|
""" Returns the default URL for a particular user.
|
|
|
|
This method can be used to customize where a user is sent when
|
|
they log in, etc. By default it returns the value of
|
|
:meth:`get_absolute_url`.
|
|
|
|
An alternative function can be supplied to customize this behavior
|
|
by specifying a either a URL or a function which returns a URL via
|
|
the ``"user_home"`` key in ``HORIZON_CONFIG``. Each of these
|
|
would be valid::
|
|
|
|
{"user_home": "/home",} # A URL
|
|
{"user_home": "my_module.get_user_home",} # Path to a function
|
|
{"user_home": lambda user: "/" + user.name,} # A function
|
|
{"user_home": None,} # Will always return the default dashboard
|
|
|
|
This can be useful if the default dashboard may not be accessible
|
|
to all users. When user_home is missing from HORIZON_CONFIG,
|
|
it will default to the settings.LOGIN_REDIRECT_URL value.
|
|
"""
|
|
user_home = self._conf['user_home']
|
|
if user_home:
|
|
if callable(user_home):
|
|
return user_home(user)
|
|
elif isinstance(user_home, basestring):
|
|
# Assume we've got a URL if there's a slash in it
|
|
if user_home.find("/") != -1:
|
|
return user_home
|
|
else:
|
|
mod, func = user_home.rsplit(".", 1)
|
|
return getattr(import_module(mod), func)(user)
|
|
# If it's not callable and not a string, it's wrong.
|
|
raise ValueError('The user_home setting must be either a string '
|
|
'or a callable object (e.g. a function).')
|
|
else:
|
|
return self.get_absolute_url()
|
|
|
|
def get_absolute_url(self):
|
|
""" Returns the default URL for Horizon's URLconf.
|
|
|
|
The default URL is determined by calling
|
|
:meth:`~horizon.Dashboard.get_absolute_url`
|
|
on the :class:`~horizon.Dashboard` instance returned by
|
|
:meth:`~horizon.get_default_dashboard`.
|
|
"""
|
|
return self.get_default_dashboard().get_absolute_url()
|
|
|
|
@property
|
|
def _lazy_urls(self):
|
|
""" Lazy loading for URL patterns.
|
|
|
|
This method avoids problems associated with attempting to evaluate
|
|
the the URLconf before the settings module has been loaded.
|
|
"""
|
|
def url_patterns():
|
|
return self._urls()[0]
|
|
|
|
return LazyURLPattern(url_patterns), self.namespace, self.slug
|
|
|
|
def _urls(self):
|
|
""" Constructs the URLconf for Horizon from registered Dashboards. """
|
|
urlpatterns = self._get_default_urlpatterns()
|
|
self._autodiscover()
|
|
|
|
# Discover each dashboard's panels.
|
|
for dash in self._registry.values():
|
|
dash._autodiscover()
|
|
|
|
# Allow for override modules
|
|
if self._conf.get("customization_module", None):
|
|
customization_module = self._conf["customization_module"]
|
|
bits = customization_module.split('.')
|
|
mod_name = bits.pop()
|
|
package = '.'.join(bits)
|
|
mod = import_module(package)
|
|
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(mod, mod_name):
|
|
raise
|
|
|
|
# Compile the dynamic urlconf.
|
|
for dash in self._registry.values():
|
|
urlpatterns += patterns('',
|
|
url(r'^%s/' % dash.slug, include(dash._decorated_urls)))
|
|
|
|
# Return the three arguments to django.conf.urls.defaults.include
|
|
return urlpatterns, self.namespace, self.slug
|
|
|
|
def _autodiscover(self):
|
|
""" Discovers modules to register from ``settings.INSTALLED_APPS``.
|
|
|
|
This makes sure that the appropriate modules get imported to register
|
|
themselves with Horizon.
|
|
"""
|
|
if not getattr(self, '_registerable_class', None):
|
|
raise ImproperlyConfigured('You must set a '
|
|
'"_registerable_class" property '
|
|
'in order to use autodiscovery.')
|
|
# Discover both dashboards and panels, in that order
|
|
for mod_name in ('dashboard', 'panel'):
|
|
for app in settings.INSTALLED_APPS:
|
|
mod = import_module(app)
|
|
try:
|
|
before_import_registry = copy.copy(self._registry)
|
|
import_module('%s.%s' % (app, mod_name))
|
|
except:
|
|
self._registry = before_import_registry
|
|
if module_has_submodule(mod, mod_name):
|
|
raise
|
|
|
|
|
|
class HorizonSite(Site):
|
|
"""
|
|
A singleton implementation of Site such that all dealings with horizon
|
|
get the same instance no matter what. There can be only one.
|
|
"""
|
|
_instance = None
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
if not cls._instance:
|
|
cls._instance = super(Site, cls).__new__(cls, *args, **kwargs)
|
|
return cls._instance
|
|
|
|
|
|
# The one true Horizon
|
|
Horizon = HorizonSite()
|