From 3914765f90fee9b5d7fc9951c392d5fb79411fb8 Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Sun, 12 Jul 2026 15:38:06 +0200 Subject: [PATCH] feat(tenancy): add Season current-season lookup and year name Complete the Season model: - name property renders the start/end years as a "YY-YY" label (e.g. "25-26") via strftime %y, and __str__ now returns it. - get_current(date) returns the active club's season covering the given date (today by default), inclusive of both boundaries, scoped through the tenant queryset so it never crosses clubs. Rename the tenant queryset's current() to current_club() for clarity and update callers/tests. Cover the new behaviour; tenancy modules stay at 100%. Co-Authored-By: Claude Opus 4.8 --- club/models.py | 12 +++++-- club/tests.py | 77 ++++++++++++++++++++++++++++++++++++++++++--- clubmanager/base.py | 2 +- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/club/models.py b/club/models.py index 8b1c136..9191a8b 100644 --- a/club/models.py +++ b/club/models.py @@ -68,15 +68,21 @@ class Season(ClubScopedModel): end_date = models.DateField(_("end date")) def __str__(self): - return f"{self.start_date} - {self.end_date}" + return self.name class Meta: verbose_name = _("season") verbose_name_plural = _("seasons") + @property + def name(self): + """Short label built from the start/end years, e.g. "25-26".""" + return f"{self.start_date:%y}-{self.end_date:%y}" + @classmethod def get_current(cls, date: datetime.date | None = None): + """Return the current club's season covering ``date`` (today by default).""" if date is None: date = timezone.now().date() - - \ No newline at end of file + + return cls.objects.current_club().filter(start_date__lte=date, end_date__gte=date).first() diff --git a/club/tests.py b/club/tests.py index bed8ac3..fe0e524 100644 --- a/club/tests.py +++ b/club/tests.py @@ -4,6 +4,7 @@ from contextlib import contextmanager from django.db import IntegrityError from django.test import RequestFactory, TestCase, override_settings +from django.utils import timezone from members.models import Member @@ -350,14 +351,82 @@ class TenantScopedModelTests(TestCase): self.assertEqual(list(Season.objects.for_club(self.club)), [mine]) - def test_current_filters_by_active_club(self): + def test_current_club_filters_by_active_club(self): mine = Season.objects.create(club=self.club, **self.dates) Season.objects.create(club=self.other, **self.dates) with with_club(self.club): - self.assertEqual(list(Season.objects.current()), [mine]) + self.assertEqual(list(Season.objects.current_club()), [mine]) - def test_str_shows_date_range(self): + def test_name_is_two_digit_year_range(self): season = Season.objects.create(club=self.club, **self.dates) - self.assertEqual(str(season), "2026-08-01 - 2027-05-31") + self.assertEqual(season.name, "26-27") + + def test_name_zero_pads_years(self): + season = Season.objects.create( + club=self.club, + start_date=datetime.date(2008, 8, 1), + end_date=datetime.date(2009, 5, 31), + ) + + self.assertEqual(season.name, "08-09") + + def test_str_is_the_name(self): + season = Season.objects.create(club=self.club, **self.dates) + + self.assertEqual(str(season), "26-27") + + +class SeasonGetCurrentTests(TestCase): + def setUp(self): + self.club = Club.objects.create(name="Ajax United", slug="ajax-united") + self.other = Club.objects.create(name="Rival FC", slug="rival-fc") + self.season = Season.objects.create( + club=self.club, + start_date=datetime.date(2026, 8, 1), + end_date=datetime.date(2027, 5, 31), + ) + + def test_returns_season_covering_the_given_date(self): + with with_club(self.club): + found = Season.get_current(datetime.date(2026, 12, 25)) + + self.assertEqual(found, self.season) + + def test_includes_boundary_dates(self): + with with_club(self.club): + self.assertEqual(Season.get_current(datetime.date(2026, 8, 1)), self.season) + self.assertEqual(Season.get_current(datetime.date(2027, 5, 31)), self.season) + + def test_returns_none_when_no_season_covers_the_date(self): + with with_club(self.club): + self.assertIsNone(Season.get_current(datetime.date(2027, 7, 1))) + + def test_is_scoped_to_the_active_club(self): + # The other club's season covers the same date but must not leak. + Season.objects.create( + club=self.other, + start_date=datetime.date(2026, 8, 1), + end_date=datetime.date(2027, 5, 31), + ) + + with with_club(self.other): + found = Season.get_current(datetime.date(2026, 12, 25)) + + self.assertEqual(found.club, self.other) + + def test_defaults_to_today(self): + today = timezone.now().date() + current = Season.objects.create( + club=self.club, + start_date=today - datetime.timedelta(days=10), + end_date=today + datetime.timedelta(days=10), + ) + + with with_club(self.club): + self.assertEqual(Season.get_current(), current) + + def test_requires_an_active_club(self): + with self.assertRaises(RuntimeError): + Season.get_current(datetime.date(2026, 12, 25)) diff --git a/clubmanager/base.py b/clubmanager/base.py index 0965486..3e607e8 100644 --- a/clubmanager/base.py +++ b/clubmanager/base.py @@ -13,7 +13,7 @@ class TenantQuerySet(models.QuerySet): def for_club(self, club: Club): return self.filter(club=club) - def current(self): + def current_club(self): return self.filter(club=require_current_club())