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

@@ -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")