Gate the picks page and form on the odds being locked #440
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!440
Loading…
Reference in a new issue
No description provided.
Delete branch "fix-picks-page-to-only-show-if-the-odds-are-locked-for-that-week-424"
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?
Closes the last gap between what the picks page shows and what the picks form
accepts.
The page
Refuses when the odds are still
PRELIMINARY, when the week isALL_FINAL,when the player has already picked, and -- new -- when every game has kicked
off, which would otherwise submit a full slate of home teams nobody chose.
The form
Every rule the page applies at render time is re-checked at submit time. The
threat is not an adversary, it is a stale tab: open the page, make a sandwich,
submit into a world that has moved.
Started games no longer depend on JavaScript. Their radios render disabled and
send no key, so the absence of a key is the evidence:
Generated picks can be neither a lock nor an upset.
Upsets are now checked against the spread instead of trusting the dropdown, and
a value that is not a team id is treated as a missed pick rather than becoming
a
picked_team_idof 0.Player lookup
All eight routes resolve their player through
get_player. Six were still onverify_playerplus a manual lookup that 404s -- the dead endget_playerwaswritten in #403 to avoid, applied then to only two routes.
Tests
First route tests in the repo:
TestClientplus aget_sessiondependencyoverride onto the existing in-memory session fixture. Constructed rather than
entered as a context manager, since
__enter__runs the lifespan and startsAPScheduler against the real engine.
23 tests over the page, the form, stale submissions, malformed values, the lock
rules and postponed games. Several were written failing first and caught real
defects on the way: three guards that built a response and discarded it, an
upset check comparing a game id to a team id, and two paths where a submission
was rolled back by a swallowed
IntegrityErrorwhile the success pagerendered.
resolves #424
This is a careful, well-tested PR. I've verified the dependency chain (
get_player/verify_player), the state enums,is_pregame, the template's disabled-radio mechanism, and the form/page validation paths. My assessment:This PR closes the render-time/submit-time gap on the picks flow cleanly: every gate the page enforces (odds locked, week not over, nothing-left-to-pick, already-picked) is re-checked in
picks_form, and the disabled-radio → absent-key → server-fills-home-team mechanism is sound and well pinned by tests. The refactor to route all eight handlers throughget_player, the id-vs-team-id hardening (_form_int), and the first route-level test suite are all solid — I found no correctness or security bugs.The tests are unusually thorough: the stale-form, malformed-value, upset-namespace-collision, and postponed-game cases each target a distinct real defect and the reasoning in the docstrings holds up against the implementation.
Nits
app/main.py:425— the error-message path returns theerror_picks.j2response without an explicitsession.rollback(), even though rows were alreadysession.add()ed in the loop atapp/main.py:422. It's harmless becauseget_session'swith Session(...)discards uncommitted state on teardown, but it's inconsistent with the already-picked path atapp/main.py:407, which rolls back explicitly. An explicitsession.rollback()before returning would make the "nothing is written" guarantee local to the handler rather than dependent on the dependency's cleanup.app/main.py:382—_append_auto_picksdoesn't guardgame.id is None, whereas both_pg_pick_from_form(app/main.py:368) and the main form loop (app/main.py:417) do. Persisted games always have an id so this is theoretical, but the inconsistency is a small readability wart.app/main.py:200-209—_missed_a_pick(strict subset<) and_picks_not_valid(!=) each rebuild both id sets, and the latter subsumes the former. Fine as-is since they emit different messages and_missed_a_pickruns first, but a single set computation shared between them would be marginally clearer.picks_formisasync defbut does synchronous SQLAlchemy work (games_for_week,commit) that blocks the event loop, and/pickslazy-loadshome_team/away_team/underdog_team/favorite_teamper game (a modest N+1 in the template). Both are pre-existing and not introduced here — worth a follow-up if this pool ever grows, but not blocking.