Skip to content

Commit 86ecf96

Browse files
authored
♻️ Anchor historical sprint window to current calendar year (#75) (#80)
Follow-up to #79. The 3-year window was anchored to the most recent year present in the webhook data, so it would drift earlier if the current year's webhooks were incomplete (a known coverage-gap risk). Anchor the cutoff to the current calendar year instead: keep conference years >= current_year - HISTORICAL_YEARS. The month no longer matters, and the window stays stable even when a year's data is missing. Adds a testable current_year override and covers both the boundary and the missing-current-year-data case.
1 parent 87748bc commit 86ecf96

2 files changed

Lines changed: 47 additions & 17 deletions

File tree

titowebhooks/tests/test_views.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
from django.urls import reverse
1111

1212
from titowebhooks.models import TitoWebhookEvent
13-
from titowebhooks.views import EVENT_SLUG, JOINER_QUESTION_ID, LEADER_QUESTION_ID
13+
from titowebhooks.views import (
14+
EVENT_SLUG,
15+
JOINER_QUESTION_ID,
16+
LEADER_QUESTION_ID,
17+
_extract_historical_sprints,
18+
)
1419

1520
TEST_PAYLOAD = {
1621
"_type": "ticket",
@@ -381,8 +386,8 @@ def test_one_row_per_person_per_year_merges_days(self):
381386
# Both Thursday (leading) and Friday (joining) captured on one row.
382387
assert merged_2025.count("Yes") >= 2
383388

384-
def test_limits_to_most_recent_three_years(self):
385-
for year in (2022, 2023, 2024, 2025, 2026):
389+
def test_window_anchored_to_current_calendar_year(self):
390+
for year in (2021, 2022, 2023, 2024, 2025, 2026):
386391
self._create_event(
387392
_historical_payload(
388393
email=f"y{year}@example.com",
@@ -392,11 +397,26 @@ def test_limits_to_most_recent_three_years(self):
392397
)
393398
)
394399

395-
body = self._download()
400+
# current_year - years (3) = 2023, so 2023 and newer are kept regardless of
401+
# which year happens to be the most recent one present in the data.
402+
rows = _extract_historical_sprints(current_year=2026)
403+
years = {r["year"] for r in rows}
404+
assert years == {2023, 2024, 2025, 2026}
405+
406+
def test_window_stable_when_current_year_data_missing(self):
407+
# No 2026 webhooks captured (e.g. coverage gap); the window must still be
408+
# anchored to the current calendar year, not the newest year in the data.
409+
for year in (2022, 2023, 2024):
410+
self._create_event(
411+
_historical_payload(
412+
email=f"y{year}@example.com",
413+
release_title="Sprint (In Person) - Thursday",
414+
created_at=f"{year}-09-01T10:00:00Z",
415+
year=year,
416+
)
417+
)
396418

397-
# Most recent year is 2026, so window is 2024-2026.
398-
assert "y2026@example.com" in body
399-
assert "y2025@example.com" in body
400-
assert "y2024@example.com" in body
401-
assert "y2023@example.com" not in body
402-
assert "y2022@example.com" not in body
419+
rows = _extract_historical_sprints(current_year=2026)
420+
years = {r["year"] for r in rows}
421+
# Anchored to 2026: cutoff 2023, so 2022 drops even though it is recent in the data.
422+
assert years == {2023, 2024}

titowebhooks/views.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,19 @@ def _event_year(payload: dict) -> int | None:
114114
return created_at.year if created_at else None
115115

116116

117-
def _extract_historical_sprints(include_online: bool = False, years: int | None = HISTORICAL_YEARS):
117+
def _extract_historical_sprints(
118+
include_online: bool = False, years: int | None = HISTORICAL_YEARS, current_year: int | None = None
119+
):
118120
"""One row per person per conference year, across all events in the webhook log.
119121
120122
Used by the "Download Historical" export so the sprints team can see who has
121123
attended in-person sprints over the last few years. Online sprints are excluded
122-
unless ``include_online`` is set. ``years`` limits the result to the most recent
123-
N conference years present in the data (``None`` keeps every year).
124+
unless ``include_online`` is set.
125+
126+
``years`` limits the result to conference years on or after ``current_year - years``
127+
(``None`` keeps every year). The window is anchored to the calendar year, not to the
128+
most recent year present in the data, so it stays stable even if a year's webhooks are
129+
incomplete. ``current_year`` defaults to today's year and exists mainly for testing.
124130
"""
125131
events = TitoWebhookEvent.objects.filter(trigger="ticket.completed")
126132
# Collect per email+year+release, keeping most recent.
@@ -163,10 +169,14 @@ def _extract_historical_sprints(include_online: bool = False, years: int | None
163169
"created_at": created_at,
164170
}
165171

166-
# Limit to the most recent N conference years present in the data.
167-
if years and by_email_year_release:
168-
max_year = max(t["year"] for t in by_email_year_release.values())
169-
cutoff = max_year - years + 1
172+
# Limit to conference years within the last ``years`` calendar years of the current
173+
# year (e.g. 2026 with years=3 -> 2023 and newer). Anchoring to the calendar year
174+
# rather than the newest year in the data keeps the window stable when a year's
175+
# webhook coverage is incomplete.
176+
if years:
177+
if current_year is None:
178+
current_year = datetime.now(timezone.utc).year
179+
cutoff = current_year - years
170180
by_email_year_release = {k: v for k, v in by_email_year_release.items() if v["year"] >= cutoff}
171181

172182
# Consolidate to one row per person per year.

0 commit comments

Comments
 (0)