Fold Roster and Staff into Teams, with a per-season switcher and real CRUD

One "Teams" nav entry now, matching how Family has none of its own and
is only reached through Members -- Roster/Staff were pure club-wide
list stubs with no way to add/remove anyone except Django admin.
Opening a team shows its roster and staff for whichever season is
selected (?season=, defaulting to the current one, same pattern as the
Memberships page); adding/editing/removing either is gated to that
team's own manager or a club admin (TeamManagerRequiredMixin, defined
earlier but never actually wired up until now).
This commit is contained in:
2026-08-03 22:08:35 +02:00
parent 9a2a06180f
commit a3e413904f
7 changed files with 551 additions and 35 deletions

View File

@@ -9,7 +9,7 @@ from club.models import ClubMembership, ClubRole, FeePayment
from members.models import Family, FamilyMembership, Member
from members.services.family import find_member_by_email
from news.models import News
from teams.models import Position, Team
from teams.models import Position, StaffAssignment, Team, TeamMembership
User = get_user_model()
@@ -27,6 +27,46 @@ class TeamForm(forms.ModelForm):
fields = ["name", "short_name"]
class TeamMembershipForm(forms.ModelForm):
"""Add/edit one roster entry -- team and season come from the view (the URL
already identifies both), never from the form itself."""
class Meta:
model = TeamMembership
fields = ["member", "position", "jersey_number", "is_captain", "is_alternate_captain"]
widgets = {"member": forms.Select(attrs={"data-searchable": "true", "data-search-placeholder": _("Type a name to search...")})}
def __init__(self, *args, club=None, team=None, season=None, **kwargs):
super().__init__(*args, **kwargs)
members = Member.objects.filter(member_of__club=club).distinct()
if team is not None and season is not None:
# Already on this team's roster this season -- offering them again
# would just fail the unique_member_per_team_per_season constraint.
taken = TeamMembership.objects.filter(team=team, season=season).exclude(pk=self.instance.pk).values_list("member_id", flat=True)
members = members.exclude(pk__in=taken)
self.fields["member"].queryset = members
self.fields["position"].queryset = Position.objects.filter(club=club, staff_position=False)
class StaffAssignmentForm(forms.ModelForm):
"""Assign/edit one staff assignment -- team and season come from the view,
same reasoning as TeamMembershipForm."""
class Meta:
model = StaffAssignment
fields = ["member", "position"]
widgets = {"member": forms.Select(attrs={"data-searchable": "true", "data-search-placeholder": _("Type a name to search...")})}
def __init__(self, *args, club=None, team=None, season=None, **kwargs):
super().__init__(*args, **kwargs)
members = Member.objects.filter(member_of__club=club).distinct()
if team is not None and season is not None:
taken = StaffAssignment.objects.filter(team=team, season=season).exclude(pk=self.instance.pk).values_list("member_id", flat=True)
members = members.exclude(pk__in=taken)
self.fields["member"].queryset = members
self.fields["position"].queryset = Position.objects.filter(club=club, staff_position=True)
class PositionForm(forms.ModelForm):
class Meta:
model = Position