wagtail-developer-reference 0.1.0__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.0/MANIFEST.in +1 -0
- wagtail_developer_reference-0.1.0/PKG-INFO +53 -0
- wagtail_developer_reference-0.1.0/README.md +29 -0
- wagtail_developer_reference-0.1.0/pyproject.toml +50 -0
- wagtail_developer_reference-0.1.0/setup.cfg +4 -0
- wagtail_developer_reference-0.1.0/setup.py +11 -0
- wagtail_developer_reference-0.1.0/wagtail_developer_reference/__init__.py +0 -0
- wagtail_developer_reference-0.1.0/wagtail_developer_reference/apps.py +4 -0
- wagtail_developer_reference-0.1.0/wagtail_developer_reference/templates/detail.html +184 -0
- wagtail_developer_reference-0.1.0/wagtail_developer_reference/templates/index.html +164 -0
- wagtail_developer_reference-0.1.0/wagtail_developer_reference/wagtail_hooks.py +185 -0
- wagtail_developer_reference-0.1.0/wagtail_developer_reference.egg-info/PKG-INFO +53 -0
- wagtail_developer_reference-0.1.0/wagtail_developer_reference.egg-info/SOURCES.txt +14 -0
- wagtail_developer_reference-0.1.0/wagtail_developer_reference.egg-info/dependency_links.txt +1 -0
- wagtail_developer_reference-0.1.0/wagtail_developer_reference.egg-info/requires.txt +6 -0
- wagtail_developer_reference-0.1.0/wagtail_developer_reference.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include wagtail_developer_reference/templates *
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wagtail-developer-reference
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A visual auditing tool for Wagtail developers to track Page models and StreamField blocks.
|
|
5
|
+
Author-email: Four Digits <info@fourdigits.nl>
|
|
6
|
+
Project-URL: Homepage, https://github.com/fourdigits/wagtail-developer-reference
|
|
7
|
+
Keywords: django,wagtail,audit,streamfield
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Framework :: Django
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: wagtail>=4.1
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: ruff; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-django; extra == "dev"
|
|
24
|
+
|
|
25
|
+
# Wagtail Developer Reference
|
|
26
|
+
|
|
27
|
+
**A visual auditing tool for Wagtail developers.** This package provides a live registry of your project's architecture, making it easy to track Page models and StreamField blocks across complex, multi-site setups.
|
|
28
|
+
|
|
29
|
+
## Why use this?
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
* **Usage Tracking:** See exactly how many times a Page type or StreamField block is used across the site.
|
|
34
|
+
* **Template Discovery:** Instantly see the file path for the template associated with every component.
|
|
35
|
+
* **Interactive Audit:** Filter by type or search for specific field names to find where logic is implemented.
|
|
36
|
+
* **Zero-Config:** No extra database tables or complex settings. It inspects your models and content on the fly.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
1. Add \`wagtail_developer_reference\` to your \`INSTALLED_APPS\`:
|
|
41
|
+
\`\`\`python
|
|
42
|
+
INSTALLED_APPS = [
|
|
43
|
+
...,
|
|
44
|
+
"wagtail_developer_reference",
|
|
45
|
+
]
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
2. **Usage:**
|
|
49
|
+
* Login to the Wagtail Admin as a **superuser**.
|
|
50
|
+
* Find the **"System Registry"** item in the sidebar menu.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
**Internal Use Only** - Copyright © 2026 Fourdigits. All rights reserved.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Wagtail Developer Reference
|
|
2
|
+
|
|
3
|
+
**A visual auditing tool for Wagtail developers.** This package provides a live registry of your project's architecture, making it easy to track Page models and StreamField blocks across complex, multi-site setups.
|
|
4
|
+
|
|
5
|
+
## Why use this?
|
|
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
|
+
|
|
8
|
+
## Features
|
|
9
|
+
* **Usage Tracking:** See exactly how many times a Page type or StreamField block is used across the site.
|
|
10
|
+
* **Template Discovery:** Instantly see the file path for the template associated with every component.
|
|
11
|
+
* **Interactive Audit:** Filter by type or search for specific field names to find where logic is implemented.
|
|
12
|
+
* **Zero-Config:** No extra database tables or complex settings. It inspects your models and content on the fly.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
1. Add \`wagtail_developer_reference\` to your \`INSTALLED_APPS\`:
|
|
17
|
+
\`\`\`python
|
|
18
|
+
INSTALLED_APPS = [
|
|
19
|
+
...,
|
|
20
|
+
"wagtail_developer_reference",
|
|
21
|
+
]
|
|
22
|
+
\`\`\`
|
|
23
|
+
|
|
24
|
+
2. **Usage:**
|
|
25
|
+
* Login to the Wagtail Admin as a **superuser**.
|
|
26
|
+
* Find the **"System Registry"** item in the sidebar menu.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
**Internal Use Only** - Copyright © 2026 Fourdigits. All rights reserved.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wagtail-developer-reference"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A visual auditing tool for Wagtail developers to track Page models and StreamField blocks."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{name = "Four Digits", email = "info@fourdigits.nl" }]
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Environment :: Web Environment",
|
|
10
|
+
"Framework :: Django",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
13
|
+
"Programming Language :: Python",
|
|
14
|
+
"Programming Language :: Python :: 3.9",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12"
|
|
18
|
+
]
|
|
19
|
+
keywords = ["django", "wagtail", "audit", "streamfield"]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"wagtail>=4.1",
|
|
22
|
+
]
|
|
23
|
+
requires-python = ">=3.10"
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"ruff",
|
|
28
|
+
"pytest",
|
|
29
|
+
"pytest-django",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/fourdigits/wagtail-developer-reference"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
37
|
+
build-backend = "setuptools.build_meta"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
include-package-data = true
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
include = ["wagtail_developer_reference*"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 88
|
|
47
|
+
target-version = "py310"
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = ["E", "F", "W", "I"]
|
|
File without changes
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
{% extends "wagtailadmin/base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block titletag %}Usage: {{ component_name }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block extra_css %}
|
|
6
|
+
<style>
|
|
7
|
+
.registry-header {
|
|
8
|
+
border-bottom: 0.3125rem solid #dcbd3d;
|
|
9
|
+
background-color: #222 !important;
|
|
10
|
+
padding: 1.25rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.registry-header h1 { color: white; margin: 0; }
|
|
14
|
+
.registry-header strong { color: #dcbd3d; }
|
|
15
|
+
.registry-header .breadcrumb { margin-bottom: 0.625rem; }
|
|
16
|
+
|
|
17
|
+
.registry-header .breadcrumb a {
|
|
18
|
+
color: #dcbd3d;
|
|
19
|
+
text-decoration: none;
|
|
20
|
+
font-weight: bold;
|
|
21
|
+
font-size: 0.9rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.registry-header p { color: #ccc; margin-top: 0.3125rem; }
|
|
25
|
+
|
|
26
|
+
.listing thead tr {
|
|
27
|
+
border-bottom: 0.125rem solid #dcbd3d;
|
|
28
|
+
background-color: #333;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.listing th { color: white !important; }
|
|
32
|
+
|
|
33
|
+
.listing thead th.sortable {
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
position: relative;
|
|
36
|
+
transition: background-color 0.2s;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.listing thead th.sortable:hover { background-color: #444; }
|
|
40
|
+
|
|
41
|
+
.listing thead th.sortable::after {
|
|
42
|
+
content: '↕';
|
|
43
|
+
font-size: 0.8rem;
|
|
44
|
+
margin-left: 0.3125rem;
|
|
45
|
+
opacity: 0.3;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.col-status { width: 7.5rem; }
|
|
49
|
+
.col-actions { text-align: right; }
|
|
50
|
+
.listing tbody tr { border-bottom: 0.0625rem solid #444; }
|
|
51
|
+
.td-status { white-space: nowrap; vertical-align: middle; }
|
|
52
|
+
|
|
53
|
+
.page-title { color: white; font-size: 1.1rem; display: block; }
|
|
54
|
+
.page-type { color: #aaa; }
|
|
55
|
+
|
|
56
|
+
.status-tag {
|
|
57
|
+
color: #222222 !important;
|
|
58
|
+
border-radius: 0;
|
|
59
|
+
padding: 0.25rem 0.625rem;
|
|
60
|
+
font-weight: bold;
|
|
61
|
+
text-transform: uppercase;
|
|
62
|
+
font-size: 0.7rem;
|
|
63
|
+
display: inline-block;
|
|
64
|
+
min-width: 4.0625rem;
|
|
65
|
+
text-align: center;
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.status-tag--live { background-color: #8cf81f; }
|
|
70
|
+
.status-tag--draft { background-color: #eeb300; }
|
|
71
|
+
|
|
72
|
+
.url-code {
|
|
73
|
+
color: white;
|
|
74
|
+
background: #000;
|
|
75
|
+
border: 0.0625rem solid #555;
|
|
76
|
+
padding: 0.25rem 0.5rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.edit-button {
|
|
80
|
+
background-color: #222222 !important;
|
|
81
|
+
color: #ffffff !important;
|
|
82
|
+
border: 0.0625rem solid #dcbd3d !important;
|
|
83
|
+
border-radius: 0;
|
|
84
|
+
transition: all 0.2s ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.edit-button:hover {
|
|
88
|
+
background-color: #dcbd3d !important;
|
|
89
|
+
color: #222222 !important;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.empty-state {
|
|
93
|
+
text-align: center;
|
|
94
|
+
padding: 3.75rem;
|
|
95
|
+
color: white;
|
|
96
|
+
background-color: #111;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.empty-state p { font-size: 1.2rem; }
|
|
100
|
+
|
|
101
|
+
[data-wagtail-theme="dark"] .listing tbody tr:hover {
|
|
102
|
+
background-color: #333 !important;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
{% endblock %}
|
|
106
|
+
|
|
107
|
+
{% block content %}
|
|
108
|
+
<header class="merged registry-header">
|
|
109
|
+
<div class="left">
|
|
110
|
+
<div class="breadcrumb">
|
|
111
|
+
<a href="{% url 'dev_reference' %}">← Back to System Registry</a>
|
|
112
|
+
</div>
|
|
113
|
+
<h1>Locations for: <strong>{{ component_name }}</strong></h1>
|
|
114
|
+
<p>Found in {{ pages|length }} location{{ pages|length|pluralize }}</p>
|
|
115
|
+
</div>
|
|
116
|
+
</header>
|
|
117
|
+
|
|
118
|
+
<div class="nice-padding">
|
|
119
|
+
<table class="listing" id="usage-table">
|
|
120
|
+
<thead>
|
|
121
|
+
<tr>
|
|
122
|
+
<th class="sortable" onclick="sortTable(0)">Page Title</th>
|
|
123
|
+
<th class="sortable col-status" onclick="sortTable(1)">Status</th>
|
|
124
|
+
<th class="sortable" onclick="sortTable(2)">URL Path</th>
|
|
125
|
+
<th class="col-actions">Action</th>
|
|
126
|
+
</tr>
|
|
127
|
+
</thead>
|
|
128
|
+
<tbody id="usage-body">
|
|
129
|
+
{% for page in pages %}
|
|
130
|
+
<tr class="usage-row">
|
|
131
|
+
<td>
|
|
132
|
+
<strong class="page-title">{{ page.title }}</strong>
|
|
133
|
+
<small class="page-type">Type: {{ page.get_verbose_name }}</small>
|
|
134
|
+
</td>
|
|
135
|
+
<td class="td-status" data-sort="{% if page.live %}1{% else %}0{% endif %}">
|
|
136
|
+
<span class="status-tag status-tag--{% if page.live %}live{% else %}draft{% endif %}">
|
|
137
|
+
{% if page.live %}Live{% else %}Draft{% endif %}
|
|
138
|
+
</span>
|
|
139
|
+
</td>
|
|
140
|
+
<td><code class="url-code">{{ page.url|default:"/(no slug)/" }}</code></td>
|
|
141
|
+
<td class="col-actions">
|
|
142
|
+
<a href="{% url 'wagtailadmin_pages:edit' page.id %}" class="button button-small edit-button" target="_blank">Edit Page</a>
|
|
143
|
+
</td>
|
|
144
|
+
</tr>
|
|
145
|
+
{% empty %}
|
|
146
|
+
<tr>
|
|
147
|
+
<td colspan="4" class="empty-state">
|
|
148
|
+
<p>No pages found using this component.</p>
|
|
149
|
+
</td>
|
|
150
|
+
</tr>
|
|
151
|
+
{% endfor %}
|
|
152
|
+
</tbody>
|
|
153
|
+
</table>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<script>
|
|
157
|
+
let currentSortCol = -1;
|
|
158
|
+
let sortAsc = true;
|
|
159
|
+
|
|
160
|
+
function sortTable(colIdx) {
|
|
161
|
+
const tbody = document.getElementById("usage-body");
|
|
162
|
+
const rows = Array.from(tbody.querySelectorAll(".usage-row"));
|
|
163
|
+
|
|
164
|
+
if (currentSortCol === colIdx) {
|
|
165
|
+
sortAsc = !sortAsc;
|
|
166
|
+
} else {
|
|
167
|
+
sortAsc = true;
|
|
168
|
+
currentSortCol = colIdx;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
rows.sort((a, b) => {
|
|
172
|
+
let valA, valB;
|
|
173
|
+
if (colIdx === 1) {
|
|
174
|
+
valA = parseInt(a.cells[colIdx].getAttribute('data-sort'));
|
|
175
|
+
valB = parseInt(b.cells[colIdx].getAttribute('data-sort'));
|
|
176
|
+
return sortAsc ? valA - valB : valB - valueA;
|
|
177
|
+
}
|
|
178
|
+
valA = a.cells[colIdx].textContent.trim().toLowerCase();
|
|
179
|
+
valB = b.cells[colIdx].textContent.trim().toLowerCase();
|
|
180
|
+
return sortAsc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
181
|
+
}).forEach(row => tbody.appendChild(row));
|
|
182
|
+
}
|
|
183
|
+
</script>
|
|
184
|
+
{% endblock %}
|
|
@@ -0,0 +1,164 @@
|
|
|
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 %}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from django.contrib.auth.decorators import user_passes_test
|
|
4
|
+
from django.core.exceptions import PermissionDenied
|
|
5
|
+
from django.shortcuts import render
|
|
6
|
+
from django.template.loader import get_template
|
|
7
|
+
from django.urls import path
|
|
8
|
+
from wagtail import hooks
|
|
9
|
+
from wagtail.admin.menu import MenuItem
|
|
10
|
+
from wagtail.blocks import StructBlock
|
|
11
|
+
from wagtail.fields import StreamField
|
|
12
|
+
from wagtail.models import Site, get_page_models
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_superuser(user):
|
|
16
|
+
if user.is_superuser:
|
|
17
|
+
return True
|
|
18
|
+
raise PermissionDenied
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WagtailAuditor:
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.results = []
|
|
24
|
+
self.seen_blocks = set()
|
|
25
|
+
self.all_page_models = get_page_models()
|
|
26
|
+
self.sites = Site.objects.all()
|
|
27
|
+
self.models_with_streamfields = [
|
|
28
|
+
model for model in self.all_page_models
|
|
29
|
+
if any(isinstance(field, StreamField) for field in model._meta.get_fields())
|
|
30
|
+
]
|
|
31
|
+
self.excluded_fields = {
|
|
32
|
+
"path", "depth", "numchild", "content_type", "page_ptr",
|
|
33
|
+
"go_live_at", "expire_at", "show_in_menus", "first_published_at",
|
|
34
|
+
"last_published_at", "latest_revision_created_at",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def get_template_path(self, template_name):
|
|
38
|
+
if not template_name:
|
|
39
|
+
return "Default Rendering"
|
|
40
|
+
try:
|
|
41
|
+
return get_template(template_name).origin.name
|
|
42
|
+
except Exception:
|
|
43
|
+
return "Template not found"
|
|
44
|
+
|
|
45
|
+
def _analyze_model_field(self, field):
|
|
46
|
+
if field.name in self.excluded_fields:
|
|
47
|
+
return None, None
|
|
48
|
+
if not field.is_relation or field.many_to_one:
|
|
49
|
+
label = getattr(field, "verbose_name", field.name)
|
|
50
|
+
if hasattr(label, "__str__"):
|
|
51
|
+
label = str(label).capitalize()
|
|
52
|
+
field_type = type(field).__name__
|
|
53
|
+
is_structure = "StreamField" in field_type or "RichTextField" in field_type
|
|
54
|
+
category = "structure" if is_structure else "metadata"
|
|
55
|
+
return f"{label} ({field_type})", category
|
|
56
|
+
return None, None
|
|
57
|
+
|
|
58
|
+
def get_internal_structure(self, object_instance):
|
|
59
|
+
structure_fields = []
|
|
60
|
+
metadata_fields = []
|
|
61
|
+
|
|
62
|
+
if hasattr(object_instance, "child_blocks"):
|
|
63
|
+
for name, block in object_instance.child_blocks.items():
|
|
64
|
+
label = getattr(block.meta, "label", name)
|
|
65
|
+
structure_fields.append(f"{label} ({type(block).__name__})")
|
|
66
|
+
elif hasattr(object_instance, "_meta"):
|
|
67
|
+
for field in object_instance._meta.get_fields():
|
|
68
|
+
info, category = self._analyze_model_field(field)
|
|
69
|
+
if category == "structure":
|
|
70
|
+
structure_fields.append(info)
|
|
71
|
+
elif category == "metadata":
|
|
72
|
+
metadata_fields.append(info)
|
|
73
|
+
|
|
74
|
+
return {"structure": structure_fields, "metadata": metadata_fields}
|
|
75
|
+
|
|
76
|
+
def _get_usage_for_site(self, model, site, block_name=None):
|
|
77
|
+
queryset = model.objects.descendant_of(site.root_page)
|
|
78
|
+
if block_name:
|
|
79
|
+
search_key = f'"{block_name}"'
|
|
80
|
+
fields = model._meta.get_fields()
|
|
81
|
+
stream_fields = [f.name for f in fields if isinstance(f, StreamField)]
|
|
82
|
+
total_count = 0
|
|
83
|
+
live_count = 0
|
|
84
|
+
for field_name in stream_fields:
|
|
85
|
+
total_count += queryset.filter(**{f"{field_name}__icontains": search_key}).count()
|
|
86
|
+
live_count += queryset.live().filter(**{f"{field_name}__icontains": search_key}).count()
|
|
87
|
+
else:
|
|
88
|
+
total_count = queryset.count()
|
|
89
|
+
live_count = queryset.live().count()
|
|
90
|
+
return {"live": live_count, "total": total_count, "draft": total_count - live_count}
|
|
91
|
+
|
|
92
|
+
def process_block(self, block_name, block_object, parent_label, site):
|
|
93
|
+
is_class = inspect.isclass(block_object)
|
|
94
|
+
block_class = block_object.__name__ if is_class else block_object.__class__.__name__
|
|
95
|
+
signature = f"{site.id}_{block_class}_{block_name}"
|
|
96
|
+
if signature in self.seen_blocks:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
site_usage = {"live": 0, "total": 0, "draft": 0}
|
|
100
|
+
for model in self.models_with_streamfields:
|
|
101
|
+
usage_data = self._get_usage_for_site(model, site, block_name)
|
|
102
|
+
site_usage["live"] += usage_data["live"]
|
|
103
|
+
site_usage["total"] += usage_data["total"]
|
|
104
|
+
site_usage["draft"] += usage_data["draft"]
|
|
105
|
+
|
|
106
|
+
self.results.append({
|
|
107
|
+
"site_name": str(site),
|
|
108
|
+
"hierarchy": f"{parent_label} → {str(getattr(block_object.meta, 'label', block_name))}",
|
|
109
|
+
"clean_name": block_name,
|
|
110
|
+
"type": "BLOCK",
|
|
111
|
+
"fields": self.get_internal_structure(block_object),
|
|
112
|
+
"usage": site_usage,
|
|
113
|
+
"template": self.get_template_path(getattr(block_object.meta, "template", None)),
|
|
114
|
+
})
|
|
115
|
+
self.seen_blocks.add(signature)
|
|
116
|
+
|
|
117
|
+
if isinstance(block_object, StructBlock):
|
|
118
|
+
for child_name, child_object in block_object.child_blocks.items():
|
|
119
|
+
self.process_block(child_name, child_object, parent_label, site)
|
|
120
|
+
|
|
121
|
+
def run(self):
|
|
122
|
+
for site in self.sites:
|
|
123
|
+
for model in self.all_page_models:
|
|
124
|
+
usage_data = self._get_usage_for_site(model, site)
|
|
125
|
+
default_tpl = f"{model._meta.app_label}/{model._meta.model_name}.html"
|
|
126
|
+
self.results.append({
|
|
127
|
+
"site_name": str(site),
|
|
128
|
+
"hierarchy": str(model._meta.verbose_name).title(),
|
|
129
|
+
"clean_name": model.__name__,
|
|
130
|
+
"type": "PAGE",
|
|
131
|
+
"fields": self.get_internal_structure(model),
|
|
132
|
+
"usage": usage_data,
|
|
133
|
+
"template": self.get_template_path(getattr(model, "template", default_tpl)),
|
|
134
|
+
})
|
|
135
|
+
for field in model._meta.get_fields():
|
|
136
|
+
if isinstance(field, StreamField):
|
|
137
|
+
for block_name, block_object in field.stream_block.child_blocks.items():
|
|
138
|
+
self.process_block(block_name, block_object, str(model._meta.verbose_name).title(), site)
|
|
139
|
+
return self.results
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@user_passes_test(is_superuser)
|
|
143
|
+
def developer_reference_view(request):
|
|
144
|
+
auditor = WagtailAuditor()
|
|
145
|
+
audit_results = auditor.run()
|
|
146
|
+
sorted_results = sorted(audit_results, key=lambda item: (item["site_name"], item["hierarchy"]))
|
|
147
|
+
return render(request, "index.html", {"audit_data": sorted_results})
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@user_passes_test(is_superuser)
|
|
151
|
+
def developer_usage_detail_view(request, component_type, component_name):
|
|
152
|
+
site_filter_name = request.GET.get('site')
|
|
153
|
+
found_pages = []
|
|
154
|
+
if component_type == "PAGE":
|
|
155
|
+
target_model = next((model for model in get_page_models() if model.__name__ == component_name), None)
|
|
156
|
+
if target_model:
|
|
157
|
+
found_pages = target_model.objects.all()
|
|
158
|
+
else:
|
|
159
|
+
search_key = f'"{component_name}"'
|
|
160
|
+
for model in get_page_models():
|
|
161
|
+
stream_fields = [f.name for f in model._meta.get_fields() if isinstance(f, StreamField)]
|
|
162
|
+
for field_name in stream_fields:
|
|
163
|
+
found_pages.extend(list(model.objects.filter(**{f"{field_name}__icontains": search_key})))
|
|
164
|
+
|
|
165
|
+
if site_filter_name:
|
|
166
|
+
found_pages = [page for page in found_pages if str(page.get_site()) == site_filter_name]
|
|
167
|
+
|
|
168
|
+
unique_pages = {page.id: page for page in found_pages}.values()
|
|
169
|
+
return render(request, "detail.html", {
|
|
170
|
+
"component_name": component_name,
|
|
171
|
+
"pages": unique_pages
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@hooks.register("register_admin_urls")
|
|
176
|
+
def register_admin_urls():
|
|
177
|
+
return [
|
|
178
|
+
path("developer-reference/", developer_reference_view, name="dev_reference"),
|
|
179
|
+
path("developer-reference/<str:component_type>/<str:component_name>/", developer_usage_detail_view, name="dev_usage_detail"),
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@hooks.register("register_admin_menu_item")
|
|
184
|
+
def register_menu():
|
|
185
|
+
return MenuItem("System Registry", "/admin/developer-reference/", icon_name="code", order=10000)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wagtail-developer-reference
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A visual auditing tool for Wagtail developers to track Page models and StreamField blocks.
|
|
5
|
+
Author-email: Four Digits <info@fourdigits.nl>
|
|
6
|
+
Project-URL: Homepage, https://github.com/fourdigits/wagtail-developer-reference
|
|
7
|
+
Keywords: django,wagtail,audit,streamfield
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Framework :: Django
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: wagtail>=4.1
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: ruff; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-django; extra == "dev"
|
|
24
|
+
|
|
25
|
+
# Wagtail Developer Reference
|
|
26
|
+
|
|
27
|
+
**A visual auditing tool for Wagtail developers.** This package provides a live registry of your project's architecture, making it easy to track Page models and StreamField blocks across complex, multi-site setups.
|
|
28
|
+
|
|
29
|
+
## Why use this?
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
* **Usage Tracking:** See exactly how many times a Page type or StreamField block is used across the site.
|
|
34
|
+
* **Template Discovery:** Instantly see the file path for the template associated with every component.
|
|
35
|
+
* **Interactive Audit:** Filter by type or search for specific field names to find where logic is implemented.
|
|
36
|
+
* **Zero-Config:** No extra database tables or complex settings. It inspects your models and content on the fly.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
1. Add \`wagtail_developer_reference\` to your \`INSTALLED_APPS\`:
|
|
41
|
+
\`\`\`python
|
|
42
|
+
INSTALLED_APPS = [
|
|
43
|
+
...,
|
|
44
|
+
"wagtail_developer_reference",
|
|
45
|
+
]
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
2. **Usage:**
|
|
49
|
+
* Login to the Wagtail Admin as a **superuser**.
|
|
50
|
+
* Find the **"System Registry"** item in the sidebar menu.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
**Internal Use Only** - Copyright © 2026 Fourdigits. All rights reserved.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
wagtail_developer_reference/__init__.py
|
|
6
|
+
wagtail_developer_reference/apps.py
|
|
7
|
+
wagtail_developer_reference/wagtail_hooks.py
|
|
8
|
+
wagtail_developer_reference.egg-info/PKG-INFO
|
|
9
|
+
wagtail_developer_reference.egg-info/SOURCES.txt
|
|
10
|
+
wagtail_developer_reference.egg-info/dependency_links.txt
|
|
11
|
+
wagtail_developer_reference.egg-info/requires.txt
|
|
12
|
+
wagtail_developer_reference.egg-info/top_level.txt
|
|
13
|
+
wagtail_developer_reference/templates/detail.html
|
|
14
|
+
wagtail_developer_reference/templates/index.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wagtail_developer_reference
|