Files
TeamForge/teams/models.py
Bernard Siebens ef05a6523d Apply ruff formatting and fix unused import linting errors
Remove unused imports flagged by ruff (F401), apply ruff format across all
files, and restore members.signals side-effect import with noqa: F401 so the
post_save signal that auto-creates Member profiles continues to fire.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 08:55:28 +02:00

193 lines
8.8 KiB
Python

import calendar
import datetime
import re
from constance import config
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import AutoSlugField
from rules import is_superuser
from rules.contrib.models import RulesModel
from .rules import is_team_admin
def team_season_file_path(instance: "TeamPicture", filename: str) -> str:
"""Generates the file path for a team picture based on the team name and season."""
return f"teams/picture/{instance.team.slug}/{instance.season.start_date.year}/{filename}"
class Season(RulesModel):
start_date = models.DateField(_("start date"))
end_date = models.DateField(_("end date"))
class Meta:
verbose_name = _("season")
verbose_name_plural = _("seasons")
ordering = ["start_date"]
rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser}
constraints = [
models.UniqueConstraint(fields=["start_date", "end_date"], name="season_start_date_end_date_unique", violation_error_message=_("Start and end date for a given season must be unique")),
models.CheckConstraint(condition=models.Q(start_date__lt=models.F("end_date")), name="season_start_date_lt_end_date", violation_error_message=_("Start date must be before end date.")),
]
def __str__(self):
return _("Season '{start_date} - '{end_date}").format(start_date=self.start_date.strftime("%y"), end_date=self.end_date.strftime("%y"))
@property
def is_current(self) -> bool:
return self.start_date <= timezone.now().date() <= self.end_date
@property
def date_range(self) -> tuple[datetime.date, datetime.date]:
return self.start_date, self.end_date
@classmethod
def for_date(cls, current_date: datetime.date | None = None, values_only: bool = False) -> "tuple[datetime.date, datetime.date] | Season":
if current_date is None:
current_date = timezone.now().date()
season = cls.objects.filter(start_date__lte=current_date, end_date__gte=current_date).order_by("-start_date").first()
if season is None:
raise cls.DoesNotExist(f"No Season covers date {current_date}")
if values_only:
return season.date_range
return season
@classmethod
def generate_default(cls, day: int = config.TF_DEFAULT_SEASON_DAY, month: int = config.TF_DEFAULT_SEASON_MONTH, duration: str = config.TF_DEFAULT_SEASON_DURATION) -> "Season":
current_year = timezone.now().date().year
start_date = datetime.date(current_year, month, day)
match = re.fullmatch(r"(\d+)([ym])", duration.strip().lower())
if match is None:
raise ValueError('Duration must be specified as "<number>y" or "<number>m", for example "1y", "1m", or "3m".')
duration_value = int(match.group(1))
duration_unit = match.group(2)
if duration_unit == "y":
end_date = cls._add_months(start_date, duration_value * 12) - datetime.timedelta(days=1)
elif duration_unit == "m":
end_date = cls._add_months(start_date, duration_value) - datetime.timedelta(days=1)
else:
raise ValueError('Duration unit must be "y" or "m".')
return cls.objects.create(start_date=start_date, end_date=end_date)
@staticmethod
def _add_months(date_value: datetime.date, months: int) -> datetime.date:
month_index = date_value.month - 1 + months
year = date_value.year + month_index // 12
month = month_index % 12 + 1
day = min(date_value.day, calendar.monthrange(year, month)[1])
return date_value.replace(year=year, month=month, day=day)
class TeamRole(RulesModel):
name = models.CharField(_("name"), max_length=255, unique=True)
abbreviation = models.CharField(_("abbreviation"), max_length=255, unique=True, help_text=_("An abbreviated version of the role name"))
staff_role = models.BooleanField(_("staff role"), default=False, help_text=_("A staff role is any supporting function for a given team (e.g., coach, team manager, ...)"))
admin_role = models.BooleanField(_("admin role"), default=False, help_text=_("An admin role is a role that has administrative privileges and can change team information and settings"))
sort_order = models.PositiveIntegerField(_("sort order"), default=10, help_text=_("The order in which the role should be displayed (low to high)"))
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("team role")
verbose_name_plural = _("team roles")
ordering = ["sort_order"]
rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser}
def __str__(self):
return f"{self.name} ({self.abbreviation})"
class Team(RulesModel):
name = models.CharField(_("name"), max_length=255, unique=True)
short_name = models.CharField(_("short name"), max_length=255, unique=True, blank=True, null=True, help_text=_("An optional short name for the team"))
slug = AutoSlugField(_("slug"), max_length=255, unique=True, populate_from="name")
logo = models.ImageField(_("logo"), upload_to="teams/logo/", blank=True, null=True)
members = models.ManyToManyField("members.Member", verbose_name=_("members"), through="TeamMembership")
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("team")
verbose_name_plural = _("teams")
ordering = ["name"]
rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser}
def __str__(self):
return self.name
@property
def initials(self) -> str:
words = self.name.split()
if len(words) == 1:
return words[0][0].upper()
return "".join(word[0].upper() for word in words[:2])
@property
def member_count(self) -> int:
season = Season.for_date()
return self.members.filter(team_memberships__season=season).count()
def get_short_name(self) -> str:
"""Returns the short name of the team, or the name if no short name is set."""
return self.short_name if self.short_name and self.short_name != "" else self.name
class TeamMembership(RulesModel):
team = models.ForeignKey(Team, on_delete=models.CASCADE, verbose_name=_("team"))
member = models.ForeignKey("members.Member", on_delete=models.CASCADE, related_name="team_memberships", verbose_name=_("member"))
season = models.ForeignKey(Season, on_delete=models.CASCADE, related_name="team_memberships", verbose_name=_("season"))
role = models.ForeignKey(TeamRole, on_delete=models.CASCADE, related_name="team_memberships", verbose_name=_("role"))
number = models.PositiveIntegerField(_("number"), blank=True, null=True, validators=[MinValueValidator(1), MaxValueValidator(99)])
captain = models.BooleanField(_("captain"), default=False)
alternate_captain = models.BooleanField(_("alternate captain"), default=False)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("team membership")
verbose_name_plural = _("team memberships")
ordering = ["team__name", "role__sort_order", "number", "member__user__last_name", "member__user__first_name"]
constraints = [
models.UniqueConstraint(fields=["team", "season", "number"], name="unique_team_membership_number", violation_error_message=_("A team can only have one member for a given season and number.")),
]
rules_permissions = {"add": is_superuser | is_team_admin, "view": is_superuser | is_team_admin, "change": is_superuser | is_team_admin, "delete": is_superuser | is_team_admin}
def __str__(self):
return f"{self.team.name} - {self.member.user.get_full_name()} ({self.role.abbreviation})"
class TeamPicture(RulesModel):
team = models.ForeignKey(Team, on_delete=models.CASCADE, verbose_name=_("team"))
season = models.ForeignKey(Season, on_delete=models.CASCADE, verbose_name=_("season"))
picture = models.ImageField(_("picture"), upload_to=team_season_file_path)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("team picture")
verbose_name_plural = _("team pictures")
ordering = ["team__name", "season__start_date"]
rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser}
def __str__(self):
return f"{self.team.name} - {self.season.start_date.year}"