# 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.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))
# 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=None):
super(TableOptions, self).__init__()
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.sequence = 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', ())
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 shouldn't 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: 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: 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 aren't specified.
prefix (str): A prefix for querystring 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*
querystring field in the url.
page_field (str): If not `None`, defines the name of the *current page*
querystring field.
per_page_field (str): If not `None`, defines the name of the *per page*
querystring 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.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(sequence)
elif self._meta.sequence:
sequence = self._meta.sequence
else:
if self._meta.fields is not None:
sequence = Sequence(tuple(self._meta.fields) + ('...', ))
else:
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 resultset 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 resultset 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
[docs]@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)