The clubmanager.app domain was taken, so the platform is now RosterChief (rosterchief.app). Renames the Django project package clubmanager/ -> rosterchief/ (git tracks it as a move, so history follows), every `from rosterchief.base import ...`, the settings/wsgi/asgi module paths, env vars (ROSTERCHIEF_BASE_DOMAIN / ROSTERCHIEF_RP_NAME), the MFA adapter (RosterChiefMFAAdapter), brand text, and the docs. Two things were deliberately NOT swept: - club.models.ClubManager stays: it is the Django manager *for Club*, not the brand. A blind rename would have silently broken it. - Migrations are untouched (history is not rewritten). The only reference was a cosmetic help_text, so a normal AlterField migration carries the new domain. Note the WebAuthn RP ID is the base domain, so moving to rosterchief.app cryptographically invalidates any passkey enrolled under the old one; they cannot be migrated and must be re-enrolled. Nothing is in production, so the real cost is zero. Add django-lucide (from bsiebens/lucide) for icons: the theme toggle now swaps sun/moon against the effective theme, and the control panel gets icons on its tabs, actions and stat groups. Its classifiers stop at Django 5.0, but that is stale metadata — verified rendering on Django 6 / Python 3.14. Also add formbuilder, shop and controlpanel to ruff's known-first-party list, which had drifted behind the apps that landed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
3.6 KiB
Python
97 lines
3.6 KiB
Python
"""Platform and per-club statistics.
|
|
|
|
``club_statistics`` returns a list of stat *groups*, so growing the model later
|
|
means adding an entry here and nothing else. ``clubs_with_totals`` annotates in
|
|
a single query — the club list must not fan out into N+1.
|
|
"""
|
|
|
|
from decimal import Decimal
|
|
|
|
from django.db.models import Count, Q, Sum
|
|
from django.utils import timezone
|
|
|
|
from club.models import Club, ClubMembership, ClubRole, Season
|
|
from events.models import Event
|
|
from members.models import Member
|
|
from shop.models import Cart, Order
|
|
from teams.models import StaffAssignment, Team, TeamMembership
|
|
|
|
ZERO = Decimal("0.00")
|
|
|
|
PAID_STATUSES = (Order.OrderStatus.PAID, Order.OrderStatus.DELIVERED)
|
|
OWED_STATUSES = (Order.OrderStatus.PENDING, Order.OrderStatus.PARTIALLY_PAID)
|
|
|
|
|
|
def clubs_with_totals(queryset=None):
|
|
"""Clubs annotated with headline counts (one query, no N+1)."""
|
|
clubs = Club.objects.all() if queryset is None else queryset
|
|
return clubs.annotate(
|
|
member_count=Count("clubmemberships__member", distinct=True),
|
|
team_count=Count("teams", distinct=True),
|
|
event_count=Count("events", distinct=True),
|
|
admin_count=Count("clubroles", filter=Q(clubroles__role=ClubRole.Roles.ADMIN), distinct=True),
|
|
)
|
|
|
|
|
|
def platform_totals():
|
|
return {
|
|
"clubs": Club.objects.active().count(),
|
|
"archived_clubs": Club.objects.archived().count(),
|
|
"members": Member.objects.count(),
|
|
"admins": ClubRole.objects.filter(role=ClubRole.Roles.ADMIN).count(),
|
|
}
|
|
|
|
|
|
def _money(queryset):
|
|
return queryset.aggregate(total=Sum("total"))["total"] or ZERO
|
|
|
|
|
|
def club_statistics(club):
|
|
"""Stat groups for one club. Add new groups here as the domain grows."""
|
|
season = Season.covering(club, timezone.localdate())
|
|
now = timezone.now()
|
|
|
|
memberships = ClubMembership.objects.filter(club=club)
|
|
events = Event.objects.filter(club=club)
|
|
orders = Order.objects.filter(club=club)
|
|
|
|
return [
|
|
{
|
|
"title": "Members",
|
|
"icon": "users",
|
|
"stats": [
|
|
("Members", memberships.values("member").distinct().count()),
|
|
("Active this season", memberships.filter(season=season, status=ClubMembership.StatusChoices.ACTIVE).count() if season else 0),
|
|
("Pending", memberships.filter(status=ClubMembership.StatusChoices.PENDING).count()),
|
|
("Lapsed", memberships.filter(status=ClubMembership.StatusChoices.LAPSED).count()),
|
|
],
|
|
},
|
|
{
|
|
"title": "Teams & staff",
|
|
"icon": "shield",
|
|
"stats": [
|
|
("Teams", Team.objects.filter(club=club).count()),
|
|
("Players this season", TeamMembership.objects.filter(team__club=club, season=season).count() if season else 0),
|
|
("Staff this season", StaffAssignment.objects.filter(team__club=club, season=season).count() if season else 0),
|
|
],
|
|
},
|
|
{
|
|
"title": "Events",
|
|
"icon": "calendar-days",
|
|
"stats": [
|
|
("Upcoming", events.filter(start__gte=now).count()),
|
|
("This season", events.filter(season=season).count() if season else 0),
|
|
],
|
|
},
|
|
{
|
|
"title": "Shop",
|
|
"icon": "shopping-cart",
|
|
"stats": [
|
|
("Orders", orders.count()),
|
|
("Revenue", _money(orders.filter(status__in=PAID_STATUSES))),
|
|
("Outstanding", _money(orders.filter(status__in=OWED_STATUSES))),
|
|
("Open carts", Cart.objects.filter(club=club, status=Cart.CartStatus.OPEN).count()),
|
|
],
|
|
},
|
|
]
|