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 @@
from .base import PageReportView, ReportView # noqa: F401

View File

@@ -0,0 +1,115 @@
import django_filters
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db.models import OuterRef, Subquery
from django.utils.translation import gettext_lazy as _
from wagtail.admin.filters import ContentTypeFilter, WagtailFilterSet
from wagtail.admin.widgets import AdminDateInput
from wagtail.coreutils import get_content_type_label
from wagtail.models import Page, PageLogEntry, get_page_content_types
from wagtail.permissions import page_permission_policy
from wagtail.users.utils import get_deleted_user_display_name
from .base import PageReportView
class AgingPagesReportFilterSet(WagtailFilterSet):
last_published_at = django_filters.DateTimeFilter(
label=_("Last published before"), lookup_expr="lte", widget=AdminDateInput
)
content_type = ContentTypeFilter(
label=_("Type"),
queryset=lambda request: get_page_content_types(include_base_page_type=False),
)
class Meta:
model = Page
fields = ["live", "last_published_at", "content_type"]
class AgingPagesView(PageReportView):
results_template_name = "wagtailadmin/reports/aging_pages_results.html"
page_title = _("Aging pages")
header_icon = "time"
filterset_class = AgingPagesReportFilterSet
index_url_name = "wagtailadmin_reports:aging_pages"
index_results_url_name = "wagtailadmin_reports:aging_pages_results"
export_headings = {
"status_string": _("Status"),
"last_published_at": _("Last published at"),
"last_published_by_user": _("Last published by"),
"content_type": _("Type"),
}
list_export = [
"title",
"status_string",
"last_published_at",
"last_published_by_user",
"content_type",
]
any_permission_required = ["add", "change", "publish"]
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.user_model = get_user_model()
self.custom_field_preprocess = self.custom_field_preprocess.copy()
self.custom_field_preprocess["content_type"] = {
self.FORMAT_CSV: get_content_type_label,
self.FORMAT_XLSX: get_content_type_label,
}
def user_id_to_python(self, user_id):
return self.user_model._meta.pk.to_python(user_id)
def add_last_publisher_name_to_page(self, username_mapping, page):
if page.last_published_by:
# Giving the last_published_by annotation an explicit output_field type
# causes an issue with prefetch_workflow_states when the field is a
# ConvertedValueField. If the last user to publish the page has been
# deleted, we will render their user id in the template, so we call
# to_python on the value so that what's rendered matches the developer's
# expectation in the case of complex primary key types (e.g. UUIDField).
try:
user_id_value = self.user_id_to_python(page.last_published_by)
except ValidationError:
user_id_value = page.last_published_by
last_published_by_user = username_mapping.get(
user_id_value, get_deleted_user_display_name(user_id=user_id_value)
)
page.last_published_by_user = last_published_by_user
else:
page.last_published_by_user = ""
def decorate_paginated_queryset(self, queryset):
user_ids = set(queryset.values_list("last_published_by", flat=True))
username_mapping = {
user.pk: user.get_username()
for user in self.user_model.objects.filter(pk__in=user_ids)
}
for page in queryset:
self.add_last_publisher_name_to_page(username_mapping, page)
return queryset
def get_queryset(self):
latest_publishing_log = PageLogEntry.objects.filter(
page=OuterRef("pk"), action__exact="wagtail.publish"
)
self.queryset = (
page_permission_policy.instances_user_has_permission_for(
self.request.user, "publish"
)
.exclude(last_published_at__isnull=True)
.prefetch_workflow_states()
.select_related("content_type")
.annotate_approved_schedule()
.order_by("last_published_at")
.annotate(
last_published_by=Subquery(latest_publishing_log.values("user")[:1])
)
)
return super().get_queryset()

View File

@@ -0,0 +1,188 @@
import datetime
from collections import defaultdict
import django_filters
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.db.models import IntegerField, Value
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from wagtail.admin.admin_url_finder import AdminURLFinder
from wagtail.admin.filters import (
ContentTypeFilter,
)
from wagtail.admin.views.pages.history import PageHistoryFilterSet
from wagtail.coreutils import get_content_type_label
from wagtail.log_actions import registry as log_action_registry
from wagtail.models import PageLogEntry
from .base import ReportView
def get_users_for_filter(user):
user_ids = set()
for log_model in log_action_registry.get_log_entry_models():
user_ids.update(log_model.objects.viewable_by_user(user).get_user_ids())
User = get_user_model()
return User.objects.filter(pk__in=user_ids).order_by(User.USERNAME_FIELD)
def get_content_types_for_filter(user):
content_type_ids = set()
for log_model in log_action_registry.get_log_entry_models():
content_type_ids.update(
log_model.objects.viewable_by_user(user).get_content_type_ids()
)
return ContentType.objects.filter(pk__in=content_type_ids).order_by("model")
def get_actions_for_filter(user):
# Only return those actions used by log entries visible to the user.
actions = set()
for log_model in log_action_registry.get_log_entry_models():
actions.update(log_model.objects.viewable_by_user(user).get_actions())
return [
action for action in log_action_registry.get_choices() if action[0] in actions
]
class SiteHistoryReportFilterSet(PageHistoryFilterSet):
label = django_filters.CharFilter(label=_("Name"), lookup_expr="icontains")
object_type = ContentTypeFilter(
label=_("Type"),
method="filter_object_type",
queryset=lambda request: get_content_types_for_filter(request.user),
)
def filter_object_type(self, queryset, name, value):
return queryset.filter_on_content_type(value)
def get_action_choices(self):
return get_actions_for_filter(self.request.user)
def get_user_choices(self):
return get_users_for_filter(self.request.user)
class Meta:
model = PageLogEntry
fields = []
class LogEntriesView(ReportView):
results_template_name = "wagtailadmin/reports/site_history_results.html"
page_title = _("Site history")
header_icon = "history"
filterset_class = SiteHistoryReportFilterSet
index_url_name = "wagtailadmin_reports:site_history"
index_results_url_name = "wagtailadmin_reports:site_history_results"
export_headings = {
"object_id": _("ID"),
"label": _("Name"),
"content_type": _("Type"),
"action": _("Action type"),
"user_display_name": _("User"),
"timestamp": _("Date/Time"),
}
list_export = [
"object_id",
"label",
"content_type",
"action",
"user_display_name",
"timestamp",
]
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.custom_field_preprocess = self.custom_field_preprocess.copy()
self.custom_field_preprocess["action"] = {
self.FORMAT_CSV: self.get_action_label,
self.FORMAT_XLSX: self.get_action_label,
}
self.custom_field_preprocess["content_type"] = {
self.FORMAT_CSV: get_content_type_label,
self.FORMAT_XLSX: get_content_type_label,
}
def get_filename(self):
return "audit-log-{}".format(datetime.datetime.today().strftime("%Y-%m-%d"))
def get_filtered_queryset(self):
"""
Since this report combines records from multiple log models, the standard pattern of
returning a queryset from get_queryset() to be filtered by filter_queryset() is not
possible - the subquery for each log model must be filtered separately before joining
with union().
Additionally, a union() on standard model-based querysets will return a queryset based on
the first model in the union, so instances of the other model(s) would be returned as the
wrong type. To avoid this, we construct values() querysets as follows:
1. For each model, construct a values() queryset consisting of id, timestamp and an
annotation to indicate which model it is, and filter this with filter_queryset
2. Form a union() queryset from these queries, and order it by -timestamp
(this is the result returned from get_filtered_queryset)
3. Apply pagination (done in MultipleObjectMixin.get_context_data)
4. (In decorate_paginated_queryset:) For each model included in the result set, look up
the set of model instances by ID. Use these to form a final list of model instances
in the same order as the query.
"""
queryset = None
# Retrieve the set of registered log models, and cast it to a list so that we assign
# an index number to each one; this index number will be used to distinguish models
# in the combined results
self.log_models = list(log_action_registry.get_log_entry_models())
for log_model_index, log_model in enumerate(self.log_models):
sub_queryset = (
log_model.objects.viewable_by_user(self.request.user)
.values("pk", "timestamp")
.annotate(
log_model_index=Value(log_model_index, output_field=IntegerField())
)
)
sub_queryset = self.filter_queryset(sub_queryset)
# disable any native ordering on the queryset; we will re-apply it on the combined result
sub_queryset = sub_queryset.order_by()
if queryset is None:
queryset = sub_queryset
else:
queryset = queryset.union(sub_queryset)
return queryset.order_by("-timestamp")
def decorate_paginated_queryset(self, queryset):
# build lists of ids from queryset, grouped by log model index
pks_by_log_model_index = defaultdict(list)
for row in queryset:
pks_by_log_model_index[row["log_model_index"]].append(row["pk"])
url_finder = AdminURLFinder(self.request.user)
# for each log model found in the queryset, look up the set of log entries by id
# and build a lookup table
object_lookup = {}
for log_model_index, pks in pks_by_log_model_index.items():
log_entries = (
self.log_models[log_model_index]
.objects.prefetch_related("user__wagtail_userprofile", "content_type")
.filter(pk__in=pks)
.with_instances()
)
for log_entry, instance in log_entries:
# annotate log entry with an 'edit_url' property
log_entry.edit_url = url_finder.get_edit_url(instance)
object_lookup[(log_model_index, log_entry.pk)] = log_entry
# return items from our lookup table in the order of the original queryset
return [object_lookup[(row["log_model_index"], row["pk"])] for row in queryset]
def get_action_label(self, action):
return force_str(log_action_registry.get_action_label(action))

View File

@@ -0,0 +1,81 @@
from warnings import warn
from django.utils.translation import gettext_lazy as _
from wagtail.admin.views.generic import BaseListingView, PermissionCheckedMixin
from wagtail.admin.views.mixins import SpreadsheetExportMixin
from wagtail.permissions import page_permission_policy
from wagtail.utils.deprecation import RemovedInWagtail70Warning
class ReportView(SpreadsheetExportMixin, PermissionCheckedMixin, BaseListingView):
template_name = "wagtailadmin/reports/base_report.html"
results_template_name = "wagtailadmin/reports/base_report_results.html"
title = ""
paginate_by = 50
_show_breadcrumbs = True
def get_breadcrumbs_items(self):
return super().get_breadcrumbs_items() + [
{"url": "", "label": self.get_page_title()}
]
def get_page_title(self):
if self.page_title:
return self.page_title
# WagtailAdminTemplateMixin uses `page_title`, but the documented approach
# for ReportView used `title`, so we need to support both during the
# deprecation period. When `title` is removed, this and the `get_context_data`
# overrides can be removed.
warn(
f"The `title` attribute in `{self.__class__.__name__}` (a `ReportView` subclass) "
"is deprecated. Use `page_title` instead.",
RemovedInWagtail70Warning,
)
return self.title
def get_filtered_queryset(self):
return self.filter_queryset(self.get_queryset())
def decorate_paginated_queryset(self, object_list):
# A hook point to allow rewriting the object list after pagination has been applied
return object_list
def get(self, request, *args, **kwargs):
self.object_list = self.get_filtered_queryset()
context = self.get_context_data()
# Decorate the queryset *after* Django's BaseListView has returned a paginated/reduced
# list of objects
context["object_list"] = self.decorate_paginated_queryset(
context["object_list"]
)
return self.render_to_response(context)
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["title"] = self.get_page_title()
return context
def render_to_response(self, context, **response_kwargs):
if self.is_export:
return self.as_spreadsheet(
context["object_list"], self.request.GET.get("export")
)
return super().render_to_response(context, **response_kwargs)
class PageReportView(ReportView):
results_template_name = "wagtailadmin/reports/base_page_report_results.html"
export_headings = {
"latest_revision_created_at": _("Updated"),
"status_string": _("Status"),
"content_type.model_class._meta.verbose_name.title": _("Type"),
}
list_export = [
"title",
"latest_revision_created_at",
"status_string",
"content_type.model_class._meta.verbose_name.title",
]
context_object_name = "pages"
permission_policy = page_permission_policy

View File

@@ -0,0 +1,69 @@
import datetime
import django_filters
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from wagtail.admin.filters import DateRangePickerWidget, WagtailFilterSet
from wagtail.models import Page
from wagtail.permissions import page_permission_policy
from .base import PageReportView
def get_users_for_filter():
User = get_user_model()
return (
User.objects.filter(locked_pages__isnull=False)
.order_by(User.USERNAME_FIELD)
.distinct()
)
class LockedPagesReportFilterSet(WagtailFilterSet):
locked_at = django_filters.DateFromToRangeFilter(widget=DateRangePickerWidget)
locked_by = django_filters.ModelChoiceFilter(
field_name="locked_by", queryset=lambda request: get_users_for_filter()
)
class Meta:
model = Page
fields = ["locked_by", "locked_at", "live"]
class LockedPagesView(PageReportView):
results_template_name = "wagtailadmin/reports/locked_pages_results.html"
page_title = _("Locked pages")
header_icon = "lock"
list_export = PageReportView.list_export + [
"locked_at",
"locked_by",
]
filterset_class = LockedPagesReportFilterSet
index_url_name = "wagtailadmin_reports:locked_pages"
index_results_url_name = "wagtailadmin_reports:locked_pages_results"
permission_required = "unlock"
def get_filename(self):
return "locked-pages-report-{}".format(
datetime.datetime.today().strftime("%Y-%m-%d")
)
def get_queryset(self):
pages = (
(
page_permission_policy.instances_user_has_permission_for(
self.request.user, "change"
)
| Page.objects.filter(locked_by=self.request.user)
)
.filter(locked=True)
.specific(defer=True)
)
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
pages = pages.select_related("locale")
self.queryset = pages
return super().get_queryset()

View File

@@ -0,0 +1,148 @@
import django_filters
from django.conf import settings
from django.db.models import Count, F, OuterRef, Q, Subquery
from django.utils.translation import gettext_lazy as _
from wagtail.admin.filters import WagtailFilterSet
from wagtail.admin.views.reports import ReportView
from wagtail.coreutils import get_content_languages
from wagtail.models import ContentType, Page, Site, get_page_models
from wagtail.permissions import page_permission_policy
def _get_locale_choices():
return list(get_content_languages().items())
def _get_site_choices():
"""Tuples of (site root page path, site display name) for all sites in project."""
choices = [
(site.root_page.path, str(site))
for site in Site.objects.all().select_related("root_page")
]
return choices
def _annotate_last_edit_info(queryset, language_code, site_root_path):
latest_edited_page_filter_kwargs = {}
page_count_filter_kwargs = {}
if language_code:
latest_edited_page_filter_kwargs["locale__language_code"] = language_code
page_count_filter_kwargs["pages__locale__language_code"] = language_code
if site_root_path:
latest_edited_page_filter_kwargs["path__startswith"] = site_root_path
page_count_filter_kwargs["pages__path__startswith"] = site_root_path
latest_edited_page = Page.objects.filter(
content_type=OuterRef("pk"), **latest_edited_page_filter_kwargs
).order_by(F("latest_revision_created_at").desc(nulls_last=True), "title", "-pk")
queryset = queryset.annotate(
count=Count("pages", filter=Q(**page_count_filter_kwargs)),
last_edited_page_id=Subquery(latest_edited_page.values("pk")[:1]),
)
return queryset
class LocaleFilter(django_filters.ChoiceFilter):
def filter(self, qs, language_code):
if language_code:
return qs.filter(pages__locale__language_code=language_code)
return qs
class SiteFilter(django_filters.ChoiceFilter):
def filter(self, qs, site_root_path):
# Value passed will be the site root page path
# To check if a page is in a site, we check if the page path starts with the
# site's root page path
if site_root_path:
return qs.filter(pages__path__startswith=site_root_path)
return qs
class PageTypesUsageReportFilterSet(WagtailFilterSet):
page_locale = LocaleFilter(
label=_("Locale"),
choices=_get_locale_choices,
empty_label=None,
null_label=_("All"),
null_value=None,
)
site = SiteFilter(
label=_("Site"),
choices=_get_site_choices,
empty_label=None,
null_label=_("All"),
null_value=None,
)
class Meta:
model = ContentType
fields = ["page_locale", "site"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sites = {
site.pk: site for site in Site.objects.all().prefetch_related("root_page")
}
self.sites_filter_enabled = True
if len(self.sites) == 1:
# If there is only one site, we don't need to show the site filter
self.sites_filter_enabled = False
del self.filters["site"]
class PageTypesUsageReportView(ReportView):
results_template_name = "wagtailadmin/reports/page_types_usage_results.html"
page_title = _("Page types usage")
header_icon = "doc-empty-inverse"
filterset_class = PageTypesUsageReportFilterSet
index_url_name = "wagtailadmin_reports:page_types_usage"
index_results_url_name = "wagtailadmin_reports:page_types_usage_results"
permission_policy = page_permission_policy
any_permission_required = ["add", "change", "publish"]
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.page_models = [model.__name__.lower() for model in get_page_models()]
self.i18n_enabled = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
def decorate_paginated_queryset(self, page_types):
pages_mapping = Page.objects.specific().in_bulk(
obj.last_edited_page_id for obj in page_types if obj.last_edited_page_id
)
for item in page_types:
item.last_edited_page = (
pages_mapping[item.last_edited_page_id]
if item.last_edited_page_id
else None
)
return page_types
def get_queryset(self):
queryset = ContentType.objects.filter(model__in=self.page_models)
page_content_type = ContentType.objects.get_for_model(Page)
has_pages = Page.objects.filter(
depth__gt=1, content_type=page_content_type
).exists()
if not has_pages:
# If there are no `wagtailcore.Page` pages, we don't need to
# show it in the report
queryset = queryset.exclude(id=page_content_type.id)
self.queryset = queryset
queryset = self.filter_queryset(queryset)
language_code = self.filters.form.cleaned_data.get("page_locale", None)
site_root_path = self.filters.form.cleaned_data.get("site", None)
queryset = _annotate_last_edit_info(queryset, language_code, site_root_path)
queryset = queryset.order_by("-count", "app_label", "model")
return queryset

View File

@@ -0,0 +1,300 @@
import datetime
import django_filters
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.db.models import CharField, Q
from django.db.models.functions import Cast
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from wagtail.admin.filters import (
DateRangePickerWidget,
FilteredModelChoiceFilter,
WagtailFilterSet,
)
from wagtail.admin.utils import get_latest_str
from wagtail.admin.widgets.button import HeaderButton
from wagtail.coreutils import get_content_type_label
from wagtail.models import (
Task,
TaskState,
Workflow,
WorkflowState,
get_default_page_content_type,
)
from wagtail.permissions import page_permission_policy
from wagtail.snippets.models import get_editable_models
from .base import ReportView
def get_requested_by_queryset(request):
User = get_user_model()
return User.objects.filter(
pk__in=set(WorkflowState.objects.values_list("requested_by__pk", flat=True))
).order_by(User.USERNAME_FIELD)
def get_editable_page_ids_query(request):
pages = page_permission_policy.instances_user_has_permission_for(
request.user, "change"
)
# Need to cast the page ids to string because Postgres doesn't support
# implicit type casts when querying on GenericRelations
# https://code.djangoproject.com/ticket/16055
# Once the issue is resolved, we can remove this function
# and change the query to page__in=pages
return pages.values_list(Cast("id", output_field=CharField()), flat=True)
def get_editable_content_type_ids(request):
editable_models = get_editable_models(request.user)
return [
ct.id for ct in ContentType.objects.get_for_models(*editable_models).values()
]
class WorkflowReportFilterSet(WagtailFilterSet):
created_at = django_filters.DateFromToRangeFilter(
label=_("Started at"), widget=DateRangePickerWidget
)
reviewable = django_filters.ChoiceFilter(
label=_("Show"),
method="filter_reviewable",
choices=(("true", _("Awaiting my review")),),
empty_label=_("All"),
widget=forms.RadioSelect,
)
requested_by = django_filters.ModelChoiceFilter(
field_name="requested_by", queryset=get_requested_by_queryset
)
def filter_reviewable(self, queryset, name, value):
if value and self.request and self.request.user:
queryset = queryset.filter(
current_task_state__in=TaskState.objects.reviewable_by(
self.request.user
)
)
return queryset
class Meta:
model = WorkflowState
fields = ["reviewable", "workflow", "status", "requested_by", "created_at"]
class WorkflowTasksReportFilterSet(WagtailFilterSet):
started_at = django_filters.DateFromToRangeFilter(
label=_("Started at"), widget=DateRangePickerWidget
)
finished_at = django_filters.DateFromToRangeFilter(
label=_("Completed at"), widget=DateRangePickerWidget
)
workflow = django_filters.ModelChoiceFilter(
field_name="workflow_state__workflow",
queryset=Workflow.objects.all(),
label=_("Workflow"),
)
# When a workflow is chosen in the 'id_workflow' selector, filter this list of tasks
# to just the ones whose workflows attribute includes the selected workflow.
task = FilteredModelChoiceFilter(
queryset=Task.objects.all(),
filter_field="id_workflow",
filter_accessor="workflows",
)
reviewable = django_filters.ChoiceFilter(
label=_("Show"),
method="filter_reviewable",
choices=(("true", _("Awaiting my review")),),
empty_label=_("All"),
widget=forms.RadioSelect,
)
def filter_reviewable(self, queryset, name, value):
if value and self.request and self.request.user:
queryset = queryset.filter(
id__in=TaskState.objects.reviewable_by(self.request.user).values_list(
"id", flat=True
)
)
return queryset
class Meta:
model = TaskState
fields = [
"reviewable",
"workflow",
"task",
"status",
"started_at",
"finished_at",
]
class WorkflowView(ReportView):
results_template_name = "wagtailadmin/reports/workflow_results.html"
page_title = _("Workflows")
header_icon = "tasks"
filterset_class = WorkflowReportFilterSet
index_url_name = "wagtailadmin_reports:workflow"
index_results_url_name = "wagtailadmin_reports:workflow_results"
permission_policy = page_permission_policy
any_permission_required = ["add", "change", "publish"]
export_headings = {
"content_object.pk": _("Page/Snippet ID"),
"content_type": _("Page/Snippet Type"),
"content_object": _("Page/Snippet Title"),
"get_status_display": _("Status"),
"created_at": _("Started at"),
}
list_export = [
"workflow",
"content_object.pk",
"content_type",
"content_object",
"get_status_display",
"requested_by",
"created_at",
]
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.custom_field_preprocess = self.custom_field_preprocess.copy()
self.custom_field_preprocess["content_object"] = {
self.FORMAT_CSV: self.get_title,
self.FORMAT_XLSX: self.get_title,
}
self.custom_field_preprocess["content_type"] = {
self.FORMAT_CSV: get_content_type_label,
self.FORMAT_XLSX: get_content_type_label,
}
@cached_property
def header_buttons(self):
return [
HeaderButton(
gettext("By task"),
reverse("wagtailadmin_reports:workflow_tasks"),
icon_name="thumbtack",
)
]
def get_title(self, content_object):
return get_latest_str(content_object)
def get_filename(self):
return "workflow-report-{}".format(
datetime.datetime.today().strftime("%Y-%m-%d")
)
def get_queryset(self):
editable_pages = Q(
base_content_type_id=get_default_page_content_type().id,
object_id__in=get_editable_page_ids_query(self.request),
)
editable_objects = Q(
content_type_id__in=get_editable_content_type_ids(self.request)
)
return (
WorkflowState.objects.filter(editable_pages | editable_objects)
.select_related("workflow", "requested_by")
.prefetch_related("content_object", "content_object__latest_revision")
.order_by("-created_at")
)
def decorate_paginated_queryset(self, object_list):
return [obj for obj in object_list if obj.content_object]
class WorkflowTasksView(ReportView):
results_template_name = "wagtailadmin/reports/workflow_tasks_results.html"
page_title = _("Workflow tasks")
header_icon = "thumbtack"
filterset_class = WorkflowTasksReportFilterSet
index_url_name = "wagtailadmin_reports:workflow_tasks"
index_results_url_name = "wagtailadmin_reports:workflow_tasks_results"
permission_policy = page_permission_policy
any_permission_required = ["add", "change", "publish"]
export_headings = {
"workflow_state.content_object.pk": _("Page/Snippet ID"),
"workflow_state.content_type": _("Page/Snippet Type"),
"workflow_state.content_object.__str__": _("Page/Snippet Title"),
"get_status_display": _("Status"),
"workflow_state.requested_by": _("Requested By"),
}
list_export = [
"task",
"workflow_state.content_object.pk",
"workflow_state.content_type",
"workflow_state.content_object.__str__",
"get_status_display",
"workflow_state.requested_by",
"started_at",
"finished_at",
"finished_by",
]
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.custom_field_preprocess = self.custom_field_preprocess.copy()
self.custom_field_preprocess["workflow_state.content_object"] = {
self.FORMAT_CSV: self.get_title,
self.FORMAT_XLSX: self.get_title,
}
self.custom_field_preprocess["workflow_state.content_type"] = {
self.FORMAT_CSV: get_content_type_label,
self.FORMAT_XLSX: get_content_type_label,
}
@cached_property
def header_buttons(self):
return [
HeaderButton(
gettext("By workflow"),
reverse("wagtailadmin_reports:workflow"),
icon_name="tasks",
)
]
def get_title(self, content_object):
return get_latest_str(content_object)
def get_filename(self):
return "workflow-tasks-{}".format(
datetime.datetime.today().strftime("%Y-%m-%d")
)
def get_queryset(self):
editable_pages = Q(
workflow_state__base_content_type_id=get_default_page_content_type().id,
workflow_state__object_id__in=get_editable_page_ids_query(self.request),
)
editable_objects = Q(
workflow_state__content_type_id__in=get_editable_content_type_ids(
self.request
)
)
return (
TaskState.objects.filter(editable_pages | editable_objects)
.select_related("workflow_state", "task")
.prefetch_related(
"workflow_state__content_object",
"workflow_state__content_object__latest_revision",
)
.order_by("-started_at")
)
def decorate_paginated_queryset(self, object_list):
return [obj for obj in object_list if obj.workflow_state.content_object]