kopia lustrzana https://github.com/wagtail/wagtail
521 wiersze
17 KiB
Python
521 wiersze
17 KiB
Python
"""Helper classes for formatting data as tables"""
|
|
|
|
from collections import OrderedDict
|
|
from collections.abc import Mapping
|
|
|
|
from django.contrib.admin.utils import quote
|
|
from django.forms import MediaDefiningClass
|
|
from django.template.loader import get_template
|
|
from django.urls import reverse
|
|
from django.utils.functional import cached_property
|
|
from django.utils.text import capfirst
|
|
from django.utils.translation import gettext, gettext_lazy
|
|
|
|
from wagtail.admin.ui.components import Component
|
|
from wagtail.coreutils import multigetattr
|
|
|
|
|
|
class BaseColumn(metaclass=MediaDefiningClass):
|
|
class Header:
|
|
# Helper object used for rendering column headers in templates -
|
|
# behaves as a component (i.e. it has a render_html method) but delegates rendering
|
|
# to Column.render_header_html
|
|
def __init__(self, column):
|
|
self.column = column
|
|
|
|
def render_html(self, parent_context):
|
|
return self.column.render_header_html(parent_context)
|
|
|
|
class Cell:
|
|
# Helper object used for rendering table cells in templates -
|
|
# behaves as a component (i.e. it has a render_html method) but delegates rendering
|
|
# to Column.render_cell_html
|
|
def __init__(self, column, instance):
|
|
self.column = column
|
|
self.instance = instance
|
|
|
|
def render_html(self, parent_context):
|
|
return self.column.render_cell_html(self.instance, parent_context)
|
|
|
|
header_template_name = "wagtailadmin/tables/column_header.html"
|
|
cell_template_name = None
|
|
|
|
def __init__(
|
|
self,
|
|
name,
|
|
label=None,
|
|
accessor=None,
|
|
classname=None,
|
|
sort_key=None,
|
|
width=None,
|
|
ascending_title_text=None,
|
|
descending_title_text=None,
|
|
):
|
|
self.name = name
|
|
self.accessor = accessor or name
|
|
if label is None:
|
|
self.label = capfirst(name.replace("_", " "))
|
|
else:
|
|
self.label = label
|
|
self.classname = classname
|
|
self.sort_key = sort_key
|
|
self.header = Column.Header(self)
|
|
self.width = width
|
|
self.ascending_title_text = ascending_title_text
|
|
self.descending_title_text = descending_title_text
|
|
|
|
def get_header_context_data(self, parent_context):
|
|
"""
|
|
Compiles the context dictionary to pass to the header template when rendering the column header
|
|
"""
|
|
table = parent_context["table"]
|
|
return {
|
|
"column": self,
|
|
"table": table,
|
|
"is_orderable": bool(self.sort_key),
|
|
"is_ascending": self.sort_key and table.ordering == self.sort_key,
|
|
"is_descending": self.sort_key and table.ordering == ("-" + self.sort_key),
|
|
"request": parent_context.get("request"),
|
|
"ascending_title_text": self.ascending_title_text
|
|
or table.get_ascending_title_text(self),
|
|
"descending_title_text": self.descending_title_text
|
|
or table.get_descending_title_text(self),
|
|
}
|
|
|
|
@cached_property
|
|
def header_template(self):
|
|
return get_template(self.header_template_name)
|
|
|
|
@cached_property
|
|
def cell_template(self):
|
|
if self.cell_template_name is None:
|
|
raise NotImplementedError(
|
|
"cell_template_name must be specified on %r" % self
|
|
)
|
|
return get_template(self.cell_template_name)
|
|
|
|
def render_header_html(self, parent_context):
|
|
"""
|
|
Renders the column's header
|
|
"""
|
|
context = self.get_header_context_data(parent_context)
|
|
return self.header_template.render(context)
|
|
|
|
def get_cell_context_data(self, instance, parent_context):
|
|
"""
|
|
Compiles the context dictionary to pass to the cell template when rendering a table cell for
|
|
the given instance
|
|
"""
|
|
return {
|
|
"instance": instance,
|
|
"column": self,
|
|
"row": parent_context["row"],
|
|
"table": parent_context["table"],
|
|
"request": parent_context.get("request"),
|
|
}
|
|
|
|
def render_cell_html(self, instance, parent_context):
|
|
"""
|
|
Renders a table cell containing data for the given instance
|
|
"""
|
|
context = self.get_cell_context_data(instance, parent_context)
|
|
return self.cell_template.render(context)
|
|
|
|
def get_cell(self, instance):
|
|
"""
|
|
Return an object encapsulating this column and an item of data, which we can use for
|
|
rendering a table cell into a template
|
|
"""
|
|
return Column.Cell(self, instance)
|
|
|
|
def __repr__(self):
|
|
return "<{}.{}: {}>".format(
|
|
self.__class__.__module__,
|
|
self.__class__.__qualname__,
|
|
self.name,
|
|
)
|
|
|
|
|
|
class Column(BaseColumn):
|
|
"""A column that displays a single field of data from the model"""
|
|
|
|
cell_template_name = "wagtailadmin/tables/cell.html"
|
|
|
|
def get_value(self, instance):
|
|
"""
|
|
Given an instance (i.e. any object containing data), extract the field of data to be
|
|
displayed in a cell of this column
|
|
"""
|
|
if callable(self.accessor):
|
|
return self.accessor(instance)
|
|
else:
|
|
try:
|
|
return multigetattr(instance, self.accessor)
|
|
except AttributeError:
|
|
return None
|
|
|
|
def get_cell_context_data(self, instance, parent_context):
|
|
context = super().get_cell_context_data(instance, parent_context)
|
|
context["value"] = self.get_value(instance)
|
|
return context
|
|
|
|
|
|
class ButtonsColumnMixin:
|
|
"""A mixin for columns that contain buttons"""
|
|
|
|
buttons = []
|
|
|
|
def get_cell_context_data(self, instance, parent_context):
|
|
context = super().get_cell_context_data(instance, parent_context)
|
|
context["buttons"] = sorted(self.get_buttons(instance, parent_context))
|
|
return context
|
|
|
|
def get_buttons(self, instance, parent_context):
|
|
return self.buttons
|
|
|
|
|
|
class TitleColumn(Column):
|
|
"""A column where data is styled as a title and wrapped in a link or <label>"""
|
|
|
|
cell_template_name = "wagtailadmin/tables/title_cell.html"
|
|
|
|
def __init__(
|
|
self,
|
|
name,
|
|
url_name=None,
|
|
get_url=None,
|
|
get_title_id=None,
|
|
label_prefix=None,
|
|
get_label_id=None,
|
|
link_classname=None,
|
|
link_attrs=None,
|
|
id_accessor="pk",
|
|
**kwargs,
|
|
):
|
|
super().__init__(name, **kwargs)
|
|
self.url_name = url_name
|
|
self._get_url_func = get_url
|
|
self._get_title_id_func = get_title_id
|
|
self.label_prefix = label_prefix
|
|
self._get_label_id_func = get_label_id
|
|
self.link_attrs = link_attrs or {}
|
|
self.link_classname = link_classname
|
|
self.id_accessor = id_accessor
|
|
|
|
def get_cell_context_data(self, instance, parent_context):
|
|
context = super().get_cell_context_data(instance, parent_context)
|
|
context["link_attrs"] = self.get_link_attrs(instance, parent_context)
|
|
context["link_attrs"]["href"] = context["link_url"] = self.get_link_url(
|
|
instance, parent_context
|
|
)
|
|
if self.link_classname is not None:
|
|
context["link_attrs"]["class"] = self.link_classname
|
|
context["title_id"] = self.get_title_id(instance, parent_context)
|
|
context["label_id"] = self.get_label_id(instance, parent_context)
|
|
return context
|
|
|
|
def get_link_attrs(self, instance, parent_context):
|
|
return self.link_attrs.copy()
|
|
|
|
def get_link_url(self, instance, parent_context):
|
|
if self._get_url_func:
|
|
return self._get_url_func(instance)
|
|
elif self.url_name:
|
|
id = multigetattr(instance, self.id_accessor)
|
|
return reverse(self.url_name, args=(quote(id),))
|
|
|
|
def get_title_id(self, instance, parent_context):
|
|
if self._get_title_id_func:
|
|
return self._get_title_id_func(instance)
|
|
|
|
def get_label_id(self, instance, parent_context):
|
|
if self._get_label_id_func:
|
|
return self._get_label_id_func(instance)
|
|
elif self.label_prefix:
|
|
id = multigetattr(instance, self.id_accessor)
|
|
return f"{self.label_prefix}-{id}"
|
|
|
|
|
|
class StatusFlagColumn(Column):
|
|
"""Represents a boolean value as a status tag (or lack thereof, if the corresponding label is None)"""
|
|
|
|
cell_template_name = "wagtailadmin/tables/status_flag_cell.html"
|
|
|
|
def __init__(self, name, true_label=None, false_label=None, **kwargs):
|
|
super().__init__(name, **kwargs)
|
|
self.true_label = true_label
|
|
self.false_label = false_label
|
|
|
|
|
|
class StatusTagColumn(Column):
|
|
"""Represents a status tag"""
|
|
|
|
cell_template_name = "wagtailadmin/tables/status_tag_cell.html"
|
|
|
|
def __init__(self, name, primary=None, **kwargs):
|
|
super().__init__(name, **kwargs)
|
|
self.primary = primary
|
|
|
|
def get_primary(self, instance):
|
|
if callable(self.primary):
|
|
return self.primary(instance)
|
|
return self.primary
|
|
|
|
def get_cell_context_data(self, instance, parent_context):
|
|
context = super().get_cell_context_data(instance, parent_context)
|
|
context["primary"] = self.get_primary(instance)
|
|
return context
|
|
|
|
|
|
class BooleanColumn(Column):
|
|
"""Represents a True/False/None value as a tick/cross/question icon"""
|
|
|
|
cell_template_name = "wagtailadmin/tables/boolean_cell.html"
|
|
|
|
|
|
class LiveStatusTagColumn(StatusTagColumn):
|
|
"""Represents a live/draft status tag"""
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(
|
|
"status_string",
|
|
label=kwargs.pop("label", gettext("Status")),
|
|
sort_key=kwargs.pop("sort_key", "live"),
|
|
primary=lambda instance: instance.live,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
class DateColumn(Column):
|
|
"""Outputs a date in human-readable format"""
|
|
|
|
cell_template_name = "wagtailadmin/tables/date_cell.html"
|
|
|
|
|
|
class UpdatedAtColumn(DateColumn):
|
|
"""Outputs the _updated_at date annotation in human-readable format"""
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(
|
|
"_updated_at",
|
|
label=kwargs.pop("label", gettext("Updated")),
|
|
sort_key=kwargs.pop("sort_key", "_updated_at"),
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
class UserColumn(Column):
|
|
"""Outputs the username and avatar for a user"""
|
|
|
|
cell_template_name = "wagtailadmin/tables/user_cell.html"
|
|
|
|
def __init__(self, name, blank_display_name="", **kwargs):
|
|
super().__init__(name, **kwargs)
|
|
self.blank_display_name = blank_display_name
|
|
|
|
def get_cell_context_data(self, instance, parent_context):
|
|
context = super().get_cell_context_data(instance, parent_context)
|
|
|
|
user = context["value"]
|
|
if user:
|
|
try:
|
|
full_name = user.get_full_name().strip()
|
|
except AttributeError:
|
|
full_name = ""
|
|
context["display_name"] = full_name or user.get_username()
|
|
else:
|
|
context["display_name"] = self.blank_display_name
|
|
return context
|
|
|
|
|
|
class BulkActionsCheckboxColumn(BaseColumn):
|
|
"""
|
|
A checkbox column for the bulk actions feature.
|
|
|
|
When using this column, there should be another column (e.g. a TitleColumn)
|
|
that has an element with the id "{obj_type}_{instance.pk}_title" that contains
|
|
the title of the object (and nothing else) for screen reader purposes.
|
|
"""
|
|
|
|
header_template_name = "wagtailadmin/bulk_actions/select_all_checkbox_cell.html"
|
|
cell_template_name = "wagtailadmin/bulk_actions/listing_checkbox_cell.html"
|
|
|
|
def __init__(self, *args, obj_type, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.obj_type = obj_type
|
|
|
|
def get_aria_describedby(self, instance):
|
|
return f"{self.obj_type}_{quote(instance.pk)}_title"
|
|
|
|
def get_cell_context_data(self, instance, parent_context):
|
|
context = super().get_cell_context_data(instance, parent_context)
|
|
context.update(
|
|
{
|
|
"obj_type": self.obj_type,
|
|
"aria_describedby": self.get_aria_describedby(instance),
|
|
}
|
|
)
|
|
return context
|
|
|
|
|
|
class ReferencesColumn(Column):
|
|
cell_template_name = "wagtailadmin/tables/references_cell.html"
|
|
|
|
def __init__(
|
|
self,
|
|
name,
|
|
label=None,
|
|
accessor=None,
|
|
classname=None,
|
|
sort_key=None,
|
|
width=None,
|
|
get_url=None,
|
|
describe_on_delete=False,
|
|
):
|
|
super().__init__(name, label, accessor, classname, sort_key, width)
|
|
self._get_url_func = get_url
|
|
self.describe_on_delete = describe_on_delete
|
|
|
|
def get_edit_url(self, instance):
|
|
if self._get_url_func:
|
|
return self._get_url_func(instance)
|
|
|
|
def get_cell_context_data(self, instance, parent_context):
|
|
context = super().get_cell_context_data(instance, parent_context)
|
|
context["edit_url"] = self.get_edit_url(instance)
|
|
context["describe_on_delete"] = self.describe_on_delete
|
|
return context
|
|
|
|
|
|
class DownloadColumn(Column):
|
|
cell_template_name = "wagtailadmin/tables/download_cell.html"
|
|
|
|
def get_cell_context_data(self, instance, parent_context):
|
|
context = super().get_cell_context_data(instance, parent_context)
|
|
context["download_url"] = instance.url
|
|
return context
|
|
|
|
|
|
class RelatedObjectsColumn(Column):
|
|
"""Outputs a list of objects related to the object through a one-to-many relationship"""
|
|
|
|
cell_template_name = "wagtailadmin/tables/related_objects_cell.html"
|
|
|
|
def get_value(self, instance):
|
|
return getattr(instance, self.accessor).all()
|
|
|
|
|
|
class Table(Component):
|
|
template_name = "wagtailadmin/tables/table.html"
|
|
classname = "listing"
|
|
header_row_classname = ""
|
|
ascending_title_text_format = gettext_lazy(
|
|
"Sort by '%(label)s' in ascending order."
|
|
)
|
|
descending_title_text_format = gettext_lazy(
|
|
"Sort by '%(label)s' in descending order."
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
columns,
|
|
data,
|
|
template_name=None,
|
|
base_url=None,
|
|
ordering=None,
|
|
classname=None,
|
|
attrs=None,
|
|
caption=None,
|
|
):
|
|
self.columns = OrderedDict([(column.name, column) for column in columns])
|
|
self.caption = caption
|
|
self.data = data
|
|
if template_name:
|
|
self.template_name = template_name
|
|
self.base_url = base_url
|
|
self.ordering = ordering
|
|
if classname is not None:
|
|
self.classname = classname
|
|
self.base_attrs = attrs or {}
|
|
|
|
def get_caption(self):
|
|
return self.caption
|
|
|
|
def get_context_data(self, parent_context):
|
|
context = super().get_context_data(parent_context)
|
|
context["table"] = self
|
|
context["request"] = parent_context.get("request")
|
|
return context
|
|
|
|
@property
|
|
def media(self):
|
|
media = super().media
|
|
for col in self.columns.values():
|
|
media += col.media
|
|
return media
|
|
|
|
@property
|
|
def rows(self):
|
|
for index, instance in enumerate(self.data):
|
|
yield Table.Row(self, instance, index)
|
|
|
|
@property
|
|
def row_count(self):
|
|
return len(self.data)
|
|
|
|
@property
|
|
def attrs(self):
|
|
attrs = self.base_attrs.copy()
|
|
attrs["class"] = self.classname
|
|
return attrs
|
|
|
|
def get_row_classname(self, instance):
|
|
return ""
|
|
|
|
def get_row_attrs(self, instance):
|
|
attrs = {}
|
|
classname = self.get_row_classname(instance)
|
|
if classname:
|
|
attrs["class"] = classname
|
|
return attrs
|
|
|
|
def has_column_widths(self):
|
|
return any(column.width for column in self.columns.values())
|
|
|
|
def get_ascending_title_text(self, column):
|
|
if self.ascending_title_text_format:
|
|
return self.ascending_title_text_format % {"label": column.label}
|
|
|
|
def get_descending_title_text(self, column):
|
|
if self.descending_title_text_format:
|
|
return self.descending_title_text_format % {"label": column.label}
|
|
|
|
class Row(Mapping):
|
|
# behaves as an OrderedDict whose items are the rendered results of
|
|
# the corresponding column's format_cell method applied to the instance
|
|
def __init__(self, table, instance, index):
|
|
self.table = table
|
|
self.columns = table.columns
|
|
self.instance = instance
|
|
self.index = index
|
|
|
|
def __len__(self):
|
|
return len(self.columns)
|
|
|
|
def __getitem__(self, key):
|
|
return self.columns[key].get_cell(self.instance)
|
|
|
|
def __iter__(self):
|
|
yield from self.columns
|
|
|
|
def __repr__(self):
|
|
return repr([col.get_cell(self.instance) for col in self.columns.values()])
|
|
|
|
@cached_property
|
|
def classname(self):
|
|
return self.table.get_row_classname(self.instance)
|
|
|
|
@cached_property
|
|
def attrs(self):
|
|
return self.table.get_row_attrs(self.instance)
|