Finish relocating Member/Family/FamilyMembership from authentication into a dedicated members app, and revert the half-applied move of member names onto the global User. - members: add first_name/last_name back to Member (the whole codebase — tests, CSV importer, club app, admin — assumes them, and login-less children in families need a name); restore local ordering/index and the member__last_name lookups in Family.__str__ and FamilyMembership. - authentication: drop first_name/last_name from User; get_full_name/ get_short_name delegate to the linked member, else fall back to email. - migrations: create members.0001_initial, repoint club.ClubMembership FK (club.0005), delete the models from authentication (0004, rewritten to plain DeleteModel ops in child-first order to avoid a SQLite table-remake crash). - fix stale imports across apps (authentication/club tests, CSV importer) and missing imports/URLs in members tests; add missing _ import in members/admin.py; export MemberCsvImporter from members.services. Full suite green (64 tests), ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
3.7 KiB
Python
100 lines
3.7 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 clubmanager.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"]
|
|
unique_together = ("family", "member")
|
|
|
|
def __str__(self):
|
|
return f"{self.family} - {self.member} ({self.get_role_display()})"
|