Simplify the line-up to yes/no picks, and stop truncating notifications
Coach mode's line-up screen no longer has lines/slots -- just a yes/no toggle per available roster player, grouped by their roster position, both in coach mode and on the published member-side view. Replaces LineupUnit/ LineupSlot with a single LineupSelection model. Notifications now show their full body text (no more truncatechars) and the unread colour bar spans the full row height via self-stretch, matching the calendar row's own marker, so it still reads correctly once a body wraps to several lines. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
@@ -1,53 +1,46 @@
|
||||
"""Placing players into a Lineup's slots and publishing it -- coach mode's C3
|
||||
screen (mobile/coach_views.py's CoachLineupView/CoachLineupPlaceView). Mirrors
|
||||
events.services.attendance's shape: small, focused functions the view writes
|
||||
through rather than touching the models directly.
|
||||
"""Picking players for a Lineup (plain yes/no, no lines/slots) and publishing
|
||||
it -- coach mode's C3 screen (mobile/coach_views.py's CoachLineupView).
|
||||
Mirrors events.services.attendance's shape: small, focused functions the
|
||||
view writes through rather than touching the models directly.
|
||||
"""
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from club.services.access import event_season
|
||||
from events.models import Attendance, LineupSlot
|
||||
from events.models import Attendance, LineupSelection
|
||||
from members.models import Member
|
||||
from notifications.services import notify_members
|
||||
from teams.models import StaffAssignment
|
||||
from teams.models import StaffAssignment, TeamMembership
|
||||
|
||||
#: Attendance statuses that mean "not actually available" -- these members
|
||||
#: stay exactly as they are on publish, never flipped to NOT_SELECTED, and
|
||||
#: aren't offered as placeable in the available-players pool.
|
||||
#: aren't offered as pickable for the line-up.
|
||||
UNAVAILABLE_STATUSES = [Attendance.AttendanceStatus.ABSENT, Attendance.AttendanceStatus.EXCUSED, Attendance.AttendanceStatus.NO_RESPONSE]
|
||||
|
||||
|
||||
def place_member(lineup, slot, member):
|
||||
"""Tap-to-place: ``member`` moves into ``slot``, vacating any other slot
|
||||
of theirs in the same ``lineup`` first (a member is only ever in one slot
|
||||
at a time). Whoever was already in ``slot``, if anyone, is simply bumped
|
||||
back to the available pool -- the tap equivalent of the mock's "slots
|
||||
accept one player and swap on drop", without true drag-and-drop's two-way
|
||||
swap (see LineupSlot's own docstring for why tap-to-place instead of
|
||||
drag-and-drop at all)."""
|
||||
LineupSlot.objects.filter(unit__lineup=lineup, member=member).exclude(pk=slot.pk).update(member=None)
|
||||
slot.member = member
|
||||
slot.save(update_fields=["member"])
|
||||
|
||||
|
||||
def clear_slot(slot):
|
||||
slot.member = None
|
||||
slot.save(update_fields=["member"])
|
||||
def toggle_selection(lineup, member) -> bool:
|
||||
"""Flips ``member``'s yes/no pick for ``lineup`` -- deletes their row if
|
||||
they were in, creates one if they weren't. Returns the new state (True =
|
||||
now selected)."""
|
||||
deleted, _details = LineupSelection.objects.filter(lineup=lineup, member=member).delete()
|
||||
if deleted:
|
||||
return False
|
||||
LineupSelection.objects.create(lineup=lineup, member=member)
|
||||
return True
|
||||
|
||||
|
||||
def publish_lineup(lineup):
|
||||
"""Marks the lineup published and writes it into the game record via
|
||||
Attendance -- the reason AttendanceStatus.SELECTED/NOT_SELECTED exist,
|
||||
previously unused anywhere. Every slotted member becomes SELECTED; every
|
||||
previously unused anywhere. Every selected member becomes SELECTED; every
|
||||
other member with an Attendance row for this event who was actually
|
||||
available (not out/silent, see UNAVAILABLE_STATUSES) becomes
|
||||
NOT_SELECTED. Notifies only the selected players."""
|
||||
lineup.published_at = timezone.now()
|
||||
lineup.save(update_fields=["published_at"])
|
||||
|
||||
selected_member_ids = set(LineupSlot.objects.filter(unit__lineup=lineup, member__isnull=False).values_list("member_id", flat=True))
|
||||
selected_member_ids = set(LineupSelection.objects.filter(lineup=lineup).values_list("member_id", flat=True))
|
||||
|
||||
Attendance.objects.filter(event=lineup.event, member_id__in=selected_member_ids).update(status=Attendance.AttendanceStatus.SELECTED)
|
||||
Attendance.objects.filter(event=lineup.event).exclude(member_id__in=selected_member_ids).exclude(status__in=UNAVAILABLE_STATUSES).update(status=Attendance.AttendanceStatus.NOT_SELECTED)
|
||||
@@ -60,6 +53,36 @@ def publish_lineup(lineup):
|
||||
return lineup
|
||||
|
||||
|
||||
def selected_members_by_position(lineup):
|
||||
"""Every LineupSelection for ``lineup``, grouped by the member's roster
|
||||
position for the lineup's own team/season ("category") -- the read side
|
||||
behind the published, member-facing Line-up card (mobile/views.py's
|
||||
EventDetailView). A selected member with no TeamMembership on this team/
|
||||
season (a guest call-up) lands in a catch-all "No position set" bucket
|
||||
rather than being dropped -- mirrors CoachLineupView's own grouping."""
|
||||
season = event_season(lineup.event)
|
||||
member_ids = list(LineupSelection.objects.filter(lineup=lineup).values_list("member_id", flat=True))
|
||||
memberships_by_member = {}
|
||||
if season is not None:
|
||||
memberships_by_member = {tm.member_id: tm for tm in TeamMembership.objects.filter(team=lineup.team, season=season, member_id__in=member_ids).select_related("position")}
|
||||
members_by_id = {member.pk: member for member in Member.objects.filter(pk__in=member_ids)}
|
||||
|
||||
buckets = {}
|
||||
for member_id in member_ids:
|
||||
member = members_by_id.get(member_id)
|
||||
if member is None:
|
||||
continue
|
||||
position = memberships_by_member[member_id].position if member_id in memberships_by_member else None
|
||||
key = position.pk if position else None
|
||||
bucket = buckets.setdefault(key, {"label": position.name if position else _("No position set"), "ordering": position.ordering if position else 9999, "members": []})
|
||||
bucket["members"].append(member)
|
||||
|
||||
categories = sorted(buckets.values(), key=lambda bucket: (bucket["ordering"], bucket["label"]))
|
||||
for bucket in categories:
|
||||
bucket["members"].sort(key=lambda member: (member.last_name, member.first_name))
|
||||
return categories
|
||||
|
||||
|
||||
def notify_dropout(event, member, note):
|
||||
"""A player who was SELECTED in a published line-up reporting, after the
|
||||
fact, that they can no longer make it (mobile.views.EventDetailView.post's
|
||||
|
||||
Reference in New Issue
Block a user