diff --git a/mobile/coach_views.py b/mobile/coach_views.py index 29fc3ae..d6e23bb 100644 --- a/mobile/coach_views.py +++ b/mobile/coach_views.py @@ -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 diff --git a/mobile/templates/mobile/coach/squad.html b/mobile/templates/mobile/coach/squad.html index 81589ad..a3b6a31 100644 --- a/mobile/templates/mobile/coach/squad.html +++ b/mobile/templates/mobile/coach/squad.html @@ -60,6 +60,15 @@
{{ assignment.member.get_full_name }}
{{ assignment.position }}
+ {% if can_manage_active_team and assignment.member_id != me.pk %} + {% trans "Remove this staff member?" as remove_staff_confirm_text %} +
+ {% csrf_token %} + +
+ {% endif %} {% empty %}
{% trans "No staff assigned for this season yet." %}
diff --git a/mobile/tests.py b/mobile/tests.py index e0257fe..80172f2 100644 --- a/mobile/tests.py +++ b/mobile/tests.py @@ -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") diff --git a/mobile/urls.py b/mobile/urls.py index 7caa43e..9aa0065 100644 --- a/mobile/urls.py +++ b/mobile/urls.py @@ -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//remove/", coach_views.CoachStaffRemoveView.as_view(), name="coach_staff_remove"), path("coach/lineup//", coach_views.CoachLineupView.as_view(), name="coach_lineup"), path("coach/lineup//publish/", coach_views.CoachLineupPublishView.as_view(), name="coach_lineup_publish"), ]