Expand {{ standings }} token in admin emails to a styled table #483
No reviewers
Labels
No labels
Kestra
bug
enhancement
someday
subtask
☁️ api
🎛️ infrastructure
🐞 sentry
📆 2025 Season
📝 pages
allpicks
📝 pages
picks
📝 pages
standings
🚀 performance
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
johnsturgeon/tgfp-web!483
Loading…
Reference in a new issue
No description provided.
Delete branch "create-a-form-page-for-sending-out-the-standings-233"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Adds a
<!-- STANDINGS -->token to the admin custom-email composer. When present in the Markdown body, it expands to the current standings as an inline-styled HTML table that mirrors the/standingspage — gray header, zebra rows, maroon names — styled inline so it survives email clients (no external CSS, no<style>block).No UI change to the compose page beyond one new row in the Markdown cheat sheet documenting the token.
How
app/standings_email.py(new):standings_markdown(session)builds a Markdown pipe table from active players (same query/sort as/standings);style_email_table(html)injects inline styles into markdown-it's table output.app/mailer.py:custom_email_bodydoes the token substitution — it renders the standings fragment in isolation and styles it before the outer email template wraps everything in layout tables, so the styler never touches the presentation tables.send_custom_emailforwards the pre-built markdown.v2.py) and background send (send_admin_email.py) each build the standings lazily — only when a message uses the token — so a token-free batch never depends on a current week existing (which would raise between seasons).Testing
tests/test_mailer.py: token present → styled<table>/<th>; token absent → body unchanged.test_admin_email_routes.pyfailures are pre-existing onmain, unrelated to this change.resolves #233
🤖 Generated with Claude Code
Summary
This PR adds a
<!-- STANDINGS -->token to admin custom emails that expands to an inline-styled standings table mirroring the/standingspage, with the standings built lazily only when a message actually uses the token. The implementation is clean, mirrors the existing standings route closely, and the substitution logic is sound; I found no blocking issues, just a few minor points worth noting.Worth fixing
app/standings_email.py:25— a nickname containing|breaks the generated table. The row is assembled as an f-string of pipe-delimited fields, so any|inplayer.nick_nameinjects an extra column and corrupts markdown-it's table parsing for that row.nick_nameis admin-controlled (set viav2.py:423/486), so this is low risk, but a one-line escape (player.nick_name.replace("|", "\\|")) would harden it. Relatedly, note thatMarkdownIt("commonmark")runs withhtml: true, so raw HTML in a nickname passes through into the email unescaped — unlike the/standingspage, where Jinja autoescapes{{ player.nick_name }}. Again low risk given admin-only input, but it's a behavioral difference worth being aware of.Nits
N+1 query pattern in
app/standings_email.py:29-31.wins_for_week,losses_for_week, andbonus_for_weekeach independently callpicks_for_week, so every player costs three separate queries for the "Last W/L/B" columns. This exactly matches the existing/standingspage (standings.j2:56-58), so it's consistent with current conventions rather than a regression — but since this now also runs inside the backgrounddrain_batchsend loop, it's worth flagging that a large active-player list multiplies the query count per email batch. Both call sites run these sync queries on the event loop, which is the established pattern in this codebase.Test coverage gaps. The new tests (
tests/test_mailer.py) cover the token-present/token-absent substitution well, but a couple of paths are untested:style_email_table's zebra striping across multiple rows (the test fixture has a single data row), and the lazy-build/caching behavior indrain_batch(send_admin_email.py:15-24), including that a token-free batch never callsstandings_markdown. The description notesstandings_markdownis DB-dependent and hard to unit test, which is fair; a small test assertingdrain_batchdoesn't invoke it when no job carries the token would be cheap insurance.app/mailer.py:88-92renders the body twice on the token path (md.render(body_markdown)is effectively computed once here, fine) — actually only the branch structure is slightly redundant. Purely cosmetic; the current form is clear enough.Overall this looks good and matches the
/standingslayout, zebra rows (i % 2 == 0→ highlighted, same asrow_with_style), and sort order faithfully. The lazy-build design correctly avoids depending on a current week for token-free batches.