django_tables2.tables — django-tables2 2.0.0a3 documentation

# Source code for django_tables2.tables

    # coding: utf-8
    from __future__ import unicode_literals

    import copy
    from collections import OrderedDict
    from itertools import count

    from django.conf import settings
    from django.core.paginator import Paginator
    from django.db import models
    from django.template.loader import get_template
    from django.utils import six
    from django.utils.encoding import force_text

    from . import columns
    from .config import RequestConfig
    from .data import TableData
    from .rows import BoundRows
    from .utils import Accessor, AttributeDict, OrderBy, OrderByTuple, Sequence

    class DeclarativeColumnsMetaclass(type):
        '''
        Metaclass that converts `.Column` objects defined on a class to the
        dictionary `.Table.base_columns`, taking into account parent class
        `base_columns` as well.
        '''
        def __new__(mcs, name, bases, attrs):
            attrs['_meta'] = opts = TableOptions(attrs.get('Meta', None), name)

            # extract declared columns
            cols, remainder = [], {}
            for attr_name, attr in attrs.items():
                if isinstance(attr, columns.Column):
                    attr._explicit = True
                    cols.append((attr_name, attr))
                else:
                    remainder[attr_name] = attr
            attrs = remainder

            cols.sort(key=lambda x: x[1].creation_counter)

            # If this class is subclassing other tables, add their fields as
            # well. Note that we loop over the bases in *reverse* - this is
            # necessary to preserve the correct order of columns.
            parent_columns = []
            for base in reversed(bases):
                if hasattr(base, 'base_columns'):
                    parent_columns = list(base.base_columns.items()) + parent_columns

            # Start with the parent columns
            base_columns = OrderedDict(parent_columns)

            # Possibly add some generated columns based on a model
            if opts.model:
                extra = OrderedDict()
                # honor Table.Meta.fields, fallback to model._meta.fields
                if opts.fields is not None:
                    # Each item in opts.fields is the name of a model field or a
                    # normal attribute on the model
                    for field_name in opts.fields:
                        field = Accessor(field_name).get_field(opts.model)
                        extra[field_name] = columns.library.column_for_field(field)
                else:
                    for field in opts.model._meta.fields:
                        extra[field.name] = columns.library.column_for_field(field)

                # update base_columns with extra columns
                for key, col in extra.items():
                    # skip current col because the parent was explicitly defined,
                    # and the current column is not.
                    if key in base_columns and base_columns[key]._explicit is True:
                        continue
                    base_columns[key] = col

            # Explicit columns override both parent and generated columns
            base_columns.update(OrderedDict(cols))

            # Apply any explicit exclude setting
            for exclusion in opts.exclude:
                if exclusion in base_columns:
                    base_columns.pop(exclusion)

            # Remove any columns from our remainder, else columns from our parent class will remain
            for attr_name in remainder:
                if attr_name in base_columns:
                    base_columns.pop(attr_name)

            # Set localize on columns
            for col_name in base_columns.keys():
                localize_column = None
                if col_name in opts.localize:
                    localize_column = True
                # unlocalize gets higher precedence
                if col_name in opts.unlocalize:
                    localize_column = False

                if localize_column is not None:
                    base_columns[col_name].localize = localize_column
            attrs['base_columns'] = base_columns
            return super(DeclarativeColumnsMetaclass, mcs).__new__(mcs, name, bases, attrs)

    class TableOptions(object):
        '''
        Extracts and exposes options for a `.Table` from a `.Table.Meta`
        when the table is defined. See `.Table` for documentation on the impact of
        variables in this class.

        Arguments:
            options (`.Table.Meta`): options for a table from `.Table.Meta`
        '''
        def __init__(self, options, class_name):
            super(TableOptions, self).__init__()
            self._check_types(options, class_name)

            DJANGO_TABLES2_TEMPLATE = getattr(settings, 'DJANGO_TABLES2_TEMPLATE', 'django_tables2/table.html')
            DJANGO_TABLES2_TABLE_ATTRS = getattr(settings, 'DJANGO_TABLES2_TABLE_ATTRS', {})

            self.attrs = AttributeDict(getattr(options, 'attrs', DJANGO_TABLES2_TABLE_ATTRS))
            self.row_attrs = getattr(options, 'row_attrs', {})
            self.pinned_row_attrs = getattr(options, 'pinned_row_attrs', {})
            self.default = getattr(options, 'default', '—')
            self.empty_text = getattr(options, 'empty_text', None)
            self.fields = getattr(options, 'fields', None)
            self.exclude = getattr(options, 'exclude', ())
            order_by = getattr(options, 'order_by', None)
            if isinstance(order_by, six.string_types):
                order_by = (order_by, )
            self.order_by = OrderByTuple(order_by) if order_by is not None else None
            self.order_by_field = getattr(options, 'order_by_field', 'sort')
            self.page_field = getattr(options, 'page_field', 'page')
            self.per_page = getattr(options, 'per_page', 25)
            self.per_page_field = getattr(options, 'per_page_field', 'per_page')
            self.prefix = getattr(options, 'prefix', '')
            self.show_header = getattr(options, 'show_header', True)
            self.show_footer = getattr(options, 'show_footer', True)
            self.sequence = getattr(options, 'sequence', ())
            self.orderable = getattr(options, 'orderable', True)
            self.model = getattr(options, 'model', None)
            self.template_name = getattr(options, 'template_name', DJANGO_TABLES2_TEMPLATE)
            self.localize = getattr(options, 'localize', ())
            self.unlocalize = getattr(options, 'unlocalize', ())

        def _check_types(self, options, class_name):
            """
            Check class Meta attributes to prevent common mistakes.
            """
            if options is None:
                return

            checks = {
                (bool, ): ['show_header', 'show_footer', 'orderable'],
                (int, ): ['per_page'],
                (tuple, list, set): ['fields', 'sequence', 'exclude', 'localize', 'unlocalize'],
                six.string_types: [
                    'template_name', 'prefix', 'order_by_field', 'page_field', 'per_page_field'
                ],
                (dict, ): ['attrs', 'row_attrs', 'pinned_row_attrs'],
                (tuple, ) + six.string_types: ['order_by'],
                (type(models.Model), ): ['model', ]
            }

            for types, keys in checks.items():
                for key in keys:
                    value = getattr(options, key, None)
                    if value is not None and not isinstance(value, types):
                        expression = '{}.{} = {}'.format(class_name, key, value.__repr__())

                        raise TypeError(
                            '{} (type {}), but type must be one of ({})'.format(
                                expression,
                                type(value).__name__,
                                ', '.join([t.__name__ for t in types])
                            )
                        )

    class TableBase(object):
        '''
        A representation of a table.

        Arguments:
            data (QuerySet, list of dicts): The data to display.
                This is a required variable, a `TypeError` will be raised if it's not passed.

            order_by: (tuple or str): The default ordering tuple or comma separated str.
                A hyphen `-` can be used to prefix a column name to indicate
                *descending* order, for example: `('name', '-age')` or `name,-age`.

            orderable (bool): Enable/disable column ordering on this table

            empty_text (str): Empty text to render when the table has no data.
                (default `.Table.Meta.empty_text`)

            exclude (iterable or str): The names of columns that should not be
                included in the table.

            attrs (dict): HTML attributes to add to the ``<table>`` tag.
                When accessing the attribute, the value is always returned as an
                `.AttributeDict` to allow easily conversion to HTML.

            row_attrs (dict): Add custom html attributes to the table rows.
                Allows custom HTML attributes to be specified which will be added
                to the ``<tr>`` tag of the rendered table.

            pinned_row_attrs (dict): Same as row_attrs but for pinned rows.

            sequence (iterable): The sequence/order of columns the columns (from
                left to right).

                Items in the sequence must be :term:`column names <column name>`, or
                `'...'` (string containing three periods). `'...'` can be used as a
                catch-all for columns that are not specified.

            prefix (str): A prefix for query string fields.
                To avoid name-clashes when  using multiple tables on single page.

            order_by_field (str): If not `None`, defines the name of the *order by*
                query string field in the URL.

            page_field (str): If not `None`, defines the name of the *current page*
                query string field.

            per_page_field (str): If not `None`, defines the name of the *per page*
                query string field.

            template_name (str): The template to render when using ``{% render_table %}``
                (defaults to DJANGO_TABLES2_TEMPLATE, which is ``'django_tables2/table.html'``
                by default).

            default (str): Text to render in empty cells (determined by
                `.Column.empty_values`, default `.Table.Meta.default`)

            request: Django's request to avoid using `RequestConfig`

            show_header (bool): If `False`, the table will not have a header
                (`<thead>`), defaults to `True`

            show_footer (bool): If `False`, the table footer will not be rendered,
                even if some columns have a footer, defaults to `True`.

            extra_columns (str, `.Column`): list of `(name, column)`-tuples containing
                extra columns to add to the instance. If `column` is `None`, the column
                with `name` will be removed from the table.
        '''
        def __init__(self, data=None, order_by=None, orderable=None, empty_text=None,
                     exclude=None, attrs=None, row_attrs=None, pinned_row_attrs=None,
                     sequence=None, prefix=None, order_by_field=None, page_field=None,
                     per_page_field=None, template_name=None, default=None, request=None,
                     show_header=None, show_footer=True, extra_columns=None):
            super(TableBase, self).__init__()

            # note that although data is a keyword argument, it used to be positional
            # so it is assumed to be the first argument to this method.
            if data is None:
                raise TypeError('Argument data to {} is required'.format(type(self).__name__))

            self.exclude = exclude or self._meta.exclude
            self.sequence = sequence
            self.data = TableData.from_data(data=data)
            self.data.set_table(self)

            if default is None:
                default = self._meta.default
            self.default = default

            # Pinned rows #406
            self.pinned_row_attrs = AttributeDict(pinned_row_attrs or self._meta.pinned_row_attrs)
            self.pinned_data = {
                'top': self.get_top_pinned_data(),
                'bottom': self.get_bottom_pinned_data()
            }

            self.rows = BoundRows(data=self.data, table=self, pinned_data=self.pinned_data)
            self.attrs = AttributeDict(attrs if attrs is not None else self._meta.attrs)

            self.row_attrs = AttributeDict(row_attrs or self._meta.row_attrs)
            self.empty_text = empty_text if empty_text is not None else self._meta.empty_text
            self.orderable = orderable
            self.prefix = prefix
            self.order_by_field = order_by_field
            self.page_field = page_field
            self.per_page_field = per_page_field
            self.show_header = show_header
            self.show_footer = show_footer

            # Make a copy so that modifying this will not touch the class
            # definition. Note that this is different from forms, where the
            # copy is made available in a ``fields`` attribute.
            base_columns = copy.deepcopy(type(self).base_columns)

            if extra_columns is not None:
                for name, column in extra_columns:
                    if column is None and name in base_columns:
                        del base_columns[name]
                    else:
                        base_columns[name] = column

            # Keep fully expanded ``sequence`` at _sequence so it's easily accessible
            # during render. The priority is as follows:
            # 1. sequence passed in as an argument
            # 2. sequence declared in ``Meta``
            # 3. sequence defaults to '...'
            if sequence is not None:
                sequence = sequence
            elif self._meta.sequence:
                sequence = self._meta.sequence
            else:
                if self._meta.fields is not None:
                    sequence = tuple(self._meta.fields) + ('...', )
                else:
                    sequence = ('...', )

            sequence = Sequence(sequence)
            self._sequence = sequence.expand(base_columns.keys())

            # reorder columns based on sequence.
            base_columns = OrderedDict((
                (x, base_columns[x]) for x in sequence if x in base_columns
            ))
            self.columns = columns.BoundColumns(self, base_columns)
            # `None` value for order_by means no order is specified. This means we
            # `shouldn't touch our data's ordering in any way. *However*
            # `table.order_by = None` means "remove any ordering from the data"
            # (it's equivalent to `table.order_by = ()`).
            if order_by is None and self._meta.order_by is not None:
                order_by = self._meta.order_by
            if order_by is None:
                self._order_by = None
                # If possible inspect the ordering on the data we were given and
                # update the table to reflect that.
                order_by = self.data.ordering
                if order_by is not None:
                    self.order_by = order_by
            else:
                self.order_by = order_by
            self.template_name = template_name
            # If a request is passed, configure for request
            if request:
                RequestConfig(request).configure(self)

            self._counter = count()

        def get_top_pinned_data(self):
            '''
            Return data for top pinned rows containing data for each row.
            Iterable type like: QuerySet, list of dicts, list of objects.
            Having a non-zero number of pinned rows
            will not result in an empty result set message being rendered,
            even if there are no regular data rows

            Returns:
                `None` (default) no pinned rows at the top, iterable, data for pinned rows at the top.

            Note:
                To show pinned row this method should be overridden.

            Example:
                >>> class TableWithTopPinnedRows(Table):
                ...     def get_top_pinned_data(self):
                ...         return [{
                ...             'column_a' : 'some value',
                ...             'column_c' : 'other value',
                ...         }]
            '''
            return None

        def get_bottom_pinned_data(self):
            '''
            Return data for bottom pinned rows containing data for each row.
            Iterable type like: QuerySet, list of dicts, list of objects.
            Having a non-zero number of pinned rows
            will not result in an empty result set message being rendered,
            even if there are no regular data rows

            Returns:
                `None` (default) no pinned rows at the bottom, iterable, data for pinned rows at the bottom.

            Note:
                To show pinned row this method should be overridden.

            Example:
                >>> class TableWithBottomPinnedRows(Table):
                ...     def get_bottom_pinned_data(self):
                ...         return [{
                ...             'column_a' : 'some value',
                ...             'column_c' : 'other value',
                ...         }]
            '''
            return None

        def before_render(self, request):
            '''
            A way to hook into the moment just before rendering the template.

            Can be used to hide a column.

            Arguments:
                request: contains the `WGSIRequest` instance, containing a `user` attribute if
                    `.django.contrib.auth.middleware.AuthenticationMiddleware` is added to
                    your `MIDDLEWARE_CLASSES`.

            Example::

                class Table(tables.Table):
                    name = tables.Column(orderable=False)
                    country = tables.Column(orderable=False)

                    def before_render(self, request):
                        if request.user.has_perm('foo.delete_bar'):
                            self.columns.hide('country')
                        else:
                            self.columns.show('country')
            '''
            return

        def as_html(self, request):
            '''
            Render the table to an HTML table, adding `request` to the context.
            '''
            # reset counter for new rendering
            self._counter = count()
            template = get_template(self.template_name)

            context = {
                'table': self,
                'request': request
            }

            self.before_render(request)
            return template.render(context)

        def as_values(self, exclude_columns=None):
            '''
            Return a row iterator of the data which would be shown in the table where
            the first row is the table headers.

            arguments:
                exclude_columns (iterable): columns to exclude in the data iterator.

            This can be used to output the table data as CSV, excel, for example using the
            `~.export.ExportMixin`.

            If a column is defined using a :ref:`table.render_FOO`, the returned value from
            that method is used. If you want to differentiate between the rendered cell
            and a value, use a `value_Foo`-method::

                class Table(tables.Table):
                    name = tables.Column()

                    def render_name(self, value):
                        return format_html('<span class="name">{}</span>', value)

                    def value_name(self, value):
                        return value

            will have a value wrapped in `<span>` in the rendered HTML, and just returns
            the value when `as_values()` is called.
            '''
            if exclude_columns is None:
                exclude_columns = ()

            def excluded(column):
                if column.column.exclude_from_export:
                    return True
                return column.name in exclude_columns

            yield [
                force_text(column.header, strings_only=True)
                for column in self.columns if not excluded(column)
            ]
            for row in self.rows:
                yield [
                    force_text(row.get_cell_value(column.name), strings_only=True)
                    for column in row.table.columns if not excluded(column)
                ]

        def has_footer(self):
            '''
            Returns True if any of the columns define a ``_footer`` attribute or a
            ``render_footer()`` method
            '''
            return self.show_footer and any(column.has_footer() for column in self.columns)

        @property
        def show_header(self):
            return (self._show_header if self._show_header is not None
                    else self._meta.show_header)

        @show_header.setter
        def show_header(self, value):
            self._show_header = value

        @property
        def order_by(self):
            return self._order_by

        @order_by.setter
        def order_by(self, value):
            '''
            Order the rows of the table based on columns.

            Arguments:
                value: iterable or comma separated string of order by aliases.
            '''
            # collapse empty values to ()
            order_by = () if not value else value
            # accept string
            order_by = order_by.split(',') if isinstance(order_by, six.string_types) else order_by
            valid = []

            # everything's been converted to a iterable, accept iterable!
            for alias in order_by:
                name = OrderBy(alias).bare
                if name in self.columns and self.columns[name].orderable:
                    valid.append(alias)
            self._order_by = OrderByTuple(valid)
            self.data.order_by(self._order_by)

        @property
        def order_by_field(self):
            return (self._order_by_field if self._order_by_field is not None
                    else self._meta.order_by_field)

        @order_by_field.setter
        def order_by_field(self, value):
            self._order_by_field = value

        @property
        def page_field(self):
            return (self._page_field if self._page_field is not None
                    else self._meta.page_field)

        @page_field.setter
        def page_field(self, value):
            self._page_field = value

        def paginate(self, klass=Paginator, per_page=None, page=1, *args, **kwargs):
            '''
            Paginates the table using a paginator and creates a ``page`` property
            containing information for the current page.

            Arguments:
                klass (`~django.core.paginator.Paginator`): A paginator class to
                    paginate the results.

                per_page (int): Number of records to display on each page.
                page (int): Page to display.

            Extra arguments are passed to the paginator.

            Pagination exceptions (`~django.core.paginator.EmptyPage` and
            `~django.core.paginator.PageNotAnInteger`) may be raised from this
            method and should be handled by the caller.
            '''

            per_page = per_page or self._meta.per_page
            self.paginator = klass(self.rows, per_page, *args, **kwargs)
            self.page = self.paginator.page(page)

            return self

        @property
        def per_page_field(self):
            return (self._per_page_field if self._per_page_field is not None
                    else self._meta.per_page_field)

        @per_page_field.setter
        def per_page_field(self, value):
            self._per_page_field = value

        @property
        def prefix(self):
            return (self._prefix if self._prefix is not None
                    else self._meta.prefix)

        @prefix.setter
        def prefix(self, value):
            self._prefix = value

        @property
        def prefixed_order_by_field(self):
            return '%s%s' % (self.prefix, self.order_by_field)

        @property
        def prefixed_page_field(self):
            return '%s%s' % (self.prefix, self.page_field)

        @property
        def prefixed_per_page_field(self):
            return '%s%s' % (self.prefix, self.per_page_field)

        @property
        def sequence(self):
            return self._sequence

        @sequence.setter
        def sequence(self, value):
            if value:
                value = Sequence(value)
                value.expand(self.base_columns.keys())
            self._sequence = value

        @property
        def orderable(self):
            if self._orderable is not None:
                return self._orderable
            else:
                return self._meta.orderable

        @orderable.setter
        def orderable(self, value):
            self._orderable = value

        @property
        def template_name(self):
            if self._template is not None:
                return self._template
            else:
                return self._meta.template_name

        @template_name.setter
        def template_name(self, value):
            self._template = value

        @property
        def paginated_rows(self):
            '''
            Return the rows for the current page if the table is paginated, else all rows.
            '''
            if hasattr(self, 'page'):
                return self.page.object_list
            return self.rows

        def get_column_class_names(self, classes_set, bound_column):
            '''
            Returns a set of HTML class names for cells (both ``td`` and ``th``) of a
            **bound column** in this table.
            By default this returns the column class names defined in the table's
            attributes.
            This method can be overridden to change the default behavior, for
            example to simply `return classes_set`.

            Arguments:
                classes_set(set of string): a set of class names to be added
                  to the cell, retrieved from the column's attributes. In the case
                  of a header cell (th), this also includes ordering classes.
                  To set the classes for a column, see `.Column`.
                  To configure ordering classes, see :ref:`ordering-class-name`

                bound_column(`.BoundColumn`): the bound column the class names are
                  determined for. Useful for accessing `bound_column.name`.

            Returns:
                A set of class names to be added to cells of this column

            If you want to add the column names to the list of classes for a column,
            override this method in your custom table::

                class MyTable(tables.Table):
                    ...

                    def get_column_class_names(self, classes_set, bound_column):
                        classes_set = super(MyTable, self).get_column_class_names(classes_set, bound_column)
                        classes_set.add(bound_column.name)

                        return classes_set
            '''
            return classes_set

    # Python 2/3 compatible way to enable the metaclass
    @six.add_metaclass(DeclarativeColumnsMetaclass)
    class Table(TableBase):
        # ensure the Table class has the right class docstring
        __doc__ = TableBase.__doc__

    # Table = DeclarativeColumnsMetaclass(str('Table'), (TableBase, ), {})

    def table_factory(model, table=Table, fields=None, exclude=None,
                      localize=None):
        """
        Returns Table class for given `model`, equivalent to defining a custom table class::

            class MyTable(tables.Table):
                class Meta:
                    model = model

        Arguments:
            model (`~django.db.models.Model`): Model associated with the new table
            table (`.Table`): Base Table class used to create the new one
            fields (list of str): Fields displayed in tables
            exclude (list of str): Fields exclude in tables
            localize (list of str): Fields to localize
        """
        attrs = {'model': model}
        if fields is not None:
            attrs['fields'] = fields
        if exclude is not None:
            attrs['exclude'] = exclude
        if localize is not None:
            attrs['localize'] = localize
        # If parent form class already has an inner Meta, the Meta we're
        # creating needs to inherit from the parent's inner meta.
        parent = (object,)
        if hasattr(table, 'Meta'):
            parent = (table.Meta, object)
        Meta = type(str('Meta'), parent, attrs)
        # Give this new table class a reasonable name.
        class_name = model.__name__ + str('Table')
        # Class attributes for the new table class.
        table_class_attrs = {
            'Meta': Meta,
        }
        return type(table)(class_name, (table,), table_class_attrs)