Overview
Version 1 · Initial project documentation created from CLAUDE.md, PLAN.md, and README.md
Historical versionRunner 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 |
The index route (/form/runner/) redirects to a default example horse.
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 "qdg" DB (138.226.220.168:3306)
tables: runner, run, race, meeting, jockey, trainers, courses, country,
trial_race, trial_run
│
▼
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.
Components
config/settings.py— DB config, installed apps, cache, static files.runner_form/queries.py— all SQL queries and Python-side stats computation.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,error.html.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.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
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]].
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.
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; the app falls back to
run.pos_1200, run.pos_800, run.pos_turn, run.pos_settling.
See [[quick-reference]] for routes and commands, and [[bugs]] for open issues and production hardening items.