wagtail-developer-reference 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wagtail_developer_reference-0.1.2/wagtail_developer_reference.egg-info → wagtail_developer_reference-0.1.3}/PKG-INFO +8 -7
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/README.md +7 -6
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/pyproject.toml +1 -1
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/setup.py +1 -1
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference/migrations/0001_initial.py +1 -2
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference/templates/detail.html +60 -3
- wagtail_developer_reference-0.1.3/wagtail_developer_reference/templates/index.html +313 -0
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference/wagtail_hooks.py +122 -98
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3/wagtail_developer_reference.egg-info}/PKG-INFO +8 -7
- wagtail_developer_reference-0.1.2/wagtail_developer_reference/templates/index.html +0 -164
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/MANIFEST.in +0 -0
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/setup.cfg +0 -0
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference/__init__.py +0 -0
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference/apps.py +0 -0
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference/migrations/__init__.py +0 -0
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference/models.py +0 -0
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference.egg-info/SOURCES.txt +0 -0
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference.egg-info/dependency_links.txt +0 -0
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference.egg-info/requires.txt +0 -0
- {wagtail_developer_reference-0.1.2 → wagtail_developer_reference-0.1.3}/wagtail_developer_reference.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wagtail-developer-reference
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A visual auditing tool for Wagtail developers to track Page models and StreamField blocks.
|
|
5
5
|
Author-email: Four Digits <info@fourdigits.nl>
|
|
6
6
|
Project-URL: Homepage, https://github.com/fourdigits/wagtail-developer-reference
|
|
@@ -32,15 +32,16 @@ Requires-Dist: pytest-django; extra == "dev"
|
|
|
32
32
|
In large Wagtail projects, it’s easy to lose track of where specific blocks are used or which templates are being rendered. This tool gives you a "God-view" of your components without needing to dig through the database or shell.
|
|
33
33
|
|
|
34
34
|
## Features
|
|
35
|
-
* **
|
|
36
|
-
* **
|
|
37
|
-
* **
|
|
38
|
-
* **
|
|
35
|
+
* **On-demand usage tracking:** Apply filters before calculating page and block usage, keeping the registry quick to open.
|
|
36
|
+
* **Interactive registry:** Combine text, PAGE/BLOCK, site and total-usage-range filters to find exactly the components you need.
|
|
37
|
+
* **Nested block discovery:** Browse visible block labels and full nested definition paths, with safe fallbacks for unlabeled blocks.
|
|
38
|
+
* **Usage inspection:** Search and paginate matching pages, then copy the visible usage table in one click.
|
|
39
|
+
* **Template discovery:** See the template path associated with every component.
|
|
39
40
|
|
|
40
41
|
## Installation
|
|
41
42
|
|
|
42
|
-
Compatibility: Python 3.9+ and Wagtail 4.1 through 7.4 LTS
|
|
43
|
-
itself requires Python 3.10+.
|
|
43
|
+
Compatibility: Python 3.9+ and Wagtail 4.1 through 7.4.2 LTS (the latest
|
|
44
|
+
stable Wagtail release at v0.1.3). Wagtail 7.4 itself requires Python 3.10+.
|
|
44
45
|
|
|
45
46
|
1. Add \`wagtail_developer_reference\` to your \`INSTALLED_APPS\`:
|
|
46
47
|
\`\`\`python
|
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
In large Wagtail projects, it’s easy to lose track of where specific blocks are used or which templates are being rendered. This tool gives you a "God-view" of your components without needing to dig through the database or shell.
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
|
-
* **
|
|
10
|
-
* **
|
|
11
|
-
* **
|
|
12
|
-
* **
|
|
9
|
+
* **On-demand usage tracking:** Apply filters before calculating page and block usage, keeping the registry quick to open.
|
|
10
|
+
* **Interactive registry:** Combine text, PAGE/BLOCK, site and total-usage-range filters to find exactly the components you need.
|
|
11
|
+
* **Nested block discovery:** Browse visible block labels and full nested definition paths, with safe fallbacks for unlabeled blocks.
|
|
12
|
+
* **Usage inspection:** Search and paginate matching pages, then copy the visible usage table in one click.
|
|
13
|
+
* **Template discovery:** See the template path associated with every component.
|
|
13
14
|
|
|
14
15
|
## Installation
|
|
15
16
|
|
|
16
|
-
Compatibility: Python 3.9+ and Wagtail 4.1 through 7.4 LTS
|
|
17
|
-
itself requires Python 3.10+.
|
|
17
|
+
Compatibility: Python 3.9+ and Wagtail 4.1 through 7.4.2 LTS (the latest
|
|
18
|
+
stable Wagtail release at v0.1.3). Wagtail 7.4 itself requires Python 3.10+.
|
|
18
19
|
|
|
19
20
|
1. Add \`wagtail_developer_reference\` to your \`INSTALLED_APPS\`:
|
|
20
21
|
\`\`\`python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "wagtail-developer-reference"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "A visual auditing tool for Wagtail developers to track Page models and StreamField blocks."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{name = "Four Digits", email = "info@fourdigits.nl" }]
|
|
@@ -98,6 +98,25 @@
|
|
|
98
98
|
|
|
99
99
|
.empty-state p { font-size: 1.2rem; }
|
|
100
100
|
|
|
101
|
+
.registry-controls {
|
|
102
|
+
display: flex;
|
|
103
|
+
gap: 1rem;
|
|
104
|
+
margin-bottom: 1.5rem;
|
|
105
|
+
align-items: end;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.registry-controls {
|
|
109
|
+
background: #333; border-left: 0.3125rem solid #dcbd3d; padding: 1rem;
|
|
110
|
+
}
|
|
111
|
+
.registry-controls div:first-of-type { flex: 1; }
|
|
112
|
+
.registry-controls label { color: white; display: block; font-weight: bold; margin-bottom: 0.5rem; }
|
|
113
|
+
.registry-controls input {
|
|
114
|
+
background: #222; border: 0.0625rem solid #dcbd3d; box-sizing: border-box;
|
|
115
|
+
color: white; min-height: 2.5rem; padding: 0.6rem; width: 100%;
|
|
116
|
+
}
|
|
117
|
+
.usage-table-actions { display: flex; justify-content: flex-end; margin-top: 1rem; }
|
|
118
|
+
.copy-table-button { border-radius: 0; }
|
|
119
|
+
|
|
101
120
|
[data-wagtail-theme="dark"] .listing tbody tr:hover {
|
|
102
121
|
background-color: #333 !important;
|
|
103
122
|
}
|
|
@@ -111,11 +130,21 @@
|
|
|
111
130
|
<a href="{% url 'dev_reference' %}">← Back to System Registry</a>
|
|
112
131
|
</div>
|
|
113
132
|
<h1>Locations for: <strong>{{ component_name }}</strong></h1>
|
|
114
|
-
<p>
|
|
133
|
+
<p>{{ page_obj.paginator.count }} location{{ page_obj.paginator.count|pluralize }} found</p>
|
|
115
134
|
</div>
|
|
116
135
|
</header>
|
|
117
136
|
|
|
118
137
|
<div class="nice-padding">
|
|
138
|
+
<form method="get" class="registry-controls" role="search">
|
|
139
|
+
<input type="hidden" name="site" value="{{ site_filter }}">
|
|
140
|
+
<div>
|
|
141
|
+
<label for="usage-search">Search pages</label>
|
|
142
|
+
<input id="usage-search" type="search" name="q" value="{{ search_query }}" placeholder="Filter by page title or slug...">
|
|
143
|
+
</div>
|
|
144
|
+
<div>
|
|
145
|
+
<button type="submit" class="button">Search</button>
|
|
146
|
+
</div>
|
|
147
|
+
</form>
|
|
119
148
|
<table class="listing" id="usage-table">
|
|
120
149
|
<thead>
|
|
121
150
|
<tr>
|
|
@@ -151,12 +180,38 @@
|
|
|
151
180
|
{% endfor %}
|
|
152
181
|
</tbody>
|
|
153
182
|
</table>
|
|
183
|
+
<div class="usage-table-actions">
|
|
184
|
+
<button id="copy-usage-table" type="button" class="button button-secondary copy-table-button">Copy displayed table</button>
|
|
185
|
+
</div>
|
|
186
|
+
{% if page_obj.paginator.num_pages > 1 %}
|
|
187
|
+
<nav class="pagination" aria-label="Usage result pages">
|
|
188
|
+
{% if page_obj.has_previous %}
|
|
189
|
+
<a class="button" href="?site={{ site_filter|urlencode }}&q={{ search_query|urlencode }}&page={{ page_obj.previous_page_number }}">Previous</a>
|
|
190
|
+
{% endif %}
|
|
191
|
+
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
|
192
|
+
{% if page_obj.has_next %}
|
|
193
|
+
<a class="button" href="?site={{ site_filter|urlencode }}&q={{ search_query|urlencode }}&page={{ page_obj.next_page_number }}">Next</a>
|
|
194
|
+
{% endif %}
|
|
195
|
+
</nav>
|
|
196
|
+
{% endif %}
|
|
154
197
|
</div>
|
|
155
198
|
|
|
156
199
|
<script>
|
|
157
200
|
let currentSortCol = -1;
|
|
158
201
|
let sortAsc = true;
|
|
159
202
|
|
|
203
|
+
function copyUsageTable() {
|
|
204
|
+
const headers = ["Page Title", "Status", "URL Path"];
|
|
205
|
+
const rows = Array.from(document.querySelectorAll("#usage-body .usage-row"));
|
|
206
|
+
const values = rows.map(row => [0, 1, 2].map(index => row.cells[index].innerText.trim().replace(/\s+/g, " ")));
|
|
207
|
+
const text = [headers, ...values].map(row => row.join("\t")).join("\n");
|
|
208
|
+
const button = document.getElementById("copy-usage-table");
|
|
209
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
210
|
+
button.textContent = "Copied";
|
|
211
|
+
window.setTimeout(() => { button.textContent = "Copy displayed table"; }, 1500);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
160
215
|
function sortTable(colIdx) {
|
|
161
216
|
const tbody = document.getElementById("usage-body");
|
|
162
217
|
const rows = Array.from(tbody.querySelectorAll(".usage-row"));
|
|
@@ -173,12 +228,14 @@
|
|
|
173
228
|
if (colIdx === 1) {
|
|
174
229
|
valA = parseInt(a.cells[colIdx].getAttribute('data-sort'));
|
|
175
230
|
valB = parseInt(b.cells[colIdx].getAttribute('data-sort'));
|
|
176
|
-
return sortAsc ? valA - valB : valB -
|
|
231
|
+
return sortAsc ? valA - valB : valB - valA;
|
|
177
232
|
}
|
|
178
233
|
valA = a.cells[colIdx].textContent.trim().toLowerCase();
|
|
179
234
|
valB = b.cells[colIdx].textContent.trim().toLowerCase();
|
|
180
235
|
return sortAsc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
181
236
|
}).forEach(row => tbody.appendChild(row));
|
|
182
237
|
}
|
|
238
|
+
|
|
239
|
+
document.getElementById("copy-usage-table").addEventListener("click", copyUsageTable);
|
|
183
240
|
</script>
|
|
184
|
-
{% endblock %}
|
|
241
|
+
{% endblock %}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
{% extends "wagtailadmin/base.html" %}
|
|
2
|
+
{% block titletag %}System Registry{% endblock %}
|
|
3
|
+
|
|
4
|
+
{% block extra_css %}
|
|
5
|
+
<style>
|
|
6
|
+
.registry-controls {
|
|
7
|
+
display: flex; gap: 1rem; margin-bottom: 1.5rem;
|
|
8
|
+
background-color: #4a4a4a; padding: 1.25rem; border-left: 5px solid #dcbd3d;
|
|
9
|
+
align-items: end;
|
|
10
|
+
}
|
|
11
|
+
.registry-controls { flex-wrap: wrap; }
|
|
12
|
+
.registry-controls div { flex: 1 1 10rem; }
|
|
13
|
+
.registry-controls .registry-search { flex: 3 1 20rem; }
|
|
14
|
+
.registry-controls .usage-range { flex: 0 1 6rem; }
|
|
15
|
+
.registry-controls .filter-actions {
|
|
16
|
+
align-items: end; display: flex; flex: 0 0 auto; gap: 0.5rem;
|
|
17
|
+
}
|
|
18
|
+
.registry-controls .filter-actions .button { margin: 0; white-space: nowrap; }
|
|
19
|
+
.quick-search {
|
|
20
|
+
background: #333; border-left: 5px solid #dcbd3d; margin-bottom: 1rem;
|
|
21
|
+
}
|
|
22
|
+
.quick-search summary {
|
|
23
|
+
color: white; cursor: pointer; font-weight: bold; padding: 0.9rem 1.25rem;
|
|
24
|
+
}
|
|
25
|
+
.quick-search-filter {
|
|
26
|
+
box-sizing: border-box; margin: 0 1.25rem 0.75rem; max-width: 34rem;
|
|
27
|
+
min-height: 2.5rem; padding: 0.5rem; width: calc(100% - 2.5rem);
|
|
28
|
+
}
|
|
29
|
+
.quick-search-status { color: #ccc; display: block; font-size: 0.85rem; padding: 0 1.25rem 0.75rem; }
|
|
30
|
+
.quick-search-list {
|
|
31
|
+
display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0 1.25rem 1.25rem;
|
|
32
|
+
}
|
|
33
|
+
.quick-search-item {
|
|
34
|
+
background: #222; border: 1px solid #777; color: white; cursor: pointer;
|
|
35
|
+
font: inherit; padding: 0.35rem 0.5rem; text-align: left;
|
|
36
|
+
}
|
|
37
|
+
.quick-search-item:hover, .quick-search-item:focus {
|
|
38
|
+
border-color: #dcbd3d; color: #dcbd3d;
|
|
39
|
+
}
|
|
40
|
+
.quick-search-item .type-tag { margin-right: 0.35rem; }
|
|
41
|
+
.registry-controls label { display: block; font-weight: bold; color: white; margin-bottom: 0.5rem; }
|
|
42
|
+
.registry-controls input, .registry-controls select {
|
|
43
|
+
box-sizing: border-box; height: 2.5rem; width: 100%;
|
|
44
|
+
border: 1px solid #dcbd3d; background: #333; color: white; border-radius: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.listing thead tr { border-bottom: 2px solid #dcbd3d; }
|
|
48
|
+
.listing tbody td { vertical-align: top; }
|
|
49
|
+
.col-centered { text-align: center; width: 5rem; }
|
|
50
|
+
.col-site { width: 10rem; }
|
|
51
|
+
.col-type { width: 8rem; white-space: nowrap; }
|
|
52
|
+
|
|
53
|
+
.sortable { cursor: pointer; position: relative; transition: background 0.2s; }
|
|
54
|
+
.sortable:hover { background: #444; }
|
|
55
|
+
.sortable::after {
|
|
56
|
+
content: '↕';
|
|
57
|
+
font-size: 0.8rem;
|
|
58
|
+
margin-left: 5px;
|
|
59
|
+
opacity: 0.3;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.type-tag {
|
|
63
|
+
background: #222; color: #dcbd3d; border: 1px solid #dcbd3d;
|
|
64
|
+
padding: 2px 10px; text-transform: uppercase; font-size: 0.7rem;
|
|
65
|
+
display: inline-block; white-space: nowrap;
|
|
66
|
+
cursor: default; pointer-events: none;
|
|
67
|
+
}
|
|
68
|
+
.type-tag--page { background: #153a5b; border-color: #5db6ff; color: #b9e2ff; }
|
|
69
|
+
.type-tag--block { background: #3c2459; border-color: #bf8cff; color: #e2c8ff; }
|
|
70
|
+
|
|
71
|
+
.field-pill {
|
|
72
|
+
background: #222; border: 1px solid #dcbd3d; padding: 2px 6px;
|
|
73
|
+
display: inline-block; font-size: 0.75rem; white-space: nowrap;
|
|
74
|
+
}
|
|
75
|
+
.field-pill--metadata { border-color: #777; background: #333; }
|
|
76
|
+
.field-type { color: #aaa; font-size: 0.8em; }
|
|
77
|
+
|
|
78
|
+
.field-toggle {
|
|
79
|
+
background: #222;
|
|
80
|
+
border: 1px solid #dcbd3d;
|
|
81
|
+
color: #dcbd3d;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
font-size: 0.75rem;
|
|
84
|
+
padding: 0.3rem 0.5rem;
|
|
85
|
+
text-align: left;
|
|
86
|
+
}
|
|
87
|
+
.field-toggle:hover { background: #333; }
|
|
88
|
+
.field-toggle::before { content: "▸"; margin-right: 0.4rem; }
|
|
89
|
+
.field-toggle[aria-expanded="true"]::before { content: "▾"; }
|
|
90
|
+
.field-cell { position: relative; }
|
|
91
|
+
.field-details {
|
|
92
|
+
align-items: flex-start; background: #222; border: 1px solid #dcbd3d;
|
|
93
|
+
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35); display: flex;
|
|
94
|
+
flex-wrap: wrap; gap: 0.25rem; left: 0; margin-top: 0.5rem;
|
|
95
|
+
max-width: 36rem; min-width: 20rem; padding: 0.5rem; position: absolute;
|
|
96
|
+
top: 100%; z-index: 10;
|
|
97
|
+
}
|
|
98
|
+
.field-details[hidden] { display: none; }
|
|
99
|
+
|
|
100
|
+
.count-draft { color: #dcbd3d; font-weight: bold; }
|
|
101
|
+
.count-live { color: #1fb53a; font-weight: bold; }
|
|
102
|
+
.zero-usage { color: #af7f7f; opacity: 0.5; }
|
|
103
|
+
|
|
104
|
+
.usage-link { background: #dcbd3d !important; color: #222 !important; font-weight: bold; border-radius: 0; }
|
|
105
|
+
.template-label { color: #ccc; font-family: monospace; font-size: 0.7rem; }
|
|
106
|
+
.template-copy { margin-left: 0.35rem; padding: 0.15rem 0.35rem; }
|
|
107
|
+
.template-empty { color: #aaa; }
|
|
108
|
+
|
|
109
|
+
[data-wagtail-theme="dark"] .listing tbody tr:hover {
|
|
110
|
+
background-color: #333 !important;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
113
|
+
{% endblock %}
|
|
114
|
+
|
|
115
|
+
{% block content %}
|
|
116
|
+
{% include "wagtailadmin/shared/header.html" with title="System Registry" icon="code" %}
|
|
117
|
+
<div class="nice-padding">
|
|
118
|
+
<details class="quick-search">
|
|
119
|
+
<summary>Browse pages and blocks</summary>
|
|
120
|
+
<input id="quick-search-filter" class="quick-search-filter" type="search" placeholder="Search pages and blocks..." autocomplete="off">
|
|
121
|
+
<span id="quick-search-status" class="quick-search-status" aria-live="polite"></span>
|
|
122
|
+
<div class="quick-search-list" aria-label="Quick registry search">
|
|
123
|
+
{% for component_type, component_name, display_name in quick_search_items %}
|
|
124
|
+
<button type="button" class="quick-search-item" data-search="{{ component_name }}" data-type="{{ component_type }}" data-quick-search="{{ component_name }} {{ display_name }}" data-quick-index="{{ forloop.counter0 }}">
|
|
125
|
+
<span class="type-tag type-tag--{{ component_type|lower }}">{{ component_type }}</span>{{ display_name }}
|
|
126
|
+
</button>
|
|
127
|
+
{% endfor %}
|
|
128
|
+
</div>
|
|
129
|
+
</details>
|
|
130
|
+
<form id="registry-filter-form" method="get" class="registry-controls">
|
|
131
|
+
<input type="hidden" name="apply" value="1">
|
|
132
|
+
<div class="registry-search"><label for="reg-search">Search Registry</label><input type="search" id="reg-search" name="q" value="{{ filters.q }}" placeholder="Name, field or site..."></div>
|
|
133
|
+
<div><label for="reg-filter">Component Type</label><select id="reg-filter" name="type"><option value="ALL" {% if filters.type == "ALL" %}selected{% endif %}>All</option><option value="PAGE" {% if filters.type == "PAGE" %}selected{% endif %}>Pages</option><option value="BLOCK" {% if filters.type == "BLOCK" %}selected{% endif %}>Blocks</option></select></div>
|
|
134
|
+
<div><label for="reg-site-filter">Site</label><select id="reg-site-filter" name="site"><option value="">All sites</option>{% for site in sites %}<option value="{{ site.id }}" {% if filters.site == site.id|stringformat:"s" %}selected{% endif %}>{{ site.site_name }}</option>{% endfor %}</select></div>
|
|
135
|
+
<div class="usage-range"><label for="min-usage">Min usage</label><input id="min-usage" type="number" min="0" name="min_usage" value="{{ filters.min_usage }}"></div>
|
|
136
|
+
<div class="usage-range"><label for="max-usage">Max usage</label><input id="max-usage" type="number" min="0" name="max_usage" value="{{ filters.max_usage }}"></div>
|
|
137
|
+
<div class="filter-actions"><button type="submit" class="button">Apply Filters</button><a class="button button-secondary" href="{% url 'dev_reference' %}">Clear</a></div>
|
|
138
|
+
</form>
|
|
139
|
+
{% if filter_error %}<p class="error-message">{{ filter_error }}</p>{% endif %}
|
|
140
|
+
{% if is_applied %}
|
|
141
|
+
<table class="listing" id="registry-table">
|
|
142
|
+
<thead>
|
|
143
|
+
<tr>
|
|
144
|
+
<th class="sortable col-site" onclick="sortTable(0)">Site</th>
|
|
145
|
+
<th class="sortable" onclick="sortTable(1)">Hierarchy</th>
|
|
146
|
+
<th class="sortable col-type" onclick="sortTable(2)">Type</th>
|
|
147
|
+
<th class="sortable" onclick="sortTable(3)">Structure & Metadata</th>
|
|
148
|
+
<th class="sortable col-centered" onclick="sortTable(4)">Draft</th>
|
|
149
|
+
<th class="sortable col-centered" onclick="sortTable(5)">Live</th>
|
|
150
|
+
<th class="sortable col-centered" onclick="sortTable(6)">Total</th>
|
|
151
|
+
<th class="sortable" onclick="sortTable(7)">Template Path</th>
|
|
152
|
+
</tr>
|
|
153
|
+
</thead>
|
|
154
|
+
<tbody id="registry-body">
|
|
155
|
+
{% for item in audit_data %}
|
|
156
|
+
<tr class="registry-row" data-type="{{ item.type|upper }}" data-site="{{ item.site_id }}">
|
|
157
|
+
<td class="col-site">{{ item.site_name }}</td>
|
|
158
|
+
<td><strong>{{ item.hierarchy }}</strong></td>
|
|
159
|
+
<td class="col-type"><span class="type-tag type-tag--{{ item.type|lower }}">{{ item.type }}</span></td>
|
|
160
|
+
<td class="field-cell">
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
class="field-toggle"
|
|
164
|
+
aria-expanded="false"
|
|
165
|
+
aria-controls="field-details-{{ forloop.counter }}"
|
|
166
|
+
>
|
|
167
|
+
S: {{ item.fields.structure|length }} · M: {{ item.fields.metadata|length }}
|
|
168
|
+
</button>
|
|
169
|
+
<div id="field-details-{{ forloop.counter }}" class="field-details" hidden>
|
|
170
|
+
{% for field_info in item.fields.structure %}
|
|
171
|
+
<div class="field-pill">
|
|
172
|
+
<code>{{ field_info.label }} <small class="field-type">({{ field_info.type }})</small></code>
|
|
173
|
+
</div>
|
|
174
|
+
{% endfor %}
|
|
175
|
+
{% for field_info in item.fields.metadata %}
|
|
176
|
+
<div class="field-pill field-pill--metadata">
|
|
177
|
+
<code>{{ field_info.label }} <small class="field-type">({{ field_info.type }})</small></code>
|
|
178
|
+
</div>
|
|
179
|
+
{% endfor %}
|
|
180
|
+
</div>
|
|
181
|
+
</td>
|
|
182
|
+
<td class="col-centered" data-sort="{{ item.usage.draft }}">
|
|
183
|
+
<span class="{% if item.usage.draft > 0 %}count-draft{% else %}zero-usage{% endif %}">{{ item.usage.draft }}</span>
|
|
184
|
+
</td>
|
|
185
|
+
<td class="col-centered" data-sort="{{ item.usage.live }}">
|
|
186
|
+
<span class="{% if item.usage.live > 0 %}count-live{% else %}zero-usage{% endif %}">{{ item.usage.live }}</span>
|
|
187
|
+
</td>
|
|
188
|
+
<td class="col-centered" data-sort="{{ item.usage.total }}">
|
|
189
|
+
{% if item.usage.total > 0 %}
|
|
190
|
+
<a href="{% url 'dev_usage_detail' item.type item.clean_name %}?site={{ item.site_id }}" class="button button-small usage-link">
|
|
191
|
+
{{ item.usage.total }}
|
|
192
|
+
</a>
|
|
193
|
+
{% else %}
|
|
194
|
+
<span class="zero-usage">0</span>
|
|
195
|
+
{% endif %}
|
|
196
|
+
</td>
|
|
197
|
+
<td>
|
|
198
|
+
{% if item.template %}
|
|
199
|
+
<span class="template-label" title="{{ item.template }}">{{ item.template_label }}</span>
|
|
200
|
+
<button type="button" class="template-copy" data-template="{{ item.template }}">Copy</button>
|
|
201
|
+
{% else %}
|
|
202
|
+
<span class="template-empty">—</span>
|
|
203
|
+
{% endif %}
|
|
204
|
+
</td>
|
|
205
|
+
</tr>
|
|
206
|
+
{% endfor %}
|
|
207
|
+
</tbody>
|
|
208
|
+
</table>
|
|
209
|
+
{% else %}
|
|
210
|
+
<p class="help-block">Choose filters and select <strong>Apply Filters</strong> to load registry usage.</p>
|
|
211
|
+
{% endif %}
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<script>
|
|
215
|
+
let currentSortColumn = -1;
|
|
216
|
+
let isSortAscending = true;
|
|
217
|
+
|
|
218
|
+
function sortTable(columnIndex) {
|
|
219
|
+
const tableBody = document.getElementById("registry-body");
|
|
220
|
+
const tableRows = Array.from(tableBody.querySelectorAll(".registry-row"));
|
|
221
|
+
|
|
222
|
+
isSortAscending = (currentSortColumn === columnIndex) ? !isSortAscending : true;
|
|
223
|
+
currentSortColumn = columnIndex;
|
|
224
|
+
|
|
225
|
+
tableRows.sort((rowA, rowB) => {
|
|
226
|
+
let cellA = rowA.cells[columnIndex];
|
|
227
|
+
let cellB = rowB.cells[columnIndex];
|
|
228
|
+
let valueA = cellA.getAttribute('data-sort') || cellA.textContent.trim().toLowerCase();
|
|
229
|
+
let valueB = cellB.getAttribute('data-sort') || cellB.textContent.trim().toLowerCase();
|
|
230
|
+
|
|
231
|
+
if (!isNaN(parseFloat(valueA)) && isFinite(valueA) && !isNaN(parseFloat(valueB)) && isFinite(valueB)) {
|
|
232
|
+
return isSortAscending ? valueA - valueB : valueB - valueA;
|
|
233
|
+
}
|
|
234
|
+
return isSortAscending ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
|
|
235
|
+
}).forEach(sortedRow => tableBody.appendChild(sortedRow));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function runFilters() {
|
|
239
|
+
const searchTerm = document.getElementById("reg-search").value.toLowerCase();
|
|
240
|
+
const typeFilter = document.getElementById("reg-filter").value;
|
|
241
|
+
const siteFilter = document.getElementById("reg-site-filter").value;
|
|
242
|
+
document.querySelectorAll(".registry-row").forEach(row => {
|
|
243
|
+
const textMatches = row.textContent.toLowerCase().includes(searchTerm);
|
|
244
|
+
const typeMatches = typeFilter === "ALL" || row.dataset.type === typeFilter;
|
|
245
|
+
const siteMatches = siteFilter === "ALL" || row.dataset.site === siteFilter;
|
|
246
|
+
row.style.display = textMatches && typeMatches && siteMatches ? "" : "none";
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
document.querySelectorAll(".field-toggle").forEach(button => {
|
|
251
|
+
button.addEventListener("click", () => {
|
|
252
|
+
const details = document.getElementById(button.getAttribute("aria-controls"));
|
|
253
|
+
const isExpanded = button.getAttribute("aria-expanded") === "true";
|
|
254
|
+
document.querySelectorAll(".field-toggle").forEach(otherButton => {
|
|
255
|
+
otherButton.setAttribute("aria-expanded", "false");
|
|
256
|
+
document.getElementById(otherButton.getAttribute("aria-controls")).hidden = true;
|
|
257
|
+
});
|
|
258
|
+
if (!isExpanded) {
|
|
259
|
+
button.setAttribute("aria-expanded", "true");
|
|
260
|
+
details.hidden = false;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
document.querySelectorAll(".template-copy").forEach(button => {
|
|
266
|
+
button.addEventListener("click", () => {
|
|
267
|
+
navigator.clipboard.writeText(button.dataset.template).then(() => {
|
|
268
|
+
button.textContent = "Copied";
|
|
269
|
+
window.setTimeout(() => { button.textContent = "Copy"; }, 1500);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const quickSearchInput = document.getElementById("quick-search-filter");
|
|
275
|
+
const quickSearchStatus = document.getElementById("quick-search-status");
|
|
276
|
+
const quickSearchItems = Array.from(document.querySelectorAll(".quick-search-item"));
|
|
277
|
+
const initialQuickSearchLimit = 30;
|
|
278
|
+
|
|
279
|
+
function runQuickSearch() {
|
|
280
|
+
const query = quickSearchInput.value.trim().toLowerCase();
|
|
281
|
+
let matches = 0;
|
|
282
|
+
quickSearchItems.forEach(button => {
|
|
283
|
+
const matchesQuery = button.dataset.quickSearch.toLowerCase().includes(query);
|
|
284
|
+
const withinInitialLimit = query || Number(button.dataset.quickIndex) < initialQuickSearchLimit;
|
|
285
|
+
const isVisible = matchesQuery && withinInitialLimit;
|
|
286
|
+
button.hidden = !isVisible;
|
|
287
|
+
if (matchesQuery) matches += 1;
|
|
288
|
+
});
|
|
289
|
+
quickSearchStatus.textContent = query
|
|
290
|
+
? `${matches} matching component${matches === 1 ? "" : "s"}`
|
|
291
|
+
: `Showing the first ${Math.min(initialQuickSearchLimit, matches)} of ${matches} components. Type to narrow the list.`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
document.querySelectorAll(".quick-search-item").forEach(button => {
|
|
295
|
+
button.addEventListener("click", () => {
|
|
296
|
+
document.getElementById("reg-search").value = button.dataset.search;
|
|
297
|
+
document.getElementById("reg-filter").value = button.dataset.type;
|
|
298
|
+
document.getElementById("registry-filter-form").submit();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
quickSearchInput.addEventListener("input", runQuickSearch);
|
|
302
|
+
document.querySelector(".quick-search").addEventListener("toggle", event => {
|
|
303
|
+
if (event.target.open) quickSearchInput.focus();
|
|
304
|
+
});
|
|
305
|
+
runQuickSearch();
|
|
306
|
+
|
|
307
|
+
if (document.getElementById("registry-table")) {
|
|
308
|
+
document.getElementById("reg-search").addEventListener("input", runFilters);
|
|
309
|
+
document.getElementById("reg-filter").addEventListener("change", runFilters);
|
|
310
|
+
document.getElementById("reg-site-filter").addEventListener("change", runFilters);
|
|
311
|
+
}
|
|
312
|
+
</script>
|
|
313
|
+
{% endblock %}
|
|
@@ -1,25 +1,24 @@
|
|
|
1
|
+
# ruff: noqa: E501
|
|
1
2
|
from __future__ import annotations
|
|
2
3
|
|
|
3
4
|
import inspect
|
|
4
5
|
from collections.abc import Iterable
|
|
5
|
-
from
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Literal, NotRequired, TypedDict
|
|
6
8
|
|
|
7
9
|
from django.contrib.auth.decorators import user_passes_test
|
|
8
10
|
from django.core.exceptions import PermissionDenied
|
|
9
|
-
from django.
|
|
11
|
+
from django.core.paginator import Paginator
|
|
12
|
+
from django.db.models import Field, Q
|
|
10
13
|
from django.http import HttpRequest, HttpResponse
|
|
11
14
|
from django.shortcuts import render
|
|
12
15
|
from django.template.loader import get_template
|
|
13
|
-
from django.urls import path
|
|
14
|
-
from django.urls import reverse
|
|
16
|
+
from django.urls import path, reverse
|
|
15
17
|
from django.urls.resolvers import URLPattern
|
|
16
18
|
from wagtail import hooks
|
|
17
19
|
from wagtail.admin.menu import MenuItem
|
|
18
|
-
from wagtail.blocks import StructBlock
|
|
19
20
|
from wagtail.fields import StreamField
|
|
20
|
-
from wagtail.models import Page
|
|
21
|
-
from wagtail.models import Site, get_page_models
|
|
22
|
-
|
|
21
|
+
from wagtail.models import Page, Site, get_page_models
|
|
23
22
|
|
|
24
23
|
ACCESS_PERMISSION_APP_LABEL: str = "wagtail_developer_reference"
|
|
25
24
|
ACCESS_PERMISSION_CODENAME: str = "access_developer_reference"
|
|
@@ -34,19 +33,27 @@ class UsageData(TypedDict):
|
|
|
34
33
|
draft: int
|
|
35
34
|
|
|
36
35
|
|
|
36
|
+
class FieldInfo(TypedDict):
|
|
37
|
+
label: str
|
|
38
|
+
type: str
|
|
39
|
+
block_name: NotRequired[str | None]
|
|
40
|
+
|
|
41
|
+
|
|
37
42
|
class FieldGroups(TypedDict):
|
|
38
|
-
structure: list[
|
|
39
|
-
metadata: list[
|
|
43
|
+
structure: list[FieldInfo]
|
|
44
|
+
metadata: list[FieldInfo]
|
|
40
45
|
|
|
41
46
|
|
|
42
47
|
class AuditItem(TypedDict):
|
|
48
|
+
site_id: int
|
|
43
49
|
site_name: str
|
|
44
50
|
hierarchy: str
|
|
45
51
|
clean_name: str
|
|
46
52
|
type: ComponentType
|
|
47
53
|
fields: FieldGroups
|
|
48
54
|
usage: UsageData
|
|
49
|
-
template: str
|
|
55
|
+
template: str | None
|
|
56
|
+
template_label: str | None
|
|
50
57
|
|
|
51
58
|
|
|
52
59
|
def user_can_access_developer_reference(user: Any) -> bool:
|
|
@@ -99,38 +106,35 @@ class WagtailAuditor:
|
|
|
99
106
|
"latest_revision_created_at",
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
def get_template_path(self, template_name: str | None) -> str:
|
|
109
|
+
def get_template_path(self, template_name: str | None) -> str | None:
|
|
103
110
|
if not template_name:
|
|
104
|
-
return
|
|
111
|
+
return None
|
|
105
112
|
try:
|
|
106
113
|
return get_template(template_name).origin.name
|
|
107
114
|
except Exception:
|
|
108
|
-
return
|
|
115
|
+
return template_name
|
|
109
116
|
|
|
110
|
-
def
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
def get_template_label(self, template_path: str | None) -> str | None:
|
|
118
|
+
return Path(template_path).name if template_path else None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _analyze_model_field(self, field: Field[Any, Any]) -> tuple[FieldInfo | None, FieldCategory | None]:
|
|
122
|
+
if field.name in self.excluded_fields or (field.is_relation and not field.many_to_one):
|
|
115
123
|
return None, None
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
label = str(label).capitalize()
|
|
120
|
-
field_type = type(field).__name__
|
|
121
|
-
is_structure = "StreamField" in field_type or "RichTextField" in field_type
|
|
122
|
-
category = "structure" if is_structure else "metadata"
|
|
123
|
-
return f"{label} ({field_type})", category
|
|
124
|
-
return None, None
|
|
124
|
+
field_type = type(field).__name__
|
|
125
|
+
category: FieldCategory = "structure" if isinstance(field, StreamField) or "RichTextField" in field_type else "metadata"
|
|
126
|
+
return {"label": str(getattr(field, "verbose_name", field.name)).capitalize(), "type": field_type}, category
|
|
125
127
|
|
|
126
128
|
def get_internal_structure(self, object_instance: Any) -> FieldGroups:
|
|
127
|
-
structure_fields: list[
|
|
128
|
-
metadata_fields: list[
|
|
129
|
+
structure_fields: list[FieldInfo] = []
|
|
130
|
+
metadata_fields: list[FieldInfo] = []
|
|
129
131
|
|
|
130
132
|
if hasattr(object_instance, "child_blocks"):
|
|
131
133
|
for name, block in object_instance.child_blocks.items():
|
|
132
|
-
label = getattr(block.meta, "label", name
|
|
133
|
-
structure_fields.append(
|
|
134
|
+
label = getattr(block.meta, "label", None) or name
|
|
135
|
+
structure_fields.append(
|
|
136
|
+
{"label": str(label), "type": type(block).__name__}
|
|
137
|
+
)
|
|
134
138
|
elif hasattr(object_instance, "_meta"):
|
|
135
139
|
for field in object_instance._meta.get_fields():
|
|
136
140
|
info, category = self._analyze_model_field(field)
|
|
@@ -172,64 +176,44 @@ class WagtailAuditor:
|
|
|
172
176
|
"draft": total_count - live_count,
|
|
173
177
|
}
|
|
174
178
|
|
|
175
|
-
def process_block(
|
|
176
|
-
self,
|
|
177
|
-
block_name: str,
|
|
178
|
-
block_object: Any,
|
|
179
|
-
parent_label: str,
|
|
180
|
-
site: Site,
|
|
181
|
-
) -> None:
|
|
179
|
+
def process_block(self, block_name: str, block_object: Any, parent_label: str, site: Site, include_usage: bool = False) -> None:
|
|
182
180
|
is_class = inspect.isclass(block_object)
|
|
183
|
-
block_class =
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
signature = f"{site.id}_{block_class}_{block_name}"
|
|
181
|
+
block_class = block_object.__name__ if is_class else block_object.__class__.__name__
|
|
182
|
+
signature = f"{site.id}_{parent_label}_{block_class}_{block_name}"
|
|
187
183
|
if signature in self.seen_blocks:
|
|
188
184
|
return
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
block_label
|
|
198
|
-
self.results.append(
|
|
199
|
-
{
|
|
200
|
-
"site_name": str(site),
|
|
201
|
-
"hierarchy": f"{parent_label} → {block_label}",
|
|
202
|
-
"clean_name": block_name,
|
|
203
|
-
"type": "BLOCK",
|
|
204
|
-
"fields": self.get_internal_structure(block_object),
|
|
205
|
-
"usage": site_usage,
|
|
206
|
-
"template": self.get_template_path(
|
|
207
|
-
getattr(block_object.meta, "template", None)
|
|
208
|
-
),
|
|
209
|
-
}
|
|
210
|
-
)
|
|
185
|
+
block_label = str(getattr(block_object.meta, "label", None) or block_name)
|
|
186
|
+
template_path = self.get_template_path(getattr(block_object.meta, "template", None))
|
|
187
|
+
usage = {"live": 0, "total": 0, "draft": 0}
|
|
188
|
+
if include_usage:
|
|
189
|
+
for model in self.models_with_streamfields:
|
|
190
|
+
model_usage = self._get_usage_for_site(model, site, block_name)
|
|
191
|
+
for key in usage:
|
|
192
|
+
usage[key] += model_usage[key]
|
|
193
|
+
self.results.append({"site_id": site.id, "site_name": site.site_name, "hierarchy": f"{parent_label} → {block_label}", "clean_name": block_name, "type": "BLOCK", "fields": self.get_internal_structure(block_object), "usage": usage, "template": template_path, "template_label": self.get_template_label(template_path)})
|
|
211
194
|
self.seen_blocks.add(signature)
|
|
195
|
+
for child_name, child_object in getattr(block_object, "child_blocks", {}).items():
|
|
196
|
+
self.process_block(child_name, child_object, f"{parent_label} → {block_label}", site, include_usage)
|
|
212
197
|
|
|
213
|
-
|
|
214
|
-
for child_name, child_object in block_object.child_blocks.items():
|
|
215
|
-
self.process_block(child_name, child_object, parent_label, site)
|
|
216
|
-
|
|
217
|
-
def run(self) -> list[AuditItem]:
|
|
198
|
+
def run(self, include_usage: bool = False) -> list[AuditItem]:
|
|
218
199
|
for site in self.sites:
|
|
219
200
|
for model in self.all_page_models:
|
|
220
|
-
usage_data = self._get_usage_for_site(model, site)
|
|
201
|
+
usage_data = self._get_usage_for_site(model, site) if include_usage else {"live": 0, "total": 0, "draft": 0}
|
|
221
202
|
default_tpl = f"{model._meta.app_label}/{model._meta.model_name}.html"
|
|
203
|
+
template_path = self.get_template_path(
|
|
204
|
+
getattr(model, "template", default_tpl)
|
|
205
|
+
)
|
|
222
206
|
self.results.append(
|
|
223
207
|
{
|
|
224
|
-
"
|
|
208
|
+
"site_id": site.id,
|
|
209
|
+
"site_name": site.site_name,
|
|
225
210
|
"hierarchy": str(model._meta.verbose_name).title(),
|
|
226
211
|
"clean_name": model.__name__,
|
|
227
212
|
"type": "PAGE",
|
|
228
213
|
"fields": self.get_internal_structure(model),
|
|
229
214
|
"usage": usage_data,
|
|
230
|
-
"template":
|
|
231
|
-
|
|
232
|
-
),
|
|
215
|
+
"template": template_path,
|
|
216
|
+
"template_label": self.get_template_label(template_path),
|
|
233
217
|
}
|
|
234
218
|
)
|
|
235
219
|
for field in model._meta.get_fields():
|
|
@@ -241,19 +225,45 @@ class WagtailAuditor:
|
|
|
241
225
|
block_object,
|
|
242
226
|
str(model._meta.verbose_name).title(),
|
|
243
227
|
site,
|
|
228
|
+
include_usage,
|
|
244
229
|
)
|
|
245
230
|
return self.results
|
|
246
231
|
|
|
247
232
|
|
|
248
233
|
@user_passes_test(require_developer_reference_access)
|
|
249
234
|
def developer_reference_view(request: HttpRequest) -> HttpResponse:
|
|
235
|
+
applied = request.GET.get("apply") == "1"
|
|
250
236
|
auditor = WagtailAuditor()
|
|
251
|
-
audit_results = auditor.run()
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
key=
|
|
255
|
-
|
|
256
|
-
|
|
237
|
+
audit_results = auditor.run(include_usage=applied)
|
|
238
|
+
quick_search_by_component = {}
|
|
239
|
+
for item in audit_results:
|
|
240
|
+
key = (item["type"], item["hierarchy"])
|
|
241
|
+
quick_search_by_component.setdefault(key, (item["type"], item["hierarchy"], item["hierarchy"]))
|
|
242
|
+
quick_search_items = sorted(quick_search_by_component.values(), key=lambda item: (item[2].casefold(), item[0]))
|
|
243
|
+
filters = {"q": request.GET.get("q", "").strip(), "type": request.GET.get("type", "ALL"), "site": request.GET.get("site", ""), "min_usage": request.GET.get("min_usage", ""), "max_usage": request.GET.get("max_usage", "")}
|
|
244
|
+
error = ""
|
|
245
|
+
filtered_results: list[AuditItem] = []
|
|
246
|
+
if applied:
|
|
247
|
+
try:
|
|
248
|
+
minimum = int(filters["min_usage"]) if filters["min_usage"] else None
|
|
249
|
+
maximum = int(filters["max_usage"]) if filters["max_usage"] else None
|
|
250
|
+
if (minimum is not None and minimum < 0) or (maximum is not None and maximum < 0) or (minimum is not None and maximum is not None and minimum > maximum):
|
|
251
|
+
raise ValueError
|
|
252
|
+
except ValueError:
|
|
253
|
+
error = "Usage range must contain non-negative numbers, with min no greater than max."
|
|
254
|
+
minimum = maximum = None
|
|
255
|
+
if not error:
|
|
256
|
+
query = filters["q"].casefold()
|
|
257
|
+
for item in audit_results:
|
|
258
|
+
matches_query = not query or query in " ".join((item["site_name"], item["hierarchy"], item["clean_name"])).casefold()
|
|
259
|
+
matches_type = filters["type"] == "ALL" or item["type"] == filters["type"]
|
|
260
|
+
matches_site = not filters["site"] or str(item["site_id"]) == filters["site"]
|
|
261
|
+
total = item["usage"]["total"]
|
|
262
|
+
matches_min = minimum is None or total >= minimum
|
|
263
|
+
matches_max = maximum is None or total <= maximum
|
|
264
|
+
if matches_query and matches_type and matches_site and matches_min and matches_max:
|
|
265
|
+
filtered_results.append(item)
|
|
266
|
+
return render(request, "index.html", {"audit_data": sorted(filtered_results, key=lambda item: (item["site_name"], item["hierarchy"])), "quick_search_items": quick_search_items, "sites": Site.objects.order_by("site_name", "id"), "filters": filters, "is_applied": applied, "filter_error": error})
|
|
257
267
|
|
|
258
268
|
|
|
259
269
|
@user_passes_test(require_developer_reference_access)
|
|
@@ -262,17 +272,28 @@ def developer_usage_detail_view(
|
|
|
262
272
|
component_type: str,
|
|
263
273
|
component_name: str,
|
|
264
274
|
) -> HttpResponse:
|
|
265
|
-
|
|
266
|
-
|
|
275
|
+
site_filter = request.GET.get("site")
|
|
276
|
+
site = Site.objects.filter(pk=site_filter).first()
|
|
277
|
+
pages = Page.objects.none()
|
|
278
|
+
|
|
279
|
+
# Do not load every matching page into Python. Scope the query to the site
|
|
280
|
+
# and let the database return only the current results page.
|
|
281
|
+
if site_filter:
|
|
282
|
+
site_pages = (
|
|
283
|
+
Page.objects.descendant_of(site.root_page) if site else Page.objects.none()
|
|
284
|
+
)
|
|
285
|
+
else:
|
|
286
|
+
site_pages = Page.objects.all()
|
|
267
287
|
if component_type == "PAGE":
|
|
268
288
|
target_model = next(
|
|
269
289
|
(model for model in get_page_models() if model.__name__ == component_name),
|
|
270
290
|
None,
|
|
271
291
|
)
|
|
272
292
|
if target_model:
|
|
273
|
-
|
|
293
|
+
pages = site_pages.filter(pk__in=target_model.objects.values("pk"))
|
|
274
294
|
else:
|
|
275
295
|
search_key = f'"{component_name}"'
|
|
296
|
+
component_filter = Q()
|
|
276
297
|
for model in get_page_models():
|
|
277
298
|
stream_fields = [
|
|
278
299
|
f.name
|
|
@@ -280,28 +301,31 @@ def developer_usage_detail_view(
|
|
|
280
301
|
if isinstance(f, StreamField)
|
|
281
302
|
]
|
|
282
303
|
for field_name in stream_fields:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
)
|
|
288
|
-
)
|
|
304
|
+
component_filter |= Q(
|
|
305
|
+
pk__in=model.objects.filter(
|
|
306
|
+
**{f"{field_name}__icontains": search_key}
|
|
307
|
+
).values("pk")
|
|
289
308
|
)
|
|
309
|
+
if component_filter:
|
|
310
|
+
pages = site_pages.filter(component_filter)
|
|
290
311
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
]
|
|
312
|
+
search_query = request.GET.get("q", "").strip()
|
|
313
|
+
if search_query:
|
|
314
|
+
pages = pages.filter(
|
|
315
|
+
Q(title__icontains=search_query) | Q(slug__icontains=search_query)
|
|
316
|
+
)
|
|
297
317
|
|
|
298
|
-
|
|
318
|
+
paginator = Paginator(pages.order_by("title", "pk").distinct(), 50)
|
|
319
|
+
page_obj = paginator.get_page(request.GET.get("page"))
|
|
299
320
|
return render(
|
|
300
321
|
request,
|
|
301
322
|
"detail.html",
|
|
302
323
|
{
|
|
303
324
|
"component_name": component_name,
|
|
304
|
-
"pages":
|
|
325
|
+
"pages": page_obj,
|
|
326
|
+
"page_obj": page_obj,
|
|
327
|
+
"search_query": search_query,
|
|
328
|
+
"site_filter": site_filter or "",
|
|
305
329
|
},
|
|
306
330
|
)
|
|
307
331
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wagtail-developer-reference
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A visual auditing tool for Wagtail developers to track Page models and StreamField blocks.
|
|
5
5
|
Author-email: Four Digits <info@fourdigits.nl>
|
|
6
6
|
Project-URL: Homepage, https://github.com/fourdigits/wagtail-developer-reference
|
|
@@ -32,15 +32,16 @@ Requires-Dist: pytest-django; extra == "dev"
|
|
|
32
32
|
In large Wagtail projects, it’s easy to lose track of where specific blocks are used or which templates are being rendered. This tool gives you a "God-view" of your components without needing to dig through the database or shell.
|
|
33
33
|
|
|
34
34
|
## Features
|
|
35
|
-
* **
|
|
36
|
-
* **
|
|
37
|
-
* **
|
|
38
|
-
* **
|
|
35
|
+
* **On-demand usage tracking:** Apply filters before calculating page and block usage, keeping the registry quick to open.
|
|
36
|
+
* **Interactive registry:** Combine text, PAGE/BLOCK, site and total-usage-range filters to find exactly the components you need.
|
|
37
|
+
* **Nested block discovery:** Browse visible block labels and full nested definition paths, with safe fallbacks for unlabeled blocks.
|
|
38
|
+
* **Usage inspection:** Search and paginate matching pages, then copy the visible usage table in one click.
|
|
39
|
+
* **Template discovery:** See the template path associated with every component.
|
|
39
40
|
|
|
40
41
|
## Installation
|
|
41
42
|
|
|
42
|
-
Compatibility: Python 3.9+ and Wagtail 4.1 through 7.4 LTS
|
|
43
|
-
itself requires Python 3.10+.
|
|
43
|
+
Compatibility: Python 3.9+ and Wagtail 4.1 through 7.4.2 LTS (the latest
|
|
44
|
+
stable Wagtail release at v0.1.3). Wagtail 7.4 itself requires Python 3.10+.
|
|
44
45
|
|
|
45
46
|
1. Add \`wagtail_developer_reference\` to your \`INSTALLED_APPS\`:
|
|
46
47
|
\`\`\`python
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
{% extends "wagtailadmin/base.html" %}
|
|
2
|
-
{% block titletag %}System Registry{% endblock %}
|
|
3
|
-
|
|
4
|
-
{% block extra_css %}
|
|
5
|
-
<style>
|
|
6
|
-
.registry-controls {
|
|
7
|
-
display: flex; gap: 1rem; margin-bottom: 1.5rem;
|
|
8
|
-
background-color: #4a4a4a; padding: 1.25rem; border-left: 5px solid #dcbd3d;
|
|
9
|
-
}
|
|
10
|
-
.registry-controls div { flex: 1; }
|
|
11
|
-
.registry-controls label { display: block; font-weight: bold; color: white; margin-bottom: 0.5rem; }
|
|
12
|
-
.registry-controls input, .registry-controls select {
|
|
13
|
-
width: 100%; padding: 0.6rem; border: 1px solid #dcbd3d; background: #333; color: white; border-radius: 0;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.listing thead tr { border-bottom: 2px solid #dcbd3d; }
|
|
17
|
-
.col-centered { text-align: center; width: 5rem; }
|
|
18
|
-
.col-site { width: 10rem; }
|
|
19
|
-
.col-type { width: 8rem; white-space: nowrap; }
|
|
20
|
-
|
|
21
|
-
.sortable { cursor: pointer; position: relative; transition: background 0.2s; }
|
|
22
|
-
.sortable:hover { background: #444; }
|
|
23
|
-
.sortable::after {
|
|
24
|
-
content: '↕';
|
|
25
|
-
font-size: 0.8rem;
|
|
26
|
-
margin-left: 5px;
|
|
27
|
-
opacity: 0.3;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.type-tag {
|
|
31
|
-
background: #222; color: #dcbd3d; border: 1px solid #dcbd3d;
|
|
32
|
-
padding: 2px 10px; text-transform: uppercase; font-size: 0.7rem;
|
|
33
|
-
display: inline-block; white-space: nowrap;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.field-pill {
|
|
37
|
-
background: #222; border: 1px solid #dcbd3d; padding: 2px 6px;
|
|
38
|
-
margin-bottom: 4px; display: inline-block; font-size: 0.75rem;
|
|
39
|
-
}
|
|
40
|
-
.field-pill--metadata { border-color: #777; background: #333; }
|
|
41
|
-
|
|
42
|
-
.count-draft { color: #dcbd3d; font-weight: bold; }
|
|
43
|
-
.count-live { color: #1fb53a; font-weight: bold; }
|
|
44
|
-
.zero-usage { color: #af7f7f; opacity: 0.5; }
|
|
45
|
-
|
|
46
|
-
.usage-link { background: #dcbd3d !important; color: #222 !important; font-weight: bold; border-radius: 0; }
|
|
47
|
-
.template-path { color: #ccc; font-family: monospace; font-size: 0.7rem; cursor: pointer; }
|
|
48
|
-
|
|
49
|
-
[data-wagtail-theme="dark"] .listing tbody tr:hover {
|
|
50
|
-
background-color: #333 !important;
|
|
51
|
-
}
|
|
52
|
-
</style>
|
|
53
|
-
{% endblock %}
|
|
54
|
-
|
|
55
|
-
{% block content %}
|
|
56
|
-
{% include "wagtailadmin/shared/header.html" with title="System Registry" icon="code" %}
|
|
57
|
-
<div class="nice-padding">
|
|
58
|
-
<div class="registry-controls">
|
|
59
|
-
<div>
|
|
60
|
-
<label>Search Registry</label>
|
|
61
|
-
<input type="text" id="reg-search" placeholder="Filter by name, field or site...">
|
|
62
|
-
</div>
|
|
63
|
-
<div id="type-filter-container">
|
|
64
|
-
<label>Component Type</label>
|
|
65
|
-
<select id="reg-filter">
|
|
66
|
-
<option value="ALL">All</option>
|
|
67
|
-
<option value="PAGE">Pages Only</option>
|
|
68
|
-
<option value="BLOCK">Blocks Only</option>
|
|
69
|
-
</select>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
<table class="listing" id="registry-table">
|
|
74
|
-
<thead>
|
|
75
|
-
<tr>
|
|
76
|
-
<th class="sortable col-site" onclick="sortTable(0)">Site</th>
|
|
77
|
-
<th class="sortable" onclick="sortTable(1)">Hierarchy</th>
|
|
78
|
-
<th class="sortable col-type" onclick="sortTable(2)">Type</th>
|
|
79
|
-
<th class="sortable" onclick="sortTable(3)">Structure & Metadata</th>
|
|
80
|
-
<th class="sortable col-centered" onclick="sortTable(4)">Draft</th>
|
|
81
|
-
<th class="sortable col-centered" onclick="sortTable(5)">Live</th>
|
|
82
|
-
<th class="sortable col-centered" onclick="sortTable(6)">Total</th>
|
|
83
|
-
<th class="sortable" onclick="sortTable(7)">Template Path</th>
|
|
84
|
-
</tr>
|
|
85
|
-
</thead>
|
|
86
|
-
<tbody id="registry-body">
|
|
87
|
-
{% for item in audit_data %}
|
|
88
|
-
<tr class="registry-row" data-type="{{ item.type|upper }}">
|
|
89
|
-
<td class="col-site">{{ item.site_name }}</td>
|
|
90
|
-
<td><strong>{{ item.hierarchy }}</strong></td>
|
|
91
|
-
<td class="col-type"><span class="type-tag">{{ item.type }}</span></td>
|
|
92
|
-
<td>
|
|
93
|
-
{% for field_info in item.fields.structure %}
|
|
94
|
-
<div class="field-pill"><code>{{ field_info }}</code></div>
|
|
95
|
-
{% endfor %}
|
|
96
|
-
{% for field_info in item.fields.metadata %}
|
|
97
|
-
<div class="field-pill field-pill--metadata"><code>{{ field_info }}</code></div>
|
|
98
|
-
{% endfor %}
|
|
99
|
-
</td>
|
|
100
|
-
<td class="col-centered" data-sort="{{ item.usage.draft }}">
|
|
101
|
-
<span class="{% if item.usage.draft > 0 %}count-draft{% else %}zero-usage{% endif %}">{{ item.usage.draft }}</span>
|
|
102
|
-
</td>
|
|
103
|
-
<td class="col-centered" data-sort="{{ item.usage.live }}">
|
|
104
|
-
<span class="{% if item.usage.live > 0 %}count-live{% else %}zero-usage{% endif %}">{{ item.usage.live }}</span>
|
|
105
|
-
</td>
|
|
106
|
-
<td class="col-centered" data-sort="{{ item.usage.total }}">
|
|
107
|
-
{% if item.usage.total > 0 %}
|
|
108
|
-
<a href="{% url 'dev_usage_detail' item.type item.clean_name %}?site={{ item.site_name }}" class="button button-small usage-link">
|
|
109
|
-
{{ item.usage.total }}
|
|
110
|
-
</a>
|
|
111
|
-
{% else %}
|
|
112
|
-
<span class="zero-usage">0</span>
|
|
113
|
-
{% endif %}
|
|
114
|
-
</td>
|
|
115
|
-
<td>
|
|
116
|
-
<small class="template-path" onclick="navigator.clipboard.writeText(this.textContent.trim())">
|
|
117
|
-
{{ item.template }}
|
|
118
|
-
</small>
|
|
119
|
-
</td>
|
|
120
|
-
</tr>
|
|
121
|
-
{% endfor %}
|
|
122
|
-
</tbody>
|
|
123
|
-
</table>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<script>
|
|
127
|
-
let currentSortColumn = -1;
|
|
128
|
-
let isSortAscending = true;
|
|
129
|
-
|
|
130
|
-
function sortTable(columnIndex) {
|
|
131
|
-
const tableBody = document.getElementById("registry-body");
|
|
132
|
-
const tableRows = Array.from(tableBody.querySelectorAll(".registry-row"));
|
|
133
|
-
|
|
134
|
-
isSortAscending = (currentSortColumn === columnIndex) ? !isSortAscending : true;
|
|
135
|
-
currentSortColumn = columnIndex;
|
|
136
|
-
|
|
137
|
-
tableRows.sort((rowA, rowB) => {
|
|
138
|
-
let cellA = rowA.cells[columnIndex];
|
|
139
|
-
let cellB = rowB.cells[columnIndex];
|
|
140
|
-
let valueA = cellA.getAttribute('data-sort') || cellA.textContent.trim().toLowerCase();
|
|
141
|
-
let valueB = cellB.getAttribute('data-sort') || cellB.textContent.trim().toLowerCase();
|
|
142
|
-
|
|
143
|
-
if (!isNaN(parseFloat(valueA)) && isFinite(valueA) && !isNaN(parseFloat(valueB)) && isFinite(valueB)) {
|
|
144
|
-
return isSortAscending ? valueA - valueB : valueB - valueA;
|
|
145
|
-
}
|
|
146
|
-
return isSortAscending ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
|
|
147
|
-
}).forEach(sortedRow => tableBody.appendChild(sortedRow));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function runFilters() {
|
|
151
|
-
const searchTerm = document.getElementById('reg-search').value.toLowerCase();
|
|
152
|
-
const filterValue = document.getElementById('reg-filter').value;
|
|
153
|
-
document.querySelectorAll('.registry-row').forEach(row => {
|
|
154
|
-
const textMatches = row.textContent.toLowerCase().includes(searchTerm);
|
|
155
|
-
const rowType = row.getAttribute('data-type');
|
|
156
|
-
const typeMatches = (filterValue === 'ALL') || (rowType === filterValue);
|
|
157
|
-
row.style.display = (textMatches && typeMatches) ? '' : 'none';
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
document.getElementById('reg-search').addEventListener('input', runFilters);
|
|
162
|
-
document.getElementById('reg-filter').addEventListener('change', runFilters);
|
|
163
|
-
</script>
|
|
164
|
-
{% endblock %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|