Coach lineup polish, referee level/position delete, feature-flag nav gating, per-job pause

- Coach line-up screen: more breathing room above the sheet, brighter event
  subtitle, and the "Schedule" button now matches the date input's height.
- Positions and referee levels can now be deleted from Settings (blocked with
  a friendly message if still in use on a roster/referee profile/inheritance
  chain).
- The Evaluations nav placeholder is now gated on the formbuilder flag, same
  as Forms itself, since the design reuses formbuilder underneath.
- Control panel: each scheduled platform job can now be paused/resumed
  individually (features.models.JobToggle), independent of the platform-wide
  Maintenance lock.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 11:17:46 +02:00
parent e621086f37
commit 1365dcf18e
23 changed files with 422 additions and 3029 deletions

View File

@@ -22,7 +22,7 @@ from billing.services import BillingError
from billing.services.dues import record_payment, start_trial, subscribe, waive
from club.models import Club, ClubMembership, ClubRole, Season
from events.models import Attendance, Competition, Event, Location
from features.models import Maintenance
from features.models import JobToggle, Maintenance
from members.models import Member
from shop.models import Order
from teams.models import Position, StaffAssignment, Team, TeamMembership
@@ -331,6 +331,61 @@ class ClubHomeLocationTests(ControlPanelTestBase):
self.assertFalse(Location.objects.filter(club=self.club, is_home=True).exists())
class JobToggleViewTests(ControlPanelTestBase):
"""Pausing/resuming one scheduled platform job -- see controlpanel.views.
JobToggleView and features.models.JobToggle."""
JOB_NAME = "events.tasks.extend_event_series"
def setUp(self):
super().setUp()
# JobToggle's cache isn't rolled back with the test transaction.
cache.clear()
self.addCleanup(cache.clear)
def toggle(self, name=JOB_NAME):
return self.client.post(reverse("controlpanel:job_toggle", args=[name]))
def test_a_job_starts_enabled(self):
response = self.client.get(reverse("controlpanel:jobs"))
self.assertContains(response, "Pause job")
def test_pausing_a_job(self):
response = self.toggle()
self.assertRedirects(response, reverse("controlpanel:jobs"))
self.assertFalse(JobToggle.is_enabled(self.JOB_NAME))
def test_resuming_a_paused_job(self):
self.toggle()
self.toggle()
self.assertTrue(JobToggle.is_enabled(self.JOB_NAME))
def test_the_jobs_page_reflects_the_paused_state(self):
self.toggle()
response = self.client.get(reverse("controlpanel:jobs"))
self.assertContains(response, "paused")
self.assertContains(response, "Resume job")
def test_an_unknown_job_name_is_a_404(self):
response = self.toggle(name="not.a.real.job")
self.assertEqual(response.status_code, 404)
def test_a_non_staff_user_cannot_toggle_a_job(self):
self.client.force_login(User.objects.create_user(email="plain-jobs@example.com", password="pw-secret-123"))
response = self.toggle()
self.assertEqual(response.status_code, 403)
self.assertTrue(JobToggle.is_enabled(self.JOB_NAME))
class StatisticsTests(TestCase):
@classmethod
def setUpTestData(cls):