wagtail-developer-reference 0.1.0__py3-none-any.whl

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.
File without changes
@@ -0,0 +1,4 @@
1
+ from django.apps import AppConfig
2
+
3
+ class WagtailDeveloperReferenceConfig(AppConfig):
4
+ name = "wagtail_developer_reference"
@@ -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' %}">&larr; 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,9 @@
1
+ wagtail_developer_reference/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ wagtail_developer_reference/apps.py,sha256=iMT8MvnJp9UFpwveO5LHZnrKWIx4Mgwj-cHrEuCM4m4,125
3
+ wagtail_developer_reference/wagtail_hooks.py,sha256=cTMiNpuBvj0UvdngBLHx6FxjTsfXjWQgWIa79ffp7g8,7973
4
+ wagtail_developer_reference/templates/detail.html,sha256=LS7a70WaC-WbMWEa1noT9V3UujlhpmVRbVtjAkppxkw,5588
5
+ wagtail_developer_reference/templates/index.html,sha256=QPFSoWDemNlV0jLBpquBpiC8Vjjc0jLgqfu1u_c2ioA,7820
6
+ wagtail_developer_reference-0.1.0.dist-info/METADATA,sha256=zN8FxAkngUjps8ZltRWAmiUGEKkftIbM8W6xkOLqdxI,2364
7
+ wagtail_developer_reference-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ wagtail_developer_reference-0.1.0.dist-info/top_level.txt,sha256=xtCtDfhSuJDrckf7kgmOJhivU-BPnXVAtCCxeJswxLE,28
9
+ wagtail_developer_reference-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ wagtail_developer_reference