Overview
Version 2 · Corrected DB name to qdb (verified against config/settings.py), added the /guide/ form-guide feature, linked new user-guide/architecture/database-schema/api-reference pages
Runner Form
A Django horse-racing form viewer. Users log in and browse runner (horse), jockey, and trainer profiles with race-by-race history and computed career stats.
Purpose
| Entity | URL pattern | What it shows |
|---|---|---|
| Runner (horse) | /form/runner/<horse_id>/ |
Past runs, career stats, brief or detailed tab (?tab=brief|detailed, ?date=, ?race=) |
| Jockey | /form/jockey/<jockey_id>/ |
Ride history, strike-rate, top runners/trainers |
| Trainer | /form/trainer/<trainer_id>/ |
Training history, strike-rate breakdowns |
| Form Guide | /form/guide/ |
Today's (or ?date=) race meetings and races by venue, ?country= filter, click a race to see runners inline |
The index route (/form/runner/) redirects to a default example horse (WINX).
Architecture
Browser
│ HTTP GET/POST
▼
views.py HTTP handlers, @custom_login_required decorator
│
▼
queries.py raw SQL via django.db.connection, no ORM models
│
▼
MySQL "qdb" DB (138.226.220.168:3306)
tables: runner, run, race, meeting, jockey, trainers, courses, country,
trial_race, trial_run, trial_meeting, sales, sales_lots,
sales_lookup, generic_lookup
│
▼
Django Templates (.html) + static CSS/JS
There is no Django ORM — all DB access is raw SQL via django.db.connection inside
queries.py. The two helpers _fetchall(sql, params) and _fetchone(sql, params) return
list[dict] / dict.
Naming note:
CLAUDE.md/PLAN.mdcall this databaseqdg, butconfig/settings.pysetsNAME: 'qdb'.qdbis what the app actually connects to — see [[database-schema]] and [[bugs]].
Components
config/settings.py— DB config, installed apps, cache, static files.runner_form/queries.py— all SQL queries and Python-side stats computation (~1,440 lines).runner_form/views.py— HTTP handlers +@custom_login_requireddecorator.runner_form/urls.py— app routes, mounted at/form/byconfig/urls.py.runner_form/templates/runner_form/—login.html,brief-form.html,detailed-form.html(largest template),jockey_form.html,trainer_form.html,form-guide.html,error.html.meeting_list.html/meeting_detail.htmlalso exist but aren't currently wired to a URL — see [[api-reference]].runner_form/static/runner_form/— single stylesheetcss/runner_form.css; minimaljs/runner_form.js(click-to-toggle notes/stewards row on past-run rows). Heavier interactions (pagination, read-more, race picker) are inline<script>blocks insidedetailed-form.html.
Data layer patterns
get_runner_form_data(horse_id)— horse + runs + stats, cached 120s.get_jockey_form_data(jockey_id)— jockey + rides + stats.get_trainer_form_data(trainer_id)— trainer + entries + stats.get_trial_runs(horse_id)— barrier trial data fromtrial_race/trial_run.get_form_guide(date, country)/get_form_guide_dates()/get_form_guide_countries(date)— data behind the/guide/page.search_form(q)— searches horses / meetings / races by name, cached 120s.get_available_dates()— race dates for the date-picker, cached 300s._normalise_runs(rows)— addsdisplay_*keys + computed flags (is_winner,is_placed,gear_list).compute_stats(runs)— career/last5/by-distance/by-track/by-class aggregations.- Cache key pattern:
runner_form:<entity>:<id>, Django's default in-memory (per-process) cache backend.
JSON API (used by the templates' date/meeting/race picker via fetch())
/form/api/dates/ → available race dates
/form/api/meetings/?date= → venues for a date
/form/api/races/?date=&meeting= → races for a meeting
/form/api/runners/?race_id= → runners in a race
/form/api/search/?q= → search across horses, meetings, races
Full request/response schemas and caching details are in [[api-reference]].
Authentication
Custom session auth, not Django's built-in auth system. Login is a hard-coded
admin / dev password check in views.py; the @custom_login_required decorator checks
request.session['is_authenticated']. Must be replaced before production — see
[[bugs]] and [[architecture]].
Development
.\venv\Scripts\Activate.ps1
python manage.py runserver
# http://127.0.0.1:8000/
Hard-refresh (Ctrl+Shift+R) after static file changes to bypass the browser cache.
Stack: Django, MySQL (remote, no ORM), Django Templates, vanilla JS. Environment: Windows 11, Python venv, IIS/wFastCGI in production. See [[architecture]] for the full tech-stack table and deployment steps.
Known data gaps
trial_race.class_level, sot, and time_winner_sec are NULL for every row in the current
DB — these fields display as — until the source data is populated. run.vp_* sectional
columns (vp_1200/800/600/400/200) are NULL for pre-2021 races (confirmed by investigation); the
app falls back to run.pos_1200, run.pos_800, run.pos_turn, run.pos_settling.
Where to go next
- [[user-guide]] — setup, verification, day-to-day usage, troubleshooting.
- [[architecture]] — request/auth flow, data layer, caching, deployment.
- [[database-schema]] — tables and columns actually queried.
- [[api-reference]] — full endpoint request/response reference.
- [[quick-reference]] — condensed commands/routes/cache table for frequent lookup.
- [[bugs]] — open issues and production hardening items.