Add the `controlpanel` app: a platform-wide (not club-scoped) admin panel for creating clubs, archiving/restoring them, managing club admins, and per-club statistics (members, teams & staff, events, shop). Statistics are annotated in one query so the club list cannot fan out into N+1, and are returned as stat *groups* so growing the domain means adding one entry. Two access rules, both enforced by PlatformStaffRequiredMixin: - staff only (is_staff/is_superuser); anonymous are sent to login, signed-in non-staff get a 403. Staff already need a second factor, so the panel is 2FA-protected for free. - base domain only: the panel manages *all* clubs, so it 404s if the tenant middleware resolved a club from the subdomain. Granting admin to an unknown email creates the account (unusable password — they set one via password reset) and the Member behind it, since a ClubRole hangs off a Member. A member who already holds a role is promoted in place, because there is only one role per member per club. UI is Tailwind + daisyUI. allauth ships an element system, so overriding allauth/layouts/base.html plus ~13 element partials restyles *every* auth and 2FA screen at once — login, signup, password reset, the 2FA challenge, TOTP enrolment, passkeys and recovery codes — rather than templating 20+ pages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
48 lines
1.6 KiB
Python
48 lines
1.6 KiB
Python
"""Granting and revoking club-admin rights from the platform panel."""
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.db import transaction
|
|
|
|
from club.models import ClubRole
|
|
from members.models import Member
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
def find_member_by_email(email):
|
|
"""The Member behind a login email, if that account exists at all."""
|
|
return Member.objects.filter(user__email__iexact=email).first()
|
|
|
|
|
|
@transaction.atomic
|
|
def grant_club_admin(club, email, first_name="", last_name=""):
|
|
"""Make the holder of ``email`` an ADMIN of ``club``, creating them if new.
|
|
|
|
A ClubRole hangs off a Member, and a Member optionally links to a User — so
|
|
an admin who has never existed needs both. The account is created without a
|
|
usable password; they set one via the password-reset flow.
|
|
"""
|
|
email = email.lower()
|
|
user, created_user = User.objects.get_or_create(email=email, defaults={"is_active": True})
|
|
if created_user:
|
|
user.set_unusable_password()
|
|
user.save(update_fields=["password"])
|
|
|
|
member, _ = Member.objects.get_or_create(
|
|
user=user,
|
|
defaults={"first_name": first_name, "last_name": last_name},
|
|
)
|
|
|
|
# One role per member per club, so promote rather than add a second row.
|
|
role, created_role = ClubRole.objects.get_or_create(club=club, member=member, defaults={"role": ClubRole.Roles.ADMIN})
|
|
if not created_role and role.role != ClubRole.Roles.ADMIN:
|
|
role.role = ClubRole.Roles.ADMIN
|
|
role.save(update_fields=["role"])
|
|
|
|
return role
|
|
|
|
|
|
def revoke_club_admin(role):
|
|
"""Remove admin rights. The membership-status sync never re-adds ADMIN."""
|
|
role.delete()
|