Add a Competitions card to the control panel's Features page

Competition rows (events.services.competitions' per-club data-source
gate) could previously only be managed through the Django admin.
Adds a card alongside Flags/Switches with create/edit/delete, mirroring
the Flags card's own modal pattern -- no cascading effects to weigh on
delete since Event.competition matches by name, not a foreign key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-20 21:40:13 +02:00
parent 3159064e2a
commit 5889b3740e
6 changed files with 269 additions and 4 deletions

View File

@@ -18,10 +18,10 @@ from billing.services.dues import next_period_start, open_period, reactivate, re
from billing.services.invoices import invoice_pdf, issue_invoice
from billing.services.plans import delete_plan, plan_deletion_impact
from club.models import Club, ClubRole
from events.models import Location
from events.models import Competition, Location
from features.models import Maintenance
from .forms import ClubAdminForm, ClubForm, DuePaymentForm, FlagForm, HomeLocationForm, MaintenanceForm, OpenPeriodForm, PlanForm, PlanPriceForm, PlatformAdminForm, SubscriptionForm, TrialForm
from .forms import ClubAdminForm, ClubForm, CompetitionForm, DuePaymentForm, FlagForm, HomeLocationForm, MaintenanceForm, OpenPeriodForm, PlanForm, PlanPriceForm, PlatformAdminForm, SubscriptionForm, TrialForm
from .messages import notify
from .mixins import PlatformStaffRequiredMixin, PlatformSuperuserRequiredMixin, RedirectOnInvalidMixin
from .services.admins import grant_club_admin, revoke_club_admin
@@ -285,10 +285,16 @@ class FeatureListView(PlatformStaffRequiredMixin, TemplateView):
for flag in flags:
flag.edit_form = FlagForm(instance=flag)
competitions = list(Competition.objects.select_related("flag").order_by("name"))
for competition in competitions:
competition.edit_form = CompetitionForm(instance=competition)
return super().get_context_data(
nav="features",
flags=flags,
flag_form=FlagForm(),
competitions=competitions,
competition_form=CompetitionForm(),
switches=Switch.objects.order_by("name"),
maintenance=Maintenance.current(),
maintenance_form=MaintenanceForm(),
@@ -348,6 +354,58 @@ class FlagUpdateView(PlatformStaffRequiredMixin, RedirectOnInvalidMixin, UpdateV
return reverse("controlpanel:features")
class CompetitionCreateView(PlatformStaffRequiredMixin, RedirectOnInvalidMixin, CreateView):
"""Reachable only via the "New competition" modal on the features page --
POST-only, and there is no standalone template to render on GET or on a
rejected submission."""
model = Competition
form_class = CompetitionForm
http_method_names = ["post"]
invalid_redirect_url_name = "controlpanel:features"
def form_valid(self, form):
response = super().form_valid(form)
notify(self.request, f"s|Competition created|Competition “{self.object.name}” created.")
return response
def get_success_url(self):
return reverse("controlpanel:features")
class CompetitionUpdateView(PlatformStaffRequiredMixin, RedirectOnInvalidMixin, UpdateView):
"""Reachable only via a competition's "Edit" modal on the features page --
POST-only, and there is no standalone template to render on GET or on a
rejected submission."""
model = Competition
form_class = CompetitionForm
http_method_names = ["post"]
invalid_redirect_url_name = "controlpanel:features"
def form_valid(self, form):
response = super().form_valid(form)
notify(self.request, f"s|Competition updated|Competition “{self.object.name}” updated.")
return response
def get_success_url(self):
return reverse("controlpanel:features")
class CompetitionDeleteView(PlatformStaffRequiredMixin, View):
"""No cascading consequences to weigh (Event.competition matches by name, not
a foreign key -- see events.services.competitions), so this is a plain confirm
modal rather than PlanDeleteView's dedicated impact page."""
def post(self, request, pk):
competition = get_object_or_404(Competition, pk=pk)
name = competition.name
competition.delete()
notify(request, f"w|Competition deleted|Competition “{name}” has been deleted.")
return redirect("controlpanel:features")
class SwitchToggleView(PlatformStaffRequiredMixin, View):
"""Global kill-switch: on or off for the whole platform."""