* 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
venv/
.venv/
ENV/
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
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.views.reports import ReportView
from wagtail.coreutils import get_content_languages
from wagtail.models import ContentType, Page, PageLogEntry, get_page_models
from wagtail.users.utils import get_deleted_user_display_name
from wagtailinventory.models import PageBlock
def _get_locale_choices():
@ -46,7 +49,7 @@ class PageTypesReportFilterSet(WagtailFilterSet):
class PageTypesReportView(ReportView):
title = "Page Types Report"
title = "Page types report"
template_name = "pages/reports/page_types_report.html"
header_icon = "doc-empty-inverse"
@ -93,3 +96,44 @@ class PageTypesReportView(ReportView):
queryset = queryset.order_by("-count", "app_label", "model")
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.admin.menu import AdminOnlyMenuItem
from .views import PageTypesReportView
from .views import BlockTypesReportView, PageTypesReportView
@hooks.register("register_reports_menu_item")
def register_page_types_report_menu_item():
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 [
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 %}