Skip to content

Instantly share code, notes, and snippets.

@newsroomdev
Created February 25, 2026 18:01
Show Gist options
  • Select an option

  • Save newsroomdev/d5362f74c744b6e8a9efc5c0dc2487a8 to your computer and use it in GitHub Desktop.

Select an option

Save newsroomdev/d5362f74c744b6e8a9efc5c0dc2487a8 to your computer and use it in GitHub Desktop.
DRAFT: AW filters tickets & epics

chore: AW formatting guardrails — editor config separate from Django/CLEAN settings

Standalone ticket
Effort estimate: 2


Description

The repo's Python formatting settings (Black, isort, flake8 via setup.cfg / tox.ini / similar) are likely conflicting with IDE autoformatting for contributors. Adding an .editorconfig file (and optionally VS Code workspace settings in .vscode/settings.json) gives every editor a single source of truth that doesn't touch the CI/linting config.

This is a low-risk quality-of-life fix that prevents "noisy" diffs caused by autoformatter conflicts and establishes a clear convention for new contributors.


Files to change

  • .editorconfig (new) — canonical editor-agnostic formatting rules:

    root = true
    
    [*]
    indent_style = space
    indent_size = 4
    end_of_line = lf
    charset = utf-8
    trim_trailing_whitespace = true
    insert_final_newline = true
    
    [*.{html,css,js,json,yml,yaml,md}]
    indent_size = 2
    
    [*.py]
    indent_size = 4
    max_line_length = 88
  • .vscode/settings.json (optional — see Open Questions) — VS Code-specific overrides so Copilot/Pylance/Black extension don't fight each other:

{
  "editor.formatOnSave": true,
  "[python]": {
    "editor.defaultFormatter": "ms-python.black-formatter",
    "editor.tabSize": 4
  },
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.tabSize": 2
  },
  "python.linting.flake8Enabled": true,
  "python.linting.enabled": true
}
  • setup.cfg — confirm [flake8] and [isort] sections are present and consistent with the above (they probably already are; this is a verification step)

Acceptance Criteria

  • .editorconfig committed to repo root
  • .vscode/settings.json committed if team agrees (see Open Questions), otherwise documented in README.md as a recommended local setup
  • A contributor cloning the repo fresh gets consistent formatting behavior without any manual configuration
  • No existing CI checks break
  • setup.cfg / tox.ini lint config is consistent with .editorconfig values (no contradictions)

Open Questions

  1. Should .vscode/settings.json be committed? Committing it makes setup automatic for VS Code users but implies VS Code as the team default, which can feel presumptuous. Alternatives: (a) skip it entirely and rely on .editorconfig alone, (b) commit it but add a note in README.md that contributors on other editors should configure equivalents manually, (c) add VS Code extension recommendations only (.vscode/extensions.json) which is lighter and more conventional. The .editorconfig alone covers all editors and is almost certainly sufficient.

feat: Filter panel UI/UX — layout, clear all, active state display

Parent epic: #298 Re-enable filters on site search
Part of: Search filters breakdown
Effort estimate: 3 (may slip to 5 if existing template is dense)
Build order: Start this ticket first — scaffold the grid + accordion shell so individual filters (298-2 through 298-5) have somewhere to slot in. Complete/close this ticket last once all accordion groups are populated.
Depends on: 298-2 through 298-5 for full acceptance (individual accordion groups); scaffold work is independent


Description

Implement the filter panel scaffolding and UX polish on the /search/ page:

  • Restructure the page into a two-column Bootstrap grid (left rail + results area)
  • Add the Bootstrap Accordion wrapper that each filter sub-issue will populate
  • "Clear filters" / "Reset filters" link
  • Active filter state visibility (inputs reflect URL values; optional: badge count on accordion headers)
  • Collapsible accordion groups (each filter group independently shown/hidden)
  • Responsive: left rail collapses on mobile

This ticket can be scaffolded before the individual filter inputs are wired up, but full acceptance requires the filter sub-issues to be in place.


Files to change

  • agendawatch/templates/agendawatch/search.html
    • Restructure <div class="container"> body into <div class="row"> with col-lg-3 (filter rail) and col-lg-9 (results)
    • Add Bootstrap Accordion wrapper (accordion, accordion-item, accordion-collapse) in the left rail
    • Move filter inputs (from sub-issues) inside the existing <form method="get"> alongside the search bar so all params submit together
    • Add "Reset filters" link: <a href="?q={{ q|urlencode }}">Reset filters</a>
  • static/css/custom.css
    • Left-rail sticky positioning on desktop: position: sticky; top: 1rem;
    • Accordion header styling to match existing design
    • Optional: active filter badge on accordion group headers
  • JS in {% block extra_js %} — on DOMContentLoaded, check each accordion group for any input with a non-empty value or checked state; if found, add Bootstrap's show class to the corresponding accordion-collapse element to auto-expand it

Open Questions

  1. Should "Reset filters" also preserve sort? — The current spec is <a href="?q={{ q|urlencode }}">Reset filters</a>, which drops sort back to the default. The sort param is currently managed via a hidden input in the search form, not a filter. Decide whether "reset filters" means "clear everything except the keyword" (current spec) or "clear filters but keep sort order."

Acceptance Criteria

  • Page uses a two-column Bootstrap grid; left rail is col-lg-3, results area is col-lg-9
  • Each filter group is a collapsible Bootstrap Accordion item
  • Filter panel collapses/stacks below results on mobile
  • "Reset filters" link clears all filter params while preserving q
  • Active filter inputs reflect current URL values on page load
  • Accordion groups with active filters are auto-expanded on page load (e.g. a shared URL with ?state=CA opens the Geography group automatically)
  • All filter inputs are inside the same <form method="get"> as the search bar
  • Sort and pagination links preserve all active filter params (via JS mergeParam workaround until Django 5.1 upgrade; see docs/issues/django-52-upgrade-epic.md)
  • "No documents found" state is still visible and styled correctly in the results column

feat: Committee Name filter on search page

Parent epic: #298 Re-enable filters on site search
Part of: Search filters breakdown
Effort estimate: 2


Description

Add a Committee Name text input to the /search/ filter panel. Filters via case-insensitive substring match against meta__committee_name.

This is a standalone accordion group — simple single-field implementation. Good first issue for a new contributor.

Files to change

agendawatch/views.py

  • Parse committee_name from request.GET
  • Apply qs.filter(meta__committee_name__icontains=committee_name) if value is set
  • Pass committee_name back in render context

agendawatch/templates/agendawatch/search.html

  • Add Committee Name accordion group in the filter panel
  • Single <input type="text" name="committee_name"> inside the form
  • Input pre-populated from context value on page load

Open Questions

  1. Text input vs. multi-select — The geography filters (#298-4) use multi-select for parity with prior behavior. Should committee name follow the same pattern? The tradeoff: committee names are long and numerous (potentially hundreds of distinct values), making checkboxes impractical and a dropdown unwieldy. A text icontains search is likely the better UX here — but confirm this is an intentional divergence from the geography approach before implementing.

Acceptance Criteria

  • Entering a partial committee name narrows results via case-insensitive substring match on meta.committee_name
  • Empty input applies no filter
  • Filter value survives pagination and sort changes
  • Input reflects active value from URL on page load

feat: Document Type filter on search page

Parent epic: #298 Re-enable filters on site search
Part of: Search filters breakdown
Effort estimate: 2


Description

Add a Document Type checkbox group to the /search/ filter panel. Allows filtering by agenda, agenda_packet, and/or minutes. Multiple selections are OR'd together.

DB values are confirmed lowercase from pipeline fixtures: agenda, agenda_packet, minutes.


Files to change

  • agendawatch/views.py — parse doc_type via request.GET.getlist("doc_type"); apply .filter(meta__asset_type__in=doc_types) when one or more values are present; pass list back in context
  • agendawatch/templates/agendawatch/search.html — add Document Type accordion group with three checkboxes (name="doc_type", values agenda, agenda_packet, minutes); check each box if its value is in the active context list

Open Questions

  1. Hardcode the list — don't query the DB — Unlike geography filters, the three document types (agenda, agenda_packet, minutes) are a known, pipeline-defined constant. The checkbox labels should be hardcoded in the template rather than queried from the DB on every load. A distinct() query on a JSONField path is a full-table scan and unnecessary here.

    The open question is: what happens if an unknown asset type appears in live data? Documents with unlisted types will silently disappear from results when any type checkbox is active. Before hardcoding, run AgendaWatchDocument.objects.values_list("meta__asset_type", flat=True).distinct() against staging/production to confirm no other types exist in the wild. If they do, decide whether to add them to the UI or treat them as pipeline data quality issues to fix upstream.


Acceptance Criteria

  • Selecting Agenda filters to meta.asset_type == "agenda"
  • Selecting Agenda Packet filters to meta.asset_type == "agenda_packet"
  • Selecting Minutes filters to meta.asset_type == "minutes"
  • Selecting two or more types returns documents matching any of them (OR)
  • Active checkboxes are checked on page load
  • Deselecting all checkboxes removes the filter (no type constraint)
  • No results state still displays correctly

feat: Geography filters (State + Place) on search page

Parent epic: #298 Re-enable filters on site search
Part of: Search filters breakdown
Effort estimate: 3


Description

Add State and Place multi-select filters to the /search/ filter panel. Both filter via exact match against the document metadata. These are the first two fields in the Committee Geography accordion group.

Files to change

agendawatch/views.py

  • Parse state and place as lists via request.GET.getlist("state") / request.GET.getlist("place")
  • Apply ORM filters:
    • qs.filter(meta__state__in=states) if list is non-empty
    • qs.filter(meta__place__in=places) if list is non-empty
  • Query distinct state/place values to populate the select options; pass alongside selected values in render context
  • Use Django's low-level cache: cache.get_or_set("distinct_states", lambda: list(AgendaWatchDocument.objects.values_list("meta__state", flat=True).distinct()), timeout=3600) — same pattern for place. Cache is invalidated on next ingest cycle (max 1-hour staleness acceptable).

agendawatch/templates/agendawatch/search.html

  • Add a Committee Geography accordion group in the left-rail filter panel
  • Two <select multiple> fields inside the form: name="state", name="place"
  • Options populated from distinct DB values (passed in context)
  • Selected options pre-populated from active URL params on page load

Open Questions

  1. Multi-select UI component Resolved — use plain <select multiple> for both State and Place. No new JS dependencies. UI polish (Tom Select, etc.) can be revisited in a later sprint.

  2. How to populate select options — avoid querying on every load Resolved — use Django cache (cache.get_or_set, 1-hour TTL). No pipeline changes needed. Can be upgraded to a pipeline-maintained lookup table in a later sprint if staleness becomes an issue.

  3. Data quality of meta.state / meta.place Pre-implementation spike (not a blocker) — run AgendaWatchDocument.objects.values_list("meta__state", flat=True).distinct() against staging before writing the filter logic. If values are inconsistent (mixed case, abbreviations), normalise upstream in the pipeline rather than in the view. Time-box to 30 minutes; if clean, proceed.


Acceptance Criteria

  • Selecting one or more states narrows results to documents where meta.state is in the selected set
  • Selecting one or more places narrows results to documents where meta.place is in the selected set
  • Multiple selections within a single field return documents matching any of them (OR)
  • Selecting from both fields applies both filters together (AND)
  • Filter values survive pagination and sort changes (handled by JS param preservation from #305)
  • Selected options are reflected in the multi-select on page load
  • No selection = no filter applied for that field

feat: Meeting Date filters on search page

Parent epic: #298 Re-enable filters on site search
Part of: Search filters breakdown
Effort estimate: 3


Description

Add date filtering controls to the /search/ filter panel under a Meeting Date accordion group. Supports a preset toggle (Past / All / Future) and explicit date range pickers (Start Date, End Date).

meta__meeting_date is stored as a string in YYYYMMDDTHHMM format, which is lexicographically sortable, so string-based __gte / __lte / __lt ORM lookups work correctly without casting.

Files to change

agendawatch/views.py

  • Parse date_preset (past | future | all), start_date, end_date from request.GET
  • Apply filters:
    • pastmeta__meeting_date__lt=today_str
    • futuremeta__meeting_date__gte=today_str
    • start_datemeta__meeting_date__gte=start_date (formatted as YYYYMMDDT0000)
    • end_datemeta__meeting_date__lte=end_date (formatted as YYYYMMDDT2359)
  • Default date_preset to all if not provided
  • Pass all four values back in render context

agendawatch/templates/agendawatch/search.html

  • Add Meeting Date accordion group in the filter panel
  • Radio group name="date_preset" with options: Past / All (default, checked) / Future
  • <input type="date" name="start_date"> and <input type="date" name="end_date">
  • All inputs pre-populated from context values on page load

Open Questions

  1. Timezone handlingdatetime.today() in the view uses the server timezone (currently UTC per settings.TIME_ZONE). Meeting dates are local events, so a user in California and one in New York could see different "Past/Future" cutoffs. Decide whether to: (a) keep UTC as-is and document it, (b) use the user's browser timezone (requires JS to pass it to the server), or (c) use a fixed US timezone. For a first pass, UTC is probably acceptable — but worth a conscious decision.

  2. Preset + date range interaction edge cases — the spec says they compose (AND), which is the correct general behavior, but some combinations are confusing:

    • "Future" preset + start_date in the past → future constraint wins, start date has no visible effect
    • start_date > end_date → returns zero results with no error message Decide whether to add client-side validation or just let the zero-results state speak for itself.
  3. Date picker value format<input type="date"> submits YYYY-MM-DD but meta__meeting_date is stored as YYYYMMDDTHHMM. The view must convert before filtering (e.g. "2026-02-24""20260224T0000" for gte, "20260224T2359" for lte). This is noted in the spec but should be covered by a unit test.


Acceptance Criteria

  • Past preset shows only documents with meeting_date before today
  • Future preset shows only documents with meeting_date on or after today
  • All (default) applies no date constraint
  • Start date picker limits results to meeting_date >= start_date
  • End date picker limits results to meeting_date <= end_date
  • Preset and date pickers compose — e.g. Past + start date narrows the past window
  • Filter values survive pagination and sort changes
  • Inputs reflect active values from URL on page load

epic: Upgrade Django 4.2 → 5.2 LTS

Standalone epic
Related: #298, #305
Effort estimate: 3


Description

We're on Django 4.2.* (LTS, EOL April 2026). We're using a hand-rolled mergeParam() JavaScript workaround to preserve URL query params across pagination and sort links — because the built-in {% querystring %} template tag only exists in Django ≥ 5.1. This upgrade removes that workaround and moves us onto the 5.2 LTS track (EOL April 2028).

Python 3.13 and PostgreSQL 17 (local) are already above the 5.2 minimums. Confirm Cloud SQL instance is PostgreSQL ≥ 14 before upgrading (Django 5.2 drops PG 13 support).

Two packages need version bumps alongside Django:

  • django-storages[google] — bump 1.13.1>=1.14 (Django 5.x support added in 1.14)
  • social-auth-app-django — bump 5.0.0>=5.4 (recommended for Django 5.x OIDC compatibility)

Pre-scan for deprecation warnings before bumping: python -Wd manage.py check && python -Wd manage.py test tests/ (pytest is not installed in the prod image; use Django's test runner)


Files to change

  • requirements.txt — bump Django==4.2.*Django==5.2.*; bump django-storages[google]==1.13.1>=1.14; bump social-auth-app-django==5.0.0>=5.4
  • agendawatch/templates/agendawatch/search.html — replace the mergeParam() JS block (added in #305) with native {% querystring %} template tag on pagination and sort links
  • accounts/middleware.py — verify no reliance on request.user being AnonymousUser before AuthenticationMiddleware runs (Django 5.0 changed this to None)
  • agendawatch/urls.py — smoke-test URL resolution after bump (Django 5.0 cleaned up dispatcher internals)

Acceptance Criteria

  • Cloud SQL PostgreSQL version confirmed ≥ 14 before deploying — resolved: agendawatch-prod and agendawatch-staging are both PG 14; aw-v2-prod / aw-v2-dev are PG 17. All clear.
  • python -Wd manage.py check produces no deprecation warnings from our own code — resolved: clean on 4.2 baseline (Feb 2026)
  • All existing tests pass (python -m pytest tests/) — resolved: 15/15 via manage.py test on 4.2 baseline (Feb 2026; pytest not in prod image, use Django test runner)
  • Django==5.2.* running in production without errors
  • mergeParam() JavaScript workaround removed from search.html
  • {% querystring %} used for all pagination and sort links
  • Squarelet OIDC login works end-to-end in staging (validates social-auth-app-django bump)
  • GCS file access works in staging (validates django-storages bump)

Open Questions

  1. What is the current Cloud SQL PostgreSQL version? Resolvedgcloud sql instances list confirms agendawatch-prod (PG 14) and agendawatch-staging (PG 14). PG 14 is the Django 5.2 minimum, so no DB upgrade needed. Not a blocker.

  2. Direct 4.2 → 5.2, or step through 5.0 / 5.1? Resolvedpython -Wd manage.py check and manage.py test both clean on the 4.2 baseline with zero deprecation warnings. No existing issues to surface incrementally. Proceed direct to 5.2.

  3. Sequencing with #298 filter sub-issues and #305. mergeParam() was introduced in #305 specifically because {% querystring %} wasn't available yet. If this epic lands first, #305 and the filter sub-issues can use {% querystring %} natively and mergeParam() never needs to be removed. If #305 merges before this epic, mergeParam() cleanup becomes an explicit step here. What order do we want these merged?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment