Keep Coach Today's session card current past an event's start time
Previously the "Tonight"/"Next up" card and the missing-line-up check flipped to the next session the instant the current one started (start__gte=now). Now an event stays current until 30 minutes past its end time, or 90 minutes past its start when no end is set (most training events carry none) -- a coach mid-practice or mid-game no longer sees the card jump ahead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,11 @@ club/season plumbing already factored into club.services.access -- see
|
||||
mobile/coach_mixins.py's CoachScopeMixin for the shared scaffolding.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Case, F, When
|
||||
from django.http import Http404, HttpResponseForbidden, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
@@ -40,6 +43,25 @@ IN_STATUSES = [Attendance.AttendanceStatus.PRESENT, Attendance.AttendanceStatus.
|
||||
#: Distinct from NO_RESPONSE ("silent"), which is a non-answer rather than a no.
|
||||
OUT_STATUSES = [Attendance.AttendanceStatus.ABSENT, Attendance.AttendanceStatus.EXCUSED, Attendance.AttendanceStatus.NOT_SELECTED]
|
||||
|
||||
#: How long an event stays "current" (CoachTodayView's session card, and the
|
||||
#: missing-line-up nudge) past the moment it starts -- events.start__gte=now
|
||||
#: alone would flip to the next session the instant this one begins, while
|
||||
#: the coach is still mid-practice/mid-game. Past end (when set) plus a grace
|
||||
#: window; past start plus an assumed length when it isn't (most training
|
||||
#: events carry no end time -- see Event.end's own help text).
|
||||
STILL_CURRENT_GRACE = datetime.timedelta(minutes=30)
|
||||
STILL_CURRENT_ASSUMED_DURATION = datetime.timedelta(minutes=90)
|
||||
|
||||
|
||||
def _still_current_events(team):
|
||||
"""Events for ``team`` that haven't yet reached their "still current"
|
||||
cutoff -- see STILL_CURRENT_GRACE/STILL_CURRENT_ASSUMED_DURATION above."""
|
||||
cutoff = Case(
|
||||
When(end__isnull=False, then=F("end") + STILL_CURRENT_GRACE),
|
||||
default=F("start") + STILL_CURRENT_ASSUMED_DURATION,
|
||||
)
|
||||
return Event.objects.filter(teams=team, cancelled=False).annotate(still_current_cutoff=cutoff).filter(still_current_cutoff__gte=timezone.now()).order_by("start")
|
||||
|
||||
|
||||
class CoachTodayView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
"""C1 -- three stat tiles (Squad/In/Silent) for the active team's next
|
||||
@@ -86,7 +108,7 @@ class CoachTodayView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
if team is not None:
|
||||
squad_count = TeamMembership.objects.filter(team=team, season=season).count() if season is not None else 0
|
||||
|
||||
upcoming = Event.objects.filter(teams=team, cancelled=False, start__gte=now).order_by("start")
|
||||
upcoming = _still_current_events(team)
|
||||
tonight_event = upcoming.filter(start__date=today).first()
|
||||
session_event = tonight_event or upcoming.first()
|
||||
|
||||
|
||||
@@ -2419,6 +2419,62 @@ class CoachTodayViewTests(TestCase):
|
||||
self.assertContains(response, "Next up")
|
||||
self.assertContains(response, "Next week")
|
||||
|
||||
def test_a_session_with_an_end_time_stays_current_until_30_minutes_past_it(self):
|
||||
ongoing = Event.objects.create(
|
||||
club=self.club,
|
||||
title="Ongoing game",
|
||||
kind=Event.EventKind.GAME,
|
||||
start=timezone.now() - datetime.timedelta(hours=2),
|
||||
end=timezone.now() - datetime.timedelta(minutes=20),
|
||||
)
|
||||
ongoing.teams.add(self.team)
|
||||
later = Event.objects.create(club=self.club, title="Later practice", kind=Event.EventKind.TRAINING, start=timezone.now() + datetime.timedelta(days=1))
|
||||
later.teams.add(self.team)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get()
|
||||
|
||||
self.assertEqual(response.context["session_event"], ongoing)
|
||||
|
||||
def test_a_session_with_an_end_time_switches_30_minutes_past_it(self):
|
||||
finished = Event.objects.create(
|
||||
club=self.club,
|
||||
title="Finished game",
|
||||
kind=Event.EventKind.GAME,
|
||||
start=timezone.now() - datetime.timedelta(hours=2),
|
||||
end=timezone.now() - datetime.timedelta(minutes=40),
|
||||
)
|
||||
finished.teams.add(self.team)
|
||||
later = Event.objects.create(club=self.club, title="Later practice", kind=Event.EventKind.TRAINING, start=timezone.now() + datetime.timedelta(days=1))
|
||||
later.teams.add(self.team)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get()
|
||||
|
||||
self.assertEqual(response.context["session_event"], later)
|
||||
|
||||
def test_a_session_with_no_end_time_stays_current_until_90_minutes_past_start(self):
|
||||
ongoing = Event.objects.create(club=self.club, title="Ongoing practice", kind=Event.EventKind.TRAINING, start=timezone.now() - datetime.timedelta(minutes=80))
|
||||
ongoing.teams.add(self.team)
|
||||
later = Event.objects.create(club=self.club, title="Later practice", kind=Event.EventKind.TRAINING, start=timezone.now() + datetime.timedelta(days=1))
|
||||
later.teams.add(self.team)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get()
|
||||
|
||||
self.assertEqual(response.context["session_event"], ongoing)
|
||||
|
||||
def test_a_session_with_no_end_time_switches_90_minutes_past_start(self):
|
||||
finished = Event.objects.create(club=self.club, title="Finished practice", kind=Event.EventKind.TRAINING, start=timezone.now() - datetime.timedelta(minutes=100))
|
||||
finished.teams.add(self.team)
|
||||
later = Event.objects.create(club=self.club, title="Later practice", kind=Event.EventKind.TRAINING, start=timezone.now() + datetime.timedelta(days=1))
|
||||
later.teams.add(self.team)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get()
|
||||
|
||||
self.assertEqual(response.context["session_event"], later)
|
||||
|
||||
def test_silent_players_are_counted_and_listed_in_needs_you(self):
|
||||
other_member = Member.objects.create(first_name="Anna", last_name="Player")
|
||||
TeamMembership.objects.create(team=self.team, member=other_member, season=self.season)
|
||||
|
||||
Reference in New Issue
Block a user