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.
This commit is contained in:
@@ -46,6 +46,7 @@ _NAV_SECTIONS = {
|
|||||||
"team_list": "team_list",
|
"team_list": "team_list",
|
||||||
"team_create": "team_list",
|
"team_create": "team_list",
|
||||||
"team_update": "team_list",
|
"team_update": "team_list",
|
||||||
|
"team_delete": "team_list",
|
||||||
"team_detail": "team_list",
|
"team_detail": "team_list",
|
||||||
"roster_list": "roster_list",
|
"roster_list": "roster_list",
|
||||||
"staff_list": "staff_list",
|
"staff_list": "staff_list",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "management/base.html" %}
|
{% extends "management/base.html" %}
|
||||||
{% load i18n lucide %}
|
{% load i18n lucide ui %}
|
||||||
|
|
||||||
{% block heading %}{% trans "Teams" %}{% endblock heading %}
|
{% block heading %}{% trans "Teams" %}{% endblock heading %}
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Name" %}</th>
|
<th>{% trans "Name" %}</th>
|
||||||
<th>{% trans "Short name" %}</th>
|
<th>{% trans "Short name" %}</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -25,10 +26,18 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a class="link link-hover" href="{% url 'management:team_detail' team.pk %}">{{ team.name }}</a></td>
|
<td><a class="link link-hover" href="{% url 'management:team_detail' team.pk %}">{{ team.name }}</a></td>
|
||||||
<td>{{ team.short_name }}</td>
|
<td>{{ team.short_name }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{% if is_club_admin %}
|
||||||
|
<div class="flex justify-end gap-1">
|
||||||
|
<a class="btn btn-sm btn-outline btn-neutral" href="{% url 'management:team_detail' team.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
|
||||||
|
<button class="btn btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('{{ team.pk|dom_id:"team_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="text-center opacity-60">{% trans "No teams yet." %}</td>
|
<td colspan="3" class="text-center opacity-60">{% trans "No teams yet." %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -36,4 +45,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% 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 %}
|
{% endblock panel %}
|
||||||
|
|||||||
@@ -256,6 +256,37 @@ class TeamManagementTests(ManagementTestBase):
|
|||||||
team = Team.objects.get(club=self.club, name="U15")
|
team = Team.objects.get(club=self.club, name="U15")
|
||||||
self.assertRedirects(response, reverse("management:team_detail", args=[team.pk]))
|
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):
|
class PositionManagementTests(ManagementTestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ urlpatterns = [
|
|||||||
path("teams/new/", views.TeamCreateView.as_view(), name="team_create"),
|
path("teams/new/", views.TeamCreateView.as_view(), name="team_create"),
|
||||||
path("teams/<uuid:pk>/", views.TeamDetailView.as_view(), name="team_detail"),
|
path("teams/<uuid:pk>/", views.TeamDetailView.as_view(), name="team_detail"),
|
||||||
path("teams/<uuid:pk>/edit/", views.TeamUpdateView.as_view(), name="team_update"),
|
path("teams/<uuid:pk>/edit/", views.TeamUpdateView.as_view(), name="team_update"),
|
||||||
|
path("teams/<uuid:pk>/delete/", views.TeamDeleteView.as_view(), name="team_delete"),
|
||||||
path("roster/", views.RosterListView.as_view(), name="roster_list"),
|
path("roster/", views.RosterListView.as_view(), name="roster_list"),
|
||||||
path("staff/", views.StaffListView.as_view(), name="staff_list"),
|
path("staff/", views.StaffListView.as_view(), name="staff_list"),
|
||||||
# News
|
# News
|
||||||
|
|||||||
@@ -713,6 +713,19 @@ class TeamUpdateView(ClubAdminRequiredMixin, UpdateView):
|
|||||||
return super().get_context_data(update_view=True, **kwargs)
|
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):
|
class TeamDetailView(ClubStaffRequiredMixin, DetailView):
|
||||||
template_name = "management/team_detail.html"
|
template_name = "management/team_detail.html"
|
||||||
context_object_name = "team"
|
context_object_name = "team"
|
||||||
|
|||||||
Reference in New Issue
Block a user