kopia lustrzana https://github.com/wagtail/wagtail
Add page types report (#10850)
rodzic
10f387aca0
commit
678dd15852
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
{% load i18n wagtailadmin_tags wagtailcore_tags %}
|
||||
|
||||
<table class="listing {% block table_classname %}{% endblock table_classname %}">
|
||||
<col width="12%" />
|
||||
<col width="12%" />
|
||||
<col width="10%" />
|
||||
<col width="20%" />
|
||||
<col />
|
||||
<thead>
|
||||
{% block post_parent_page_headers %}
|
||||
<tr class="table-headers">
|
||||
<th class="title">
|
||||
{% trans 'Type' %}
|
||||
</th>
|
||||
<th class="app-label">
|
||||
{% trans 'App' %}
|
||||
</th>
|
||||
<th class="count">
|
||||
{% trans 'Pages' %}
|
||||
</th>
|
||||
<th class="last-edited-page">
|
||||
{% trans 'Last edited page' %}
|
||||
</th>
|
||||
<th class="last-edited-at">
|
||||
{% trans 'Last edit' %}
|
||||
</th>
|
||||
{% block extra_columns %}
|
||||
{% endblock extra_columns %}
|
||||
</tr>
|
||||
{% endblock post_parent_page_headers %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if page_types %}
|
||||
{% for page_type in page_types %}
|
||||
<tr class="{% block page_row_classname %}{% endblock page_row_classname %}">
|
||||
<td class="page-type" valign="top" data-listing-page-type>
|
||||
{% block page_type %}
|
||||
{{ page_type.name|title }}
|
||||
{% endblock page_type %}
|
||||
</td>
|
||||
<td class="app-label" valign="top">
|
||||
{{ page_type.app_label }}.{{ page_type.model }}
|
||||
</td>
|
||||
<td class="count" valign="top">
|
||||
{% if page_type.count > 0 %}
|
||||
<a href="{% url 'wagtailadmin_pages:type_use' content_type_app_name=page_type.app_label content_type_model_name=page_type.model %}">{{ page_type.count }}</a>
|
||||
{% else %}
|
||||
{{ page_type.count }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="last-edited-page" valign="top">
|
||||
{% 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 %}
|
||||
<a href="{% url 'wagtailadmin_pages:edit' page_type.last_edited_page.id %}">
|
||||
{{ page_display_title }}
|
||||
</a>
|
||||
{% else %}
|
||||
<p>{{ page_display_title }}</p>
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td class="last-edited-at" valign="top">
|
||||
{% if page_type.last_edited_page.latest_revision_created_at %}
|
||||
{% human_readable_date page_type.last_edited_page.latest_revision_created_at %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
{% block extra_page_data %}
|
||||
{% endblock extra_page_data %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% block no_results %}
|
||||
<p>{% trans "No page types found." %}</p>
|
||||
{% endblock no_results %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "wagtailadmin/reports/base_report.html" %}
|
||||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
{% block results %}
|
||||
{% with object_list as page_types %}
|
||||
<div id="page-results">
|
||||
{% if page_types %}
|
||||
{% block listing %}
|
||||
{% include "wagtailadmin/reports/listing/_list_page_types_usage.html" %}
|
||||
{% endblock listing %}
|
||||
{% else %}
|
||||
{% block no_results %}
|
||||
<p>{% trans "No page types found." %}</p>
|
||||
{% endblock no_results %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endblock results %}
|
|
@ -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"<a href={edit_event_page_url}>{latest_edited_event_page.get_admin_display_title()}</a>",
|
||||
html=True,
|
||||
)
|
||||
# For pages that the user cannot edit, it should only show the page title
|
||||
self.assertContains(
|
||||
response,
|
||||
f"<p>{latest_edited_simple_page.get_admin_display_title()}</p>",
|
||||
html=True,
|
||||
)
|
||||
edit_simple_page_url = reverse(
|
||||
"wagtailadmin_pages:edit", args=(latest_edited_simple_page.id,)
|
||||
)
|
||||
self.assertNotContains(
|
||||
response,
|
||||
f"<a href={edit_simple_page_url}>{latest_edited_simple_page.get_admin_display_title()}</a>",
|
||||
html=True,
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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)
|
|
@ -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(
|
||||
|
|
Ładowanie…
Reference in New Issue