* Add block types report

* Annotate content_types to query

* Render page types blocks are used on

* Render block name and status

* Show remaining page types in tooltip

* Update gitignore

* Custom css for report

* Remove not needed !important

* Lint

* Fix malformed HTML

* Rename tag block

* Add tests to tags

* Add more test cases

* Lint

* Lint

* Lint

* Annotate content_types to query

* Render page types blocks are used on

* Show remaining page types in tooltip

* Custom css for report

* Lint

* Rename tag block

* Improve tags code readability

* Better code blocks nomenclature

* Use defaultdict

* Only count live pages
This commit is contained in:
Jhonatan Lopes 2023-09-22 06:03:25 -03:00 коммит произвёл GitHub
Родитель 9aa4d3bcd2
Коммит 1b9a749ff0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 611 добавлений и 4 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -92,6 +92,7 @@ celerybeat-schedule
# virtualenv # virtualenv
venv/ venv/
.venv/
ENV/ ENV/
dockerpythonvenv/ dockerpythonvenv/

Просмотреть файл

@ -0,0 +1,32 @@
.w-block-report-title {
appearance: none;
background: transparent;
border: 0;
padding: 0;
color: inherit;
text-align: start;
line-height: inherit;
}
.w-block-report-tippy {
appearance: none;
background: transparent;
border: 0;
padding: 0;
color: inherit;
text-align: start;
line-height: inherit;
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-thickness: 1px;
text-underline-offset: 1px;
}
.tippy-content a {
color: var(--w-color-white);
text-decoration: dotted;
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-thickness: 1px;
text-underline-offset: 1px;
}

Просмотреть файл

Просмотреть файл

@ -0,0 +1,52 @@
from django import template
from django.urls import reverse
from django.utils.html import format_html, mark_safe
register = template.Library()
@register.simple_tag
def render_content_type(content_type):
"""Render a Content Type with a link to the content type's admin page"""
return format_html(
"<a href='{}'>{}</a>",
reverse(
"wagtailadmin_pages:type_use",
kwargs={
"content_type_app_name": content_type.app_label,
"content_type_model_name": content_type.model,
},
),
content_type.name.title(),
)
@register.simple_tag
def render_content_types(content_types):
"""Render a list of content types"""
return mark_safe(", ".join([render_content_type(content_type) for content_type in content_types]))
@register.inclusion_tag("tags/reports/page_types_block.html")
def page_types_block(content_types):
content_types_hidden = []
count_hidden = 0
if len(content_types) > 3:
content_types_hidden = content_types[3:]
content_types = content_types[:3]
count_hidden = len(content_types_hidden)
return {
"content_types_shown": content_types,
"content_types_hidden": content_types_hidden,
"count_hidden": count_hidden,
}
@register.inclusion_tag("tags/reports/block_name.html")
def block_name(page_block):
full_name = page_block["block"]
short_name = full_name.split(".")[-1]
return {
"full_name": full_name,
"short_name": short_name,
}

Просмотреть файл

@ -0,0 +1,260 @@
from django.contrib.auth import get_user_model
from django.urls import reverse
from factory import Faker
from wagtailinventory.helpers import create_page_inventory, delete_page_inventory
from networkapi.reports.views import BlockTypesReportView
from networkapi.utility.faker import StreamfieldProvider
from networkapi.wagtailpages.factory.campaign_page import CampaignPageFactory
from networkapi.wagtailpages.factory.opportunity import OpportunityPageFactory
from networkapi.wagtailpages.factory.primary_page import PrimaryPageFactory
from networkapi.wagtailpages.tests.base import WagtailpagesTestCase
Faker.add_provider(StreamfieldProvider)
class PatchedPrimaryPageFactory(PrimaryPageFactory):
body = Faker("streamfield", fields=["paragraph", "image", "airtable"])
class PatchedCampaignPageFactory(CampaignPageFactory):
body = Faker("streamfield", fields=["paragraph", "image"])
class PatchedOpportunityPageFactory(OpportunityPageFactory):
body = Faker("streamfield", fields=["paragraph"])
class BlockTypesReportViewTest(WagtailpagesTestCase):
def setUp(self):
super().setUp()
self.view = BlockTypesReportView()
User = get_user_model()
self.user = User.objects.create_superuser("admin-user", "admin@example.com", "password")
self.client.force_login(self.user)
def test_view(self):
"""Tests that the queryset is correct."""
# Create some pages with custom and standard blocks
primary_page = PatchedPrimaryPageFactory(parent=self.homepage)
campaign_page = PatchedCampaignPageFactory(parent=self.homepage)
opportunity_page = PatchedOpportunityPageFactory(parent=self.homepage)
# Update `wagtailinventory`'s index
create_page_inventory(primary_page)
create_page_inventory(campaign_page)
create_page_inventory(opportunity_page)
# Request the view
response = self.client.get(reverse("block_types_report"))
# Get the objects:
object_list = response.context["object_list"]
# The first, most used, should be the RichTextBlock created on all three pages
block = object_list[0]
self.assertEqual(block["block"], "wagtail.blocks.field_block.RichTextBlock")
self.assertEqual(block["count"], 3)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual(
[primary_page.content_type, campaign_page.content_type, opportunity_page.content_type],
block["content_types"],
)
# Two pages have crated ImageBlocks
# Each ImageBlock has a ImageChooserBlock and a CharBlock
# These should be in alphabetical order since they all have the same count
block = object_list[1]
self.assertEqual(
block["block"], "networkapi.wagtailpages.pagemodels.customblocks.annotated_image_block.AnnotatedImageBlock"
)
self.assertEqual(block["count"], 2)
self.assertEqual(block["type_label"], "Custom")
self.assertTrue(block["is_custom_block"])
self.assertListEqual([primary_page.content_type, campaign_page.content_type], block["content_types"])
block = object_list[2]
self.assertEqual(
block["block"], "networkapi.wagtailpages.pagemodels.customblocks.annotated_image_block.RadioSelectBlock"
)
self.assertEqual(block["count"], 2)
self.assertEqual(block["type_label"], "Custom")
self.assertTrue(block["is_custom_block"])
self.assertListEqual([primary_page.content_type, campaign_page.content_type], block["content_types"])
block = object_list[3]
self.assertEqual(block["block"], "wagtail.blocks.field_block.CharBlock")
self.assertEqual(block["count"], 2)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual([primary_page.content_type, campaign_page.content_type], block["content_types"])
block = object_list[4]
self.assertEqual(block["block"], "wagtail.images.blocks.ImageChooserBlock")
self.assertEqual(block["count"], 2)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual([primary_page.content_type, campaign_page.content_type], block["content_types"])
# FInally, we have the AirTableBlock, use only in one page
# This one is made of a URLBlock and a IntegerBlock
block = object_list[5]
self.assertEqual(
block["block"], "networkapi.wagtailpages.pagemodels.customblocks.airtable_block.AirTableBlock"
)
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Custom")
self.assertTrue(block["is_custom_block"])
self.assertListEqual([primary_page.content_type], block["content_types"])
block = object_list[6]
self.assertEqual(block["block"], "wagtail.blocks.field_block.IntegerBlock")
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual([primary_page.content_type], block["content_types"])
block = object_list[7]
self.assertEqual(block["block"], "wagtail.blocks.field_block.URLBlock")
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual([primary_page.content_type], block["content_types"])
def test_page_unpublished(self):
"""Tests that the queryset is updated when a page is unpublished"""
# Create some pages with custom and standard blocks
primary_page = PatchedPrimaryPageFactory(parent=self.homepage)
campaign_page = PatchedCampaignPageFactory(parent=self.homepage)
opportunity_page = PatchedOpportunityPageFactory(parent=self.homepage)
# Update `wagtailinventory`'s index
create_page_inventory(primary_page)
create_page_inventory(campaign_page)
create_page_inventory(opportunity_page)
# Unpublish primary page
primary_page.unpublish()
# Request the view
response = self.client.get(reverse("block_types_report"))
# Get the objects:
object_list = response.context["object_list"]
# The first, most used, should be the RichTextBlock created on the two live pages
block = object_list[0]
self.assertEqual(block["block"], "wagtail.blocks.field_block.RichTextBlock")
self.assertEqual(block["count"], 2)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual(
[campaign_page.content_type, opportunity_page.content_type],
block["content_types"],
)
# Two pages have crated ImageBlocks
# Each ImageBlock has a ImageChooserBlock and a CharBlock
# These should be in alphabetical order since they all have the same count
block = object_list[1]
self.assertEqual(
block["block"], "networkapi.wagtailpages.pagemodels.customblocks.annotated_image_block.AnnotatedImageBlock"
)
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Custom")
self.assertTrue(block["is_custom_block"])
self.assertListEqual([campaign_page.content_type], block["content_types"])
block = object_list[2]
self.assertEqual(
block["block"], "networkapi.wagtailpages.pagemodels.customblocks.annotated_image_block.RadioSelectBlock"
)
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Custom")
self.assertTrue(block["is_custom_block"])
self.assertListEqual([campaign_page.content_type], block["content_types"])
block = object_list[3]
self.assertEqual(block["block"], "wagtail.blocks.field_block.CharBlock")
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual([campaign_page.content_type], block["content_types"])
block = object_list[4]
self.assertEqual(block["block"], "wagtail.images.blocks.ImageChooserBlock")
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual([campaign_page.content_type], block["content_types"])
def test_page_deleted(self):
"""Tests that the queryset is updated when a page is deleted"""
# Create some pages with custom and standard blocks
primary_page = PatchedPrimaryPageFactory(parent=self.homepage)
campaign_page = PatchedCampaignPageFactory(parent=self.homepage)
opportunity_page = PatchedOpportunityPageFactory(parent=self.homepage)
# Update `wagtailinventory`'s index
create_page_inventory(primary_page)
create_page_inventory(campaign_page)
create_page_inventory(opportunity_page)
# Delete primary page
primary_page.delete()
# Update the inventory
delete_page_inventory(primary_page)
# Request the view
response = self.client.get(reverse("block_types_report"))
# Get the objects:
object_list = response.context["object_list"]
# The first, most used, should be the RichTextBlock created on the two live pages
block = object_list[0]
self.assertEqual(block["block"], "wagtail.blocks.field_block.RichTextBlock")
self.assertEqual(block["count"], 2)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual(
[campaign_page.content_type, opportunity_page.content_type],
block["content_types"],
)
# Two pages have crated ImageBlocks
# Each ImageBlock has a ImageChooserBlock and a CharBlock
# These should be in alphabetical order since they all have the same count
block = object_list[1]
self.assertEqual(
block["block"], "networkapi.wagtailpages.pagemodels.customblocks.annotated_image_block.AnnotatedImageBlock"
)
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Custom")
self.assertTrue(block["is_custom_block"])
self.assertListEqual([campaign_page.content_type], block["content_types"])
block = object_list[2]
self.assertEqual(
block["block"], "networkapi.wagtailpages.pagemodels.customblocks.annotated_image_block.RadioSelectBlock"
)
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Custom")
self.assertTrue(block["is_custom_block"])
self.assertListEqual([campaign_page.content_type], block["content_types"])
block = object_list[3]
self.assertEqual(block["block"], "wagtail.blocks.field_block.CharBlock")
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual([campaign_page.content_type], block["content_types"])
block = object_list[4]
self.assertEqual(block["block"], "wagtail.images.blocks.ImageChooserBlock")
self.assertEqual(block["count"], 1)
self.assertEqual(block["type_label"], "Core")
self.assertFalse(block["is_custom_block"])
self.assertListEqual([campaign_page.content_type], block["content_types"])

Просмотреть файл

@ -0,0 +1,108 @@
from django.contrib.contenttypes.models import ContentType
from django.test import SimpleTestCase
from django.urls import reverse
from django.utils.html import format_html, mark_safe
from networkapi.reports.templatetags import report_tags
class RenderContentTypeTagTests(SimpleTestCase):
def test_render_content_type(self):
content_type = ContentType(app_label="myapp", model="mymodel")
expected_link = reverse(
"wagtailadmin_pages:type_use",
kwargs={
"content_type_app_name": content_type.app_label,
"content_type_model_name": content_type.model,
},
)
expected_name = "Mymodel"
result = report_tags.render_content_type(content_type)
self.assertHTMLEqual(result, format_html("<a href='{}'>{}</a>", expected_link, expected_name))
self.assertIsInstance(result, str)
self.assertTrue(mark_safe(result))
class RenderContentTypesTagTests(SimpleTestCase):
def test_render_list_of_content_types(self):
content_types = [
ContentType(app_label="myapp", model="mymodel1"),
ContentType(app_label="myapp", model="mymodel2"),
ContentType(app_label="myapp", model="mymodel3"),
]
expected_link_1 = reverse(
"wagtailadmin_pages:type_use",
kwargs={
"content_type_app_name": content_types[0].app_label,
"content_type_model_name": content_types[0].model,
},
)
expected_link_2 = reverse(
"wagtailadmin_pages:type_use",
kwargs={
"content_type_app_name": content_types[1].app_label,
"content_type_model_name": content_types[1].model,
},
)
expected_link_3 = reverse(
"wagtailadmin_pages:type_use",
kwargs={
"content_type_app_name": content_types[2].app_label,
"content_type_model_name": content_types[2].model,
},
)
expected_html = """
<a href='{}'>{}</a>, <a href='{}'>{}</a>, <a href='{}'>{}</a>
""".format(
expected_link_1,
"Mymodel1",
expected_link_2,
"Mymodel2",
expected_link_3,
"Mymodel3",
)
result = report_tags.render_content_types(content_types)
self.assertHTMLEqual(result, expected_html)
self.assertIsInstance(result, str)
self.assertTrue(mark_safe(result))
class PageTypesBlockTagTests(SimpleTestCase):
def test_page_types_block(self):
content_types = [
ContentType(app_label="myapp", model="mymodel1"),
ContentType(app_label="myapp", model="mymodel2"),
ContentType(app_label="myapp", model="mymodel3"),
ContentType(app_label="myapp", model="mymodel4"),
]
result = report_tags.page_types_block(content_types)
self.assertIsInstance(result, dict)
self.assertIn("content_types_shown", result)
self.assertIn("content_types_hidden", result)
self.assertIn("count_hidden", result)
self.assertEqual(result["content_types_shown"], content_types[:3])
self.assertEqual(result["content_types_hidden"], content_types[3:])
self.assertEqual(result["count_hidden"], 1)
class BlockNameTagTests(SimpleTestCase):
def test_block_name(self):
page_block = {
"block": "myapp.mymodel.MyBlock",
"count": 1,
}
result = report_tags.block_name(page_block)
self.assertIsInstance(result, dict)
self.assertIn("full_name", result)
self.assertIn("short_name", result)
self.assertEqual(result["full_name"], "myapp.mymodel.MyBlock")
self.assertEqual(result["short_name"], "MyBlock")

Просмотреть файл

@ -1,11 +1,14 @@
from collections import defaultdict
import django_filters import django_filters
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db.models import Count, OuterRef, Q, Subquery from django.db.models import BooleanField, Case, Count, OuterRef, Q, Subquery, When
from wagtail.admin.filters import WagtailFilterSet from wagtail.admin.filters import WagtailFilterSet
from wagtail.admin.views.reports import ReportView from wagtail.admin.views.reports import ReportView
from wagtail.coreutils import get_content_languages from wagtail.coreutils import get_content_languages
from wagtail.models import ContentType, Page, PageLogEntry, get_page_models from wagtail.models import ContentType, Page, PageLogEntry, get_page_models
from wagtail.users.utils import get_deleted_user_display_name from wagtail.users.utils import get_deleted_user_display_name
from wagtailinventory.models import PageBlock
def _get_locale_choices(): def _get_locale_choices():
@ -46,7 +49,7 @@ class PageTypesReportFilterSet(WagtailFilterSet):
class PageTypesReportView(ReportView): class PageTypesReportView(ReportView):
title = "Page Types Report" title = "Page types report"
template_name = "pages/reports/page_types_report.html" template_name = "pages/reports/page_types_report.html"
header_icon = "doc-empty-inverse" header_icon = "doc-empty-inverse"
@ -93,3 +96,44 @@ class PageTypesReportView(ReportView):
queryset = queryset.order_by("-count", "app_label", "model") queryset = queryset.order_by("-count", "app_label", "model")
return queryset return queryset
class BlockTypesReportView(ReportView):
title = "Block types report"
template_name = "pages/reports/block_types_report.html"
header_icon = "placeholder"
def decorate_paginated_queryset(self, object_list):
# Build a cache map of PageBlock's block name to content types
page_blocks = PageBlock.objects.all().prefetch_related("page__content_type")
blocks_to_content_types = defaultdict(list)
for page_block in page_blocks:
if page_block.page.live and (
page_block.page.content_type not in blocks_to_content_types[page_block.block]
):
blocks_to_content_types[page_block.block].append(page_block.page.content_type)
# Get the content_types for each block name
for block_report_item in object_list:
content_types = blocks_to_content_types.get(block_report_item["block"], [])
block_report_item["content_types"] = content_types
block_report_item["type_label"] = "Custom" if block_report_item["is_custom_block"] else "Core"
return object_list
def get_queryset(self):
queryset = (
PageBlock.objects.all()
.values("block")
.annotate(
count=Count("page", filter=Q(page__live=True)),
is_custom_block=Case(
When(block__startswith="wagtail.", then=False), default=True, output_field=BooleanField()
),
)
)
self.queryset = queryset
queryset = queryset.order_by("-count", "block")
return queryset

Просмотреть файл

@ -2,13 +2,13 @@ from django.urls import path, reverse
from wagtail import hooks from wagtail import hooks
from wagtail.admin.menu import AdminOnlyMenuItem from wagtail.admin.menu import AdminOnlyMenuItem
from .views import PageTypesReportView from .views import BlockTypesReportView, PageTypesReportView
@hooks.register("register_reports_menu_item") @hooks.register("register_reports_menu_item")
def register_page_types_report_menu_item(): def register_page_types_report_menu_item():
return AdminOnlyMenuItem( return AdminOnlyMenuItem(
"Page Types", reverse("page_types_report"), icon_name=PageTypesReportView.header_icon, order=700 "Page types", reverse("page_types_report"), icon_name=PageTypesReportView.header_icon, order=700
) )
@ -17,3 +17,17 @@ def register_page_types_report_url():
return [ return [
path("reports/page-types-report/", PageTypesReportView.as_view(), name="page_types_report"), path("reports/page-types-report/", PageTypesReportView.as_view(), name="page_types_report"),
] ]
@hooks.register("register_reports_menu_item")
def register_block_types_report_menu_item():
return AdminOnlyMenuItem(
"Block types", reverse("block_types_report"), icon_name=BlockTypesReportView.header_icon, order=701
)
@hooks.register("register_admin_urls")
def register_block_types_report_url():
return [
path("reports/block-types-report/", BlockTypesReportView.as_view(), name="block_types_report"),
]

Просмотреть файл

@ -0,0 +1,22 @@
{% extends "wagtailadmin/reports/base_report.html" %}
{% load wagtailadmin_tags static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/reports.css' %}">
{% endblock extra_css %}
{% block results %}
{% with object_list as block_report_items %}
<div id="page-results">
{% if block_report_items %}
{% block listing %}
{% include "pages/reports/include/_list_block_types.html" %}
{% endblock listing %}
{% else %}
{% block no_results %}
<p>No block types match this report's criteria.</p>
{% endblock no_results %}
{% endif %}
</div>
{% endwith %}
{% endblock results %}

Просмотреть файл

@ -0,0 +1,54 @@
{% load wagtailadmin_tags wagtailcore_tags report_tags %}
<table class="listing {% block table_classname %}{% endblock table_classname %}">
<col width="25%" />
<col width="10%" />
<col width="10%" />
<col />
<thead>
{% block post_parent_page_headers %}
<tr class="table-headers">
<th class="title">
Block
</th>
<th class="app-label">
Pages
</th>
<th class="app-label">
Type
</th>
<th class="app-label">
Used on
</th>
{% block extra_columns %}
{% endblock extra_columns %}
</tr>
{% endblock post_parent_page_headers %}
</thead>
<tbody>
{% if block_report_items %}
{% for block_report_item in block_report_items %}
<tr class="{% block page_row_classname %}{% endblock page_row_classname %}">
<td class="app-label" valign="top">
{% block_name block_report_item %}
</td>
<td class="app-label" valign="top">
{{ block_report_item.count }}
</td>
<td class="app-label" valign="top">
<span class="status-tag status-tag--label">{{ block_report_item.type_label }}</span>
</td>
<td class="app-label" valign="top">
{% page_types_block block_report_item.content_types %}
</td>
{% block extra_page_data %}
{% endblock extra_page_data %}
</tr>
{% endfor %}
{% else %}
{% block no_results %}
<p>No blocks found.</p>
{% endblock no_results %}
{% endif %}
</tbody>
</table>

Просмотреть файл

@ -0,0 +1,5 @@
{% load wagtailcore_tags wagtailadmin_tags i18n %}
<button type="button" class="w-block-report-title" data-tippy-content="{{ full_name }}" data-tippy-maxWidth="50rem">
{{ short_name }}
</button>

Просмотреть файл

@ -0,0 +1,15 @@
{% load report_tags %}
{% render_content_types content_types_shown %}
{% if content_types_hidden %}
and
<button type="button"
class="w-block-report-tippy"
data-tippy-content="{% render_content_types content_types_hidden %}"
data-tippy-allowHTML="true"
data-tippy-interactive="true"
data-tippy-trigger="click"
>
{{ count_hidden }} more
</button>
{% endif %}