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>
This commit is contained in:
140
accounts/models.py
Normal file
140
accounts/models.py
Normal file
@@ -0,0 +1,140 @@
|
||||
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()})"
|
||||
Reference in New Issue
Block a user