From 9a2a06180f997739c0d86aae57ad2466535922cd Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Mon, 3 Aug 2026 21:42:23 +0200 Subject: [PATCH] Add edit/delete actions to the teams list Admin-only, matching TeamCreateView/TeamUpdateView's existing gate. Deleting cascades away the team's roster and staff assignments -- no ProtectedError to catch, unlike a Member that orders/invoices can still reference. Edit links to the detail page rather than the edit form directly, same convention as the news and member lists. --- management/context_processors.py | 1 + .../templates/management/team_list.html | 23 ++++++++++++-- management/tests.py | 31 +++++++++++++++++++ management/urls.py | 1 + management/views.py | 13 ++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/management/context_processors.py b/management/context_processors.py index 00e9d7a..b52b7da 100644 --- a/management/context_processors.py +++ b/management/context_processors.py @@ -46,6 +46,7 @@ _NAV_SECTIONS = { "team_list": "team_list", "team_create": "team_list", "team_update": "team_list", + "team_delete": "team_list", "team_detail": "team_list", "roster_list": "roster_list", "staff_list": "staff_list", diff --git a/management/templates/management/team_list.html b/management/templates/management/team_list.html index b5d744f..cbe82ee 100644 --- a/management/templates/management/team_list.html +++ b/management/templates/management/team_list.html @@ -1,5 +1,5 @@ {% extends "management/base.html" %} -{% load i18n lucide %} +{% load i18n lucide ui %} {% block heading %}{% trans "Teams" %}{% endblock heading %} @@ -18,6 +18,7 @@ {% trans "Name" %} {% trans "Short name" %} + @@ -25,10 +26,18 @@ {{ team.name }} {{ team.short_name }} + + {% if is_club_admin %} +
+ {% lucide "pencil" size=14 %} {% trans "Edit" %} + +
+ {% endif %} + {% empty %} - {% trans "No teams yet." %} + {% trans "No teams yet." %} {% endfor %} @@ -36,4 +45,14 @@ + + {% if is_club_admin %} + {% trans "Delete team" as delete_team_title %} + {% trans "Delete" as delete_label %} + {% for team in teams %} + {% url 'management:team_delete' team.pk as team_delete_url %} + {% blocktrans with name=team.name asvar delete_team_body %}Delete ā€œ{{ name }}ā€? This also removes its roster and staff assignments. This cannot be undone.{% endblocktrans %} + {% include "controlpanel/_confirm_modal.html" with modal_id=team.pk|dom_id:"team_delete_modal" title=delete_team_title body=delete_team_body action_url=team_delete_url submit_label=delete_label %} + {% endfor %} + {% endif %} {% endblock panel %} diff --git a/management/tests.py b/management/tests.py index 8c45b17..f9f58c5 100644 --- a/management/tests.py +++ b/management/tests.py @@ -256,6 +256,37 @@ class TeamManagementTests(ManagementTestBase): team = Team.objects.get(club=self.club, name="U15") self.assertRedirects(response, reverse("management:team_detail", args=[team.pk])) + def test_deleting_a_team(self): + team = Team.objects.create(club=self.club, name="U16", short_name="U16") + + response = self.club_post("team_delete", {}, team.pk) + + self.assertRedirects(response, reverse("management:team_list")) + self.assertFalse(Team.objects.filter(pk=team.pk).exists()) + + def test_deleting_a_team_cascades_its_roster_and_staff(self): + team = Team.objects.create(club=self.club, name="U17", short_name="U17") + position = Position.objects.create(club=self.club, name="Coach17", short_name="C17", staff_position=True) + member = Member.objects.create(first_name="Sam", last_name="Staffer") + StaffAssignment.objects.create(team=team, member=member, season=self.season, position=position) + + self.club_post("team_delete", {}, team.pk) + + self.assertFalse(StaffAssignment.objects.filter(team=team).exists()) + + def test_a_non_admin_cannot_delete_a_team(self): + team = Team.objects.create(club=self.club, name="U18", short_name="U18") + coach_user = User.objects.create_user(email="coach-team-delete@example.com", password="pw-secret-123") + coach_member = Member.objects.create(user=coach_user, first_name="Cara", last_name="Coach") + position = Position.objects.create(club=self.club, name="CoachTeamDelete", short_name="CTD", staff_position=True, management_position=True) + StaffAssignment.objects.create(team=team, member=coach_member, season=self.season, position=position) + self.client.force_login(coach_user) + + response = self.club_post("team_delete", {}, team.pk) + + self.assertEqual(response.status_code, 403) + self.assertTrue(Team.objects.filter(pk=team.pk).exists()) + class PositionManagementTests(ManagementTestBase): def setUp(self): diff --git a/management/urls.py b/management/urls.py index 6a1e2a5..21feb81 100644 --- a/management/urls.py +++ b/management/urls.py @@ -40,6 +40,7 @@ urlpatterns = [ path("teams/new/", views.TeamCreateView.as_view(), name="team_create"), path("teams//", views.TeamDetailView.as_view(), name="team_detail"), path("teams//edit/", views.TeamUpdateView.as_view(), name="team_update"), + path("teams//delete/", views.TeamDeleteView.as_view(), name="team_delete"), path("roster/", views.RosterListView.as_view(), name="roster_list"), path("staff/", views.StaffListView.as_view(), name="staff_list"), # News diff --git a/management/views.py b/management/views.py index e000c25..20c92f5 100644 --- a/management/views.py +++ b/management/views.py @@ -713,6 +713,19 @@ class TeamUpdateView(ClubAdminRequiredMixin, UpdateView): return super().get_context_data(update_view=True, **kwargs) +class TeamDeleteView(ClubAdminRequiredMixin, View): + def post(self, request, pk): + team = get_object_or_404(Team.objects.filter(club=request.club), pk=pk) + name = str(team) + # TeamMembership/StaffAssignment cascade away with the team -- no ProtectedError + # to catch, unlike a Member (which orders/invoices can still reference). + team.delete() + + body = _("ā€œ%(team)sā€ has been deleted.") % {"team": name} + notify(request, f"w|{_('Team deleted')}|{body}") + return redirect("management:team_list") + + class TeamDetailView(ClubStaffRequiredMixin, DetailView): template_name = "management/team_detail.html" context_object_name = "team"