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, andupdated_atare 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
MatchQuestionIssuancesnapshots (answer_choices,definition_snapshot).
Operator workflow¶
- 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. - 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.
- Select an existing definition or choose New definition.
- 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.
- Review Raw JSON when advanced control is needed.
- 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_countslots.1is the usual baseline;0excludes 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_stateat 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/awayfor a winner question. - Display text: player-facing label; may be static or a
store_statetemplate (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 /
*Nsyntax 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 usegame_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_statekeys 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/awaylabeled fromstore_stateteam 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
leaguesrow 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_statekey 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.