Styling & the Gameweek/Season Selector UI
Premier League Predictor: Django & MySQL
Chapter 10 · Styling & the Gameweek/Season Selector UI
Every page this course has built since Chapter 4 has been visited against a specific, hardcoded gameweek_id or season_id baked straight into the URL. This chapter replaces that with a real, working selector — and gives the app the dark-theme styling it's been missing since Chapter 1. The selector itself, though, ends up looking genuinely different from the FastAPI sibling's own version, for a real, structural reason worth walking through rather than skipping past.
A Server-Rendered Selector, Not a Client-Side One
plpredict-fastapi1's own fixture_entry.js fetches its team list from a JSON route on every page load, because FastAPI's vanilla-JS frontend has nothing else to hand it that data — so its selector, quite reasonably, broadcasts a CustomEvent and lets JavaScript re-fetch and re-render whatever changed. Chapter 4's own fixture_entry view is built completely differently: it already renders the team grid, the gameweek, and everything else server-side, via {% for team in teams %}, before the page is ever sent to the browser. A season/gameweek selector for a page built this way doesn't need to swap data into an already-loaded page at all — the far simpler, more idiomatic answer is real navigation: picking a different gameweek just loads a different, freshly server-rendered page.
A Context Processor: Every Page Gets the Selector's Own Data, Automatically
The FastAPI sibling's own real problem — letting fixtures.js, predictions.js, and both table scripts each react to a selection change without any of them needing to know about the others — still exists here, just one layer down the stack. Every page this course has built needs the same season/gameweek dropdown data available in its own template context, without every single view having to remember to fetch and pass it in by hand. Django's own answer to exactly that problem is a context processor:
request.resolver_match.kwargs is what makes this genuinely automatic: Django sets resolver_match on the request the moment its own URL routing has matched a view, well before any template ever renders — so selector_context() can look at whichever URL the current request actually hit, pull gameweek_id straight out of it if present, and resolve the right season from there. Any template rendered anywhere in the app — the fixture-entry page, a predictions page, either league table — automatically receives all_seasons/all_gameweeks/current_season without its own view ever mentioning the selector at all. That's the same real goal selection-changed serves for the FastAPI sibling — no page needs to know the selector's own internals — solved at the template-context layer instead of with a browser event, because Django's own rendering pipeline already has a real, built-in mechanism for exactly this.
The Selector Partial & Real Navigation
base.html includes it once, with {% include 'predictor/_selector.html' %} near the top of the page, so every template that extends base.html gets the same live selector for free. Switching the gameweek dropdown navigates directly to that gameweek's own fixture-entry page. Switching the season dropdown is one step more involved — there's no single "the" gameweek to jump to until one is picked — so it goes through one small, real redirect view instead:
plpredict-fastapi1's own Chapter 10 has to add GET /api/seasons and GET /api/seasons/{id}/gameweeks specifically to feed its own selector's two dropdowns. This chapter needs neither: all_seasons/all_gameweeks already arrive in every template's own context via selector_context(), rendered directly with {% for %} the same way Chapter 4's own team grid was — the exact same "server-side rendering skips a whole client-side fetch" pattern Chapter 4's own finding-box already established, showing up here a second time.
Resolving Chapter 4's Own Flagged Gap — Server-Side, Not With a Re-Fetch
Chapter 4 was honest that usedTeamIds only lived in the page's own JavaScript memory, empty on every reload and reflecting only fixtures added during that exact visit. The FastAPI sibling fixes this with a live client-side re-fetch on every selection change. Django's own fixture_entry view already queries the database directly on every render — the far more direct fix is to compute the real, correct set of used teams there, and bake it straight into the HTML the server sends:
fixtures.js then only needs to seed its own JavaScript Set from what the DOM already correctly shows, instead of trusting a variable that started life empty:
addFixture(), from Chapter 4, is updated to call disableUsedButtons() too, replacing its own original inline disabling loop — the same logic, now written once and shared, rather than kept as two near-identical copies.
used_team_ids is recomputed fresh from Fixture.objects.filter(gameweek=gameweek) on every request, not carried over in memory from a previous page view at all. The FastAPI sibling achieves the identical real outcome with a dedicated client-side re-fetch call on every selection change; this course achieves it by never letting the client-side state be wrong in the first place, since the server-rendered HTML was already correct the moment it arrived.
Real Styling
The same green accent (#44b78b/#7fd6b4) and dark surfaces (#0d1117/#161b22) used across every chapter of this course's own documentation — the finished app looks like a genuine continuation of the material teaching it, not a visually disconnected afterthought.
Where This Course Is Headed
Deployment — getting this app running somewhere real, not just localhost (Chapter 11); and a capstone integrating the finished predictor into the existing Astro-based site (Chapter 12).
Hands-On Exercises
Explain why this chapter's selector uses real page navigation plus a Django context processor rather than a JS CustomEvent the way the FastAPI/PostgreSQL sibling does, naming the specific architectural fact about fixture_entry (and every other page in this course) that makes navigation the more natural choice here.
📄 View solutionTrace through exactly what selector_context() does when a request hits /gameweeks/5/fixtures/, explaining how it correctly resolves gameweek 5's own season for the season dropdown even though the function's own signature never receives a season_id directly.
📄 View solutionBuild the selector into a real setup with at least two seasons and two gameweeks, enter a fixture in one gameweek, navigate to the other gameweek and back using only the selector dropdowns, and confirm the previously-entered fixture's own two teams are still shown disabled — then explain specifically why this works even though no client-side state was ever preserved across the navigation at all.
📄 View solutionChapter 10 Quick Reference
- Real navigation, not a CustomEvent — fixture_entry is already fully server-rendered per gameweek, so a different selection is just a different page
- selector_context() — a Django context processor giving every template access to all_seasons/all_gameweeks/current_season automatically, the backend-layer analog to the sibling's own selection-changed event
- request.resolver_match.kwargs — how the context processor knows which season "belongs" to the current URL with no view needing to say so explicitly
- season_default_gameweek — a small redirect view handling the "season picked, no gameweek chosen yet" case
- Two JSON routes skipped — no GET /seasons or GET /seasons/{id}/gameweeks needed at all, the same server-rendering payoff Chapter 4 already established for the team grid
- usedTeamIds, fixed server-side — computed fresh in fixture_entry on every request and baked into the disabled attribute directly, rather than re-fetched by the client
- Styling — real, working dark-theme CSS reusing this course's own documentation accent colors, deliberately not a full design system
- Next chapter: Deployment