wagtail-developer-reference 0.1.3__tar.gz → 0.1.4__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 (22) hide show
  1. {wagtail_developer_reference-0.1.3/wagtail_developer_reference.egg-info → wagtail_developer_reference-0.1.4}/PKG-INFO +9 -7
  2. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/README.md +8 -6
  3. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/pyproject.toml +1 -1
  4. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/setup.py +1 -1
  5. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/wagtail_developer_reference/templates/detail.html +3 -3
  6. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/wagtail_developer_reference/templates/index.html +82 -45
  7. wagtail_developer_reference-0.1.4/wagtail_developer_reference/tests/__init__.py +0 -0
  8. wagtail_developer_reference-0.1.4/wagtail_developer_reference/tests/test_auditor.py +100 -0
  9. wagtail_developer_reference-0.1.4/wagtail_developer_reference/wagtail_hooks.py +744 -0
  10. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4/wagtail_developer_reference.egg-info}/PKG-INFO +9 -7
  11. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/wagtail_developer_reference.egg-info/SOURCES.txt +3 -1
  12. wagtail_developer_reference-0.1.3/wagtail_developer_reference/wagtail_hooks.py +0 -358
  13. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/MANIFEST.in +0 -0
  14. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/setup.cfg +0 -0
  15. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/wagtail_developer_reference/__init__.py +0 -0
  16. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/wagtail_developer_reference/apps.py +0 -0
  17. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/wagtail_developer_reference/migrations/0001_initial.py +0 -0
  18. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/wagtail_developer_reference/migrations/__init__.py +0 -0
  19. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/wagtail_developer_reference/models.py +0 -0
  20. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/wagtail_developer_reference.egg-info/dependency_links.txt +0 -0
  21. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/wagtail_developer_reference.egg-info/requires.txt +0 -0
  22. {wagtail_developer_reference-0.1.3 → wagtail_developer_reference-0.1.4}/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.4
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
@@ -6,16 +6,18 @@
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
- * **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.
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.
13
15
  * **Template discovery:** See the template path associated with every component.
14
16
 
15
17
  ## Installation
16
18
 
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+.
19
+ Compatibility: Python 3.9+ and Wagtail 4.1 through 7.4.2 LTS. Wagtail 7.4
20
+ itself requires Python 3.10+.
19
21
 
20
22
  1. Add \`wagtail_developer_reference\` to your \`INSTALLED_APPS\`:
21
23
  \`\`\`python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wagtail-developer-reference"
3
- version = "0.1.3"
3
+ version = "0.1.4"
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.4",
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>
@@ -120,15 +120,16 @@
120
120
  <input id="quick-search-filter" class="quick-search-filter" type="search" placeholder="Search pages and blocks..." autocomplete="off">
121
121
  <span id="quick-search-status" class="quick-search-status" aria-live="polite"></span>
122
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 }}
123
+ {% for item in quick_search_items %}
124
+ <button type="button" class="quick-search-item" data-search="{{ item.hierarchy }}" data-type="{{ item.component_type }}" data-definition="{{ item.definition_id }}">
125
+ <span class="type-tag type-tag--{{ item.component_type|lower }}">{{ item.component_type }}</span>{{ item.hierarchy }}
126
126
  </button>
127
127
  {% endfor %}
128
128
  </div>
129
129
  </details>
130
130
  <form id="registry-filter-form" method="get" class="registry-controls">
131
131
  <input type="hidden" name="apply" value="1">
132
+ <input id="definition-filter" type="hidden" name="definition" value="{{ filters.definition }}">
132
133
  <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
134
  <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
135
  <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>
@@ -179,21 +180,15 @@
179
180
  {% endfor %}
180
181
  </div>
181
182
  </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>
183
+ {% if usage_requested %}
184
+ <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>
185
+ <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>
186
+ <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>
187
+ {% else %}
188
+ <td class="col-centered"><span class="template-empty">—</span></td>
189
+ <td class="col-centered"><span class="template-empty">—</span></td>
190
+ <td class="col-centered"><span class="template-empty">Add min or max</span></td>
191
+ {% endif %}
197
192
  <td>
198
193
  {% if item.template %}
199
194
  <span class="template-label" title="{{ item.template }}">{{ item.template_label }}</span>
@@ -206,6 +201,13 @@
206
201
  {% endfor %}
207
202
  </tbody>
208
203
  </table>
204
+ {% if page_obj.paginator.num_pages > 1 %}
205
+ <nav class="pagination" aria-label="Registry result pages">
206
+ {% 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 %}
207
+ <span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} ({{ page_obj.paginator.count }} results)</span>
208
+ {% 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 %}
209
+ </nav>
210
+ {% endif %}
209
211
  {% else %}
210
212
  <p class="help-block">Choose filters and select <strong>Apply Filters</strong> to load registry usage.</p>
211
213
  {% endif %}
@@ -273,41 +275,76 @@
273
275
 
274
276
  const quickSearchInput = document.getElementById("quick-search-filter");
275
277
  const quickSearchStatus = document.getElementById("quick-search-status");
276
- const quickSearchItems = Array.from(document.querySelectorAll(".quick-search-item"));
277
- const initialQuickSearchLimit = 30;
278
+ const quickSearchList = document.querySelector(".quick-search-list");
279
+ const definitionFilter = document.getElementById("definition-filter");
280
+ let quickSearchTimer;
281
+ let quickSearchController;
282
+
283
+ function selectQuickItem(button) {
284
+ document.getElementById("reg-search").value = button.dataset.search;
285
+ document.getElementById("reg-filter").value = button.dataset.type;
286
+ definitionFilter.value = button.dataset.definition;
287
+ document.getElementById("registry-filter-form").submit();
288
+ }
278
289
 
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;
290
+ function bindQuickItems() {
291
+ quickSearchList.querySelectorAll(".quick-search-item").forEach(button => {
292
+ button.addEventListener("click", () => selectQuickItem(button));
288
293
  });
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
294
  }
293
295
 
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();
296
+ function renderQuickItems(results, total) {
297
+ quickSearchList.replaceChildren();
298
+ results.forEach(item => {
299
+ const button = document.createElement("button");
300
+ button.type = "button";
301
+ button.className = "quick-search-item";
302
+ button.dataset.search = item.name;
303
+ button.dataset.type = item.type;
304
+ button.dataset.definition = item.id;
305
+ const badge = document.createElement("span");
306
+ badge.className = `type-tag type-tag--${item.type.toLowerCase()}`;
307
+ badge.textContent = item.type;
308
+ button.append(badge, document.createTextNode(item.name));
309
+ quickSearchList.appendChild(button);
299
310
  });
311
+ quickSearchStatus.textContent = total > results.length
312
+ ? `Showing ${results.length} of ${total} matching components.`
313
+ : `${total} matching component${total === 1 ? "" : "s"}.`;
314
+ bindQuickItems();
315
+ }
316
+
317
+ async function loadQuickItems() {
318
+ if (quickSearchController) quickSearchController.abort();
319
+ quickSearchController = new AbortController();
320
+ const query = quickSearchInput.value.trim();
321
+ const params = new URLSearchParams({q: query, limit: "30"});
322
+ try {
323
+ const response = await fetch(`{% url 'dev_catalog_search' %}?${params}`, {
324
+ headers: {"X-Requested-With": "XMLHttpRequest"},
325
+ signal: quickSearchController.signal,
326
+ });
327
+ if (!response.ok) throw new Error("Catalog search failed");
328
+ const data = await response.json();
329
+ renderQuickItems(data.results, data.total);
330
+ } catch (error) {
331
+ if (error.name !== "AbortError") {
332
+ quickSearchStatus.textContent = "Could not load components.";
333
+ }
334
+ }
335
+ }
336
+
337
+ bindQuickItems();
338
+ quickSearchStatus.textContent = "Showing the first 30 components. Type to search all definitions.";
339
+ quickSearchInput.addEventListener("input", () => {
340
+ window.clearTimeout(quickSearchTimer);
341
+ quickSearchTimer = window.setTimeout(loadQuickItems, 200);
342
+ });
343
+ document.getElementById("reg-search").addEventListener("input", () => {
344
+ definitionFilter.value = "";
300
345
  });
301
- quickSearchInput.addEventListener("input", runQuickSearch);
302
346
  document.querySelector(".quick-search").addEventListener("toggle", event => {
303
347
  if (event.target.open) quickSearchInput.focus();
304
348
  });
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
349
  </script>
313
350
  {% 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)