Events can now target members.Group audiences alongside teams, or go club_wide (every ACTIVE ClubMembership member for the event's season) instead of specific teams/groups -- the two are mutually exclusive, enforced in EventForm/EventSeriesForm.clean() since an M2M can't be validated via a DB CheckConstraint or Event.clean() (no PK yet). Attendance sync (events/signals.py) now reacts to GroupMembership and ClubMembership changes the same way it already did for TeamMembership. Authorization: club.services.access.groups_manageable_by mirrors teams_managed_by (all groups for an ADMIN, else only the ones the user belongs to -- Group has no manager/owner concept); a non-admin needs at least one managed team or belonged-to group to create/edit an event, club_wide stays admin-only, and EventManagerRequiredMixin gained a get_groups() hook so a non-admin who creates a group-only event isn't immediately locked out of managing it. Also adds rosterchief.mail.ResendEmailBackend, an HTTP-API-based Django email backend for Resend (resend.com) using the existing `requests` dependency -- no new SDK. Opt in via DJANGO_EMAIL_BACKEND and RESEND_API_KEY; every Django-sent email (allauth's password reset included) follows whichever EMAIL_BACKEND is configured, so this covers all of them for free. Resend's own SMTP relay remains a valid code-free alternative, documented alongside it in .env.production.example. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
126 lines
5.1 KiB
Python
126 lines
5.1 KiB
Python
from django import forms
|
|
from django.contrib import admin
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from .models import Attendance, Competition, Event, EventReferee, EventSeries, Location, Opponent
|
|
|
|
|
|
@admin.register(Opponent)
|
|
class OpponentAdmin(admin.ModelAdmin):
|
|
list_display = ["name", "club"]
|
|
list_filter = ["club"]
|
|
search_fields = ["name"]
|
|
|
|
|
|
@admin.register(Location)
|
|
class LocationAdmin(admin.ModelAdmin):
|
|
list_display = ["name", "city", "club"]
|
|
list_filter = ["club"]
|
|
search_fields = ["name", "city"]
|
|
|
|
|
|
class AttendanceInline(admin.TabularInline):
|
|
model = Attendance
|
|
extra = 0
|
|
raw_id_fields = ["member"]
|
|
|
|
|
|
class EventRefereeInline(admin.TabularInline):
|
|
model = EventReferee
|
|
extra = 0
|
|
raw_id_fields = ["member", "assigned_by"]
|
|
|
|
|
|
@admin.register(EventSeries)
|
|
class EventSeriesAdmin(admin.ModelAdmin):
|
|
list_display = ["title", "kind", "rrule", "dtstart", "until", "generated_until", "club"]
|
|
list_filter = ["kind", "club"]
|
|
search_fields = ["title"]
|
|
autocomplete_fields = ["location", "opponent", "teams", "groups", "invited_members", "excluded_members"]
|
|
fieldsets = [
|
|
[None, {"fields": ["title", "kind"]}],
|
|
[_("Recurrence"), {"fields": ["rrule", "dtstart", "until", "excluded_dates", "generated_until"]}],
|
|
[_("Timing"), {"fields": ["duration", "gathering_offset", "deadline_offset"]}],
|
|
[_("Audience"), {"fields": ["teams", "groups", "club_wide", "invited_members", "excluded_members"]}],
|
|
[_("Where"), {"fields": ["location", "opponent"]}],
|
|
]
|
|
|
|
|
|
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", "groups", "club_wide", "invited_members", "excluded_members",
|
|
"start", "end", "gathering", "deadline", "location", "opponent",
|
|
"competition", "external_game_id", "score_for", "score_against", "is_live", "max_referees",
|
|
]
|
|
|
|
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"]
|
|
date_hierarchy = "start"
|
|
autocomplete_fields = ["season", "series", "location", "opponent", "teams", "groups", "invited_members", "excluded_members"]
|
|
inlines = [AttendanceInline, EventRefereeInline]
|
|
fieldsets = [
|
|
[None, {"fields": ["title", "kind", "season"]}],
|
|
[_("Series"), {"fields": ["series", "detached", "cancelled"]}],
|
|
[_("Audience"), {"fields": ["teams", "groups", "club_wide", "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", "max_referees"]}],
|
|
]
|
|
|
|
|
|
@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"]
|
|
list_filter = ["status", "showed_up", "event__kind"]
|
|
search_fields = ["event__title", "member__first_name", "member__last_name"]
|
|
raw_id_fields = ["event", "member"]
|
|
|
|
|
|
@admin.register(EventReferee)
|
|
class EventRefereeAdmin(admin.ModelAdmin):
|
|
list_display = ["event", "display_name", "fee", "km", "total_payable", "assigned_by"]
|
|
search_fields = ["event__title", "member__first_name", "member__last_name", "external_name"]
|
|
raw_id_fields = ["event", "member", "assigned_by"]
|
|
|
|
@admin.display(description=_("referee"))
|
|
def display_name(self, obj):
|
|
return obj.display_name
|
|
|
|
@admin.display(description=_("total payable"))
|
|
def total_payable(self, obj):
|
|
return obj.total_payable
|