horizon-13.0.0/0000775000175100017510000000000013245512211013303 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/0000775000175100017510000000000013245512210014772 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/0000775000175100017510000000000013245512210015717 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/dash_template/0000775000175100017510000000000013245512210020531 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/dash_template/templates/0000775000175100017510000000000013245512210022527 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/dash_template/templates/dash_name/0000775000175100017510000000000013245512210024446 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/dash_template/templates/dash_name/base.html0000666000175100017510000000044213245511643026261 0ustar zuulzuul00000000000000{% load horizon %}{% jstemplate %}[% extends 'base.html' %] [% block sidebar %] [% include 'horizon/common/_sidebar.html' %] [% endblock %] [% block main %] [% include "horizon/_messages.html" %] [% block {{ dash_name }}_main %][% endblock %] [% endblock %] {% endjstemplate %} horizon-13.0.0/horizon/conf/dash_template/dashboard.py.tmpl0000666000175100017510000000160713245511643024024 0ustar zuulzuul00000000000000# 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. from django.utils.translation import ugettext_lazy as _ import horizon class {{ dash_name|title }}(horizon.Dashboard): name = _("{{ dash_name|title }}") slug = "{{ dash_name|slugify }}" panels = () # Add your panels here. default_panel = '' # Specify the slug of the dashboard's default panel. horizon.register({{ dash_name|title }}) horizon-13.0.0/horizon/conf/dash_template/static/0000775000175100017510000000000013245512210022020 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/dash_template/static/dash_name/0000775000175100017510000000000013245512210023737 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/dash_template/static/dash_name/js/0000775000175100017510000000000013245512210024353 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/dash_template/static/dash_name/js/dash_name.js0000666000175100017510000000006113245511643026640 0ustar zuulzuul00000000000000/* Additional JavaScript for {{ dash_name }}. */ horizon-13.0.0/horizon/conf/dash_template/static/dash_name/scss/0000775000175100017510000000000013245512210024712 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/dash_template/static/dash_name/scss/dash_name.scss0000666000175100017510000000005313245511643027537 0ustar zuulzuul00000000000000/* Additional SCSS for {{ dash_name }}. */ horizon-13.0.0/horizon/conf/dash_template/__init__.py0000666000175100017510000000000013245511643022643 0ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/default.py0000666000175100017510000000333113245511643017730 0ustar zuulzuul00000000000000# 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. from django.conf import settings from django.utils.translation import ugettext_lazy 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 reporting issue with this site. 'bug_url': None, # 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")}, 'password_autocomplete': 'off', # Enable or disable simplified floating IP address management. 'simple_ip_management': True, 'integration_tests_support': getattr(settings, 'INTEGRATION_TESTS_SUPPORT', False) } horizon-13.0.0/horizon/conf/panel_template/0000775000175100017510000000000013245512210020711 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/panel_template/tests.py.tmpl0000666000175100017510000000134113245511643023412 0ustar zuulzuul00000000000000# 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. from horizon.test import helpers as test class {{ panel_name|title }}Tests(test.TestCase): # Unit tests for {{ panel_name }}. def test_me(self): self.assertTrue(1 + 1 == 2) horizon-13.0.0/horizon/conf/panel_template/views.py0000666000175100017510000000150513245511643022434 0ustar zuulzuul00000000000000# 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. from horizon import views class IndexView(views.APIView): # A very simple class-based view... template_name = '{{ dash_name }}/{{ panel_name }}/index.html' def get_data(self, request, context, *args, **kwargs): # Add data to the context here... return context horizon-13.0.0/horizon/conf/panel_template/templates/0000775000175100017510000000000013245512210022707 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/panel_template/templates/panel_name/0000775000175100017510000000000013245512210025006 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/panel_template/templates/panel_name/index.html0000666000175100017510000000052413245511643027017 0ustar zuulzuul00000000000000{% load horizon %}{% jstemplate %}[% extends 'base.html' %] [% load i18n %] [% block title %][% trans "{{ panel_name|title }}" %][% endblock %] [% block page_header %] [% include "horizon/common/_page_header.html" with title=_("{{ panel_name|title }}") %] [% endblock page_header %] [% block main %] [% endblock %] {% endjstemplate %} horizon-13.0.0/horizon/conf/panel_template/urls.py.tmpl0000666000175100017510000000130413245511643023234 0ustar zuulzuul00000000000000# 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. from django.conf.urls import url from {{ dash_path }}.{{ panel_name }} import views urlpatterns = [ url(r'^$', views.IndexView.as_view(), name='index'), ] horizon-13.0.0/horizon/conf/panel_template/panel.py.tmpl0000666000175100017510000000161013245511643023346 0ustar zuulzuul00000000000000# 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. from django.utils.translation import ugettext_lazy as _ import horizon {% if dashboard %}from {{ dash_path }} import dashboard{% endif %} class {{ panel_name|title }}(horizon.Panel): name = _("{{ panel_name|title }}") slug = "{{ panel_name|slugify }}" {% if dashboard %} dashboard.{{ dash_name|title }}.register({{ panel_name|title }}){% endif %} horizon-13.0.0/horizon/conf/panel_template/__init__.py0000666000175100017510000000000013245511643023023 0ustar zuulzuul00000000000000horizon-13.0.0/horizon/conf/__init__.py0000666000175100017510000000425313245511643020047 0ustar zuulzuul00000000000000# 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.utils.functional import empty from django.utils.functional import LazyObject from django.utils.functional import SimpleLazyObject class LazySettings(LazyObject): def _setup(self, name=None): from django.conf import settings from horizon.conf.default import HORIZON_CONFIG as DEFAULT_CONFIG 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) def get_webroot(): from django.conf import settings return settings.WEBROOT HORIZON_CONFIG = LazySettings() WEBROOT = SimpleLazyObject(get_webroot) horizon-13.0.0/horizon/messages.py0000666000175100017510000000653113245511643017173 0ustar zuulzuul00000000000000# 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. """ Drop-in replacement for django.contrib.messages which handles Horizon's messaging needs (e.g. AJAX communication, etc.). """ from django.contrib import messages as _messages from django.contrib.messages import constants from django.utils.encoding import force_text from django.utils.safestring import SafeData def horizon_message_already_queued(request, message): _message = force_text(message) if request.is_ajax(): for tag, msg, extra in request.horizon['async_messages']: if _message == msg: return True else: for msg in _messages.get_messages(request)._queued_messages: if msg.message == _message: return True return False def add_message(request, level, message, extra_tags='', fail_silently=False): """Attempts to add a message to the request using the 'messages' app.""" if not horizon_message_already_queued(request, message): if request.is_ajax(): tag = constants.DEFAULT_TAGS[level] # if message is marked as safe, pass "safe" tag as extra_tags so # that client can skip HTML escape for the message when rendering if isinstance(message, SafeData): extra_tags = extra_tags + ' safe' request.horizon['async_messages'].append([tag, force_text(message), extra_tags]) else: return _messages.add_message(request, level, message, extra_tags, fail_silently) def debug(request, message, extra_tags='', fail_silently=False): """Adds a message with the ``DEBUG`` level.""" add_message(request, constants.DEBUG, message, extra_tags=extra_tags, fail_silently=fail_silently) def info(request, message, extra_tags='', fail_silently=False): """Adds a message with the ``INFO`` level.""" add_message(request, constants.INFO, message, extra_tags=extra_tags, fail_silently=fail_silently) def success(request, message, extra_tags='', fail_silently=False): """Adds a message with the ``SUCCESS`` level.""" add_message(request, constants.SUCCESS, message, extra_tags=extra_tags, fail_silently=fail_silently) def warning(request, message, extra_tags='', fail_silently=False): """Adds a message with the ``WARNING`` level.""" add_message(request, constants.WARNING, message, extra_tags=extra_tags, fail_silently=fail_silently) def error(request, message, extra_tags='', fail_silently=False): """Adds a message with the ``ERROR`` level.""" add_message(request, constants.ERROR, message, extra_tags=extra_tags, fail_silently=fail_silently) horizon-13.0.0/horizon/contrib/0000775000175100017510000000000013245512210016432 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/contrib/bootstrap_datepicker.py0000666000175100017510000000306013245511643023226 0ustar zuulzuul00000000000000# Copyright 2014 IBM Corp. # # 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. # Map Horizon languages to datepicker locales LOCALE_MAPPING = { 'ar': 'ar', 'az': 'az', 'bg': 'bg', 'ca': 'ca', 'cs': 'cs', 'cy': 'cy', 'da': 'da', 'de': 'de', 'el': 'el', 'es': 'es', 'et': 'et', 'fa': 'fa', 'fi': 'fi', 'fr': 'fr', 'gl': 'gl', 'he': 'he', 'hr': 'hr', 'hu': 'hu', 'id': 'id', 'is': 'is', 'it': 'it', 'ja': 'ja', 'ka': 'ka', 'kk': 'kk', 'ko': 'kr', # difference between horizon and datepicker 'lt': 'lt', 'lv': 'lv', 'mk': 'mk', 'ms': 'ms', 'nb': 'nb', 'nl-be': 'nl-BE', 'nl': 'nl', 'no': 'no', 'pl': 'pl', 'pt-br': 'pt-BR', 'pt': 'pt', 'ro': 'ro', 'rs-latin': 'rs-latin', 'sr': 'rs', # difference between horizon and datepicker 'ru': 'ru', 'sk': 'sk', 'sl': 'sl', 'sq': 'sq', 'sv': 'sv', 'sw': 'sw', 'th': 'th', 'tr': 'tr', 'ua': 'ua', 'vi': 'vi', 'zh-cn': 'zh-CN', 'zh-tw': 'zh-TW', } horizon-13.0.0/horizon/contrib/staticfiles/0000775000175100017510000000000013245512210020744 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/contrib/staticfiles/finders.py0000666000175100017510000000322313245511643022763 0ustar zuulzuul00000000000000# Copyright 2016 IBM Corp. # # 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 os from django.apps import apps from django.contrib.staticfiles.finders import AppDirectoriesFinder class HorizonStaticFinder(AppDirectoriesFinder): """Static files finder that also looks into the directory of each panel.""" def __init__(self, app_names=None, *args, **kwargs): super(HorizonStaticFinder, self).__init__(*args, **kwargs) app_configs = apps.get_app_configs() for app_config in app_configs: if 'openstack_dashboard' in app_config.path: for panel in os.listdir(app_config.path): panel_path = os.path.join(app_config.path, panel) if os.path.isdir(panel_path) and panel != self.source_dir: # Look for the static folder static_path = os.path.join(panel_path, self.source_dir) if os.path.isdir(static_path): panel_name = app_config.name + panel app_storage = self.storage_class(static_path) self.storages[panel_name] = app_storage horizon-13.0.0/horizon/contrib/staticfiles/__init__.py0000666000175100017510000000000013245511643023056 0ustar zuulzuul00000000000000horizon-13.0.0/horizon/contrib/__init__.py0000666000175100017510000000000013245511643020544 0ustar zuulzuul00000000000000horizon-13.0.0/horizon/tables/0000775000175100017510000000000013245512210016244 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/tables/actions.py0000666000175100017510000011744613245511643020306 0ustar zuulzuul00000000000000# 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. from collections import defaultdict from collections import OrderedDict import copy import functools import logging import types from django.conf import settings from django.core import urlresolvers from django import shortcuts from django.template.loader import render_to_string from django.utils.functional import Promise from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy import six from horizon import exceptions from horizon import messages from horizon.utils import functions from horizon.utils import html from horizon.utils import settings as utils_settings LOG = logging.getLogger(__name__) # For Bootstrap integration; can be overridden in settings. ACTION_CSS_CLASSES = () STRING_SEPARATOR = "__" class BaseActionMetaClass(type): """Metaclass for adding all actions options from inheritance tree to action. This way actions can inherit from each other but still use the class attributes DSL. Meaning, all attributes of Actions are defined as class attributes, but in the background, it will be used as parameters for the initializer of the object. The object is then initialized clean way. Similar principle is used in DataTableMetaclass. """ def __new__(mcs, name, bases, attrs): # Options of action are set as class attributes, loading them. options = {} if attrs: options = attrs # Iterate in reverse to preserve final order for base in bases[::-1]: # It actually throws all super classes away except immediate # superclass. But it's fine, immediate super-class base_options # includes everything because superclasses was created also by # this metaclass. Same principle is used in DataTableMetaclass. if hasattr(base, 'base_options') and base.base_options: base_options = {} # Updating options by superclasses. base_options.update(base.base_options) # Updating superclass options by actual class options. base_options.update(options) options = base_options # Saving all options to class attribute, this will be used for # instantiating of the specific Action. attrs['base_options'] = options return type.__new__(mcs, name, bases, attrs) def __call__(cls, *args, **kwargs): cls.base_options.update(kwargs) # Adding cls.base_options to each init call. klass = super(BaseActionMetaClass, cls).__call__( *args, **cls.base_options) return klass @six.add_metaclass(BaseActionMetaClass) class BaseAction(html.HTMLElement): """Common base class for all ``Action`` classes.""" def __init__(self, **kwargs): super(BaseAction, self).__init__() self.datum = kwargs.get('datum', None) self.table = kwargs.get('table', None) self.handles_multiple = kwargs.get('handles_multiple', False) self.requires_input = kwargs.get('requires_input', False) self.preempt = kwargs.get('preempt', False) self.policy_rules = kwargs.get('policy_rules', None) self.action_type = kwargs.get('action_type', 'default') def data_type_matched(self, datum): """Method to see if the action is allowed for a certain type of data. Only affects mixed data type tables. """ if datum: action_data_types = getattr(self, "allowed_data_types", []) # If the data types of this action is empty, we assume it accepts # all kinds of data and this method will return True. if action_data_types: datum_type = getattr(datum, self.table._meta.data_type_name, None) if datum_type and (datum_type not in action_data_types): return False return True def get_policy_target(self, request, datum): """Provide the target for a policy request. This method is meant to be overridden to return target details when one of the policy checks requires them. E.g., {"user_id": datum.id} """ return {} def allowed(self, request, datum): """Determine whether this action is allowed for the current request. This method is meant to be overridden with more specific checks. """ return True def _allowed(self, request, datum): policy_check = utils_settings.import_setting("POLICY_CHECK_FUNCTION") if policy_check and self.policy_rules: target = self.get_policy_target(request, datum) return (policy_check(self.policy_rules, request, target) and self.allowed(request, datum)) return self.allowed(request, datum) def update(self, request, datum): """Allows per-action customization based on current conditions. This is particularly useful when you wish to create a "toggle" action that will be rendered differently based on the value of an attribute on the current row's data. By default this method is a no-op. """ pass def get_default_classes(self): """Returns a list of the default classes for the action. Defaults to ``["btn", "btn-default", "btn-sm"]``. """ return getattr(settings, "ACTION_CSS_CLASSES", ACTION_CSS_CLASSES) def get_default_attrs(self): """Returns a list of the default HTML attributes for the action. Defaults to returning an ``id`` attribute with the value ``{{ table.name }}__action_{{ action.name }}__{{ creation counter }}``. """ if self.datum is not None: bits = (self.table.name, "row_%s" % self.table.get_object_id(self.datum), "action_%s" % self.name) else: bits = (self.table.name, "action_%s" % self.name) return {"id": STRING_SEPARATOR.join(bits)} def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.name) def associate_with_table(self, table): self.table = table class Action(BaseAction): """Represents an action which can be taken on this table's data. .. attribute:: name Required. The short name or "slug" representing this action. This name should not be changed at runtime. .. attribute:: verbose_name A descriptive name used for display purposes. Defaults to the value of ``name`` with the first letter of each word capitalized. .. attribute:: verbose_name_plural Used like ``verbose_name`` in cases where ``handles_multiple`` is ``True``. Defaults to ``verbose_name`` with the letter "s" appended. .. attribute:: method The HTTP method for this action. Defaults to ``POST``. Other methods may or may not succeed currently. .. attribute:: requires_input Boolean value indicating whether or not this action can be taken without any additional input (e.g. an object id). Defaults to ``True``. .. attribute:: preempt Boolean value indicating whether this action should be evaluated in the period after the table is instantiated but before the data has been loaded. This can allow actions which don't need access to the full table data to bypass any API calls and processing which would otherwise be required to load the table. .. attribute:: allowed_data_types A list that contains the allowed data types of the action. If the datum's type is in this list, the action will be shown on the row for the datum. Default to be an empty list (``[]``). When set to empty, the action will accept any kind of data. .. attribute:: policy_rules list of scope and rule tuples to do policy checks on, the composition of which is (scope, rule) * scope: service type managing the policy for action * rule: string representing the action to be checked .. code-block:: python for a policy that requires a single rule check: policy_rules should look like "(("compute", "compute:create_instance"),)" for a policy that requires multiple rule checks: rules should look like "(("identity", "identity:list_users"), ("identity", "identity:list_roles"))" At least one of the following methods must be defined: .. method:: single(self, data_table, request, object_id) Handler for a single-object action. .. method:: multiple(self, data_table, request, object_ids) Handler for multi-object actions. .. method:: handle(self, data_table, request, object_ids) If a single function can work for both single-object and multi-object cases then simply providing a ``handle`` function will internally route both ``single`` and ``multiple`` requests to ``handle`` with the calls from ``single`` being transformed into a list containing only the single object id. """ def __init__(self, single_func=None, multiple_func=None, handle_func=None, attrs=None, **kwargs): super(Action, self).__init__(**kwargs) self.method = kwargs.get('method', "POST") self.requires_input = kwargs.get('requires_input', True) self.verbose_name = kwargs.get('verbose_name', self.name.title()) self.verbose_name_plural = kwargs.get('verbose_name_plural', "%ss" % self.verbose_name) self.allowed_data_types = kwargs.get('allowed_data_types', []) self.icon = kwargs.get('icon', None) if attrs: self.attrs.update(attrs) # Don't set these if they're None if single_func: self.single = single_func if multiple_func: self.multiple = multiple_func if handle_func: self.handle = handle_func # Ensure we have the appropriate methods has_handler = hasattr(self, 'handle') and callable(self.handle) has_single = hasattr(self, 'single') and callable(self.single) has_multiple = hasattr(self, 'multiple') and callable(self.multiple) if has_handler or has_multiple: self.handles_multiple = True if not has_handler and (not has_single or has_multiple): cls_name = self.__class__.__name__ raise NotImplementedError('You must define either a "handle" ' 'method or a "single" or "multiple" ' 'method on %s.' % cls_name) if not has_single: def single(self, data_table, request, object_id): return self.handle(data_table, request, [object_id]) self.single = types.MethodType(single, self) if not has_multiple and self.handles_multiple: def multiple(self, data_table, request, object_ids): return self.handle(data_table, request, object_ids) self.multiple = types.MethodType(multiple, self) def get_param_name(self): """Returns the full POST parameter name for this action. Defaults to ``{{ table.name }}__{{ action.name }}``. """ return "__".join([self.table.name, self.name]) class LinkAction(BaseAction): """A table action which is simply a link rather than a form POST. .. attribute:: name Required. The short name or "slug" representing this action. This name should not be changed at runtime. .. attribute:: verbose_name A string which will be rendered as the link text. (Required) .. attribute:: url A string or a callable which resolves to a url to be used as the link target. You must either define the ``url`` attribute or override the ``get_link_url`` method on the class. .. attribute:: allowed_data_types A list that contains the allowed data types of the action. If the datum's type is in this list, the action will be shown on the row for the datum. Defaults to be an empty list (``[]``). When set to empty, the action will accept any kind of data. """ # class attribute name is used for ordering of Actions in table name = "link" ajax = False def __init__(self, attrs=None, **kwargs): super(LinkAction, self).__init__(**kwargs) self.method = kwargs.get('method', "GET") self.bound_url = kwargs.get('bound_url', None) self.name = kwargs.get('name', self.name) self.verbose_name = kwargs.get('verbose_name', self.name.title()) self.url = kwargs.get('url', None) self.allowed_data_types = kwargs.get('allowed_data_types', []) self.icon = kwargs.get('icon', None) self.kwargs = kwargs self.action_type = kwargs.get('action_type', 'default') if not kwargs.get('verbose_name', None): raise NotImplementedError('A LinkAction object must have a ' 'verbose_name attribute.') if attrs: self.attrs.update(attrs) if self.ajax: self.classes = list(self.classes) + ['ajax-update'] def get_ajax_update_url(self): table_url = self.table.get_absolute_url() params = urlencode( OrderedDict([("action", self.name), ("table", self.table.name)]) ) return "%s?%s" % (table_url, params) def render(self, **kwargs): action_dict = copy.copy(kwargs) action_dict.update({"action": self, "is_single": True}) return render_to_string("horizon/common/_data_table_action.html", action_dict) def associate_with_table(self, table): super(LinkAction, self).associate_with_table(table) if self.ajax: self.attrs['data-update-url'] = self.get_ajax_update_url() def get_link_url(self, datum=None): """Returns the final URL based on the value of ``url``. If ``url`` is callable it will call the function. If not, it will then try to call ``reverse`` on ``url``. Failing that, it will simply return the value of ``url`` as-is. When called for a row action, the current row data object will be passed as the first parameter. """ if not self.url: raise NotImplementedError('A LinkAction class must have a ' 'url attribute or define its own ' 'get_link_url method.') if callable(self.url): return self.url(datum, **self.kwargs) try: if datum: obj_id = self.table.get_object_id(datum) return urlresolvers.reverse(self.url, args=(obj_id,)) else: return urlresolvers.reverse(self.url) except urlresolvers.NoReverseMatch as ex: LOG.info('No reverse found for "%(url)s": %(exception)s', {'url': self.url, 'exception': ex}) return self.url class FilterAction(BaseAction): """A base class representing a filter action for a table. .. attribute:: name The short name or "slug" representing this action. Defaults to ``"filter"``. .. attribute:: verbose_name A descriptive name used for display purposes. Defaults to the value of ``name`` with the first letter of each word capitalized. .. attribute:: param_name A string representing the name of the request parameter used for the search term. Default: ``"q"``. .. attribute:: filter_type A string representing the type of this filter. If this is set to ``"server"`` then ``filter_choices`` must also be provided. Default: ``"query"``. .. attribute:: filter_choices Required for server type filters. A tuple of tuples representing the filter options. Tuple composition should evaluate to (string, string, boolean, string, boolean), representing the following: * The first value is the filter parameter. * The second value represents display value. * The third optional value indicates whether or not it should be applied to the API request as an API query attribute. API type filters do not need to be accounted for in the filter method since the API will do the filtering. However, server type filters in general will need to be performed in the filter method. By default this attribute is not provided (``False``). * The fourth optional value is used as help text if provided. The default is ``None`` which means no help text. * The fifth optional value determines whether or not the choice is displayed to users. It defaults to ``True``. This is useful when the choice needs to be displayed conditionally. .. attribute:: needs_preloading If True, the filter function will be called for the initial GET request with an empty ``filter_string``, regardless of the value of ``method``. """ # TODO(gabriel): The method for a filter action should be a GET, # but given the form structure of the table that's currently impossible. # At some future date this needs to be reworked to get the filter action # separated from the table's POST form. # class attribute name is used for ordering of Actions in table name = "filter" def __init__(self, **kwargs): super(FilterAction, self).__init__(**kwargs) self.method = kwargs.get('method', "POST") self.name = kwargs.get('name', self.name) self.verbose_name = kwargs.get('verbose_name', _("Filter")) self.filter_type = kwargs.get('filter_type', "query") self.filter_choices = kwargs.get('filter_choices') self.needs_preloading = kwargs.get('needs_preloading', False) self.param_name = kwargs.get('param_name', 'q') self.icon = "search" if self.filter_type == 'server' and self.filter_choices is None: raise NotImplementedError( 'A FilterAction object with the ' 'filter_type attribute set to "server" must also have a ' 'filter_choices attribute.') def get_param_name(self): """Returns the full query parameter name for this action. Defaults to ``{{ table.name }}__{{ action.name }}__{{ action.param_name }}``. """ return "__".join([self.table.name, self.name, self.param_name]) def assign_type_string(self, table, data, type_string): for datum in data: setattr(datum, table._meta.data_type_name, type_string) def data_type_filter(self, table, data, filter_string): filtered_data = [] for data_type in table._meta.data_types: func_name = "filter_%s_data" % data_type filter_func = getattr(self, func_name, None) if not filter_func and not callable(filter_func): # The check of filter function implementation should happen # in the __init__. However, the current workflow of DataTable # and actions won't allow it. Need to be fixed in the future. cls_name = self.__class__.__name__ raise NotImplementedError( "You must define a %(func_name)s method for %(data_type)s" " data type in %(cls_name)s." % {'func_name': func_name, 'data_type': data_type, 'cls_name': cls_name}) _data = filter_func(table, data, filter_string) self.assign_type_string(table, _data, data_type) filtered_data.extend(_data) return filtered_data def filter(self, table, data, filter_string): """Provides the actual filtering logic. This method must be overridden by subclasses and return the filtered data. """ return data def is_api_filter(self, filter_field): """Determine if agiven filter field should be used as an API filter.""" if self.filter_type == 'server': for choice in self.filter_choices: if (choice[0] == filter_field and len(choice) > 2 and choice[2]): return True return False def get_select_options(self): """Provide the value, string, and help_text for the template to render. help_text is returned if applicable. """ if self.filter_choices: return [choice[:4] for choice in self.filter_choices # Display it If the fifth element is True or does not exist if len(choice) < 5 or choice[4]] class NameFilterAction(FilterAction): """A filter action for name property.""" def filter(self, table, items, filter_string): """Naive case-insensitive search.""" query = filter_string.lower() return [item for item in items if query in item.name.lower()] class FixedFilterAction(FilterAction): """A filter action with fixed buttons.""" def __init__(self, **kwargs): super(FixedFilterAction, self).__init__(**kwargs) self.filter_type = kwargs.get('filter_type', "fixed") self.needs_preloading = kwargs.get('needs_preloading', True) self.fixed_buttons = self.get_fixed_buttons() self.filter_string = '' def filter(self, table, images, filter_string): self.filter_string = filter_string categories = self.categorize(table, images) self.categories = defaultdict(list, categories) for button in self.fixed_buttons: button['count'] = len(self.categories[button['value']]) if not filter_string: return images return self.categories[filter_string] def get_fixed_buttons(self): """Returns a list of dict describing fixed buttons used for filtering. Each list item should be a dict with the following keys: * ``text``: Text to display on the button * ``icon``: Icon class for icon element (inserted before text). * ``value``: Value returned when the button is clicked. This value is passed to ``filter()`` as ``filter_string``. """ return [] def categorize(self, table, rows): """Override to separate rows into categories. To have filtering working properly on the client, each row will need CSS class(es) beginning with 'category-', followed by the value of the fixed button. Return a dict with a key for the value of each fixed button, and a value that is a list of rows in that category. """ return {} class BatchAction(Action): """A table action which takes batch action on one or more objects. This action should not require user input on a per-object basis. .. attribute:: name A short name or "slug" representing this action. Should be one word such as "delete", "add", "disable", etc. .. method:: action_present Method returning a present action name. This is used as an action label. Method must accept an integer/long parameter and return the display forms of the name properly pluralised (depending on the integer) and translated in a string or tuple/list. The returned display form is highly recommended to be a complete action name with a form of a transitive verb and an object noun. Each word is capitalized and the string should be marked as translatable. If tuple or list - then setting self.current_present_action = n will set the current active item from the list(action_present[n]) .. method:: action_past Method returning a past action name. This is usually used to display a message when the action is completed. Method must accept an integer/long parameter and return the display forms of the name properly pluralised (depending on the integer) and translated in a string or tuple/list. The detail is same as that of ``action_present``. .. attribute:: success_url Optional location to redirect after completion of the delete action. Defaults to the current page. .. attribute:: help_text Optional message for providing an appropriate help text for the horizon user. """ help_text = _("This action cannot be undone.") def __init__(self, **kwargs): super(BatchAction, self).__init__(**kwargs) action_present_method = callable(getattr(self, 'action_present', None)) action_past_method = callable(getattr(self, 'action_past', None)) if not action_present_method or not action_past_method: raise NotImplementedError( 'The %s BatchAction class must have both action_past and ' 'action_present methods.' % self.__class__.__name__ ) self.success_url = kwargs.get('success_url', None) # If setting a default name, don't initialize it too early self.verbose_name = kwargs.get('verbose_name', self._get_action_name) self.verbose_name_plural = kwargs.get( 'verbose_name_plural', lambda: self._get_action_name('plural')) self.current_present_action = 0 self.current_past_action = 0 # Keep record of successfully handled objects self.success_ids = [] self.help_text = kwargs.get('help_text', self.help_text) def _allowed(self, request, datum=None): # Override the default internal action method to prevent batch # actions from appearing on tables with no data. if not self.table.data and not datum: return False return super(BatchAction, self)._allowed(request, datum) def _get_action_name(self, items=None, past=False): """Retreive action name based on the number of items and `past` flag. :param items: A list or tuple of items (or container with a __len__ method) to count the number of concerned items for which this method is called. When this method is called for a single item (by the BatchAction itself), this parameter can be omitted and the number of items will be considered as "one". If we want to evaluate to "zero" this parameter must not be omitted (and should be an empty container). :param past: Boolean flag indicating if the action took place in the past. By default a present action is considered. """ action_type = "past" if past else "present" if items is None: # Called without items parameter (by a single instance.) count = 1 else: count = len(items) action_attr = getattr(self, "action_%s" % action_type)(count) if isinstance(action_attr, (six.string_types, Promise)): action = action_attr else: toggle_selection = getattr(self, "current_%s_action" % action_type) action = action_attr[toggle_selection] return action def action(self, request, datum_id): """Accepts a single object id and performs the specific action. This method is required. Return values are discarded, errors raised are caught and logged. """ def update(self, request, datum): """Switches the action verbose name, if needed.""" if getattr(self, 'action_present', False): self.verbose_name = self._get_action_name() self.verbose_name_plural = self._get_action_name('plural') def get_success_url(self, request=None): """Returns the URL to redirect to after a successful action.""" if self.success_url: return self.success_url return request.get_full_path() def get_default_attrs(self): """Returns a list of the default HTML attributes for the action.""" attrs = super(BatchAction, self).get_default_attrs() attrs.update({'data-batch-action': 'true'}) return attrs def handle(self, table, request, obj_ids): action_success = [] action_failure = [] action_not_allowed = [] for datum_id in obj_ids: datum = table.get_object_by_id(datum_id) datum_display = table.get_object_display(datum) or datum_id if not table._filter_action(self, request, datum): action_not_allowed.append(datum_display) LOG.warning(u'Permission denied to %(name)s: "%(dis)s"', { 'name': self._get_action_name(past=True).lower(), 'dis': datum_display }) continue try: self.action(request, datum_id) # Call update to invoke changes if needed self.update(request, datum) action_success.append(datum_display) self.success_ids.append(datum_id) LOG.info(u'%(action)s: "%(datum_display)s"', {'action': self._get_action_name(past=True), 'datum_display': datum_display}) except Exception as ex: handled_exc = isinstance(ex, exceptions.HandledException) if handled_exc: # In case of HandledException, an error message should be # handled in exceptions.handle() or other logic, # so we don't need to handle the error message here. # NOTE(amotoki): To raise HandledException from the logic, # pass escalate=True and do not pass redirect argument # to exceptions.handle(). # If an exception is handled, the original exception object # is stored in ex.wrapped[1]. ex = ex.wrapped[1] else: # Handle the exception but silence it since we'll display # an aggregate error message later. Otherwise we'd get # multiple error messages displayed to the user. action_failure.append(datum_display) action_description = ( self._get_action_name(past=True).lower(), datum_display) LOG.warning( 'Action %(action)s Failed for %(reason)s', { 'action': action_description, 'reason': ex}) # Begin with success message class, downgrade to info if problems. success_message_level = messages.success if action_not_allowed: msg = _('You are not allowed to %(action)s: %(objs)s') params = {"action": self._get_action_name(action_not_allowed).lower(), "objs": functions.lazy_join(", ", action_not_allowed)} messages.error(request, msg % params) success_message_level = messages.info if action_failure: msg = _('Unable to %(action)s: %(objs)s') params = {"action": self._get_action_name(action_failure).lower(), "objs": functions.lazy_join(", ", action_failure)} messages.error(request, msg % params) success_message_level = messages.info if action_success: msg = _('%(action)s: %(objs)s') params = {"action": self._get_action_name(action_success, past=True), "objs": functions.lazy_join(", ", action_success)} success_message_level(request, msg % params) return shortcuts.redirect(self.get_success_url(request)) class DeleteAction(BatchAction): """A table action used to perform delete operations on table data. .. attribute:: name A short name or "slug" representing this action. Defaults to 'delete' .. method:: action_present Method returning a present action name. This is used as an action label. Method must accept an integer/long parameter and return the display forms of the name properly pluralised (depending on the integer) and translated in a string or tuple/list. The returned display form is highly recommended to be a complete action name with a form of a transitive verb and an object noun. Each word is capitalized and the string should be marked as translatable. If tuple or list - then setting self.current_present_action = n will set the current active item from the list(action_present[n]) .. method:: action_past Method returning a past action name. This is usually used to display a message when the action is completed. Method must accept an integer/long parameter and return the display forms of the name properly pluralised (depending on the integer) and translated in a string or tuple/list. The detail is same as that of ``action_present``. .. attribute:: success_url Optional location to redirect after completion of the delete action. Defaults to the current page. .. attribute:: help_text Optional message for providing an appropriate help text for the horizon user. """ name = "delete" def __init__(self, **kwargs): super(DeleteAction, self).__init__(**kwargs) self.name = kwargs.get('name', self.name) self.icon = "trash" self.action_type = "danger" def action(self, request, obj_id): """Action entry point. Overrides base class' action method. Accepts a single object id passing it over to the delete method responsible for the object's destruction. """ return self.delete(request, obj_id) def delete(self, request, obj_id): """Required. Deletes an object referenced by obj_id. Override to provide delete functionality specific to your data. """ class handle_exception_with_detail_message(object): """Decorator to allow special exception handling in BatchAction.action(). An exception from BatchAction.action() or DeleteAction.delete() is normally caught by BatchAction.handle() and BatchAction.handle() displays an aggregated error message. However, there are cases where we would like to provide an error message which explains a failure reason if some exception occurs so that users can understand situation better. This decorator allows us to do this kind of special handling easily. This can be applied to BatchAction.action() and DeleteAction.delete() methods. :param normal_log_message: Log message template when an exception other than ``target_exception`` is detected. Keyword substituion "%(id)s" and "%(exc)s" can be used. :param target_exception: Exception class should be handled specially. If this exception is caught, a log message will be logged using ``target_log_message`` and a user visible will be shown using ``target_user_message``. In this case, an aggregated error message generated by BatchAction.handle() does not include an object which causes this exception. :param target_log_message: Log message template when an exception specified in ``target_exception`` is detected. Keyword substituion "%(id)s" and "%(exc)s" can be used. :param target_user_message: User visible message template when an exception specified in ``target_exception`` is detected. It is recommended to use an internationalized string. Keyword substituion "%(name)s" and "%(exc)s" can be used. :param logger_name: (optional) Logger name to be used. The usual pattern is to pass __name__ of a caller. This allows us to show a module name of a caller in a logged message. """ def __init__(self, normal_log_message, target_exception, target_log_message, target_user_message, logger_name=None): self.logger = logging.getLogger(logger_name or __name__) self.normal_log_message = normal_log_message self.target_exception = target_exception self.target_log_message = target_log_message self.target_user_message = target_user_message def __call__(self, fn): @functools.wraps(fn) def decorated(instance, request, obj_id): try: fn(instance, request, obj_id) except self.target_exception as e: self.logger.info(self.target_log_message, {'id': obj_id, 'exc': e}) obj = instance.table.get_object_by_id(obj_id) name = instance.table.get_object_display(obj) msg = self.target_user_message % {'name': name, 'exc': e} # 'escalate=True' is required to notify the caller # (DeleteAction) of the failure. exceptions.handle() will # raise a wrapped exception of HandledException and BatchAction # will handle it. 'redirect' should not be passed here as # 'redirect' has a priority over 'escalate' argument. exceptions.handle(request, msg, escalate=True) except Exception as e: self.logger.info(self.normal_log_message, {'id': obj_id, 'exc': e}) # NOTE: No exception handling is required here because # BatchAction.handle() does it. What we need to do is # just to re-raise the exception. raise return decorated class Deprecated(type): # TODO(lcastell) Replace class with similar functionality from # oslo_log.versionutils when it's finally added in 11.0 def __new__(meta, name, bases, kwargs): cls = super(Deprecated, meta).__new__(meta, name, bases, kwargs) if name != 'UpdateAction': LOG.warning( "WARNING:The UpdateAction class defined in module '%(mod)s' " "is deprecated as of Newton and may be removed in " "Horizon P (12.0). Class '%(name)s' defined at " "module '%(module)s' shall no longer subclass it.", {'mod': UpdateAction.__module__, 'name': name, 'module': kwargs['__module__']}) return cls @six.add_metaclass(Deprecated) class UpdateAction(object): """**DEPRECATED**: A table action for cell updates by inline editing.""" name = "update" def action(self, request, datum, obj_id, cell_name, new_cell_value): self.update_cell(request, datum, obj_id, cell_name, new_cell_value) @staticmethod def action_present(count): return ungettext_lazy( u"Update Item", u"Update Items", count ) @staticmethod def action_past(count): return ungettext_lazy( u"Updated Item", u"Updated Items", count ) def update_cell(self, request, datum, obj_id, cell_name, new_cell_value): """Method for saving data of the cell. This method must implements saving logic of the inline edited table cell. """ def allowed(self, request, datum, cell): """Determine whether updating is allowed for the current request. This method is meant to be overridden with more specific checks. Data of the row and of the cell are passed to the method. """ return True horizon-13.0.0/horizon/tables/views.py0000666000175100017510000003623113245511643017773 0ustar zuulzuul00000000000000# 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. from collections import defaultdict from django import shortcuts from horizon import views from horizon.templatetags.horizon import has_permissions class MultiTableMixin(object): """A generic mixin which provides methods for handling DataTables.""" data_method_pattern = "get_%s_data" def __init__(self, *args, **kwargs): super(MultiTableMixin, self).__init__(*args, **kwargs) self.table_classes = getattr(self, "table_classes", []) self._data = {} self._tables = {} self._data_methods = defaultdict(list) self.get_data_methods(self.table_classes, self._data_methods) def _get_data_dict(self): if not self._data: for table in self.table_classes: data = [] name = table._meta.name func_list = self._data_methods.get(name, []) for func in func_list: data.extend(func()) self._data[name] = data return self._data def get_data_methods(self, table_classes, methods): for table in table_classes: name = table._meta.name if table._meta.mixed_data_type: for data_type in table._meta.data_types: func = self.check_method_exist(self.data_method_pattern, data_type) if func: type_name = table._meta.data_type_name methods[name].append(self.wrap_func(func, type_name, data_type)) else: func = self.check_method_exist(self.data_method_pattern, name) if func: methods[name].append(func) def wrap_func(self, data_func, type_name, data_type): def final_data(): data = data_func() self.assign_type_string(data, type_name, data_type) return data return final_data def check_method_exist(self, func_pattern="%s", *names): func_name = func_pattern % names func = getattr(self, func_name, None) if not func or not callable(func): cls_name = self.__class__.__name__ raise NotImplementedError("You must define a %s method " "in %s." % (func_name, cls_name)) else: return func def assign_type_string(self, data, type_name, data_type): for datum in data: setattr(datum, type_name, data_type) def get_tables(self): if not self.table_classes: raise AttributeError('You must specify one or more DataTable ' 'classes for the "table_classes" attribute ' 'on %s.' % self.__class__.__name__) if not self._tables: for table in self.table_classes: if not has_permissions(self.request.user, table._meta): continue func_name = "get_%s_table" % table._meta.name table_func = getattr(self, func_name, None) if table_func is None: tbl = table(self.request, **self.kwargs) else: tbl = table_func(self, self.request, **self.kwargs) self._tables[table._meta.name] = tbl return self._tables def get_context_data(self, **kwargs): context = super(MultiTableMixin, self).get_context_data(**kwargs) tables = self.get_tables() for name, table in tables.items(): context["%s_table" % name] = table return context def has_prev_data(self, table): return False def has_more_data(self, table): return False def needs_filter_first(self, table): return False def handle_table(self, table): name = table.name data = self._get_data_dict() self._tables[name].data = data[table._meta.name] self._tables[name].needs_filter_first = \ self.needs_filter_first(table) self._tables[name]._meta.has_more_data = self.has_more_data(table) self._tables[name]._meta.has_prev_data = self.has_prev_data(table) handled = self._tables[name].maybe_handle() return handled def get_server_filter_info(self, request, table=None): if not table: table = self.get_table() filter_action = table._meta._filter_action if filter_action is None or filter_action.filter_type != 'server': return None param_name = filter_action.get_param_name() filter_string = request.POST.get(param_name) filter_string_session = request.session.get(param_name, "") changed = (filter_string is not None and filter_string != filter_string_session) if filter_string is None: filter_string = filter_string_session filter_field_param = param_name + '_field' filter_field = request.POST.get(filter_field_param) filter_field_session = request.session.get(filter_field_param) if filter_field is None and filter_field_session is not None: filter_field = filter_field_session filter_info = { 'action': filter_action, 'value_param': param_name, 'value': filter_string, 'field_param': filter_field_param, 'field': filter_field, 'changed': changed } return filter_info def handle_server_filter(self, request, table=None): """Update the table server filter information in the session. Returns True if the filter has been changed. """ if not table: table = self.get_table() filter_info = self.get_server_filter_info(request, table) if filter_info is None: return False request.session[filter_info['value_param']] = filter_info['value'] if filter_info['field_param']: request.session[filter_info['field_param']] = filter_info['field'] return filter_info['changed'] def update_server_filter_action(self, request, table=None): """Update the table server side filter action. It is done based on the current filter. The filter info may be stored in the session and this will restore it. """ if not table: table = self.get_table() filter_info = self.get_server_filter_info(request, table) if filter_info is not None: action = filter_info['action'] setattr(action, 'filter_string', filter_info['value']) if filter_info['field_param']: setattr(action, 'filter_field', filter_info['field']) class MultiTableView(MultiTableMixin, views.HorizonTemplateView): """Generic view to handle multiple DataTable classes in a single view. Each DataTable class must be a :class:`~horizon.tables.DataTable` class or its subclass. Three steps are required to use this view: set the ``table_classes`` attribute with a tuple of the desired :class:`~horizon.tables.DataTable` classes; define a ``get_{{ table_name }}_data`` method for each table class which returns a set of data for that table; and specify a template for the ``template_name`` attribute. """ def construct_tables(self): tables = self.get_tables().values() # Early out before data is loaded for table in tables: preempted = table.maybe_preempt() if preempted: return preempted # Load data into each table and check for action handlers for table in tables: handled = self.handle_table(table) if handled: return handled # If we didn't already return a response, returning None continues # with the view as normal. return None def get(self, request, *args, **kwargs): handled = self.construct_tables() if handled: return handled context = self.get_context_data(**kwargs) return self.render_to_response(context) def post(self, request, *args, **kwargs): # GET and POST handling are the same return self.get(request, *args, **kwargs) class DataTableView(MultiTableView): """A class-based generic view to handle basic DataTable processing. Three steps are required to use this view: set the ``table_class`` attribute with the desired :class:`~horizon.tables.DataTable` class; define a ``get_data`` method which returns a set of data for the table; and specify a template for the ``template_name`` attribute. Optionally, you can override the ``has_more_data`` method to trigger pagination handling for APIs that support it. """ table_class = None context_object_name = 'table' template_name = 'horizon/common/_data_table_view.html' def _get_data_dict(self): if not self._data: self.update_server_filter_action(self.request) self._data = {self.table_class._meta.name: self.get_data()} return self._data def get_data(self): return [] def get_tables(self): if not self._tables: self._tables = {} if has_permissions(self.request.user, self.table_class._meta): self._tables[self.table_class._meta.name] = self.get_table() return self._tables def get_table(self): # Note: this method cannot be easily memoized, because get_context_data # uses its cached value directly. if not self.table_class: raise AttributeError('You must specify a DataTable class for the ' '"table_class" attribute on %s.' % self.__class__.__name__) if not hasattr(self, "table"): self.table = self.table_class(self.request, **self.kwargs) return self.table def get_context_data(self, **kwargs): context = super(DataTableView, self).get_context_data(**kwargs) if hasattr(self, "table"): context[self.context_object_name] = self.table return context def post(self, request, *args, **kwargs): # If the server side table filter changed then go back to the first # page of data. Otherwise GET and POST handling are the same. if self.handle_server_filter(request): return shortcuts.redirect(self.get_table().get_absolute_url()) return self.get(request, *args, **kwargs) def get_filters(self, filters=None, filters_map=None): """Converts a string given by the user into a valid api filter value. :filters: Default filter values. {'filter1': filter_value, 'filter2': filter_value} :filters_map: mapping between user input and valid api filter values. {'filter_name':{_("true_value"):True, _("false_value"):False} """ filters = filters or {} filters_map = filters_map or {} filter_action = self.table._meta._filter_action if filter_action: filter_field = self.table.get_filter_field() if filter_action.is_api_filter(filter_field): filter_string = self.table.get_filter_string().strip() if filter_field and filter_string: filter_map = filters_map.get(filter_field, {}) # We use the filter_string given by the user and # look for valid values in the filter_map that's why # we apply lower() filters[filter_field] = filter_map.get( filter_string.lower(), filter_string) return filters class MixedDataTableView(DataTableView): """A class-based generic view to handle DataTable with mixed data types. Basic usage is the same as DataTableView. Three steps are required to use this view: #. Set the ``table_class`` attribute with desired :class:`~horizon.tables.DataTable` class. In the class the ``data_types`` list should have at least two elements. #. Define a ``get_{{ data_type }}_data`` method for each data type which returns a set of data for the table. #. Specify a template for the ``template_name`` attribute. """ table_class = None context_object_name = 'table' def _get_data_dict(self): if not self._data: table = self.table_class self._data = {table._meta.name: []} for data_type in table.data_types: func_name = "get_%s_data" % data_type data_func = getattr(self, func_name, None) if data_func is None: cls_name = self.__class__.__name__ raise NotImplementedError("You must define a %s method " "for %s data type in %s." % (func_name, data_type, cls_name)) data = data_func() self.assign_type_string(data, data_type) self._data[table._meta.name].extend(data) return self._data def assign_type_string(self, data, type_string): for datum in data: setattr(datum, self.table_class.data_type_name, type_string) def get_table(self): self.table = super(MixedDataTableView, self).get_table() if not self.table._meta.mixed_data_type: raise AttributeError('You must have at least two elements in ' 'the data_types attribute ' 'in table %s to use MixedDataTableView.' % self.table._meta.name) return self.table class PagedTableMixin(object): def __init__(self, *args, **kwargs): super(PagedTableMixin, self).__init__(*args, **kwargs) self._has_prev_data = False self._has_more_data = False def has_prev_data(self, table): return self._has_prev_data def has_more_data(self, table): return self._has_more_data def _get_marker(self): try: meta = self.table_class._meta except AttributeError: meta = self.table_classes[0]._meta prev_marker = self.request.GET.get(meta.prev_pagination_param, None) if prev_marker: return prev_marker, "asc" else: marker = self.request.GET.get(meta.pagination_param, None) if marker: return marker, "desc" return None, "desc" horizon-13.0.0/horizon/tables/base.py0000666000175100017510000022513713245511643017555 0ustar zuulzuul00000000000000# 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. import collections import copy import inspect import json import logging from operator import attrgetter import sys from django.conf import settings from django.core import exceptions as core_exceptions from django.core import urlresolvers from django import forms from django.http import HttpResponse from django import template from django.template.defaultfilters import slugify from django.template.defaultfilters import truncatechars from django.template.loader import render_to_string from django.utils.html import escape from django.utils import http from django.utils.http import urlencode from django.utils.safestring import mark_safe from django.utils import termcolors from django.utils.translation import ugettext_lazy as _ import six from horizon import conf from horizon import exceptions from horizon.forms import ThemableCheckboxInput from horizon import messages from horizon.tables.actions import FilterAction from horizon.tables.actions import LinkAction from horizon.utils import html LOG = logging.getLogger(__name__) PALETTE = termcolors.PALETTES[termcolors.DEFAULT_PALETTE] STRING_SEPARATOR = "__" @six.python_2_unicode_compatible class Column(html.HTMLElement): """A class which represents a single column in a :class:`.DataTable`. .. attribute:: transform A string or callable. If ``transform`` is a string, it should be the name of the attribute on the underlying data class which should be displayed in this column. If it is a callable, it will be passed the current row's data at render-time and should return the contents of the cell. Required. .. attribute:: verbose_name The name for this column which should be used for display purposes. Defaults to the value of ``transform`` with the first letter of each word capitalized if the ``transform`` is not callable, otherwise it defaults to an empty string (``""``). .. attribute:: sortable Boolean to determine whether this column should be sortable or not. Defaults to ``True``. .. attribute:: hidden Boolean to determine whether or not this column should be displayed when rendering the table. Default: ``False``. .. attribute:: link A string or callable which returns a URL which will be wrapped around this column's text as a link. .. attribute:: allowed_data_types A list of data types for which the link should be created. Default is an empty list (``[]``). When the list is empty and the ``link`` attribute is not None, all the rows under this column will be links. .. attribute:: status Boolean designating whether or not this column represents a status (i.e. "enabled/disabled", "up/down", "active/inactive"). Default: ``False``. .. attribute:: status_choices A tuple of tuples representing the possible data values for the status column and their associated boolean equivalent. Positive states should equate to ``True``, negative states should equate to ``False``, and indeterminate states should be ``None``. Values are compared in a case-insensitive manner. Example (these are also the default values):: status_choices = ( ('enabled', True), ('true', True), ('up', True), ('active', True), ('yes', True), ('on', True), ('none', None), ('unknown', None), ('', None), ('disabled', False), ('down', False), ('false', False), ('inactive', False), ('no', False), ('off', False), ) .. attribute:: display_choices A tuple of tuples representing the possible values to substitute the data when displayed in the column cell. .. attribute:: empty_value A string or callable to be used for cells which have no data. Defaults to the string ``"-"``. .. attribute:: summation A string containing the name of a summation method to be used in the generation of a summary row for this column. By default the options are ``"sum"`` or ``"average"``, which behave as expected. Optional. .. attribute:: filters A list of functions (often template filters) to be applied to the value of the data for this column prior to output. This is effectively a shortcut for writing a custom ``transform`` function in simple cases. .. attribute:: classes An iterable of CSS classes which should be added to this column. Example: ``classes=('foo', 'bar')``. .. attribute:: attrs A dict of HTML attribute strings which should be added to this column. Example: ``attrs={"data-foo": "bar"}``. .. attribute:: cell_attributes_getter A callable to get the HTML attributes of a column cell depending on the data. For example, to add additional description or help information for data in a column cell (e.g. in Images panel, for the column 'format'):: helpText = { 'ARI':'Amazon Ramdisk Image', 'QCOW2':'QEMU' Emulator' } getHoverHelp(data): text = helpText.get(data, None) if text: return {'title': text} else: return {} ... ... cell_attributes_getter = getHoverHelp .. attribute:: truncate An integer for the maximum length of the string in this column. If the length of the data in this column is larger than the supplied number, the data for this column will be truncated and an ellipsis will be appended to the truncated data. Defaults to ``None``. .. attribute:: link_classes An iterable of CSS classes which will be added when the column's text is displayed as a link. This is left for backward compatibility. Deprecated in favor of the link_attributes attribute. Example: ``link_classes=('link-foo', 'link-bar')``. Defaults to ``None``. .. attribute:: wrap_list Boolean value indicating whether the contents of this cell should be wrapped in a ```` tag. Useful in conjunction with Django's ``unordered_list`` template filter. Defaults to ``False``. .. attribute:: form_field A form field used for inline editing of the column. A django forms.Field can be used or django form.Widget can be used. Example: ``form_field=forms.CharField(required=True)``. Defaults to ``None``. .. attribute:: form_field_attributes The additional html attributes that will be rendered to form_field. Example: ``form_field_attributes={'class': 'bold_input_field'}``. Defaults to ``None``. .. attribute:: update_action The class that inherits from tables.actions.UpdateAction, update_cell method takes care of saving inline edited data. The tables.base.Row get_data method needs to be connected to table for obtaining the data. Example: ``update_action=UpdateCell``. Defaults to ``None``. .. attribute:: link_attrs A dict of HTML attribute strings which should be added when the column's text is displayed as a link. Examples: ``link_attrs={"data-foo": "bar"}``. ``link_attrs={"target": "_blank", "class": "link-foo link-bar"}``. Defaults to ``None``. .. attribute:: policy_rules List of scope and rule tuples to do policy checks on, the composition of which is (scope, rule) * scope: service type managing the policy for action * rule: string representing the action to be checked for a policy that requires a single rule check, policy_rules should look like: .. code-block:: none "(("compute", "compute:create_instance"),)" for a policy that requires multiple rule checks, rules should look like: .. code-block:: none "(("identity", "identity:list_users"), ("identity", "identity:list_roles"))" .. attribute:: help_text A string of simple help text displayed in a tooltip when you hover over the help icon beside the Column name. Defaults to ``None``. """ summation_methods = { "sum": sum, "average": lambda data: sum(data, 0.0) / len(data) } # Used to retain order when instantiating columns on a table creation_counter = 0 transform = None name = None verbose_name = None status_choices = ( ('enabled', True), ('true', True), ('up', True), ('yes', True), ('active', True), ('on', True), ('none', None), ('unknown', None), ('', None), ('disabled', False), ('down', False), ('false', False), ('inactive', False), ('no', False), ('off', False), ) def __init__(self, transform, verbose_name=None, sortable=True, link=None, allowed_data_types=None, hidden=False, attrs=None, status=False, status_choices=None, display_choices=None, empty_value=None, filters=None, classes=None, summation=None, auto=None, truncate=None, link_classes=None, wrap_list=False, form_field=None, form_field_attributes=None, update_action=None, link_attrs=None, policy_rules=None, cell_attributes_getter=None, help_text=None): allowed_data_types = allowed_data_types or [] self.classes = list(classes or getattr(self, "classes", [])) super(Column, self).__init__() self.attrs.update(attrs or {}) if callable(transform): self.transform = transform self.name = "<%s callable>" % transform.__name__ else: self.transform = six.text_type(transform) self.name = self.transform # Empty string is a valid value for verbose_name if verbose_name is None: if callable(transform): self.verbose_name = '' else: self.verbose_name = self.transform.title() else: self.verbose_name = verbose_name self.auto = auto self.sortable = sortable self.link = link self.allowed_data_types = allowed_data_types self.hidden = hidden self.status = status self.empty_value = empty_value or _('-') self.filters = filters or [] self.truncate = truncate self.wrap_list = wrap_list self.form_field = form_field self.form_field_attributes = form_field_attributes or {} self.update_action = update_action self.link_attrs = link_attrs or {} self.policy_rules = policy_rules or [] self.help_text = help_text if link_classes: self.link_attrs['class'] = ' '.join(link_classes) self.cell_attributes_getter = cell_attributes_getter if status_choices: self.status_choices = status_choices self.display_choices = display_choices if summation is not None and summation not in self.summation_methods: raise ValueError( "Summation method %(summation)s must be one of %(keys)s.", {'summation': summation, 'keys': ", ".join(self.summation_methods.keys())}) self.summation = summation self.creation_counter = Column.creation_counter Column.creation_counter += 1 if self.sortable and not self.auto: self.classes.append("sortable") if self.hidden: self.classes.append("hide") if self.link is not None: self.classes.append('anchor') def __str__(self): return six.text_type(self.verbose_name) def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.name) def allowed(self, request): """Determine whether processing/displaying the column is allowed. It is determined based on the current request. """ if not self.policy_rules: return True policy_check = getattr(settings, "POLICY_CHECK_FUNCTION", None) if policy_check: return policy_check(self.policy_rules, request) return True def get_raw_data(self, datum): """Returns the raw data for this column. No filters or formatting are applied to the returned data. This is useful when doing calculations on data in the table. """ # Callable transformations if callable(self.transform): data = self.transform(datum) # Dict lookups elif isinstance(datum, collections.Mapping) and \ self.transform in datum: data = datum.get(self.transform) else: # Basic object lookups data = getattr(datum, self.transform, None) if not hasattr(datum, self.transform): msg = "The attribute %(attr)s doesn't exist on %(obj)s." LOG.debug(termcolors.colorize(msg, **PALETTE['ERROR']), {'attr': self.transform, 'obj': datum}) return data def get_data(self, datum): """Returns the final display data for this column from the given inputs. The return value will be either the attribute specified for this column or the return value of the attr:`~horizon.tables.Column.transform` method for this column. """ datum_id = self.table.get_object_id(datum) if datum_id in self.table._data_cache[self]: return self.table._data_cache[self][datum_id] data = self.get_raw_data(datum) display_value = None if self.display_choices: display_value = [display for (value, display) in self.display_choices if value.lower() == (data or '').lower()] if display_value: data = display_value[0] else: for filter_func in self.filters: try: data = filter_func(data) except Exception: msg = ("Filter '%(filter)s' failed with data " "'%(data)s' on column '%(col_name)s'") args = {'filter': filter_func.__name__, 'data': data, 'col_name': six.text_type(self.verbose_name)} LOG.warning(msg, args) if data and self.truncate: data = truncatechars(data, self.truncate) self.table._data_cache[self][datum_id] = data return self.table._data_cache[self][datum_id] def get_link_url(self, datum): """Returns the final value for the column's ``link`` property. If ``allowed_data_types`` of this column is not empty and the datum has an assigned type, check if the datum's type is in the ``allowed_data_types`` list. If not, the datum won't be displayed as a link. If ``link`` is a callable, it will be passed the current data object and should return a URL. Otherwise ``get_link_url`` will attempt to call ``reverse`` on ``link`` with the object's id as a parameter. Failing that, it will simply return the value of ``link``. """ if self.allowed_data_types: data_type_name = self.table._meta.data_type_name data_type = getattr(datum, data_type_name, None) if data_type and (data_type not in self.allowed_data_types): return None obj_id = self.table.get_object_id(datum) if callable(self.link): if 'request' in inspect.getargspec(self.link).args: return self.link(datum, request=self.table.request) return self.link(datum) try: return urlresolvers.reverse(self.link, args=(obj_id,)) except urlresolvers.NoReverseMatch: return self.link if getattr(settings, 'INTEGRATION_TESTS_SUPPORT', False): def get_default_attrs(self): attrs = super(Column, self).get_default_attrs() attrs.update({'data-selenium': self.name}) return attrs def get_summation(self): """Returns the summary value for the data in this column. It returns the summary value if a valid summation method is specified for it. Otherwise returns ``None``. """ if self.summation not in self.summation_methods: return None summation_function = self.summation_methods[self.summation] data = [self.get_raw_data(datum) for datum in self.table.data] data = [raw_data for raw_data in data if raw_data is not None] if len(data): try: summation = summation_function(data) for filter_func in self.filters: summation = filter_func(summation) return summation except TypeError: pass return None class WrappingColumn(Column): """A column that wraps its contents. Useful for data like UUIDs or names""" def __init__(self, *args, **kwargs): super(WrappingColumn, self).__init__(*args, **kwargs) self.classes.append('word-break') class Row(html.HTMLElement): """Represents a row in the table. When iterated, the ``Row`` instance will yield each of its cells. Rows are capable of AJAX updating, with a little added work: The ``ajax`` property needs to be set to ``True``, and subclasses need to define a ``get_data`` method which returns a data object appropriate for consumption by the table (effectively the "get" lookup versus the table's "list" lookup). The automatic update interval is configurable by setting the key ``ajax_poll_interval`` in the ``HORIZON_CONFIG`` dictionary. Default: ``2500`` (measured in milliseconds). .. attribute:: table The table which this row belongs to. .. attribute:: datum The data object which this row represents. .. attribute:: id A string uniquely representing this row composed of the table name and the row data object's identifier. .. attribute:: cells The cells belonging to this row stored in a ``OrderedDict`` object. This attribute is populated during instantiation. .. attribute:: status Boolean value representing the status of this row calculated from the values of the table's ``status_columns`` if they are set. .. attribute:: status_class Returns a css class for the status of the row based on ``status``. .. attribute:: ajax Boolean value to determine whether ajax updating for this row is enabled. .. attribute:: ajax_action_name String that is used for the query parameter key to request AJAX updates. Generally you won't need to change this value. Default: ``"row_update"``. .. attribute:: ajax_cell_action_name String that is used for the query parameter key to request AJAX updates of cell. Generally you won't need to change this value. It is also used for inline edit of the cell. Default: ``"cell_update"``. """ ajax = False ajax_action_name = "row_update" ajax_cell_action_name = "cell_update" def __init__(self, table, datum=None): super(Row, self).__init__() self.table = table self.datum = datum self.selected = False if self.datum: self.load_cells() else: self.id = None self.cells = [] def load_cells(self, datum=None): """Load the row's data and initialize all the cells in the row. It also set the appropriate row properties which require the row's data to be determined. The row's data is provided either at initialization or as an argument to this function. This function is called automatically by :meth:`~horizon.tables.Row.__init__` if the ``datum`` argument is provided. However, by not providing the data during initialization this function allows for the possibility of a two-step loading pattern when you need a row instance but don't yet have the data available. """ # Compile all the cells on instantiation. table = self.table if datum: self.datum = datum else: datum = self.datum cells = [] for column in table.columns.values(): cell = table._meta.cell_class(datum, column, self) cells.append((column.name or column.auto, cell)) self.cells = collections.OrderedDict(cells) if self.ajax: 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") self.attrs['data-object-id'] = table.get_object_id(datum) # Add the row's status class and id to the attributes to be rendered. self.classes.append(self.status_class) id_vals = {"table": self.table.name, "sep": STRING_SEPARATOR, "id": table.get_object_id(datum)} self.id = "%(table)s%(sep)srow%(sep)s%(id)s" % id_vals self.attrs['id'] = self.id # Add the row's display name if available display_name = table.get_object_display(datum) display_name_key = table.get_object_display_key(datum) if display_name: self.attrs['data-display'] = escape(display_name) self.attrs['data-display-key'] = escape(display_name_key) def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.id) def __iter__(self): return iter(self.cells.values()) @property def status(self): column_names = self.table._meta.status_columns if column_names: statuses = dict([(column_name, self.cells[column_name].status) for column_name in column_names]) return self.table.calculate_row_status(statuses) @property def status_class(self): column_names = self.table._meta.status_columns if column_names: return self.table.get_row_status_class(self.status) else: return '' def render(self): return render_to_string("horizon/common/_data_table_row.html", {"row": self}) def get_cells(self): """Returns the bound cells for this row in order.""" return list(self.cells.values()) def get_ajax_update_url(self): table_url = self.table.get_absolute_url() marker_name = self.table._meta.pagination_param marker = self.table.request.GET.get(marker_name, None) if not marker: marker_name = self.table._meta.prev_pagination_param marker = self.table.request.GET.get(marker_name, None) request_params = [ ("action", self.ajax_action_name), ("table", self.table.name), ("obj_id", self.table.get_object_id(self.datum)), ] if marker: request_params.append((marker_name, marker)) params = urlencode(collections.OrderedDict(request_params)) return "%s?%s" % (table_url, params) def can_be_selected(self, datum): """Determines whether the row can be selected. By default if multiselect enabled return True. You can remove the checkbox after an ajax update here if required. """ return True def get_data(self, request, obj_id): """Fetches the updated data for the row based on the given object ID. Must be implemented by a subclass to allow AJAX updating. """ return {} class Cell(html.HTMLElement): """Represents a single cell in the table.""" def __init__(self, datum, column, row, attrs=None, classes=None): self.classes = classes or getattr(self, "classes", []) super(Cell, self).__init__() self.attrs.update(attrs or {}) self.datum = datum self.column = column self.row = row self.wrap_list = column.wrap_list self.inline_edit_available = self.column.update_action is not None # initialize the update action if available if self.inline_edit_available: self.update_action = self.column.update_action() self.attrs['data-cell-name'] = column.name self.attrs['data-update-url'] = self.get_ajax_update_url() self.inline_edit_mod = False # add tooltip to cells if the truncate variable is set if column.truncate: # NOTE(tsufiev): trying to pull cell raw data out of datum for # those columns where truncate is False leads to multiple errors # in unit tests data = getattr(datum, column.name, '') or '' if len(data) > column.truncate: self.attrs['data-toggle'] = 'tooltip' self.attrs['title'] = data if getattr(settings, 'INTEGRATION_TESTS_SUPPORT', False): self.attrs['data-selenium'] = data self.data = self.get_data(datum, column, row) def get_data(self, datum, column, row): """Fetches the data to be displayed in this cell.""" table = row.table if column.auto == "multi_select": data = "" if row.can_be_selected(datum): widget = ThemableCheckboxInput(check_test=lambda value: False) # Convert value to string to avoid accidental type conversion data = widget.render('object_ids', six.text_type(table.get_object_id(datum)), {'class': 'table-row-multi-select'}) table._data_cache[column][table.get_object_id(datum)] = data elif column.auto == "form_field": widget = column.form_field if issubclass(widget.__class__, forms.Field): widget = widget.widget widget_name = "%s__%s" % \ (column.name, six.text_type(table.get_object_id(datum))) # Create local copy of attributes, so it don't change column # class form_field_attributes form_field_attributes = {} form_field_attributes.update(column.form_field_attributes) # Adding id of the input so it pairs with label correctly form_field_attributes['id'] = widget_name if (template.defaultfilters.urlize in column.filters or template.defaultfilters.yesno in column.filters): data = widget.render(widget_name, column.get_raw_data(datum), form_field_attributes) else: data = widget.render(widget_name, column.get_data(datum), form_field_attributes) table._data_cache[column][table.get_object_id(datum)] = data elif column.auto == "actions": data = table.render_row_actions(datum) table._data_cache[column][table.get_object_id(datum)] = data else: data = column.get_data(datum) if column.cell_attributes_getter: cell_attributes = column.cell_attributes_getter(data) or {} self.attrs.update(cell_attributes) return data def __repr__(self): return '<%s: %s, %s>' % (self.__class__.__name__, self.column.name, self.row.id) @property def id(self): return ("%s__%s" % (self.column.name, six.text_type(self.row.table.get_object_id(self.datum)))) @property def value(self): """Returns a formatted version of the data for final output. This takes into consideration the :attr:`~horizon.tables.Column.link`` and :attr:`~horizon.tables.Column.empty_value` attributes. """ try: data = self.column.get_data(self.datum) if data is None: if callable(self.column.empty_value): data = self.column.empty_value(self.datum) else: data = self.column.empty_value except Exception: data = None exc_info = sys.exc_info() raise six.reraise(template.TemplateSyntaxError, exc_info[1], exc_info[2]) if self.url and not self.column.auto == "form_field": link_attrs = ' '.join(['%s="%s"' % (k, v) for (k, v) in self.column.link_attrs.items()]) # Escape the data inside while allowing our HTML to render data = mark_safe('%s' % ( (escape(self.url), link_attrs, escape(six.text_type(data))))) return data @property def url(self): if self.column.link: url = self.column.get_link_url(self.datum) if url: return url else: return None @property def status(self): """Gets the status for the column based on the cell's data.""" # Deal with status column mechanics based in this cell's data if hasattr(self, '_status'): return self._status if self.column.status or \ self.column.name in self.column.table._meta.status_columns: # returns the first matching status found data_status_lower = six.text_type( self.column.get_raw_data(self.datum)).lower() for status_name, status_value in self.column.status_choices: if six.text_type(status_name).lower() == data_status_lower: self._status = status_value return self._status self._status = None return self._status def get_status_class(self, status): """Returns a css class name determined by the status value.""" if status is True: return "status_up" elif status is False: return "status_down" else: return "warning" def get_default_classes(self): """Returns a flattened string of the cell's CSS classes.""" if not self.url: self.column.classes = [cls for cls in self.column.classes if cls != "anchor"] column_class_string = self.column.get_final_attrs().get('class', "") classes = set(column_class_string.split(" ")) if self.column.status: classes.add(self.get_status_class(self.status)) if self.inline_edit_available: classes.add("inline_edit_available") return list(classes) def get_ajax_update_url(self): column = self.column table_url = column.table.get_absolute_url() params = urlencode(collections.OrderedDict([ ("action", self.row.ajax_cell_action_name), ("table", column.table.name), ("cell_name", column.name), ("obj_id", column.table.get_object_id(self.datum)) ])) return "%s?%s" % (table_url, params) @property def update_allowed(self): """Determines whether update of given cell is allowed. Calls allowed action of defined UpdateAction of the Column. """ return self.update_action.allowed(self.column.table.request, self.datum, self) def render(self): return render_to_string("horizon/common/_data_table_cell.html", {"cell": self}) class DataTableOptions(object): """Contains options for :class:`.DataTable` objects. .. attribute:: name A short name or slug for the table. .. attribute:: verbose_name A more verbose name for the table meant for display purposes. .. attribute:: columns A list of column objects or column names. Controls ordering/display of the columns in the table. .. attribute:: table_actions A list of action classes derived from the :class:`~horizon.tables.Action` class. These actions will handle tasks such as bulk deletion, etc. for multiple objects at once. .. attribute:: table_actions_menu A list of action classes similar to ``table_actions`` except these will be displayed in a menu instead of as individual buttons. Actions from this list will take precedence over actions from the ``table_actions`` list. .. attribute:: table_actions_menu_label A label of a menu button for ``table_actions_menu``. The default is "Actions" or "More Actions" depending on ``table_actions``. .. attribute:: row_actions A list similar to ``table_actions`` except tailored to appear for each row. These actions act on a single object at a time. .. attribute:: actions_column Boolean value to control rendering of an additional column containing the various actions for each row. Defaults to ``True`` if any actions are specified in the ``row_actions`` option. .. attribute:: multi_select Boolean value to control rendering of an extra column with checkboxes for selecting multiple objects in the table. Defaults to ``True`` if any actions are specified in the ``table_actions`` option. .. attribute:: filter Boolean value to control the display of the "filter" search box in the table actions. By default it checks whether or not an instance of :class:`.FilterAction` is in ``table_actions``. .. attribute:: template String containing the template which should be used to render the table. Defaults to ``"horizon/common/_data_table.html"``. .. attribute:: row_actions_dropdown_template String containing the template which should be used to render the row actions dropdown. Defaults to ``"horizon/common/_data_table_row_actions_dropdown.html"``. .. attribute:: row_actions_row_template String containing the template which should be used to render the row actions. Defaults to ``"horizon/common/_data_table_row_actions_row.html"``. .. attribute:: table_actions_template String containing the template which should be used to render the table actions. Defaults to ``"horizon/common/_data_table_table_actions.html"``. .. attribute:: context_var_name The name of the context variable which will contain the table when it is rendered. Defaults to ``"table"``. .. attribute:: prev_pagination_param The name of the query string parameter which will be used when paginating backward in this table. When using multiple tables in a single view this will need to be changed to differentiate between the tables. Default: ``"prev_marker"``. .. attribute:: pagination_param The name of the query string parameter which will be used when paginating forward in this table. When using multiple tables in a single view this will need to be changed to differentiate between the tables. Default: ``"marker"``. .. attribute:: status_columns A list or tuple of column names which represents the "state" of the data object being represented. If ``status_columns`` is set, when the rows are rendered the value of this column will be used to add an extra class to the row in the form of ``"status_up"`` or ``"status_down"`` for that row's data. The row status is used by other Horizon components to trigger tasks such as dynamic AJAX updating. .. attribute:: cell_class The class which should be used for rendering the cells of this table. Optional. Default: :class:`~horizon.tables.Cell`. .. attribute:: row_class The class which should be used for rendering the rows of this table. Optional. Default: :class:`~horizon.tables.Row`. .. attribute:: column_class The class which should be used for handling the columns of this table. Optional. Default: :class:`~horizon.tables.Column`. .. attribute:: css_classes A custom CSS class or classes to add to the ```` tag of the rendered table, for when the particular table requires special styling. Default: ``""``. .. attribute:: mixed_data_type A toggle to indicate if the table accepts two or more types of data. Optional. Default: ``False`` .. attribute:: data_types A list of data types that this table would accept. Default to be an empty list, but if the attribute ``mixed_data_type`` is set to ``True``, then this list must have at least one element. .. attribute:: data_type_name The name of an attribute to assign to data passed to the table when it accepts mix data. Default: ``"_table_data_type"`` .. attribute:: footer Boolean to control whether or not to show the table's footer. Default: ``True``. .. attribute:: hidden_title Boolean to control whether or not to show the table's title. Default: ``True``. .. attribute:: permissions A list of permission names which this table requires in order to be displayed. Defaults to an empty list (``[]``). """ def __init__(self, options): self.name = getattr(options, 'name', self.__class__.__name__) verbose_name = (getattr(options, 'verbose_name', None) or self.name.title()) self.verbose_name = verbose_name self.columns = getattr(options, 'columns', None) self.status_columns = getattr(options, 'status_columns', []) self.table_actions = getattr(options, 'table_actions', []) self.row_actions = getattr(options, 'row_actions', []) self.table_actions_menu = getattr(options, 'table_actions_menu', []) self.table_actions_menu_label = getattr(options, 'table_actions_menu_label', None) self.cell_class = getattr(options, 'cell_class', Cell) self.row_class = getattr(options, 'row_class', Row) self.column_class = getattr(options, 'column_class', Column) self.css_classes = getattr(options, 'css_classes', '') self.prev_pagination_param = getattr(options, 'prev_pagination_param', 'prev_marker') self.pagination_param = getattr(options, 'pagination_param', 'marker') self.browser_table = getattr(options, 'browser_table', None) self.footer = getattr(options, 'footer', True) self.hidden_title = getattr(options, 'hidden_title', True) self.no_data_message = getattr(options, "no_data_message", _("No items to display.")) self.permissions = getattr(options, 'permissions', []) # Set self.filter if we have any FilterActions filter_actions = [action for action in self.table_actions if issubclass(action, FilterAction)] if len(filter_actions) > 1: raise NotImplementedError("Multiple filter actions are not " "currently supported.") self.filter = getattr(options, 'filter', len(filter_actions) > 0) if len(filter_actions) == 1: self._filter_action = filter_actions.pop() else: self._filter_action = None self.template = getattr(options, 'template', 'horizon/common/_data_table.html') self.row_actions_dropdown_template = \ getattr(options, 'row_actions_dropdown_template', 'horizon/common/_data_table_row_actions_dropdown.html') self.row_actions_row_template = \ getattr(options, 'row_actions_row_template', 'horizon/common/_data_table_row_actions_row.html') self.table_actions_template = \ getattr(options, 'table_actions_template', 'horizon/common/_data_table_table_actions.html') self.context_var_name = six.text_type(getattr(options, 'context_var_name', 'table')) self.actions_column = getattr(options, 'actions_column', len(self.row_actions) > 0) self.multi_select = getattr(options, 'multi_select', len(self.table_actions) > 0) # Set runtime table defaults; not configurable. self.has_prev_data = False self.has_more_data = False # Set mixed data type table attr self.mixed_data_type = getattr(options, 'mixed_data_type', False) self.data_types = getattr(options, 'data_types', []) # If the data_types has more than 2 elements, set mixed_data_type # to True automatically. if len(self.data_types) > 1: self.mixed_data_type = True # However, if the mixed_data_type is set to True manually and # the data_types is empty, raise an error. if self.mixed_data_type and len(self.data_types) <= 1: raise ValueError("If mixed_data_type is set to True in class %s, " "data_types should has more than one types" % self.name) self.data_type_name = getattr(options, 'data_type_name', "_table_data_type") self.filter_first_message = \ getattr(options, 'filter_first_message', _('Please specify a search criteria first.')) class DataTableMetaclass(type): """Metaclass to add options to DataTable class and collect columns.""" def __new__(mcs, name, bases, attrs): # Process options from Meta class_name = name dt_attrs = {} dt_attrs["_meta"] = opts = DataTableOptions(attrs.get("Meta", None)) # Gather columns; this prevents the column from being an attribute # on the DataTable class and avoids naming conflicts. columns = [] for attr_name, obj in attrs.items(): if isinstance(obj, (opts.column_class, Column)): column_instance = attrs[attr_name] column_instance.name = attr_name column_instance.classes.append('normal_column') columns.append((attr_name, column_instance)) else: dt_attrs[attr_name] = obj columns.sort(key=lambda x: x[1].creation_counter) # Iterate in reverse to preserve final order for base in reversed(bases): if hasattr(base, 'base_columns'): columns[0:0] = base.base_columns.items() dt_attrs['base_columns'] = collections.OrderedDict(columns) # If the table is in a ResourceBrowser, the column number must meet # these limits because of the width of the browser. if opts.browser_table == "navigation" and len(columns) > 3: raise ValueError("You can assign at most three columns to %s." % class_name) if opts.browser_table == "content" and len(columns) > 2: raise ValueError("You can assign at most two columns to %s." % class_name) if opts.columns: # Remove any columns that weren't declared if we're being explicit # NOTE: we're iterating a COPY of the list here! for column_data in columns[:]: if column_data[0] not in opts.columns: columns.pop(columns.index(column_data)) # Re-order based on declared columns columns.sort(key=lambda x: dt_attrs['_meta'].columns.index(x[0])) # Add in our auto-generated columns if opts.multi_select and opts.browser_table != "navigation": multi_select = opts.column_class("multi_select", verbose_name="", auto="multi_select") multi_select.classes.append('multi_select_column') columns.insert(0, ("multi_select", multi_select)) if opts.actions_column: actions_column = opts.column_class("actions", verbose_name=_("Actions"), auto="actions") actions_column.classes.append('actions_column') columns.append(("actions", actions_column)) # Store this set of columns internally so we can copy them per-instance dt_attrs['_columns'] = collections.OrderedDict(columns) # Gather and register actions for later access since we only want # to instantiate them once. # (list() call gives deterministic sort order, which sets don't have.) actions = list(set(opts.row_actions) | set(opts.table_actions) | set(opts.table_actions_menu)) actions.sort(key=attrgetter('name')) actions_dict = collections.OrderedDict([(action.name, action()) for action in actions]) dt_attrs['base_actions'] = actions_dict if opts._filter_action: # Replace our filter action with the instantiated version opts._filter_action = actions_dict[opts._filter_action.name] # Create our new class! return type.__new__(mcs, name, bases, dt_attrs) @six.python_2_unicode_compatible @six.add_metaclass(DataTableMetaclass) class DataTable(object): """A class which defines a table with all data and associated actions. .. attribute:: name String. Read-only access to the name specified in the table's Meta options. .. attribute:: multi_select Boolean. Read-only access to whether or not this table should display a column for multi-select checkboxes. .. attribute:: data Read-only access to the data this table represents. .. attribute:: filtered_data Read-only access to the data this table represents, filtered by the :meth:`~horizon.tables.FilterAction.filter` method of the table's :class:`~horizon.tables.FilterAction` class (if one is provided) using the current request's query parameters. """ def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs): self.request = request self.data = data self.kwargs = kwargs self._needs_form_wrapper = needs_form_wrapper self._no_data_message = self._meta.no_data_message self.breadcrumb = None self.current_item_id = None self.permissions = self._meta.permissions self.needs_filter_first = False self._filter_first_message = self._meta.filter_first_message # Create a new set columns = [] for key, _column in self._columns.items(): if _column.allowed(request): column = copy.copy(_column) column.table = self columns.append((key, column)) self.columns = collections.OrderedDict(columns) self._populate_data_cache() # Associate these actions with this table for action in self.base_actions.values(): action.associate_with_table(self) self.needs_summary_row = any([col.summation for col in self.columns.values()]) def __str__(self): return six.text_type(self._meta.verbose_name) def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self._meta.name) @property def name(self): return self._meta.name @property def footer(self): return self._meta.footer @property def multi_select(self): return self._meta.multi_select @property def filtered_data(self): # This function should be using django.utils.functional.cached_property # decorator, but unfortunately due to bug in Django # https://code.djangoproject.com/ticket/19872 it would make it fail # when being mocked by mox in tests. if not hasattr(self, '_filtered_data'): self._filtered_data = self.data if self._meta.filter and self._meta._filter_action: action = self._meta._filter_action filter_string = self.get_filter_string() filter_field = self.get_filter_field() request_method = self.request.method needs_preloading = (not filter_string and request_method == 'GET' and action.needs_preloading) valid_method = (request_method == action.method) not_api_filter = (filter_string and not action.is_api_filter(filter_field)) if valid_method or needs_preloading or not_api_filter: if self._meta.mixed_data_type: self._filtered_data = action.data_type_filter( self, self.data, filter_string) else: self._filtered_data = action.filter( self, self.data, filter_string) return self._filtered_data def slugify_name(self): return str(slugify(self._meta.name)) def get_filter_string(self): """Get the filter string value. For 'server' type filters this is saved in the session so that it gets persisted across table loads. For other filter types this is obtained from the POST dict. """ filter_action = self._meta._filter_action param_name = filter_action.get_param_name() filter_string = '' if filter_action.filter_type == 'server': filter_string = self.request.session.get(param_name, '') else: filter_string = self.request.POST.get(param_name, '') return filter_string def get_filter_field(self): """Get the filter field value used for 'server' type filters. This is the value from the filter action's list of filter choices. """ filter_action = self._meta._filter_action param_name = '%s_field' % filter_action.get_param_name() filter_field = self.request.session.get(param_name, '') return filter_field def _populate_data_cache(self): self._data_cache = {} # Set up hash tables to store data points for each column for column in self.get_columns(): self._data_cache[column] = {} def _filter_action(self, action, request, datum=None): try: # Catch user errors in permission functions here row_matched = True if self._meta.mixed_data_type: row_matched = action.data_type_matched(datum) return action._allowed(request, datum) and row_matched except AssertionError: # don't trap mox exceptions (which subclass AssertionError) # when testing! raise except Exception: LOG.exception("Error while checking action permissions.") return None def is_browser_table(self): if self._meta.browser_table: return True return False def render(self): """Renders the table using the template from the table options.""" table_template = template.loader.get_template(self._meta.template) extra_context = {self._meta.context_var_name: self, 'hidden_title': self._meta.hidden_title} return table_template.render(extra_context, self.request) def get_absolute_url(self): """Returns the canonical URL for this table. This is used for the POST action attribute on the form element wrapping the table. In many cases it is also useful for redirecting after a successful action on the table. For convenience it defaults to the value of ``request.get_full_path()`` with any query string stripped off, e.g. the path at which the table was requested. """ return self.request.get_full_path().partition('?')[0] def get_full_url(self): """Returns the full URL path for this table. This is used for the POST action attribute on the form element wrapping the table. We use this method to persist the pagination marker. """ return self.request.get_full_path() def get_empty_message(self): """Returns the message to be displayed when there is no data.""" return self._no_data_message def get_filter_first_message(self): """Return the message to be displayed first in the filter. when the user needs to provide a search criteria first before loading any data. """ return self._filter_first_message def get_object_by_id(self, lookup): """Returns the data object whose ID matches ``loopup`` parameter. The data object is looked up from the table's dataset and the data which matches the ``lookup`` parameter specified. An error will be raised if the match is not a single data object. We will convert the object id and ``lookup`` to unicode before comparison. Uses :meth:`~horizon.tables.DataTable.get_object_id` internally. """ if not isinstance(lookup, six.text_type): lookup = str(lookup) if six.PY2: lookup = lookup.decode('utf-8') matches = [] for datum in self.data: obj_id = self.get_object_id(datum) if not isinstance(obj_id, six.text_type): obj_id = str(obj_id) if six.PY2: obj_id = obj_id.decode('utf-8') if obj_id == lookup: matches.append(datum) if len(matches) > 1: raise ValueError("Multiple matches were returned for that id: %s." % matches) if not matches: raise exceptions.Http302(self.get_absolute_url(), _('No match returned for the id "%s".') % lookup) return matches[0] @property def has_actions(self): """Indicates whether there are any available actions on this table. Returns a boolean value. """ if not self.base_actions: return False return any(self.get_table_actions()) or any(self._meta.row_actions) @property def needs_form_wrapper(self): """Returns if this table should be rendered wrapped in a ```` tag. Returns a boolean value. """ # If needs_form_wrapper is explicitly set, defer to that. if self._needs_form_wrapper is not None: return self._needs_form_wrapper # Otherwise calculate whether or not we need a form element. return self.has_actions def get_table_actions(self): """Returns a list of the action instances for this table.""" button_actions = [self.base_actions[action.name] for action in self._meta.table_actions if action not in self._meta.table_actions_menu] menu_actions = [self.base_actions[action.name] for action in self._meta.table_actions_menu] bound_actions = button_actions + menu_actions return [action for action in bound_actions if self._filter_action(action, self.request)] def get_row_actions(self, datum): """Returns a list of the action instances for a specific row.""" bound_actions = [] for action in self._meta.row_actions: # Copy to allow modifying properties per row bound_action = copy.copy(self.base_actions[action.name]) bound_action.attrs = copy.copy(bound_action.attrs) bound_action.datum = datum # Remove disallowed actions. if not self._filter_action(bound_action, self.request, datum): continue # Hook for modifying actions based on data. No-op by default. bound_action.update(self.request, datum) # Pre-create the URL for this link with appropriate parameters if issubclass(bound_action.__class__, LinkAction): bound_action.bound_url = bound_action.get_link_url(datum) bound_actions.append(bound_action) return bound_actions def set_multiselect_column_visibility(self, visible=True): """hide checkbox column if no current table action is allowed.""" if not self.multi_select: return select_column = list(self.columns.values())[0] # Try to find if the hidden class need to be # removed or added based on visible flag. hidden_found = 'hidden' in select_column.classes if hidden_found and visible: select_column.classes.remove('hidden') elif not hidden_found and not visible: select_column.classes.append('hidden') def render_table_actions(self): """Renders the actions specified in ``Meta.table_actions``.""" template_path = self._meta.table_actions_template table_actions_template = template.loader.get_template(template_path) bound_actions = self.get_table_actions() extra_context = {"table_actions": bound_actions, "table_actions_buttons": [], "table_actions_menu": []} if self._meta.filter and ( self._filter_action(self._meta._filter_action, self.request)): extra_context["filter"] = self._meta._filter_action for action in bound_actions: if action.__class__ in self._meta.table_actions_menu: extra_context['table_actions_menu'].append(action) elif action != extra_context.get('filter'): extra_context['table_actions_buttons'].append(action) if self._meta.table_actions_menu_label: extra_context['table_actions_menu_label'] = \ self._meta.table_actions_menu_label self.set_multiselect_column_visibility(len(bound_actions) > 0) return table_actions_template.render(extra_context, self.request) def render_row_actions(self, datum, row=False): """Renders the actions specified in ``Meta.row_actions``. The actions are rendered using the current row data. If `row` is True, the actions are rendered in a row of buttons. Otherwise they are rendered in a dropdown box. """ if row: template_path = self._meta.row_actions_row_template else: template_path = self._meta.row_actions_dropdown_template row_actions_template = template.loader.get_template(template_path) bound_actions = self.get_row_actions(datum) extra_context = {"row_actions": bound_actions, "row_id": self.get_object_id(datum)} return row_actions_template.render(extra_context, self.request) @staticmethod def parse_action(action_string): """Parses the ``action_string`` parameter sent back with the POST data. By default this parses a string formatted as ``{{ table_name }}__{{ action_name }}__{{ row_id }}`` and returns each of the pieces. The ``row_id`` is optional. """ if action_string: bits = action_string.split(STRING_SEPARATOR) table = bits[0] action = bits[1] try: object_id = STRING_SEPARATOR.join(bits[2:]) if object_id == '': object_id = None except IndexError: object_id = None return table, action, object_id def take_action(self, action_name, obj_id=None, obj_ids=None): """Locates the appropriate action and routes the object data to it. The action should return an HTTP redirect if successful, or a value which evaluates to ``False`` if unsuccessful. """ # See if we have a list of ids obj_ids = obj_ids or self.request.POST.getlist('object_ids') action = self.base_actions.get(action_name, None) if not action or action.method != self.request.method: # We either didn't get an action or we're being hacked. Goodbye. return None # Meanwhile, back in Gotham... if not action.requires_input or obj_id or obj_ids: if obj_id: obj_id = self.sanitize_id(obj_id) if obj_ids: obj_ids = [self.sanitize_id(i) for i in obj_ids] # Single handling is easy if not action.handles_multiple: response = action.single(self, self.request, obj_id) # Otherwise figure out what to pass along else: # Preference given to a specific id, since that implies # the user selected an action for just one row. if obj_id: obj_ids = [obj_id] response = action.multiple(self, self.request, obj_ids) return response elif action and action.requires_input and not (obj_id or obj_ids): messages.info(self.request, _("Please select a row before taking that action.")) return None @classmethod def check_handler(cls, request): """Determine whether the request should be handled by this table.""" if request.method == "POST" and "action" in request.POST: table, action, obj_id = cls.parse_action(request.POST["action"]) elif "table" in request.GET and "action" in request.GET: table = request.GET["table"] action = request.GET["action"] obj_id = request.GET.get("obj_id", None) else: table = action = obj_id = None return table, action, obj_id def maybe_preempt(self): """Determine whether the request should be handled in earlier phase. It determines the request should be handled by a preemptive action on this table or by an AJAX row update before loading any data. """ request = self.request table_name, action_name, obj_id = self.check_handler(request) if table_name == self.name: # Handle AJAX row updating. new_row = self._meta.row_class(self) if new_row.ajax and new_row.ajax_action_name == action_name: try: datum = new_row.get_data(request, obj_id) if self.get_object_id(datum) == self.current_item_id: self.selected = True new_row.classes.append('current_selected') new_row.load_cells(datum) error = False except Exception: datum = None error = exceptions.handle(request, ignore=True) if request.is_ajax(): if not error: return HttpResponse(new_row.render()) else: return HttpResponse(status=error.status_code) elif new_row.ajax_cell_action_name == action_name: # inline edit of the cell actions return self.inline_edit_handle(request, table_name, action_name, obj_id, new_row) preemptive_actions = [action for action in self.base_actions.values() if action.preempt] if action_name: for action in preemptive_actions: if action.name == action_name: handled = self.take_action(action_name, obj_id) if handled: return handled return None def inline_edit_handle(self, request, table_name, action_name, obj_id, new_row): """Inline edit handler. Showing form or handling update by POST of the cell. """ try: cell_name = request.GET['cell_name'] datum = new_row.get_data(request, obj_id) # TODO(lsmola) extract load cell logic to Cell and load # only 1 cell. This is kind of ugly. if request.GET.get('inline_edit_mod') == "true": new_row.table.columns[cell_name].auto = "form_field" inline_edit_mod = True else: inline_edit_mod = False # Load the cell and set the inline_edit_mod. new_row.load_cells(datum) cell = new_row.cells[cell_name] cell.inline_edit_mod = inline_edit_mod # If not allowed, neither edit mod or updating is allowed. if not cell.update_allowed: datum_display = (self.get_object_display(datum) or "N/A") LOG.info('Permission denied to Update Action: "%s"', datum_display) return HttpResponse(status=401) # If it is post request, we are updating the cell. if request.method == "POST": return self.inline_update_action(request, datum, cell, obj_id, cell_name) error = False except Exception: datum = None error = exceptions.handle(request, ignore=True) if request.is_ajax(): if not error: return HttpResponse(cell.render()) else: return HttpResponse(status=error.status_code) def inline_update_action(self, request, datum, cell, obj_id, cell_name): """Handling update by POST of the cell.""" new_cell_value = request.POST.get( cell_name + '__' + obj_id, None) if issubclass(cell.column.form_field.__class__, forms.Field): try: # using Django Form Field to parse the # right value from POST and to validate it new_cell_value = ( cell.column.form_field.clean( new_cell_value)) cell.update_action.action( self.request, datum, obj_id, cell_name, new_cell_value) response = { 'status': 'updated', 'message': '' } return HttpResponse( json.dumps(response), status=200, content_type="application/json") except core_exceptions.ValidationError: # if there is a validation error, I will # return the message to the client exc_type, exc_value, exc_traceback = ( sys.exc_info()) response = { 'status': 'validation_error', 'message': ' '.join(exc_value.messages)} return HttpResponse( json.dumps(response), status=400, content_type="application/json") def maybe_handle(self): """Handles table actions if needed. It determines whether the request should be handled by any action on this table after data has been loaded. """ request = self.request table_name, action_name, obj_id = self.check_handler(request) if table_name == self.name and action_name: action_names = [action.name for action in self.base_actions.values() if not action.preempt] # do not run preemptive actions here if action_name in action_names: return self.take_action(action_name, obj_id) return None def sanitize_id(self, obj_id): """Override to modify an incoming obj_id to match existing API. It is used to modify an incoming obj_id (used in Horizon) to the data type or format expected by the API. """ return obj_id def get_object_id(self, datum): """Returns the identifier for the object this row will represent. By default this returns an ``id`` attribute on the given object, but this can be overridden to return other values. .. warning:: Make sure that the value returned is a unique value for the id otherwise rendering issues can occur. """ return datum.id def get_object_display_key(self, datum): return 'name' def get_object_display(self, datum): """Returns a display name that identifies this object. By default, this returns a ``name`` attribute from the given object, but this can be overridden to return other values. """ display_key = self.get_object_display_key(datum) return getattr(datum, display_key, None) def has_prev_data(self): """Returns a boolean value indicating whether there is previous data. Returns True if there is previous data available to this table from the source (generally an API). The method is largely meant for internal use, but if you want to override it to provide custom behavior you can do so at your own risk. """ return self._meta.has_prev_data def has_more_data(self): """Returns a boolean value indicating whether there is more data. Returns True if there is more data available to this table from the source (generally an API). The method is largely meant for internal use, but if you want to override it to provide custom behavior you can do so at your own risk. """ return self._meta.has_more_data def get_prev_marker(self): """Returns the identifier for the first object in the current data set. The return value will be used as marker/limit-based paging in the API. """ return http.urlquote_plus(self.get_object_id(self.data[0])) \ if self.data else '' def get_marker(self): """Returns the identifier for the last object in the current data set. The return value will be used as marker/limit-based paging in the API. """ return http.urlquote_plus(self.get_object_id(self.data[-1])) \ if self.data else '' def get_prev_pagination_string(self): """Returns the query parameter string to paginate to the prev page.""" return "=".join([self._meta.prev_pagination_param, self.get_prev_marker()]) def get_pagination_string(self): """Returns the query parameter string to paginate to the next page.""" return "=".join([self._meta.pagination_param, self.get_marker()]) def calculate_row_status(self, statuses): """Returns a boolean value determining the overall row status. It is detremined based on the dictionary of column name to status mappings passed in. By default, it uses the following logic: #. If any statuses are ``False``, return ``False``. #. If no statuses are ``False`` but any or ``None``, return ``None``. #. If all statuses are ``True``, return ``True``. This provides the greatest protection against false positives without weighting any particular columns. The ``statuses`` parameter is passed in as a dictionary mapping column names to their statuses in order to allow this function to be overridden in such a way as to weight one column's status over another should that behavior be desired. """ values = statuses.values() if any([status is False for status in values]): return False elif any([status is None for status in values]): return None else: return True def get_row_status_class(self, status): """Returns a css class name determined by the status value. This class name is used to indicate the status of the rows in the table if any ``status_columns`` have been specified. """ if status is True: return "status_up" elif status is False: return "status_down" else: return "warning" def get_columns(self): """Returns this table's columns including auto-generated ones.""" return self.columns.values() def get_rows(self): """Return the row data for this table broken out by columns.""" rows = [] try: for datum in self.filtered_data: row = self._meta.row_class(self, datum) if self.get_object_id(datum) == self.current_item_id: self.selected = True row.classes.append('current_selected') rows.append(row) except Exception: # Exceptions can be swallowed at the template level here, # re-raising as a TemplateSyntaxError makes them visible. LOG.exception("Error while rendering table rows.") exc_info = sys.exc_info() raise six.reraise(template.TemplateSyntaxError, exc_info[1], exc_info[2]) return rows def css_classes(self): """Returns the additional CSS class to be added to
tag.""" return self._meta.css_classes horizon-13.0.0/horizon/tables/__init__.py0000666000175100017510000000350013245511643020366 0ustar zuulzuul00000000000000# 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. # Convenience imports for public API components. # Importing non-modules that are not used explicitly from horizon.tables.actions import Action from horizon.tables.actions import BatchAction from horizon.tables.actions import DeleteAction from horizon.tables.actions import FilterAction from horizon.tables.actions import FixedFilterAction from horizon.tables.actions import LinkAction from horizon.tables.actions import NameFilterAction from horizon.tables.actions import UpdateAction from horizon.tables.base import Column from horizon.tables.base import DataTable from horizon.tables.base import Row from horizon.tables.base import WrappingColumn from horizon.tables.views import DataTableView from horizon.tables.views import MixedDataTableView from horizon.tables.views import MultiTableMixin from horizon.tables.views import MultiTableView from horizon.tables.views import PagedTableMixin __all__ = [ 'Action', 'BatchAction', 'DeleteAction', 'FilterAction', 'FixedFilterAction', 'LinkAction', 'NameFilterAction', 'UpdateAction', 'Column', 'DataTable', 'Row', 'WrappingColumn', 'DataTableView', 'MixedDataTableView', 'MultiTableMixin', 'MultiTableView', 'PagedTableMixin', ] horizon-13.0.0/horizon/tables/formset.py0000666000175100017510000001544013245511643020314 0ustar zuulzuul00000000000000# 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 collections import logging import sys import six from django import template from django.template import loader from horizon.tables import base as horizon_tables LOG = logging.getLogger(__name__) class FormsetCell(horizon_tables.Cell): """A DataTable cell that knows about its field from the formset.""" def __init__(self, *args, **kwargs): super(FormsetCell, self).__init__(*args, **kwargs) try: self.field = (self.row.form or {})[self.column.name] except KeyError: self.field = None else: if self.field.errors: self.attrs['class'] = (self.attrs.get('class', '') + ' error form-group') self.attrs['title'] = ' '.join( six.text_type(error) for error in self.field.errors) class FormsetRow(horizon_tables.Row): """A DataTable row that knows about its form from the formset.""" template_path = 'horizon/common/_formset_table_row.html' def __init__(self, column, datum, form): self.form = form super(FormsetRow, self).__init__(column, datum) if not self.cells: # We need to be able to handle empty rows, because there may # be extra empty forms in a formset. The original DataTable breaks # on this, because it sets self.cells to [], but later expects a # OrderedDict. We just fill self.cells with empty Cells. cells = [] for column in self.table.columns.values(): cell = self.table._meta.cell_class(None, column, self) cells.append((column.name or column.auto, cell)) self.cells = collections.OrderedDict(cells) def render(self): return loader.render_to_string(self.template_path, {"row": self, "form": self.form}) class FormsetDataTableMixin(object): """A mixin for DataTable to support Django Formsets. This works the same as the ``FormsetDataTable`` below, but can be used to add to existing DataTable subclasses. """ formset_class = None def __init__(self, *args, **kwargs): super(FormsetDataTableMixin, self).__init__(*args, **kwargs) self._formset = None # Override Meta settings, because we need custom Form and Cell classes, # and also our own template. self._meta.row_class = FormsetRow self._meta.cell_class = FormsetCell self._meta.template = 'horizon/common/_formset_table.html' def get_required_columns(self): """Lists names of columns that have required fields.""" required_columns = [] if self.formset_class: empty_form = self.get_formset().empty_form for column in self.columns.values(): field = empty_form.fields.get(column.name) if field and field.required: required_columns.append(column.name) return required_columns def _get_formset_data(self): """Formats the self.filtered_data in a way suitable for a formset.""" data = [] for datum in self.filtered_data: form_data = {} for column in self.columns.values(): value = column.get_data(datum) form_data[column.name] = value form_data['id'] = self.get_object_id(datum) data.append(form_data) return data def get_formset(self): """Provide the formset corresponding to this DataTable. Use this to validate the formset and to get the submitted data back. """ if self._formset is None: self._formset = self.formset_class( self.request.POST or None, initial=self._get_formset_data(), prefix=self._meta.name) return self._formset def get_empty_row(self): """Return a row with no data, for adding at the end of the table.""" return self._meta.row_class(self, None, self.get_formset().empty_form) def get_rows(self): """Return the row data for this table broken out by columns. The row objects get an additional ``form`` parameter, with the formset form corresponding to that row. """ try: rows = [] if self.formset_class is None: formset = [] else: formset = self.get_formset() formset.is_valid() for datum, form in six.moves.zip_longest(self.filtered_data, formset): row = self._meta.row_class(self, datum, form) if self.get_object_id(datum) == self.current_item_id: self.selected = True row.classes.append('current_selected') rows.append(row) except Exception: # Exceptions can be swallowed at the template level here, # re-raising as a TemplateSyntaxError makes them visible. LOG.exception("Error while rendering table rows.") exc_info = sys.exc_info() raise six.reraise(template.TemplateSyntaxError, exc_info[1], exc_info[2]) return rows def get_object_id(self, datum): # We need to support ``None`` when there are more forms than data. if datum is None: return None return super(FormsetDataTableMixin, self).get_object_id(datum) class FormsetDataTable(FormsetDataTableMixin, horizon_tables.DataTable): """A DataTable with support for Django Formsets. Note that :attr:`horizon.tables.DataTableOptions.row_class` and :attr:`horizon.tables.DataTaleOptions.cell_class` are overwritten in this class, so setting them in ``Meta`` has no effect. .. attribute:: formset_class A class made with ``django.forms.formsets.formset_factory`` containing the definition of the formset to use with this data table. The columns that are named the same as the formset fields will be replaced with form widgets in the table. Any hidden fields from the formset will also be included. The fields that are not hidden and don't correspond to any column will not be included in the form. """ horizon-13.0.0/horizon/tabs/0000775000175100017510000000000013245512210015723 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/tabs/views.py0000666000175100017510000001463713245511643017460 0ustar zuulzuul00000000000000# 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. from django import http from horizon import exceptions from horizon import tables from horizon.tabs.base import TableTab from horizon import views class TabView(views.HorizonTemplateView): """A generic view for displaying a :class:`horizon.tabs.TabGroup`. This view handles selecting specific tabs and deals with AJAX requests gracefully. .. attribute:: tab_group_class The only required attribute for ``TabView``. It should be a class which inherits from :class:`horizon.tabs.TabGroup`. """ tab_group_class = None _tab_group = None def __init__(self): if not self.tab_group_class: raise AttributeError("You must set the tab_group_class attribute " "on %s." % self.__class__.__name__) def get_tabs(self, request, **kwargs): """Returns the initialized tab group for this view.""" if self._tab_group is None: self._tab_group = self.tab_group_class(request, **kwargs) return self._tab_group def get_context_data(self, **kwargs): """Adds the ``tab_group`` variable to the context data.""" context = super(TabView, self).get_context_data(**kwargs) try: tab_group = self.get_tabs(self.request, **kwargs) context["tab_group"] = tab_group # Make sure our data is pre-loaded to capture errors. context["tab_group"].load_tab_data() except Exception: exceptions.handle(self.request) return context def handle_tabbed_response(self, tab_group, context): """Sends back an AJAX-appropriate response for the tab group if needed. Otherwise renders the response as normal. """ if self.request.is_ajax(): if tab_group.selected: return http.HttpResponse(tab_group.selected.render()) else: return http.HttpResponse(tab_group.render()) return self.render_to_response(context) def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) return self.handle_tabbed_response(context["tab_group"], context) class TabbedTableView(tables.MultiTableMixin, TabView): def __init__(self, *args, **kwargs): super(TabbedTableView, self).__init__(*args, **kwargs) self.table_classes = [] self._table_dict = {} def load_tabs(self): """Loads the tab group. It compiles the table instances for each table attached to any :class:`horizon.tabs.TableTab` instances on the tab group. This step is necessary before processing any tab or table actions. """ tab_group = self.get_tabs(self.request, **self.kwargs) tabs = tab_group.get_tabs() for tab in [t for t in tabs if issubclass(t.__class__, TableTab)]: self.table_classes.extend(tab.table_classes) for table in tab._tables.values(): self._table_dict[table._meta.name] = {'table': table, 'tab': tab} def get_tables(self): """A no-op on this class. Tables are handled at the tab level.""" # Override the base class implementation so that the MultiTableMixin # doesn't freak out. We do the processing at the TableTab level. return {} def handle_table(self, table_dict): """Loads the table data based on a given table_dict and handles them. For the given dict containing a ``DataTable`` and a ``TableTab`` instance, it loads the table data for that tab and calls the table's :meth:`~horizon.tables.DataTable.maybe_handle` method. The return value will be the result of ``maybe_handle``. """ table = table_dict['table'] tab = table_dict['tab'] tab.load_table_data() table_name = table._meta.name tab._tables[table_name]._meta.has_prev_data = self.has_prev_data(table) tab._tables[table_name]._meta.has_more_data = self.has_more_data(table) handled = tab._tables[table_name].maybe_handle() return handled def get(self, request, *args, **kwargs): self.load_tabs() # Gather our table instances. It's important that they're the # actual instances and not the classes! table_instances = [t['table'] for t in self._table_dict.values()] # Early out before any tab or table data is loaded for table in table_instances: preempted = table.maybe_preempt() if preempted: return preempted # If we have an action, determine if it belongs to one of our tables. # We don't iterate through all of the tables' maybes_handle # methods; just jump to the one that's got the matching name. table_name, action, obj_id = tables.DataTable.check_handler(request) if table_name in self._table_dict: handled = self.handle_table(self._table_dict[table_name]) if handled: return handled context = self.get_context_data(**kwargs) return self.handle_tabbed_response(context["tab_group"], context) def post(self, request, *args, **kwargs): # Direct POST to its appropriate tab # Note some table actions like filter do not have an 'action' if 'action' in request.POST: targetslug = request.POST['action'].split('__')[0] tabs = self.get_tabs(self.request, **self.kwargs).get_tabs() matches = [tab for tab in tabs if tab.slug == targetslug] if matches: # Call POST on first match only. There shouldn't be a case # where multiple tabs have the same slug and processing the # request twice could lead to unpredictable behavior. matches[0].post(request, *args, **kwargs) # GET and POST handling are the same return self.get(request, *args, **kwargs) horizon-13.0.0/horizon/tabs/base.py0000666000175100017510000004210613245511643017225 0ustar zuulzuul00000000000000# 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. from collections import OrderedDict import sys import six from django.template.loader import render_to_string from django.template import TemplateSyntaxError from horizon import exceptions from horizon.utils import html SEPARATOR = "__" CSS_TAB_GROUP_CLASSES = ["nav", "nav-tabs", "ajax-tabs"] CSS_ACTIVE_TAB_CLASSES = ["active"] CSS_DISABLED_TAB_CLASSES = ["disabled"] class TabGroup(html.HTMLElement): """A container class which knows how to manage and render Tab objects. .. attribute:: slug The URL slug and pseudo-unique identifier for this tab group. .. attribute:: template_name The name of the template which will be used to render this tab group. Default: ``"horizon/common/_tab_group.html"`` .. attribute:: sticky Boolean to control whether the active tab state should be stored across requests for a given user. (State storage is all done client-side.) .. attribute:: show_single_tab Boolean to control whether the tab bar is shown when the tab group has only one tab. Default: ``False`` .. attribute:: param_name The name of the GET request parameter which will be used when requesting specific tab data. Default: ``tab``. .. attribute:: classes A list of CSS classes which should be displayed on this tab group. .. attribute:: attrs A dictionary of HTML attributes which should be rendered into the markup for this tab group. .. attribute:: selected Read-only property which is set to the instance of the currently-selected tab if there is one, otherwise ``None``. .. attribute:: active Read-only property which is set to the value of the current active tab. This may not be the same as the value of ``selected`` if no specific tab was requested via the ``GET`` parameter. """ slug = None template_name = "horizon/common/_tab_group.html" param_name = 'tab' sticky = False show_single_tab = False _selected = None _active = None @property def selected(self): return self._selected @property def active(self): return self._active def __init__(self, request, **kwargs): super(TabGroup, self).__init__() if not hasattr(self, "tabs"): raise NotImplementedError('%s must declare a "tabs" attribute.' % self.__class__) if not self.slug: raise NotImplementedError('%s must declare a "slug" attribute.' % self.__class__) self.request = request self.kwargs = kwargs self._data = None tab_instances = [] for tab in self.tabs: tab_instances.append((tab.slug, tab(self, request))) self._tabs = OrderedDict(tab_instances) if self.sticky: self.attrs['data-sticky-tabs'] = 'sticky' if not self._set_active_tab(): self.tabs_not_available() def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.slug) def load_tab_data(self): """Preload all data that for the tabs that will be displayed.""" for tab in self._tabs.values(): if tab.load and not tab.data_loaded: try: tab._data = tab.get_context_data(self.request) except Exception: tab._data = False exceptions.handle(self.request) def get_id(self): """Returns the id for this tab group. Defaults to the value of the tab group's :attr:`horizon.tabs.Tab.slug`. """ return self.slug def get_default_classes(self): """Returns a list of the default classes for the tab group. Defaults to ``["nav", "nav-tabs", "ajax-tabs"]``. """ default_classes = super(TabGroup, self).get_default_classes() default_classes.extend(CSS_TAB_GROUP_CLASSES) return default_classes def tabs_not_available(self): """The fallback handler if no tabs are either allowed or enabled. In the event that no tabs are either allowed or enabled, this method is the fallback handler. By default it's a no-op, but it exists to make redirecting or raising exceptions possible for subclasses. """ pass def _set_active_tab(self): marked_active = None # See if we have a selected tab via the GET parameter. tab = self.get_selected_tab() if tab: tab._active = True self._active = tab marked_active = tab # Iterate through to mark them all accordingly. for tab in self._tabs.values(): if tab._allowed and tab._enabled and not marked_active: tab._active = True self._active = tab marked_active = True elif tab == marked_active: continue else: tab._active = False return marked_active def render(self): """Renders the HTML output for this tab group.""" return render_to_string(self.template_name, {"tab_group": self}) def get_tabs(self): """Returns a list of the allowed tabs for this tab group.""" return [tab for tab in self._tabs.values() if tab._allowed] def get_tab(self, tab_name, allow_disabled=False): """Returns a specific tab from this tab group. If the tab is not allowed or not enabled this method returns ``None``. If the tab is disabled but you wish to return it anyway, you can pass ``True`` to the allow_disabled argument. """ tab = self._tabs.get(tab_name, None) if tab and tab._allowed and (tab._enabled or allow_disabled): return tab return None def get_loaded_tabs(self): return [tab for tab in self._tabs.values() if self.get_tab(tab.slug)] def get_selected_tab(self): """Returns the tab specific by the GET request parameter. In the event that there is no GET request parameter, the value of the query parameter is invalid, or the tab is not allowed/enabled, the return value of this function is None. """ selected = self.request.GET.get(self.param_name, None) if selected: try: tab_group, tab_name = selected.split(SEPARATOR) except ValueError: return None if tab_group == self.get_id(): self._selected = self.get_tab(tab_name) return self._selected class Tab(html.HTMLElement): """A reusable interface for constructing a tab within a TabGroup. .. attribute:: name The display name for the tab which will be rendered as the text for the tab element in the HTML. Required. .. attribute:: slug The URL slug and id attribute for the tab. This should be unique for a given tab group. Required. .. attribute:: preload Determines whether the contents of the tab should be rendered into the page's HTML when the tab group is rendered, or whether it should be loaded dynamically when the tab is selected. Default: ``True``. .. attribute:: classes A list of CSS classes which should be displayed on this tab. .. attribute:: attrs A dictionary of HTML attributes which should be rendered into the markup for this tab. .. attribute:: load Read-only access to determine whether or not this tab's data should be loaded immediately. .. attribute:: permissions A list of permission names which this tab requires in order to be displayed. Defaults to an empty list (``[]``). """ name = None slug = None preload = True _active = None permissions = [] def __init__(self, tab_group, request=None): super(Tab, self).__init__() # Priority: constructor, class-defined, fallback if not self.name: raise ValueError("%s must have a name." % self.__class__.__name__) self.name = six.text_type(self.name) # Force unicode. if not self.slug: raise ValueError("%s must have a slug." % self.__class__.__name__) self.tab_group = tab_group self.request = request if request: self._allowed = self.allowed(request) and ( self._has_permissions(request)) self._enabled = self.enabled(request) def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.slug) def _has_permissions(self, request): return request.user.has_perms(self.permissions) def is_active(self): """Method to access whether or not this tab is the active tab.""" if self._active is None: self.tab_group._set_active_tab() return self._active @property def load(self): load_preloaded = self.preload or self.is_active() return load_preloaded and self._allowed and self._enabled @property def data(self): if getattr(self, "_data", None) is None: self._data = self.get_context_data(self.request) return self._data @property def data_loaded(self): return getattr(self, "_data", None) is not None def render(self): """Renders the tab to HTML. :meth:`~horizon.tabs.Tab.get_context_data` method and the :meth:`~horizon.tabs.Tab.get_template_name` method are called. If :attr:`~horizon.tabs.Tab.preload` is ``False`` and ``force_load`` is not ``True``, or either :meth:`~horizon.tabs.Tab.allowed` or :meth:`~horizon.tabs.Tab.enabled` returns ``False`` this method will return an empty string. """ if not self.load: return '' try: context = self.data except exceptions.Http302: raise except Exception: exc_type, exc_value, exc_traceback = sys.exc_info() raise six.reraise(TemplateSyntaxError, exc_value, exc_traceback) return render_to_string(self.get_template_name(self.request), context) def get_id(self): """Returns the id for this tab. Defaults to ``"{{ tab_group.slug }}__{{ tab.slug }}"``. """ return SEPARATOR.join([self.tab_group.slug, self.slug]) def get_query_string(self): return "=".join((self.tab_group.param_name, self.get_id())) def get_default_classes(self): """Returns a list of the default classes for the tab. Defaults to and empty list (``[]``), however additional classes may be added depending on the state of the tab as follows: If the tab is the active tab for the tab group, in which the class ``"active"`` will be added. If the tab is not enabled, the classes the class ``"disabled"`` will be added. """ default_classes = super(Tab, self).get_default_classes() if self.is_active(): default_classes.extend(CSS_ACTIVE_TAB_CLASSES) if not self._enabled: default_classes.extend(CSS_DISABLED_TAB_CLASSES) return default_classes def get_template_name(self, request): """Returns the name of the template to be used for rendering this tab. By default it returns the value of the ``template_name`` attribute on the ``Tab`` class. """ if not hasattr(self, "template_name"): raise AttributeError("%s must have a template_name attribute or " "override the get_template_name method." % self.__class__.__name__) return self.template_name def get_context_data(self, request, **kwargs): """Return a dictionary of context data used to render the tab. Required. """ return kwargs def enabled(self, request): """Determines whether or not the tab should be accessible. For example, the tab should be rendered into the HTML on load and respond to a click event. If a tab returns ``False`` from ``enabled`` it will ignore the value of ``preload`` and only render the HTML of the tab after being clicked. The default behavior is to return ``True`` for all cases. """ return True def allowed(self, request): """Determines whether or not the tab is displayed. Tab instances can override this method to specify conditions under which this tab should not be shown at all by returning ``False``. The default behavior is to return ``True`` for all cases. """ return True def post(self, request, *args, **kwargs): """Handles POST data sent to a tab. Tab instances can override this method to have tab-specific POST logic without polluting the TabView code. The default behavior is to ignore POST data. """ pass class TableTab(Tab): """A Tab class which knows how to deal with DataTable classes inside of it. This distinct class is required due to the complexity involved in handling both dynamic tab loading, dynamic table updating and table actions all within one view. .. attribute:: table_classes An iterable containing the :class:`~horizon.tables.DataTable` classes which this tab will contain. Equivalent to the :attr:`~horizon.tables.MultiTableView.table_classes` attribute on :class:`~horizon.tables.MultiTableView`. For each table class you need to define a corresponding ``get_{{ table_name }}_data`` method as with :class:`~horizon.tables.MultiTableView`. """ table_classes = None def __init__(self, tab_group, request): super(TableTab, self).__init__(tab_group, request) if not self.table_classes: class_name = self.__class__.__name__ raise NotImplementedError("You must define a table_class " "attribute on %s" % class_name) # Instantiate our table classes but don't assign data yet table_instances = [(table._meta.name, table(request, **tab_group.kwargs)) for table in self.table_classes] self._tables = OrderedDict(table_instances) self._table_data_loaded = False def load_table_data(self): """Calls the ``get_{{ table_name }}_data`` methods for each table class. When returning, the loaded data is set on the tables. """ # We only want the data to be loaded once, so we track if we have... if not self._table_data_loaded: for table_name, table in self._tables.items(): # Fetch the data function. func_name = "get_%s_data" % table_name data_func = getattr(self, func_name, None) if data_func is None: cls_name = self.__class__.__name__ raise NotImplementedError( "You must define a %(func_name)s method on" " %(cls_name)s." % {'func_name': func_name, 'cls_name': cls_name}) # Load the data. table.data = data_func() table._meta.has_prev_data = self.has_prev_data(table) table._meta.has_more_data = self.has_more_data(table) # Mark our data as loaded so we don't run the loaders again. self._table_data_loaded = True def get_context_data(self, request, **kwargs): """Adds a ``{{ table_name }}_table`` item to the context for each table. The target tables are specified by the :attr:`~horizon.tabs.TableTab.table_classes` attribute. If only one table class is provided, a shortcut ``table`` context variable is also added containing the single table. """ context = super(TableTab, self).get_context_data(request, **kwargs) # If the data hasn't been manually loaded before now, # make certain it's loaded before setting the context. self.load_table_data() for table_name, table in self._tables.items(): # If there's only one table class, add a shortcut name as well. if len(self.table_classes) == 1: context["table"] = table context["%s_table" % table_name] = table return context def has_prev_data(self, table): return False def has_more_data(self, table): return False class DetailTabsGroup(TabGroup): template_name = "horizon/common/_detail_tab_group.html" horizon-13.0.0/horizon/tabs/__init__.py0000666000175100017510000000177713245511643020063 0ustar zuulzuul00000000000000# 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. # Importing non-modules that are not used explicitly from horizon.tabs.base import DetailTabsGroup from horizon.tabs.base import Tab from horizon.tabs.base import TabGroup from horizon.tabs.base import TableTab from horizon.tabs.views import TabbedTableView from horizon.tabs.views import TabView __all__ = [ 'DetailTabsGroup', 'Tab', 'TabGroup', 'TableTab', 'TabbedTableView', 'TabView', ] horizon-13.0.0/horizon/decorators.py0000666000175100017510000001003013245511643017516 0ustar zuulzuul00000000000000# Copyright 2012 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # # Copyright 2012 CRS4 # # 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. """ General-purpose decorators for use with Horizon. """ import functools from django.utils.decorators import available_attrs from django.utils.translation import ugettext_lazy as _ def _current_component(view_func, dashboard=None, panel=None): """Sets the currently-active dashboard and/or panel on the request.""" @functools.wraps(view_func, assigned=available_attrs(view_func)) def dec(request, *args, **kwargs): if dashboard: request.horizon['dashboard'] = dashboard if panel: request.horizon['panel'] = panel return view_func(request, *args, **kwargs) return dec def require_auth(view_func): """Performs user authentication check. Similar to Django's `login_required` decorator, except that this throws :exc:`~horizon.exceptions.NotAuthenticated` exception if the user is not signed-in. """ from horizon.exceptions import NotAuthenticated @functools.wraps(view_func, assigned=available_attrs(view_func)) def dec(request, *args, **kwargs): if request.user.is_authenticated(): return view_func(request, *args, **kwargs) raise NotAuthenticated(_("Please log in to continue.")) return dec def require_perms(view_func, required): """Enforces permission-based access controls. :param list required: A tuple of permission names, all of which the request user must possess in order access the decorated view. Example usage:: from horizon.decorators import require_perms @require_perms(['foo.admin', 'foo.member']) def my_view(request): ... Raises a :exc:`~horizon.exceptions.NotAuthorized` exception if the requirements are not met. """ from horizon.exceptions import NotAuthorized # We only need to check each permission once for a view, so we'll use a set current_perms = getattr(view_func, '_required_perms', set([])) view_func._required_perms = current_perms | set(required) @functools.wraps(view_func, assigned=available_attrs(view_func)) def dec(request, *args, **kwargs): if request.user.is_authenticated(): if request.user.has_perms(view_func._required_perms): return view_func(request, *args, **kwargs) raise NotAuthorized(_("You are not authorized to access %s") % request.path) # If we don't have any permissions, just return the original view. if required: return dec else: return view_func def require_component_access(view_func, component): """Perform component can_access check to access the view. :param component containing the view (panel or dashboard). Raises a :exc:`~horizon.exceptions.NotAuthorized` exception if the user cannot access the component containing the view. By example the check of component policy rules will be applied to its views. """ from horizon.exceptions import NotAuthorized @functools.wraps(view_func, assigned=available_attrs(view_func)) def dec(request, *args, **kwargs): if not component.can_access({'request': request}): raise NotAuthorized(_("You are not authorized to access %s") % request.path) return view_func(request, *args, **kwargs) return dec horizon-13.0.0/horizon/loaders.py0000666000175100017510000000430213245511643017007 0ustar zuulzuul00000000000000# 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. """ Wrapper for loading templates from "templates" directories in panel modules. """ import io import os import django from django.conf import settings from django.template.engine import Engine from django.template.loaders.base import Loader as tLoaderCls from django.utils._os import safe_join if django.VERSION >= (1, 9): from django.template.exceptions import TemplateDoesNotExist else: from django.template.base import TemplateDoesNotExist # Set up a cache of the panel directories to search. panel_template_dirs = {} class TemplateLoader(tLoaderCls): is_usable = True def get_template_sources(self, template_name): bits = template_name.split('/', 2) if len(bits) == 3: dash_name, panel_name, remainder = bits key = os.path.join(dash_name, panel_name) if key in panel_template_dirs: template_dir = panel_template_dirs[key] try: yield safe_join(template_dir, panel_name, remainder) except UnicodeDecodeError: # The template dir name wasn't valid UTF-8. raise except ValueError: # The joined path was located outside of template_dir. pass def load_template_source(self, template_name, template_dirs=None): for path in self.get_template_sources(template_name): try: with io.open(path, encoding=settings.FILE_CHARSET) as file: return (file.read(), path) except IOError: pass raise TemplateDoesNotExist(template_name) e = Engine() _loader = TemplateLoader(e) horizon-13.0.0/horizon/version.py0000666000175100017510000000125613245511643017050 0ustar zuulzuul00000000000000# Copyright 2012 OpenStack Foundation # # 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 pbr.version version_info = pbr.version.VersionInfo('horizon') horizon-13.0.0/horizon/utils/0000775000175100017510000000000013245512210016132 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/utils/validators.py0000666000175100017510000000521213245511643020667 0ustar zuulzuul00000000000000# 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. import re from oslo_utils import netutils from django.core.exceptions import ValidationError from django.core import validators from django.utils.translation import ugettext_lazy as _ from horizon import conf def validate_port_range(port): if not netutils.is_valid_port(port): raise ValidationError(_("Not a valid port number")) def validate_icmp_type_range(icmp_type): if not netutils.is_valid_icmp_type(icmp_type): if icmp_type == -1: return raise ValidationError(_("Not a valid ICMP type")) def validate_icmp_code_range(icmp_code): if not netutils.is_valid_icmp_code(icmp_code): if icmp_code == -1: return raise ValidationError(_("Not a valid ICMP code")) def validate_ip_protocol(ip_proto): if ip_proto < -1 or ip_proto > 255: raise ValidationError(_("Not a valid IP protocol number")) def password_validator(): return conf.HORIZON_CONFIG["password_validator"]["regex"] def password_validator_msg(): return conf.HORIZON_CONFIG["password_validator"]["help_text"] def validate_port_or_colon_separated_port_range(port_range): """Accepts a port number or a single-colon separated range.""" if port_range.count(':') > 1: raise ValidationError(_("One colon allowed in port range")) ports = port_range.split(':') for port in ports: validate_port_range(port) def validate_metadata(value): error_msg = _('Invalid metadata entry. Use comma-separated' ' key=value pairs') if value: specs = value.split(",") for spec in specs: keyval = spec.split("=") # ensure both sides of "=" exist, but allow blank value if not len(keyval) == 2 or not keyval[0]: raise ValidationError(error_msg) # Same as POSIX [:print:]. Accordingly, diacritics are disallowed. PRINT_REGEX = re.compile(r'^[\x20-\x7E]*$') validate_printable_ascii = validators.RegexValidator( PRINT_REGEX, _("The string may only contain ASCII printable characters."), "invalid_characters") horizon-13.0.0/horizon/utils/memoized.py0000666000175100017510000001606713245511653020343 0ustar zuulzuul00000000000000# 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. import collections import functools import threading import warnings import weakref class UnhashableKeyWarning(RuntimeWarning): """Raised when trying to memoize a function with an unhashable argument.""" def _try_weakref(arg, remove_callback): """Return a weak reference to arg if possible, or arg itself if not.""" try: arg = weakref.ref(arg, remove_callback) except TypeError: # Not all types can have a weakref. That includes strings # and floats and such, so just pass them through directly. pass return arg def _get_key(args, kwargs, remove_callback): """Calculate the cache key, using weak references where possible.""" # Use tuples, because lists are not hashable. weak_args = tuple(_try_weakref(arg, remove_callback) for arg in args) # Use a tuple of (key, values) pairs, because dict is not hashable. # Sort it, so that we don't depend on the order of keys. weak_kwargs = tuple(sorted( (key, _try_weakref(value, remove_callback)) for (key, value) in kwargs.items())) return weak_args, weak_kwargs def memoized(func): """Decorator that caches function calls. Caches the decorated function's return value the first time it is called with the given arguments. If called later with the same arguments, the cached value is returned instead of calling the decorated function again. The cache uses weak references to the passed arguments, so it doesn't keep them alive in memory forever. """ # The dictionary in which all the data will be cached. This is a separate # instance for every decorated function, and it's stored in a closure of # the wrapped function. cache = {} locks = collections.defaultdict(threading.Lock) @functools.wraps(func) def wrapped(*args, **kwargs): # We need to have defined key early, to be able to use it in the # remove() function, but we calculate the actual value of the key # later on, because we need the remove() function for that. key = None def remove(ref): """A callback to remove outdated items from cache.""" try: # The key here is from closure, and is calculated later. del cache[key] del locks[key] except KeyError: # Some other weak reference might have already removed that # key -- in that case we don't need to do anything. pass key = _get_key(args, kwargs, remove) try: with locks[key]: try: # We want cache hit to be as fast as possible, and don't # really care much about the speed of a cache miss, because # it will only happen once and likely calls some external # API, database, or some other slow thing. That's why the # hit is in straightforward code, and the miss is in an # exception. value = cache[key] except KeyError: value = cache[key] = func(*args, **kwargs) except TypeError: # The calculated key may be unhashable when an unhashable # object, such as a list, is passed as one of the arguments. In # that case, we can't cache anything and simply always call the # decorated function. warnings.warn( "The key %r is not hashable and cannot be memoized." % (key,), UnhashableKeyWarning, 2) value = func(*args, **kwargs) return value return wrapped # We can use @memoized for methods now too, because it uses weakref and so # it doesn't keep the instances in memory forever. We might want to separate # them in the future, however. memoized_method = memoized def memoized_with_request(request_func, request_index=0): """Decorator for caching functions which receive a request argument memoized functions with a request argument are memoized only during the rendering of a single view because the request argument is a new request instance on each view. If you want a function to be memoized for multiple views use this decorator. It replaces the request argument in the call to the decorated function with the result of calling request_func on that request object. request_function is a function which will receive the request argument. request_index indicates which argument of the decorated function is the request object to pass into request_func, which will also be replaced by the result of request_func being called. your memoized function will instead receive request_func(request) passed as argument at the request_index. The intent of that function is to extract the information needed from the request, and thus the memoizing will operate just on that part of the request that is relevant to the function being memoized. short example:: @memoized def _get_api_client(username, token_id, project_id, auth_url) return api_client.Client(username, token_id, project_id, auth_url) def get_api_client(request): return _api_client(request.user.username, request.user.token.id, request.user.tenant_id) @memoized_with_request(get_api_client) def some_api_function(api_client, *args, **kwargs): # is like returning get_api_client( # request).some_method(*args, **kwargs) # but with memoization. return api_client.some_method(*args, **kwargs) @memoized_with_request(get_api_client, 1) def some_other_funt(param, api_client, other_param): # The decorated function will be called this way: # some_other_funt(param, request, other_param) # but will be called behind the scenes this way: # some_other_funt(param, get_api_client(request), other_param) return api_client.some_method(param, other_param) See openstack_dashboard.api.nova for a complete example. """ def wrapper(func): memoized_func = memoized(func) @functools.wraps(func) def wrapped(*args, **kwargs): args = list(args) request = args.pop(request_index) args.insert(request_index, request_func(request)) return memoized_func(*args, **kwargs) return wrapped return wrapper horizon-13.0.0/horizon/utils/html.py0000666000175100017510000000517513245511643017473 0ustar zuulzuul00000000000000# 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.forms.utils import flatatt class HTMLElement(object): """A generic base class that gracefully handles html-style attributes.""" def __init__(self): self.attrs = getattr(self, "attrs", {}) self.classes = getattr(self, "classes", []) def get_default_classes(self): """Returns an iterable of default classes. They will be combined with any other declared classes. """ return [] def get_default_attrs(self): """Returns a dict of default attributes. They will be combined with other declared attributes. """ return {} def get_final_attrs(self, classes=True): """Returns a dict containing the final attributes to be rendered.""" final_attrs = copy.copy(self.get_default_attrs()) final_attrs.update(self.attrs) if classes: final_attrs['class'] = self.get_final_css() else: final_attrs.pop('class', None) return final_attrs def get_final_css(self): """Returns a final css class concatenated string.""" default = " ".join(self.get_default_classes()) defined = self.attrs.get('class', '') additional = " ".join(getattr(self, "classes", [])) non_empty = [test for test in (defined, default, additional) if test] final_classes = " ".join(non_empty).strip() return final_classes @property def attr_string(self): """Returns a flattened string of HTML attributes. HTML attributes are flattened based on the ``attrs`` dict provided to the class. """ return flatatt(self.get_final_attrs()) @property def attr_string_nc(self): """Returns a flattened string of HTML attributes. HTML attributes are flattened based on the ``attrs`` dict provided to the class. """ return flatatt(self.get_final_attrs(False)) @property def class_string(self): """Returns a list of class name of HTML Element in string.""" classes_str = " ".join(self.classes) return classes_str horizon-13.0.0/horizon/utils/settings.py0000666000175100017510000000172713245511643020366 0ustar zuulzuul00000000000000# 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 six from django.conf import settings from django.utils.module_loading import import_string def import_object(name_or_object): if isinstance(name_or_object, six.string_types): return import_string(name_or_object) return name_or_object def import_setting(name): """Imports an object specified either directly or as a module path.""" value = getattr(settings, name, None) return import_object(value) horizon-13.0.0/horizon/utils/units.py0000666000175100017510000001036613245511643017667 0ustar zuulzuul00000000000000# 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 decimal import pint from horizon.utils import functions # Mapping of units from Ceilometer to Pint INFORMATION_UNITS = ( ('B', 'byte'), ('KB', 'Kibyte'), ('MB', 'Mibyte'), ('GB', 'Gibyte'), ('TB', 'Tibyte'), ('PB', 'Pibyte'), ('EB', 'Eibyte'), ) TIME_UNITS = ('ns', 's', 'min', 'hr', 'day', 'week', 'month', 'year') ureg = pint.UnitRegistry() def is_supported(unit): """Returns a bool indicating whether the unit specified is supported.""" return unit in functions.get_keys(INFORMATION_UNITS) + TIME_UNITS def is_larger(unit_1, unit_2): """Returns a boolean indicating whether unit_1 is larger than unit_2. E.g: >>> is_larger('KB', 'B') True >>> is_larger('min', 'day') False """ unit_1 = functions.value_for_key(INFORMATION_UNITS, unit_1) unit_2 = functions.value_for_key(INFORMATION_UNITS, unit_2) return ureg.parse_expression(unit_1) > ureg.parse_expression(unit_2) def convert(value, source_unit, target_unit, fmt=False): """Converts value from source_unit to target_unit. Returns a tuple containing the converted value and target_unit. Having fmt set to True causes the value to be formatted to 1 decimal digit if it's a decimal or be formatted as integer if it's an integer. E.g: >>> convert(2, 'hr', 'min') (120.0, 'min') >>> convert(2, 'hr', 'min', fmt=True) (120, 'min') >>> convert(30, 'min', 'hr', fmt=True) (0.5, 'hr') """ orig_target_unit = target_unit source_unit = functions.value_for_key(INFORMATION_UNITS, source_unit) target_unit = functions.value_for_key(INFORMATION_UNITS, target_unit) q = ureg.Quantity(value, source_unit) q = q.to(ureg.parse_expression(target_unit)) value = functions.format_value(q.magnitude) if fmt else q.magnitude return value, orig_target_unit def normalize(value, unit): """Converts the value so that it belongs to some expected range. Returns the new value and new unit. E.g: >>> normalize(1024, 'KB') (1, 'MB') >>> normalize(90, 'min') (1.5, 'hr') >>> normalize(1.0, 'object') (1, 'object') """ if value < 0: raise ValueError('Negative value: %s %s.' % (value, unit)) if unit in functions.get_keys(INFORMATION_UNITS): return _normalize_information(value, unit) elif unit in TIME_UNITS: return _normalize_time(value, unit) else: # Unknown unit, just return it return functions.format_value(value), unit def _normalize_information(value, unit): value = decimal.Decimal(str(value)) while value < 1: prev_unit = functions.previous_key(INFORMATION_UNITS, unit) if prev_unit is None: break value, unit = convert(value, unit, prev_unit) while value >= 1024: next_unit = functions.next_key(INFORMATION_UNITS, unit) if next_unit is None: break value, unit = convert(value, unit, next_unit) return functions.format_value(value), unit def _normalize_time(value, unit): # Normalize time by converting to next higher unit when value is # at least 2 units value, unit = convert(value, unit, 's') if value >= 120: value, unit = convert(value, 's', 'min') if value >= 120: value, unit = convert(value, 'min', 'hr') if value >= 48: value, unit = convert(value, 'hr', 'day') if value >= 730: value, unit = convert(value, 'day', 'year') elif value >= 62: value, unit = convert(value, 'day', 'month') elif value >= 14: value, unit = convert(value, 'day', 'week') return functions.format_value(value), unit horizon-13.0.0/horizon/utils/__init__.py0000666000175100017510000000000013245511643020244 0ustar zuulzuul00000000000000horizon-13.0.0/horizon/utils/csvbase.py0000666000175100017510000001100313245511643020140 0ustar zuulzuul00000000000000# 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. from __future__ import division from csv import DictWriter from csv import writer from django.http import HttpResponse from django.http import StreamingHttpResponse from django import template as django_template import six from six import StringIO class CsvDataMixin(object): """CSV data Mixin - provides handling for CSV data. .. attribute:: columns A list of CSV column definitions. If omitted - no column titles will be shown in the result file. Optional. """ def __init__(self): self.out = StringIO() super(CsvDataMixin, self).__init__() if hasattr(self, "columns"): columns = [self.encode(col) for col in self.columns] self.writer = DictWriter(self.out, columns) self.is_dict = True else: self.writer = writer(self.out) self.is_dict = False def write_csv_header(self): if self.is_dict: try: self.writer.writeheader() except AttributeError: # For Python<2.7 self.writer.writerow(dict(zip( self.writer.fieldnames, self.writer.fieldnames))) def write_csv_row(self, args): if self.is_dict: self.writer.writerow(dict(zip( self.writer.fieldnames, [self.encode(col) for col in args]))) else: self.writer.writerow([self.encode(col) for col in args]) def encode(self, value): value = six.text_type(value) if six.PY2: # csv and StringIO cannot work with mixed encodings, # so encode all with utf-8 value = value.encode('utf-8') return value class BaseCsvResponse(CsvDataMixin, HttpResponse): """Base CSV response class. Provides handling of CSV data.""" def __init__(self, request, template, context, content_type, **kwargs): super(BaseCsvResponse, self).__init__() self['Content-Disposition'] = 'attachment; filename="%s"' % ( kwargs.get("filename", "export.csv"),) self['Content-Type'] = content_type self.context = context self.header = None if template: # Display some header info if provided as a template header_template = django_template.loader.get_template(template) self.header = header_template.render(self.context, request) if self.header: self.out.write(self.encode(self.header)) self.write_csv_header() for row in self.get_row_data(): self.write_csv_row(row) self.out.flush() self.content = self.out.getvalue() self.out.close() def get_row_data(self): return [] class BaseCsvStreamingResponse(CsvDataMixin, StreamingHttpResponse): """Base CSV Streaming class. Provides streaming response for CSV data.""" def __init__(self, request, template, context, content_type, **kwargs): super(BaseCsvStreamingResponse, self).__init__() self['Content-Disposition'] = 'attachment; filename="%s"' % ( kwargs.get("filename", "export.csv"),) self['Content-Type'] = content_type self.context = context self.header = None if template: # Display some header info if provided as a template header_template = django_template.loader.get_template(template) self.header = header_template.render(self.context, request) self._closable_objects.append(self.out) self.streaming_content = self.get_content() def buffer(self): buf = self.out.getvalue() self.out.truncate(0) return buf def get_content(self): if self.header: self.out.write(self.encode(self.header)) self.write_csv_header() yield self.buffer() for row in self.get_row_data(): self.write_csv_row(row) yield self.buffer() def get_row_data(self): return [] horizon-13.0.0/horizon/utils/filters.py0000666000175100017510000000375113245511643020175 0ustar zuulzuul00000000000000# 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. import datetime import iso8601 from django.template.defaultfilters import register from django.template.defaultfilters import timesince from django.utils.safestring import mark_safe from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @register.filter def replace_underscores(string): return string.replace("_", " ") @register.filter def parse_isotime(timestr, default=None): # This duplicates oslo timeutils parse_isotime but with a # @register.filter annotation and a silent fallback on error. try: return iso8601.parse_date(timestr) except (iso8601.ParseError, TypeError): return default or '' @register.filter def timesince_or_never(dt, default=None): """Call the Django ``timesince`` filter or a given default string. It returns the string *default* if *dt* is not a valid ``date`` or ``datetime`` object. When *default* is None, "Never" is returned. """ if default is None: default = _("Never") if isinstance(dt, datetime.date): return timesince(dt) else: return default @register.filter def timesince_sortable(dt): delta = timezone.now() - dt # timedelta.total_seconds() not supported on python < 2.7 seconds = delta.seconds + (delta.days * 24 * 3600) return mark_safe("%s" % (seconds, timesince(dt))) horizon-13.0.0/horizon/utils/lazy_encoder.py0000666000175100017510000000201513245511643021173 0ustar zuulzuul00000000000000# Copyright 2016 Red Hat, 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. from django.core.serializers.json import DjangoJSONEncoder from django.utils.encoding import force_text from django.utils.functional import Promise class LazyTranslationEncoder(DjangoJSONEncoder): """JSON encoder that resolves lazy objects like translations""" def default(self, obj): if isinstance(obj, Promise): return force_text(obj) return super(LazyTranslationEncoder, self).default(obj) horizon-13.0.0/horizon/utils/escape.py0000666000175100017510000000240413245511643017757 0ustar zuulzuul00000000000000# Copyright 2016, Rackspace, US, 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. import django.utils.html def escape(text, existing=django.utils.html.escape): # Replace our angular markup string with a different string # (which just happens to be the Django comment string) # this prevents user-supplied data from being intepreted in # our pages by angularjs, thus preventing it from being used # for XSS attacks. Note that we use {$ $} instead of the # standard {{ }} - this is configured in horizon.framework # angularjs module through $interpolateProvider. return existing(text).replace('{$', '{%').replace('$}', '%}') # this will be invoked as early as possible in settings.py def monkeypatch_escape(): django.utils.html.escape = escape horizon-13.0.0/horizon/utils/functions.py0000666000175100017510000001355613245511643020541 0ustar zuulzuul00000000000000# 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 datetime import decimal import math import re from oslo_utils import units import six from django.conf import settings from django.contrib.auth import logout from django import http from django.utils.encoding import force_text from django.utils.functional import lazy from django.utils import translation def _lazy_join(separator, strings): return separator.join([force_text(s) for s in strings]) lazy_join = lazy(_lazy_join, six.text_type) def bytes_to_gigabytes(bytes): # Converts the number of bytes to the next highest number of Gigabytes # For example 5000000 (5 Meg) would return '1' return int(math.ceil(float(bytes) / units.Gi)) def add_logout_reason(request, response, reason, status='success'): # Store the translated string in the cookie lang = translation.get_language_from_request(request) with translation.override(lang): reason = six.text_type(reason) if six.PY2: reason = reason.encode('utf-8') response.set_cookie('logout_reason', reason, max_age=10) response.set_cookie('logout_status', status, max_age=10) def logout_with_message(request, msg, redirect=True, status='success'): """Send HttpResponseRedirect to LOGOUT_URL. `msg` is a message displayed on the login page after the logout, to explain the logout reason. """ logout(request) if redirect: response = http.HttpResponseRedirect( '%s?next=%s' % (settings.LOGOUT_URL, request.path)) else: response = http.HttpResponseRedirect(settings.LOGOUT_URL) add_logout_reason(request, response, msg, status) return response def get_config_value(request, key, default, search_in_settings=True): """Retrieves the value of `key` from configuration in the following order: - from the session; if not found there then - from cookies; if not found there then - from the settings file if `search_in_settings` is True, otherwise this step is skipped; if not found there - `default` is returned """ value = request.session.get(key, request.COOKIES.get(key)) if value is None: if search_in_settings: value = getattr(settings, key, default) else: value = default if isinstance(default, int): try: value = int(value) except ValueError: value = request.session[key] = int(default) return value def save_config_value(request, response, key, value): """Sets value of key `key` to `value` in both session and cookies.""" request.session[key] = value response.set_cookie(key, value, expires=one_year_from_now()) return response def get_page_size(request): return get_config_value(request, 'API_RESULT_PAGE_SIZE', 20) def get_log_length(request): return get_config_value(request, 'INSTANCE_LOG_LENGTH', 35) def get_timezone(request): # Session and cookie store timezone as django_timezone. # In case there is no timezone neither in session nor in cookie, # use default value from settings file where it's called TIME_ZONE. return get_config_value(request, 'django_timezone', getattr(settings, 'TIME_ZONE', 'UTC')) def get_language(request): return get_config_value(request, settings.LANGUAGE_COOKIE_NAME, request.LANGUAGE_CODE, search_in_settings=False) def natural_sort(attr): return lambda x: [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', getattr(x, attr, x))] def get_keys(tuple_of_tuples): """Returns a tuple containing first component of each tuple. It processes a tuple of 2-element tuples and returns a tuple containing first component of each tuple. """ return tuple([t[0] for t in tuple_of_tuples]) def value_for_key(tuple_of_tuples, key): """Returns a value containing to the given key. It processes a tuple of 2-element tuples and returns the value corresponding to the given key. If no value is found, the key is returned. """ for t in tuple_of_tuples: if t[0] == key: return t[1] else: return key def next_key(tuple_of_tuples, key): """Returns the key which comes after the given key. It processes a tuple of 2-element tuples and returns the key which comes after the given key. """ for i, t in enumerate(tuple_of_tuples): if t[0] == key: try: return tuple_of_tuples[i + 1][0] except IndexError: return None def previous_key(tuple_of_tuples, key): """Returns the key which comes before the give key. It Processes a tuple of 2-element tuples and returns the key which comes before the given key. """ for i, t in enumerate(tuple_of_tuples): if t[0] == key: try: return tuple_of_tuples[i - 1][0] except IndexError: return None def format_value(value): """Returns the given value rounded to one decimal place if deciaml. Returns the integer if an integer is given. """ value = decimal.Decimal(str(value)) if int(value) == value: return int(value) # On Python 3, an explicit cast to float is required return float(round(value, 1)) def one_year_from_now(): now = datetime.datetime.utcnow() return now + datetime.timedelta(days=365) horizon-13.0.0/horizon/utils/scss_filter.py0000666000175100017510000000256313245511643021045 0ustar zuulzuul00000000000000# (c) Copyright 2015 Hewlett-Packard Development Company, L.P. # # 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. from django.conf import settings from django_pyscss.compressor import DjangoScssFilter from django_pyscss import DjangoScssCompiler from scss.namespace import Namespace from scss.types import String import six class HorizonScssFilter(DjangoScssFilter): def __init__(self, *args, **kwargs): super(HorizonScssFilter, self).__init__(*args, **kwargs) self.namespace = Namespace() # Add variables to the SCSS Global Namespace Here self.namespace.set_variable( '$static_url', String(six.text_type(getattr(settings, 'STATIC_URL', '/static/'))) ) # Create a compiler with the right namespace @property def compiler(self): return DjangoScssCompiler( namespace=self.namespace ) horizon-13.0.0/horizon/utils/secret_key.py0000666000175100017510000000574313245511643020665 0ustar zuulzuul00000000000000# 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. import logging import os import random import string from oslo_concurrency import lockutils class FilePermissionError(Exception): """The key file permissions are insecure.""" pass def generate_key(key_length=64): """Secret key generator. The quality of randomness depends on operating system support, see http://docs.python.org/library/random.html#random.SystemRandom. """ if hasattr(random, 'SystemRandom'): logging.info('Generating a secure random key using SystemRandom.') choice = random.SystemRandom().choice else: msg = "WARNING: SystemRandom not present. Generating a random "\ "key using random.choice (NOT CRYPTOGRAPHICALLY SECURE)." logging.warning(msg) choice = random.choice return ''.join(map(lambda x: choice(string.digits + string.ascii_letters), range(key_length))) def read_from_file(key_file='.secret_key'): if (os.stat(key_file).st_mode & 0o777) != 0o600: raise FilePermissionError( "Insecure permissions on key file %s, should be 0600." % os.path.abspath(key_file)) with open(key_file, 'r') as f: key = f.readline() return key def generate_or_read_from_file(key_file='.secret_key', key_length=64): """Multiprocess-safe secret key file generator. Useful to replace the default (and thus unsafe) SECRET_KEY in settings.py upon first start. Save to use, i.e. when multiple Python interpreters serve the dashboard Django application (e.g. in a mod_wsgi + daemonized environment). Also checks if file permissions are set correctly and throws an exception if not. """ abspath = os.path.abspath(key_file) # check, if key_file already exists # if yes, then just read and return key if os.path.exists(key_file): key = read_from_file(key_file) return key # otherwise, first lock to make sure only one process lock = lockutils.external_lock(key_file + ".lock", lock_path=os.path.dirname(abspath)) with lock: if not os.path.exists(key_file): key = generate_key(key_length) old_umask = os.umask(0o177) # Use '0600' file permissions with open(key_file, 'w') as f: f.write(key) os.umask(old_umask) else: key = read_from_file(key_file) return key horizon-13.0.0/horizon/utils/file_discovery.py0000666000175100017510000001031513245511643021525 0ustar zuulzuul00000000000000# 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 logging from os import path from os import walk LOG = logging.getLogger(__name__) MODULE_EXT = '.module.js' MOCK_EXT = '.mock.js' SPEC_EXT = '.spec.js' def discover_files(base_path, sub_path='', ext='', trim_base_path=False): """Discovers all files with certain extension in given paths.""" file_list = [] for root, dirs, files in walk(path.join(base_path, sub_path)): if trim_base_path: root = path.relpath(root, base_path) file_list.extend([path.join(root, file_name) for file_name in files if file_name.endswith(ext)]) return sorted(file_list) def sort_js_files(js_files): """Sorts JavaScript files in `js_files`. It sorts JavaScript files in a given `js_files` into source files, mock files and spec files based on file extension. Output: * sources: source files for production. The order of source files is significant and should be listed in the below order: - First, all the that defines the other application's angular module. Those files have extension of `.module.js`. The order among them is not significant. - Followed by all other source code files. The order among them is not significant. * mocks: mock files provide mock data/services for tests. They have extension of `.mock.js`. The order among them is not significant. * specs: spec files for testing. They have extension of `.spec.js`. The order among them is not significant. """ modules = [f for f in js_files if f.endswith(MODULE_EXT)] mocks = [f for f in js_files if f.endswith(MOCK_EXT)] specs = [f for f in js_files if f.endswith(SPEC_EXT)] other_sources = [f for f in js_files if not f.endswith(MODULE_EXT) and not f.endswith(MOCK_EXT) and not f.endswith(SPEC_EXT)] sources = modules + other_sources return sources, mocks, specs def discover_static_files(base_path, sub_path=''): """Discovers static files in given paths. It returns JavaScript sources, mocks, specs and HTML templates, all grouped in lists. """ js_files = discover_files(base_path, sub_path=sub_path, ext='.js', trim_base_path=True) sources, mocks, specs = sort_js_files(js_files) html_files = discover_files(base_path, sub_path=sub_path, ext='.html', trim_base_path=True) p = path.join(base_path, sub_path) _log(sources, 'JavaScript source', p) _log(mocks, 'JavaScript mock', p) _log(specs, 'JavaScript spec', p) _log(html_files, 'HTML template', p) return sources, mocks, specs, html_files def populate_horizon_config(horizon_config, base_path, sub_path='', prepend=False): sources, mocks, specs, template = discover_static_files( base_path, sub_path=sub_path) if prepend: horizon_config.setdefault('js_files', [])[:0] = sources horizon_config.setdefault('js_spec_files', [])[:0] = mocks + specs horizon_config.setdefault('external_templates', [])[:0] = template else: horizon_config.setdefault('js_files', []).extend(sources) horizon_config.setdefault('js_spec_files', []).extend(mocks + specs) horizon_config.setdefault('external_templates', []).extend(template) def _log(file_list, list_name, in_path): """Logs result at debug level""" file_names = '\n'.join(file_list) LOG.debug("\nDiscovered %(size)d %(name)s file(s) in %(path)s:\n" "%(files)s\n", {'size': len(file_list), 'name': list_name, 'path': in_path, 'files': file_names}) horizon-13.0.0/horizon/utils/babel_extract_angular.py0000666000175100017510000001330213245511643023026 0ustar zuulzuul00000000000000# -*- encoding: utf-8 -*- # Copyright 2015, Rackspace, US, 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. import re from six.moves import html_parser # regex to find filter translation expressions filter_regex = re.compile( r"""{\$\s*('([^']|\\')+'|"([^"]|\\")+")\s*\|\s*translate\s*\$}""" ) # browser innerHTML decodes some html entities automatically, so when # we extract the msgid and want to match what Javascript sees, we need # to leave some entities alone, but decode all the rest. Add entries # to HTML_ENTITIES as necessary. HTML_ENTITY_PASSTHROUGH = {'amp', 'gt', 'lt'} HTML_ENTITY_DECODED = { 'reg': u'®', 'times': u'×' } class AngularGettextHTMLParser(html_parser.HTMLParser): """Parse HTML to find translate directives. Currently this parses for these forms of translation: content The content will be translated. Angular value templating will be recognised and transformed into gettext-familiar translation strings (i.e. "{$ expression $}" becomes "%(expression)")

content

The content will be translated. As above. {$ 'content' | translate $} The string will be translated, minus expression handling (i.e. just bare strings are allowed.) """ def __init__(self): try: super(AngularGettextHTMLParser, self).__init__( convert_charrefs=False ) except TypeError: # handle HTMLParser not being a type on Python 2 html_parser.HTMLParser.__init__(self) self.in_translate = False self.inner_tags = [] self.data = '' self.strings = [] self.line = 0 self.plural = False self.plural_form = '' self.comments = [] def handle_starttag(self, tag, attrs): self.line = self.getpos()[0] if tag == 'translate' or \ (attrs and 'translate' in [attr[0] for attr in attrs]): self.in_translate = True self.plural_form = '' for attr, value in attrs: if attr == 'translate-plural': self.plural = True self.plural_form = value if attr == 'translate-comment': self.comments.append(value) elif self.in_translate: s = tag if attrs: s += ' ' + ' '.join('%s="%s"' % a for a in attrs) self.data += '<%s>' % s self.inner_tags.append(tag) else: for attr in attrs: if not attr[1]: continue for match in filter_regex.findall(attr[1]): if match: self.strings.append( (self.line, u'gettext', match[0][1:-1], []) ) def handle_data(self, data): if self.in_translate: self.data += data else: for match in filter_regex.findall(data): self.strings.append( (self.line, u'gettext', match[0][1:-1], []) ) def handle_entityref(self, name): if self.in_translate: if name in HTML_ENTITY_PASSTHROUGH: self.data += '&%s;' % name else: self.data += HTML_ENTITY_DECODED[name] def handle_charref(self, name): if self.in_translate: self.data += '&#%s;' % name def handle_comment(self, comment): if self.in_translate: self.data += '' % comment def handle_endtag(self, tag): if self.in_translate: if len(self.inner_tags) > 0: tag = self.inner_tags.pop() self.data += "" % tag return if self.plural_form: messages = ( self.data.strip(), self.plural_form ) func_name = u'ngettext' else: messages = self.data.strip() func_name = u'gettext' self.strings.append( (self.line, func_name, messages, self.comments) ) self.in_translate = False self.data = '' self.comments = [] def extract_angular(fileobj, keywords, comment_tags, options): """Extract messages from angular template (HTML) files. It extract messages from angular template (HTML) files that use angular-gettext translate directive as per https://angular-gettext.rocketeer.be/ :param fileobj: the file-like object the messages should be extracted from :param keywords: This is a standard parameter so it isaccepted but ignored. :param comment_tags: This is a standard parameter so it is accepted but ignored. :param options: Another standard parameter that is accepted but ignored. :return: an iterator over ``(lineno, funcname, message, comments)`` tuples :rtype: ``iterator`` """ parser = AngularGettextHTMLParser() for line in fileobj: parser.feed(line) for string in parser.strings: yield(string) horizon-13.0.0/horizon/browsers/0000775000175100017510000000000013245512210016640 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/browsers/views.py0000666000175100017510000000770113245511643020367 0ustar zuulzuul00000000000000# 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. from django.utils.translation import ugettext_lazy as _ from django.views import generic import horizon from horizon.tables import MultiTableView from horizon.utils import memoized class ResourceBrowserView(MultiTableView): browser_class = None def __init__(self, *args, **kwargs): if not self.browser_class: raise ValueError("You must specify a ResourceBrowser subclass " "for the browser_class attribute on %s." % self.__class__.__name__) self.table_classes = (self.browser_class.navigation_table_class, self.browser_class.content_table_class) self.navigation_selection = False super(ResourceBrowserView, self).__init__(*args, **kwargs) @memoized.memoized_method def get_browser(self): browser = self.browser_class(self.request, **self.kwargs) browser.set_tables(self.get_tables()) if not self.navigation_selection: ct = browser.content_table item = browser.navigable_item_name.lower() ct._no_data_message = _("Select a %s to browse.") % item return browser def get_tables(self): tables = super(ResourceBrowserView, self).get_tables() # Tells the navigation table what is selected. navigation_table = tables[ self.browser_class.navigation_table_class._meta.name] navigation_item = self.kwargs.get( self.browser_class.navigation_kwarg_name) navigation_table.current_item_id = navigation_item return tables def get_context_data(self, **kwargs): context = super(ResourceBrowserView, self).get_context_data(**kwargs) browser = self.get_browser() context["%s_browser" % browser.name] = browser return context class AngularIndexView(generic.TemplateView): '''View for Angularized panel title: to display title for browser window or tab. page_title: to display current position in breadcrumb. Sample usage is as follows. from horizon.browsers import views views.AngularIndexView.as_view(title="Images") views.AngularIndexView.as_view(title="Browser Title", page_title="Page Title") ''' template_name = 'angular.html' title = _("Horizon") page_title = None def get_context_data(self, **kwargs): context = super(AngularIndexView, self).get_context_data(**kwargs) context["title"] = self.title if self.page_title is None: context["page_title"] = self.title else: context["page_title"] = self.page_title return context class AngularDetailsView(generic.TemplateView): '''View for Angularized details view This is used to load ngdetails view via Django. i.e. refresh or link directly for '^ngdetails/' ''' template_name = 'angular.html' def get_context_data(self, **kwargs): context = super(AngularDetailsView, self).get_context_data(**kwargs) # some parameters are needed for navigation side bar and breadcrumb. title = _("Horizon") context["title"] = title context["page_title"] = title dashboard = horizon.get_default_dashboard() self.request.horizon['dashboard'] = dashboard self.request.horizon['panel'] = dashboard.get_panels()[0] return context horizon-13.0.0/horizon/browsers/breadcrumb.py0000666000175100017510000000333013245511643021332 0ustar zuulzuul00000000000000# 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. from django import template from horizon.utils import html class Breadcrumb(html.HTMLElement): def __init__(self, request, template, root, subfolder_path, url, attr=None): super(Breadcrumb, self).__init__() self.template = template self.request = request self.root = root self.subfolder_path = subfolder_path self.url = url self._subfolders = [] def get_subfolders(self): if self.subfolder_path and not self._subfolders: (parent, slash, folder) = self.subfolder_path.strip('/') \ .rpartition('/') while folder: path = "%s%s%s/" % (parent, slash, folder) self._subfolders.insert(0, (folder, path)) (parent, slash, folder) = parent.rpartition('/') return self._subfolders def render(self): """Renders the table using the template from the table options.""" breadcrumb_template = template.loader.get_template(self.template) extra_context = {"breadcrumb": self} return breadcrumb_template.render(extra_context, self.request) horizon-13.0.0/horizon/browsers/base.py0000666000175100017510000001346613245511643020151 0ustar zuulzuul00000000000000# 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. from django import template from django.utils.translation import ugettext_lazy as _ from horizon.browsers.breadcrumb import Breadcrumb from horizon.tables import DataTable from horizon.utils import html class ResourceBrowser(html.HTMLElement): """A class which defines a browser for displaying data. .. attribute:: name A short name or slug for the browser. .. attribute:: verbose_name A more verbose name for the browser meant for display purposes. .. attribute:: navigation_table_class This table displays data on the left side of the browser. Set the ``navigation_table_class`` attribute with the desired :class:`~horizon.tables.DataTable` class. This table class must set browser_table attribute in Meta to ``"navigation"``. .. attribute:: content_table_class This table displays data on the right side of the browser. Set the ``content_table_class`` attribute with the desired :class:`~horizon.tables.DataTable` class. This table class must set browser_table attribute in Meta to ``"content"``. .. attribute:: navigation_kwarg_name This attribute represents the key of the navigatable items in the kwargs property of this browser's view. Defaults to ``"navigation_kwarg"``. .. attribute:: content_kwarg_name This attribute represents the key of the content items in the kwargs property of this browser's view. Defaults to ``"content_kwarg"``. .. attribute:: template String containing the template which should be used to render the browser. Defaults to ``"horizon/common/_resource_browser.html"``. .. attribute:: context_var_name The name of the context variable which will contain the browser when it is rendered. Defaults to ``"browser"``. .. attribute:: has_breadcrumb Indicates if the content table of the browser would have breadcrumb. Defaults to false. .. attribute:: breadcrumb_template This is a template used to render the breadcrumb. Defaults to ``"horizon/common/_breadcrumb.html"``. """ name = None verbose_name = None navigation_table_class = None content_table_class = None navigation_kwarg_name = "navigation_kwarg" content_kwarg_name = "content_kwarg" navigable_item_name = _("Navigation Item") template = "horizon/common/_resource_browser.html" context_var_name = "browser" has_breadcrumb = False breadcrumb_template = "horizon/common/_breadcrumb.html" breadcrumb_url = None def __init__(self, request, tables_dict=None, attrs=None, **kwargs): super(ResourceBrowser, self).__init__() self.name = self.name or self.__class__.__name__ self.verbose_name = self.verbose_name or self.name.title() self.request = request self.kwargs = kwargs self.has_breadcrumb = getattr(self, "has_breadcrumb") if self.has_breadcrumb: self.breadcrumb_template = getattr(self, "breadcrumb_template") self.breadcrumb_url = getattr(self, "breadcrumb_url") if not self.breadcrumb_url: raise ValueError("You must specify a breadcrumb_url " "if the has_breadcrumb is set to True.") self.attrs.update(attrs or {}) self.check_table_class(self.content_table_class, "content_table_class") self.check_table_class(self.navigation_table_class, "navigation_table_class") if tables_dict: self.set_tables(tables_dict) def check_table_class(self, cls, attr_name): if not cls or not issubclass(cls, DataTable): raise ValueError("You must specify a DataTable subclass for " "the %s attribute on %s." % (attr_name, self.__class__.__name__)) def set_tables(self, tables): """Sets the table instances on the browser. ``tables`` argument specifies tables to be set. It is a dictionary mapping table names to table instances (as constructed by MultiTableView). """ self.navigation_table = tables[self.navigation_table_class._meta.name] self.content_table = tables[self.content_table_class._meta.name] navigation_item = self.kwargs.get(self.navigation_kwarg_name) content_path = self.kwargs.get(self.content_kwarg_name) if self.has_breadcrumb: self.prepare_breadcrumb(tables, navigation_item, content_path) def prepare_breadcrumb(self, tables, navigation_item, content_path): if self.has_breadcrumb and navigation_item and content_path: for table in tables.values(): table.breadcrumb = Breadcrumb(self.request, self.breadcrumb_template, navigation_item, content_path, self.breadcrumb_url) def render(self): browser_template = template.loader.get_template(self.template) extra_context = {self.context_var_name: self} return browser_template.render(extra_context, self.request) horizon-13.0.0/horizon/browsers/__init__.py0000666000175100017510000000147513245511643020773 0ustar zuulzuul00000000000000# 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. # Importing non-modules that are not used explicitly from horizon.browsers.base import ResourceBrowser from horizon.browsers.views import ResourceBrowserView __all__ = [ 'ResourceBrowser', 'ResourceBrowserView', ] horizon-13.0.0/horizon/views.py0000666000175100017510000001070413245511643016516 0ustar zuulzuul00000000000000# 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. from django.conf import settings from django import shortcuts from django import template from django.utils import encoding from django.views import generic from osprofiler import profiler import horizon from horizon import exceptions class PageTitleMixin(object): """A mixin that renders out a page title into a view. Many views in horizon have a page title that would ordinarily be defined and passed through in get_context_data function, this often leads to a lot of duplicated work in each view. This mixin standardises the process of defining a page title, letting views simply define a variable that is rendered into the context for them. There are cases when page title in a view may also display some context data, for that purpose the page_title variable supports the django templating language and will be rendered using the context defined by the views get_context_data. """ page_title = "" def render_context_with_title(self, context): """Render a page title and insert it into the context. This function takes in a context dict and uses it to render the page_title variable. It then appends this title to the context using the 'page_title' key. If there is already a page_title key defined in context received then this function will do nothing. """ if "page_title" not in context: con = template.Context(context) # NOTE(sambetts): Use force_text to ensure lazy translations # are handled correctly. temp = template.Template(encoding.force_text(self.page_title)) context["page_title"] = temp.render(con) return context def render_to_response(self, context): """render_to_response() with a page title. This is an override of the default render_to_response function that exists in the django generic views. This is here to inject the page title into the context before the main template is rendered. """ context = self.render_context_with_title(context) return super(PageTitleMixin, self).render_to_response(context) def trace(name): def decorator(func): if getattr(settings, 'OPENSTACK_PROFILER', {}).get('enabled', False): return profiler.trace(name, info=None, hide_args=False, allow_multiple_trace=True)(func) else: return func return decorator class HorizonTemplateView(PageTitleMixin, generic.TemplateView): @trace('horizon.render_to_response') def render_to_response(self, context): return super(HorizonTemplateView, self).render_to_response(context) class HorizonFormView(PageTitleMixin, generic.FormView): pass def user_home(request): """Reversible named view to direct a user to the appropriate homepage.""" return shortcuts.redirect(horizon.get_user_home(request.user)) class APIView(HorizonTemplateView): """A quick class-based view for putting API data into a template. Subclasses must define one method, ``get_data``, and a template name via the ``template_name`` attribute on the class. Errors within the ``get_data`` function are automatically caught by the :func:`horizon.exceptions.handle` error handler if not otherwise caught. """ def get_data(self, request, context, *args, **kwargs): """Load necessary API data into the context. This method should handle any necessary API calls, update the context object, and return the context object at the end. """ return context def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) try: context = self.get_data(request, context, *args, **kwargs) except Exception: exceptions.handle(request) return self.render_to_response(context) horizon-13.0.0/horizon/workflows/0000775000175100017510000000000013245512210017027 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/workflows/views.py0000666000175100017510000002013613245511643020553 0ustar zuulzuul00000000000000# 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. import copy import json from django import forms from django import http from django import shortcuts from django.views import generic import six from horizon import exceptions from horizon.forms import views as hz_views from horizon.forms.views import ADD_TO_FIELD_HEADER from horizon import messages class WorkflowView(hz_views.ModalBackdropMixin, generic.TemplateView): """A generic view which handles the intricacies of workflow processing. .. attribute:: workflow_class The :class:`~horizon.workflows.Workflow` class which this view handles. Required. .. attribute:: template_name The template to use when rendering this view via standard HTTP requests. Required. .. attribute:: ajax_template_name The template to use when rendering the workflow for AJAX requests. In general the default common template should be used. Defaults to ``"horizon/common/_workflow.html"``. .. attribute:: context_object_name The key which should be used for the workflow object in the template context. Defaults to ``"workflow"``. """ workflow_class = None template_name = 'horizon/common/_workflow_base.html' context_object_name = "workflow" ajax_template_name = 'horizon/common/_workflow.html' step_errors = {} def __init__(self): super(WorkflowView, self).__init__() if not self.workflow_class: raise AttributeError("You must set the workflow_class attribute " "on %s." % self.__class__.__name__) def get_initial(self): """Returns initial data for the workflow. Defaults to using the GET parameters to allow pre-seeding of the workflow context values. """ return copy.copy(self.request.GET) def get_workflow(self): """Returns the instantiated workflow class.""" extra_context = self.get_initial() entry_point = self.request.GET.get("step", None) workflow = self.workflow_class(self.request, context_seed=extra_context, entry_point=entry_point) return workflow def get_context_data(self, **kwargs): """Returns the template context, including the workflow class. This method should be overridden in subclasses to provide additional context data to the template. """ context = super(WorkflowView, self).get_context_data(**kwargs) workflow = self.get_workflow() workflow.verify_integrity() context[self.context_object_name] = workflow next = self.request.GET.get(workflow.redirect_param_name) context['REDIRECT_URL'] = next context['layout'] = self.get_layout() # For consistency with Workflow class context['modal'] = 'modal' in context['layout'] if ADD_TO_FIELD_HEADER in self.request.META: context['add_to_field'] = self.request.META[ADD_TO_FIELD_HEADER] return context def get_layout(self): """Returns classes for the workflow element in template. The returned classes are determied based on the workflow characteristics. """ if self.request.is_ajax(): layout = ['modal', ] else: layout = ['static_page', ] if self.workflow_class.wizard: layout += ['wizard', ] return layout def get_template_names(self): """Returns the template name to use for this request.""" if self.request.is_ajax(): template = self.ajax_template_name else: template = self.template_name return template def get_object_id(self, obj): return getattr(obj, "id", None) def get_object_display(self, obj): return getattr(obj, "name", None) def add_error_to_step(self, error_msg, step): self.step_errors[step] = error_msg def set_workflow_step_errors(self, context): workflow = context['workflow'] for step in self.step_errors: error_msg = self.step_errors[step] workflow.add_error_to_step(error_msg, step) def get(self, request, *args, **kwargs): """Handler for HTTP GET requests.""" try: context = self.get_context_data(**kwargs) except exceptions.NotAvailable: exceptions.handle(request) self.set_workflow_step_errors(context) return self.render_to_response(context) def validate_steps(self, request, workflow, start, end): """Validates the workflow steps from ``start`` to ``end``, inclusive. Returns a dict describing the validation state of the workflow. """ errors = {} for step in workflow.steps[start:end + 1]: if not step.action.is_valid(): errors[step.slug] = dict( (field, [six.text_type(error) for error in errors]) for (field, errors) in step.action.errors.items()) return { 'has_errors': bool(errors), 'workflow_slug': workflow.slug, 'errors': errors, } def post(self, request, *args, **kwargs): """Handler for HTTP POST requests.""" context = self.get_context_data(**kwargs) workflow = context[self.context_object_name] try: # Check for the VALIDATE_STEP* headers, if they are present # and valid integers, return validation results as JSON, # otherwise proceed normally. validate_step_start = int(self.request.META.get( 'HTTP_X_HORIZON_VALIDATE_STEP_START', '')) validate_step_end = int(self.request.META.get( 'HTTP_X_HORIZON_VALIDATE_STEP_END', '')) except ValueError: # No VALIDATE_STEP* headers, or invalid values. Just proceed # with normal workflow handling for POSTs. pass else: # There are valid VALIDATE_STEP* headers, so only do validation # for the specified steps and return results. data = self.validate_steps(request, workflow, validate_step_start, validate_step_end) return http.HttpResponse(json.dumps(data), content_type="application/json") if not workflow.is_valid(): return self.render_to_response(context) try: success = workflow.finalize() except forms.ValidationError: return self.render_to_response(context) except Exception: success = False exceptions.handle(request) if success: msg = workflow.format_status_message(workflow.success_message) messages.success(request, msg) else: msg = workflow.format_status_message(workflow.failure_message) messages.error(request, msg) if "HTTP_X_HORIZON_ADD_TO_FIELD" in self.request.META: field_id = self.request.META["HTTP_X_HORIZON_ADD_TO_FIELD"] response = http.HttpResponse() if workflow.object: data = [self.get_object_id(workflow.object), self.get_object_display(workflow.object)] response.content = json.dumps(data) response["X-Horizon-Add-To-Field"] = field_id return response next_url = self.request.POST.get(workflow.redirect_param_name) return shortcuts.redirect(next_url or workflow.get_success_url()) horizon-13.0.0/horizon/workflows/base.py0000666000175100017510000010324513245511643020333 0ustar zuulzuul00000000000000# 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. import copy from importlib import import_module import inspect import logging from django.core import urlresolvers from django import forms from django.forms.forms import NON_FIELD_ERRORS from django import template from django.template.defaultfilters import linebreaks from django.template.defaultfilters import safe from django.template.defaultfilters import slugify from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from openstack_auth import policy import six from horizon import base from horizon import exceptions from horizon.templatetags.horizon import has_permissions from horizon.utils import html LOG = logging.getLogger(__name__) class WorkflowContext(dict): def __init__(self, workflow, *args, **kwargs): super(WorkflowContext, self).__init__(*args, **kwargs) self._workflow = workflow def __setitem__(self, key, val): super(WorkflowContext, self).__setitem__(key, val) return self._workflow._trigger_handlers(key) def __delitem__(self, key): return self.__setitem__(key, None) def set(self, key, val): return self.__setitem__(key, val) def unset(self, key): return self.__delitem__(key) class ActionMetaclass(forms.forms.DeclarativeFieldsMetaclass): def __new__(mcs, name, bases, attrs): # Pop Meta for later processing opts = attrs.pop("Meta", None) # Create our new class cls = super(ActionMetaclass, mcs).__new__(mcs, name, bases, attrs) # Process options from Meta cls.name = getattr(opts, "name", name) cls.slug = getattr(opts, "slug", slugify(name)) cls.permissions = getattr(opts, "permissions", ()) cls.policy_rules = getattr(opts, "policy_rules", ()) cls.progress_message = getattr(opts, "progress_message", _("Processing...")) cls.help_text = getattr(opts, "help_text", "") cls.help_text_template = getattr(opts, "help_text_template", None) return cls @six.python_2_unicode_compatible @six.add_metaclass(ActionMetaclass) class Action(forms.Form): """An ``Action`` represents an atomic logical interaction with the system. This is easier to understand with a conceptual example: in the context of a "launch instance" workflow, actions would include "naming the instance", "selecting an image", and ultimately "launching the instance". Because ``Actions`` are always interactive, they always provide form controls, and thus inherit from Django's ``Form`` class. However, they have some additional intelligence added to them: * ``Actions`` are aware of the permissions required to complete them. * ``Actions`` have a meta-level concept of "help text" which is meant to be displayed in such a way as to give context to the action regardless of where the action is presented in a site or workflow. * ``Actions`` understand how to handle their inputs and produce outputs, much like :class:`~horizon.forms.SelfHandlingForm` does now. ``Action`` classes may define the following attributes in a ``Meta`` class within them: .. attribute:: name The verbose name for this action. Defaults to the name of the class. .. attribute:: slug A semi-unique slug for this action. Defaults to the "slugified" name of the class. .. attribute:: permissions A list of permission names which this action requires in order to be completed. Defaults to an empty list (``[]``). .. attribute:: policy_rules list of scope and rule tuples to do policy checks on, the composition of which is (scope, rule) * scope: service type managing the policy for action * rule: string representing the action to be checked for a policy that requires a single rule check:: policy_rules should look like "(("compute", "compute:create_instance"),)" for a policy that requires multiple rule checks:: rules should look like "(("identity", "identity:list_users"), ("identity", "identity:list_roles"))" where two service-rule clauses are OR-ed. .. attribute:: help_text A string of simple help text to be displayed alongside the Action's fields. .. attribute:: help_text_template A path to a template which contains more complex help text to be displayed alongside the Action's fields. In conjunction with :meth:`~horizon.workflows.Action.get_help_text` method you can customize your help text template to display practically anything. """ def __init__(self, request, context, *args, **kwargs): if request.method == "POST": super(Action, self).__init__(request.POST, initial=context) else: super(Action, self).__init__(initial=context) if not hasattr(self, "handle"): raise AttributeError("The action %s must define a handle method." % self.__class__.__name__) self.request = request self._populate_choices(request, context) self.required_css_class = 'required' def __str__(self): return force_text(self.name) def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.slug) def _populate_choices(self, request, context): for field_name, bound_field in self.fields.items(): meth = getattr(self, "populate_%s_choices" % field_name, None) if meth is not None and callable(meth): bound_field.choices = meth(request, context) def get_help_text(self, extra_context=None): """Returns the help text for this step.""" text = "" extra_context = extra_context or {} if self.help_text_template: tmpl = template.loader.get_template(self.help_text_template) text += tmpl.render(extra_context, self.request) else: text += linebreaks(force_text(self.help_text)) return safe(text) def add_action_error(self, message): """Adds an error to the Action's Step based on API issues.""" self.errors[NON_FIELD_ERRORS] = self.error_class([message]) def handle(self, request, context): """Handles any requisite processing for this action. The method should return either ``None`` or a dictionary of data to be passed to :meth:`~horizon.workflows.Step.contribute`. Returns ``None`` by default, effectively making it a no-op. """ return None class MembershipAction(Action): """An action that allows a user to add/remove members from a group. Extend the Action class with additional helper method for membership management. """ def get_default_role_field_name(self): return "default_" + self.slug + "_role" def get_member_field_name(self, role_id): return self.slug + "_role_" + role_id @six.python_2_unicode_compatible class Step(object): """A wrapper around an action which defines its context in a workflow. It knows about details such as: * The workflow's context data (data passed from step to step). * The data which must be present in the context to begin this step (the step's dependencies). * The keys which will be added to the context data upon completion of the step. * The connections between this step's fields and changes in the context data (e.g. if that piece of data changes, what needs to be updated in this step). A ``Step`` class has the following attributes: .. attribute:: action_class The :class:`~horizon.workflows.Action` class which this step wraps. .. attribute:: depends_on A list of context data keys which this step requires in order to begin interaction. .. attribute:: contributes A list of keys which this step will contribute to the workflow's context data. Optional keys should still be listed, even if their values may be set to ``None``. .. attribute:: connections A dictionary which maps context data key names to lists of callbacks. The callbacks may be functions, dotted python paths to functions which may be imported, or dotted strings beginning with ``"self"`` to indicate methods on the current ``Step`` instance. .. attribute:: before Another ``Step`` class. This optional attribute is used to provide control over workflow ordering when steps are dynamically added to workflows. The workflow mechanism will attempt to place the current step before the step specified in the attribute. .. attribute:: after Another ``Step`` class. This attribute has the same purpose as :meth:`~horizon.workflows.Step.before` except that it will instead attempt to place the current step after the given step. .. attribute:: help_text A string of simple help text which will be prepended to the ``Action`` class' help text if desired. .. attribute:: template_name A path to a template which will be used to render this step. In general the default common template should be used. Default: ``"horizon/common/_workflow_step.html"``. .. attribute:: has_errors A boolean value which indicates whether or not this step has any errors on the action within it or in the scope of the workflow. This attribute will only accurately reflect this status after validation has occurred. .. attribute:: slug Inherited from the ``Action`` class. .. attribute:: name Inherited from the ``Action`` class. .. attribute:: permissions Inherited from the ``Action`` class. """ action_class = None depends_on = () contributes = () connections = None before = None after = None help_text = "" template_name = "horizon/common/_workflow_step.html" def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.slug) def __str__(self): return force_text(self.name) def __init__(self, workflow): super(Step, self).__init__() self.workflow = workflow cls = self.__class__.__name__ if not (self.action_class and issubclass(self.action_class, Action)): raise AttributeError("action_class not specified for %s." % cls) self.slug = self.action_class.slug self.name = self.action_class.name self.permissions = self.action_class.permissions self.policy_rules = self.action_class.policy_rules self.has_errors = False self._handlers = {} if self.connections is None: # We want a dict, but don't want to declare a mutable type on the # class directly. self.connections = {} # Gather our connection handlers and make sure they exist. for key, handlers in self.connections.items(): self._handlers[key] = [] # TODO(gabriel): This is a poor substitute for broader handling if not isinstance(handlers, (list, tuple)): raise TypeError("The connection handlers for %s must be a " "list or tuple." % cls) for possible_handler in handlers: if callable(possible_handler): # If it's callable we know the function exists and is valid self._handlers[key].append(possible_handler) continue elif not isinstance(possible_handler, six.string_types): raise TypeError("Connection handlers must be either " "callables or strings.") bits = possible_handler.split(".") if bits[0] == "self": root = self for bit in bits[1:]: try: root = getattr(root, bit) except AttributeError: raise AttributeError("The connection handler %s " "could not be found on %s." % (possible_handler, cls)) handler = root elif len(bits) == 1: # Import by name from local module not supported raise ValueError("Importing a local function as a string " "is not supported for the connection " "handler %s on %s." % (possible_handler, cls)) else: # Try a general import module_name = ".".join(bits[:-1]) try: mod = import_module(module_name) handler = getattr(mod, bits[-1]) except ImportError: raise ImportError("Could not import %s from the " "module %s as a connection " "handler on %s." % (bits[-1], module_name, cls)) except AttributeError: raise AttributeError("Could not import %s from the " "module %s as a connection " "handler on %s." % (bits[-1], module_name, cls)) self._handlers[key].append(handler) @property def action(self): if not getattr(self, "_action", None): try: # Hook in the action context customization. workflow_context = dict(self.workflow.context) context = self.prepare_action_context(self.workflow.request, workflow_context) self._action = self.action_class(self.workflow.request, context) except Exception: LOG.exception("Problem instantiating action class.") raise return self._action def prepare_action_context(self, request, context): """Hook to customize how the workflow context is passed to the action. This is the reverse of what "contribute" does to make the action outputs sane for the workflow. Changes to the context are not saved globally here. They are localized to the action. Simply returns the unaltered context by default. """ return context def get_id(self): """Returns the ID for this step. Suitable for use in HTML markup.""" return "%s__%s" % (self.workflow.slug, self.slug) def _verify_contributions(self, context): for key in self.contributes: # Make sure we don't skip steps based on weird behavior of # POST query dicts. field = self.action.fields.get(key, None) if field and field.required and not context.get(key): context.pop(key, None) failed_to_contribute = set(self.contributes) failed_to_contribute -= set(context.keys()) if failed_to_contribute: raise exceptions.WorkflowError("The following expected data was " "not added to the workflow context " "by the step %s: %s." % (self.__class__, failed_to_contribute)) return True def contribute(self, data, context): """Adds the data listed in ``contributes`` to the workflow's context. By default, the context is simply updated with all the data returned by the action. Note that even if the value of one of the ``contributes`` keys is not present (e.g. optional) the key should still be added to the context with a value of ``None``. """ if data: for key in self.contributes: context[key] = data.get(key, None) return context def render(self): """Renders the step.""" step_template = template.loader.get_template(self.template_name) extra_context = {"form": self.action, "step": self} return step_template.render(extra_context, self.workflow.request) def get_help_text(self): """Returns the help text for this step.""" text = linebreaks(force_text(self.help_text)) text += self.action.get_help_text() return safe(text) def add_step_error(self, message): """Adds an error to the Step based on API issues.""" self.action.add_action_error(message) def has_required_fields(self): """Returns True if action contains any required fields.""" return any(field.required for field in self.action.fields.values()) def allowed(self, request): """Determines whether or not the step is displayed. Step instances can override this method to specify conditions under which this tab should not be shown at all by returning ``False``. The default behavior is to return ``True`` for all cases. """ return True class WorkflowMetaclass(type): def __new__(mcs, name, bases, attrs): super(WorkflowMetaclass, mcs).__new__(mcs, name, bases, attrs) attrs["_cls_registry"] = set([]) return type.__new__(mcs, name, bases, attrs) class UpdateMembersStep(Step): """A step that allows a user to add/remove members from a group. .. attribute:: show_roles Set to False to disable the display of the roles dropdown. .. attribute:: available_list_title The title used for the available list column. .. attribute:: members_list_title The title used for the members list column. .. attribute:: no_available_text The placeholder text used when the available list is empty. .. attribute:: no_members_text The placeholder text used when the members list is empty. """ template_name = "horizon/common/_workflow_step_update_members.html" show_roles = True available_list_title = _("All available") members_list_title = _("Members") no_available_text = _("None available.") no_members_text = _("No members.") def get_member_field_name(self, role_id): if issubclass(self.action_class, MembershipAction): return self.action.get_member_field_name(role_id) else: return self.slug + "_role_" + role_id @six.python_2_unicode_compatible @six.add_metaclass(WorkflowMetaclass) class Workflow(html.HTMLElement): """A Workflow is a collection of Steps. Its interface is very straightforward, but it is responsible for handling some very important tasks such as: * Handling the injection, removal, and ordering of arbitrary steps. * Determining if the workflow can be completed by a given user at runtime based on all available information. * Dispatching connections between steps to ensure that when context data changes all the applicable callback functions are executed. * Verifying/validating the overall data integrity and subsequently triggering the final method to complete the workflow. The ``Workflow`` class has the following attributes: .. attribute:: name The verbose name for this workflow which will be displayed to the user. Defaults to the class name. .. attribute:: slug The unique slug for this workflow. Required. .. attribute:: steps Read-only access to the final ordered set of step instances for this workflow. .. attribute:: default_steps A list of :class:`~horizon.workflows.Step` classes which serve as the starting point for this workflow's ordered steps. Defaults to an empty list (``[]``). .. attribute:: finalize_button_name The name which will appear on the submit button for the workflow's form. Defaults to ``"Save"``. .. attribute:: success_message A string which will be displayed to the user upon successful completion of the workflow. Defaults to ``"{{ workflow.name }} completed successfully."`` .. attribute:: failure_message A string which will be displayed to the user upon failure to complete the workflow. Defaults to ``"{{ workflow.name }} did not complete."`` .. attribute:: depends_on A roll-up list of all the ``depends_on`` values compiled from the workflow's steps. .. attribute:: contributions A roll-up list of all the ``contributes`` values compiled from the workflow's steps. .. attribute:: template_name Path to the template which should be used to render this workflow. In general the default common template should be used. Default: ``"horizon/common/_workflow.html"``. .. attribute:: entry_point The slug of the step which should initially be active when the workflow is rendered. This can be passed in upon initialization of the workflow, or set anytime after initialization but before calling either ``get_entry_point`` or ``render``. .. attribute:: redirect_param_name The name of a parameter used for tracking the URL to redirect to upon completion of the workflow. Defaults to ``"next"``. .. attribute:: object The object (if any) which this workflow relates to. In the case of a workflow which creates a new resource the object would be the created resource after the relevant creation steps have been undertaken. In the case of a workflow which updates a resource it would be the resource being updated after it has been retrieved. .. attribute:: wizard Whether to present the workflow as a wizard, with "prev" and "next" buttons and validation after every step. """ slug = None default_steps = () template_name = "horizon/common/_workflow.html" finalize_button_name = _("Save") success_message = _("%s completed successfully.") failure_message = _("%s did not complete.") redirect_param_name = "next" multipart = False wizard = False _registerable_class = Step def __str__(self): return self.name def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.slug) def __init__(self, request=None, context_seed=None, entry_point=None, *args, **kwargs): super(Workflow, self).__init__(*args, **kwargs) if self.slug is None: raise AttributeError("The workflow %s must have a slug." % self.__class__.__name__) self.name = getattr(self, "name", self.__class__.__name__) self.request = request self.depends_on = set([]) self.contributions = set([]) self.entry_point = entry_point self.object = None # Put together our steps in order. Note that we pre-register # non-default steps so that we can identify them and subsequently # insert them in order correctly. self._registry = dict([(step_class, step_class(self)) for step_class in self.__class__._cls_registry if step_class not in self.default_steps]) self._gather_steps() # Determine all the context data we need to end up with. for step in self.steps: self.depends_on = self.depends_on | set(step.depends_on) self.contributions = self.contributions | set(step.contributes) # Initialize our context. For ease we can preseed it with a # regular dictionary. This should happen after steps have been # registered and ordered. self.context = WorkflowContext(self) context_seed = context_seed or {} clean_seed = dict([(key, val) for key, val in context_seed.items() if key in self.contributions | self.depends_on]) self.context_seed = clean_seed self.context.update(clean_seed) if request and request.method == "POST": for step in self.steps: valid = step.action.is_valid() # Be sure to use the CLEANED data if the workflow is valid. if valid: data = step.action.cleaned_data else: data = request.POST self.context = step.contribute(data, self.context) @property def steps(self): if getattr(self, "_ordered_steps", None) is None: self._gather_steps() return self._ordered_steps def get_step(self, slug): """Returns the instantiated step matching the given slug.""" for step in self.steps: if step.slug == slug: return step def _gather_steps(self): ordered_step_classes = self._order_steps() for default_step in self.default_steps: self.register(default_step) self._registry[default_step] = default_step(self) self._ordered_steps = [] for step_class in ordered_step_classes: cls = self._registry[step_class] if (has_permissions(self.request.user, cls) and policy.check(cls.policy_rules, self.request) and cls.allowed(self.request)): self._ordered_steps.append(cls) def _order_steps(self): steps = list(copy.copy(self.default_steps)) additional = self._registry.keys() for step in additional: try: min_pos = steps.index(step.after) except ValueError: min_pos = 0 try: max_pos = steps.index(step.before) except ValueError: max_pos = len(steps) if min_pos > max_pos: raise exceptions.WorkflowError("The step %(new)s can't be " "placed between the steps " "%(after)s and %(before)s; the " "step %(before)s comes before " "%(after)s." % {"new": additional, "after": step.after, "before": step.before}) steps.insert(max_pos, step) return steps def get_entry_point(self): """Returns the slug of the step which the workflow should begin on. This method takes into account both already-available data and errors within the steps. """ # If we have a valid specified entry point, use it. if self.entry_point: if self.get_step(self.entry_point): return self.entry_point # Otherwise fall back to calculating the appropriate entry point. for step in self.steps: if step.has_errors: return step.slug try: step._verify_contributions(self.context) except exceptions.WorkflowError: return step.slug # If nothing else, just return the first step. return self.steps[0].slug def _trigger_handlers(self, key): responses = [] handlers = [(step.slug, f) for step in self.steps for f in step._handlers.get(key, [])] for slug, handler in handlers: responses.append((slug, handler(self.request, self.context))) return responses @classmethod def register(cls, step_class): """Registers a :class:`~horizon.workflows.Step` with the workflow.""" if not inspect.isclass(step_class): raise ValueError('Only classes may be registered.') elif not issubclass(step_class, cls._registerable_class): raise ValueError('Only %s classes or subclasses may be registered.' % cls._registerable_class.__name__) if step_class in cls._cls_registry: return False else: cls._cls_registry.add(step_class) return True @classmethod def unregister(cls, step_class): """Unregisters a :class:`~horizon.workflows.Step` from the workflow.""" try: cls._cls_registry.remove(step_class) except KeyError: raise base.NotRegistered('%s is not registered' % cls) return cls._unregister(step_class) def validate(self, context): """Hook for custom context data validation. Should return a booleanvalue or raise :class:`~horizon.exceptions.WorkflowValidationError`. """ return True def is_valid(self): """Verifies that all required data is present in the context. It also calls the ``validate`` method to allow for finer-grained checks on the context data. """ missing = self.depends_on - set(self.context.keys()) if missing: raise exceptions.WorkflowValidationError( "Unable to complete the workflow. The values %s are " "required but not present." % ", ".join(missing)) # Validate each step. Cycle through all of them to catch all errors # in one pass before returning. steps_valid = True for step in self.steps: if not step.action.is_valid(): steps_valid = False step.has_errors = True if not steps_valid: return steps_valid return self.validate(self.context) def finalize(self): """Finalizes a workflow by running through all the actions. It runs all the actions in order and calling their ``handle`` methods. Returns ``True`` on full success, or ``False`` for a partial success, e.g. there were non-critical errors. (If it failed completely the function wouldn't return.) """ partial = False for step in self.steps: try: data = step.action.handle(self.request, self.context) if data is True or data is None: continue elif data is False: partial = True else: self.context = step.contribute(data or {}, self.context) except Exception: partial = True exceptions.handle(self.request) if not self.handle(self.request, self.context): partial = True return not partial def handle(self, request, context): """Handles any final processing for this workflow. Should return a boolean value indicating success. """ return True def get_success_url(self): """Returns a URL to redirect the user to upon completion. By default it will attempt to parse a ``success_url`` attribute on the workflow, which can take the form of a reversible URL pattern name, or a standard HTTP URL. """ try: return urlresolvers.reverse(self.success_url) except urlresolvers.NoReverseMatch: return self.success_url def format_status_message(self, message): """Hook to allow customization of the message returned to the user. This is called upon both successful or unsuccessful completion of the workflow. By default it simply inserts the workflow's name into the message string. """ if "%s" in message: return message % self.name else: return message def verify_integrity(self): provided_keys = self.contributions | set(self.context_seed.keys()) if len(self.depends_on - provided_keys): raise exceptions.NotAvailable( _("The current user has insufficient permission to complete " "the requested task.")) def render(self): """Renders the workflow.""" workflow_template = template.loader.get_template(self.template_name) extra_context = {"workflow": self} if self.request.is_ajax(): extra_context['modal'] = True return workflow_template.render(extra_context, self.request) def get_absolute_url(self): """Returns the canonical URL for this workflow. This is used for the POST action attribute on the form element wrapping the workflow. For convenience it defaults to the value of ``request.get_full_path()`` with any query string stripped off, e.g. the path at which the workflow was requested. """ return self.request.get_full_path().partition('?')[0] def add_error_to_step(self, message, slug): """Adds an error message to the workflow's Step. This is useful when you wish for API errors to appear as errors on the form rather than using the messages framework. The workflow's Step is specified by its slug. """ step = self.get_step(slug) if step: step.add_step_error(message) horizon-13.0.0/horizon/workflows/__init__.py0000666000175100017510000000175713245511643021165 0ustar zuulzuul00000000000000# 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. # Importing non-modules that are not used explicitly from horizon.workflows.base import Action from horizon.workflows.base import MembershipAction from horizon.workflows.base import Step from horizon.workflows.base import UpdateMembersStep from horizon.workflows.base import Workflow from horizon.workflows.views import WorkflowView __all__ = [ 'Action', 'MembershipAction', 'Step', 'UpdateMembersStep', 'Workflow', 'WorkflowView', ] horizon-13.0.0/horizon/templates/0000775000175100017510000000000013245512210016770 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/templates/horizon/0000775000175100017510000000000013245512210020460 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/templates/horizon/_script_i18n.html0000666000175100017510000000023313245511643023661 0ustar zuulzuul00000000000000{% comment %} Django's JavaScript i18n Implementation {% endcomment %} horizon-13.0.0/horizon/templates/horizon/common/0000775000175100017510000000000013245512210021750 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/templates/horizon/common/_data_table_row_action_row.html0000666000175100017510000000104413245511643030202 0ustar zuulzuul00000000000000{% if action.method != "GET" %} {% else %} {% if action.icon != None %} {% endif %} {{ action.verbose_name }} {% endif %} horizon-13.0.0/horizon/templates/horizon/common/_detail_tab_group.html0000666000175100017510000000014613245511643026315 0ustar zuulzuul00000000000000{% extends "horizon/common/_tab_group.html" %} {% block additional_classes %}detail-tabs{% endblock %}horizon-13.0.0/horizon/templates/horizon/common/_workflow_step_update_members.html0000666000175100017510000000501013245511643030765 0ustar zuulzuul00000000000000{% load i18n %}
{% include "horizon/common/_form_fields.html" %}
horizon-13.0.0/horizon/templates/horizon/common/_modal_form.html0000666000175100017510000000372213245511643025133 0ustar zuulzuul00000000000000{% extends "horizon/common/_modal.html" %} {% block content %} {% if table %} {% endif %} {% csrf_token %} {% endblock %} horizon-13.0.0/horizon/templates/horizon/common/fields/0000775000175100017510000000000013245512210023216 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/templates/horizon/common/fields/_themable_spinner.html0000666000175100017510000000176213245511643027603 0ustar zuulzuul00000000000000{% load form_helpers %}
{{ field|add_bootstrap_class|autocomplete:'off' }}
horizon-13.0.0/horizon/templates/horizon/common/fields/_themable_radiobutton.html0000666000175100017510000000111613245511643030450 0ustar zuulzuul00000000000000{% if field.auto_id %} {% if not is_horizontal %} {% include "horizon/common/_form_field_decorator.html" %} {% endif %} {% endif %}
{% for choice in field %}
{{ choice.tag }}
{% endfor %}
horizon-13.0.0/horizon/templates/horizon/common/fields/_themable_checkbox.html0000666000175100017510000000070313245511643027705 0ustar zuulzuul00000000000000
{% if field.auto_id %} {{ field }} {% endif %}
horizon-13.0.0/horizon/templates/horizon/common/fields/_themable_select.html0000666000175100017510000000331013245511643027373 0ustar zuulzuul00000000000000{% load horizon %} {% minifyspace %} {% endminifyspace %} horizon-13.0.0/horizon/templates/horizon/common/_detail_table.html0000666000175100017510000000002313245511643025414 0ustar zuulzuul00000000000000{{ table.render }} horizon-13.0.0/horizon/templates/horizon/common/_limit_summary.html0000666000175100017510000000407713245511643025713 0ustar zuulzuul00000000000000{% load i18n horizon humanize sizeformat %} {% spaceless %}

{% trans "Limit Summary" %}

{% for quota in charts %} {% if forloop.first or forloop.counter0|divisibleby:6 %}
{% endif %}
{{ quota.name }}
{% if quota.max|quotainf != '-1' %} {% if quota.type == "totalRAMUsed" %} {% blocktrans trimmed with usedphrase=quota.text used=quota.used|mb_float_format available=quota.max|quotainf|mb_float_format %} {{ usedphrase }} {{ used }} of {{ available }} {% endblocktrans %} {% elif quota.type == "totalGigabytesUsed" %} {% blocktrans trimmed with usedphrase=quota.text used=quota.used|diskgbformat available=quota.max|quotainf|diskgbformat %} {{ usedphrase }} {{ used }} of {{ available }} {% endblocktrans %} {% else %} {% blocktrans trimmed with usedphrase=quota.text used=quota.used|intcomma available=quota.max|quotainf|intcomma %} {{ usedphrase }} {{ used }} of {{ available }} {% endblocktrans %} {% endif %} {% else %} {% blocktrans trimmed with usedphrase=quota.text used=quota.used|intcomma %} {{ usedphrase }} {{ used }} (No Limit) {% endblocktrans %} {% endif %}
{% if forloop.last or forloop.counter|divisibleby:6 %} {% if not forloop.first %}
{% endif %} {% endif %} {% endfor %}
{% endspaceless %} horizon-13.0.0/horizon/templates/horizon/common/_keystone_provider_selector.html0000666000175100017510000000151513245511643030465 0ustar zuulzuul00000000000000{% load i18n %} {% if keystone_providers.support %} {% endif %} horizon-13.0.0/horizon/templates/horizon/common/_usage_summary.html0000666000175100017510000000207213245511643025672 0ustar zuulzuul00000000000000{% load i18n sizeformat %}

{% trans "Usage Summary" %}

{% with start=form.start end=form.end datepicker_id='date_form' %} {% include 'horizon/common/_datepicker_form.html' %} {% endwith %}
{% trans "Active Instances:" %}
{{ usage.summary.instances|default:'0' }}
{% trans "Active RAM:" %}
{{ usage.summary.memory_mb|mb_float_format|default:'0' }}
{% trans "This Period's VCPU-Hours:" %}
{{ usage.summary.vcpu_hours|floatformat:2|default:'0' }}
{% trans "This Period's GB-Hours:" %}
{{ usage.summary.disk_gb_hours|floatformat:2|default:'0' }}
{% trans "This Period's RAM-Hours:" %}
{{ usage.summary.memory_mb_hours|floatformat:2|default:'0' }}
horizon-13.0.0/horizon/templates/horizon/common/_sidebar_module.html0000666000175100017510000000043613245511643025771 0ustar zuulzuul00000000000000{% for module in modules %}

{{ module.title }}

{% endfor %} horizon-13.0.0/horizon/templates/horizon/common/_data_table_row.html0000666000175100017510000000030413245511643025754 0ustar zuulzuul00000000000000 {% spaceless %} {% for cell in row %} {% include "horizon/common/_data_table_cell.html" %} {% endfor %} {% endspaceless %} horizon-13.0.0/horizon/templates/horizon/common/_formset_table.html0000666000175100017510000000257713245511643025651 0ustar zuulzuul00000000000000{% extends 'horizon/common/_data_table.html' %} {% load i18n %} {% block table_columns %} {% if not table.is_browser_table %} {% for column in columns %} {% endfor %} {% endif %} {% endblock table_columns %} {% block table %} {% with table.get_formset as formset %} {{ formset.management_form }} {% if formset.non_field_errors %}
{{ formset.non_field_errors }}
{% endif %} {% endwith %} {{ block.super }} {% endblock table %} horizon-13.0.0/horizon/templates/horizon/common/_form_field.html0000666000175100017510000000306413245511643025121 0ustar zuulzuul00000000000000{% load form_helpers %}
{% if field|is_checkbox %}
{% include 'horizon/common/fields/_themable_checkbox.html' %}
{% elif field|is_radio %} {% include 'horizon/common/fields/_themable_radiobutton.html' %} {% else %} {% if field.auto_id %} {% include "horizon/common/_form_field_decorator.html" %} {% endif %}
{% with add_item_link=field|add_item_url %} {% if add_item_link %}
{{ field|add_bootstrap_class }}
{% else %} {% if field|is_number %} {% include 'horizon/common/fields/_themable_spinner.html' %} {% else %} {{ field|add_bootstrap_class }} {% endif %} {% endif %} {% endwith %}
{% endif %} {% for error in field.errors %} {{ error }} {% endfor %}
horizon-13.0.0/horizon/templates/horizon/common/_workflow.html0000666000175100017510000000671313245511643024671 0ustar zuulzuul00000000000000{% load i18n %} {% with workflow.get_entry_point as entry_point %}
{% csrf_token %} {% if REDIRECT_URL %}{% endif %}
{% endwith %} {% block modal-js %} {% if workflow.wizard %} {% endif %} {% endblock %} horizon-13.0.0/horizon/templates/horizon/common/_formset_table_row.html0000666000175100017510000000144613245511643026532 0ustar zuulzuul00000000000000{% load i18n %} {% for cell in row %} {% if cell.field %} {{ cell.field }} {% else %} {% if cell.wrap_list %}{% endif %} {% endif %} {% if forloop.first %} {% for field in row.form.hidden_fields %} {{ field }} {% for error in field.errors %} {% blocktrans with name=field.name %}{{ name }}: {{ error }}{% endblocktrans %} {% endfor %} {% endfor %} {% if row.form.non_field_errors %}
{{ row.form.non_field_errors }}
{% endif %} {% endif %} {% endfor %} horizon-13.0.0/horizon/templates/horizon/common/_data_table_cell.html0000666000175100017510000000347113245511643026074 0ustar zuulzuul00000000000000{% if cell.inline_edit_mod and cell.update_allowed %}
{{ cell.value }} {% if cell.column.form_field.label %} {% endif %}
{% else %} {% if cell.inline_edit_available and cell.update_allowed %}
{% if cell.wrap_list %}
    {% endif %}{{ cell.value }}{% if cell.wrap_list %}
{% endif %}
{% else %} {% if cell.wrap_list %}
    {% endif %}{{ cell.value }}{% if cell.wrap_list %}
{% endif %} {% endif %} {% endif %} horizon-13.0.0/horizon/templates/horizon/common/_datepicker.html0000666000175100017510000000044613245511643025127 0ustar zuulzuul00000000000000{% load i18n %} {% block trimmed %}
{{ datepicker_input }}
{% endblock %} horizon-13.0.0/horizon/templates/horizon/common/_form_field_decorator.html0000666000175100017510000000045113245511643027160 0ustar zuulzuul00000000000000{% if field.field.required %}{% include "horizon/common/_form_field_required.html" %}{% endif %} {% if field.help_text %} {% endif %} horizon-13.0.0/horizon/templates/horizon/common/_resource_browser.html0000666000175100017510000000126713245511643026410 0ustar zuulzuul00000000000000{% load i18n %}
{{ browser.content_table.render }}
{% blocktrans count nav_items=browser.navigation_table.data|length %}Displaying {{ nav_items }} item{% plural %}Displaying {{ nav_items }} items{% endblocktrans %} {% blocktrans count content_items=browser.content_table.data|length %}Displaying {{ content_items }} item{% plural %}Displaying {{ content_items }} items{% endblocktrans %}
horizon-13.0.0/horizon/templates/horizon/common/_sidebar.html0000666000175100017510000000012013245511643024412 0ustar zuulzuul00000000000000{% load branding horizon i18n %} horizon-13.0.0/horizon/templates/horizon/common/_datepicker_form.html0000666000175100017510000000150213245511643026144 0ustar zuulzuul00000000000000{% load i18n %}

{% trans "Select a period of time to query its usage:" %} {% trans "The date should be in YYYY-MM-DD format." %}

{% with datepicker_input=form.start datepicker_label="From" %} {% include 'horizon/common/_datepicker.html' %} {% endwith %}
{% trans 'to' %}
{% with datepicker_input=form.end datepicker_label="To" %} {% include 'horizon/common/_datepicker.html' %} {% endwith %}
horizon-13.0.0/horizon/templates/horizon/common/_data_table_action.html0000666000175100017510000000326113245511643026427 0ustar zuulzuul00000000000000{% load horizon %} {% minifyspace %} {% if action.method != "GET" %} {% else %} {% if action.icon != None %} {% endif %} {% else %} href="{{ action.bound_url }}"> {% endif %} {{ action.verbose_name }} {% endif %} {% endminifyspace %} horizon-13.0.0/horizon/templates/horizon/common/_page_header.html0000666000175100017510000000044713245511643025241 0ustar zuulzuul00000000000000{% load i18n %} {% block page_header %} {% endblock %} horizon-13.0.0/horizon/templates/horizon/common/_data_table_row_actions_dropdown.html0000666000175100017510000000171113245511643031413 0ustar zuulzuul00000000000000{% load horizon i18n %} {% spaceless %} {# This makes sure whitespace doesn't affect positioning for dropdown. #} {% if row_actions|length == 1 %} {% include "horizon/common/_data_table_action.html" with action=row_actions.0 is_single=1 %} {% elif row_actions|length > 1 %}
{% for action in row_actions %} {% if forloop.first %} {% include "horizon/common/_data_table_action.html" with is_small=1 is_single=1 %} {% endif %} {% endfor %}
{% endif %} {% endspaceless %} horizon-13.0.0/horizon/templates/horizon/common/_data_table_table_actions.html0000666000175100017510000000544413245511643027766 0ustar zuulzuul00000000000000{% load i18n bootstrap %}
{% block table_filter %} {% if filter.filter_type == 'fixed' %}
{% for button in filter.fixed_buttons %} {% endfor %}
{% elif filter.filter_type == 'query' %} {% elif filter.filter_type == 'server' %} {% endif %} {% endblock table_filter %} {% block table_actions %} {% comment %} For each single action in the Table Actions area {% endcomment %} {% for action in table_actions_buttons %} {% include "horizon/common/_data_table_action.html" with is_table_action=1 is_single=1 %} {% endfor %} {% comment %} If additional actions are defined, scoop them into the actions dropdown menu {% endcomment %} {% if table_actions_menu|length > 0 %}
{% if table_actions_buttons|length > 0 %} {{ table_actions_menu_label|default:_("More Actions") }} {% else %} {{ table_actions_menu_label|default:_("Actions") }} {% endif %}
{% endif %} {% endblock table_actions %}
horizon-13.0.0/horizon/templates/horizon/common/_form_field_required.html0000666000175100017510000000006613245511643027020 0ustar zuulzuul00000000000000 horizon-13.0.0/horizon/templates/horizon/common/_form_errors.html0000666000175100017510000000052513245511643025351 0ustar zuulzuul00000000000000{% for hidden in form.hidden_fields %} {{ hidden }} {% endfor %} {% if form.warnings %}
{{ form.warnings }}
{% endif %} {% if form.non_field_errors %}
{% for err in form.non_field_errors %}

{{ err }}

{% endfor %}
{% endif %} horizon-13.0.0/horizon/templates/horizon/common/_modal_form_add_members.html0000666000175100017510000000164513245511643027457 0ustar zuulzuul00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block modal-body %} {% endblock %} {% block modal-js %} {% endblock %} horizon-13.0.0/horizon/templates/horizon/common/_data_table_view.html0000666000175100017510000000017313245511643026123 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% block title %}{{ page_title }}{% endblock %} {% block main %}{{ table.render }}{% endblock %} horizon-13.0.0/horizon/templates/horizon/common/_breadcrumb.html0000666000175100017510000000074113245511643025120 0ustar zuulzuul00000000000000{% load i18n %} {% with subfolders=breadcrumb.get_subfolders %} {% endwith %} horizon-13.0.0/horizon/templates/horizon/common/_detail.html0000666000175100017510000000047613245511643024261 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %} {{ page_title }} {% endblock %} {% block page_header %} {% include 'horizon/common/_detail_header.html' %} {% endblock %} {% block main %}
{{ tab_group.render }}
{% endblock %} horizon-13.0.0/horizon/templates/horizon/common/_workflow_step.html0000666000175100017510000000033013245511643025711 0ustar zuulzuul00000000000000
{% include "horizon/common/_form_fields.html" %}
{{ step.get_help_text }}
horizon-13.0.0/horizon/templates/horizon/common/_data_table_row_actions_row.html0000666000175100017510000000035013245511643030364 0ustar zuulzuul00000000000000{% load i18n %}
{% block table_actions %} {% for action in row_actions %} {% include "horizon/common/_data_table_row_action_row.html" %} {% endfor %} {% endblock table_actions %}
horizon-13.0.0/horizon/templates/horizon/common/_workflow_base.html0000666000175100017510000000046413245511643025660 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans workflow.name %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=workflow.name %} {% endblock page_header %} {% block main %} {% include 'horizon/common/_workflow.html' %} {% endblock %} horizon-13.0.0/horizon/templates/horizon/common/_tab_group.html0000666000175100017510000000142113245511643024770 0ustar zuulzuul00000000000000{% with tab_group.get_tabs as tabs %} {% if tabs %} {# Tab Navigation #} {% if tab_group.show_single_tab or tabs|length > 1 %} {% endif %} {# Tab Content #}
{% for tab in tabs %}
{{ tab.render }}
{% endfor %}
{% endif %} {% endwith %} horizon-13.0.0/horizon/templates/horizon/common/_horizontal_fields.html0000666000175100017510000000027513245511643026533 0ustar zuulzuul00000000000000{% include 'horizon/common/_form_errors.html' with form=form %} {% for field in form.visible_fields %} {% include 'horizon/common/_horizontal_field.html' with field=field %} {% endfor %} horizon-13.0.0/horizon/templates/horizon/common/_modal.html0000666000175100017510000000171613245511643024111 0ustar zuulzuul00000000000000 {% block modal-js %} {% endblock %} horizon-13.0.0/horizon/templates/horizon/common/_region_selector.html0000666000175100017510000000115513245511643026175 0ustar zuulzuul00000000000000{% if regions.support %} {% endif %} horizon-13.0.0/horizon/templates/horizon/common/_data_table_pagination.html0000666000175100017510000000130713245511643027302 0ustar zuulzuul00000000000000{% load i18n %}
horizon-13.0.0/horizon/templates/horizon/common/_detail_header.html0000666000175100017510000000070213245511643025561 0ustar zuulzuul00000000000000 horizon-13.0.0/horizon/templates/horizon/common/_data_table.html0000666000175100017510000000527413245511643025100 0ustar zuulzuul00000000000000{% load i18n %} {% with table.needs_form_wrapper as needs_form_wrapper %}
{% if needs_form_wrapper %}
{% csrf_token %}{% endif %} {% with columns=table.get_columns rows=table.get_rows %} {% block table %}
{{ column }}
{% blocktrans count counter=rows|length trimmed %} Displaying {{ counter }} item{% plural %} Displaying {{ counter }} items{% endblocktrans %} {% if table.has_prev_data or table.has_more_data %} | {% endif %} {% if table.has_prev_data %} {% trans "« Prev" %} {% endif %} {% if table.has_more_data %} {% trans "Next »" %} {% endif %}
{% block table_caption %} {% endblock table_caption %} {% block table_breadcrumb %} {% if table.breadcrumb %} {% endif %} {% endblock table_breadcrumb %} {% if table.footer and rows %} {% include "horizon/common/_data_table_pagination.html" %} {% endif %} {% block table_columns %} {% if not table.is_browser_table %} {% for column in columns %} {% endfor %} {% endif %} {% endblock table_columns %} {% block table_body %} {% for row in rows %} {{ row.render }} {% empty %} {% if table.needs_filter_first %} {% else %} {% endif %} {% endfor %} {% endblock table_body %} {% block table_footer %} {% if table.footer and rows %} {% if table.needs_summary_row %} {% for column in columns %} {% if forloop.first %} {% else %} {% endif %} {% endfor %} {% endif %} {% include "horizon/common/_data_table_pagination.html" %} {% endif %} {% endblock table_footer %}
{% if not hidden_title %} {{ table }} {% endif %} {{ table.render_table_actions }}
{{ column }} {% if column.help_text %} {% endif %}
{{ table.get_filter_first_message }}{{ table.get_empty_message }}
{% trans "Summary" %}{{ column.get_summation|default_if_none:"–"}}
{% endblock table %} {% endwith %} {% if needs_form_wrapper %}{% endif %} {% endwith %} horizon-13.0.0/horizon/templates/horizon/common/_form_fields.html0000666000175100017510000000026713245511643025306 0ustar zuulzuul00000000000000{% include 'horizon/common/_form_errors.html' with form=form %} {% for field in form.visible_fields %} {% include 'horizon/common/_form_field.html' with field=field %} {% endfor %} horizon-13.0.0/horizon/templates/horizon/common/_horizontal_field.html0000666000175100017510000000206613245511643026350 0ustar zuulzuul00000000000000{% load form_helpers %}
{% if field|is_checkbox %} {% with is_horizontal=1 %} {% include 'horizon/common/fields/_themable_checkbox.html' %} {% endwith %} {% elif field|is_number %} {% include 'horizon/common/fields/_themable_spinner.html' %} {% elif field|is_radio %} {% with is_horizontal=1 %} {% include 'horizon/common/fields/_themable_radiobutton.html' %} {% endwith %} {% else %} {{ field|add_bootstrap_class }} {% endif %} {% for error in field.errors %} {{ error }} {% endfor %}
horizon-13.0.0/horizon/templates/horizon/common/_domain_page_header.html0000666000175100017510000000047413245511643026570 0ustar zuulzuul00000000000000{% load i18n %} {% block page_header %} {% endblock %} horizon-13.0.0/horizon/templates/horizon/_subnav_list.html0000666000175100017510000000126413245511643024054 0ustar zuulzuul00000000000000{% load horizon %} {% for heading, panels in components.items %} {% with panels|has_permissions_on_list:user as filtered_panels %} {% if filtered_panels %} {% if accessible_panels %} {% if heading %}

{{ heading }}

{% endif %} {% endif %} {% endif %} {% endwith %} {% endfor %} horizon-13.0.0/horizon/templates/horizon/_messages.html0000666000175100017510000000241213245511643023326 0ustar zuulzuul00000000000000{% load i18n %}
{% for message in messages %} {% if "info" in message.tags %}

{% trans "Info: " %}{{ message }}

{% endif %} {% if "warning" in message.tags %}

{% trans "Warning: " %}{{ message }}

{% endif %} {% if "success" in message.tags %}

{% trans "Success: " %}{{ message }}

{% endif %} {% if "error" in message.tags %}

{% trans "Error: " %}{{ message }}

{% endif %} {% endfor %}
horizon-13.0.0/horizon/templates/horizon/_sidebar.html0000666000175100017510000000545313245511643023140 0ustar zuulzuul00000000000000{% load horizon i18n %} horizon-13.0.0/horizon/templates/horizon/client_side/0000775000175100017510000000000013245512210022742 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/templates/horizon/client_side/_confirm.html0000666000175100017510000000100113245511643025427 0ustar zuulzuul00000000000000{% extends "horizon/client_side/template.html" %} {% load i18n horizon %} {% block id %}confirm_modal{% endblock %} {% block template %}{% spaceless %}{% jstemplate %}
{% blocktrans %}You have selected: [[selection]]. {% endblocktrans %} {% trans 'Please confirm your selection.'%} [[help]]
{% endjstemplate %}{% endspaceless %}{% endblock %} horizon-13.0.0/horizon/templates/horizon/client_side/_loading_modal.html0000666000175100017510000000103513245511643026572 0ustar zuulzuul00000000000000{% extends "horizon/client_side/template.html" %} {% load i18n horizon %} {% block id %}loader-modal{% endblock %} {% block template %}{% spaceless %}{% jstemplate %} {% endjstemplate %}{% endspaceless %}{% endblock %} horizon-13.0.0/horizon/templates/horizon/client_side/_membership.html0000666000175100017510000000173513245511643026143 0ustar zuulzuul00000000000000{% extends "horizon/client_side/template.html" %} {% load horizon %} {% block id %}membership_template{% endblock %} {% block template %}{% spaceless %}{% jstemplate %} {% endjstemplate %}{% endspaceless %}{% endblock %} horizon-13.0.0/horizon/templates/horizon/client_side/templates.html0000666000175100017510000000064613245511643025647 0ustar zuulzuul00000000000000{% include "horizon/client_side/_modal.html" %} {% include "horizon/client_side/_table_row.html" %} {% include "horizon/client_side/_alert_message.html" %} {% include "horizon/client_side/_loading_modal.html" %} {% include "horizon/client_side/_loading_inline.html" %} {% include "horizon/client_side/_membership.html" %} {% include "horizon/client_side/_confirm.html" %} {% include "horizon/client_side/_progress.html" %}horizon-13.0.0/horizon/templates/horizon/client_side/_progress.html0000666000175100017510000000107513245511643025651 0ustar zuulzuul00000000000000{% extends "horizon/client_side/template.html" %} {% load i18n horizon bootstrap %} {% block id %}progress-modal{% endblock %} {% block template %}{% spaceless %}{% jstemplate %} {% endjstemplate %}{% endspaceless %}{% endblock %} horizon-13.0.0/horizon/templates/horizon/client_side/template.html0000666000175100017510000000014713245511643025460 0ustar zuulzuul00000000000000 horizon-13.0.0/horizon/templates/horizon/client_side/_loading_inline.html0000666000175100017510000000061413245511643026756 0ustar zuulzuul00000000000000{% extends "horizon/client_side/template.html" %} {% load i18n horizon %} {% block id %}loader-inline{% endblock %} {% block template %}{% spaceless %}{% jstemplate %}
[[text]]…
{% endjstemplate %}{% endspaceless %}{% endblock %} horizon-13.0.0/horizon/templates/horizon/client_side/_script_loader.html0000666000175100017510000000104413245511643026633 0ustar zuulzuul00000000000000 horizon-13.0.0/horizon/templates/horizon/client_side/_modal.html0000666000175100017510000000147713245511643025107 0ustar zuulzuul00000000000000{% extends "horizon/client_side/template.html" %} {% load horizon %} {% block id %}modal_template{% endblock %} {% block template %}{% spaceless %}{% jstemplate %} {% endjstemplate %}{% endspaceless %}{% endblock %} horizon-13.0.0/horizon/templates/horizon/client_side/_table_row.html0000666000175100017510000000045313245511643025762 0ustar zuulzuul00000000000000{% extends "horizon/client_side/template.html" %} {% load horizon %} {% block id %}empty_row_template{% endblock %} {% block template %}{% spaceless %}{% jstemplate %} [[no_items_label]] {% endjstemplate %}{% endspaceless %}{% endblock %} horizon-13.0.0/horizon/templates/horizon/client_side/_alert_message.html0000666000175100017510000000102613245511643026614 0ustar zuulzuul00000000000000{% extends "horizon/client_side/template.html" %} {% load horizon %} {% block id %}alert_message_template{% endblock %} {% block template %}{% spaceless %}{% jstemplate %}

[[type_display]] [[#safe]] [[[message]]] [[/safe]] [[^safe]] [[message]] [[/safe]]

{% endjstemplate %}{% endspaceless %}{% endblock %} horizon-13.0.0/horizon/templates/horizon/jasmine/0000775000175100017510000000000013245512210022106 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/templates/horizon/jasmine/jasmine.html0000666000175100017510000000456213245511643024444 0ustar zuulzuul00000000000000 Jasmine Spec Runner {% for file in HORIZON_CONFIG.xstatic_lib_files %} {% endfor %} {% for file in HORIZON_CONFIG.js_files %} {% endfor %} {% block source %} {% endblock %} {% block spec %} {% for file in HORIZON_CONFIG.js_spec_files %} {% endfor %} {% endblock %} horizon-13.0.0/horizon/templates/horizon/jasmine/jasmine_legacy.html0000666000175100017510000002453113245511643025766 0ustar zuulzuul00000000000000{% extends "horizon/jasmine/jasmine.html" %} {% block source %} {% include "horizon/_script_i18n.html" %} {% include "horizon/client_side/templates.html" %} {% endblock %} {% block spec %} {% endblock %} {% block content %}
cat1
dog1
cat2
dog2
Displaying 4 items
cat1
Displaying 4 items

Flavors

Flavor Name VCPU RAM (MB) Root Disk (GB) Ephemeral Disk (GB) Swap Disk (MB) Max. VMs Delete
-
-
-
Displaying 3 items
{% endblock %} horizon-13.0.0/horizon/templates/horizon/jasmine/index.html0000666000175100017510000000053713245511643024123 0ustar zuulzuul00000000000000 Jasmine Spec Runner Index

Available tests

horizon-13.0.0/horizon/templates/horizon/_nav_list.html0000666000175100017510000000060713245511643023342 0ustar zuulzuul00000000000000{% load horizon i18n %}
horizon-13.0.0/horizon/templates/not_authorized.html0000666000175100017510000000047613245511643022736 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block breadcrumb_nav %} {% endblock %} {% block title %}{% trans "Unauthorized. Please try logging in again." %}{% endblock %} {% block main %} {% trans "You are not authorized to access this page" %} {% trans "Login" %} {% endblock %} horizon-13.0.0/horizon/templates/auth/0000775000175100017510000000000013245512210017731 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/templates/auth/_login_form.html0000666000175100017510000000570213245511643023130 0ustar zuulzuul00000000000000{% load i18n %} {% block pre_login %}
{% csrf_token %} {% endblock %}
{% block login_header %} {% endblock %}
{% block login_body %} {% comment %} These fake fields are required to prevent Chrome v34+ from autofilling form. {% endcomment %} {% if HORIZON_CONFIG.password_autocomplete != "on" %} {%endif%} {% include "auth/_description.html" %}
{% if request.user.is_authenticated and 'next' in request.GET %}

{% trans "You do not have permission to access the resource:" %}

{{ request.GET.next }}

{% url 'horizon:user_home' as home_url %} {% blocktrans trimmed %} Login as different user or go back to home page {% endblocktrans %}

{% endif %} {% if request.COOKIES.logout_reason %} {% if request.COOKIES.logout_status == "success" %}
{% else %}
{% endif %}

{{ request.COOKIES.logout_reason }}

{% endif %} {% if csrf_failure %}

{{ csrf_failure }}

{% endif %} {% if next %} {% endif %} {% include "horizon/common/_form_fields.html" %}
{% endblock %}
{% block post_login%}
{% endblock %} horizon-13.0.0/horizon/templates/auth/_login.html0000666000175100017510000000027213245511643022102 0ustar zuulzuul00000000000000{% load i18n %} {% if 'is_modal' in request.GET or 'is_modal' in request.POST %} {% include 'auth/_login_modal.html' %} {% else %} {% include 'auth/_login_page.html' %} {% endif %} horizon-13.0.0/horizon/templates/auth/_login_page.html0000666000175100017510000000103713245511643023076 0ustar zuulzuul00000000000000{% extends 'auth/_login_form.html' %} {% load i18n %} {% block pre_login %}
{{ block.super }} {% endblock %} {% block login_header %} {% include 'auth/_splash.html' %} {{ block.super }} {% endblock %} {% block login_footer %} {{ block.super }} {% include '_login_form_footer.html' %} {% endblock %} {% block post_login %} {{ block.super }}
{% endblock %} horizon-13.0.0/horizon/templates/auth/_description.html0000666000175100017510000000062213245511643023314 0ustar zuulzuul00000000000000{% load i18n %} {% comment %} This help text will only show up if websso is enabled because websso introduces new authentication mechanisms. {% endcomment %}
{% block websso-help-text %} {% blocktrans trimmed %} If you are not sure which authentication method to use, contact your administrator. {% endblocktrans %} {% endblock %}
horizon-13.0.0/horizon/templates/auth/_splash.html0000666000175100017510000000017713245511643022270 0ustar zuulzuul00000000000000{% load themes %}
horizon-13.0.0/horizon/templates/auth/login.html0000666000175100017510000000042613245511643021744 0ustar zuulzuul00000000000000{% extends "base.html" %} {% load i18n %} {% block title %}{% trans "Login" %}{% endblock %} {% block body_id %}splash{% endblock %} {% block content %} {% include 'auth/_login.html' %} {% endblock %} {% block footer %} {% include '_login_footer.html' %} {% endblock %} horizon-13.0.0/horizon/templates/auth/_login_modal.html0000666000175100017510000000130613245511643023255 0ustar zuulzuul00000000000000{% extends 'auth/_login_form.html' %} {% load i18n %} {% block pre_login %} {% endblock %} horizon-13.0.0/horizon/templates/bootstrap/0000775000175100017510000000000013245512210021005 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/templates/bootstrap/progress_bar.html0000666000175100017510000000153613245511643024403 0ustar zuulzuul00000000000000{% load horizon %} {% minifyspace %} {% if text %}
{% endif %}
{% for this_bar in bars %}
{% if not text %} {{ this_bar.percent }}% {% endif %}
{% endfor %}
{% if text %} {{ text }}
{% endif %} {% endminifyspace %} horizon-13.0.0/horizon/templates/bootstrap/breadcrumb.html0000666000175100017510000000070313245511643024014 0ustar zuulzuul00000000000000{% spaceless %} {% endspaceless %} horizon-13.0.0/horizon/hacking/0000775000175100017510000000000013245512210016376 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/hacking/__init__.py0000666000175100017510000000000013245511643020510 0ustar zuulzuul00000000000000horizon-13.0.0/horizon/hacking/checks.py0000666000175100017510000000272513245511643020231 0ustar zuulzuul00000000000000# Copyright (c) 2015 Intel, Inc. # All Rights Reserved. # # 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 re """ Guidelines for writing new hacking checks - Use only for Horizon specific tests. OpenStack general tests should be submitted to the common 'hacking' module. - Pick numbers in the range M3xx. Find the current test with the highest allocated number and then pick the next value. If nova has an N3xx code for that test, use the same number. - Keep the test method code in the source file ordered based on the M3xx value. - List the new rule in the top level HACKING.rst file - Add test cases for each new rule to /tests/unit/test_hacking.py """ mutable_default_args = re.compile(r"^\s*def .+\((.+=\{\}|.+=\[\])") def no_mutable_default_args(logical_line): msg = "M322: Method's default argument shouldn't be mutable!" if mutable_default_args.match(logical_line): yield (0, msg) def factory(register): register(no_mutable_default_args) horizon-13.0.0/horizon/test/0000775000175100017510000000000013245512210015751 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/test/unit/0000775000175100017510000000000013245512210016730 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/test/unit/test_messages.py0000666000175100017510000000427013245511643022166 0ustar zuulzuul00000000000000# 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. import json from django import http from django.utils.encoding import force_text from django.utils.safestring import mark_safe from horizon import messages from horizon import middleware from horizon.test import helpers as test class MessageTests(test.TestCase): def test_middleware_header(self): req = self.request string = "Giant ants are attacking San Francisco!" expected = ["error", force_text(string), ""] self.assertIn("async_messages", req.horizon) self.assertItemsEqual(req.horizon['async_messages'], []) req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' messages.error(req, string) self.assertItemsEqual(req.horizon['async_messages'], [expected]) res = http.HttpResponse() res = middleware.HorizonMiddleware().process_response(req, res) self.assertEqual(json.dumps([expected]), res['X-Horizon-Messages']) def test_error_message(self): req = self.request string = mark_safe("We are now safe from ants! Go here!") expected = ["error", force_text(string), " safe"] self.assertIn("async_messages", req.horizon) self.assertItemsEqual(req.horizon['async_messages'], []) req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' messages.error(req, string) self.assertItemsEqual(req.horizon['async_messages'], [expected]) res = http.HttpResponse() res = middleware.HorizonMiddleware().process_response(req, res) self.assertEqual(json.dumps([expected]), res['X-Horizon-Messages']) horizon-13.0.0/horizon/test/unit/tables/0000775000175100017510000000000013245512210020202 5ustar zuulzuul00000000000000horizon-13.0.0/horizon/test/unit/tables/test_tables.py0000666000175100017510000021153013245511653023103 0ustar zuulzuul00000000000000# encoding=utf-8 # # Copyright 2012 Nebula, Inc. # Copyright 2014 IBM Corp. # # 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 unittest import uuid from django.core.urlresolvers import reverse from django import forms from django import http from django import shortcuts from django.template import defaultfilters from django.test.utils import override_settings from django.utils.translation import ungettext_lazy import mock from mox3.mox import IsA import six from horizon import exceptions from horizon import tables from horizon.tables import actions from horizon.tables import formset as table_formset from horizon.tables import views as table_views from horizon.test import helpers as test class FakeObject(object): def __init__(self, id, name, value, status, optional=None, excluded=None): self.id = id self.name = name self.value = value self.status = status self.optional = optional self.excluded = excluded self.extra = "extra" def __str__(self): return u"%s: %s" % (self.__class__.__name__, self.name) TEST_DATA = ( FakeObject('1', 'object_1', 'value_1', 'up', 'optional_1', 'excluded_1'), FakeObject('2', 'object_2', 'evil', 'down', 'optional_2'), FakeObject('3', 'object_3', 'value_3', 'up'), FakeObject('4', u'öbject_4', u'välue_1', u'üp', u'öptional_1', u'exclüded_1'), ) TEST_DATA_2 = ( FakeObject('1', 'object_1', 'value_1', 'down', 'optional_1', 'excluded_1'), ) TEST_DATA_3 = ( FakeObject('1', 'object_1', 'value_1', 'up', 'optional_1', 'excluded_1'), ) TEST_DATA_4 = ( FakeObject('1', 'object_1', 2, 'up'), FakeObject('2', 'object_2', 4, 'up'), ) TEST_DATA_5 = ( FakeObject('1', 'object_1', 'value_1', 'A Status that is longer than 35 characters!', 'optional_1'), ) TEST_DATA_6 = ( FakeObject('1', 'object_1', 'DELETED', 'down'), FakeObject('2', 'object_2', 'CREATED', 'up'), FakeObject('3', 'object_3', 'STANDBY', 'standby'), ) TEST_DATA_7 = ( FakeObject('1', 'wrapped name', 'wrapped value', 'status', 'not wrapped optional'), ) class MyLinkAction(tables.LinkAction): name = "login" verbose_name = "Log In" url = "login" attrs = { "class": "ajax-modal", } def get_link_url(self, datum=None, *args, **kwargs): return reverse(self.url) class MyAction(tables.Action): name = "delete" verbose_name = "Delete Me" verbose_name_plural = "Delete Them" def allowed(self, request, obj=None): return getattr(obj, 'status', None) != 'down' def handle(self, data_table, request, object_ids): return shortcuts.redirect('http://example.com/?ids=%s' % ",".join(object_ids)) class MyColumn(tables.Column): pass class MyRowSelectable(tables.Row): ajax = True def can_be_selected(self, datum): return datum.value != 'DELETED' class MyRow(tables.Row): ajax = True @classmethod def get_data(cls, request, obj_id): return TEST_DATA_2[0] class MyBatchAction(tables.BatchAction): name = "batch" def action(self, request, object_ids): pass @staticmethod def action_present(count): # Translators: test code, don't really have to translate return ungettext_lazy( u"Batch Item", u"Batch Items", count ) @staticmethod def action_past(count): # Translators: test code, don't really have to translate return ungettext_lazy( u"Batched Item", u"Batched Items", count ) class MyBatchActionWithHelpText(MyBatchAction): name = "batch_help" help_text = "this is help." @staticmethod def action_present(count): # No translation return u"BatchHelp Item" @staticmethod def action_past(count): # No translation return u"BatchedHelp Item" class MyToggleAction(tables.BatchAction): name = "toggle" def action_present(self, count): if self.current_present_action: # Translators: test code, don't really have to translate return ungettext_lazy( u"Up Item", u"Up Items", count ) else: # Translators: test code, don't really have to translate return ungettext_lazy( u"Down Item", u"Down Items", count ) def action_past(self, count): if self.current_past_action: # Translators: test code, don't really have to translate return ungettext_lazy( u"Upped Item", u"Upped Items", count ) else: # Translators: test code, don't really have to translate return ungettext_lazy( u"Downed Item", u"Downed Items", count ) def allowed(self, request, obj=None): if not obj: return False self.down = getattr(obj, 'status', None) == 'down' if self.down: self.current_present_action = 1 return self.down or getattr(obj, 'status', None) == 'up' def action(self, request, object_ids): if self.down: # up it self.current_past_action = 1 class MyDisabledAction(MyToggleAction): def allowed(self, request, obj=None): return False class MyFilterAction(tables.FilterAction): def filter(self, table, objs, filter_string): q = filter_string.lower() def comp(obj): if q in obj.name.lower(): return True return False return filter(comp, objs) class MyServerFilterAction(tables.FilterAction): filter_type = 'server' filter_choices = (('name', 'Name', False), ('status', 'Status', True)) needs_preloading = True def filter(self, table, items, filter_string): filter_field = table.get_filter_field() if filter_field == 'name' and filter_string: return [item for item in items if filter_string in item.name] return items class MyUpdateAction(tables.UpdateAction): def allowed(self, *args): return True def update_cell(self, *args): pass class MyUpdateActionNotAllowed(MyUpdateAction): def allowed(self, *args): return False def get_name(obj): return "custom %s" % obj.name def get_link(obj): return reverse('login') class MyTable(tables.DataTable): tooltip_dict = {'up': {'title': 'service is up and running', 'style': 'color:green;cursor:pointer'}, 'down': {'title': 'service is not available', 'style': 'color:red;cursor:pointer'}} id = tables.Column('id', hidden=True, sortable=False) name = tables.Column(get_name, verbose_name="Verbose Name", sortable=True, form_field=forms.CharField(required=True), form_field_attributes={'class': 'test'}, update_action=MyUpdateAction) value = tables.Column('value', sortable=True, link='http://example.com/', attrs={'class': 'green blue'}, summation="average", link_classes=('link-modal',), link_attrs={'data-type': 'modal dialog', 'data-tip': 'click for dialog'}) status = tables.Column('status', link=get_link, truncate=35, cell_attributes_getter=tooltip_dict.get) optional = tables.Column('optional', empty_value='N/A') excluded = tables.Column('excluded') class Meta(object): name = "my_table" verbose_name = "My Table" status_columns = ["status"] columns = ('id', 'name', 'value', 'optional', 'status') row_class = MyRow column_class = MyColumn table_actions = (MyFilterAction, MyAction, MyBatchAction, MyBatchActionWithHelpText) row_actions = (MyAction, MyLinkAction, MyBatchAction, MyToggleAction, MyBatchActionWithHelpText) class TableWithColumnsPolicy(tables.DataTable): name = tables.Column('name') restricted = tables.Column('restricted', policy_rules=[('compute', 'role:admin')]) class MyServerFilterTable(MyTable): class Meta(object): name = "my_table" verbose_name = "My Table" status_columns = ["status"] columns = ('id', 'name', 'value', 'optional', 'status') row_class = MyRow column_class = MyColumn table_actions = (MyServerFilterAction, MyAction, MyBatchAction) row_actions = (MyAction, MyLinkAction, MyBatchAction, MyToggleAction, MyBatchActionWithHelpText) class MyTableSelectable(MyTable): class Meta(object): name = "my_table" columns = ('id', 'name', 'value', 'status') row_class = MyRowSelectable status_columns = ["status"] multi_select = True class MyTableNotAllowedInlineEdit(MyTable): name = tables.Column(get_name, verbose_name="Verbose Name", sortable=True, form_field=forms.CharField(required=True), form_field_attributes={'class': 'test'}, update_action=MyUpdateActionNotAllowed) class Meta(object): name = "my_table" columns = ('id', 'name', 'value', 'optional', 'status') row_class = MyRow class MyTableWrapList(MyTable): name = tables.Column('name', form_field=forms.CharField(required=True), form_field_attributes={'class': 'test'}, update_action=MyUpdateActionNotAllowed, wrap_list=True) value = tables.Column('value', wrap_list=True) optional = tables.Column('optional', wrap_list=False) class NoActionsTable(tables.DataTable): id = tables.Column('id') class Meta(object): name = "no_actions_table" verbose_name = "No Actions Table" table_actions = () row_actions = () class DisabledActionsTable(tables.DataTable): id = tables.Column('id') class Meta(object): name = "disabled_actions_table" verbose_name = "Disabled Actions Table" table_actions = (MyDisabledAction,) row_actions = () multi_select = True class DataTableTests(test.TestCase): def test_table_instantiation(self): """Tests everything that happens when the table is instantiated.""" self.table = MyTable(self.request, TEST_DATA) # Properties defined on the table self.assertEqual(TEST_DATA, self.table.data) self.assertEqual("my_table", self.table.name) # Verify calculated options that weren't specified explicitly self.assertTrue(self.table._meta.actions_column) self.assertTrue(self.table._meta.multi_select) # Test for verbose_name self.assertEqual(u"My Table", six.text_type(self.table)) # Column ordering and exclusion. # This should include auto-columns for multi_select and actions, # but should not contain the excluded column. # Additionally, auto-generated columns should use the custom # column class specified on the table. self.assertQuerysetEqual(self.table.columns.values(), ['', '', '', '', '', '', '']) # Actions (these also test ordering) self.assertQuerysetEqual(self.table.base_actions.values(), ['', '', '', '', '', '']) self.assertQuerysetEqual(self.table.get_table_actions(), ['', '', '', '']) self.assertQuerysetEqual(self.table.get_row_actions(TEST_DATA[0]), ['', '', '', '', '']) # Auto-generated columns multi_select = self.table.columns['multi_select'] self.assertEqual("multi_select", multi_select.auto) self.assertEqual("multi_select_column", multi_select.get_final_attrs().get('class', "")) actions = self.table.columns['actions'] self.assertEqual("actions", actions.auto) self.assertEqual("actions_column", actions.get_final_attrs().get('class', "")) # In-line edit action on column. name_column = self.table.columns['name'] self.assertEqual(MyUpdateAction, name_column.update_action) self.assertEqual(forms.CharField, name_column.form_field.__class__) self.assertEqual({'class': 'test'}, name_column.form_field_attributes) @override_settings(POLICY_CHECK_FUNCTION=lambda *args: False) def test_table_column_policy_not_allowed(self): self.table = TableWithColumnsPolicy(self.request, TEST_DATA) self.assertEqual(TEST_DATA, self.table.data) # The column "restricted" is not rendered because of policy expected_columns = [''] self.assertQuerysetEqual(self.table.columns.values(), expected_columns) @override_settings(POLICY_CHECK_FUNCTION=lambda *args: True) def test_table_column_policy_allowed(self): self.table = TableWithColumnsPolicy(self.request, TEST_DATA) self.assertEqual(TEST_DATA, self.table.data) # Policy check returns True so the column "restricted" is rendered expected_columns = ['', ''] self.assertQuerysetEqual(self.table.columns.values(), expected_columns) def test_table_force_no_multiselect(self): class TempTable(MyTable): class Meta(object): columns = ('id',) table_actions = (MyFilterAction, MyAction,) row_actions = (MyAction, MyLinkAction,) multi_select = False self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.columns.values(), ['', '']) def test_table_force_no_actions_column(self): class TempTable(MyTable): class Meta(object): columns = ('id',) table_actions = (MyFilterAction, MyAction,) row_actions = (MyAction, MyLinkAction,) actions_column = False self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.columns.values(), ['', '']) def test_table_natural_no_inline_editing(self): class TempTable(MyTable): name = tables.Column(get_name, verbose_name="Verbose Name", sortable=True) class Meta(object): name = "my_table" columns = ('id', 'name', 'value', 'optional', 'status') self.table = TempTable(self.request, TEST_DATA_2) name_column = self.table.columns['name'] self.assertIsNone(name_column.update_action) self.assertIsNone(name_column.form_field) self.assertEqual({}, name_column.form_field_attributes) def test_table_natural_no_actions_column(self): class TempTable(MyTable): class Meta(object): columns = ('id',) table_actions = (MyFilterAction, MyAction,) self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.columns.values(), ['', '']) def test_table_natural_no_multiselect(self): class TempTable(MyTable): class Meta(object): columns = ('id',) row_actions = (MyAction, MyLinkAction,) self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.columns.values(), ['', '']) def test_table_column_inheritance(self): class TempTable(MyTable): extra = tables.Column('extra') class Meta(object): name = "temp_table" table_actions = (MyFilterAction, MyAction,) row_actions = (MyAction, MyLinkAction,) self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.columns.values(), ['', '', '', '', '', '', '', '', '']) def test_table_construction(self): self.table = MyTable(self.request, TEST_DATA) # Verify we retrieve the right columns for headers columns = self.table.get_columns() self.assertQuerysetEqual(columns, ['', '', '', '', '', '', '']) # Verify we retrieve the right rows from our data rows = self.table.get_rows() self.assertQuerysetEqual(rows, ['', '', '', '']) # Verify each row contains the right cells self.assertQuerysetEqual(rows[0].get_cells(), ['', '', '', '', '', '', '']) def test_table_column(self): self.table = MyTable(self.request, TEST_DATA) row = self.table.get_rows()[0] row3 = self.table.get_rows()[2] id_col = self.table.columns['id'] name_col = self.table.columns['name'] value_col = self.table.columns['value'] # transform self.assertEqual('1', row.cells['id'].data) # Standard attr access self.assertEqual('custom object_1', row.cells['name'].data) # Callable # name and verbose_name self.assertEqual("Id", six.text_type(id_col)) self.assertEqual("Verbose Name", six.text_type(name_col)) # sortable self.assertFalse(id_col.sortable) self.assertNotIn("sortable", id_col.get_final_attrs().get('class', "")) self.assertTrue(name_col.sortable) self.assertIn("sortable", name_col.get_final_attrs().get('class', "")) # hidden self.assertTrue(id_col.hidden) self.assertIn("hide", id_col.get_final_attrs().get('class', "")) self.assertFalse(name_col.hidden) self.assertNotIn("hide", name_col.get_final_attrs().get('class', "")) # link, link_classes, link_attrs, and get_link_url self.assertIn('href="http://example.com/"', row.cells['value'].value) self.assertIn('class="link-modal"', row.cells['value'].value) self.assertIn('data-type="modal dialog"', row.cells['value'].value) self.assertIn('data-tip="click for dialog"', row.cells['value'].value) self.assertIn('href="/auth/login/"', row.cells['status'].value) # empty_value self.assertEqual("N/A", row3.cells['optional'].value) # classes self.assertEqual("green blue sortable anchor normal_column", value_col.get_final_attrs().get('class', "")) # status cell_status = row.cells['status'].status self.assertTrue(cell_status) self.assertEqual('status_up', row.cells['status'].get_status_class(cell_status)) # status_choices id_col.status = True id_col.status_choices = (('1', False), ('2', True), ('3', None)) cell_status = row.cells['id'].status self.assertFalse(cell_status) self.assertEqual('status_down', row.cells['id'].get_status_class(cell_status)) cell_status = row3.cells['id'].status self.assertIsNone(cell_status) self.assertEqual('warning', row.cells['id'].get_status_class(cell_status)) # Ensure data is not cached on the column across table instances self.table = MyTable(self.request, TEST_DATA_2) row = self.table.get_rows()[0] self.assertIn("down", row.cells['status'].value) def test_table_row(self): self.table = MyTable(self.request, TEST_DATA) row = self.table.get_rows()[0] self.assertEqual(self.table, row.table) self.assertEqual(TEST_DATA[0], row.datum) self.assertEqual('my_table__row__1', row.id) # Verify row status works even if status isn't set on the column self.assertTrue(row.status) self.assertEqual('status_up', row.status_class) # Check the cells as well cell_status = row.cells['status'].status self.assertTrue(cell_status) self.assertEqual('status_up', row.cells['status'].get_status_class(cell_status)) def test_table_column_truncation(self): self.table = MyTable(self.request, TEST_DATA_5) row = self.table.get_rows()[0] self.assertEqual(35, len(row.cells['status'].data)) self.assertEqual(u'A Status that is longer than 35 ...', row.cells['status'].data) def test_table_rendering(self): self.table = MyTable(self.request, TEST_DATA) # Table actions table_actions = self.table.render_table_actions() resp = http.HttpResponse(table_actions) self.assertContains(resp, "table_search", 1) self.assertContains(resp, "my_table__filter__q", 1) self.assertContains(resp, "my_table__delete", 1) self.assertContains(resp, 'id="my_table__action_delete"', 1) # Table BatchActions self.assertContains(resp, 'id="my_table__action_batch_help"', 1) self.assertContains(resp, 'help_text="this is help."', 1) self.assertContains(resp, 'BatchHelp Item', 1) # Row actions row_actions = self.table.render_row_actions(TEST_DATA[0]) resp = http.HttpResponse(row_actions) self.assertContains(resp, "