Architecture
Version 1 · Initial detailed architecture doc: request/auth flow, data layer, caching, deployment
Runner Form — Architecture
Request flow
- Browser sends an HTTP request to Django.
config/urls.pyincludesrunner_form.urlsat the/form/prefix.@custom_login_requiredchecksrequest.session['is_authenticated']; redirects to the login page if absent.- The view function calls one or more functions in
queries.py. queries.pychecks the cache first; on a miss it runs raw SQL againstqdbviadjango.db.connection, normalises the rows, and caches the result.- The view assembles a context dict and renders the matching template.
- The rendered HTML (or JSON, for
/api/*) is returned to the browser.
Authentication flow
Custom session auth — not Django's User model or django.contrib.auth.
- User submits the login form.
views.login_view()compares the POSTed credentials against a hard-coded check (username == 'admin' and password == <dev password>, seeviews.py— value not reproduced here).- On success,
request.session['is_authenticated'] = Trueandrequest.session['username']are set; the user is redirected to/form/runner/. @custom_login_requiredgates every other view (except the public/api/search/) on that session flag.SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'— sessions live entirely in a signed browser cookie, no DB session table.- Logout calls
request.session.flush().
This must be replaced with real user accounts or OAuth before any shared/production deployment — see [[bugs]].
Data layer
No Django ORM models. runner_form/queries.py (~1,440 lines) owns all SQL:
_fetchall(sql, params)/_fetchone(sql, params)— thin wrappers overdjango.db.connection.cursor(), returninglist[dict]/dict | None._base_run_select(where_sql)— the one wide query (joined tojockey,race,meeting,country,courses,generic_lookup) that every runner/jockey/trainer/meeting view builds on._normalise_runs(rows)— addsdisplay_*fields and computed flags (is_winner,is_placed,gear_list).compute_stats(runs)— pure-Python aggregation into career/last5/by-distance/by-track/ by-class/by-country/by-jockey/by-trainer/by-sot buckets.
See [[database-schema]] for the tables and columns, and [[api-reference]] for what each endpoint returns.
Caching
Django's default in-memory cache (per worker process). Every cache key is prefixed
runner_form: and most are version-suffixed (v2, v3, v7, …) so a query/shape fix can be
force-invalidated by bumping the suffix rather than flushing the cache. Full key/timeout table
in [[api-reference]].
Per-process caching does not share state across multiple IIS/wFastCGI workers — switching
CACHES['default'] to django-redis is the documented fix for multi-worker production
deployment.
Tech stack
| Component | Technology |
|---|---|
| Web framework | Django (Runner_Form_Project_Documentation.docx records 5.2.8; CLAUDE.md records 6.0.6 — reconcile against the active venv before relying on either) |
| Language | Python 3.10+ |
| Database | MySQL, database qdb @ 138.226.220.168:3306, driver mysqlclient |
| Frontend | Django Templates + vanilla JS, no framework |
| Sessions | Signed cookies (no DB writes) |
| Cache | Django in-memory (default) — Redis recommended for production |
| Static files | Django staticfiles in dev; IIS/nginx in production |
Deployment (IIS / Windows Server)
- Install Python and pip on the server.
pip install wfastcgi, then runwfastcgi-enable.- Enable the FastCGI feature in IIS Manager.
- Add a
web.configpointing IIS atwfastcgi.pywithWSGI_HANDLER=config.wsgi.applicationandDJANGO_SETTINGS_MODULE=config.settings. python manage.py collectstatic, then grantIIS_IUSRSread access tostatic/.
A static/web.config already exists in this repo for static file serving.
Production hardening
See [[bugs]] for the open items (hard-coded login, plaintext DB credentials, single-process cache) that block a real production deployment, and [[user-guide]] for day-to-day setup and troubleshooting.