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:
100
news/tests.py
Normal file
100
news/tests.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import datetime
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.db import IntegrityError
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from club.models import Club
|
||||
|
||||
from .models import News, NewsPhoto
|
||||
|
||||
|
||||
def make_photo(news_item, *, is_main=False):
|
||||
image = SimpleUploadedFile("photo.jpg", b"fake-image-bytes", content_type="image/jpeg")
|
||||
return NewsPhoto.objects.create(news_item=news_item, image=image, is_main=is_main)
|
||||
|
||||
|
||||
class NewsModelTests(TestCase):
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
|
||||
def test_slug_is_derived_from_title(self):
|
||||
item = News.objects.create(club=self.club, title="Season Kickoff", body="Body text.")
|
||||
|
||||
self.assertEqual(item.slug, "season-kickoff")
|
||||
|
||||
def test_slug_is_unique_per_club_not_globally(self):
|
||||
News.objects.create(club=self.club, title="Season Kickoff", body="First.")
|
||||
second = News.objects.create(club=self.club, title="Season Kickoff", body="Second.")
|
||||
|
||||
self.assertEqual(second.slug, "season-kickoff-2")
|
||||
|
||||
def test_two_clubs_can_share_the_same_slug(self):
|
||||
other_club = Club.objects.create(name="Rival FC", slug="rival-fc")
|
||||
News.objects.create(club=self.club, title="Season Kickoff", body="First.")
|
||||
|
||||
other = News.objects.create(club=other_club, title="Season Kickoff", body="Other club.")
|
||||
|
||||
self.assertEqual(other.slug, "season-kickoff")
|
||||
|
||||
def test_defaults_to_draft_and_internal(self):
|
||||
item = News.objects.create(club=self.club, title="Draft item", body="Body.")
|
||||
|
||||
self.assertEqual(item.status, News.Status.DRAFT)
|
||||
self.assertEqual(item.visibility, News.Visibility.INTERNAL)
|
||||
self.assertIsNone(item.published_at)
|
||||
|
||||
def test_publish_defaults_the_publish_date_to_now(self):
|
||||
item = News.objects.create(club=self.club, title="Item", body="Body.")
|
||||
|
||||
item.publish()
|
||||
|
||||
self.assertEqual(item.status, News.Status.PUBLISHED)
|
||||
self.assertIsNotNone(item.published_at)
|
||||
self.assertFalse(item.is_scheduled)
|
||||
|
||||
def test_publish_accepts_a_future_date_and_is_scheduled(self):
|
||||
item = News.objects.create(club=self.club, title="Item", body="Body.")
|
||||
future = timezone.now() + datetime.timedelta(days=7)
|
||||
|
||||
item.publish(at=future)
|
||||
|
||||
self.assertEqual(item.status, News.Status.PUBLISHED)
|
||||
self.assertEqual(item.published_at, future)
|
||||
self.assertTrue(item.is_scheduled)
|
||||
|
||||
def test_unpublish_clears_the_publish_date(self):
|
||||
item = News.objects.create(club=self.club, title="Item", body="Body.")
|
||||
item.publish()
|
||||
|
||||
item.unpublish()
|
||||
|
||||
self.assertEqual(item.status, News.Status.DRAFT)
|
||||
self.assertIsNone(item.published_at)
|
||||
|
||||
|
||||
class NewsPhotoModelTests(TestCase):
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
self.item = News.objects.create(club=self.club, title="Match report", body="Body.")
|
||||
|
||||
def test_a_second_main_photo_is_rejected_at_the_database_level(self):
|
||||
make_photo(self.item, is_main=True)
|
||||
|
||||
with self.assertRaises(IntegrityError):
|
||||
make_photo(self.item, is_main=True)
|
||||
|
||||
def test_two_non_main_photos_are_fine(self):
|
||||
make_photo(self.item, is_main=False)
|
||||
make_photo(self.item, is_main=False)
|
||||
|
||||
self.assertEqual(self.item.photos.count(), 2)
|
||||
|
||||
def test_two_different_news_items_can_each_have_a_main_photo(self):
|
||||
other_item = News.objects.create(club=self.club, title="Other item", body="Body.")
|
||||
|
||||
make_photo(self.item, is_main=True)
|
||||
make_photo(other_item, is_main=True)
|
||||
|
||||
self.assertEqual(NewsPhoto.objects.filter(is_main=True).count(), 2)
|
||||
Reference in New Issue
Block a user