Files
RosterChief/accounts/models.py
Bernard Siebens 2ead824c5d Add accounts app: custom User, Member, families & guardianship
Introduce the foundational accounts app:
- Custom email-as-username User (AbstractBaseUser + PermissionsMixin) set
  as AUTH_USER_MODEL, decoupled from membership so children can be members
  without a login.
- Member model holding personal/roster data (names, contact email, phone +
  emergency phone via django-phonenumber-field, license number, DOB) with an
  optional link to a User.
- Family household grouping and directional Guardianship (guardian -> child)
  with uniqueness and no-self-guardian constraints.
- Custom UserAdmin plus Member/Family admin with inlines and autocomplete.
- Settings: register apps, AUTH_USER_MODEL, phonenumber defaults (BE/E164).
- Add CLAUDE.md and a tracked static/ directory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:08:51 +02:00

141 lines
4.0 KiB
Python

from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from django.utils import timezone
from phonenumber_field.modelfields import PhoneNumberField
from .managers import UserManager
class User(AbstractBaseUser, PermissionsMixin):
"""Login identity. Only people who actually sign in get a User.
Personal/roster data lives on :class:`Member`; this model carries just the
authentication identity. ``PermissionsMixin`` provides ``is_superuser``,
``groups`` and ``user_permissions`` (the basis for the access tiers).
"""
email = models.EmailField(unique=True)
is_staff = models.BooleanField(
default=False,
help_text="Whether the user can log into the admin site.",
)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(default=timezone.now)
objects = UserManager()
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
def __str__(self):
return self.get_full_name()
def get_full_name(self):
member = getattr(self, "member", None)
if member is not None:
return f"{member.first_name} {member.last_name}".strip()
return self.email
def get_short_name(self):
member = getattr(self, "member", None)
if member is not None:
return member.first_name
return self.email
class Family(models.Model):
"""A household grouping members together."""
name = models.CharField(max_length=150, help_text='e.g. "The Smiths"')
address = models.CharField(max_length=255, blank=True)
class Meta:
ordering = ["name"]
verbose_name_plural = "families"
def __str__(self):
return self.name
class Member(models.Model):
"""A person in the club. May or may not have a login (:attr:`user`)."""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="member",
)
first_name = models.CharField(max_length=150)
last_name = models.CharField(max_length=150)
# Contact email — optional. Children may have none; a login email lives on User.
email = models.EmailField(blank=True)
phone = PhoneNumberField(blank=True)
emergency_phone = PhoneNumberField(blank=True)
license_number = models.CharField(max_length=50, blank=True)
date_of_birth = models.DateField()
family = models.ForeignKey(
Family,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="members",
)
guardians = models.ManyToManyField(
"self",
through="Guardianship",
through_fields=("child", "guardian"),
symmetrical=False,
related_name="dependents",
)
class Meta:
ordering = ["last_name", "first_name"]
def __str__(self):
return f"{self.first_name} {self.last_name}"
class Guardianship(models.Model):
"""Directional guardian -> child relationship between two members."""
class Relationship(models.TextChoices):
PARENT = "parent", "Parent"
GUARDIAN = "guardian", "Guardian"
OTHER = "other", "Other"
guardian = models.ForeignKey(
Member,
on_delete=models.CASCADE,
related_name="guardian_links",
)
child = models.ForeignKey(
Member,
on_delete=models.CASCADE,
related_name="child_links",
)
relationship = models.CharField(
max_length=20,
choices=Relationship.choices,
default=Relationship.PARENT,
)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["guardian", "child"],
name="unique_guardianship",
),
models.CheckConstraint(
condition=~models.Q(guardian=models.F("child")),
name="guardian_not_self",
),
]
def __str__(self):
return f"{self.guardian}{self.child} ({self.get_relationship_display()})"