horizon-13.0.0/ 0000775 0001751 0001751 00000000000 13245512211 013303 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/ 0000775 0001751 0001751 00000000000 13245512210 014772 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/ 0000775 0001751 0001751 00000000000 13245512210 015717 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/dash_template/ 0000775 0001751 0001751 00000000000 13245512210 020531 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/dash_template/templates/ 0000775 0001751 0001751 00000000000 13245512210 022527 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/dash_template/templates/dash_name/ 0000775 0001751 0001751 00000000000 13245512210 024446 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/dash_template/templates/dash_name/base.html 0000666 0001751 0001751 00000000442 13245511643 026261 0 ustar zuul zuul 0000000 0000000 {% 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.tmpl 0000666 0001751 0001751 00000001607 13245511643 024024 0 ustar zuul zuul 0000000 0000000 # 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/ 0000775 0001751 0001751 00000000000 13245512210 022020 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/dash_template/static/dash_name/ 0000775 0001751 0001751 00000000000 13245512210 023737 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/dash_template/static/dash_name/js/ 0000775 0001751 0001751 00000000000 13245512210 024353 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/dash_template/static/dash_name/js/dash_name.js 0000666 0001751 0001751 00000000061 13245511643 026640 0 ustar zuul zuul 0000000 0000000 /* Additional JavaScript for {{ dash_name }}. */ horizon-13.0.0/horizon/conf/dash_template/static/dash_name/scss/ 0000775 0001751 0001751 00000000000 13245512210 024712 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/dash_template/static/dash_name/scss/dash_name.scss 0000666 0001751 0001751 00000000053 13245511643 027537 0 ustar zuul zuul 0000000 0000000 /* Additional SCSS for {{ dash_name }}. */ horizon-13.0.0/horizon/conf/dash_template/__init__.py 0000666 0001751 0001751 00000000000 13245511643 022643 0 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/default.py 0000666 0001751 0001751 00000003331 13245511643 017730 0 ustar zuul zuul 0000000 0000000 # 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/ 0000775 0001751 0001751 00000000000 13245512210 020711 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/panel_template/tests.py.tmpl 0000666 0001751 0001751 00000001341 13245511643 023412 0 ustar zuul zuul 0000000 0000000 # 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.py 0000666 0001751 0001751 00000001505 13245511643 022434 0 ustar zuul zuul 0000000 0000000 # 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/ 0000775 0001751 0001751 00000000000 13245512210 022707 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/panel_template/templates/panel_name/ 0000775 0001751 0001751 00000000000 13245512210 025006 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/panel_template/templates/panel_name/index.html 0000666 0001751 0001751 00000000524 13245511643 027017 0 ustar zuul zuul 0000000 0000000 {% 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.tmpl 0000666 0001751 0001751 00000001304 13245511643 023234 0 ustar zuul zuul 0000000 0000000 # 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.tmpl 0000666 0001751 0001751 00000001610 13245511643 023346 0 ustar zuul zuul 0000000 0000000 # 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__.py 0000666 0001751 0001751 00000000000 13245511643 023023 0 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/conf/__init__.py 0000666 0001751 0001751 00000004253 13245511643 020047 0 ustar zuul zuul 0000000 0000000 # 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.py 0000666 0001751 0001751 00000006531 13245511643 017173 0 ustar zuul zuul 0000000 0000000 # 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/ 0000775 0001751 0001751 00000000000 13245512210 016432 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/contrib/bootstrap_datepicker.py 0000666 0001751 0001751 00000003060 13245511643 023226 0 ustar zuul zuul 0000000 0000000 # 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/ 0000775 0001751 0001751 00000000000 13245512210 020744 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/contrib/staticfiles/finders.py 0000666 0001751 0001751 00000003223 13245511643 022763 0 ustar zuul zuul 0000000 0000000 # 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__.py 0000666 0001751 0001751 00000000000 13245511643 023056 0 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/contrib/__init__.py 0000666 0001751 0001751 00000000000 13245511643 020544 0 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/tables/ 0000775 0001751 0001751 00000000000 13245512210 016244 5 ustar zuul zuul 0000000 0000000 horizon-13.0.0/horizon/tables/actions.py 0000666 0001751 0001751 00000117446 13245511643 020306 0 ustar zuul zuul 0000000 0000000 # 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.py 0000666 0001751 0001751 00000036231 13245511643 017773 0 ustar zuul zuul 0000000 0000000 # 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.py 0000666 0001751 0001751 00000225137 13245511643 017555 0 ustar zuul zuul 0000000 0000000 # 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 ``