1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
|
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 .columns import BoundColumns, Column, library
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
columns, remainder = [], {}
for attr_name, attr in attrs.items():
if isinstance(attr, Column):
attr._explicit = True
columns.append((attr_name, attr))
else:
remainder[attr_name] = attr
attrs = remainder
columns.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] = 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] = 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(columns))
# 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 column_name in base_columns.keys():
localize_column = None
if column_name in opts.localize:
localize_column = True
# unlocalize gets higher precedence
if column_name in opts.unlocalize:
localize_column = False
if localize_column is not None:
base_columns[column_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 = f"{class_name}.{key} = {value.__repr__()}"
allowed = ", ".join([t.__name__ for t in types])
raise TypeError(
f"{expression} (type {type(value).__name__}), but type must be one of ({allowed})"
)
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(f"Argument data to {type(self).__name__} is required")
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 = 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 f"{self.prefix}{self.order_by_field}"
@property
def prefixed_page_field(self):
return f"{self.prefix}{self.page_field}"
@property
def prefixed_per_page_field(self):
return f"{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)
|