Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

View File

@@ -0,0 +1,520 @@
"""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)

View File

@@ -0,0 +1,174 @@
from django.utils.safestring import mark_safe
from django.utils.translation import gettext
from wagtail.admin.ui.tables import BaseColumn, BulkActionsCheckboxColumn, Column, Table
class PageTitleColumn(BaseColumn):
header_template_name = "wagtailadmin/pages/listing/_page_title_column_header.html"
cell_template_name = "wagtailadmin/pages/listing/_page_title_cell.html"
classname = "title"
def get_header_context_data(self, parent_context):
context = super().get_header_context_data(parent_context)
parent_page = parent_context.get("parent_page")
context["items_count"] = parent_context.get("items_count")
context["page_obj"] = parent_context.get("page_obj")
context["parent_page"] = parent_page
if parent_page and (
parent_context.get("is_searching") or parent_context.get("is_filtering")
):
# Results are switchable between searching the whole tree and searching just this parent.
# Add extra signposting to show which scope we're in, and provide a link to switch scope.
if parent_context.get("is_searching_whole_tree"):
context["result_scope"] = "whole_tree"
else:
context["result_scope"] = "parent"
else:
# No signposting needed
context["result_scope"] = None
# If results are not paginated e.g. when using the OrderingColumn,
# all items are displayed on the page
context["start_index"] = 1
context["end_index"] = context["items_count"]
if context["page_obj"]:
context["start_index"] = context["page_obj"].start_index()
context["end_index"] = context["page_obj"].end_index()
return context
def get_cell_context_data(self, instance, parent_context):
context = super().get_cell_context_data(instance, parent_context)
context["page_perms"] = instance.permissions_for_user(
parent_context["request"].user
)
context["parent_page"] = getattr(instance, "annotated_parent_page", None)
context["show_locale_labels"] = parent_context.get("show_locale_labels")
context["perms"] = parent_context.get("perms")
context["actions_next_url"] = parent_context.get("actions_next_url")
return context
class ParentPageColumn(Column):
cell_template_name = "wagtailadmin/pages/listing/_parent_page_cell.html"
def get_value(self, instance):
return instance.get_parent()
class PageStatusColumn(BaseColumn):
cell_template_name = "wagtailadmin/pages/listing/_page_status_cell.html"
class BulkActionsColumn(BulkActionsCheckboxColumn):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs, obj_type="page")
def get_header_context_data(self, parent_context):
context = super().get_header_context_data(parent_context)
parent_page = parent_context.get("parent_page")
if parent_page:
context["parent"] = parent_page.id
return context
class OrderingColumn(BaseColumn):
header_template_name = "wagtailadmin/pages/listing/_ordering_header.html"
cell_template_name = "wagtailadmin/pages/listing/_ordering_cell.html"
class NavigateToChildrenColumn(BaseColumn):
cell_template_name = "wagtailadmin/pages/listing/_navigation_explore.html"
def get_cell_context_data(self, instance, parent_context):
context = super().get_cell_context_data(instance, parent_context)
context["page"] = instance
context["page_perms"] = instance.permissions_for_user(
parent_context["request"].user
)
return context
def render_header_html(self, parent_context):
return mark_safe("<th></th>")
class PageTable(Table):
def __init__(
self,
*args,
use_row_ordering_attributes=False,
parent_page=None,
show_locale_labels=False,
actions_next_url=None,
**kwargs,
):
super().__init__(*args, **kwargs)
# If true, attributes will be added on the <tr> element to support reordering
self.use_row_ordering_attributes = use_row_ordering_attributes
# The parent page of the pages being listed - used to add extra context to the title text
# of the reordering links. Leave this undefined if the pages being listed do not share a
# common parent.
self.parent_page = parent_page
if self.parent_page:
# Use more detailed title text that mentions the parent page, if we have one and the
# current strings have not been overridden from Table's default
if self.ascending_title_text_format == Table.ascending_title_text_format:
self.ascending_title_text_format = gettext(
"Sort the order of child pages within '%(parent)s' by '%(label)s' in ascending order."
)
if self.descending_title_text_format == Table.descending_title_text_format:
self.descending_title_text_format = gettext(
"Sort the order of child pages within '%(parent)s' by '%(label)s' in descending order."
)
self.show_locale_labels = show_locale_labels
self.actions_next_url = actions_next_url
def get_ascending_title_text(self, column):
return self.ascending_title_text_format % {
"parent": self.parent_page and self.parent_page.get_admin_display_title(),
"label": column.label,
}
def get_descending_title_text(self, column):
return self.descending_title_text_format % {
"parent": self.parent_page and self.parent_page.get_admin_display_title(),
"label": column.label,
}
def get_row_classname(self, instance):
if not instance.live:
return "unpublished"
else:
return ""
def get_row_attrs(self, instance):
attrs = super().get_row_attrs(instance)
if self.use_row_ordering_attributes:
attrs["id"] = "page_%d" % instance.id
attrs["data-w-orderable-item-id"] = instance.id
attrs["data-w-orderable-item-label"] = instance.get_admin_display_title()
attrs["data-w-orderable-target"] = "item"
return attrs
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
context["show_locale_labels"] = self.show_locale_labels
context["perms"] = parent_context.get("perms")
context["items_count"] = parent_context.get("items_count")
context["page_obj"] = parent_context.get("page_obj")
context["parent_page"] = parent_context.get("parent_page")
context["is_searching"] = parent_context.get("is_searching")
context["is_filtering"] = parent_context.get("is_filtering")
context["is_searching_whole_tree"] = parent_context.get(
"is_searching_whole_tree"
)
context["actions_next_url"] = (
self.actions_next_url or parent_context.get("request").path
)
return context