diff --git a/horizon/base.py b/horizon/base.py index 0ae4b6fb50..a13375549f 100644 --- a/horizon/base.py +++ b/horizon/base.py @@ -39,26 +39,13 @@ from django.utils.module_loading import module_has_submodule from django.utils.translation import ugettext as _ from horizon import loaders +from horizon import conf from horizon.decorators import require_auth, require_perms, _current_component LOG = logging.getLogger(__name__) -# Default configuration dictionary. Do not mutate directly. Use copy.copy(). -HORIZON_CONFIG = { - # Allow for ordering dashboards; list or tuple if provided. - 'dashboards': None, - # Name of a default dashboard; defaults to first alphabetically if None - 'default_dashboard': None, - # Default redirect url for users' home - 'user_home': settings.LOGIN_REDIRECT_URL, - 'exceptions': {'unauthorized': [], - 'not_found': [], - 'recoverable': []} -} - - def _decorate_urlconf(urlpatterns, decorator, *args, **kwargs): for pattern in urlpatterns: if getattr(pattern, 'callback', None): @@ -591,9 +578,7 @@ class Site(Registry, HorizonComponent): @property def _conf(self): - conf = copy.copy(HORIZON_CONFIG) - conf.update(getattr(settings, 'HORIZON_CONFIG', {})) - return conf + return conf.HORIZON_CONFIG @property def dashboards(self): @@ -633,11 +618,11 @@ class Site(Registry, HorizonComponent): """ Returns an ordered tuple of :class:`~horizon.Dashboard` modules. Orders dashboards according to the ``"dashboards"`` key in - ``settings.HORIZON_CONFIG`` or else returns all registered dashboards + ``HORIZON_CONFIG`` or else returns all registered dashboards in alphabetical order. Any remaining :class:`~horizon.Dashboard` classes registered with - Horizon but not listed in ``settings.HORIZON_CONFIG['dashboards']`` + Horizon but not listed in ``HORIZON_CONFIG['dashboards']`` will be appended to the end of the list alphabetically. """ if self.dashboards: @@ -660,7 +645,7 @@ class Site(Registry, HorizonComponent): def get_default_dashboard(self): """ Returns the default :class:`~horizon.Dashboard` instance. - If ``"default_dashboard"`` is specified in ``settings.HORIZON_CONFIG`` + 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. """ @@ -680,7 +665,7 @@ class Site(Registry, HorizonComponent): 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 ``settings.HORIZON_CONFIG``. Each of these + the ``"user_home"`` key in ``HORIZON_CONFIG``. Each of these would be valid:: {"user_home": "/home",} # A URL @@ -741,9 +726,8 @@ class Site(Registry, HorizonComponent): dash._autodiscover() # Allow for override modules - config = getattr(settings, "HORIZON_CONFIG", {}) - if config.get("customization_module", None): - customization_module = config["customization_module"] + if self._conf.get("customization_module", None): + customization_module = self._conf["customization_module"] bits = customization_module.split('.') mod_name = bits.pop() package = '.'.join(bits) diff --git a/horizon/conf/__init__.py b/horizon/conf/__init__.py index e69de29bb2..ad5d51dad6 100644 --- a/horizon/conf/__init__.py +++ b/horizon/conf/__init__.py @@ -0,0 +1,35 @@ +import copy + +from django.utils.functional import LazyObject, empty + +from .default import HORIZON_CONFIG as DEFAULT_CONFIG + + +class LazySettings(LazyObject): + def _setup(self, name=None): + from django.conf import settings + HORIZON_CONFIG = copy.copy(DEFAULT_CONFIG) + HORIZON_CONFIG.update(settings.HORIZON_CONFIG) + + # Ensure we always have our exception configuration... + for exc_category in ['unauthorized', 'not_found', 'recoverable']: + if exc_category not in HORIZON_CONFIG['exceptions']: + default_exc_config = DEFAULT_CONFIG['exceptions'][exc_category] + HORIZON_CONFIG['exceptions'][exc_category] = default_exc_config + + # Ensure our password validator always exists... + if 'regex' not in HORIZON_CONFIG['password_validator']: + default_pw_regex = DEFAULT_CONFIG['password_validator']['regex'] + HORIZON_CONFIG['password_validator']['regex'] = default_pw_regex + if 'help_text' not in HORIZON_CONFIG['password_validator']: + default_pw_help = DEFAULT_CONFIG['password_validator']['help_text'] + HORIZON_CONFIG['password_validator']['help_text'] = default_pw_help + + self._wrapped = HORIZON_CONFIG + + def __getitem__(self, name, fallback=None): + if self._wrapped is empty: + self._setup(name) + return self._wrapped.get(name, fallback) + +HORIZON_CONFIG = LazySettings() diff --git a/horizon/conf/default.py b/horizon/conf/default.py new file mode 100644 index 0000000000..869086f4c5 --- /dev/null +++ b/horizon/conf/default.py @@ -0,0 +1,30 @@ +from django.conf import settings +from django.utils.translation import ugettext as _ + +# Default configuration dictionary. Do not mutate. +HORIZON_CONFIG = { + # Allow for ordering dashboards; list or tuple if provided. + 'dashboards': None, + + # Name of a default dashboard; defaults to first alphabetically if None + 'default_dashboard': None, + + # Default redirect url for users' home + 'user_home': settings.LOGIN_REDIRECT_URL, + + # AJAX settings for JavaScript + 'ajax_queue_limit': 10, + 'ajax_poll_interval': 2500, + + # URL for additional help with this site. + 'help_url': None, + + # Exception configuration. + 'exceptions': {'unauthorized': [], + 'not_found': [], + 'recoverable': []}, + + # Password configuration. + 'password_validator': {'regex': '.*', + 'help_text': _("Password is not accepted")} +} diff --git a/horizon/context_processors.py b/horizon/context_processors.py index 8e65db2f09..f5d2ecfdc6 100644 --- a/horizon/context_processors.py +++ b/horizon/context_processors.py @@ -21,7 +21,7 @@ Context processors used by Horizon. """ -from django.conf import settings +from horizon import conf def horizon(request): @@ -37,7 +37,7 @@ def horizon(request): for each template/template fragment which takes context that is used to render the complete output. """ - context = {"HORIZON_CONFIG": getattr(settings, "HORIZON_CONFIG", {}), + context = {"HORIZON_CONFIG": conf.HORIZON_CONFIG, "True": True, "False": False} diff --git a/horizon/exceptions.py b/horizon/exceptions.py index 91c1118ff3..313f0c5ac9 100644 --- a/horizon/exceptions.py +++ b/horizon/exceptions.py @@ -22,7 +22,6 @@ import logging import os import sys -from django.conf import settings from django.contrib.auth import logout from django.http import HttpRequest from django.utils import termcolors @@ -30,6 +29,7 @@ from django.utils.translation import ugettext as _ from django.views.debug import SafeExceptionReporterFilter, CLEANSED_SUBSTITUTE from horizon import messages +from horizon.conf import HORIZON_CONFIG LOG = logging.getLogger(__name__) PALETTE = termcolors.PALETTES[termcolors.DEFAULT_PALETTE] @@ -194,12 +194,10 @@ class HandledException(HorizonException): self.wrapped = wrapped -HORIZON_CONFIG = getattr(settings, "HORIZON_CONFIG", {}) -EXCEPTION_CONFIG = HORIZON_CONFIG.get("exceptions", {}) -UNAUTHORIZED = tuple(EXCEPTION_CONFIG.get('unauthorized', [])) -NOT_FOUND = tuple(EXCEPTION_CONFIG.get('not_found', [])) +UNAUTHORIZED = tuple(HORIZON_CONFIG['exceptions']['unauthorized']) +NOT_FOUND = tuple(HORIZON_CONFIG['exceptions']['not_found']) RECOVERABLE = (AlreadyExists,) -RECOVERABLE += tuple(EXCEPTION_CONFIG.get('recoverable', [])) +RECOVERABLE += tuple(HORIZON_CONFIG['exceptions']['recoverable']) def error_color(msg): diff --git a/horizon/static/horizon/js/horizon.communication.js b/horizon/static/horizon/js/horizon.communication.js index 74baf780fa..f1116d1eeb 100644 --- a/horizon/static/horizon/js/horizon.communication.js +++ b/horizon/static/horizon/js/horizon.communication.js @@ -2,7 +2,7 @@ * * Note: The number of concurrent AJAX connections hanlded in the queue * can be configured by setting an "ajax_queue_limit" key in - * settings.HORIZON_CONFIG to the desired number (or None to disable queue + * HORIZON_CONFIG to the desired number (or None to disable queue * limiting). */ horizon.ajax = { diff --git a/horizon/tables/base.py b/horizon/tables/base.py index 600d797c10..7c95c2d8d5 100644 --- a/horizon/tables/base.py +++ b/horizon/tables/base.py @@ -35,6 +35,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.safestring import mark_safe from django.utils import termcolors +from horizon import conf from horizon import exceptions from horizon import messages from horizon.utils import html @@ -359,7 +360,7 @@ class Row(html.HTMLElement): lookup versus the table's "list" lookup). The automatic update interval is configurable by setting the key - ``ajax_poll_interval`` in the ``settings.HORIZON_CONFIG`` dictionary. + ``ajax_poll_interval`` in the ``HORIZON_CONFIG`` dictionary. Default: ``2500`` (measured in milliseconds). .. attribute:: table @@ -452,7 +453,7 @@ class Row(html.HTMLElement): self.cells = SortedDict(cells) if self.ajax: - interval = settings.HORIZON_CONFIG.get('ajax_poll_interval', 2500) + interval = conf.HORIZON_CONFIG['ajax_poll_interval'] self.attrs['data-update-interval'] = interval self.attrs['data-update-url'] = self.get_ajax_update_url() self.classes.append("ajax-update") diff --git a/horizon/utils/validators.py b/horizon/utils/validators.py index 8ea1350ce8..143375a323 100644 --- a/horizon/utils/validators.py +++ b/horizon/utils/validators.py @@ -14,12 +14,9 @@ # License for the specific language governing permissions and limitations # under the License. -from django.conf import settings from django.core.exceptions import ValidationError -from django.utils.translation import ugettext as _ -horizon_config = getattr(settings, "HORIZON_CONFIG", {}) -password_config = horizon_config.get("password_validator", {}) +from horizon import conf def validate_port_range(port): @@ -28,8 +25,8 @@ def validate_port_range(port): def password_validator(): - return password_config.get("regex", ".*") + return conf.HORIZON_CONFIG["password_validator"]["regex"] def password_validator_msg(): - return password_config.get("help_text", _("Password is not accepted")) + return conf.HORIZON_CONFIG["password_validator"]["help_text"] diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 68a0d2f21d..822645753a 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -183,7 +183,8 @@ def novaclient(request): request.user.token.id, project_id=request.user.tenant_id, auth_url=url_for(request, 'compute'), - insecure=insecure) + insecure=insecure, + http_log_debug=settings.DEBUG) c.client.auth_token = request.user.token.id c.client.management_url = url_for(request, 'compute') return c diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index d841e6a2b4..e9918cdc62 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -2,6 +2,8 @@ import os from django.utils.translation import ugettext_lazy as _ +from openstack_dashboard import exceptions + DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -12,14 +14,23 @@ TEMPLATE_DEBUG = DEBUG # https://docs.djangoproject.com/en/1.4/ref/settings/#secure-proxy-ssl-header # SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') +# Default OpenStack Dashboard configuration. +HORIZON_CONFIG = { + 'dashboards': ('project', 'admin', 'settings',), + 'default_dashboard': 'project', + 'user_home': 'openstack_dashboard.views.get_user_home', + 'ajax_queue_limit': 10, + 'help_url': "http://docs.openstack.org", + 'exceptions': {'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED}, +} + # Specify a regular expression to validate user passwords. -# HORIZON_CONFIG = { -# "password_validator": { -# "regex": '.*', -# "help_text": _("Your password does not meet the requirements.") -# }, -# 'help_url': "http://docs.openstack.org" -# } +# HORIZON_CONFIG["password_validator"] = { +# "regex": '.*', +# "help_text": _("Your password does not meet the requirements.") +# }, LOCAL_PATH = os.path.dirname(os.path.abspath(__file__)) diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index d03d268886..ddbb38ca95 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -16,7 +16,6 @@ SECRET_KEY = generate_or_read_from_file(os.path.join(TEST_DIR, ROOT_URLCONF = 'openstack_dashboard.urls' TEMPLATE_DIRS = ( os.path.join(TEST_DIR, 'templates'), - #os.path.join(ROOT_PATH, 'templates'), ) TEMPLATE_CONTEXT_PROCESSORS += ( diff --git a/openstack_dashboard/test/test_data/exceptions.py b/openstack_dashboard/test/test_data/exceptions.py index 9b9583eb51..9f58692033 100644 --- a/openstack_dashboard/test/test_data/exceptions.py +++ b/openstack_dashboard/test/test_data/exceptions.py @@ -25,7 +25,7 @@ from .utils import TestDataContainer def create_stubbed_exception(cls, status_code=500): msg = "Expected failure." - def fake_init_exception(self, code, message): + def fake_init_exception(self, code, message, **kwargs): self.code = code self.message = message