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

@@ -1,7 +1,7 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.http import Http404
from .services.access import has_management_access, is_club_admin, teams_managed_by
from .services.access import can_add_news, can_edit_news, can_publish_news, has_management_access, is_club_admin, teams_managed_by
class ClubStaffRequiredMixin(LoginRequiredMixin, UserPassesTestMixin):
@@ -49,3 +49,31 @@ class TeamManagerRequiredMixin(ClubStaffRequiredMixin):
if is_club_admin(user, club):
return True
return teams_managed_by(user, club).filter(pk=self.get_team().pk).exists()
class NewsAuthorRequiredMixin(ClubStaffRequiredMixin):
"""ADMIN, EDITOR, or a current-season coach_manager -- who's trusted to
author club content in the first place (creating a draft)."""
def test_func(self):
return can_add_news(self.request.user, self.request.club)
class NewsPublisherRequiredMixin(ClubStaffRequiredMixin):
"""ADMIN/EDITOR only -- the release-flow gate for pushing a news item live
(or pulling it back)."""
def test_func(self):
return can_publish_news(self.request.user, self.request.club)
class NewsEditRequiredMixin(ClubStaffRequiredMixin):
"""Whoever may edit *this* news item right now: broad while it's a draft,
editor/admin-only once published. ``self.get_news_item()`` must return the
News the view acts on before ``test_func`` runs."""
def get_news_item(self):
raise NotImplementedError("Subclasses must return the News item this view acts on.")
def test_func(self):
return can_edit_news(self.request.user, self.get_news_item())

View File

@@ -151,3 +151,22 @@ def can_edit_event(user: User, event: Event) -> bool:
def can_manage_shop(user: User, club: Club) -> bool:
return is_club_admin(user, club)
def can_add_news(user: User, club: Club) -> bool:
"""ADMIN, EDITOR, or a current-season coach_manager -- who's trusted to
author club content, not just anyone on staff (a physio shouldn't post news)."""
return is_club_admin(user, club) or has_club_role(user, club, ClubRole.Roles.EDITOR) or is_coach_manager(user, club)
def can_publish_news(user: User, club: Club) -> bool:
"""Only ADMIN/EDITOR may push a news item live -- the release-flow gate."""
return is_club_admin(user, club) or has_club_role(user, club, ClubRole.Roles.EDITOR)
def can_edit_news(user: User, news_item) -> bool:
"""Broad while it's a draft (anyone who could create one); editor/admin-only
once published -- an editor is accountable for what's actually live."""
if news_item.status == news_item.Status.PUBLISHED:
return can_publish_news(user, news_item.club)
return can_add_news(user, news_item.club)