123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728 |
- 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.encoding import force_str
- 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:
- extra[field_name] = columns.library.column_for_field(
- field=Accessor(field_name).get_field(opts.model),
- accessor=field_name,
- linkify=opts.linkify.get(field_name),
- )
- else:
- for field in opts.model._meta.fields:
- extra[field.name] = columns.library.column_for_field(
- field, linkify=opts.linkify.get(field.name), accessor=field.name
- )
- # update base_columns with extra columns
- for key, column in extra.items():
- # skip current column 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] = column
- # 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().__new__(mcs, name, bases, attrs)
- class TableOptions:
- """
- 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().__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 = 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)
- linkify = getattr(options, "linkify", [])
- if not isinstance(linkify, dict):
- linkify = dict.fromkeys(linkify, True)
- self.linkify = linkify
- self.exclude = getattr(options, "exclude", ())
- order_by = getattr(options, "order_by", None)
- if isinstance(order_by, str):
- 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"],
- (tuple, list, set, dict): ["linkify"],
- str: ["template_name", "prefix", "order_by_field", "page_field", "per_page_field"],
- (dict,): ["attrs", "row_attrs", "pinned_row_attrs"],
- (tuple, list, str): ["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 Table(metaclass=DeclarativeColumnsMetaclass):
- """
- 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().__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)
- for tag in ["thead", "tbody", "tfoot"]:
- # Add these attrs even if they haven't been passed so we can safely refer to them in the templates
- self.attrs[tag] = AttributeDict(self.attrs.get(tag, {}))
- 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.
- Note that any invisible columns will be part of the row iterator.
- """
- if exclude_columns is None:
- exclude_columns = ()
- columns = [
- column
- for column in self.columns.iterall()
- if not (column.column.exclude_from_export or column.name in exclude_columns)
- ]
- yield [force_str(column.header, strings_only=True) for column in columns]
- for row in self.rows:
- yield [
- force_str(row.get_cell_value(column.name), strings_only=True) for column in columns
- ]
- 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, str) 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, paginator_class=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:
- paginator_class (`~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 = paginator_class(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().get_column_class_names(classes_set, bound_column)
- classes_set.add(bound_column.name)
- return classes_set
- """
- return classes_set
- def table_factory(model, table=Table, fields=None, exclude=None, localize=None):
- """
- Return 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 = (table.Meta, object) if hasattr(table, "Meta") else (object,)
- Meta = type("Meta", parent, attrs)
- # Give this new table class a reasonable name.
- class_name = model.__name__ + "AutogeneratedTable"
- # Class attributes for the new table class.
- table_class_attrs = {"Meta": Meta}
- return type(table)(class_name, (table,), table_class_attrs)
|