Django Admin as the Real Manual-Entry Tool for Teams & Fixtures

Premier League Predictor: Django & MySQL

Chapter 3 · Django Admin as the Real Manual-Entry Tool for Teams & Fixtures

Chapter 2 built the schema; this chapter turns Team, Season, SeasonTeam, Gameweek, and Fixture into real, working admin tooling — the actual real work happening for a project this admin-entry-heavy.

Team Admin: Search, Not Scroll

# predictor/admin.py from django.contrib import admin from .models import Team, Season, SeasonTeam, Gameweek, Fixture @admin.register(Team) class TeamAdmin(admin.ModelAdmin): list_display = ('name', 'short_name') search_fields = ('name', 'short_name')

search_fields does two real jobs at once: it powers the admin's own search box on TeamAdmin's list page, and — as covered below — it's the exact thing that lets other admin screens offer a real, searchable autocomplete for picking an existing team instead of a plain dropdown.

SeasonTeam as an Inline: A Real Django-Specific Payoff

plpredict-fastapi1's own Chapter 3 had to hand-write real POST/DELETE routes just to add or remove a team from a season. Django's admin gets the identical job largely for free, via a TabularInline nested directly on the Season edit page:

class SeasonTeamInline(admin.TabularInline): model = SeasonTeam extra = 1 autocomplete_fields = ('team',) max_num = 20 validate_max = True @admin.register(Season) class SeasonAdmin(admin.ModelAdmin): list_display = ('name', 'start_date', 'end_date', 'is_current') inlines = [SeasonTeamInline]

Opening a Season in the admin now shows every one of its current SeasonTeam rows as editable table rows right there on the page, with one blank row ready for the next addition — adding or removing a team is a normal save on the Season page itself, not a separate screen.

autocomplete_fields depends directly on TeamAdmin's own search_fields
autocomplete_fields = ('team',) turns the inline's plain foreign-key dropdown into a real, searchable, type-ahead widget — genuinely necessary once Team holds dozens of historical clubs, not just this season's 20. This isn't automatic in the abstract: Django's own admin requires the target model's admin (TeamAdmin) to define search_fields before autocomplete_fields can use it anywhere else — exactly why TeamAdmin's own search_fields, defined above for its own list page, turns out to matter well beyond that one page.
Verified: max_num alone never enforces the cap — only validate_max=True does
It's genuinely easy to assume max_num = 20 stops a 21st SeasonTeam row from ever being saved. Checked directly against Django's own documented formset behavior: max_num by itself only limits how many extra, blank forms the inline displays for adding new rows — it does not, on its own, reject a submission that somehow includes more rows than that. Real enforcement on save requires the separate validate_max = True flag above, mirroring the identical validate_max option Django's own formset factories expose. Without it, this "cap" is cosmetic — the same underlying lesson plpredict-fastapi1's own Chapter 3 delivered with an explicit COUNT() query before every insert, arrived at here from the opposite direction: a built-in-looking limit that turns out to need one more explicit setting before it's actually real.

Fixtures via the Admin: Real, Working, and Genuinely Slower

Fixture gets the same inline treatment, nested on Gameweek instead:

class FixtureInline(admin.TabularInline): model = Fixture extra = 0 autocomplete_fields = ('home_team', 'away_team') fields = ('home_team', 'away_team', 'kickoff_time', 'home_score', 'away_score', 'status') @admin.register(Gameweek) class GameweekAdmin(admin.ModelAdmin): list_display = ('season', 'number') list_filter = ('season',) inlines = [FixtureInline]

Fixture's two foreign keys to Team don't need Chapter 2's own related_name disambiguation here — that clash only ever concerned the reverse accessor on Team, and this inline is nested under Gameweek, the model with exactly one foreign key pointing at it. Each row simply renders two separate autocomplete widgets, one per team.

A real, honest baseline — not the final answer
This genuinely works: an admin can open a gameweek and add all 10 fixtures right there, each team searched by name via autocomplete. It's also genuinely slower than it needs to be — every fixture still means opening two separate search boxes and typing, ten times a week, every week. Chapter 4 builds a real, purpose-built click-to-pair interface on top of this same underlying data specifically to fix that — this chapter's own admin path stays available afterward as a real fallback, not something the next chapter replaces outright.

Where This Course Is Headed

The fast click-to-pair fixture-entry UI, built outside the admin, creating real Fixture rows against this chapter's own schema and reusing Team records exactly as they already exist (Chapter 4); recording predictions (Chapter 5); entering results (Chapter 6); both league tables (Chapters 7-8); and promotion/relegation, which reuses this chapter's own SeasonTeamInline directly at the season boundary (Chapter 9).

Hands-On Exercises

Exercise 1

Explain why autocomplete_fields = ('team',) on SeasonTeamInline requires TeamAdmin to define search_fields, and what would happen if TeamAdmin's search_fields were removed.

📄 View solution
Exercise 2

Explain, precisely, what max_num = 20 alone actually controls on SeasonTeamInline, and what validate_max = True adds that max_num by itself does not provide.

📄 View solution
Exercise 3

Register the admin classes from this chapter, create a superuser (python manage.py createsuperuser), open a real Season in the admin, add teams via the SeasonTeamInline until you reach the 20-team cap, and confirm attempting to add a 21st is genuinely rejected on save, not just visually limited.

📄 View solution

Chapter 3 Quick Reference

  • TeamAdmin — search_fields powers both its own list-page search box and every autocomplete_fields reference to Team elsewhere in the admin
  • SeasonTeamInline — a real TabularInline on SeasonAdmin, getting the "add/remove a team from a season" job largely for free compared to the FastAPI sibling's own hand-written routes
  • Verified gotcha — max_num = 20 alone only limits displayed blank rows; validate_max = True is what actually enforces the cap on save
  • FixtureInline — a real, working way to enter a gameweek's fixtures through the admin, genuinely slower than what Chapter 4 builds next
  • No fk_name needed — FixtureInline is nested under Gameweek, which has only one foreign key pointing at it; the related_name clash from Chapter 2 was specific to Team
  • Next chapter: The fast click-to-pair fixture-entry UI, built outside the admin