Add a news app: coach_manager authoring, team tagging, photos, editor release flow

News and NewsPhoto (team-tagged instead of categorised, one photo taggable
as main via a partial unique constraint), gated per club/services/access.py:
any current-season coach_manager, EDITOR, or ADMIN can draft and edit a news
item; only EDITOR/ADMIN can publish it, or edit it once it's live. Publishing
takes a date so it can be scheduled ahead of time rather than only right now.

Authoring/release only for now -- no member-facing reading page or public API
yet, the visibility field (internal/external/both) is there for when those land.
This commit is contained in:
2026-08-03 18:46:37 +02:00
parent ce35348b31
commit 3e0c63ec36
20 changed files with 985 additions and 19 deletions

View File

@@ -2,11 +2,13 @@ from decimal import Decimal
from django import forms
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from club.models import ClubMembership, ClubRole, FeePayment
from members.models import Family, FamilyMembership, Member
from members.services.family import find_member_by_email
from news.models import News
from teams.models import Position, Team
User = get_user_model()
@@ -146,6 +148,53 @@ class ClubMembershipForm(forms.ModelForm):
fields = ["license", "status", "fee_status", "fee_amount"]
class NewsForm(forms.ModelForm):
"""Title/teams/visibility/body only -- status and published_at are never
directly editable, only through the publish/unpublish actions."""
class Meta:
model = News
fields = ["title", "teams", "visibility", "body"]
widgets = {"teams": forms.CheckboxSelectMultiple, "body": forms.Textarea(attrs={"rows": 8})}
def __init__(self, *args, club=None, **kwargs):
super().__init__(*args, **kwargs)
self.fields["teams"].queryset = Team.objects.filter(club=club)
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
"""Django's own documented recipe for a multi-file upload field: the plain
FileField only ever picks up one of several selected files, so clean() has
to iterate the list itself instead."""
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", MultipleFileInput(attrs={"multiple": True}))
super().__init__(*args, **kwargs)
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
return [single_file_clean(item, initial) for item in data]
return [single_file_clean(data, initial)] if data else []
class NewsPhotoUploadForm(forms.Form):
images = MultipleFileField(label=_("Photos"))
class NewsPublishForm(forms.Form):
published_at = forms.DateTimeField(
label=_("Publish date"),
initial=timezone.now,
widget=forms.DateTimeInput(attrs={"type": "datetime-local"}),
help_text=_("Leave as now to publish immediately, or pick a future date/time to schedule it."),
)
class RecordFeePaymentForm(forms.Form):
"""Money received against one membership's fee -- see club.services.fees.record_payment.
Reusable for any amount, partial or the exact remaining balance; "Mark fully