Email the pool when the picks page opens #457
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!457
Loading…
Reference in a new issue
No description provided.
Delete branch "re-automate-the-picks-page-is-ready-email-209"
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?
The Discord announcement went out on the odds lock but the email never did, so
anyone not in the Discord found out by checking the site.
Same transition, two channels
Rule 4 already dispatched the Discord announce;
launch_announce_picks_opennow schedules both. Separate job ids --
announce_picks_page_is_open_discordand
..._email-- so one failing leaves its ownjobstaterow and does nothide the other.
Still exactly once per week, because the reconciler that flips
odds_statefalsifies its own condition.
Layering
send_picks_page_ready_email(first_name, recipients)takes the same shape assend_welcome_email. The job owns the session and the loop over activeplayers:
That keeps
app/mailer.pyfree of the database -- noSession, noengine,no
Player-- and puts the session on the entry point, where the rest of thecodebase keeps it. Same split as #453: the mailer knows how to send, the caller
knows who to send to.
asyncio.runat the job boundary becausefastapi_mailis async andAPScheduler runs jobs in a thread pool.
Template
email_picks_page_ready.j2was one line; it now matches the welcome mail'sstructure and links to the picks page. Its URLs are hardcoded for now, the same
way the nag bot and the Discord announce already do -- #456 collects all of
them behind
SITE_BASE_URL.Tests
The two lock tests now assert both dispatches, in order, which pins that the
lock announces on both channels exactly once.
284 passing.
resolves #209
🤖 Generated with Claude Code
This is a clean, well-scoped change. I read the actual files (
app/jobs/announce_picks_page.py,app/mailer.py,app/models/player.py) plus the dispatcher test wiring to confirm the behavior.This PR adds an email announcement alongside the existing Discord one when the picks page opens, splitting them into two independent scheduler jobs so a failure in one doesn't hide the other. The layering matches the
#453convention (mailer stays DB-free, the job owns the session), the per-player error handling keeps one bad address from stranding the rest of the roster, and the test coverage is genuinely good.The change looks good to merge. A couple of small observations, none blocking.
Nits
app/jobs/announce_picks_page.py:45-47— the session is closed before the players are iterated, so the emails are sent against detached ORM instances. This is safe here (the accessed fields —first_name,email,nick_name— are plain columns already loaded byactive_players, and there's nocommit()to expire them), so noDetachedInstanceError. Worth being aware of if a future edit ever touches a relationship or lazy-loaded attribute inside_email_every_active_player; at that point thewithblock would need to wrap the loop.The PR description's code snippet shows
asyncio.run(...)inside the per-player loop, but the merged code correctly wraps the whole roster in a singleasyncio.run(_email_every_active_player(...))(andtest_one_event_loop_for_the_whole_rosterpins exactly that). Just a stale description — the code is the better version.announce_picks_page_to_discordstill has no error handling, so a webhook failure fails that job (leaving its ownjobstaterow). That's consistent with the PR's stated "separate job ids, one failing doesn't hide the other" design and is pre-existing, so it's fine — noting only that the two channels are now asymmetric: the email job can never fail the whole job (all per-player exceptions swallowed), while the Discord job can. That asymmetry is intentional and reasonable.Test coverage
Coverage is solid: active-only filtering, empty roster, single-event-loop, and the partial-failure resilience case are all exercised in
tests/test_announce_picks_page.py, andtests/test_dispatcher.pyconfirms both job ids are dispatched in order on the lock transition (thedispatchedfixture patchesadd_job, solaunch_announce_picks_openadding both jobs is genuinely exercised). No gaps worth blocking on — the untested pieces (announce_picks_page_to_discord, the.j2render) were untested before this PR too.