Files
RosterChief/members/models.py
Bernard Siebens eace903f05 chore: rebrand ClubManager -> RosterChief, add lucide icons
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>
2026-07-13 15:42:20 +02:00

102 lines
3.8 KiB
Python

from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from rosterchief.base import UUIDModel
class Family(UUIDModel):
name = models.CharField(_("name"), max_length=255, blank=True)
class Meta:
verbose_name = _("family")
verbose_name_plural = _("families")
ordering = ["name"]
def __str__(self):
if self.name:
return self.name
surnames = sorted({last_name for last_name in self.memberships.values_list("member__last_name", flat=True) if last_name})
if surnames:
return _("%(surnames)s family") % {"surnames": " / ".join(surnames)}
return _("Family %(id)s") % {"id": str(self.pk)[:8]}
@property
def guardians(self):
return Member.objects.filter(
family_memberships__family=self,
family_memberships__role__in=[FamilyMembership.FamilyRole.PARENT, FamilyMembership.FamilyRole.GUARDIAN],
)
@property
def children(self):
return Member.objects.filter(
family_memberships__family=self,
family_memberships__role=FamilyMembership.FamilyRole.CHILD,
)
class Member(UUIDModel):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, related_name="member", null=True, blank=True, verbose_name=_("user"))
first_name = models.CharField(_("first name"), max_length=150)
last_name = models.CharField(_("last name"), max_length=150)
date_of_birth = models.DateField(_("date of birth"), null=True, blank=True)
email = models.EmailField(_("email"), blank=True)
phone = PhoneNumberField(_("phone number"), null=True, blank=True)
emergency_phone = PhoneNumberField(_("emergency phone number"), null=True, blank=True)
class Meta:
verbose_name = _("member")
verbose_name_plural = _("members")
ordering = ["last_name", "first_name"]
indexes = [models.Index(fields=["last_name", "first_name"])]
def __str__(self):
return self.get_full_name()
def get_full_name(self):
return f"{self.first_name} {self.last_name}".strip()
def get_short_name(self):
return self.first_name
@property
def contact_email(self):
"""Best email to reach this member: own contact email, else login email."""
return self.email or (self.user.email if self.user_id else "")
@property
def guardians(self):
return Member.objects.filter(
family_memberships__role__in=[FamilyMembership.FamilyRole.PARENT, FamilyMembership.FamilyRole.GUARDIAN],
family_memberships__family__memberships__member=self,
family_memberships__family__memberships__role=FamilyMembership.FamilyRole.CHILD,
).distinct()
class FamilyMembership(models.Model):
class FamilyRole(models.TextChoices):
PARENT = "parent", _("parent")
CHILD = "child", _("child")
GUARDIAN = "guardian", _("guardian")
OTHER = "other", _("other")
family = models.ForeignKey(Family, on_delete=models.CASCADE, related_name="memberships", verbose_name=_("family"))
member = models.ForeignKey(Member, on_delete=models.CASCADE, related_name="family_memberships", verbose_name=_("member"))
role = models.CharField(_("role"), max_length=255, choices=FamilyRole.choices, default=FamilyRole.PARENT)
class Meta:
verbose_name = _("family membership")
verbose_name_plural = _("family memberships")
ordering = ["family", "role", "member__last_name", "member__first_name"]
constraints = [
models.UniqueConstraint(fields=["family", "member"], name="unique_member_per_family"),
]
def __str__(self):
return f"{self.family} - {self.member} ({self.get_role_display()})"