Draft: Listing rework code review fixes - #449
Draft
jhancock532 wants to merge 43 commits into
Draft
Conversation
# Conflicts: # tbx/static_src/sass/main.scss
Ruff-format migration 0012 and replace flaky draft-page URL assertion with focused unit tests for None URL handling. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Brings in PR #440, which moves the canonical tag into base_page.html. Conflicts resolved: - blog_listing.html, events_listing.html, work_index_page.html: main removed the old per-listing canonical block; this branch had already replaced that block with the listing_meta.html include. Kept ours, which now emits robots only, so canonical comes solely from base_page.html. - navigation/models.py: both sides added a field in the same place. Kept both header_cta_page/header_cta_text and carbon_impact_figure. Also renumbered the navigation migrations. Both main and this branch added a 0009, which git does not flag as a conflict but which leaves Django with two leaf nodes: 0009_primary_nav_dropdown_fields -> 0010 0010_migrate_primary_nav_from_child_display_behaviour -> 0011 0011_delete_navigationset -> 0012 0012_alter_navigationsettings_primary_navigation -> 0013 Dependencies rechained onto main's 0009_navigationsettings_carbon_impact_figure. makemigrations --check reports no changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR #438 — replies to review comments
Draft replies describing the implementation as it stands.
Commit progress
The review work is split across these commits:
a802608e:has()support checks_header.scss/_primary-nav-dropdown.scsscomments01f1cdd76ccea52d#listing-panelselector comments38369e9cecfc0a535ced380177b7ff31d63348b83bc6e438d9d5d3d400b1f273Some backend and frontend review fixes are grouped into a shared commit where their final-state changes touch the same functions and are easier to review together. No replies from this file have been posted automatically.
Verification
git diff --checkpassed.helenb
Culture dropdown with one “Culture” option
Agreed. Dropdown visibility is now based on the unfiltered listing and hides a dropdown when its only option repeats the dropdown label (case-insensitively). An active selection still keeps it visible, and a single option with a different label remains usable. This is shared by the taxonomy and Events listings through
dropdown_is_visible().Commit:
5ced3801Events “When” filter cannot be deselected
The timing choices are now independent checkboxes. With neither selected, the listing uses its default upcoming view without counting timing as an active filter. Selecting upcoming or past shows that group; selecting both shows all events, with upcoming events first.
timingis therefore a repeatable query parameter.Commit:
5ced3801Dropdowns without JavaScript / why not use
<select>?Filtering now works without JavaScript. The option panels are rendered expanded, and the form submits selected checkboxes with a normal GET request using the visible Apply filters button. When JavaScript initialises, it turns the groups into button-controlled disclosures, hides Apply filters, and enhances changes with htmx.
We retained checkboxes rather than a native
<select multiple>because the checkbox presentation matches the agreed design and gives a clearer multi-select experience than a native multiple-select control. Both submit without JavaScript, so the accessibility concern is addressed without changing the interaction we designed for.Commit:
3bc6e438Keep or remove the Apply filters button
Kept. It is the submit path when JavaScript is unavailable and is hidden when the enhanced behavior initialises.
Commit:
3bc6e438Use data attributes for htmx targets
Updated. Active-filter links, Clear all, pagination, and the programmatic htmx request target
[data-listing-results]; styling continues to use classes. Swaps useouterHTMLso the selected results wrapper replaces the existing wrapper rather than becoming nested inside it.Commit:
3bc6e438Track dropdown open state without a CSS class
Open state is represented by the toggle's
aria-expandedvalue and the panel'shiddenproperty. The JavaScript no longer uses a styling class as application state.Commit:
3bc6e438Focus and screen-reader updates
The open filter and focus are retained after a checkbox update so another option can be selected. Tabbing out of an option panel closes it and returns focus to its toggle; Escape also closes the disclosures and restores focus.
A polite
aria-liveregion outside the swapped results announces “Listing updated” with the result count after an htmx update, rather than causing the result set to be re-read.Commit:
3bc6e438Filter group label
Changed to “Filter the {{ item.title }} listing” for both Events and taxonomy listings.
Commit:
01f1cdd7Robots meta tags
Robots metadata is decided by the server response, not by htmx. An unfiltered or single-filter listing is indexable; two or more active filters render
noindex, nofollow. This matrix is covered directly by the SEO tests intbx/core/listing/tests/test_filters.py:test_no_filters_uses_plain_title_and_no_robots,test_single_filter_title_and_no_robots, andtest_multi_filter_is_noindex.test_context_only_contains_title_and_robotsalso confirms the listing context no longer carries a canonical value.The tag does not appear to change while filtering in the browser because htmx swaps only the document title; the robots tag stays at whatever the initial full page load produced. This is intentional — crawlers read the server response, not the htmx-updated DOM — so indexing stays correct even though the in-page value looks static. The empty placeholder meta element (the second
document-robotsnode with nonameattribute) has been removed.Note that this is a separate mechanism from the site-wide robots tag in
base.html, which emitsnoindexwheneverSEO_NOINDEXis set, the request host doesn't match the site record, or the page is the incident page. On a local:8000setup that global tag fires on every page regardless of filters, becauserequest.get_host()includes the port (localhost:8000) while the WagtailSite.hostnamedoes not (localhost), so the comparison never matches. That masks the listing's own robots logic in view-source, so the tests above — which assert the context values directly — are the reliable confirmation rather than a local page load.Commit:
5ced3801Listing SCSS comments
9remdropdown minimum was removed.min-width: 100%on small screens, with the wider minimum applied from the medium breakpoint, so they remain inside the viewport..listing-panel;#listing-panelremains the JavaScript/htmx hook.Commit:
6ccea52d@supports selector(:has(...))Removed the redundant wrappers around all three existing
:has()rules. The rules themselves remain.Commit:
a802608eSeparate listing bundle / htmx dependency
Kept as a separate entry so htmx and the filter behavior load only on the three listing pages and changes to them do not invalidate the site-wide
main.jsbundle.“public” filter uses the listing page theme
This remains a consequence of the listing page having one configured theme rather than a behavior introduced by filtering. It needs a separate design decision if the listing theme should react to a selected division.
Running the review pass (tbx-pr-review)
Thanks — this was useful. Running a review pass over the branch helped surface several fixes that were unrelated to the specific inline threads, which are in their own commits: division filtering on Work listings (
ecfc0a53), test isolation from the development cache (38369e9c), and disabling htmx history snapshots on listings (77b7ff31), alongside query-efficiency work on the listing context. Test coverage was extended accordingly.tm-kn
Use django-filter rather than reading
request.GETAgreed on the underlying point: no user value should reach the ORM without being validated first. Rather than adding django-filter, the implementation applies that principle directly. No raw
request.GETvalue is passed to a query — every parameter is checked against a known set of valid values and unknown or stale values are discarded before filtering runs.This is done in two ways because the two listings filter different data shapes, and I'd rather each validation live next to the thing it validates than force both through one abstraction:
TaxonomyFilterState.from_request()allow-lists every sector, service, and division slug against live database values before a value reaches an ORM filter. The validated state is then shared by result filtering, faceting, selected labels, URLs, and SEO. Work also needs different relations from News (WorkPageandHistoricalWorkPageplus page-tree paths), so those application functions stay explicit and testable.EventFilterFormvalidates?timing=/?type=against its choices (viaLenientMultipleChoiceField, which drops stale values instead of erroring).Both paths implement the same rule — keep known values, silently drop the rest — so the validation guarantee is consistent even though the mechanism differs. django-filter would add a dependency and a second layer over state that already drives facets, pills, URLs, and SEO, which felt like more than this needs; happy to revisit if you feel strongly.
Commit:
5ced3801Why swap metadata with htmx?
Only the document title is swapped now, so the browser tab and history reflect the selected filters. Canonical output comes from
base_page.html, and robots metadata is rendered only in the initial server response; neither is swapped by htmx.Commit:
5ced3801Canonical URLs for repeated content
Agreed. Listing-specific canonical handling has been removed.
base_page.htmlsupplies the canonical URL and omits filter parameters while preserving pagination.Commit:
5ced3801JakubMastalerz
Conflict with #440
Resolved in the same direction as #440: listing templates do not emit a canonical tag. Canonical output is owned by
base_page.html; the listing SEO context contains only the filtered document title and the conditional robots value.Commit:
5ced3801Additional changes and open questions (no existing review thread)
Event type dropdown now shows all types used by the listing
Not raised in review, flagged during manual testing. The event-type dropdown previously faceted against the default upcoming-only view, so a type used only by past events (e.g. a past-only "In person" or "Online event") never appeared even though the type exists on the listing. It now facets across all timings when no timing is selected, so the default view offers every type used by any event — matching how the Work/News dropdowns show every taxonomy term used by the listing when nothing is selected. Selecting a timing still narrows the type options to that timing. Covered by two new tests in
test_events_listing.py.Commit:
06a483c0Open question: should "Upcoming" show as active by default on Events?
The events listing shows upcoming events by default, so it may read more clearly if the "Upcoming" timing control appeared active/selected by default. Raising this rather than changing it, because it interacts with the "can't deselect" issue resolved earlier:
noindexfilter count.