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

@@ -7,12 +7,13 @@ from django.db import IntegrityError
from django.test import TestCase
from django.utils import timezone
from club.models import Club, ClubMembership, Season
from club.models import Club, ClubMembership, ClubRole, Season
from members.models import Member
from notifications.models import Notification
from teams.models import Position, Team, TeamMembership
from .models import News, NewsPhoto
from .services import notify_editors_of_pending_review
from .tasks import notify_news_published
User = get_user_model()
@@ -76,6 +77,13 @@ class NewsModelTests(TestCase):
self.assertEqual(item.published_at, future)
self.assertTrue(item.is_scheduled)
def test_submit_for_review_moves_a_draft_to_pending_review(self):
item = News.objects.create(club=self.club, title="Item", body="Body.")
item.submit_for_review()
self.assertEqual(item.status, News.Status.PENDING_REVIEW)
def test_unpublish_clears_the_publish_date(self):
item = News.objects.create(club=self.club, title="Item", body="Body.")
item.publish()
@@ -211,3 +219,47 @@ class NotifyNewsPublishedTests(TestCase):
notification = Notification.objects.get(member=member)
self.assertEqual(notification.body, "Bold text.")
self.assertEqual(len(mail.outbox), 1)
class NotifyEditorsOfPendingReviewTests(TestCase):
"""news.services.notify_editors_of_pending_review -- in-app only (no
email), every ADMIN/EDITOR for the club."""
@classmethod
def setUpTestData(cls):
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
def make_role(self, first_name, role):
member = Member.objects.create(first_name=first_name, last_name="Staff")
ClubRole.objects.create(club=self.club, member=member, role=role)
return member
def test_notifies_admins_and_editors(self):
admin = self.make_role("Ada", ClubRole.Roles.ADMIN)
editor = self.make_role("Ed", ClubRole.Roles.EDITOR)
author = self.make_role("Cara", ClubRole.Roles.MEMBER)
news_item = News.objects.create(club=self.club, title="Draft item", body="Body.", created_by=author)
notify_editors_of_pending_review(news_item)
self.assertTrue(Notification.objects.filter(member=admin).exists())
self.assertTrue(Notification.objects.filter(member=editor).exists())
self.assertFalse(Notification.objects.filter(member=author).exists())
def test_sends_no_email(self):
self.make_role("Ada", ClubRole.Roles.ADMIN)
news_item = News.objects.create(club=self.club, title="Draft item", body="Body.")
notify_editors_of_pending_review(news_item)
self.assertEqual(len(mail.outbox), 0)
self.assertIsNone(Notification.objects.first().sent_at)
def test_the_notification_names_the_author(self):
self.make_role("Ada", ClubRole.Roles.ADMIN)
author = Member.objects.create(first_name="Cara", last_name="Coach")
news_item = News.objects.create(club=self.club, title="Draft item", body="Body.", created_by=author)
notify_editors_of_pending_review(news_item)
self.assertIn("Cara Coach", Notification.objects.first().body)