1# Copyright 2012 Nebula, Inc.
2#
3#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4#    not use this file except in compliance with the License. You may obtain
5#    a copy of the License at
6#
7#         http://www.apache.org/licenses/LICENSE-2.0
8#
9#    Unless required by applicable law or agreed to in writing, software
10#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12#    License for the specific language governing permissions and limitations
13#    under the License.
14
15import collections
16import collections.abc
17import copy
18import inspect
19import json
20import logging
21from operator import attrgetter
22import sys
23
24from django.conf import settings
25from django.core import exceptions as core_exceptions
26from django import forms
27from django.http import HttpResponse
28from django import template
29from django.template.defaultfilters import slugify
30from django.template.defaultfilters import truncatechars
31from django.template.loader import render_to_string
32from django import urls
33from django.utils import encoding
34from django.utils.html import escape
35from django.utils import http
36from django.utils.http import urlencode
37from django.utils.safestring import mark_safe
38from django.utils import termcolors
39from django.utils.translation import ugettext_lazy as _
40
41from horizon import conf
42from horizon import exceptions
43from horizon.forms import ThemableCheckboxInput
44from horizon import messages
45from horizon.tables.actions import BatchAction
46from horizon.tables.actions import FilterAction
47from horizon.tables.actions import LinkAction
48from horizon.utils import html
49from horizon.utils import settings as utils_settings
50
51
52LOG = logging.getLogger(__name__)
53PALETTE = termcolors.PALETTES[termcolors.DEFAULT_PALETTE]
54STRING_SEPARATOR = "__"
55
56
57class Column(html.HTMLElement):
58    """A class which represents a single column in a :class:`.DataTable`.
59
60    .. attribute:: transform
61
62        A string or callable. If ``transform`` is a string, it should be the
63        name of the attribute on the underlying data class which
64        should be displayed in this column. If it is a callable, it
65        will be passed the current row's data at render-time and should
66        return the contents of the cell. Required.
67
68    .. attribute:: verbose_name
69
70        The name for this column which should be used for display purposes.
71        Defaults to the value of ``transform`` with the first letter
72        of each word capitalized if the ``transform`` is not callable,
73        otherwise it defaults to an empty string (``""``).
74
75    .. attribute:: sortable
76
77        Boolean to determine whether this column should be sortable or not.
78        Defaults to ``True``.
79
80    .. attribute:: hidden
81
82        Boolean to determine whether or not this column should be displayed
83        when rendering the table. Default: ``False``.
84
85    .. attribute:: link
86
87        A string or callable which returns a URL which will be wrapped around
88        this column's text as a link.
89
90    .. attribute:: allowed_data_types
91
92        A list of data types for which the link should be created.
93        Default is an empty list (``[]``).
94
95        When the list is empty and the ``link`` attribute is not None, all the
96        rows under this column will be links.
97
98    .. attribute::  status
99
100        Boolean designating whether or not this column represents a status
101        (i.e. "enabled/disabled", "up/down", "active/inactive").
102        Default: ``False``.
103
104    .. attribute::  status_choices
105
106        A tuple of tuples representing the possible data values for the
107        status column and their associated boolean equivalent. Positive
108        states should equate to ``True``, negative states should equate
109        to ``False``, and indeterminate states should be ``None``.
110
111        Values are compared in a case-insensitive manner.
112
113        Example (these are also the default values)::
114
115            status_choices = (
116                    ('enabled', True),
117                    ('true', True),
118                    ('up', True),
119                    ('active', True),
120                    ('yes', True),
121                    ('on', True),
122                    ('none', None),
123                    ('unknown', None),
124                    ('', None),
125                    ('disabled', False),
126                    ('down', False),
127                    ('false', False),
128                    ('inactive', False),
129                    ('no', False),
130                    ('off', False),
131                )
132
133    .. attribute::  display_choices
134
135        A tuple of tuples representing the possible values to substitute
136        the data when displayed in the column cell.
137
138    .. attribute:: empty_value
139
140        A string or callable to be used for cells which have no data.
141        Defaults to the string ``"-"``.
142
143    .. attribute:: summation
144
145        A string containing the name of a summation method to be used in
146        the generation of a summary row for this column. By default the
147        options are ``"sum"`` or ``"average"``, which behave as expected.
148        Optional.
149
150    .. attribute:: filters
151
152        A list of functions (often template filters) to be applied to the
153        value of the data for this column prior to output. This is effectively
154        a shortcut for writing a custom ``transform`` function in simple cases.
155
156    .. attribute:: classes
157
158        An iterable of CSS classes which should be added to this column.
159        Example: ``classes=('foo', 'bar')``.
160
161    .. attribute:: attrs
162
163        A dict of HTML attribute strings which should be added to this column.
164        Example: ``attrs={"data-foo": "bar"}``.
165
166    .. attribute:: cell_attributes_getter
167
168       A callable to get the HTML attributes of a column cell depending
169       on the data. For example, to add additional description or help
170       information for data in a column cell (e.g. in Images panel, for the
171       column 'format')::
172
173            helpText = {
174              'ARI':'Amazon Ramdisk Image',
175              'QCOW2':'QEMU' Emulator'
176              }
177
178            getHoverHelp(data):
179              text = helpText.get(data, None)
180              if text:
181                  return {'title': text}
182              else:
183                  return {}
184            ...
185            ...
186            cell_attributes_getter = getHoverHelp
187
188    .. attribute:: truncate
189
190        An integer for the maximum length of the string in this column. If the
191        length of the data in this column is larger than the supplied number,
192        the data for this column will be truncated and an ellipsis will be
193        appended to the truncated data.
194        Defaults to ``None``.
195
196    .. attribute:: link_classes
197
198        An iterable of CSS classes which will be added when the column's text
199        is displayed as a link.
200        This is left for backward compatibility. Deprecated in favor of the
201        link_attributes attribute.
202        Example: ``link_classes=('link-foo', 'link-bar')``.
203        Defaults to ``None``.
204
205    .. attribute:: wrap_list
206
207        Boolean value indicating whether the contents of this cell should be
208        wrapped in a ``<ul></ul>`` tag. Useful in conjunction with Django's
209        ``unordered_list`` template filter. Defaults to ``False``.
210
211    .. attribute:: form_field
212
213        A form field used for inline editing of the column. A django
214        forms.Field can be used or django form.Widget can be used.
215
216        Example: ``form_field=forms.CharField()``.
217        Defaults to ``None``.
218
219    .. attribute:: form_field_attributes
220
221        The additional html attributes that will be rendered to form_field.
222        Example: ``form_field_attributes={'class': 'bold_input_field'}``.
223        Defaults to ``None``.
224
225    .. attribute:: update_action
226
227        The class that inherits from tables.actions.UpdateAction, update_cell
228        method takes care of saving inline edited data. The tables.base.Row
229        get_data method needs to be connected to table for obtaining the data.
230        Example: ``update_action=UpdateCell``.
231        Defaults to ``None``.
232
233    .. attribute:: link_attrs
234
235        A dict of HTML attribute strings which should be added when the
236        column's text is displayed as a link.
237        Examples:
238        ``link_attrs={"data-foo": "bar"}``.
239        ``link_attrs={"target": "_blank", "class": "link-foo link-bar"}``.
240        Defaults to ``None``.
241
242    .. attribute:: policy_rules
243
244        List of scope and rule tuples to do policy checks on, the
245        composition of which is (scope, rule)
246
247        * scope: service type managing the policy for action
248        * rule: string representing the action to be checked
249
250        for a policy that requires a single rule check,
251        policy_rules should look like:
252
253        .. code-block:: none
254
255            "(("compute", "compute:create_instance"),)"
256
257        for a policy that requires multiple rule checks,
258        rules should look like:
259
260        .. code-block:: none
261
262            "(("identity", "identity:list_users"),
263              ("identity", "identity:list_roles"))"
264
265    .. attribute:: help_text
266
267        A string of simple help text displayed in a tooltip when you hover
268        over the help icon beside the Column name. Defaults to ``None``.
269    """
270    summation_methods = {
271        "sum": sum,
272        "average": lambda data: sum(data, 0.0) / len(data)
273    }
274    # Used to retain order when instantiating columns on a table
275    creation_counter = 0
276
277    transform = None
278    name = None
279    verbose_name = None
280    status_choices = (
281        ('enabled', True),
282        ('true', True),
283        ('up', True),
284        ('yes', True),
285        ('active', True),
286        ('on', True),
287        ('none', None),
288        ('unknown', None),
289        ('', None),
290        ('disabled', False),
291        ('down', False),
292        ('false', False),
293        ('inactive', False),
294        ('no', False),
295        ('off', False),
296    )
297
298    def __init__(self, transform, verbose_name=None, sortable=True,
299                 link=None, allowed_data_types=None, hidden=False, attrs=None,
300                 status=False, status_choices=None, display_choices=None,
301                 empty_value=None, filters=None, classes=None, summation=None,
302                 auto=None, truncate=None, link_classes=None, wrap_list=False,
303                 form_field=None, form_field_attributes=None,
304                 update_action=None, link_attrs=None, policy_rules=None,
305                 cell_attributes_getter=None, help_text=None):
306
307        allowed_data_types = allowed_data_types or []
308        self.classes = list(classes or getattr(self, "classes", []))
309        super().__init__()
310        self.attrs.update(attrs or {})
311
312        if callable(transform):
313            self.transform = transform
314            self.name = "<%s callable>" % transform.__name__
315        else:
316            self.transform = str(transform)
317            self.name = self.transform
318
319        # Empty string is a valid value for verbose_name
320        if verbose_name is None:
321            if callable(transform):
322                self.verbose_name = ''
323            else:
324                self.verbose_name = self.transform.title()
325        else:
326            self.verbose_name = str(verbose_name)
327
328        self.auto = auto
329        self.sortable = sortable
330        self.link = link
331        self.allowed_data_types = allowed_data_types
332        self.hidden = hidden
333        self.status = status
334        self.empty_value = empty_value or _('-')
335        self.filters = filters or []
336        self.truncate = truncate
337        self.wrap_list = wrap_list
338        self.form_field = form_field
339        self.form_field_attributes = form_field_attributes or {}
340        self.update_action = update_action
341        self.link_attrs = link_attrs or {}
342        self.policy_rules = policy_rules or []
343        self.help_text = help_text
344        if link_classes:
345            self.link_attrs['class'] = ' '.join(link_classes)
346        self.cell_attributes_getter = cell_attributes_getter
347
348        if status_choices:
349            self.status_choices = status_choices
350        self.display_choices = display_choices
351
352        if summation is not None and summation not in self.summation_methods:
353            raise ValueError(
354                "Summation method %(summation)s must be one of %(keys)s." %
355                {'summation': summation,
356                 'keys': ", ".join(self.summation_methods.keys())})
357        self.summation = summation
358
359        self.creation_counter = Column.creation_counter
360        Column.creation_counter += 1
361
362        if self.sortable and not self.auto:
363            self.classes.append("sortable")
364        if self.hidden:
365            self.classes.append("hide")
366        if self.link is not None:
367            self.classes.append('anchor')
368
369    def __str__(self):
370        return self.verbose_name
371
372    def __repr__(self):
373        return '<%s: %s>' % (self.__class__.__name__, self.name)
374
375    def allowed(self, request):
376        """Determine whether processing/displaying the column is allowed.
377
378        It is determined based on the current request.
379        """
380        if not self.policy_rules:
381            return True
382
383        policy_check = utils_settings.import_setting("POLICY_CHECK_FUNCTION")
384
385        if policy_check:
386            return policy_check(self.policy_rules, request)
387        return True
388
389    def get_raw_data(self, datum):
390        """Returns the raw data for this column.
391
392        No filters or formatting are applied to the returned data.
393        This is useful when doing calculations on data in the table.
394        """
395        # Callable transformations
396        if callable(self.transform):
397            data = self.transform(datum)
398        # Dict lookups
399        elif (isinstance(datum, collections.abc.Mapping) and
400              self.transform in datum):
401            data = datum.get(self.transform)
402        else:
403            # Basic object lookups
404            data = getattr(datum, self.transform, None)
405            if not hasattr(datum, self.transform):
406                msg = "The attribute %(attr)s doesn't exist on %(obj)s."
407                LOG.debug(termcolors.colorize(msg, **PALETTE['ERROR']),
408                          {'attr': self.transform, 'obj': datum})
409        return data
410
411    def get_data(self, datum):
412        """Returns the final display data for this column from the given inputs.
413
414        The return value will be either the attribute specified for this column
415        or the return value of the attr:`~horizon.tables.Column.transform`
416        method for this column.
417        """
418        datum_id = self.table.get_object_id(datum)
419
420        if datum_id in self.table._data_cache[self]:
421            return self.table._data_cache[self][datum_id]
422
423        data = self.get_raw_data(datum)
424        display_value = None
425
426        if self.display_choices:
427            display_value = [display for (value, display) in
428                             self.display_choices
429                             if value.lower() == (data or '').lower()]
430
431        if display_value:
432            data = display_value[0]
433        else:
434            for filter_func in self.filters:
435                try:
436                    data = filter_func(data)
437                except Exception:
438                    msg = ("Filter '%(filter)s' failed with data "
439                           "'%(data)s' on column '%(col_name)s'")
440                    args = {'filter': filter_func.__name__,
441                            'data': data,
442                            'col_name': self.verbose_name}
443                    LOG.warning(msg, args)
444
445        if data and self.truncate:
446            data = truncatechars(data, self.truncate)
447
448        self.table._data_cache[self][datum_id] = data
449
450        return self.table._data_cache[self][datum_id]
451
452    def get_link_url(self, datum):
453        """Returns the final value for the column's ``link`` property.
454
455        If ``allowed_data_types`` of this column  is not empty and the datum
456        has an assigned type, check if the datum's type is in the
457        ``allowed_data_types`` list. If not, the datum won't be displayed
458        as a link.
459
460        If ``link`` is a callable, it will be passed the current data object
461        and should return a URL. Otherwise ``get_link_url`` will attempt to
462        call ``reverse`` on ``link`` with the object's id as a parameter.
463        Failing that, it will simply return the value of ``link``.
464        """
465        if self.allowed_data_types:
466            data_type_name = self.table._meta.data_type_name
467            data_type = getattr(datum, data_type_name, None)
468            if data_type and (data_type not in self.allowed_data_types):
469                return None
470        obj_id = self.table.get_object_id(datum)
471        if callable(self.link):
472            if 'request' in inspect.getfullargspec(self.link).args:
473                return self.link(datum, request=self.table.request)
474            return self.link(datum)
475        try:
476            return urls.reverse(self.link, args=(obj_id,))
477        except urls.NoReverseMatch:
478            return self.link
479
480    if settings.INTEGRATION_TESTS_SUPPORT:
481        def get_default_attrs(self):
482            attrs = super().get_default_attrs()
483            attrs.update({'data-selenium': self.name})
484            return attrs
485
486    def get_summation(self):
487        """Returns the summary value for the data in this column.
488
489        It returns the summary value if a valid summation method is
490        specified for it. Otherwise returns ``None``.
491        """
492        if self.summation not in self.summation_methods:
493            return None
494
495        summation_function = self.summation_methods[self.summation]
496        data = [self.get_raw_data(datum) for datum in self.table.data]
497        data = [raw_data for raw_data in data if raw_data is not None]
498
499        if data:
500            try:
501                summation = summation_function(data)
502                for filter_func in self.filters:
503                    summation = filter_func(summation)
504                return summation
505            except TypeError:
506                pass
507        return None
508
509
510class WrappingColumn(Column):
511    """A column that wraps its contents. Useful for data like UUIDs or names"""
512
513    def __init__(self, *args, **kwargs):
514        super().__init__(*args, **kwargs)
515        self.classes.append('word-break')
516
517
518class Row(html.HTMLElement):
519    """Represents a row in the table.
520
521    When iterated, the ``Row`` instance will yield each of its cells.
522
523    Rows are capable of AJAX updating, with a little added work:
524
525    The ``ajax`` property needs to be set to ``True``, and
526    subclasses need to define a ``get_data`` method which returns a data
527    object appropriate for consumption by the table (effectively the "get"
528    lookup versus the table's "list" lookup).
529
530    The automatic update interval is configurable by setting the key
531    ``ajax_poll_interval`` in the ``HORIZON_CONFIG`` dictionary.
532    Default: ``2500`` (measured in milliseconds).
533
534    .. attribute:: table
535
536        The table which this row belongs to.
537
538    .. attribute:: datum
539
540        The data object which this row represents.
541
542    .. attribute:: id
543
544        A string uniquely representing this row composed of the table name
545        and the row data object's identifier.
546
547    .. attribute:: cells
548
549        The cells belonging to this row stored in a ``OrderedDict`` object.
550        This attribute is populated during instantiation.
551
552    .. attribute:: status
553
554        Boolean value representing the status of this row calculated from
555        the values of the table's ``status_columns`` if they are set.
556
557    .. attribute:: status_class
558
559        Returns a css class for the status of the row based on ``status``.
560
561    .. attribute:: ajax
562
563        Boolean value to determine whether ajax updating for this row is
564        enabled.
565
566    .. attribute:: ajax_action_name
567
568        String that is used for the query parameter key to request AJAX
569        updates. Generally you won't need to change this value.
570        Default: ``"row_update"``.
571
572    .. attribute:: ajax_cell_action_name
573
574        String that is used for the query parameter key to request AJAX
575        updates of cell. Generally you won't need to change this value.
576        It is also used for inline edit of the cell.
577        Default: ``"cell_update"``.
578    """
579    ajax = False
580    ajax_action_name = "row_update"
581    ajax_cell_action_name = "cell_update"
582
583    def __init__(self, table, datum=None):
584        super().__init__()
585        self.table = table
586        self.datum = datum
587        self.selected = False
588        if self.datum:
589            self.load_cells()
590        else:
591            self.id = None
592            self.cells = []
593
594    def load_cells(self, datum=None):
595        """Load the row's data and initialize all the cells in the row.
596
597        It also set the appropriate row properties which require
598        the row's data to be determined.
599
600        The row's data is provided either at initialization or as an
601        argument to this function.
602
603        This function is called automatically by
604        :meth:`~horizon.tables.Row.__init__` if the ``datum`` argument is
605        provided. However, by not providing the data during initialization
606        this function allows for the possibility of a two-step loading
607        pattern when you need a row instance but don't yet have the data
608        available.
609        """
610        # Compile all the cells on instantiation.
611        table = self.table
612        if datum:
613            self.datum = datum
614        else:
615            datum = self.datum
616        cells = []
617        for column in table.columns.values():
618            cell = table._meta.cell_class(datum, column, self)
619            cells.append((column.name or column.auto, cell))
620        self.cells = collections.OrderedDict(cells)
621
622        if self.ajax:
623            interval = conf.HORIZON_CONFIG['ajax_poll_interval']
624            self.attrs['data-update-interval'] = interval
625            self.attrs['data-update-url'] = self.get_ajax_update_url()
626            self.classes.append("ajax-update")
627
628        self.attrs['data-object-id'] = table.get_object_id(datum)
629
630        # Add the row's status class and id to the attributes to be rendered.
631        self.classes.append(self.status_class)
632        id_vals = {"table": self.table.name,
633                   "sep": STRING_SEPARATOR,
634                   "id": table.get_object_id(datum)}
635        self.id = "%(table)s%(sep)srow%(sep)s%(id)s" % id_vals
636        self.attrs['id'] = self.id
637
638        # Add the row's display name if available
639        display_name = table.get_object_display(datum)
640        display_name_key = table.get_object_display_key(datum)
641
642        if display_name:
643            self.attrs['data-display'] = escape(display_name)
644            self.attrs['data-display-key'] = escape(display_name_key)
645
646    def __repr__(self):
647        return '<%s: %s>' % (self.__class__.__name__, self.id)
648
649    def __iter__(self):
650        return iter(self.cells.values())
651
652    @property
653    def status(self):
654        column_names = self.table._meta.status_columns
655        if column_names:
656            statuses = dict((column_name, self.cells[column_name].status) for
657                            column_name in column_names)
658            return self.table.calculate_row_status(statuses)
659
660    @property
661    def status_class(self):
662        column_names = self.table._meta.status_columns
663        if column_names:
664            return self.table.get_row_status_class(self.status)
665        return ''
666
667    def render(self):
668        return render_to_string("horizon/common/_data_table_row.html",
669                                {"row": self})
670
671    def get_cells(self):
672        """Returns the bound cells for this row in order."""
673        return list(self.cells.values())
674
675    def get_ajax_update_url(self):
676        table_url = self.table.get_absolute_url()
677        marker_name = self.table._meta.pagination_param
678        marker = self.table.request.GET.get(marker_name, None)
679        if not marker:
680            marker_name = self.table._meta.prev_pagination_param
681            marker = self.table.request.GET.get(marker_name, None)
682        request_params = [
683            ("action", self.ajax_action_name),
684            ("table", self.table.name),
685            ("obj_id", self.table.get_object_id(self.datum)),
686        ]
687        if marker:
688            request_params.append((marker_name, marker))
689        params = urlencode(collections.OrderedDict(request_params))
690        return "%s?%s" % (table_url, params)
691
692    def can_be_selected(self, datum):
693        """Determines whether the row can be selected.
694
695        By default if multiselect enabled return True.
696        You can remove the checkbox after an ajax update here if required.
697        """
698        return True
699
700    def get_data(self, request, obj_id):
701        """Fetches the updated data for the row based on the given object ID.
702
703        Must be implemented by a subclass to allow AJAX updating.
704        """
705        return {}
706
707
708class Cell(html.HTMLElement):
709    """Represents a single cell in the table."""
710
711    def __init__(self, datum, column, row, attrs=None, classes=None):
712        self.classes = classes or getattr(self, "classes", [])
713        super().__init__()
714        self.attrs.update(attrs or {})
715
716        self.datum = datum
717        self.column = column
718        self.row = row
719        self.wrap_list = column.wrap_list
720        self.inline_edit_available = self.column.update_action is not None
721        # initialize the update action if available
722        if self.inline_edit_available:
723            self.update_action = self.column.update_action()
724            self.attrs['data-cell-name'] = column.name
725            self.attrs['data-update-url'] = self.get_ajax_update_url()
726        self.inline_edit_mod = False
727        # add tooltip to cells if the truncate variable is set
728        if column.truncate:
729            # NOTE(tsufiev): trying to pull cell raw data out of datum for
730            # those columns where truncate is False leads to multiple errors
731            # in unit tests
732            data = getattr(datum, column.name, '') or ''
733            data = encoding.force_text(data)
734            if len(data) > column.truncate:
735                self.attrs['data-toggle'] = 'tooltip'
736                self.attrs['title'] = data
737                if settings.INTEGRATION_TESTS_SUPPORT:
738                    self.attrs['data-selenium'] = data
739        self.data = self.get_data(datum, column, row)
740
741    def get_data(self, datum, column, row):
742        """Fetches the data to be displayed in this cell."""
743        table = row.table
744        if column.auto == "multi_select":
745            data = ""
746            if row.can_be_selected(datum):
747                widget = ThemableCheckboxInput(check_test=lambda value: False)
748                # Convert value to string to avoid accidental type conversion
749                data = widget.render('object_ids',
750                                     table.get_object_id(datum),
751                                     {'class': 'table-row-multi-select'})
752            table._data_cache[column][table.get_object_id(datum)] = data
753        elif column.auto == "form_field":
754            widget = column.form_field
755            if issubclass(widget.__class__, forms.Field):
756                widget = widget.widget
757
758            widget_name = "%s__%s" % \
759                (column.name,
760                 table.get_object_id(datum))
761
762            # Create local copy of attributes, so it don't change column
763            # class form_field_attributes
764            form_field_attributes = {}
765            form_field_attributes.update(column.form_field_attributes)
766            # Adding id of the input so it pairs with label correctly
767            form_field_attributes['id'] = widget_name
768
769            if (template.defaultfilters.urlize in column.filters or
770                    template.defaultfilters.yesno in column.filters):
771                data = widget.render(widget_name,
772                                     column.get_raw_data(datum),
773                                     form_field_attributes)
774            else:
775                data = widget.render(widget_name,
776                                     column.get_data(datum),
777                                     form_field_attributes)
778            table._data_cache[column][table.get_object_id(datum)] = data
779        elif column.auto == "actions":
780            data = table.render_row_actions(datum)
781            table._data_cache[column][table.get_object_id(datum)] = data
782        else:
783            data = column.get_data(datum)
784            if column.cell_attributes_getter:
785                cell_attributes = column.cell_attributes_getter(data) or {}
786                self.attrs.update(cell_attributes)
787        return data
788
789    def __repr__(self):
790        return '<%s: %s, %s>' % (self.__class__.__name__,
791                                 self.column.name,
792                                 self.row.id)
793
794    @property
795    def id(self):
796        return ("%s__%s" % (self.column.name,
797                self.row.table.get_object_id(self.datum)))
798
799    @property
800    def value(self):
801        """Returns a formatted version of the data for final output.
802
803        This takes into consideration the
804        :attr:`~horizon.tables.Column.link`` and
805        :attr:`~horizon.tables.Column.empty_value`
806        attributes.
807        """
808        try:
809            data = self.column.get_data(self.datum)
810            if data is None:
811                if callable(self.column.empty_value):
812                    data = self.column.empty_value(self.datum)
813                else:
814                    data = self.column.empty_value
815        except Exception as e:
816            raise template.TemplateSyntaxError from e
817
818        if self.url and not self.column.auto == "form_field":
819            link_attrs = ' '.join(['%s="%s"' % (k, v) for (k, v) in
820                                  self.column.link_attrs.items()])
821            # Escape the data inside while allowing our HTML to render
822            data = mark_safe('<a href="%s" %s>%s</a>' % (
823                             (escape(self.url),
824                              link_attrs,
825                              escape(data))))
826        return data
827
828    @property
829    def url(self):
830        if self.column.link:
831            url = self.column.get_link_url(self.datum)
832            if url:
833                return url
834        else:
835            return None
836
837    @property
838    def status(self):
839        """Gets the status for the column based on the cell's data."""
840        # Deal with status column mechanics based in this cell's data
841        if hasattr(self, '_status'):
842            # pylint: disable=access-member-before-definition
843            return self._status
844
845        if self.column.status or \
846                self.column.name in self.column.table._meta.status_columns:
847            # returns the first matching status found
848            data_status_lower = str(
849                self.column.get_raw_data(self.datum)).lower()
850            for status_name, status_value in self.column.status_choices:
851                if str(status_name).lower() == data_status_lower:
852                    self._status = status_value
853                    return self._status
854        self._status = None
855        return self._status
856
857    def get_status_class(self, status):
858        """Returns a css class name determined by the status value."""
859        if status is True:
860            return "status_up"
861        if status is False:
862            return "status_down"
863        return "warning"
864
865    def get_default_classes(self):
866        """Returns a flattened string of the cell's CSS classes."""
867        if not self.url:
868            self.column.classes = [cls for cls in self.column.classes
869                                   if cls != "anchor"]
870        column_class_string = self.column.get_final_attrs().get('class', "")
871        classes = set(column_class_string.split(" "))
872        if self.column.status:
873            classes.add(self.get_status_class(self.status))
874
875        if self.inline_edit_available:
876            classes.add("inline_edit_available")
877
878        return list(classes)
879
880    def get_ajax_update_url(self):
881        column = self.column
882        table_url = column.table.get_absolute_url()
883        params = urlencode(collections.OrderedDict([
884            ("action", self.row.ajax_cell_action_name),
885            ("table", column.table.name),
886            ("cell_name", column.name),
887            ("obj_id", column.table.get_object_id(self.datum))
888        ]))
889
890        return "%s?%s" % (table_url, params)
891
892    @property
893    def update_allowed(self):
894        """Determines whether update of given cell is allowed.
895
896        Calls allowed action of defined UpdateAction of the Column.
897        """
898        return self.update_action.allowed(self.column.table.request,
899                                          self.datum,
900                                          self)
901
902    def render(self):
903        return render_to_string("horizon/common/_data_table_cell.html",
904                                {"cell": self})
905
906
907class DataTableOptions(object):
908    """Contains options for :class:`.DataTable` objects.
909
910    .. attribute:: name
911
912        A short name or slug for the table.
913
914    .. attribute:: verbose_name
915
916        A more verbose name for the table meant for display purposes.
917
918    .. attribute:: columns
919
920        A list of column objects or column names. Controls ordering/display
921        of the columns in the table.
922
923    .. attribute:: table_actions
924
925        A list of action classes derived from the
926        :class:`~horizon.tables.Action` class. These actions will handle tasks
927        such as bulk deletion, etc. for multiple objects at once.
928
929    .. attribute:: table_actions_menu
930
931        A list of action classes similar to ``table_actions`` except these
932        will be displayed in a menu instead of as individual buttons. Actions
933        from this list will take precedence over actions from the
934        ``table_actions`` list.
935
936    .. attribute:: table_actions_menu_label
937
938        A label of a menu button for ``table_actions_menu``. The default is
939        "Actions" or "More Actions" depending on ``table_actions``.
940
941    .. attribute:: row_actions
942
943        A list similar to ``table_actions`` except tailored to appear for
944        each row. These actions act on a single object at a time.
945
946    .. attribute:: actions_column
947
948        Boolean value to control rendering of an additional column containing
949        the various actions for each row. Defaults to ``True`` if any actions
950        are specified in the ``row_actions`` option.
951
952    .. attribute:: multi_select
953
954        Boolean value to control rendering of an extra column with checkboxes
955        for selecting multiple objects in the table. Defaults to ``True`` if
956        any actions are specified in the ``table_actions`` option.
957
958    .. attribute:: filter
959
960        Boolean value to control the display of the "filter" search box
961        in the table actions. By default it checks whether or not an instance
962        of :class:`.FilterAction` is in ``table_actions``.
963
964    .. attribute:: template
965
966        String containing the template which should be used to render the
967        table. Defaults to ``"horizon/common/_data_table.html"``.
968
969    .. attribute:: row_actions_dropdown_template
970
971        String containing the template which should be used to render the
972        row actions dropdown. Defaults to
973        ``"horizon/common/_data_table_row_actions_dropdown.html"``.
974
975    .. attribute:: row_actions_row_template
976
977        String containing the template which should be used to render the
978        row actions. Defaults to
979        ``"horizon/common/_data_table_row_actions_row.html"``.
980
981    .. attribute:: table_actions_template
982
983        String containing the template which should be used to render the
984        table actions. Defaults to
985        ``"horizon/common/_data_table_table_actions.html"``.
986
987    .. attribute:: context_var_name
988
989        The name of the context variable which will contain the table when
990        it is rendered. Defaults to ``"table"``.
991
992    .. attribute:: prev_pagination_param
993
994        The name of the query string parameter which will be used when
995        paginating backward in this table. When using multiple tables in a
996        single view this will need to be changed to differentiate between the
997        tables. Default: ``"prev_marker"``.
998
999    .. attribute:: pagination_param
1000
1001        The name of the query string parameter which will be used when
1002        paginating forward in this table. When using multiple tables in a
1003        single view this will need to be changed to differentiate between the
1004        tables. Default: ``"marker"``.
1005
1006    .. attribute:: status_columns
1007
1008        A list or tuple of column names which represents the "state"
1009        of the data object being represented.
1010
1011        If ``status_columns`` is set, when the rows are rendered the value
1012        of this column will be used to add an extra class to the row in
1013        the form of ``"status_up"`` or ``"status_down"`` for that row's
1014        data.
1015
1016        The row status is used by other Horizon components to trigger tasks
1017        such as dynamic AJAX updating.
1018
1019    .. attribute:: cell_class
1020
1021        The class which should be used for rendering the cells of this table.
1022        Optional. Default: :class:`~horizon.tables.Cell`.
1023
1024    .. attribute:: row_class
1025
1026        The class which should be used for rendering the rows of this table.
1027        Optional. Default: :class:`~horizon.tables.Row`.
1028
1029    .. attribute:: column_class
1030
1031        The class which should be used for handling the columns of this table.
1032        Optional. Default: :class:`~horizon.tables.Column`.
1033
1034    .. attribute:: css_classes
1035
1036        A custom CSS class or classes to add to the ``<table>`` tag of the
1037        rendered table, for when the particular table requires special styling.
1038        Default: ``""``.
1039
1040    .. attribute:: mixed_data_type
1041
1042        A toggle to indicate if the table accepts two or more types of data.
1043        Optional. Default: ``False``
1044
1045    .. attribute:: data_types
1046
1047        A list of data types that this table would accept. Default to be an
1048        empty list, but if the attribute ``mixed_data_type`` is set to
1049        ``True``, then this list must have at least one element.
1050
1051    .. attribute:: data_type_name
1052
1053        The name of an attribute to assign to data passed to the table when it
1054        accepts mix data. Default: ``"_table_data_type"``
1055
1056    .. attribute:: footer
1057
1058        Boolean to control whether or not to show the table's footer.
1059        Default: ``True``.
1060
1061    .. attribute:: hidden_title
1062
1063        Boolean to control whether or not to show the table's title.
1064        Default: ``True``.
1065
1066    .. attribute:: permissions
1067
1068        A list of permission names which this table requires in order to be
1069        displayed. Defaults to an empty list (``[]``).
1070    """
1071    def __init__(self, options):
1072        self.name = getattr(options, 'name', self.__class__.__name__)
1073        verbose_name = (getattr(options, 'verbose_name', None) or
1074                        self.name.title())
1075        self.verbose_name = verbose_name
1076        self.columns = getattr(options, 'columns', None)
1077        self.status_columns = getattr(options, 'status_columns', [])
1078        self.table_actions = getattr(options, 'table_actions', [])
1079        self.row_actions = getattr(options, 'row_actions', [])
1080        self.table_actions_menu = getattr(options, 'table_actions_menu', [])
1081        self.table_actions_menu_label = getattr(options,
1082                                                'table_actions_menu_label',
1083                                                None)
1084        self.cell_class = getattr(options, 'cell_class', Cell)
1085        self.row_class = getattr(options, 'row_class', Row)
1086        self.column_class = getattr(options, 'column_class', Column)
1087        self.css_classes = getattr(options, 'css_classes', '')
1088        self.prev_pagination_param = getattr(options,
1089                                             'prev_pagination_param',
1090                                             'prev_marker')
1091        self.pagination_param = getattr(options, 'pagination_param', 'marker')
1092        self.browser_table = getattr(options, 'browser_table', None)
1093        self.footer = getattr(options, 'footer', True)
1094        self.hidden_title = getattr(options, 'hidden_title', True)
1095        self.no_data_message = getattr(options,
1096                                       "no_data_message",
1097                                       _("No items to display."))
1098        self.permissions = getattr(options, 'permissions', [])
1099
1100        # Set self.filter if we have any FilterActions
1101        filter_actions = [action for action in self.table_actions if
1102                          issubclass(action, FilterAction)]
1103        batch_actions = [action for action in self.table_actions if
1104                         issubclass(action, BatchAction)]
1105        if len(filter_actions) > 1:
1106            raise NotImplementedError("Multiple filter actions are not "
1107                                      "currently supported.")
1108        self.filter = getattr(options, 'filter', len(filter_actions) > 0)
1109        if len(filter_actions) == 1:
1110            self._filter_action = filter_actions.pop()
1111        else:
1112            self._filter_action = None
1113
1114        self.template = getattr(options,
1115                                'template',
1116                                'horizon/common/_data_table.html')
1117        self.row_actions_dropdown_template = \
1118            getattr(options,
1119                    'row_actions_dropdown_template',
1120                    'horizon/common/_data_table_row_actions_dropdown.html')
1121        self.row_actions_row_template = \
1122            getattr(options,
1123                    'row_actions_row_template',
1124                    'horizon/common/_data_table_row_actions_row.html')
1125        self.table_actions_template = \
1126            getattr(options,
1127                    'table_actions_template',
1128                    'horizon/common/_data_table_table_actions.html')
1129        self.context_var_name = getattr(options,
1130                                        'context_var_name',
1131                                        'table')
1132        self.actions_column = getattr(options,
1133                                      'actions_column',
1134                                      len(self.row_actions) > 0)
1135        self.multi_select = getattr(options,
1136                                    'multi_select',
1137                                    len(batch_actions) > 0)
1138
1139        # Set runtime table defaults; not configurable.
1140        self.has_prev_data = False
1141        self.has_more_data = False
1142
1143        # Set mixed data type table attr
1144        self.mixed_data_type = getattr(options, 'mixed_data_type', False)
1145        self.data_types = getattr(options, 'data_types', [])
1146
1147        # If the data_types has more than 2 elements, set mixed_data_type
1148        # to True automatically.
1149        if len(self.data_types) > 1:
1150            self.mixed_data_type = True
1151
1152        # However, if the mixed_data_type is set to True manually and
1153        # the data_types is empty, raise an error.
1154        if self.mixed_data_type and len(self.data_types) <= 1:
1155            raise ValueError("If mixed_data_type is set to True in class %s, "
1156                             "data_types should has more than one types" %
1157                             self.name)
1158
1159        self.data_type_name = getattr(options,
1160                                      'data_type_name',
1161                                      "_table_data_type")
1162
1163        self.filter_first_message = \
1164            getattr(options,
1165                    'filter_first_message',
1166                    _('Please specify a search criteria first.'))
1167
1168
1169class DataTableMetaclass(type):
1170    """Metaclass to add options to DataTable class and collect columns."""
1171    def __new__(cls, name, bases, attrs):
1172        # Process options from Meta
1173        class_name = name
1174        dt_attrs = {}
1175        dt_attrs["_meta"] = opts = DataTableOptions(attrs.get("Meta", None))
1176
1177        # Gather columns; this prevents the column from being an attribute
1178        # on the DataTable class and avoids naming conflicts.
1179        columns = []
1180        for attr_name, obj in attrs.items():
1181            if isinstance(obj, (opts.column_class, Column)):
1182                column_instance = attrs[attr_name]
1183                column_instance.name = attr_name
1184                column_instance.classes.append('normal_column')
1185                columns.append((attr_name, column_instance))
1186            else:
1187                dt_attrs[attr_name] = obj
1188        columns.sort(key=lambda x: x[1].creation_counter)
1189
1190        # Iterate in reverse to preserve final order
1191        for base in reversed(bases):
1192            if hasattr(base, 'base_columns'):
1193                columns[0:0] = base.base_columns.items()
1194        dt_attrs['base_columns'] = collections.OrderedDict(columns)
1195
1196        # If the table is in a ResourceBrowser, the column number must meet
1197        # these limits because of the width of the browser.
1198        if opts.browser_table == "navigation" and len(columns) > 3:
1199            raise ValueError("You can assign at most three columns to %s."
1200                             % class_name)
1201        if opts.browser_table == "content" and len(columns) > 2:
1202            raise ValueError("You can assign at most two columns to %s."
1203                             % class_name)
1204
1205        if opts.columns:
1206            # Remove any columns that weren't declared if we're being explicit
1207            # NOTE: we're iterating a COPY of the list here!
1208            for column_data in columns[:]:
1209                if column_data[0] not in opts.columns:
1210                    columns.pop(columns.index(column_data))
1211            # Re-order based on declared columns
1212            columns.sort(key=lambda x: dt_attrs['_meta'].columns.index(x[0]))
1213        # Add in our auto-generated columns
1214        if opts.multi_select and opts.browser_table != "navigation":
1215            multi_select = opts.column_class("multi_select",
1216                                             verbose_name="",
1217                                             auto="multi_select")
1218            multi_select.classes.append('multi_select_column')
1219            columns.insert(0, ("multi_select", multi_select))
1220        if opts.actions_column:
1221            actions_column = opts.column_class("actions",
1222                                               verbose_name=_("Actions"),
1223                                               auto="actions")
1224            actions_column.classes.append('actions_column')
1225            columns.append(("actions", actions_column))
1226        # Store this set of columns internally so we can copy them per-instance
1227        dt_attrs['_columns'] = collections.OrderedDict(columns)
1228
1229        # Gather and register actions for later access since we only want
1230        # to instantiate them once.
1231        # (list() call gives deterministic sort order, which sets don't have.)
1232        actions = list(set(opts.row_actions) | set(opts.table_actions) |
1233                       set(opts.table_actions_menu))
1234        actions.sort(key=attrgetter('name'))
1235        actions_dict = collections.OrderedDict([(action.name, action())
1236                                                for action in actions])
1237        dt_attrs['base_actions'] = actions_dict
1238        if opts._filter_action:
1239            # Replace our filter action with the instantiated version
1240            opts._filter_action = actions_dict[opts._filter_action.name]
1241
1242        # Create our new class!
1243        return type.__new__(cls, name, bases, dt_attrs)
1244
1245
1246class DataTable(object, metaclass=DataTableMetaclass):
1247    """A class which defines a table with all data and associated actions.
1248
1249    .. attribute:: name
1250
1251        String. Read-only access to the name specified in the
1252        table's Meta options.
1253
1254    .. attribute:: multi_select
1255
1256        Boolean. Read-only access to whether or not this table
1257        should display a column for multi-select checkboxes.
1258
1259    .. attribute:: data
1260
1261        Read-only access to the data this table represents.
1262
1263    .. attribute:: filtered_data
1264
1265        Read-only access to the data this table represents, filtered by
1266        the :meth:`~horizon.tables.FilterAction.filter` method of the table's
1267        :class:`~horizon.tables.FilterAction` class (if one is provided)
1268        using the current request's query parameters.
1269    """
1270
1271    def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):
1272        self.request = request
1273        self.data = data
1274        self.kwargs = kwargs
1275        self._needs_form_wrapper = needs_form_wrapper
1276        self._no_data_message = self._meta.no_data_message
1277        self.breadcrumb = None
1278        self.current_item_id = None
1279        self.permissions = self._meta.permissions
1280        self.needs_filter_first = False
1281        self._filter_first_message = self._meta.filter_first_message
1282
1283        # Create a new set
1284        columns = []
1285        for key, _column in self._columns.items():
1286            if _column.allowed(request):
1287                column = copy.copy(_column)
1288                column.table = self
1289                columns.append((key, column))
1290        self.columns = collections.OrderedDict(columns)
1291        self._populate_data_cache()
1292
1293        # Associate these actions with this table
1294        for action in self.base_actions.values():
1295            action.associate_with_table(self)
1296
1297        self.needs_summary_row = any([col.summation
1298                                      for col in self.columns.values()])
1299        # For multi-process, we need to set the multi_column to be visible
1300        # or hidden each time.
1301        # Example: first process the multi_column visible but second
1302        # process the column is hidden. Updating row by ajax will
1303        # make the bug#1799151
1304        if request.GET.get('action') == 'row_update':
1305            bound_actions = self.get_table_actions()
1306            batch_actions = [action for action in bound_actions
1307                             if isinstance(action, BatchAction)]
1308            self.set_multiselect_column_visibility(bool(batch_actions))
1309
1310    def __str__(self):
1311        return str(self._meta.verbose_name)
1312
1313    def __repr__(self):
1314        return '<%s: %s>' % (self.__class__.__name__, self._meta.name)
1315
1316    @property
1317    def name(self):
1318        return self._meta.name
1319
1320    @property
1321    def footer(self):
1322        return self._meta.footer
1323
1324    @property
1325    def multi_select(self):
1326        return self._meta.multi_select
1327
1328    @property
1329    def filtered_data(self):
1330        # This function should be using django.utils.functional.cached_property
1331        # decorator, but unfortunately due to bug in Django
1332        # https://code.djangoproject.com/ticket/19872 it would make it fail
1333        # when being mocked in tests.
1334        # TODO(amotoki): Check if this trick is still required.
1335        if not hasattr(self, '_filtered_data'):
1336            self._filtered_data = self.data
1337            if self._meta.filter and self._meta._filter_action:
1338                action = self._meta._filter_action
1339                filter_string = self.get_filter_string()
1340                filter_field = self.get_filter_field()
1341                request_method = self.request.method
1342                needs_preloading = (not filter_string and
1343                                    request_method == 'GET' and
1344                                    action.needs_preloading)
1345                valid_method = (request_method == action.method)
1346                not_api_filter = (filter_string and
1347                                  not action.is_api_filter(filter_field))
1348
1349                if valid_method or needs_preloading or not_api_filter:
1350                    if self._meta.mixed_data_type:
1351                        self._filtered_data = action.data_type_filter(
1352                            self, self.data, filter_string)
1353                    else:
1354                        self._filtered_data = action.filter(
1355                            self, self.data, filter_string)
1356        return self._filtered_data
1357
1358    def slugify_name(self):
1359        return str(slugify(self._meta.name))
1360
1361    def get_filter_string(self):
1362        """Get the filter string value.
1363
1364        For 'server' type filters this is saved in the session so that
1365        it gets persisted across table loads.  For other filter types
1366        this is obtained from the POST dict.
1367        """
1368        filter_action = self._meta._filter_action
1369        param_name = filter_action.get_param_name()
1370        filter_string = ''
1371        if filter_action.filter_type == 'server':
1372            filter_string = self.request.session.get(param_name, '')
1373        else:
1374            filter_string = self.request.POST.get(param_name, '')
1375        return filter_string
1376
1377    def get_filter_field(self):
1378        """Get the filter field value used for 'server' type filters.
1379
1380        This is the value from the filter action's list of filter choices.
1381        """
1382        filter_action = self._meta._filter_action
1383        param_name = '%s_field' % filter_action.get_param_name()
1384        filter_field = self.request.session.get(param_name, '')
1385        return filter_field
1386
1387    def _populate_data_cache(self):
1388        self._data_cache = {}
1389        # Set up hash tables to store data points for each column
1390        for column in self.get_columns():
1391            self._data_cache[column] = {}
1392
1393    def _filter_action(self, action, request, datum=None):
1394        try:
1395            # Catch user errors in permission functions here
1396            row_matched = True
1397            if self._meta.mixed_data_type:
1398                row_matched = action.data_type_matched(datum)
1399            return action._allowed(request, datum) and row_matched
1400        except AssertionError:
1401            # don't trap mox exceptions (which subclass AssertionError)
1402            # when testing!
1403            # TODO(amotoki): Check if this trick is still required.
1404            raise
1405        except Exception:
1406            LOG.exception("Error while checking action permissions.")
1407            return None
1408
1409    def is_browser_table(self):
1410        if self._meta.browser_table:
1411            return True
1412        return False
1413
1414    def render(self):
1415        """Renders the table using the template from the table options."""
1416        table_template = template.loader.get_template(self._meta.template)
1417        extra_context = {self._meta.context_var_name: self,
1418                         'hidden_title': self._meta.hidden_title}
1419        return table_template.render(extra_context, self.request)
1420
1421    def get_absolute_url(self):
1422        """Returns the canonical URL for this table.
1423
1424        This is used for the POST action attribute on the form element
1425        wrapping the table. In many cases it is also useful for redirecting
1426        after a successful action on the table.
1427
1428        For convenience it defaults to the value of
1429        ``request.get_full_path()`` with any query string stripped off,
1430        e.g. the path at which the table was requested.
1431        """
1432        return self.request.get_full_path().partition('?')[0]
1433
1434    def get_full_url(self):
1435        """Returns the full URL path for this table.
1436
1437        This is used for the POST action attribute on the form element
1438        wrapping the table. We use this method to persist the
1439        pagination marker.
1440
1441        """
1442        return self.request.get_full_path()
1443
1444    def get_empty_message(self):
1445        """Returns the message to be displayed when there is no data."""
1446        return self._no_data_message
1447
1448    def get_filter_first_message(self):
1449        """Return the message to be displayed first in the filter.
1450
1451        when the user needs to provide a search criteria first
1452        before loading any data.
1453        """
1454        return self._filter_first_message
1455
1456    def get_object_by_id(self, lookup):
1457        """Returns the data object whose ID matches ``loopup`` parameter.
1458
1459        The data object is looked up from the table's dataset and
1460        the data which matches the ``lookup`` parameter specified.
1461        An error will be raised if the match is not a single data object.
1462
1463        We will convert the object id and ``lookup`` to unicode before
1464        comparison.
1465
1466        Uses :meth:`~horizon.tables.DataTable.get_object_id` internally.
1467        """
1468        if not isinstance(lookup, str):
1469            lookup = str(lookup)
1470        matches = []
1471        for datum in self.data:
1472            obj_id = self.get_object_id(datum)
1473            if not isinstance(obj_id, str):
1474                obj_id = str(obj_id)
1475            if obj_id == lookup:
1476                matches.append(datum)
1477        if len(matches) > 1:
1478            raise ValueError("Multiple matches were returned for that id: %s."
1479                             % matches)
1480        if not matches:
1481            raise exceptions.Http302(self.get_absolute_url(),
1482                                     _('No match returned for the id "%s".')
1483                                     % lookup)
1484        return matches[0]
1485
1486    @property
1487    def has_actions(self):
1488        """Indicates whether there are any available actions on this table.
1489
1490        Returns a boolean value.
1491        """
1492        if not self.base_actions:
1493            return False
1494        return any(self.get_table_actions()) or any(self._meta.row_actions)
1495
1496    @property
1497    def needs_form_wrapper(self):
1498        """Returns if this table should be rendered wrapped in a ``<form>`` tag.
1499
1500        Returns a boolean value.
1501        """
1502        # If needs_form_wrapper is explicitly set, defer to that.
1503        if self._needs_form_wrapper is not None:
1504            return self._needs_form_wrapper
1505        # Otherwise calculate whether or not we need a form element.
1506        return self.has_actions
1507
1508    def get_table_actions(self):
1509        """Returns a list of the action instances for this table."""
1510        button_actions = [self.base_actions[action.name] for action in
1511                          self._meta.table_actions if
1512                          action not in self._meta.table_actions_menu]
1513        menu_actions = [self.base_actions[action.name] for
1514                        action in self._meta.table_actions_menu]
1515        bound_actions = button_actions + menu_actions
1516        return [action for action in bound_actions if
1517                self._filter_action(action, self.request)]
1518
1519    def get_row_actions(self, datum):
1520        """Returns a list of the action instances for a specific row."""
1521        bound_actions = []
1522        for action in self._meta.row_actions:
1523            # Copy to allow modifying properties per row
1524            bound_action = copy.copy(self.base_actions[action.name])
1525            bound_action.attrs = copy.copy(bound_action.attrs)
1526            bound_action.datum = datum
1527            # Remove disallowed actions.
1528            if not self._filter_action(bound_action,
1529                                       self.request,
1530                                       datum):
1531                continue
1532            # Hook for modifying actions based on data. No-op by default.
1533            bound_action.update(self.request, datum)
1534            # Pre-create the URL for this link with appropriate parameters
1535            if issubclass(bound_action.__class__, LinkAction):
1536                bound_action.bound_url = bound_action.get_link_url(datum)
1537            bound_actions.append(bound_action)
1538        return bound_actions
1539
1540    def set_multiselect_column_visibility(self, visible=True):
1541        """hide checkbox column if no current table action is allowed."""
1542        if not self.multi_select:
1543            return
1544        select_column = list(self.columns.values())[0]
1545        # Try to find if the hidden class need to be
1546        # removed or added based on visible flag.
1547        hidden_found = 'hidden' in select_column.classes
1548        if hidden_found and visible:
1549            select_column.classes.remove('hidden')
1550        elif not hidden_found and not visible:
1551            select_column.classes.append('hidden')
1552
1553    def render_table_actions(self):
1554        """Renders the actions specified in ``Meta.table_actions``."""
1555        template_path = self._meta.table_actions_template
1556        table_actions_template = template.loader.get_template(template_path)
1557        bound_actions = self.get_table_actions()
1558        batch_actions = [action for action in bound_actions
1559                         if isinstance(action, BatchAction)]
1560        extra_context = {"table_actions": bound_actions,
1561                         "table_actions_buttons": [],
1562                         "table_actions_menu": []}
1563        if self._meta.filter and (
1564                self._filter_action(self._meta._filter_action, self.request)):
1565            extra_context["filter"] = self._meta._filter_action
1566        for action in bound_actions:
1567            if action.__class__ in self._meta.table_actions_menu:
1568                extra_context['table_actions_menu'].append(action)
1569            elif action != extra_context.get('filter'):
1570                extra_context['table_actions_buttons'].append(action)
1571        if self._meta.table_actions_menu_label:
1572            extra_context['table_actions_menu_label'] = \
1573                self._meta.table_actions_menu_label
1574        self.set_multiselect_column_visibility(bool(batch_actions))
1575        return table_actions_template.render(extra_context, self.request)
1576
1577    def render_row_actions(self, datum, row=False):
1578        """Renders the actions specified in ``Meta.row_actions``.
1579
1580        The actions are rendered using the current row data.
1581        If `row` is True, the actions are rendered in a row
1582        of buttons. Otherwise they are rendered in a dropdown box.
1583        """
1584        if row:
1585            template_path = self._meta.row_actions_row_template
1586        else:
1587            template_path = self._meta.row_actions_dropdown_template
1588
1589        row_actions_template = template.loader.get_template(template_path)
1590        bound_actions = self.get_row_actions(datum)
1591        extra_context = {"row_actions": bound_actions,
1592                         "row_id": self.get_object_id(datum)}
1593        return row_actions_template.render(extra_context, self.request)
1594
1595    @staticmethod
1596    def parse_action(action_string):
1597        """Parses the ``action_string`` parameter sent back with the POST data.
1598
1599        By default this parses a string formatted as
1600        ``{{ table_name }}__{{ action_name }}__{{ row_id }}`` and returns
1601        each of the pieces. The ``row_id`` is optional.
1602        """
1603        if action_string:
1604            bits = action_string.split(STRING_SEPARATOR)
1605            table = bits[0]
1606            action = bits[1]
1607            try:
1608                object_id = STRING_SEPARATOR.join(bits[2:])
1609                if object_id == '':
1610                    object_id = None
1611            except IndexError:
1612                object_id = None
1613            return table, action, object_id
1614
1615    def take_action(self, action_name, obj_id=None, obj_ids=None):
1616        """Locates the appropriate action and routes the object data to it.
1617
1618        The action should return an HTTP redirect if successful,
1619        or a value which evaluates to ``False`` if unsuccessful.
1620        """
1621        # See if we have a list of ids
1622        obj_ids = obj_ids or self.request.POST.getlist('object_ids')
1623        action = self.base_actions.get(action_name, None)
1624        if not action or action.method != self.request.method:
1625            # We either didn't get an action or we're being hacked. Goodbye.
1626            return None
1627
1628        # Meanwhile, back in Gotham...
1629        if not action.requires_input or obj_id or obj_ids:
1630            if obj_id:
1631                obj_id = self.sanitize_id(obj_id)
1632            if obj_ids:
1633                obj_ids = [self.sanitize_id(i) for i in obj_ids]
1634            # Single handling is easy
1635            if not action.handles_multiple:
1636                response = action.single(self, self.request, obj_id)
1637            # Otherwise figure out what to pass along
1638            else:
1639                # Preference given to a specific id, since that implies
1640                # the user selected an action for just one row.
1641                if obj_id:
1642                    obj_ids = [obj_id]
1643                response = action.multiple(self, self.request, obj_ids)
1644            return response
1645        if action and action.requires_input and not (obj_id or obj_ids):
1646            messages.info(self.request,
1647                          _("Please select a row before taking that action."))
1648        return None
1649
1650    @classmethod
1651    def check_handler(cls, request):
1652        """Determine whether the request should be handled by this table."""
1653        if request.method == "POST" and "action" in request.POST:
1654            table, action, obj_id = cls.parse_action(request.POST["action"])
1655        elif "table" in request.GET and "action" in request.GET:
1656            table = request.GET["table"]
1657            action = request.GET["action"]
1658            obj_id = request.GET.get("obj_id", None)
1659        else:
1660            table = action = obj_id = None
1661        return table, action, obj_id
1662
1663    def maybe_preempt(self):
1664        """Determine whether the request should be handled in earlier phase.
1665
1666        It determines the request should be handled by a preemptive action
1667        on this table or by an AJAX row update before loading any data.
1668        """
1669        request = self.request
1670        table_name, action_name, obj_id = self.check_handler(request)
1671
1672        if table_name == self.name:
1673            # Handle AJAX row updating.
1674            new_row = self._meta.row_class(self)
1675
1676            if new_row.ajax and new_row.ajax_action_name == action_name:
1677                try:
1678                    datum = new_row.get_data(request, obj_id)
1679                    if self.get_object_id(datum) == self.current_item_id:
1680                        self.selected = True
1681                        new_row.classes.append('current_selected')
1682                    new_row.load_cells(datum)
1683                    error = False
1684                except Exception:
1685                    datum = None
1686                    error = exceptions.handle(request, ignore=True)
1687                if request.is_ajax():
1688                    if not error:
1689                        return HttpResponse(new_row.render())
1690                    return HttpResponse(status=error.status_code)
1691            elif new_row.ajax_cell_action_name == action_name:
1692                # inline edit of the cell actions
1693                return self.inline_edit_handle(request, table_name,
1694                                               action_name, obj_id,
1695                                               new_row)
1696
1697            preemptive_actions = [action for action in
1698                                  self.base_actions.values() if action.preempt]
1699            if action_name:
1700                for action in preemptive_actions:
1701                    if action.name == action_name:
1702                        handled = self.take_action(action_name, obj_id)
1703                        if handled:
1704                            return handled
1705        return None
1706
1707    def inline_edit_handle(self, request, table_name, action_name, obj_id,
1708                           new_row):
1709        """Inline edit handler.
1710
1711        Showing form or handling update by POST of the cell.
1712        """
1713        try:
1714            cell_name = request.GET['cell_name']
1715            datum = new_row.get_data(request, obj_id)
1716            # TODO(lsmola) extract load cell logic to Cell and load
1717            # only 1 cell. This is kind of ugly.
1718            if request.GET.get('inline_edit_mod') == "true":
1719                new_row.table.columns[cell_name].auto = "form_field"
1720                inline_edit_mod = True
1721            else:
1722                inline_edit_mod = False
1723
1724            # Load the cell and set the inline_edit_mod.
1725            new_row.load_cells(datum)
1726            cell = new_row.cells[cell_name]
1727            cell.inline_edit_mod = inline_edit_mod
1728
1729            # If not allowed, neither edit mod or updating is allowed.
1730            if not cell.update_allowed:
1731                datum_display = (self.get_object_display(datum) or "N/A")
1732                LOG.info('Permission denied to Update Action: "%s"',
1733                         datum_display)
1734                return HttpResponse(status=401)
1735            # If it is post request, we are updating the cell.
1736            if request.method == "POST":
1737                return self.inline_update_action(request,
1738                                                 datum,
1739                                                 cell,
1740                                                 obj_id,
1741                                                 cell_name)
1742
1743            error = False
1744        except Exception:
1745            datum = None
1746            error = exceptions.handle(request, ignore=True)
1747        if request.is_ajax():
1748            if not error:
1749                return HttpResponse(cell.render())
1750            return HttpResponse(status=error.status_code)
1751
1752    def inline_update_action(self, request, datum, cell, obj_id, cell_name):
1753        """Handling update by POST of the cell."""
1754        new_cell_value = request.POST.get(
1755            cell_name + '__' + obj_id, None)
1756        if issubclass(cell.column.form_field.__class__,
1757                      forms.Field):
1758            try:
1759                # using Django Form Field to parse the
1760                # right value from POST and to validate it
1761                new_cell_value = (
1762                    cell.column.form_field.clean(
1763                        new_cell_value))
1764                cell.update_action.action(
1765                    self.request, datum, obj_id, cell_name, new_cell_value)
1766                response = {
1767                    'status': 'updated',
1768                    'message': ''
1769                }
1770                return HttpResponse(
1771                    json.dumps(response),
1772                    status=200,
1773                    content_type="application/json")
1774
1775            except core_exceptions.ValidationError:
1776                # if there is a validation error, I will
1777                # return the message to the client
1778                exc_type, exc_value, exc_traceback = (
1779                    sys.exc_info())
1780                response = {
1781                    'status': 'validation_error',
1782                    'message': ' '.join(exc_value.messages)}
1783                return HttpResponse(
1784                    json.dumps(response),
1785                    status=400,
1786                    content_type="application/json")
1787
1788    def maybe_handle(self):
1789        """Handles table actions if needed.
1790
1791        It determines whether the request should be handled by any action on
1792        this table after data has been loaded.
1793        """
1794        request = self.request
1795        table_name, action_name, obj_id = self.check_handler(request)
1796        if table_name == self.name and action_name:
1797            action_names = [action.name for action in
1798                            self.base_actions.values() if not action.preempt]
1799            # do not run preemptive actions here
1800            if action_name in action_names:
1801                return self.take_action(action_name, obj_id)
1802        return None
1803
1804    def sanitize_id(self, obj_id):
1805        """Override to modify an incoming obj_id to match existing API.
1806
1807        It is used to modify an incoming obj_id (used in Horizon)
1808        to the data type or format expected by the API.
1809        """
1810        return obj_id
1811
1812    def get_object_id(self, datum):
1813        """Returns the identifier for the object this row will represent.
1814
1815        By default this returns an ``id`` attribute on the given object,
1816        but this can be overridden to return other values.
1817
1818        .. warning::
1819
1820            Make sure that the value returned is a unique value for the id
1821            otherwise rendering issues can occur.
1822        """
1823        return datum.id
1824
1825    def get_object_display_key(self, datum):
1826        return 'name'
1827
1828    def get_object_display(self, datum):
1829        """Returns a display name that identifies this object.
1830
1831        By default, this returns a ``name`` attribute from the given object,
1832        but this can be overridden to return other values.
1833        """
1834        display_key = self.get_object_display_key(datum)
1835        return getattr(datum, display_key, None)
1836
1837    def has_prev_data(self):
1838        """Returns a boolean value indicating whether there is previous data.
1839
1840        Returns True if there is previous data available to this table
1841        from the source (generally an API).
1842
1843        The method is largely meant for internal use, but if you want to
1844        override it to provide custom behavior you can do so at your own risk.
1845        """
1846        return self._meta.has_prev_data
1847
1848    def has_more_data(self):
1849        """Returns a boolean value indicating whether there is more data.
1850
1851        Returns True if there is more data available to this table
1852        from the source (generally an API).
1853
1854        The method is largely meant for internal use, but if you want to
1855        override it to provide custom behavior you can do so at your own risk.
1856        """
1857        return self._meta.has_more_data
1858
1859    def get_prev_marker(self):
1860        """Returns the identifier for the first object in the current data set.
1861
1862        The return value will be used as marker/limit-based paging in the API.
1863        """
1864        return http.urlquote_plus(self.get_object_id(self.data[0])) \
1865            if self.data else ''
1866
1867    def get_marker(self):
1868        """Returns the identifier for the last object in the current data set.
1869
1870        The return value will be used as marker/limit-based paging in the API.
1871        """
1872        return http.urlquote_plus(self.get_object_id(self.data[-1])) \
1873            if self.data else ''
1874
1875    def get_prev_pagination_string(self):
1876        """Returns the query parameter string to paginate to the prev page."""
1877        return "=".join([self._meta.prev_pagination_param,
1878                         self.get_prev_marker()])
1879
1880    def get_pagination_string(self):
1881        """Returns the query parameter string to paginate to the next page."""
1882        return "=".join([self._meta.pagination_param, self.get_marker()])
1883
1884    def calculate_row_status(self, statuses):
1885        """Returns a boolean value determining the overall row status.
1886
1887        It is detremined based on the dictionary of column name
1888        to status mappings passed in.
1889
1890        By default, it uses the following logic:
1891
1892        #. If any statuses are ``False``, return ``False``.
1893        #. If no statuses are ``False`` but any or ``None``, return ``None``.
1894        #. If all statuses are ``True``, return ``True``.
1895
1896        This provides the greatest protection against false positives without
1897        weighting any particular columns.
1898
1899        The ``statuses`` parameter is passed in as a dictionary mapping
1900        column names to their statuses in order to allow this function to
1901        be overridden in such a way as to weight one column's status over
1902        another should that behavior be desired.
1903        """
1904        values = statuses.values()
1905        if any([status is False for status in values]):
1906            return False
1907        if any([status is None for status in values]):
1908            return None
1909        return True
1910
1911    def get_row_status_class(self, status):
1912        """Returns a css class name determined by the status value.
1913
1914        This class name is used to indicate the status of the rows in the table
1915        if any ``status_columns`` have been specified.
1916        """
1917        if status is True:
1918            return "status_up"
1919        if status is False:
1920            return "status_down"
1921        return "warning"
1922
1923    def get_columns(self):
1924        """Returns this table's columns including auto-generated ones."""
1925        return self.columns.values()
1926
1927    def get_rows(self):
1928        """Return the row data for this table broken out by columns."""
1929        rows = []
1930        try:
1931            for datum in self.filtered_data:
1932                row = self._meta.row_class(self, datum)
1933                if self.get_object_id(datum) == self.current_item_id:
1934                    self.selected = True
1935                    row.classes.append('current_selected')
1936                rows.append(row)
1937        except Exception as e:
1938            # Exceptions can be swallowed at the template level here,
1939            # re-raising as a TemplateSyntaxError makes them visible.
1940            LOG.exception("Error while rendering table rows.")
1941            raise template.TemplateSyntaxError from e
1942
1943        return rows
1944
1945    def css_classes(self):
1946        """Returns the additional CSS class to be added to <table> tag."""
1947        return self._meta.css_classes
1948