Skip to content

Match question definition editor

The workspace at /admin/match-question-definitions maintains effective MatchQuestionDefinition rows in PostgreSQL. These are Match questions (pre-game, Play kind: "match"), not in-game listener props.

Authoring is league-scoped. The league filter includes MLB, NHL, and NFL.

For how they issue, lock, and resolve, also read Match question issuance & resolution. League cap, allowlist, grace, and earliest-issue: League Defaults. Per-match overrides and issued rows: Matches. To inspect league / match / definition points and allowlists: Question Inheritance (kind=match). To reconstruct why a published Match question issued: Issuance Explorer (kind=match).

Persistence model

  • Save writes only to PostgreSQL.
  • Edits are held as in-memory drafts until Save to DB.
  • ensure_match_questions / Beat consume database rows. Bulk create/update is also available via Export to CSV / Import from CSV (preview then confirm). There is no JSON seed import path for Match question definitions.
  • id, league_id, created_at, and updated_at are display-only in the editor and cannot be overwritten through Save to DB or CSV import.
  • Definitions are unique by name within the selected league at the API layer.
  • Changing a definition does not rewrite already-issued MatchQuestionIssuance snapshots (answer_choices, definition_snapshot).

Operator workflow

  1. Open /admin/match-question-definitions (Manage → Match Questions). A deep link (?league=mlb&id=<definition uuid> or ?folder=Pre-game) opens that definition’s editor or folder after the list loads.
  2. Use the filter bar above the list (League, Search, Show archived). Switch League to load that league’s definitions. Browse definitions in the folder tree. Click a folder or breadcrumb to drill in; drag a definition or folder to move it (cannot drop a folder into itself). Folder moves are drafts until Save to DB. Click a definition to open the editor. New definition inherits the folder you are browsing.
  3. Select an existing definition or choose New definition.
  4. Work through the guided tabs. Switching definitions or starting a new row prompts to discard if the current draft is dirty; each dirty rule shows an unsaved indicator in the sidebar.
  5. Review Raw JSON when advanced control is needed.
  6. Choose Save to DB. The current draft is POST/PUT immediately (no multi-definition confirm modal). A toast reports success or validation errors.

Unsaved drafts are protected when leaving the page (refresh or close). Clone / Archive / Reactivate / Delete sit on the editor header (same semantics as in-game definitions; delete is blocked while issuances exist).

Export to CSV downloads every Match question definition across all leagues (including archived). Columns are the live match_question_definitions table fields plus a league slug; JSON columns are written as JSON strings. The same file is GET /api/match-question-definitions/export.

Import from CSV uploads that same export-shaped file. The API validates every row and shows creates, field-level updates, and errors before anything is written (POST /api/match-question-definitions/import?dry_run=true). Confirm applies all creates and updates in one transaction (dry_run=false); any row error refuses the whole import. Blank id creates a definition; an id that is not in the database is an error. Rows missing from the file are not deleted. Changing a definition does not rewrite already-issued Match question snapshots.

Keep JSON cells intact. Desktop spreadsheet apps often break JSON and UUIDs.

There is no Copy all JSON, Field browser, or Issuance timing tab. Match questions are not selected from live stream events.

Overview

  • Name: operations label, unique within the league; not player-facing.
  • Folder: slash-separated admin path (empty = unfiled). Organization only. Unlimited nesting. Match questions have no issuance group.
  • Active: controls whether the definition may be selected for new issuances. Deactivating does not remove existing issuances.
  • Issuance weight: relative selection weight when more eligible definitions exist than remaining match_question_max_count slots. 1 is the usual baseline; 0 excludes the definition. This is not in-game listener weighted selection (no groups, probability, or period caps).
  • Override question points: optional question-effective points. Leave empty to inherit match override → league default. Absolute (e.g. 10) or multiplier of the effective match/league value (e.g. *2).
  • Player-facing copy — question / success / failed text. Built-in home/away names are always in store_state at issue; chips insert those keys. Extra extract keys from the Resolution tab also appear as chips.

Answers

Each answer choice includes:

  • Identifier: stable stored answer value, commonly home / away for a winner question.
  • Display text: player-facing label; may be static or a store_state template (e.g. {{$.store_state.home_team_full_name}}).
  • Correct condition: expression evaluated against the official payload (plus winner / scores / store_state), or against a stream close event if a trigger fired.
  • Override points: optional; same absolute / *N syntax as Overview. Blank inherits the question-effective value.

Choice order in the editor is player-facing order. Avoid changing identifiers after users have submitted answers. An active definition needs at least one choice template (operators typically add two).

Resolution

After the match ends, a named official source is fetched and each answer’s correct_condition is evaluated. Optional stream triggers can close earlier from a live push event. There is no in-game correction window.

  • Official source (resolution_source): registry key for the game-end fetch. Winner / team-total questions typically use game_summary. Other keys (game_boxscore, daily_schedule, stream) appear in the select and field-help even when a fetcher is not registered yet.
  • Stream close triggers (optional): JSONPath conditions on the current push event. Leave empty unless a mid-game event can decide the question. Empty is correct for Who will win the match. Conditions in the list are OR'ed.
  • Issuance state extract: extra store_state keys from the match issue context ($.match.*, $.store_state.<key>). Built-in home/away names, ids, and aliases are always present — use no rows when those are enough. Rows execute top to bottom; later rows may read earlier keys through $.store_state.<key>.

Open the ? help on Official source, Stream close triggers, or Issuance state extract for the same catalog in field_help.js.

Built-in issue-time store_state keys (always present, no extract row needed):

Key Meaning
home_team_full_name / away_team_full_name Market + name
home_team_name / away_team_name Team name
home_team_alias / away_team_alias Abbreviation
home_team_id / away_team_id Internal ids
home_team_sr_id / away_team_sr_id Sportradar ids
match_id / match_sr_id / match_scheduled / match_status / league_id Match identity

Raw JSON

Raw JSON exposes the writable row shape (same body as create/update):

  • Format: prettifies valid JSON.
  • Reset from form: discards raw-editor changes and regenerates JSON from guided controls.
  • Apply JSON → form: parses valid JSON into guided controls.
  • Copy JSON: copies the current raw text.
  • Save to DB: validates and persists editable fields for the current draft.

Invalid JSON remains in the editor with an error and is not applied.

Example: Who will win the match

Author this as a normal Match question definition (not a special template):

  • Overview question: “Who will win the match?”
  • Answers home / away labeled from store_state team names
  • correct_condition: {{$.winner}} == 'home' / 'away'
  • resolution_source: game_summary
  • Empty stream triggers
  • No extra extract rows

REST API

Match question definition CRUD lives outside /admin, as a normal REST resource. Mutating routes require an admin API key.

Method Path Purpose
GET /api/match-question-definitions?league={league} List definitions
POST /api/match-question-definitions?league={league} Create
GET /api/match-question-definitions/{definition_id} Read one
PUT /api/match-question-definitions/{definition_id} Replace editable fields
POST /api/match-question-definitions/{definition_id}/clone Clone
POST /api/match-question-definitions/{definition_id}/archive Set inactive (hidden from default list)
DELETE /api/match-question-definitions/{definition_id} Hard-delete when no issuances
POST /api/match-question-definitions/{definition_id}/activate Reactivate
GET /api/match-question-definitions/export CSV of every league (including archived)
POST /api/match-question-definitions/import?dry_run= Preview (true) or apply (false) that CSV

definition_id is the internal Match question definition UUID. List/create are league-scoped via the league query parameter (include_inactive=true shows archived). Get/update/clone/archive/activate/delete are ID-scoped.

There is no preview or sample-catalog endpoint (no Field browser).

Code map

Concern Location
REST routes src/api/routes/match_question_definitions.py
Admin HTML page src/api/admin/routes/match_question_definitions.py
Workspace template src/api/admin/templates/match_question_definitions/workspace.jinja2
Editor state src/api/admin/static/js/match_question_definitions_app.js
Shared QD CSS src/api/admin/static/css/question_definitions.css
CRUD helpers src/lib/match_question_definitions/
CSV import src/lib/csv_import.py
Request/response schemas src/lib/match_question_definitions/schemas.py
Issue-time store_state src/lib/match_question_issuances/store_state.py
Ensure / lock / official / stale src/lib/match_question_issuances/
Field help resolution_source, match_question_*, override_question_points in field_help.js
API tests src/api/tests/test_match_question_definitions.py
Admin HTML tests src/api/tests/admin/test_match_question_definitions.py

Common failures

  • MLB league not found: seed the leagues row before using the editor.
  • Duplicate name: choose a unique name within the league.
  • Save returns validation errors: inspect nested trigger and answer-choice fields; Raw JSON must match the Pydantic schema. Active definitions need answer choices; blank identifiers/text are rejected.
  • Cannot delete: issuances still reference this definition — Archive instead.
  • Placeholders stay in copy: verify store_state key spelling (built-in team keys vs extract rows on Resolution).
  • Questions not appearing on Play: the definition must be active and allowed by league/match allowlist; Beat only issues inside the earliest-issue window, and not after lock or once in-game questions exist. See the issuance & resolution guide.