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,2 @@
# Import components from the Laces library which was extracted from Wagtail.
from laces.components import Component, MediaContainer # noqa: F401

View File

@@ -0,0 +1,47 @@
from django.conf import settings
from wagtail.admin.ui.components import Component
class EditingSessionsModule(Component):
template_name = "wagtailadmin/shared/editing_sessions/module.html"
def __init__(
self,
current_session,
ping_url,
release_url,
other_sessions,
revision_id=None,
):
self.current_session = current_session
self.ping_url = ping_url
self.release_url = release_url
self.sessions_list = EditingSessionsList(current_session, other_sessions)
self.revision_id = revision_id
def get_context_data(self, parent_context):
ping_interval = getattr(
settings,
"WAGTAIL_EDITING_SESSION_PING_INTERVAL",
10000,
)
return {
"current_session": self.current_session,
"ping_url": self.ping_url,
"release_url": self.release_url,
"ping_interval": str(ping_interval), # avoid the need to | unlocalize
"sessions_list": self.sessions_list,
"revision_id": self.revision_id,
}
class EditingSessionsList(Component):
template_name = "wagtailadmin/shared/editing_sessions/list.html"
def __init__(self, current_session, other_sessions):
self.current_session = current_session
self.sessions = other_sessions
def get_context_data(self, parent_context):
return {"current_session": self.current_session, "sessions": self.sessions}

View File

@@ -0,0 +1,40 @@
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from wagtail.admin.ui.components import Component
from wagtail.utils.registry import ModelFieldRegistry
display_class_registry = ModelFieldRegistry()
def register_display_class(field_class, to=None, display_class=None, exact_class=False):
"""
Define how model field values should be rendered in the admin.
The `display_class` should be a subclass of `wagtail.admin.ui.components.Component`
that takes a single argument in its constructor: the value of the field.
This is mainly useful for defining how fields are rendered in the inspect view,
but it can also be used in other places, e.g. listing views.
"""
if display_class is None:
raise ImproperlyConfigured(
"register_display_class must be passed a 'display_class' keyword argument"
)
if to and field_class != models.ForeignKey:
raise ImproperlyConfigured(
"The 'to' argument on register_display_class is only valid for ForeignKey fields"
)
display_class_registry.register(
field_class, to=to, value=display_class, exact_class=exact_class
)
class BaseFieldDisplay(Component):
def __init__(self, value):
self.value = value
def get_context_data(self, parent_context):
return {"value": self.value}

View File

@@ -0,0 +1,357 @@
from django.urls import reverse
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy, ngettext
from wagtail import hooks
from wagtail.admin.ui.components import Component
from wagtail.admin.userbar import AccessibilityItem
from wagtail.models import DraftStateMixin, LockableMixin, Page, ReferenceIndex
class BaseSidePanel(Component):
class SidePanelToggle(Component):
template_name = "wagtailadmin/shared/side_panel_toggle.html"
aria_label = ""
icon_name = ""
has_counter = True
counter_classname = ""
keyboard_shortcut = None
def __init__(self, panel):
self.panel = panel
def get_context_data(self, parent_context):
# Inherit classes from fragments defined in slim_header.html
inherit = {
"nav_icon_button_classes",
"nav_icon_classes",
"nav_icon_counter_classes",
}
context = {key: parent_context.get(key) for key in inherit}
context["toggle"] = self
context["panel"] = self.panel
context["count"] = 0
return context
def __init__(self, object, request):
self.object = object
self.request = request
self.model = type(self.object)
self.toggle = self.SidePanelToggle(panel=self)
def get_context_data(self, parent_context):
context = {"panel": self, "object": self.object, "request": self.request}
if issubclass(self.model, Page):
context["page"] = self.object
return context
class StatusSidePanel(BaseSidePanel):
class SidePanelToggle(BaseSidePanel.SidePanelToggle):
aria_label = gettext_lazy("Toggle status")
icon_name = "info-circle"
counter_classname = "w-bg-critical-200"
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
form = parent_context.get("form")
context["count"] = form and len(
form.errors.keys() & {"go_live_at", "expire_at"}
)
return context
name = "status"
title = gettext_lazy("Status")
template_name = "wagtailadmin/shared/side_panels/status.html"
order = 100
def __init__(
self,
*args,
show_schedule_publishing_toggle=None,
live_object=None,
scheduled_object=None,
locale=None,
translations=None,
usage_url=None,
history_url=None,
last_updated_info=None,
**kwargs,
):
super().__init__(*args, **kwargs)
self.show_schedule_publishing_toggle = show_schedule_publishing_toggle
self.live_object = live_object
self.scheduled_object = scheduled_object
self.locale = locale
self.translations = translations
self.usage_url = usage_url
self.history_url = history_url
self.last_updated_info = last_updated_info
self.locking_enabled = isinstance(self.object, LockableMixin)
def get_status_templates(self, context):
templates = ["wagtailadmin/shared/side_panels/includes/status/workflow.html"]
if self.locale:
templates.append(
"wagtailadmin/shared/side_panels/includes/status/locale.html"
)
if self.object.pk:
if self.locking_enabled:
templates.append(
"wagtailadmin/shared/side_panels/includes/status/locked.html"
)
if self.usage_url:
templates.append(
"wagtailadmin/shared/side_panels/includes/status/usage.html"
)
return templates
def get_scheduled_publishing_context(self, parent_context):
if not isinstance(self.object, DraftStateMixin):
return {"draftstate_enabled": False}
context = {
# Used for hiding the info completely if the model doesn't extend DraftStateMixin
"draftstate_enabled": True,
# Show error message if any of the scheduled publishing fields has errors
"schedule_has_errors": False,
# The dialog toggle can be hidden (e.g. if PublishingPanel is not present)
# but the scheduled publishing info should still be shown
"show_schedule_publishing_toggle": self.show_schedule_publishing_toggle,
# These are the dates that show up with the unticked calendar icon,
# aka "draft schedule"
"draft_go_live_at": None,
"draft_expire_at": None,
# These are the dates that show up with the ticked calendar icon,
# aka "active schedule"
"scheduled_go_live_at": None,
"scheduled_expire_at": None,
# This is for an edge case where the live object already has an
# expire_at, which can still take effect if the active schedule's
# go_live_at is later than that
"live_expire_at": None,
}
# Reuse logic from the toggle to get the count of errors
if self.toggle.get_context_data(parent_context)["count"]:
context["schedule_has_errors"] = True
# Only consider draft schedule if the object hasn't been created
# or if there are unpublished changes
if not self.object.pk or self.object.has_unpublished_changes:
context["draft_go_live_at"] = self.object.go_live_at
context["draft_expire_at"] = self.object.expire_at
# Get active schedule from the scheduled revision's object (if any)
if self.scheduled_object:
context["scheduled_go_live_at"] = self.scheduled_object.go_live_at
context["scheduled_expire_at"] = self.scheduled_object.expire_at
# Ignore draft schedule if it's the same as the active schedule
if context["draft_go_live_at"] == context["scheduled_go_live_at"]:
context["draft_go_live_at"] = None
if context["draft_expire_at"] == context["scheduled_expire_at"]:
context["draft_expire_at"] = None
# The live object can still have its own active expiry date
# that's separate from the active schedule
if (
self.live_object
and self.live_object.expire_at
and not self.live_object.expired
):
context["live_expire_at"] = self.live_object.expire_at
# Ignore the live object's expiry date if the active schedule has
# an earlier go_live_at, as the active schedule's expiry date will
# override the live object's expiry date when the draft is published
if (
context["scheduled_go_live_at"]
and context["scheduled_go_live_at"] < context["live_expire_at"]
):
context["live_expire_at"] = None
# Only show the box for the live object expire_at edge case
# if it passes the checks above
context["has_live_publishing_schedule"] = bool(context["live_expire_at"])
# Only show the main scheduled publishing box if it has at least one of
# the draft/active schedule dates after passing the checks above
context["has_draft_publishing_schedule"] = any(
(
context["scheduled_go_live_at"],
context["scheduled_expire_at"],
context["draft_go_live_at"],
context["draft_expire_at"],
)
)
return context
def get_lock_context(self, parent_context):
self.lock = None
lock_context = {}
if self.locking_enabled:
self.lock = self.object.get_lock()
if self.lock:
lock_context = self.lock.get_context_for_user(
self.request.user, parent_context
)
return {
"lock": self.lock,
"user_can_lock": parent_context.get("user_can_lock"),
"user_can_unlock": parent_context.get("user_can_unlock"),
"lock_context": lock_context,
}
def get_usage_context(self):
return {
"usage_count": ReferenceIndex.get_grouped_references_to(
self.object
).count(),
"usage_url": self.usage_url,
}
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
context["locale"] = self.locale
context["translations"] = self.translations
if self.translations:
context["translations_total"] = len(self.translations) + 1
context["model_name"] = capfirst(self.model._meta.verbose_name)
context["base_model_name"] = context["model_name"]
context["history_url"] = self.history_url
context["status_templates"] = self.get_status_templates(context)
context["last_updated_info"] = self.last_updated_info
context.update(self.get_scheduled_publishing_context(parent_context))
context.update(self.get_lock_context(parent_context))
if self.object.pk and self.usage_url:
context.update(self.get_usage_context())
return context
class PageStatusSidePanel(StatusSidePanel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.object.pk:
self.usage_url = reverse("wagtailadmin_pages:usage", args=(self.object.pk,))
permissions = self.object.permissions_for_user(self.request.user)
if permissions.can_view_revisions():
self.history_url = reverse(
"wagtailadmin_pages:history",
args=(self.object.pk,),
)
def get_status_templates(self, context):
templates = super().get_status_templates(context)
templates.insert(
-1, "wagtailadmin/shared/side_panels/includes/status/privacy.html"
)
return templates
def get_usage_context(self):
context = super().get_usage_context()
context["usage_url_text"] = ngettext(
"Referenced %(count)s time",
"Referenced %(count)s times",
context["usage_count"],
) % {"count": context["usage_count"]}
return context
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
page = self.object
if page.id:
context.update(
{
"workflow_history_url": reverse(
"wagtailadmin_pages:workflow_history", args=(page.id,)
),
"revisions_compare_url_name": "wagtailadmin_pages:revisions_compare",
"lock_url": reverse("wagtailadmin_pages:lock", args=(page.id,)),
"unlock_url": reverse("wagtailadmin_pages:unlock", args=(page.id,)),
}
)
context.update(
{
"model_name": self.model.get_verbose_name(),
"base_model_name": Page._meta.verbose_name,
"model_description": self.model.get_page_description(),
"status_templates": self.get_status_templates(context),
}
)
return context
class CommentsSidePanel(BaseSidePanel):
class SidePanelToggle(BaseSidePanel.SidePanelToggle):
aria_label = gettext_lazy("Toggle comments")
icon_name = "comment"
name = "comments"
title = gettext_lazy("Comments")
template_name = "wagtailadmin/shared/side_panels/comments.html"
order = 300
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
context["form"] = parent_context.get("form")
return context
class ChecksSidePanel(BaseSidePanel):
class SidePanelToggle(BaseSidePanel.SidePanelToggle):
aria_label = gettext_lazy("Toggle checks")
icon_name = "glasses"
name = "checks"
title = gettext_lazy("Checks")
template_name = "wagtailadmin/shared/side_panels/checks.html"
order = 350
def get_axe_configuration(self):
# Retrieve the Axe configuration from the userbar.
userbar_items = [AccessibilityItem()]
for fn in hooks.get_hooks("construct_wagtail_userbar"):
fn(self.request, userbar_items)
for item in userbar_items:
if isinstance(item, AccessibilityItem):
return item.get_axe_configuration(self.request)
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
context["axe_configuration"] = self.get_axe_configuration()
return context
class PreviewSidePanel(BaseSidePanel):
class SidePanelToggle(BaseSidePanel.SidePanelToggle):
aria_label = gettext_lazy("Toggle preview")
icon_name = "mobile-alt"
has_counter = False
keyboard_shortcut = "mod+p"
name = "preview"
title = gettext_lazy("Preview")
template_name = "wagtailadmin/shared/side_panels/preview.html"
order = 400
def __init__(self, object, request, *, preview_url):
super().__init__(object, request)
self.preview_url = preview_url
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
context["preview_url"] = self.preview_url
context["has_multiple_modes"] = len(self.object.preview_modes) > 1
return context

View File

@@ -0,0 +1,286 @@
from typing import Any, List, Mapping
from warnings import warn
from django import forms
from django.urls import reverse
from django.utils.functional import cached_property
from wagtail.admin.staticfiles import versioned_static
from wagtail.telepath import Adapter, adapter
from wagtail.utils.deprecation import RemovedInWagtail70Warning
class BaseSidebarAdapter(Adapter):
@cached_property
def media(self):
return forms.Media(
js=[
versioned_static("wagtailadmin/js/sidebar.js"),
]
)
# Main menu
class MenuItem:
def __init__(
self,
name: str,
label: str,
icon_name: str = "",
classname: str = "",
classnames: str = "",
attrs: Mapping[str, Any] = None,
):
if classnames:
warn(
"The `classnames` kwarg for sidebar MenuItem is deprecated - use `classname` instead.",
category=RemovedInWagtail70Warning,
)
self.name = name
self.label = label
self.icon_name = icon_name
self.classname = classname or classnames
self.attrs = attrs or {}
def js_args(self):
return [
{
"name": self.name,
"label": self.label,
"icon_name": self.icon_name,
"classname": self.classname,
"attrs": self.attrs,
}
]
@adapter("wagtail.sidebar.LinkMenuItem", base=BaseSidebarAdapter)
class LinkMenuItem(MenuItem):
def __init__(
self,
name: str,
label: str,
url: str,
icon_name: str = "",
classname: str = "",
classnames: str = "",
attrs: Mapping[str, Any] = None,
):
if classnames:
warn(
"The `classnames` kwarg for sidebar LinkMenuItem is deprecated - use `classname` instead.",
category=RemovedInWagtail70Warning,
)
super().__init__(
name,
label,
icon_name=icon_name,
classname=classname or classnames,
attrs=attrs,
)
self.url = url
def js_args(self):
args = super().js_args()
args[0]["url"] = self.url
return args
def __eq__(self, other):
return (
self.__class__ == other.__class__
and self.name == other.name
and self.label == other.label
and self.url == other.url
and self.icon_name == other.icon_name
and self.classname == other.classname
and self.attrs == other.attrs
)
@adapter("wagtail.sidebar.ActionMenuItem", base=BaseSidebarAdapter)
class ActionMenuItem(MenuItem):
def __init__(
self,
name: str,
label: str,
action: str,
icon_name: str = "",
classname: str = "",
classnames: str = "",
method: str = "POST",
attrs: Mapping[str, Any] = None,
):
if classnames:
warn(
"The `classnames` kwarg for sidebar ActionMenuItem is deprecated - use `classname` instead.",
category=RemovedInWagtail70Warning,
)
super().__init__(
name,
label,
icon_name=icon_name,
classname=classname or classnames,
attrs=attrs,
)
self.action = action
self.method = method
def js_args(self):
args = super().js_args()
args[0]["action"] = self.action
args[0]["method"] = self.method
return args
def __eq__(self, other):
return (
self.__class__ == other.__class__
and self.name == other.name
and self.label == other.label
and self.action == other.action
and self.method == other.method
and self.icon_name == other.icon_name
and self.classname == other.classname
and self.attrs == other.attrs
)
@adapter("wagtail.sidebar.SubMenuItem", base=BaseSidebarAdapter)
class SubMenuItem(MenuItem):
def __init__(
self,
name: str,
label: str,
menu_items: List[MenuItem],
icon_name: str = "",
classname: str = "",
classnames: str = "",
footer_text: str = "",
attrs: Mapping[str, Any] = None,
):
if classnames:
warn(
"The `classnames` kwarg for sidebar SubMenuItem is deprecated - use `classname` instead.",
category=RemovedInWagtail70Warning,
)
super().__init__(
name,
label,
icon_name=icon_name,
classname=classname or classnames,
attrs=attrs,
)
self.menu_items = menu_items
self.footer_text = footer_text
def js_args(self):
args = super().js_args()
args[0]["footer_text"] = self.footer_text
args.append(self.menu_items)
return args
def __eq__(self, other):
return (
self.__class__ == other.__class__
and self.name == other.name
and self.label == other.label
and self.menu_items == other.menu_items
and self.icon_name == other.icon_name
and self.classname == other.classname
and self.footer_text == other.footer_text
and self.attrs == other.attrs
)
@adapter("wagtail.sidebar.PageExplorerMenuItem", base=BaseSidebarAdapter)
class PageExplorerMenuItem(LinkMenuItem):
def __init__(
self,
name: str,
label: str,
url: str,
start_page_id: int,
icon_name: str = "",
classname: str = "",
classnames: str = "",
attrs: Mapping[str, Any] = None,
):
if classnames:
warn(
"The `classnames` kwarg for sidebar PageExplorerMenuItem is deprecated - use `classname` instead.",
category=RemovedInWagtail70Warning,
)
super().__init__(
name,
label,
url,
icon_name=icon_name,
classname=classname or classnames,
attrs=attrs,
)
self.start_page_id = start_page_id
def js_args(self):
args = super().js_args()
args.append(self.start_page_id)
return args
def __eq__(self, other):
return (
self.__class__ == other.__class__
and self.name == other.name
and self.label == other.label
and self.url == other.url
and self.start_page_id == other.start_page_id
and self.icon_name == other.icon_name
and self.classname == other.classname
and self.attrs == other.attrs
)
# Modules
@adapter("wagtail.sidebar.WagtailBrandingModule", base=BaseSidebarAdapter)
class WagtailBrandingModule:
def js_args(self):
return [
reverse("wagtailadmin_home"),
]
@adapter("wagtail.sidebar.SearchModule", base=BaseSidebarAdapter)
class SearchModule:
def __init__(self, search_area):
self.search_area = search_area
def js_args(self):
return [self.search_area.url]
@adapter("wagtail.sidebar.MainMenuModule", base=BaseSidebarAdapter)
class MainMenuModule:
def __init__(
self, menu_items: List[MenuItem], account_menu_items: List[MenuItem], user
):
self.menu_items = menu_items
self.account_menu_items = account_menu_items
self.user = user
def js_args(self):
from wagtail.admin.templatetags.wagtailadmin_tags import avatar_url
try:
first_name = self.user.first_name
except AttributeError:
first_name = None
return [
self.menu_items,
self.account_menu_items,
{
"name": first_name or self.user.get_username(),
"avatarUrl": avatar_url(self.user, size=50),
},
]

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