Skip to content

Release: develop to main (Oct 22, 2026) — judging locks, progress, bulk assignments - #308

Merged
gregv merged 3 commits into
mainfrom
develop
Oct 23, 2026
Merged

gregv merged 3 commits into
mainfrom
develop

Conversation

@gregv

@gregv gregv commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Everything on develop since the Oct 16 release (#306): PR #307, 3 commits. Backend half of the Fall 2026 judging overhaul — merge and deploy this before the frontend release (frontend-ohack.dev develop → main). Fly auto-deploys main. No new env vars. One new Firestore collection, judging_round_locks, created on first use — no migration.

Release notes

Judging (#307)

  • Round locks. Admins can close scoring per round. While locked, POST /api/judge/score and POST /api/judge/draft answer 409 round_locked; the lock is read uncached on every write, so it takes effect immediately. New routes: GET /api/judge/locks/<event> (public shape, no admin identity) and POST /api/judge/admin/locks/<event>/<round> (judge.admin, Slack-audited).
  • Judge progress. GET /api/judge/admin/progress/<event>/<round> returns who has submitted, who is mid-draft and who hasn't started, per judge and per team, in one call.
  • Bulk assignments. POST /api/judge/admin/assignments/bulk creates every judge × team pair for a panel in one request (idempotent; replace prunes pairs no longer on the panel; empty or mistyped input is rejected so a client bug can't wipe a panel).
  • The judge teams payload now carries the event title and lock state, resolves team members with one batched read instead of one per member, and get_teams_batch chunks Firestore in queries by 30.
  • Fixes: score and draft writes now require an assignment for that event and round — a Round-1-only judge could previously post a Round 2 score that counted in results. A failed Firestore read is no longer treated as "no rows" (it used to risk re-inserting every assignment or showing an empty progress board).

Prod checklist (after Fly deploys main)

  1. GET https://api.ohack.dev/api/judge/locks/fall-2026 with a judge token → 200 {"locks": {"round1": {"locked": false, …}, "round2": …}}.
  2. Admin: POST /api/judge/admin/locks/fall-2026/round2 {"locked": true} → 200; a judge's round 2 score POST → 409 round_locked; unlock → 200 again.
  3. GET /api/judge/admin/progress/fall-2026/round1 (admin) → judge rows with assigned / submitted / missing_team_ids.

🤖 Generated with Claude Code

gregv and others added 3 commits October 21, 2026 23:04
…, batched member lookup

- judging_round_locks collection read uncached on every judge write; POST /score
  and /draft answer 409 round_locked once an admin locks the round (fail-open on
  read errors). GET /api/judge/locks/<event>, POST /api/judge/admin/locks/<event>/<round>.
- GET /api/judge/admin/progress/<event>/<round>: judge x team submitted/draft/
  missing matrix + summary + lock, computed server-side in 3 queries.
- POST /api/judge/admin/assignments/bulk: idempotent cross-product assignment
  creation with optional replace (prunes pairs outside the panel's set).
- get_judge_teams embeds event summary + locks and resolves members with ONE
  db.get_all instead of a serial get_user_by_id per member per team.
- get_teams_batch chunks the __name__ in query by 30 and dedupes ids.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ignment check, strict bulk input, raise on failed reads, public lock view

- /score and /draft now require an assignment for THIS event + round
  (is_judge_assigned_for_round); the any-round check let a Round-1-only judge
  post a Round 2 score that counted in results.
- bulk-assign: replace must be a JSON bool, empty judge/team lists are rejected
  (empty_cross_product — replace would otherwise wipe the panel), ids must be
  non-empty strings, room/demo_times typed; replace collapses duplicate docs.
- _rows() raises on a fetcher's {error} result instead of treating a failed
  Firestore read as no rows (bulk would re-insert everything; progress would
  show an empty board mid-round).
- GET /locks and the judge teams payload use public_lock_view (no locked_by /
  locked_by_name — PropelAuth ids are PII); admins keep the full lock.
- Dead except-TypeError branch removed; _locks_db renamed _db.
- Tests: batched member lookup (one get_all, missing docs dropped, failure →
  per-user fallback), get_judge_teams shape (event + public locks + round2
  slot preference), failed-read paths, bulk validation, duplicate collapse,
  round-scoped 403 at the route.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ress

feat(judging): round locks, judge progress endpoint, bulk assignments — backend for the Fall 2026 judging overhaul
if is_round_locked(data['event_id'], data['round']):
info(logger, "Score rejected: round locked", judge_id=user_id,
team_id=data['team_id'], event_id=data['event_id'], round_name=data['round'])
return _round_locked_response(data['event_id'], data['round'])
return {"error": "Forbidden: Judge not assigned to this team for this round"}, 403

if is_round_locked(data['event_id'], data['round']):
return _round_locked_response(data['event_id'], data['round'])
"""Lock state for both rounds. Any signed-in user (judges read it to
render a read-only scoring page); it carries no scores."""
info(logger, "API called: GET /locks/{event_id}", event_id=event_id)
return {"locks": get_round_locks(event_id, public=True)}
result = set_round_lock(event_id, round_name, locked,
actor_id=user_id, actor_name=_actor_display_name(user_id))
if not result.get("success"):
return result, 400
actor_id=user_id, actor_name=_actor_display_name(user_id))
if not result.get("success"):
return result, 400
return result
return {"error": "invalid_round"}, 400
result = get_judging_progress(event_id, round_name)
if "error" in result:
return result, 500
result = get_judging_progress(event_id, round_name)
if "error" in result:
return result, 500
return result
@gregv
gregv merged commit 002c2b2 into main Oct 23, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants