Add billing-ending banner, events CRUD, RBIHF import, public API, team photos, and sponsors
A large batch of club-management features built up over one session: - Club dashboard banner warning admins 1 month before billing ends - Full Events/EventSeries CRUD (recurrence builder, occurrence lifecycle, per-team permissions), with match->game rename and game-specific fields (score, competition, live status, external game ID) - Django-admin competition dropdown, gated per-club by feature flag - Auto-import of RBIHF fixtures (scrape -> diff -> preview -> confirm), with location/opponent dropdowns suggested from existing club data - Feature-flag-gated Shop/Forms nav sections, reusing the same flag machinery for the RBIHF import button - Team roster now scoped to members active this season or next, sorted and grouped by position - Club sport type (ice hockey / other), shown in the control panel's club subtitle - Per-season team photo upload from the team page - New public read-only API (Django Ninja) at /api/v1/: news, team rosters, upcoming/live/per-team games, and sponsors -- auto-documented via Swagger UI, CORS-enabled for a club's own external website - Club sponsors: admin-only CRUD (logo, URL, active date window) plus a date-windowed, optionally randomized API endpoint - Assorted fixes: NullBooleanField dropdown rendering, cross-club event validation timing, searchable-select chip placement, btn-neutral -> default button style sweep, calendar-month chart windows Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R1gj3J1QPfP38XWpnpbFpy
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.http import Http404
|
||||
from waffle import flag_is_active
|
||||
|
||||
from .services.access import can_add_news, can_edit_news, can_publish_news, has_management_access, is_club_admin, is_coach_manager, teams_managed_by
|
||||
|
||||
@@ -36,6 +37,27 @@ class ClubAdminRequiredMixin(ClubStaffRequiredMixin):
|
||||
return is_club_admin(self.request.user, self.request.club)
|
||||
|
||||
|
||||
class FeatureRequiredMixin(ClubAdminRequiredMixin):
|
||||
"""Gate for a whole management section (shop, forms, ...) this club doesn't
|
||||
have at all unless its waffle Flag (see the ``features`` app, set per-club
|
||||
from the control panel's Features page) is active for it. Checked before
|
||||
the admin-only test below and as a plain 404 rather than folded into
|
||||
``test_func``'s 403: a club with the feature off doesn't have a permissions
|
||||
problem, the section just doesn't exist there, same reasoning as
|
||||
``ClubStaffRequiredMixin`` 404ing the whole app off the base domain.
|
||||
|
||||
Subclasses set ``feature_flag`` to the Flag's name, e.g. ``"shop"``.
|
||||
"""
|
||||
|
||||
feature_flag: str = ""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
club = getattr(request, "club", None)
|
||||
if club is not None and not flag_is_active(request, self.feature_flag):
|
||||
raise Http404(f"The “{self.feature_flag}” feature isn't enabled for this club.")
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class TeamManagerRequiredMixin(ClubStaffRequiredMixin):
|
||||
"""A manager of *this* team, or a club ADMIN. ``self.get_team()`` must return the
|
||||
``Team`` the view acts on (e.g. from the URL's ``pk``) before ``test_func`` runs.
|
||||
@@ -51,6 +73,23 @@ class TeamManagerRequiredMixin(ClubStaffRequiredMixin):
|
||||
return teams_managed_by(user, club).filter(pk=self.get_team().pk).exists()
|
||||
|
||||
|
||||
class EventManagerRequiredMixin(ClubStaffRequiredMixin):
|
||||
"""Admin, or a manager of at least one of this event's/series' *current*
|
||||
teams. ``self.get_teams()`` must return the Team queryset/iterable the
|
||||
view acts on (e.g. ``self.get_object().teams.all()``) before ``test_func``
|
||||
runs. Events/series aren't single-team like a roster entry -- ``teams`` is
|
||||
M2M, so authority is "manages at least one", not "manages the one"."""
|
||||
|
||||
def get_teams(self):
|
||||
raise NotImplementedError("Subclasses must return the Teams this view acts on.")
|
||||
|
||||
def test_func(self):
|
||||
user, club = self.request.user, self.request.club
|
||||
if is_club_admin(user, club):
|
||||
return True
|
||||
return teams_managed_by(user, club).filter(pk__in=self.get_teams().values_list("pk", flat=True)).exists()
|
||||
|
||||
|
||||
class ManagementPositionRequiredMixin(ClubStaffRequiredMixin):
|
||||
"""ADMIN, or anyone with a current-season *management*-position
|
||||
StaffAssignment on any team -- unlike ``TeamManagerRequiredMixin``, the
|
||||
|
||||
Reference in New Issue
Block a user