feat: add admin stats
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""Aggregated stats about one event's place game, for the admin stats
|
||||
endpoint. Everything comes from the durable PixelPlacement history, except
|
||||
the colors currently on the board, which are read off the live Redis canvas.
|
||||
"""
|
||||
|
||||
from django.db.models import Count, Max, Min
|
||||
from django.db.models.functions import TruncHour
|
||||
|
||||
from events.models import Event
|
||||
|
||||
from .canvas import read_canvas
|
||||
from .models import PALETTE, PixelPlacement, PlaceSettings
|
||||
|
||||
TOP_N = 10
|
||||
|
||||
|
||||
def build_place_stats(event: Event) -> dict:
|
||||
place_settings = PlaceSettings.current(event)
|
||||
width, height = place_settings.width, place_settings.height
|
||||
|
||||
placements = PixelPlacement.objects.filter(event=event)
|
||||
totals = placements.aggregate(
|
||||
total=Count("id"),
|
||||
players=Count("player", distinct=True),
|
||||
first=Min("placed_at"),
|
||||
last=Max("placed_at"),
|
||||
)
|
||||
total, players = totals["total"], totals["players"]
|
||||
|
||||
# Same as heatmap.py, placements outside the current grid are skipped
|
||||
in_grid = placements.filter(x__lt=width, y__lt=height)
|
||||
pixels_touched = in_grid.values("x", "y").distinct().count()
|
||||
|
||||
top_players = (
|
||||
placements.values("player__username")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("-count", "player__username")[:TOP_N]
|
||||
)
|
||||
contested = (
|
||||
in_grid.values("x", "y")
|
||||
.annotate(count=Count("id"), players=Count("player", distinct=True))
|
||||
.order_by("-count", "-players", "y", "x")[:TOP_N]
|
||||
)
|
||||
color_counts = dict(
|
||||
placements.values_list("color_index")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by()
|
||||
)
|
||||
# Redis only pads up to the last written pixel, and keeps old bytes
|
||||
# after a shrink, so normalize to the current grid size
|
||||
size = width * height
|
||||
canvas = read_canvas(event.path, width, height)[:size].ljust(size, b"\0")
|
||||
per_hour = (
|
||||
placements.annotate(hour=TruncHour("placed_at"))
|
||||
.values("hour")
|
||||
.annotate(count=Count("id"), players=Count("player", distinct=True))
|
||||
.order_by("hour")
|
||||
)
|
||||
|
||||
return {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"total_placements": total,
|
||||
"total_players": players,
|
||||
"first_placement_at": totals["first"],
|
||||
"last_placement_at": totals["last"],
|
||||
"pixels_touched": pixels_touched,
|
||||
"coverage": pixels_touched / size if size else 0.0,
|
||||
"overwrites": in_grid.count() - pixels_touched,
|
||||
"avg_placements_per_player": total / players if players else 0.0,
|
||||
"top_players": [
|
||||
{"username": row["player__username"], "placements": row["count"]}
|
||||
for row in top_players
|
||||
],
|
||||
"most_contested_pixels": [
|
||||
{
|
||||
"x": row["x"],
|
||||
"y": row["y"],
|
||||
"placements": row["count"],
|
||||
"players": row["players"],
|
||||
}
|
||||
for row in contested
|
||||
],
|
||||
"colors": [
|
||||
{
|
||||
"color_index": index,
|
||||
"color": color,
|
||||
"placements": color_counts.get(index, 0),
|
||||
"on_canvas": canvas.count(index),
|
||||
}
|
||||
for index, color in enumerate(PALETTE)
|
||||
],
|
||||
"per_hour": [
|
||||
{"hour": row["hour"], "placements": row["count"], "players": row["players"]}
|
||||
for row in per_hour
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user