Add Team/TeamRole/TeamMembership/TeamPicture models and fix two test bugs

- Fix Member.create(): post_save signal creates Member immediately on User creation,
  so new users always hit the hasattr branch; check `created` flag to set initial
  password and notes for genuinely new users
- Fix Season.for_date(): replace get() with filter().order_by("-start_date").first()
  to handle overlapping seasons gracefully and raise DoesNotExist when none match
- Add TeamRole, Team, TeamMembership, TeamPicture models with rules permissions
- Add migration 0002 for new models
- Add test suites for members and teams covering all new model behaviour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 08:53:05 +02:00
parent a02f234411
commit 6c0115d4a2
7 changed files with 572 additions and 12 deletions

View File

View File

@@ -83,7 +83,13 @@ class Member(RulesModel):
if hasattr(user, "member"): if hasattr(user, "member"):
member = user.member member = user.member
if password is not None and password != "": if created:
if password is None or password == "":
initial_password = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(20))
password = initial_password
member.notes = f"Initial password: {initial_password}"
user.set_password(password)
elif password is not None and password != "":
user.set_password(password) user.set_password(password)
else: else:
member = cls() member = cls()

View File

@@ -2,6 +2,8 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.test import TestCase from django.test import TestCase
from members.models import Member
User = get_user_model() User = get_user_model()
@@ -28,3 +30,63 @@ class MembersTestCase(TestCase):
self.assertTrue(self.user_a.has_perm("members.member_manager")) self.assertTrue(self.user_a.has_perm("members.member_manager"))
self.assertTrue(self.user_a.has_perm("members.add_member")) self.assertTrue(self.user_a.has_perm("members.add_member"))
self.assertFalse(self.user_a.is_superuser) self.assertFalse(self.user_a.is_superuser)
class MemberCreateTest(TestCase):
def test_create_new_member(self):
member = Member.create(first_name="Alice", last_name="Smith", email="alice@test.com")
self.assertIsNotNone(member.pk)
self.assertEqual(member.user.first_name, "Alice")
self.assertEqual(member.user.last_name, "Smith")
self.assertEqual(member.user.email, "alice@test.com")
def test_create_sets_initial_password_note(self):
member = Member.create(first_name="Alice", last_name="Smith", email="alice@test.com")
self.assertIn("Initial password:", member.notes)
def test_create_with_explicit_password(self):
member = Member.create(first_name="Alice", last_name="Smith", email="alice@test.com", password="secret123")
self.assertTrue(member.user.check_password("secret123"))
self.assertIsNone(member.notes)
def test_create_existing_user_reuses_member(self):
existing_user = User.objects.create(username="bob@test.com", email="bob@test.com", first_name="Bob", last_name="Old")
original_member_pk = existing_user.member.pk
member = Member.create(first_name="Bob", last_name="New", email="bob@test.com")
self.assertEqual(member.pk, original_member_pk)
self.assertEqual(member.user.last_name, "New")
def test_create_update_existing_member(self):
existing_member = Member.create(first_name="Carol", last_name="Old", email="carol@test.com")
updated_member = Member.create(first_name="Carol", last_name="Updated", email="carol@test.com", member=existing_member)
self.assertEqual(updated_member.pk, existing_member.pk)
self.assertEqual(updated_member.user.last_name, "Updated")
def test_create_update_existing_member_with_password(self):
existing_member = Member.create(first_name="Dave", last_name="D", email="dave@test.com")
Member.create(first_name="Dave", last_name="D", email="dave@test.com", password="newpass", member=existing_member)
existing_member.user.refresh_from_db()
self.assertTrue(existing_member.user.check_password("newpass"))
def test_create_reactivates_inactive_user(self):
member = Member.create(first_name="Eve", last_name="E", email="eve@test.com")
member.user.is_active = False
member.user.save()
reactivated = Member.create(first_name="Eve", last_name="E", email="eve@test.com", member=member)
self.assertTrue(reactivated.user.is_active)
def test_create_member_is_active_by_default(self):
member = Member.create(first_name="Frank", last_name="F", email="frank@test.com")
self.assertTrue(member.user.is_active)
class MemberManagerTest(TestCase):
def test_queryset_select_related(self):
User.objects.create(username="user_mgr", first_name="Mgr", last_name="Test", email="mgr@test.com")
# select_related means no extra query is needed to access member.user
with self.assertNumQueries(1):
members = list(Member.objects.all())
_ = [m.user.get_full_name() for m in members]

View File

@@ -0,0 +1,103 @@
# Generated by Django 6.0.3 on 2026-06-01 20:21
import django.core.validators
import django.db.models.deletion
import django_extensions.db.fields
import rules.contrib.models
import teams.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0006_member_notes'),
('teams', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Team',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
('short_name', models.CharField(blank=True, help_text='An optional short name for the team', max_length=255, null=True, unique=True, verbose_name='short name')),
('slug', django_extensions.db.fields.AutoSlugField(blank=True, editable=False, max_length=255, populate_from='name', unique=True, verbose_name='slug')),
('logo', models.ImageField(blank=True, null=True, upload_to='teams/logo/', verbose_name='logo')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'team',
'verbose_name_plural': 'teams',
'ordering': ['name'],
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name='TeamRole',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
('abbreviation', models.CharField(help_text='An abbreviated version of the role name', max_length=255, unique=True, verbose_name='abbreviation')),
('staff_role', models.BooleanField(default=False, help_text='A staff role is any supporting function for a given team (e.g., coach, team manager, ...)', verbose_name='staff role')),
('admin_role', models.BooleanField(default=False, help_text='An admin role is a role that has administrative privileges and can change team information and settings', verbose_name='admin role')),
('sort_order', models.PositiveIntegerField(default=10, help_text='The order in which the role should be displayed (low to high)', verbose_name='sort order')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'team role',
'verbose_name_plural': 'team roles',
'ordering': ['sort_order'],
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.CreateModel(
name='TeamMembership',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('number', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(99)], verbose_name='number')),
('captain', models.BooleanField(default=False, verbose_name='captain')),
('alternate_captain', models.BooleanField(default=False, verbose_name='alternate captain')),
('created', models.DateTimeField(auto_now_add=True)),
('modified', models.DateTimeField(auto_now=True)),
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to='members.member', verbose_name='member')),
('season', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to='teams.season', verbose_name='season')),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teams.team', verbose_name='team')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to='teams.teamrole', verbose_name='role')),
],
options={
'verbose_name': 'team membership',
'verbose_name_plural': 'team memberships',
'ordering': ['team__name', 'role__sort_order', 'number', 'member__user__last_name', 'member__user__first_name'],
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.AddField(
model_name='team',
name='members',
field=models.ManyToManyField(through='teams.TeamMembership', to='members.member', verbose_name='members'),
),
migrations.CreateModel(
name='TeamPicture',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('picture', models.ImageField(upload_to=teams.models.team_season_file_path, verbose_name='picture')),
('created', models.DateTimeField(auto_now_add=True)),
('modified', models.DateTimeField(auto_now=True)),
('season', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teams.season', verbose_name='season')),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teams.team', verbose_name='team')),
],
options={
'verbose_name': 'team picture',
'verbose_name_plural': 'team pictures',
'ordering': ['team__name', 'season__start_date'],
},
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
migrations.AddConstraint(
model_name='teammembership',
constraint=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.'),
),
]

View File

@@ -3,12 +3,20 @@ import datetime
import re import re
from constance import config from constance import config
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import AutoSlugField
from rules import is_superuser from rules import is_superuser
from rules.contrib.models import RulesModel 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): class Season(RulesModel):
start_date = models.DateField(_("start date")) start_date = models.DateField(_("start date"))
@@ -40,21 +48,17 @@ class Season(RulesModel):
if current_date is None: if current_date is None:
current_date = timezone.now().date() current_date = timezone.now().date()
season = cls.objects.get(start_date__lte=current_date, end_date__gte=current_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: if values_only:
return season.date_range return season.date_range
return season return season
@classmethod @classmethod
def generate_default(cls, day: int | None = None, month: int | None = None, duration: str | None = None) -> "Season": 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":
if day is None:
day = config.TF_DEFAULT_SEASON_DAY
if month is None:
month = config.TF_DEFAULT_SEASON_MONTH
if duration is None:
duration = config.TF_DEFAULT_SEASON_DURATION
current_year = timezone.now().date().year current_year = timezone.now().date().year
start_date = datetime.date(current_year, month, day) start_date = datetime.date(current_year, month, day)
@@ -82,3 +86,103 @@ class Season(RulesModel):
day = min(date_value.day, calendar.monthrange(year, month)[1]) day = min(date_value.day, calendar.monthrange(year, month)[1])
return date_value.replace(year=year, month=month, day=day) 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}"

43
teams/rules.py Normal file
View File

@@ -0,0 +1,43 @@
from typing import TYPE_CHECKING
import rules
from django.contrib.auth.models import AbstractUser
if TYPE_CHECKING:
from .models import TeamMembership, Season
@rules.predicate
def is_team_admin(user: AbstractUser | None, teammembership: "TeamMembership | None") -> bool:
"""
Determine if a user is a team admin within a specific team membership context.
:param user: The user to check for team admin privileges; can be None.
:param teammembership: The specific team membership to evaluate; can be None.
:return: A boolean indicating whether the user is a team admin for the given
team membership.
"""
from .models import TeamMembership, Season
if user is None or teammembership is None:
return False
return TeamMembership.objects.filter(team=teammembership.team, member__user=user, role__admin_role=True, season=Season.for_date()).exists()
@rules.predicate
def is_a_team_admin(user: AbstractUser | None) -> bool:
"""
Determine if a user is a team admin.
:param user: The user to check for team admin privileges; can be None.
:return: A boolean indicating whether the user is a team admin for the given
team membership.
"""
from .models import Season, TeamMembership
if user is None:
return False
return TeamMembership.objects.filter(member__user=user, role__admin_role=True, season=Season.for_date()).exists()

View File

@@ -1,3 +1,245 @@
from django.test import TestCase import datetime
# Create your tests here. from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone
from members.models import Member
from teams.models import Season, Team, TeamMembership, TeamPicture, TeamRole
User = get_user_model()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def make_member(email, first="Test", last="User"):
user = User.objects.create_user(username=email, email=email, first_name=first, last_name=last)
return user.member
def make_season(start=datetime.date(2025, 9, 1), end=datetime.date(2026, 8, 31)):
return Season.objects.create(start_date=start, end_date=end)
def make_role(name="Player", abbreviation="P", admin_role=False):
return TeamRole.objects.create(name=name, abbreviation=abbreviation, admin_role=admin_role)
def make_team(name="Test Team"):
return Team.objects.create(name=name)
# ---------------------------------------------------------------------------
# Season
# ---------------------------------------------------------------------------
class SeasonStrTest(TestCase):
def test_str(self):
season = Season(start_date=datetime.date(2025, 9, 1), end_date=datetime.date(2026, 8, 31))
self.assertEqual(str(season), "Season '25 - '26")
class SeasonIsCurrentTest(TestCase):
def setUp(self):
today = timezone.now().date()
self.season = Season.objects.create(
start_date=today - datetime.timedelta(days=30),
end_date=today + datetime.timedelta(days=30),
)
def test_is_current_true(self):
self.assertTrue(self.season.is_current)
def test_is_current_false_past(self):
past = Season.objects.create(start_date=datetime.date(2020, 1, 1), end_date=datetime.date(2020, 12, 31))
self.assertFalse(past.is_current)
def test_is_current_false_future(self):
future = Season.objects.create(start_date=datetime.date(2099, 1, 1), end_date=datetime.date(2099, 12, 31))
self.assertFalse(future.is_current)
class SeasonDateRangeTest(TestCase):
def test_date_range(self):
start, end = datetime.date(2025, 9, 1), datetime.date(2026, 8, 31)
season = Season(start_date=start, end_date=end)
self.assertEqual(season.date_range, (start, end))
class SeasonForDateTest(TestCase):
def setUp(self):
self.start = datetime.date(2025, 9, 1)
self.end = datetime.date(2026, 8, 31)
self.season = Season.objects.create(start_date=self.start, end_date=self.end)
def test_for_date_returns_season(self):
self.assertEqual(Season.for_date(datetime.date(2026, 1, 15)), self.season)
def test_for_date_values_only(self):
self.assertEqual(Season.for_date(datetime.date(2026, 1, 15), values_only=True), (self.start, self.end))
def test_for_date_defaults_to_today(self):
today = timezone.now().date()
season_today = Season.objects.create(
start_date=today - datetime.timedelta(days=1),
end_date=today + datetime.timedelta(days=364),
)
self.assertEqual(Season.for_date(), season_today)
class SeasonGenerateDefaultTest(TestCase):
def test_generate_year_duration(self):
season = Season.generate_default(day=1, month=9, duration="1y")
self.assertEqual(season.start_date, datetime.date(2026, 9, 1))
self.assertEqual(season.end_date, datetime.date(2027, 8, 31))
def test_generate_month_duration(self):
season = Season.generate_default(day=1, month=9, duration="3m")
self.assertEqual(season.start_date, datetime.date(2026, 9, 1))
self.assertEqual(season.end_date, datetime.date(2026, 11, 30))
def test_generate_invalid_duration_raises(self):
with self.assertRaises(ValueError):
Season.generate_default(day=1, month=9, duration="invalid")
def test_generate_persists_to_db(self):
Season.generate_default(day=1, month=9, duration="1y")
self.assertEqual(Season.objects.count(), 1)
class SeasonAddMonthsTest(TestCase):
def test_add_months_normal(self):
self.assertEqual(Season._add_months(datetime.date(2025, 1, 1), 3), datetime.date(2025, 4, 1))
def test_add_months_year_rollover(self):
self.assertEqual(Season._add_months(datetime.date(2025, 11, 1), 3), datetime.date(2026, 2, 1))
def test_add_months_day_clamp(self):
self.assertEqual(Season._add_months(datetime.date(2025, 1, 31), 1), datetime.date(2025, 2, 28))
def test_add_months_twelve(self):
self.assertEqual(Season._add_months(datetime.date(2025, 9, 1), 12), datetime.date(2026, 9, 1))
# ---------------------------------------------------------------------------
# TeamRole
# ---------------------------------------------------------------------------
class TeamRoleStrTest(TestCase):
def test_str(self):
role = TeamRole(name="Coach", abbreviation="C")
self.assertEqual(str(role), "Coach (C)")
def test_default_flags(self):
role = TeamRole.objects.create(name="Player", abbreviation="P")
self.assertFalse(role.staff_role)
self.assertFalse(role.admin_role)
self.assertEqual(role.sort_order, 10)
# ---------------------------------------------------------------------------
# Team
# ---------------------------------------------------------------------------
class TeamStrTest(TestCase):
def test_str(self):
team = Team(name="Red Dragons")
self.assertEqual(str(team), "Red Dragons")
class TeamInitialsTest(TestCase):
def test_single_word(self):
self.assertEqual(Team(name="Falcons").initials, "F")
def test_two_words(self):
self.assertEqual(Team(name="Red Dragons").initials, "RD")
def test_more_than_two_words_uses_first_two(self):
self.assertEqual(Team(name="The Red Dragons").initials, "TR")
class TeamGetShortNameTest(TestCase):
def test_returns_short_name_when_set(self):
team = Team(name="Red Dragons", short_name="RD")
self.assertEqual(team.get_short_name(), "RD")
def test_falls_back_to_name_when_none(self):
team = Team(name="Red Dragons", short_name=None)
self.assertEqual(team.get_short_name(), "Red Dragons")
def test_falls_back_to_name_when_empty_string(self):
team = Team(name="Red Dragons", short_name="")
self.assertEqual(team.get_short_name(), "Red Dragons")
class TeamMemberCountTest(TestCase):
def setUp(self):
today = timezone.now().date()
self.season = Season.objects.create(
start_date=today - datetime.timedelta(days=30),
end_date=today + datetime.timedelta(days=335),
)
self.team = make_team()
self.role = make_role()
self.member1 = make_member("m1@test.com", "Alice", "One")
self.member2 = make_member("m2@test.com", "Bob", "Two")
def test_member_count(self):
TeamMembership.objects.create(team=self.team, member=self.member1, season=self.season, role=self.role)
TeamMembership.objects.create(team=self.team, member=self.member2, season=self.season, role=self.role)
self.assertEqual(self.team.member_count, 2)
def test_member_count_zero_when_empty(self):
self.assertEqual(self.team.member_count, 0)
# ---------------------------------------------------------------------------
# TeamMembership
# ---------------------------------------------------------------------------
class TeamMembershipStrTest(TestCase):
def setUp(self):
today = timezone.now().date()
self.season = Season.objects.create(
start_date=today - datetime.timedelta(days=30),
end_date=today + datetime.timedelta(days=335),
)
self.team = make_team("Blue Hawks")
self.role = make_role("Forward", "F")
self.member = make_member("player@test.com", "John", "Doe")
self.membership = TeamMembership.objects.create(
team=self.team, member=self.member, season=self.season, role=self.role
)
def test_str(self):
self.assertEqual(str(self.membership), "Blue Hawks - John Doe (F)")
def test_unique_number_constraint(self):
from django.db import IntegrityError
member2 = make_member("player2@test.com", "Jane", "Doe")
TeamMembership.objects.create(team=self.team, member=member2, season=self.season, role=self.role, number=7)
with self.assertRaises(IntegrityError):
TeamMembership.objects.create(team=self.team, member=self.member, season=self.season, role=self.role, number=7)
def test_captain_default_false(self):
self.assertFalse(self.membership.captain)
self.assertFalse(self.membership.alternate_captain)
# ---------------------------------------------------------------------------
# TeamPicture
# ---------------------------------------------------------------------------
class TeamPictureStrTest(TestCase):
def setUp(self):
self.season = Season.objects.create(
start_date=datetime.date(2025, 9, 1),
end_date=datetime.date(2026, 8, 31),
)
self.team = make_team("Green Eagles")
self.picture = TeamPicture(team=self.team, season=self.season, picture="teams/picture/green-eagles/2025/photo.jpg")
def test_str(self):
self.assertEqual(str(self.picture), "Green Eagles - 2025")