Line-up: show each player's turnout rate, notify on changes after publish

A "yes" to this game doesn't say how reliably a player actually shows up --
each row on the line-up screen now shows this season's turnout rate
(events.services.attendance.player_attendance_rankings, one query for the
whole team), color-coded, omitted for anyone with too little history to
mean anything.

Also: editing a published line-up and hitting "Save" never notified anyone
(it only ever wrote LineupSelection, never touched Attendance.status) --
"Publish"/"Publish changes" now stays reachable after the first publish,
and publish_lineup diffs against who was SELECTED before this run so only
players whose status actually changed get notified, not everyone currently
selected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 17:59:18 +02:00
parent 240b6bd345
commit 8c415ed46a
5 changed files with 149 additions and 11 deletions

View File

@@ -573,6 +573,43 @@ class LineupServiceTests(EventsTestBase):
notified_member_ids = set(Notification.objects.filter(member__in=[self.alice, self.bob]).values_list("member_id", flat=True))
self.assertEqual(notified_member_ids, {self.alice.pk})
def test_republish_does_not_renotify_a_player_still_selected(self):
event, lineup = self.make_game_with_lineup()
LineupSelection.objects.create(lineup=lineup, member=self.alice)
Attendance.objects.update_or_create(event=event, member=self.alice, defaults={"status": Attendance.AttendanceStatus.PRESENT})
publish_lineup(lineup)
Notification.objects.filter(member=self.alice).delete()
publish_lineup(lineup)
self.assertFalse(Notification.objects.filter(member=self.alice).exists())
def test_republish_notifies_a_newly_added_player(self):
event, lineup = self.make_game_with_lineup()
Attendance.objects.update_or_create(event=event, member=self.alice, defaults={"status": Attendance.AttendanceStatus.PRESENT})
publish_lineup(lineup)
Notification.objects.filter(member=self.alice).delete()
LineupSelection.objects.create(lineup=lineup, member=self.alice)
publish_lineup(lineup)
notification = Notification.objects.get(member=self.alice)
self.assertIn("in the line-up", notification.body)
def test_republish_notifies_a_dropped_player_with_a_different_message(self):
event, lineup = self.make_game_with_lineup()
LineupSelection.objects.create(lineup=lineup, member=self.alice)
Attendance.objects.update_or_create(event=event, member=self.alice, defaults={"status": Attendance.AttendanceStatus.PRESENT})
publish_lineup(lineup)
Notification.objects.filter(member=self.alice).delete()
LineupSelection.objects.filter(lineup=lineup, member=self.alice).delete()
publish_lineup(lineup)
notification = Notification.objects.get(member=self.alice)
self.assertIn("not in it this time", notification.body)
self.assertEqual(Attendance.objects.get(event=event, member=self.alice).status, Attendance.AttendanceStatus.NOT_SELECTED)
def test_notify_dropout_notifies_the_teams_managers(self):
event, _lineup = self.make_game_with_lineup()
manager = Member.objects.create(first_name="Cara", last_name="Coach")