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,7 +1,8 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Attendance, Event, EventSeries, Location, Opponent
|
||||
from .models import Attendance, Competition, Event, EventSeries, Location, Opponent
|
||||
|
||||
|
||||
@admin.register(Opponent)
|
||||
@@ -39,8 +40,38 @@ class EventSeriesAdmin(admin.ModelAdmin):
|
||||
]
|
||||
|
||||
|
||||
class EventAdminForm(forms.ModelForm):
|
||||
"""The `competition` field is a plain CharField on Event (it just stores a
|
||||
name), but the admin should only ever offer a competition this club is
|
||||
actually allowed to use -- one whose feature flag is active for it, set
|
||||
from the control panel's Features page. A competition with no flag never
|
||||
appears at all."""
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = [
|
||||
"title", "kind", "season", "series", "detached", "cancelled", "teams", "invited_members", "excluded_members",
|
||||
"start", "end", "gathering", "deadline", "location", "opponent",
|
||||
"competition", "external_game_id", "score_for", "score_against", "is_live",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
club = self.instance.club if self.instance.club_id else None
|
||||
competitions = Competition.objects.filter(flag__isnull=False).select_related("flag")
|
||||
if club is not None:
|
||||
competitions = [competition for competition in competitions if competition.flag.is_active_for_club(club)]
|
||||
self.fields["competition"] = forms.ChoiceField(
|
||||
choices=[("", "---------"), *[(competition.name, competition.name) for competition in competitions]],
|
||||
required=False,
|
||||
label=self.fields["competition"].label,
|
||||
help_text=_("Only competitions whose feature flag is active for this club are offered here."),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Event)
|
||||
class EventAdmin(admin.ModelAdmin):
|
||||
form = EventAdminForm
|
||||
list_display = ["title", "kind", "start", "season", "series", "detached", "club"]
|
||||
list_filter = ["kind", "club", "teams", "detached", "cancelled"]
|
||||
search_fields = ["title"]
|
||||
@@ -53,9 +84,18 @@ class EventAdmin(admin.ModelAdmin):
|
||||
[_("Audience"), {"fields": ["teams", "invited_members", "excluded_members"]}],
|
||||
[_("When"), {"fields": ["start", "end", "gathering", "deadline"]}],
|
||||
[_("Where"), {"fields": ["location", "opponent"]}],
|
||||
[_("Game"), {"fields": ["competition", "external_game_id", "score_for", "score_against", "is_live"]}],
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Competition)
|
||||
class CompetitionAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "sport_type", "module", "flag"]
|
||||
list_filter = ["sport_type"]
|
||||
search_fields = ["name", "module"]
|
||||
raw_id_fields = ["flag"]
|
||||
|
||||
|
||||
@admin.register(Attendance)
|
||||
class AttendanceAdmin(admin.ModelAdmin):
|
||||
list_display = ["event", "member", "status", "showed_up"]
|
||||
|
||||
Reference in New Issue
Block a user