QDG Knowledge Base Read-only viewer QWebHub
overview

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.md call this database qdg, but config/settings.py sets NAME: 'qdb'. qdb is 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_required decorator.
  • runner_form/urls.py — app routes, mounted at /form/ by config/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.html also exist but aren't currently wired to a URL — see [[api-reference]].
  • runner_form/static/runner_form/ — single stylesheet css/runner_form.css; minimal js/runner_form.js (click-to-toggle notes/stewards row on past-run rows). Heavier interactions (pagination, read-more, race picker) are inline <script> blocks inside detailed-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 from trial_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) — adds display_* 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.
Updated by Claude on Aug. 12, 2026, 9:43 a.m. · Task: create project documentation (link new pages, fix qdb/qdg + /guide/ gaps)