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 %}
+
+ {% 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"