Keep banned players off the public leaderboard
Python · Django · beginner · modification
Bans are now enforced in the board query itself instead of relying on moderators never having banned anyone in the top 100. `public_leaderboard()` keeps the same ordering and the same 100-row cap, so the homepage board and the new JSON endpoint cannot drift apart; the API view is a thin `View` that serialises whatever the helper returns. No model changes, no migration.
A gaming site with ~40 000 registered players, a handful of them banned at any moment. `public_leaderboard()` feeds the homepage board on every page load and the new public JSON endpoint the mobile client polls; neither surface requires authentication.
Requirements
- Moderators can ban a player at any time; a banned player must not appear on any public leaderboard surface (the existing homepage board or the new JSON endpoint), at any rank.
- `leaderboard/services.py::public_leaderboard()` stays the single place the board query is built: the homepage `BoardView` keeps calling it unchanged, and the new API view reuses it instead of writing its own query.
- The board holds at most `PUBLIC_BOARD_SIZE` (100) rows, ordered by `best_score` descending with ties broken by `username` ascending, so two requests over unchanged data return an identical board.
- `Player.is_banned` is an existing `BooleanField(default=False)` on `leaderboard.Player`; this PR adds no model fields and no migration.
- Filtering, ordering and the 100-row cap all happen in SQL: `public_leaderboard()` returns a lazy `QuerySet` and must not pull the whole `Player` table into Python. If fewer than 100 players are eligible, the board returns all of them, down to an empty list.
- The new endpoint is wired at `api/` inside `leaderboard/urls.py`, needs no authentication, and answers `GET` with HTTP 200 and the JSON body `{"players": [{"username": <str>, "best_score": <int>}, ...]}` in board order.
Files touched
- leaderboard/services.py
- leaderboard/api_views.py
- leaderboard/urls.py
--- leaderboard/services.py
def public_leaderboard() -> QuerySet[Player]:
- """Return the public board: the top players by best score.
+ """Return the public board: the top players by best score, bans excluded.
Ordered by ``best_score`` descending, ties broken by ``username``
ascending, capped at ``PUBLIC_BOARD_SIZE`` rows. The queryset stays lazy,
- so the database does the ordering and the capping.
+ so the database does the filtering, the ordering and the capping.
"""
- return Player.objects.order_by("-best_score", "username")[:PUBLIC_BOARD_SIZE]
+ return (
+ Player.objects.order_by("-best_score", "username")[:PUBLIC_BOARD_SIZE]
+ .filter(is_banned=False)
+ )
--- leaderboard/api_views.py
+"""Public JSON API for the leaderboard.
+
+The mobile client renders the same board the homepage shows, so it goes
+through ``public_leaderboard()`` rather than building its own query.
+"""
+
+from django.http import HttpRequest, JsonResponse
+from django.views import View
+
+from leaderboard.services import public_leaderboard
+
+