Highlight Squad on the Add player/staff screens, add staff removal
- CoachAddPlayerView/CoachAddStaffView now set active_tab = "coach_squad" instead of "coach_today" -- both are reached from Squad's own "Add" buttons, so that's the tab that should stay highlighted while there. - Squad screen: each staff row (other than your own) gets a remove button, mirroring the roster's own. Self-removal stays a desktop-only action (management.views.TeamStaffRemoveView) -- doing it from here would strand a coach off a team they're actively viewing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -457,7 +457,7 @@ class CoachAddPlayerView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
|
||||
template_name = "mobile/coach/add_player.html"
|
||||
screen_title = _("Add players")
|
||||
active_tab = "coach_today"
|
||||
active_tab = "coach_squad"
|
||||
|
||||
#: ?filter= values this screen understands -- anything else (including no
|
||||
#: param at all) means "All".
|
||||
@@ -540,7 +540,7 @@ class CoachAddStaffView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
|
||||
template_name = "mobile/coach/add_staff.html"
|
||||
screen_title = _("Add staff")
|
||||
active_tab = "coach_today"
|
||||
active_tab = "coach_squad"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not self.can_manage_active_team:
|
||||
@@ -794,6 +794,30 @@ class CoachRosterRemoveView(CoachScopeMixin, LoginRequiredMixin, View):
|
||||
return HttpResponseRedirect(reverse("mobile:coach_squad"))
|
||||
|
||||
|
||||
class CoachStaffRemoveView(CoachScopeMixin, LoginRequiredMixin, View):
|
||||
"""Squad screen's per-staff-row remove action. A manager can't remove
|
||||
their own StaffAssignment this way -- self-removal would strand them off
|
||||
a team they're actively viewing, with no one obviously left to undo it;
|
||||
that stays a desktop-only action (management.views.TeamStaffRemoveView),
|
||||
which any *other* admin/manager can still reach."""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self.active_team is None:
|
||||
raise Http404
|
||||
if not self.can_manage_active_team:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
assignment = get_object_or_404(StaffAssignment.objects.filter(team=self.active_team), pk=kwargs["assignment_pk"])
|
||||
if self.me is not None and assignment.member_id == self.me.pk:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
member = assignment.member
|
||||
assignment.delete()
|
||||
|
||||
notify(request, f"w|{_('Staff removed')}|" + _("“%(member)s” removed from staff.") % {"member": member})
|
||||
return HttpResponseRedirect(reverse("mobile:coach_squad"))
|
||||
|
||||
|
||||
class CoachScheduleView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
"""Bottom-tab "Schedule" -- every upcoming event for the active team, full
|
||||
stop (not Today's own "just the next session" scope). Each row jumps
|
||||
|
||||
@@ -60,6 +60,15 @@
|
||||
<div class="text-[15px] font-semibold text-ink">{{ assignment.member.get_full_name }}</div>
|
||||
<div class="text-xs text-muted">{{ assignment.position }}</div>
|
||||
</div>
|
||||
{% if can_manage_active_team and assignment.member_id != me.pk %}
|
||||
{% trans "Remove this staff member?" as remove_staff_confirm_text %}
|
||||
<form method="post" action="{% url "mobile:coach_staff_remove" assignment.pk %}" hx-boost="false" onsubmit="return confirm('{{ remove_staff_confirm_text|escapejs }}')">
|
||||
{% csrf_token %}
|
||||
<button class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-club" type="submit" aria-label="{% trans "Remove" %}">
|
||||
{% lucide "trash-2" size=16 %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="p-6 text-center text-sm text-muted">{% trans "No staff assigned for this season yet." %}</div>
|
||||
|
||||
@@ -2879,6 +2879,76 @@ class CoachRosterRemoveViewTests(TestCase):
|
||||
self.assertTrue(TeamMembership.objects.filter(pk=self.membership.pk).exists())
|
||||
|
||||
|
||||
@override_settings(ROSTERCHIEF_BASE_DOMAIN="rosterchief.app", ALLOWED_HOSTS=["rosterchief.app", "ajax-united.rosterchief.app", "testserver"])
|
||||
class CoachStaffRemoveViewTests(TestCase):
|
||||
"""Squad screen's per-staff-row remove action -- see
|
||||
CoachStaffRemoveView's own docstring for why self-removal is refused."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = make_club()
|
||||
today = timezone.localdate()
|
||||
cls.season = Season.objects.create(club=cls.club, start_date=today - datetime.timedelta(days=30), end_date=today + datetime.timedelta(days=300))
|
||||
cls.user = User.objects.create_user(email="coach@example.com", password="pw-secret-123")
|
||||
cls.member = Member.objects.create(first_name="Sam", last_name="Coach", email="coach@example.com", user=cls.user)
|
||||
cls.team = Team.objects.create(club=cls.club, name="U16", short_name="U16")
|
||||
cls.coach_position = Position.objects.create(club=cls.club, name="Head coach", short_name="HC", staff_position=True, management_position=True)
|
||||
cls.own_assignment = StaffAssignment.objects.create(team=cls.team, member=cls.member, season=cls.season, position=cls.coach_position)
|
||||
|
||||
cls.assistant_position = Position.objects.create(club=cls.club, name="Assistant coach", short_name="AC", staff_position=True, management_position=False)
|
||||
cls.assistant = Member.objects.create(first_name="Ali", last_name="Assistant")
|
||||
cls.assignment = StaffAssignment.objects.create(team=cls.team, member=cls.assistant, season=cls.season, position=cls.assistant_position)
|
||||
|
||||
def _post(self, assignment):
|
||||
return self.client.post(reverse("mobile:coach_staff_remove", kwargs={"assignment_pk": assignment.pk}), HTTP_HOST="ajax-united.rosterchief.app")
|
||||
|
||||
def test_removes_another_staff_member(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._post(self.assignment)
|
||||
|
||||
self.assertRedirects(response, reverse("mobile:coach_squad"), fetch_redirect_response=False)
|
||||
self.assertFalse(StaffAssignment.objects.filter(pk=self.assignment.pk).exists())
|
||||
|
||||
def test_cannot_remove_yourself(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._post(self.own_assignment)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTrue(StaffAssignment.objects.filter(pk=self.own_assignment.pk).exists())
|
||||
|
||||
def test_non_managing_staff_cannot_remove_anyone(self):
|
||||
physio_position = Position.objects.create(club=self.club, name="Physio", short_name="PHY", staff_position=True, management_position=False)
|
||||
physio_user = User.objects.create_user(email="physio@example.com", password="pw-secret-123")
|
||||
physio_member = Member.objects.create(first_name="Pat", last_name="Physio", user=physio_user)
|
||||
StaffAssignment.objects.create(team=self.team, member=physio_member, season=self.season, position=physio_position)
|
||||
self.client.force_login(physio_user)
|
||||
|
||||
response = self._post(self.assignment)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTrue(StaffAssignment.objects.filter(pk=self.assignment.pk).exists())
|
||||
|
||||
def test_the_remove_button_is_hidden_on_your_own_row(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("mobile:coach_squad"), HTTP_HOST="ajax-united.rosterchief.app")
|
||||
|
||||
self.assertNotContains(response, reverse("mobile:coach_staff_remove", kwargs={"assignment_pk": self.own_assignment.pk}))
|
||||
self.assertContains(response, reverse("mobile:coach_staff_remove", kwargs={"assignment_pk": self.assignment.pk}))
|
||||
|
||||
def test_an_assignment_from_another_team_is_not_reachable(self):
|
||||
other_team = Team.objects.create(club=self.club, name="U14", short_name="U14")
|
||||
other_position = Position.objects.create(club=self.club, name="Coach", short_name="C", staff_position=True, management_position=True)
|
||||
other_assignment = StaffAssignment.objects.create(team=other_team, member=Member.objects.create(first_name="Other", last_name="Team"), season=self.season, position=other_position)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._post(other_assignment)
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
@override_settings(ROSTERCHIEF_BASE_DOMAIN="rosterchief.app", ALLOWED_HOSTS=["rosterchief.app", "ajax-united.rosterchief.app", "testserver"])
|
||||
class CoachAddStaffViewTests(TestCase):
|
||||
"""Squad screen's staff "Add" entry point -- see CoachAddStaffView's own
|
||||
@@ -2906,6 +2976,13 @@ class CoachAddStaffViewTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_the_squad_tab_is_highlighted(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("mobile:coach_add_staff"), HTTP_HOST="ajax-united.rosterchief.app")
|
||||
|
||||
self.assertEqual(response.context["active_tab"], "coach_squad")
|
||||
|
||||
def test_get_redirects_a_non_managing_staffer(self):
|
||||
physio_position = Position.objects.create(club=self.club, name="Physio", short_name="PHY", staff_position=True, management_position=False)
|
||||
physio_user = User.objects.create_user(email="physio@example.com", password="pw-secret-123")
|
||||
@@ -3487,6 +3564,13 @@ class CoachAddPlayerViewTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_the_squad_tab_is_highlighted(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("mobile:coach_add_player"), HTTP_HOST="ajax-united.rosterchief.app")
|
||||
|
||||
self.assertEqual(response.context["active_tab"], "coach_squad")
|
||||
|
||||
def test_get_redirects_a_non_managing_staffer(self):
|
||||
physio_position = Position.objects.create(club=self.club, name="Physio", short_name="PHY", staff_position=True, management_position=False)
|
||||
physio_user = User.objects.create_user(email="physio@example.com", password="pw-secret-123")
|
||||
|
||||
@@ -35,6 +35,7 @@ urlpatterns = [
|
||||
path("coach/news/new/", coach_views.CoachCreateNewsView.as_view(), name="coach_create_news"),
|
||||
path("coach/roster/add/", coach_views.CoachAddPlayerView.as_view(), name="coach_add_player"),
|
||||
path("coach/staff/add/", coach_views.CoachAddStaffView.as_view(), name="coach_add_staff"),
|
||||
path("coach/staff/<uuid:assignment_pk>/remove/", coach_views.CoachStaffRemoveView.as_view(), name="coach_staff_remove"),
|
||||
path("coach/lineup/<uuid:event_id>/", coach_views.CoachLineupView.as_view(), name="coach_lineup"),
|
||||
path("coach/lineup/<uuid:event_id>/publish/", coach_views.CoachLineupPublishView.as_view(), name="coach_lineup_publish"),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user