wagtail-developer-reference 0.1.3__tar.gz → 0.1.5__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.
Files changed (24) hide show
  1. {wagtail_developer_reference-0.1.3/wagtail_developer_reference.egg-info → wagtail_developer_reference-0.1.5}/PKG-INFO +28 -7
  2. wagtail_developer_reference-0.1.5/README.md +69 -0
  3. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/pyproject.toml +1 -1
  4. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/setup.py +1 -1
  5. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/wagtail_developer_reference/templates/detail.html +3 -3
  6. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/wagtail_developer_reference/templates/index.html +127 -46
  7. wagtail_developer_reference-0.1.5/wagtail_developer_reference/tests/__init__.py +0 -0
  8. wagtail_developer_reference-0.1.5/wagtail_developer_reference/tests/test_auditor.py +100 -0
  9. wagtail_developer_reference-0.1.5/wagtail_developer_reference/tests/test_registry.py +37 -0
  10. wagtail_developer_reference-0.1.5/wagtail_developer_reference/wagtail_hooks.py +749 -0
  11. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5/wagtail_developer_reference.egg-info}/PKG-INFO +28 -7
  12. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/wagtail_developer_reference.egg-info/SOURCES.txt +4 -1
  13. wagtail_developer_reference-0.1.3/README.md +0 -48
  14. wagtail_developer_reference-0.1.3/wagtail_developer_reference/wagtail_hooks.py +0 -358
  15. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/MANIFEST.in +0 -0
  16. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/setup.cfg +0 -0
  17. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/wagtail_developer_reference/__init__.py +0 -0
  18. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/wagtail_developer_reference/apps.py +0 -0
  19. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/wagtail_developer_reference/migrations/0001_initial.py +0 -0
  20. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/wagtail_developer_reference/migrations/__init__.py +0 -0
  21. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/wagtail_developer_reference/models.py +0 -0
  22. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/wagtail_developer_reference.egg-info/dependency_links.txt +0 -0
  23. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/wagtail_developer_reference.egg-info/requires.txt +0 -0
  24. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.5}/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
3
+ Version: 0.1.5
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,16 +32,18 @@ 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
- * **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.
35
+ * **Fast registry:** Code definitions are cached per worker; the normal registry response stays small and paginates at 20 rows.
36
+ * **On-demand exact usage:** PAGE and BLOCK counts are calculated only for a usage range, scoped to the exact page model, StreamField and nested definition path.
37
+ * **Scalable StreamField inspection:** Usage scans read raw StreamField JSON, avoiding chooser-object queries while keeping totals current.
38
+ * **Searchable block picker:** An accessible, collapsed picker searches all definitions server-side and returns at most 30 choices at once.
39
+ * **Multi-site aware:** Site root pages are included, nested subsites are excluded from parent-site counts, and an empty site filter aggregates globally.
40
+ * **Usage inspection:** Open an exact definition's paginated page list and copy the visible usage table in one click.
39
41
  * **Template discovery:** See the template path associated with every component.
40
42
 
41
43
  ## Installation
42
44
 
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+.
45
+ Compatibility: Python 3.9+ and Wagtail 4.1 through 7.4.2 LTS. Wagtail 7.4
46
+ itself requires Python 3.10+.
45
47
 
46
48
  1. Add \`wagtail_developer_reference\` to your \`INSTALLED_APPS\`:
47
49
  \`\`\`python
@@ -70,5 +72,24 @@ By default, no account can see the menu item or access the URLs. Superusers are
70
72
  granted access automatically by this package; assign the permission directly or
71
73
  through a group.
72
74
 
75
+ ## Settings
76
+
77
+ ### `WAGTAIL_DEVELOPER_REFERENCE_ALLOW_ALL_ADMIN_USERS`
78
+
79
+ Default: `False`.
80
+
81
+ Set this to `True` in your Django settings to allow all active, authenticated
82
+ users with access to the Wagtail admin (`wagtailadmin.access_admin`) to see the
83
+ **System Registry** menu item and access the Developer Reference URLs, without
84
+ assigning the additional Developer Reference permission:
85
+
86
+ ```python
87
+ WAGTAIL_DEVELOPER_REFERENCE_ALLOW_ALL_ADMIN_USERS = True
88
+ ```
89
+
90
+ This includes active superusers. When omitted or set to `False`, the explicit
91
+ user or group permission described above is still required, including for
92
+ superusers.
93
+
73
94
  ## License
74
95
  **Internal Use Only** - Copyright © 2026 Fourdigits. All rights reserved.
@@ -0,0 +1,69 @@
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
+ * **Fast registry:** Code definitions are cached per worker; the normal registry response stays small and paginates at 20 rows.
10
+ * **On-demand exact usage:** PAGE and BLOCK counts are calculated only for a usage range, scoped to the exact page model, StreamField and nested definition path.
11
+ * **Scalable StreamField inspection:** Usage scans read raw StreamField JSON, avoiding chooser-object queries while keeping totals current.
12
+ * **Searchable block picker:** An accessible, collapsed picker searches all definitions server-side and returns at most 30 choices at once.
13
+ * **Multi-site aware:** Site root pages are included, nested subsites are excluded from parent-site counts, and an empty site filter aggregates globally.
14
+ * **Usage inspection:** Open an exact definition's paginated page list and copy the visible usage table in one click.
15
+ * **Template discovery:** See the template path associated with every component.
16
+
17
+ ## Installation
18
+
19
+ Compatibility: Python 3.9+ and Wagtail 4.1 through 7.4.2 LTS. Wagtail 7.4
20
+ itself requires Python 3.10+.
21
+
22
+ 1. Add \`wagtail_developer_reference\` to your \`INSTALLED_APPS\`:
23
+ \`\`\`python
24
+ INSTALLED_APPS = [
25
+ ...,
26
+ "wagtail_developer_reference",
27
+ ]
28
+ \`\`\`
29
+
30
+ 2. Run migrations so Django creates the package permission:
31
+ ```bash
32
+ python manage.py migrate
33
+ ```
34
+
35
+ 3. Grant access in the Django admin:
36
+ * Open a user or group in the Django admin.
37
+ * Assign **Wagtail developer reference | developer reference access | Can access the
38
+ Wagtail developer reference**.
39
+
40
+ 4. **Usage:**
41
+ * Login to the Wagtail Admin with an account that has that permission explicitly
42
+ assigned.
43
+ * Find the **"System Registry"** item in the sidebar menu.
44
+
45
+ By default, no account can see the menu item or access the URLs. Superusers are not
46
+ granted access automatically by this package; assign the permission directly or
47
+ through a group.
48
+
49
+ ## Settings
50
+
51
+ ### `WAGTAIL_DEVELOPER_REFERENCE_ALLOW_ALL_ADMIN_USERS`
52
+
53
+ Default: `False`.
54
+
55
+ Set this to `True` in your Django settings to allow all active, authenticated
56
+ users with access to the Wagtail admin (`wagtailadmin.access_admin`) to see the
57
+ **System Registry** menu item and access the Developer Reference URLs, without
58
+ assigning the additional Developer Reference permission:
59
+
60
+ ```python
61
+ WAGTAIL_DEVELOPER_REFERENCE_ALLOW_ALL_ADMIN_USERS = True
62
+ ```
63
+
64
+ This includes active superusers. When omitted or set to `False`, the explicit
65
+ user or group permission described above is still required, including for
66
+ superusers.
67
+
68
+ ## License
69
+ **Internal Use Only** - Copyright © 2026 Fourdigits. All rights reserved.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wagtail-developer-reference"
3
- version = "0.1.3"
3
+ version = "0.1.5"
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" }]
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="wagtail-developer-reference",
5
- version="0.1.3",
5
+ version="0.1.5",
6
6
  packages=find_packages(),
7
7
  include_package_data=True,
8
8
  install_requires=[
@@ -129,7 +129,7 @@
129
129
  <div class="breadcrumb">
130
130
  <a href="{% url 'dev_reference' %}">&larr; Back to System Registry</a>
131
131
  </div>
132
- <h1>Locations for: <strong>{{ component_name }}</strong></h1>
132
+ <h1>Usage: <strong>{{ component_name }}</strong></h1>
133
133
  <p>{{ page_obj.paginator.count }} location{{ page_obj.paginator.count|pluralize }} found</p>
134
134
  </div>
135
135
  </header>
@@ -159,14 +159,14 @@
159
159
  <tr class="usage-row">
160
160
  <td>
161
161
  <strong class="page-title">{{ page.title }}</strong>
162
- <small class="page-type">Type: {{ page.get_verbose_name }}</small>
162
+ <small class="page-type">Type: {{ page.content_type.name|title }}</small>
163
163
  </td>
164
164
  <td class="td-status" data-sort="{% if page.live %}1{% else %}0{% endif %}">
165
165
  <span class="status-tag status-tag--{% if page.live %}live{% else %}draft{% endif %}">
166
166
  {% if page.live %}Live{% else %}Draft{% endif %}
167
167
  </span>
168
168
  </td>
169
- <td><code class="url-code">{{ page.url|default:"/(no slug)/" }}</code></td>
169
+ <td><code class="url-code">{{ page.url_path|default:"/(no path)/" }}</code></td>
170
170
  <td class="col-actions">
171
171
  <a href="{% url 'wagtailadmin_pages:edit' page.id %}" class="button button-small edit-button" target="_blank">Edit Page</a>
172
172
  </td>
@@ -39,6 +39,24 @@
39
39
  }
40
40
  .quick-search-item .type-tag { margin-right: 0.35rem; }
41
41
  .registry-controls label { display: block; font-weight: bold; color: white; margin-bottom: 0.5rem; }
42
+ .registry-controls .usage-label { display: flex; align-items: center; gap: 0.35rem; margin-bottom: 0.5rem; }
43
+ .registry-controls .usage-label label { margin-bottom: 0; }
44
+ .usage-help { position: relative; display: inline-flex; }
45
+ .usage-help-icon {
46
+ background: transparent; border: 1px solid currentColor; border-radius: 50%;
47
+ color: white; cursor: help; font: bold 0.8rem serif;
48
+ width: 1.25rem; height: 1.25rem; padding: 0;
49
+ }
50
+ .usage-help-icon:focus-visible { outline: 2px solid #dcbd3d; outline-offset: 2px; }
51
+ .usage-help-tooltip {
52
+ display: none; position: absolute; top: 100%; right: 0; z-index: 20;
53
+ width: 16rem; max-width: calc(100vw - 3rem); padding: 0.75rem;
54
+ background: #222; border: 1px solid #dcbd3d; color: white;
55
+ font-size: 0.875rem; font-weight: normal; line-height: 1.5;
56
+ }
57
+ .usage-help:hover .usage-help-tooltip,
58
+ .usage-help:focus-within .usage-help-tooltip { display: block; }
59
+ .usage-help-tooltip[hidden] { display: none; }
42
60
  .registry-controls input, .registry-controls select {
43
61
  box-sizing: border-box; height: 2.5rem; width: 100%;
44
62
  border: 1px solid #dcbd3d; background: #333; color: white; border-radius: 0;
@@ -120,19 +138,29 @@
120
138
  <input id="quick-search-filter" class="quick-search-filter" type="search" placeholder="Search pages and blocks..." autocomplete="off">
121
139
  <span id="quick-search-status" class="quick-search-status" aria-live="polite"></span>
122
140
  <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 }}
141
+ {% for item in quick_search_items %}
142
+ <button type="button" class="quick-search-item" data-search="{{ item.hierarchy }}" data-type="{{ item.component_type }}" data-definition="{{ item.definition_id }}">
143
+ <span class="type-tag type-tag--{{ item.component_type|lower }}">{{ item.component_type }}</span>{{ item.hierarchy }}
126
144
  </button>
127
145
  {% endfor %}
128
146
  </div>
129
147
  </details>
130
148
  <form id="registry-filter-form" method="get" class="registry-controls">
131
149
  <input type="hidden" name="apply" value="1">
150
+ <input id="definition-filter" type="hidden" name="definition" value="{{ filters.definition }}">
132
151
  <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
152
  <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
153
  <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>
154
+ <div class="usage-range">
155
+ <span class="usage-label">
156
+ <label for="min-usage">Min usage</label>
157
+ <span class="usage-help">
158
+ <button id="min-usage-help" class="usage-help-icon" type="button" aria-label="About Min usage" aria-describedby="min-usage-tooltip"><span aria-hidden="true">i</span></button>
159
+ <span id="min-usage-tooltip" class="usage-help-tooltip" role="tooltip">Usage counts for pages, blocks, images and documents are only shown when Min usage is filled in. Enter 0 to include items with no usage, then apply the filters.</span>
160
+ </span>
161
+ </span>
162
+ <input id="min-usage" type="number" min="0" name="min_usage" value="{{ filters.min_usage }}" aria-describedby="min-usage-tooltip">
163
+ </div>
136
164
  <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
165
  <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
166
  </form>
@@ -179,21 +207,15 @@
179
207
  {% endfor %}
180
208
  </div>
181
209
  </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>
210
+ {% if usage_requested %}
211
+ <td class="col-centered" data-sort="{{ item.usage.draft }}"><span class="{% if item.usage.draft > 0 %}count-draft{% else %}zero-usage{% endif %}">{{ item.usage.draft }}</span></td>
212
+ <td class="col-centered" data-sort="{{ item.usage.live }}"><span class="{% if item.usage.live > 0 %}count-live{% else %}zero-usage{% endif %}">{{ item.usage.live }}</span></td>
213
+ <td class="col-centered" data-sort="{{ item.usage.total }}">{% if item.usage.total > 0 %}<a href="{% url 'dev_definition_usage' item.definition_id %}{% if item.site_id %}?site={{ item.site_id }}{% endif %}" class="button button-small usage-link">{{ item.usage.total }}</a>{% else %}<span class="zero-usage">0</span>{% endif %}</td>
214
+ {% else %}
215
+ <td class="col-centered"><span class="template-empty">—</span></td>
216
+ <td class="col-centered"><span class="template-empty">—</span></td>
217
+ <td class="col-centered"><span class="template-empty">Add min or max</span></td>
218
+ {% endif %}
197
219
  <td>
198
220
  {% if item.template %}
199
221
  <span class="template-label" title="{{ item.template }}">{{ item.template_label }}</span>
@@ -206,12 +228,36 @@
206
228
  {% endfor %}
207
229
  </tbody>
208
230
  </table>
231
+ {% if page_obj.paginator.num_pages > 1 %}
232
+ <nav class="pagination" aria-label="Registry result pages">
233
+ {% if page_obj.has_previous %}<a class="button" href="?apply=1&amp;q={{ filters.q|urlencode }}&amp;type={{ filters.type|urlencode }}&amp;site={{ filters.site|urlencode }}&amp;min_usage={{ filters.min_usage|urlencode }}&amp;max_usage={{ filters.max_usage|urlencode }}&amp;definition={{ filters.definition|urlencode }}&amp;page={{ page_obj.previous_page_number }}">Previous</a>{% endif %}
234
+ <span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} ({{ page_obj.paginator.count }} results)</span>
235
+ {% if page_obj.has_next %}<a class="button" href="?apply=1&amp;q={{ filters.q|urlencode }}&amp;type={{ filters.type|urlencode }}&amp;site={{ filters.site|urlencode }}&amp;min_usage={{ filters.min_usage|urlencode }}&amp;max_usage={{ filters.max_usage|urlencode }}&amp;definition={{ filters.definition|urlencode }}&amp;page={{ page_obj.next_page_number }}">Next</a>{% endif %}
236
+ </nav>
237
+ {% endif %}
209
238
  {% else %}
210
239
  <p class="help-block">Choose filters and select <strong>Apply Filters</strong> to load registry usage.</p>
211
240
  {% endif %}
212
241
  </div>
213
242
 
214
243
  <script>
244
+ const usageHelp = document.getElementById("min-usage-help");
245
+ const usageTooltip = document.getElementById("min-usage-tooltip");
246
+ ["mouseenter", "focus", "click"].forEach(eventName => {
247
+ usageHelp.addEventListener(eventName, () => {
248
+ usageTooltip.hidden = false;
249
+ usageTooltip.style.transform = "";
250
+ const tooltipLeft = usageTooltip.getBoundingClientRect().left;
251
+ if (tooltipLeft < 16) {
252
+ usageTooltip.style.transform = `translateX(${16 - tooltipLeft}px)`;
253
+ }
254
+ });
255
+ });
256
+ window.addEventListener("resize", () => { usageTooltip.hidden = true; });
257
+ document.addEventListener("keydown", event => {
258
+ if (event.key === "Escape") usageTooltip.hidden = true;
259
+ });
260
+
215
261
  let currentSortColumn = -1;
216
262
  let isSortAscending = true;
217
263
 
@@ -273,41 +319,76 @@
273
319
 
274
320
  const quickSearchInput = document.getElementById("quick-search-filter");
275
321
  const quickSearchStatus = document.getElementById("quick-search-status");
276
- const quickSearchItems = Array.from(document.querySelectorAll(".quick-search-item"));
277
- const initialQuickSearchLimit = 30;
322
+ const quickSearchList = document.querySelector(".quick-search-list");
323
+ const definitionFilter = document.getElementById("definition-filter");
324
+ let quickSearchTimer;
325
+ let quickSearchController;
326
+
327
+ function selectQuickItem(button) {
328
+ document.getElementById("reg-search").value = button.dataset.search;
329
+ document.getElementById("reg-filter").value = button.dataset.type;
330
+ definitionFilter.value = button.dataset.definition;
331
+ document.getElementById("registry-filter-form").submit();
332
+ }
278
333
 
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;
334
+ function bindQuickItems() {
335
+ quickSearchList.querySelectorAll(".quick-search-item").forEach(button => {
336
+ button.addEventListener("click", () => selectQuickItem(button));
288
337
  });
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
338
  }
293
339
 
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();
340
+ function renderQuickItems(results, total) {
341
+ quickSearchList.replaceChildren();
342
+ results.forEach(item => {
343
+ const button = document.createElement("button");
344
+ button.type = "button";
345
+ button.className = "quick-search-item";
346
+ button.dataset.search = item.name;
347
+ button.dataset.type = item.type;
348
+ button.dataset.definition = item.id;
349
+ const badge = document.createElement("span");
350
+ badge.className = `type-tag type-tag--${item.type.toLowerCase()}`;
351
+ badge.textContent = item.type;
352
+ button.append(badge, document.createTextNode(item.name));
353
+ quickSearchList.appendChild(button);
299
354
  });
355
+ quickSearchStatus.textContent = total > results.length
356
+ ? `Showing ${results.length} of ${total} matching components.`
357
+ : `${total} matching component${total === 1 ? "" : "s"}.`;
358
+ bindQuickItems();
359
+ }
360
+
361
+ async function loadQuickItems() {
362
+ if (quickSearchController) quickSearchController.abort();
363
+ quickSearchController = new AbortController();
364
+ const query = quickSearchInput.value.trim();
365
+ const params = new URLSearchParams({q: query, limit: "30"});
366
+ try {
367
+ const response = await fetch(`{% url 'dev_catalog_search' %}?${params}`, {
368
+ headers: {"X-Requested-With": "XMLHttpRequest"},
369
+ signal: quickSearchController.signal,
370
+ });
371
+ if (!response.ok) throw new Error("Catalog search failed");
372
+ const data = await response.json();
373
+ renderQuickItems(data.results, data.total);
374
+ } catch (error) {
375
+ if (error.name !== "AbortError") {
376
+ quickSearchStatus.textContent = "Could not load components.";
377
+ }
378
+ }
379
+ }
380
+
381
+ bindQuickItems();
382
+ quickSearchStatus.textContent = "Showing the first 30 components. Type to search all definitions.";
383
+ quickSearchInput.addEventListener("input", () => {
384
+ window.clearTimeout(quickSearchTimer);
385
+ quickSearchTimer = window.setTimeout(loadQuickItems, 200);
386
+ });
387
+ document.getElementById("reg-search").addEventListener("input", () => {
388
+ definitionFilter.value = "";
300
389
  });
301
- quickSearchInput.addEventListener("input", runQuickSearch);
302
390
  document.querySelector(".quick-search").addEventListener("toggle", event => {
303
391
  if (event.target.open) quickSearchInput.focus();
304
392
  });
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
393
  </script>
313
394
  {% endblock %}
@@ -0,0 +1,100 @@
1
+ from django.test import SimpleTestCase, TestCase
2
+ from wagtail.blocks import CharBlock, StreamBlock, StructBlock
3
+ from wagtail.models import Page, Site
4
+
5
+ from wagtail_developer_reference.wagtail_hooks import WagtailAuditor
6
+
7
+
8
+ class TimelineBlock(StructBlock):
9
+ title = CharBlock(required=False)
10
+ content = StreamBlock(
11
+ [
12
+ ("logo", CharBlock(required=False)),
13
+ ("image", CharBlock(required=False)),
14
+ ],
15
+ required=False,
16
+ )
17
+
18
+
19
+ class DefinitionPathTests(SimpleTestCase):
20
+ def setUp(self):
21
+ self.auditor = object.__new__(WagtailAuditor)
22
+ self.root_block = StreamBlock([("timeline", TimelineBlock())])
23
+
24
+ def test_raw_nested_stream_paths_are_exact_and_unique(self):
25
+ value = [
26
+ {
27
+ "type": "timeline",
28
+ "value": {
29
+ "title": "First",
30
+ "content": [
31
+ {"type": "logo", "value": "A"},
32
+ {"type": "image", "value": "B"},
33
+ {"type": "image", "value": "C"},
34
+ ],
35
+ },
36
+ }
37
+ ]
38
+ present = set()
39
+
40
+ self.auditor._collect_present_paths(self.root_block, value, (), present)
41
+
42
+ self.assertIn(("timeline",), present)
43
+ self.assertIn(("timeline", "title"), present)
44
+ self.assertIn(("timeline", "content", "logo"), present)
45
+ self.assertIn(("timeline", "content", "image"), present)
46
+ self.assertEqual(
47
+ len([path for path in present if path[-1] == "image"]), 1
48
+ )
49
+
50
+ def test_definition_id_includes_model_field_and_path(self):
51
+ first = WagtailAuditor._definition_id(
52
+ "BLOCK", "demo.articlepage", "body", ("timeline", "title")
53
+ )
54
+ same = WagtailAuditor._definition_id(
55
+ "BLOCK", "demo.articlepage", "body", ("timeline", "title")
56
+ )
57
+ other = WagtailAuditor._definition_id(
58
+ "BLOCK", "demo.homepage", "body", ("timeline", "title")
59
+ )
60
+
61
+ self.assertEqual(first, same)
62
+ self.assertNotEqual(first, other)
63
+
64
+
65
+ class SiteScopeTests(TestCase):
66
+ def test_site_root_is_included_and_nested_site_is_excluded(self):
67
+ tree_root = Page.get_first_root_node()
68
+ site_root = tree_root.add_child(
69
+ instance=Page(title="Package test site", slug="package-test-site")
70
+ )
71
+ included_child = site_root.add_child(
72
+ instance=Page(title="Included", slug="included")
73
+ )
74
+ nested_root = site_root.add_child(
75
+ instance=Page(title="Nested site", slug="nested-site")
76
+ )
77
+ nested_child = nested_root.add_child(
78
+ instance=Page(title="Nested child", slug="nested-child")
79
+ )
80
+ site = Site.objects.create(
81
+ hostname="package-test.example",
82
+ port=8123,
83
+ root_page=site_root,
84
+ )
85
+ Site.objects.create(
86
+ hostname="nested-package-test.example",
87
+ port=8124,
88
+ root_page=nested_root,
89
+ )
90
+
91
+ page_ids = set(
92
+ WagtailAuditor()._scope_page_queryset(site).values_list(
93
+ "pk", flat=True
94
+ )
95
+ )
96
+
97
+ self.assertIn(site_root.pk, page_ids)
98
+ self.assertIn(included_child.pk, page_ids)
99
+ self.assertNotIn(nested_root.pk, page_ids)
100
+ self.assertNotIn(nested_child.pk, page_ids)
@@ -0,0 +1,37 @@
1
+ from django.contrib.auth import get_user_model
2
+ from django.contrib.auth.models import Permission
3
+ from django.test import TestCase
4
+ from django.urls import reverse
5
+
6
+
7
+ class RegistryHelpTests(TestCase):
8
+ def test_min_usage_help_is_available_without_applying_filters(self):
9
+ user = get_user_model().objects.create_superuser(
10
+ username="registry-admin", email="admin@example.com", password="password"
11
+ )
12
+ user.user_permissions.add(
13
+ Permission.objects.get(
14
+ content_type__app_label="wagtail_developer_reference",
15
+ codename="access_developer_reference",
16
+ )
17
+ )
18
+ self.client.force_login(user)
19
+
20
+ response = self.client.get(reverse("dev_reference"))
21
+
22
+ self.assertContains(response, '<label for="min-usage">Min usage</label>')
23
+ self.assertContains(
24
+ response,
25
+ '<button id="min-usage-help" class="usage-help-icon" type="button" '
26
+ 'aria-label="About Min usage" aria-describedby="min-usage-tooltip">'
27
+ '<span aria-hidden="true">i</span></button>',
28
+ html=True,
29
+ )
30
+ self.assertContains(
31
+ response,
32
+ '<span id="min-usage-tooltip" class="usage-help-tooltip" role="tooltip">'
33
+ "Usage counts for pages, blocks, images and documents are only shown "
34
+ "when Min usage is filled in. Enter 0 to include items with no usage, "
35
+ "then apply the filters.</span>",
36
+ html=True,
37
+ )