import json as _json
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from apps.applets.models import Applet
from apps.drama.models import GameEvent, Note, ScrollPosition, record
from apps.epic.models import Room
from apps.lyric.models import User
def _seed_billboard_applets():
for slug, name, cols, rows in [
("my-scrolls", "My Scrolls", 4, 3),
("my-buds", "My Buds", 4, 3),
("most-recent-scroll", "Most Recent Scroll", 8, 6),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "billboard"},
)
class BillboardViewTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="test@billboard.io")
self.client.force_login(self.user)
_seed_billboard_applets()
def test_uses_billboard_template(self):
response = self.client.get("/billboard/")
self.assertTemplateUsed(response, "apps/billboard/billboard.html")
def test_passes_applets_context(self):
response = self.client.get("/billboard/")
self.assertIn("applets", response.context)
slugs = [e["applet"].slug for e in response.context["applets"]]
self.assertIn("my-scrolls", slugs)
self.assertIn("my-buds", slugs)
self.assertIn("most-recent-scroll", slugs)
def test_passes_my_rooms_context(self):
room = Room.objects.create(name="Test Room", owner=self.user)
response = self.client.get("/billboard/")
self.assertIn(room, response.context["my_rooms"])
def test_passes_recent_room_and_events(self):
room = Room.objects.create(name="Test Room", owner=self.user)
record(
room, GameEvent.SLOT_FILLED, actor=self.user,
slot_number=1, token_type="coin",
token_display="Coin-on-a-String", renewal_days=7,
)
response = self.client.get("/billboard/")
self.assertEqual(response.context["recent_room"], room)
self.assertEqual(len(response.context["recent_events"]), 1)
def test_recent_events_capped_at_36(self):
room = Room.objects.create(name="Test Room", owner=self.user)
for i in range(40):
record(
room, GameEvent.SLOT_FILLED, actor=self.user,
slot_number=1, token_type="coin",
token_display="Coin-on-a-String", renewal_days=7,
)
response = self.client.get("/billboard/")
self.assertEqual(len(response.context["recent_events"]), 36)
def test_recent_events_in_chronological_order(self):
room = Room.objects.create(name="Test Room", owner=self.user)
for _ in range(3):
record(
room, GameEvent.SLOT_FILLED, actor=self.user,
slot_number=1, token_type="coin",
token_display="Coin-on-a-String", renewal_days=7,
)
response = self.client.get("/billboard/")
events = response.context["recent_events"]
timestamps = [e.timestamp for e in events]
self.assertEqual(timestamps, sorted(timestamps))
def test_recent_room_is_none_when_no_events(self):
response = self.client.get("/billboard/")
self.assertIsNone(response.context["recent_room"])
self.assertEqual(list(response.context["recent_events"]), [])
# ── recent_buds + recent_notes (applet feed) ──────────────────────
# Mirrors the recent_posts pattern: each in-grid applet now lists
# its own most-recent items (capped at 3) plus an empty-state row.
def test_passes_recent_buds_context(self):
first = User.objects.create(email="first-bud@test.io")
second = User.objects.create(email="second-bud@test.io")
self.user.buds.add(first)
self.user.buds.add(second)
response = self.client.get("/billboard/")
# Most-recently-added bud is first
self.assertEqual(list(response.context["recent_buds"]), [second, first])
def test_recent_buds_capped_at_3(self):
added = [
User.objects.create(email=f"bud{i}@test.io") for i in range(5)
]
for u in added:
self.user.buds.add(u)
response = self.client.get("/billboard/")
recent = list(response.context["recent_buds"])
self.assertEqual(len(recent), 3)
# Newest three in newest-first order
self.assertEqual(recent, list(reversed(added))[:3])
def test_recent_buds_empty_when_no_buds(self):
response = self.client.get("/billboard/")
self.assertEqual(list(response.context["recent_buds"]), [])
def test_passes_recent_notes_context(self):
Note.objects.create(
user=self.user, slug="stargazer",
earned_at=timezone.now() - timezone.timedelta(days=2),
)
Note.objects.create(
user=self.user, slug="super-schizo",
earned_at=timezone.now(),
)
response = self.client.get("/billboard/")
slugs = [n.slug for n in response.context["recent_notes"]]
# Most-recently-earned first
self.assertEqual(slugs, ["super-schizo", "stargazer"])
def test_recent_notes_capped_at_3(self):
slugs = ["stargazer", "super-schizo", "super-nomad", "ladidah", "doodah"]
base = timezone.now()
for i, slug in enumerate(slugs):
Note.objects.create(
user=self.user, slug=slug,
earned_at=base - timezone.timedelta(hours=i),
)
response = self.client.get("/billboard/")
names = [n.slug for n in response.context["recent_notes"]]
self.assertEqual(len(names), 3)
# Newest-first; oldest two trimmed
self.assertEqual(names, slugs[:3])
def test_recent_notes_excludes_other_users(self):
other = User.objects.create(email="other-note@test.io")
Note.objects.create(user=other, slug="stargazer", earned_at=timezone.now())
response = self.client.get("/billboard/")
self.assertEqual(list(response.context["recent_notes"]), [])
def test_recent_notes_empty_when_no_notes(self):
response = self.client.get("/billboard/")
self.assertEqual(list(response.context["recent_notes"]), [])
# ── 3-column applet row contract ──────────────────────────────────
# Each applet's list item renders `
| | ` —
# title + body wrapped in `.row-title` / `.row-body`, timestamp
# in a `