Add a news review workflow and a staff notification area

News gains a PENDING_REVIEW status between draft and published. A
non-editor author (can_add_news but not can_publish_news -- a
coach_manager, not an ADMIN/EDITOR) gets a "Send for review" button
instead of Publish; an editor/admin always sees Publish directly, no
review step. Submitting notifies every ADMIN/EDITOR in-app only (see
notify_members' new send_email=False) -- a review queue that emailed
on every submission would get noisy fast.

The notification area itself: a topbar bell (badge + dropdown, same
<details>/<summary> convention as the sidebar's user-menu, generalised
to a shared .dismissable-details close handler) visible on every page,
plus a fuller "Notifications" card on the dashboard, both fed by a new
notification_bell context processor. "Mark all read" clears the
signed-in staff member's own unread notifications for this club.

This is the reusable notification system's first consumer beyond news
publishing itself -- validates that notify_members()/Notification
generalise the way they were meant to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-21 09:52:59 +02:00
parent 20a44a915f
commit 0ecdeac354
16 changed files with 421 additions and 26 deletions

View File

@@ -298,3 +298,30 @@ def sidebar_counters(request):
"pending_parent_claims_count": ParentClaim.objects.filter(club=club, status=ParentClaim.Status.PENDING).count(),
"games_missing_referees_count": games_missing_referees_count(club, limit=int(RefereeManagementDashboardView.DEFAULT_RANGE)),
}
def notification_bell(request):
"""The topbar bell's badge count and dropdown contents -- every signed-in
staff member can have notifications (not just admins, unlike
sidebar_counters' admin-only queues above), since notifications.Notification
is keyed to whichever Member they are, not to a role.
None (not 0) when there's no club/signed-in user/Member row to key off --
same "hidden means not applicable, 0 means an empty inbox" distinction
sidebar_counters draws."""
club = getattr(request, "club", None)
if club is None or not request.user.is_authenticated:
return {"unread_notification_count": None, "recent_notifications": None}
from members.models import Member
from notifications.models import Notification
member = Member.objects.filter(user=request.user).first()
if member is None:
return {"unread_notification_count": None, "recent_notifications": None}
notifications = Notification.objects.filter(club=club, member=member).order_by("-created")[:8]
return {
"unread_notification_count": Notification.objects.filter(club=club, member=member, read_at__isnull=True).count(),
"recent_notifications": notifications,
}