diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 86486cbfbc..60bf2238b0 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -13,6 +13,7 @@ Changelog * Allow `Page.permissions_for_user()` to be overridden by specific page types (Sébastien Corbin) * Improve visual alignment of explore icon in Page listings for longer content (Krzysztof Jeziorny) * Add `extra_actions` blocks to Snippets and generic index templates (Bhuvnesh Sharma) + * Added page types usage report (Jhonatan Lopes) * Fix: Update system check for overwriting storage backends to recognise the `STORAGES` setting introduced in Django 4.2 (phijma-leukeleu) * Fix: Prevent password change form from raising a validation error when browser autocomplete fills in the "Old password" field (Chiemezuo Akujobi) * Fix: Ensure that the legacy dropdown options, when closed, do not get accidentally clicked by other interactions wide viewports (CheesyPhoenix, Christer Jensen) diff --git a/docs/releases/6.0.md b/docs/releases/6.0.md index 447102e73f..d7d962d2fb 100644 --- a/docs/releases/6.0.md +++ b/docs/releases/6.0.md @@ -23,6 +23,7 @@ depth: 1 * Allow `Page.permissions_for_user()` to be overridden by specific page types (Sébastien Corbin) * Improve visual alignment of explore icon in Page listings for longer content (Krzysztof Jeziorny) * Add `extra_actions` blocks to Snippets and generic index templates (Bhuvnesh Sharma) + * Added page types usage report (Jhonatan Lopes) ### Bug fixes diff --git a/wagtail/admin/templates/wagtailadmin/reports/listing/_list_page_types_usage.html b/wagtail/admin/templates/wagtailadmin/reports/listing/_list_page_types_usage.html new file mode 100644 index 0000000000..54dbf798b0 --- /dev/null +++ b/wagtail/admin/templates/wagtailadmin/reports/listing/_list_page_types_usage.html @@ -0,0 +1,89 @@ +{% load i18n wagtailadmin_tags wagtailcore_tags %} + + + + + + + + + {% block post_parent_page_headers %} + + + + + + + {% block extra_columns %} + {% endblock extra_columns %} + + {% endblock post_parent_page_headers %} + + + {% if page_types %} + {% for page_type in page_types %} + + + + + + + {% block extra_page_data %} + {% endblock extra_page_data %} + + {% endfor %} + {% else %} + {% block no_results %} +

{% trans "No page types found." %}

+ {% endblock no_results %} + {% endif %} + +
+ {% trans 'Type' %} + + {% trans 'App' %} + + {% trans 'Pages' %} + + {% trans 'Last edited page' %} + + {% trans 'Last edit' %} +
+ {% block page_type %} + {{ page_type.name|title }} + {% endblock page_type %} + + {{ page_type.app_label }}.{{ page_type.model }} + + {% if page_type.count > 0 %} + {{ page_type.count }} + {% else %} + {{ page_type.count }} + {% endif %} + + {% if page_type.last_edited_page %} + {% page_permissions page_type.last_edited_page as perms %} + {% with page_display_title=page_type.last_edited_page.get_admin_display_title %} + {% if perms.can_edit %} + + {{ page_display_title }} + + {% else %} +

{{ page_display_title }}

+ {% endif %} + {% endwith %} + {% i18n_enabled as show_locale_labels %} + {% if show_locale_labels %} + {% locale_label_from_id page_type.last_edited_page.locale_id as locale_label %} + {% status locale_label classname="w-status--label" %} + {% endif %} + {% else %} + - + {% endif %} +
+ {% if page_type.last_edited_page.latest_revision_created_at %} + {% human_readable_date page_type.last_edited_page.latest_revision_created_at %} + {% else %} + - + {% endif %} +
\ No newline at end of file diff --git a/wagtail/admin/templates/wagtailadmin/reports/page_types_usage.html b/wagtail/admin/templates/wagtailadmin/reports/page_types_usage.html new file mode 100644 index 0000000000..6cbdcd1346 --- /dev/null +++ b/wagtail/admin/templates/wagtailadmin/reports/page_types_usage.html @@ -0,0 +1,18 @@ +{% extends "wagtailadmin/reports/base_report.html" %} +{% load i18n wagtailadmin_tags %} + +{% block results %} + {% with object_list as page_types %} +
+ {% if page_types %} + {% block listing %} + {% include "wagtailadmin/reports/listing/_list_page_types_usage.html" %} + {% endblock listing %} + {% else %} + {% block no_results %} +

{% trans "No page types found." %}

+ {% endblock no_results %} + {% endif %} +
+ {% endwith %} +{% endblock results %} \ No newline at end of file diff --git a/wagtail/admin/tests/test_reports_views.py b/wagtail/admin/tests/test_reports_views.py index dde81d68f7..0bfcec0036 100644 --- a/wagtail/admin/tests/test_reports_views.py +++ b/wagtail/admin/tests/test_reports_views.py @@ -6,16 +6,30 @@ from django.conf import settings from django.conf.locale import LANG_INFO from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType -from django.test import TestCase +from django.db.models import F +from django.test import RequestFactory, TestCase from django.test.utils import override_settings from django.urls import reverse from django.utils import timezone, translation from openpyxl import load_workbook from wagtail.admin.views.mixins import ExcelDateFormatter +from wagtail.admin.views.reports import page_types_usage from wagtail.admin.views.reports.audit_logging import LogEntriesView -from wagtail.models import GroupPagePermission, ModelLogEntry, Page, PageLogEntry -from wagtail.test.testapp.models import Advert +from wagtail.models import ( + GroupPagePermission, + Locale, + ModelLogEntry, + Page, + PageLogEntry, + Site, +) +from wagtail.test.testapp.models import ( + Advert, + EventPage, + EventPageSpeaker, + SimplePage, +) from wagtail.test.utils import WagtailTestUtils @@ -718,3 +732,381 @@ class TestFilteredAgingPagesView(WagtailTestUtils, TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, self.aboutus_page.title) self.assertNotContains(response, self.home_page.title) + + +class PageTypesUsageReportViewTest(WagtailTestUtils, TestCase): + fixtures = ["test.json"] + + def setUp(self): + self.user = self.login() + + def get(self, params={}): + return self.client.get(reverse("wagtailadmin_reports:page_types_usage"), params) + + @staticmethod + def display_name(content_type): + return f"{content_type.app_label}.{content_type.model}" + + def test_simple(self): + response = self.get() + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "wagtailadmin/reports/page_types_usage.html") + + def test_displays_only_page_types(self): + """Asserts that the correct models are included in the queryset.""" + response = self.get() + # Assert that the response contains page models: + event_page_content_type = ContentType.objects.get_for_model(EventPage) + event_page_content_type_full_name = self.display_name(event_page_content_type) + self.assertContains(response, event_page_content_type_full_name) + simple_page_content_type = ContentType.objects.get_for_model(SimplePage) + simple_page_content_type_full_name = self.display_name(simple_page_content_type) + self.assertContains(response, simple_page_content_type_full_name) + # But it should not contain non-page models: + event_page_speaker_content_type = ContentType.objects.get_for_model( + EventPageSpeaker + ) + event_page_speaker_content_type_full_name = self.display_name( + event_page_speaker_content_type + ) + self.assertNotContains(response, event_page_speaker_content_type_full_name) + + def test_displays_wagtailcore_page_if_has_instances(self): + """Asserts that the wagtailcore.Page model is included in the queryset if it has instances.""" + page_content_type = ContentType.objects.get_for_model(Page) + page_content_type_full_name = self.display_name(page_content_type) + + # Start with no pages: + Page.objects.filter(depth__gt=1, content_type=page_content_type).delete() + + # There aren't any Page instances and it is not creatable, it shouldn't be included in the report + response = self.get() + self.assertNotContains(response, page_content_type_full_name) + + # Create a page: + page = Page(title="Page") + Page.get_first_root_node().add_child(instance=page) + + # There is a Page now, so report should include the `Page` ContentType + response = self.get() + self.assertContains(response, page_content_type_full_name) + + +class PageTypesUsageReportViewQuerysetTests(WagtailTestUtils, TestCase): + def setUp(self): + super().setUp() + self.view = page_types_usage.PageTypesUsageReportView() + self.view.request = RequestFactory().get( + reverse("wagtailadmin_reports:page_types_usage") + ) + self.root = Page.objects.first() + self.home = Page.objects.get(slug="home") + + self.simple_page_a = SimplePage(title="Simple page A", content="hello") + self.simple_page_b = SimplePage(title="Simple page B", content="hello") + self.simple_page_c = SimplePage(title="Simple page C", content="hello") + + Page.get_first_root_node().add_child(instance=self.simple_page_a) + Page.get_first_root_node().add_child(instance=self.simple_page_b) + Page.get_first_root_node().add_child(instance=self.simple_page_c) + self.simple_page_a.save_revision().publish() + self.simple_page_b.save_revision().publish() + self.simple_page_c.save_revision().publish() + + self.event_page = EventPage( + title="Event Page", + audience="public", + location="foo", + cost="bar", + date_from=datetime.date.today(), + ) + Page.get_first_root_node().add_child(instance=self.event_page) + + def test_queryset_ordering(self): + """Asserts that the queryset is ordered by page model count.""" + # Test that the queryset is correctly ordered by page model count + queryset = self.view.get_queryset() + # Convert queryset to list of ids to make it easier to test + queryset_list_pks = list(queryset.values_list("pk", flat=True)) + # Get the positions of the content types in the list + simple_page_content_type = ContentType.objects.get_for_model(SimplePage) + simple_page_position = queryset_list_pks.index(simple_page_content_type.pk) + event_page_content_type = ContentType.objects.get_for_model(EventPage) + event_page_position = queryset_list_pks.index(event_page_content_type.pk) + # Assert that the SimplePage comes before EventPage, since it has more entries + self.assertTrue(simple_page_position < event_page_position) + # There should be 3 SimplePages + self.assertEqual(queryset.get(id=simple_page_content_type.pk).count, 3) + # There should be 1 EventPage + self.assertEqual(queryset.get(id=event_page_content_type.pk).count, 1) + + def test_queryset_last_edited_page(self): + """Tests that the queryset correctly returns the last edited page.""" + # Edit the first simple page + revision = self.simple_page_a.save_revision() + revision.publish() + # Edit the second simple page + revision = self.simple_page_b.save_revision() + revision.publish() + # Edit the third simple page + revision = self.simple_page_c.save_revision() + revision.publish() + # Re-edit the first simple page + revision = self.simple_page_a.save_revision() + revision.publish() + # Get the queryset: + queryset = self.view.decorate_paginated_queryset(self.view.get_queryset()) + # Assert that the first simple page is the last edited page + self.simple_page_a.refresh_from_db() + self.assertEqual(queryset[0].last_edited_page.specific, self.simple_page_a) + + +@override_settings(LANGUAGE_CODE="en", WAGTAIL_I18N_ENABLED=True) +class PageTypesReportFiltersTests(WagtailTestUtils, TestCase): + def setUp(self): + self.user = self.login() + self.default_locale = Locale.get_default() + self.fr_locale, _ = Locale.objects.get_or_create(language_code="fr") + + def get(self, params={}): + return self.client.get(reverse("wagtailadmin_reports:page_types_usage"), params) + + def test_locale_filtering(self): + # Create pages in default locale + event_page = EventPage( + title="Event Page", + audience="public", + location="foo", + cost="bar", + date_from=datetime.date.today(), + ) + simple_page = SimplePage(title="Simple Page", content="hello") + Page.get_first_root_node().add_child(instance=event_page) + Page.get_first_root_node().add_child(instance=simple_page) + event_page.save_revision().publish() + simple_page.save_revision().publish() + # Translate pages to French + event_page.copy_for_translation(self.fr_locale) + simple_page.copy_for_translation(self.fr_locale) + + # Edit the simple page in English to make sure that it's the latest + simple_page.title = "Updated Simple Page English title" + revision = simple_page.save_revision() + simple_page.publish(revision) + + response = self.get() + page_types = { + content_type.id: content_type + for content_type in response.context["object_list"] + } + + event_page_row = page_types.get(ContentType.objects.get_for_model(EventPage).pk) + simple_page_row = page_types.get( + ContentType.objects.get_for_model(SimplePage).pk + ) + + self.assertEqual(event_page_row.count, 2) + self.assertEqual(simple_page_row.count, 2) + # The last edited page should be the French version + self.assertEqual(event_page_row.last_edited_page.locale, self.fr_locale) + # The last edited SimplePage should be the English version + self.assertEqual(simple_page_row.last_edited_page.locale, self.default_locale) + + # Filter by French locale + response = self.get({"page_locale": self.fr_locale.language_code}) + page_types = { + content_type.id: content_type + for content_type in response.context["object_list"] + } + + event_page_row = page_types.get(ContentType.objects.get_for_model(EventPage).pk) + simple_page_row = page_types.get( + ContentType.objects.get_for_model(SimplePage).pk + ) + + # There should be 1 of each page (only the French locale ones) + self.assertEqual(event_page_row.count, 1) + self.assertEqual(simple_page_row.count, 1) + # The last edited page should be the French version (even though page was later edited in English) + self.assertEqual(event_page_row.last_edited_page.locale, self.fr_locale) + self.assertEqual(simple_page_row.last_edited_page.locale, self.fr_locale) + + def test_site_filtering_with_single_site(self): + """Asserts that the site filter is not displayed when there is only one site.""" + sites = Site.objects.all() + self.assertEqual(sites.count(), 1) + + response = self.get() + filterset = response.context["filters"] + + # Assert that the filterset does not have the site field + self.assertNotIn("site", filterset.form.fields) + self.assertNotIn("site", filterset.filters.keys()) + self.assertFalse(filterset.sites_filter_enabled) + + def test_site_filtering_with_multiple_sites(self): + root_page = Page.get_first_root_node() + # Create pages in default locale + event_page = EventPage( + title="Event Page", + audience="public", + location="foo", + cost="bar", + date_from=datetime.date.today(), + ) + simple_page = SimplePage(title="Simple Page", content="hello") + root_page.add_child(instance=event_page) + root_page.add_child(instance=simple_page) + + # Create a new site and add the pages to it + simple_page_site = Site.objects.create( + hostname="example.com", root_page=simple_page, is_default_site=False + ) + self.assertEqual(Site.objects.count(), 2) + + response = self.get() + page_types = { + content_type.id: content_type + for content_type in response.context["object_list"] + } + + event_page_row = page_types.get(ContentType.objects.get_for_model(EventPage).pk) + simple_page_row = page_types.get( + ContentType.objects.get_for_model(SimplePage).pk + ) + + self.assertEqual(event_page_row.count, 1) + self.assertEqual(simple_page_row.count, 1) + + # Filter by the simple_page_site + response = self.get({"site": simple_page_site.root_page.path}) + page_types = { + content_type.id: content_type + for content_type in response.context["object_list"] + } + + simple_page_row = page_types.get( + ContentType.objects.get_for_model(SimplePage).pk + ) + + # There should be 1 SimplePage + self.assertEqual(simple_page_row.count, 1) + # There shouldn't be a regular Page + self.assertFalse(ContentType.objects.get_for_model(EventPage).pk in page_types) + + @override_settings( + WAGTAIL_CONTENT_LANGUAGES=[ + ("en", "English"), + ("de", "German"), + ("fr", "French"), + ], + ) + def test_get_locale_choices(self): + choices = page_types_usage._get_locale_choices() + + expected_choices = [ + ("en", "English"), + ("de", "German"), + ("fr", "French"), + ] + + self.assertCountEqual(choices, expected_choices) + + +class TestPageTypesUsageReportViewPermissions(WagtailTestUtils, TestCase): + fixtures = ["test.json"] + + def setUp(self): + self.user = self.login() + + def get(self, params={}): + return self.client.get(reverse("wagtailadmin_reports:page_types_usage"), params) + + def test_simple(self): + response = self.get() + self.assertEqual(response.status_code, 200) + + def test_get_with_no_permission(self): + group = Group.objects.create(name="test group") + self.user.is_superuser = False + self.user.save() + self.user.groups.add(group) + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + # No GroupPagePermission created + + response = self.get() + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("wagtailadmin_home")) + + def test_get_with_minimal_permissions(self): + group = Group.objects.create(name="test group") + self.user.is_superuser = False + self.user.save() + self.user.groups.add(group) + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + GroupPagePermission.objects.create( + group=group, + page=Page.objects.first(), + permission_type="add", + ) + + response = self.get() + + self.assertEqual(response.status_code, 200) + + def test_get_with_page_specific_permissions(self): + group = Group.objects.create(name="test group") + self.user.is_superuser = False + self.user.save() + self.user.groups.add(group) + self.user.user_permissions.add( + Permission.objects.get( + content_type__app_label="wagtailadmin", codename="access_admin" + ) + ) + latest_edited_event_page = EventPage.objects.order_by( + F("latest_revision_created_at").desc(nulls_last=True), "title", "-pk" + ).first() + latest_edited_simple_page = SimplePage.objects.order_by( + F("latest_revision_created_at").desc(nulls_last=True), "title", "-pk" + ).first() + GroupPagePermission.objects.create( + group=group, + page=latest_edited_event_page, + permission_type="change", + ) + + response = self.get() + + self.assertEqual(response.status_code, 200) + # For pages that the user can edit, it should show the page title and link to edit the page: + edit_event_page_url = reverse( + "wagtailadmin_pages:edit", args=(latest_edited_event_page.id,) + ) + self.assertContains( + response, + f"{latest_edited_event_page.get_admin_display_title()}", + html=True, + ) + # For pages that the user cannot edit, it should only show the page title + self.assertContains( + response, + f"

{latest_edited_simple_page.get_admin_display_title()}

", + html=True, + ) + edit_simple_page_url = reverse( + "wagtailadmin_pages:edit", args=(latest_edited_simple_page.id,) + ) + self.assertNotContains( + response, + f"{latest_edited_simple_page.get_admin_display_title()}", + html=True, + ) diff --git a/wagtail/admin/urls/reports.py b/wagtail/admin/urls/reports.py index 9e1bde3d52..26400a2fcc 100644 --- a/wagtail/admin/urls/reports.py +++ b/wagtail/admin/urls/reports.py @@ -3,6 +3,9 @@ from django.urls import path from wagtail.admin.views.reports.aging_pages import AgingPagesView from wagtail.admin.views.reports.audit_logging import LogEntriesView from wagtail.admin.views.reports.locked_pages import LockedPagesView +from wagtail.admin.views.reports.page_types_usage import ( + PageTypesUsageReportView, +) from wagtail.admin.views.reports.workflows import WorkflowTasksView, WorkflowView app_name = "wagtailadmin_reports" @@ -12,4 +15,7 @@ urlpatterns = [ path("workflow_tasks/", WorkflowTasksView.as_view(), name="workflow_tasks"), path("site-history/", LogEntriesView.as_view(), name="site_history"), path("aging-pages/", AgingPagesView.as_view(), name="aging_pages"), + path( + "page-types-usage/", PageTypesUsageReportView.as_view(), name="page_types_usage" + ), ] diff --git a/wagtail/admin/views/reports/page_types_usage.py b/wagtail/admin/views/reports/page_types_usage.py new file mode 100644 index 0000000000..fca2cee452 --- /dev/null +++ b/wagtail/admin/views/reports/page_types_usage.py @@ -0,0 +1,152 @@ +import django_filters +from django.conf import settings +from django.core.exceptions import PermissionDenied +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.permission_policies.pages import PagePermissionPolicy + + +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): + template_name = "wagtailadmin/reports/page_types_usage.html" + title = _("Page types usage") + header_icon = "doc-empty-inverse" + filterset_class = PageTypesUsageReportFilterSet + + 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 + + self.filters, 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 + + def dispatch(self, request, *args, **kwargs): + if not PagePermissionPolicy().user_has_any_permission( + request.user, ["add", "change", "publish"] + ): + raise PermissionDenied + return super().dispatch(request, *args, **kwargs) diff --git a/wagtail/admin/wagtail_hooks.py b/wagtail/admin/wagtail_hooks.py index 69bbfc84e7..c6ee7e64ec 100644 --- a/wagtail/admin/wagtail_hooks.py +++ b/wagtail/admin/wagtail_hooks.py @@ -874,6 +874,13 @@ class AgingPagesReportMenuItem(MenuItem): ) +class PageTypesReportMenuItem(MenuItem): + def is_shown(self, request): + return PagePermissionPolicy().user_has_any_permission( + request.user, ["add", "change", "publish"] + ) + + @hooks.register("register_reports_menu_item") def register_locked_pages_menu_item(): return LockedPagesMenuItem( @@ -929,6 +936,17 @@ def register_aging_pages_report_menu_item(): ) +@hooks.register("register_reports_menu_item") +def register_page_types_report_menu_item(): + return PageTypesReportMenuItem( + _("Page types usage"), + reverse("wagtailadmin_reports:page_types_usage"), + name="page-types-usage", + icon_name="doc-empty-inverse", + order=1200, + ) + + @hooks.register("register_admin_menu_item") def register_reports_menu(): return SubmenuMenuItem(