diff --git a/mobile/coach_views.py b/mobile/coach_views.py index 6a5d6f5..a38558d 100644 --- a/mobile/coach_views.py +++ b/mobile/coach_views.py @@ -38,22 +38,32 @@ IN_STATUSES = [Attendance.AttendanceStatus.PRESENT, Attendance.AttendanceStatus. class CoachTodayView(CoachScopeMixin, LoginRequiredMixin, TemplateView): """C1 -- three stat tiles (Squad/In/Silent) for the active team's next - upcoming session, a "tonight's session" card when one is scheduled today, - a "needs you" list, and an "Also yours" card surfacing the coach's own - member-side RSVP obligation (the same hero_attendance/rsvp_closed pattern - mobile.views.HomeView already computes, scoped to self.me only -- a - coach's own obligations, not the whole roster's). + upcoming session, a session card for it (today's, if there's one on the + calendar today, otherwise whichever is soonest), a "needs you" list, and + an "Also yours" card surfacing the coach's own member-side RSVP + obligation (the same hero_attendance/rsvp_closed pattern mobile.views. + HomeView already computes, scoped to self.me only -- a coach's own + obligations, not the whole roster's). "Needs you" is scoped down from the design mock to what has real backing - data today: a silent-players count and (for a game) an unpublished- - line-up flag for the next session. The mock's member-blocker row stays - deferred -- no coach-facing member-edit screen exists yet to link to. + data today: a silent-players count for the next session, plus an + unpublished-line-up flag for *every* upcoming game within + UPCOMING_GAMES_CHECKED (not just whichever happens to be the very next + session -- a practice landing before Saturday's game shouldn't hide that + the game's own line-up still needs building). The mock's member-blocker + row stays deferred -- no coach-facing member-edit screen exists yet to + link to. """ template_name = "mobile/coach/today.html" screen_title = _("Today") active_tab = "coach_today" + #: How many of the team's soonest upcoming games to check for a missing + #: line-up -- unbounded would mean querying arbitrarily far into a full + #: season; this many is already more advance notice than useful. + UPCOMING_GAMES_CHECKED = 5 + def get_context_data(self, **kwargs): now = timezone.now() today = timezone.localdate() @@ -79,11 +89,30 @@ class CoachTodayView(CoachScopeMixin, LoginRequiredMixin, TemplateView): in_count = attendances.filter(status__in=IN_STATUSES).count() silent_count = attendances.filter(status=Attendance.AttendanceStatus.NO_RESPONSE).count() if silent_count > 0: - needs_you.append({"severity": "warn", "title": _("Silent players"), "detail": _("%(count)d haven't answered yet") % {"count": silent_count}}) - if session_event.kind == Event.EventKind.GAME: - lineup = Lineup.objects.filter(event=session_event).first() - if lineup is None or lineup.published_at is None: - needs_you.append({"severity": "club", "title": _("Line-up not published"), "detail": _("Build it before the game.")}) + needs_you.append( + { + "severity": "warn", + "title": _("Silent players"), + "detail": _("%(count)d haven't answered yet") % {"count": silent_count}, + "action_label": _("Review"), + "action_url": reverse("mobile:coach_attendance", kwargs={"event_id": session_event.pk}) if self.can_manage_active_team else "", + } + ) + + upcoming_games = list(upcoming.filter(kind=Event.EventKind.GAME)[: self.UPCOMING_GAMES_CHECKED]) + published_event_ids = set(Lineup.objects.filter(event__in=upcoming_games, published_at__isnull=False).values_list("event_id", flat=True)) + for game in upcoming_games: + if game.pk in published_event_ids: + continue + needs_you.append( + { + "severity": "club", + "title": _("Line-up not published"), + "detail": _("%(title)s ยท %(date)s") % {"title": game.title, "date": timezone.localtime(game.start).strftime("%a %d %b, %H:%M")}, + "action_label": _("Build"), + "action_url": reverse("mobile:coach_lineup", kwargs={"event_id": game.pk}) if self.can_manage_active_team else "", + } + ) hero_attendance = None rsvp_closed = False diff --git a/mobile/templates/mobile/coach/today.html b/mobile/templates/mobile/coach/today.html index eb79492..39eeb30 100644 --- a/mobile/templates/mobile/coach/today.html +++ b/mobile/templates/mobile/coach/today.html @@ -18,6 +18,15 @@

{% trans "Once you're assigned to a team's staff, its schedule and roster will show up here." %}

{% else %} + {% if session_event %} +
+ + {% if tonight_event %}{% trans "Stats for tonight" %}{% else %}{% blocktrans with date=session_event.start|date:"D d M" %}Stats for {{ date }}{% endblocktrans %}{% endif %} + + {{ session_event.title }} +
+ {% endif %} +
{{ squad_count }}
@@ -33,18 +42,21 @@
- {% if tonight_event %} + {% if session_event %}
- {% blocktrans with time=tonight_event.start|date:"H:i" %}Tonight · {{ time }}{% endblocktrans %} + + {% if tonight_event %}{% blocktrans with time=session_event.start|date:"H:i" %}Tonight · {{ time }}{% endblocktrans %}{% else %}{% blocktrans with date=session_event.start|date:"D d M H:i" %}Next up · {{ date }}{% endblocktrans %}{% endif %} +
-

{{ tonight_event.title }}

+

{{ session_event.title }}

+ {% if session_event.location %}

{{ session_event.location.name }}

{% endif %} {% if can_manage_active_team %}
- {% trans "Check attendance" %} - {% if tonight_event.kind == "game" %} - {% trans "Line-up" %} + {% trans "Check attendance" %} + {% if session_event.kind == "game" %} + {% trans "Line-up" %} {% endif %}
{% endif %} @@ -61,10 +73,8 @@
{{ item.title }}
{{ item.detail }}
- {% if item.severity == "warn" and session_event and can_manage_active_team %} - {% trans "Review" %} - {% elif item.severity == "club" and session_event and can_manage_active_team %} - {% trans "Build" %} + {% if item.action_url %} + {{ item.action_label }} {% endif %}
{% endfor %} @@ -86,7 +96,7 @@ {% endif %} - {% if not tonight_event and not needs_you and not hero_attendance %} + {% if not session_event and not needs_you and not hero_attendance %}

{% trans "Nothing needs your attention right now." %}

diff --git a/mobile/tests.py b/mobile/tests.py index 061db72..bef19da 100644 --- a/mobile/tests.py +++ b/mobile/tests.py @@ -15,6 +15,7 @@ from news.models import News from notifications.models import Notification from teams.models import Position, RefereeLevel, RefereeProfile, StaffAssignment, Team, TeamMembership +from .coach_views import CoachTodayView from .models import CalendarFeedToken, PushSubscription from .services.icons import render_fallback_icon @@ -2405,7 +2406,7 @@ class CoachTodayViewTests(TestCase): self.assertEqual(response.context["tonight_event"], event) self.assertContains(response, "Practice") - def test_no_session_today_omits_the_tonight_card(self): + def test_no_session_today_still_shows_the_next_upcoming_one(self): event = Event.objects.create(club=self.club, title="Next week", kind=Event.EventKind.TRAINING, start=timezone.now() + datetime.timedelta(days=5)) event.teams.add(self.team) self.client.force_login(self.user) @@ -2415,6 +2416,8 @@ class CoachTodayViewTests(TestCase): self.assertIsNone(response.context["tonight_event"]) self.assertEqual(response.context["session_event"], event) self.assertNotContains(response, "Tonight") + self.assertContains(response, "Next up") + self.assertContains(response, "Next week") def test_silent_players_are_counted_and_listed_in_needs_you(self): other_member = Member.objects.create(first_name="Anna", last_name="Player") @@ -2472,6 +2475,54 @@ class CoachTodayViewTests(TestCase): self.assertNotContains(response, "Line-up not published") + def test_a_later_games_missing_lineup_shows_even_when_the_next_session_is_a_practice(self): + # session_event (the very next thing on the calendar) is a practice -- + # the game further out still needs flagging, not just whatever's soonest. + practice = Event.objects.create(club=self.club, title="Practice", kind=Event.EventKind.TRAINING, start=timezone.now() + datetime.timedelta(minutes=5)) + practice.teams.add(self.team) + game = Event.objects.create(club=self.club, title="Saturday's game", kind=Event.EventKind.GAME, start=timezone.now() + datetime.timedelta(days=3)) + game.teams.add(self.team) + self.client.force_login(self.user) + + response = self._get() + + self.assertEqual(response.context["session_event"], practice) + self.assertContains(response, "Saturday's game") + self.assertContains(response, "Line-up not published") + + def test_several_upcoming_games_each_missing_a_lineup_are_all_listed(self): + first = Event.objects.create(club=self.club, title="Game one", kind=Event.EventKind.GAME, start=timezone.now() + datetime.timedelta(days=1)) + first.teams.add(self.team) + second = Event.objects.create(club=self.club, title="Game two", kind=Event.EventKind.GAME, start=timezone.now() + datetime.timedelta(days=3)) + second.teams.add(self.team) + self.client.force_login(self.user) + + response = self._get() + + self.assertContains(response, "Game one") + self.assertContains(response, "Game two") + self.assertContains(response, "Line-up not published", count=2) + + def test_missing_lineup_build_link_points_at_the_specific_game(self): + game = Event.objects.create(club=self.club, title="Big game", kind=Event.EventKind.GAME, start=timezone.now() + datetime.timedelta(days=1)) + game.teams.add(self.team) + self.client.force_login(self.user) + + response = self._get() + + self.assertContains(response, reverse("mobile:coach_lineup", kwargs={"event_id": game.pk})) + + def test_missing_lineup_check_is_capped(self): + cap = CoachTodayView.UPCOMING_GAMES_CHECKED + for day in range(cap + 1): + game = Event.objects.create(club=self.club, title=f"Game {day}", kind=Event.EventKind.GAME, start=timezone.now() + datetime.timedelta(days=day + 1)) + game.teams.add(self.team) + self.client.force_login(self.user) + + response = self._get() + + self.assertContains(response, "Line-up not published", count=cap) + def test_header_shows_the_persons_actual_role_not_a_hardcoded_label(self): self.client.force_login(self.user)