applet rows: 3-col grid <title> | <body> | <ts> mirroring post.html's .post-line shape — _my_posts_applet_item / _my_buds_applet_item / _my_notes_item / _my_scrolls_item / _my_games_item all gain a .applet-list-entry.row-3col w. <a class="row-title"> (clickable, 35c/32+... server-side truncated via new lyric_extras.truncate_title filter) + <span class="row-body"> (most-recent activity excerpt, dimmed 0.6 opacity, CSS-text-overflow: ellipsis clipped to whatever space remains — no server-side trunc here so the full line lives in the DOM for inspectors) + <time class="row-ts"> (relative_ts formatted, same minmax(3rem,auto) rightward column allocation post.html's .post-line-time uses, font-size 0.75rem + opacity 0.5 + right-aligned + nowrap); SCSS grid minmax(4rem,auto) 1fr minmax(3rem,auto) lifted from .post-line's template so the timestamp column lines up across post.html / scroll.html / every applet list; per-applet data shapes — _recent_posts annotates each Post w. latest_line (Line FK ordered by -id, None for empty Note-unlock posts); _recent_buds select_related('to_user__active_title') warms the bud's donned-Note FK in one query for the buds row body ("the {{ bud.active_title_display }}" + "since {{ bud.active_title.earned_at|relative_ts }}" — the "since " prefix is unique to this row since the ts is "when they donned it", not the row's own creation); _recent_notes attaches description from _NOTE_META per slug; annotate_latest_event(rooms) helper added to apps.epic.utils (next to rooms_for_user) — attaches room.latest_event per Room w. one .events.order_by('-timestamp').first() per item, used by _billboard_context for my_rooms (My Scrolls applet) AND by apps.gameboard.views.gameboard + toggle_game_applets for my_games (My Games applet), keeping the My Scrolls + My Games shapes symmetric; _billboard_context.my_rooms = annotate_latest_event(...) swaps rooms_for_user(...).order_by("-created_at") materialisation point — bud row's "no active title" branch silently drops body + ts cells so unrecognised buds still surface but don't fabricate a "since None" line; new truncate_title filter is the existing _truncate_post_title view helper hoisted into the template namespace (literal ... past 35 chars, None-safe); 5 ITs in BillboardViewTest cover row content / row absence on missing activity / "since" prefix uniquely on the buds row + 1 in GameboardViewTest for My Games row event prose; deferred row-prose body content cap on <span class="row-body"> purely to CSS text-overflow: ellipsis per user's "middle col should take up the remaining space" steer (initial pass also server-side trunc'd the body to 35c; removed) — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,84 @@ class BillboardViewTest(TestCase):
|
||||
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> | <ts>` —
|
||||
# title + body wrapped in `.row-title` / `.row-body`, timestamp
|
||||
# in a `<time class="row-ts">` element. The body shows the
|
||||
# most-recent activity (post line / room event / note description /
|
||||
# bud's active title). Right column carries `relative_ts` text.
|
||||
|
||||
def test_my_posts_applet_row_shows_latest_line_and_ts(self):
|
||||
from apps.billboard.models import Line, Post
|
||||
post = Post.objects.create(
|
||||
owner=self.user, kind=Post.KIND_USER_POST, title="StampedPost",
|
||||
)
|
||||
Line.objects.create(post=post, text="first line text", author=self.user)
|
||||
Line.objects.create(post=post, text="latest line text", author=self.user)
|
||||
response = self.client.get("/billboard/")
|
||||
body = response.content.decode()
|
||||
# Title preserved
|
||||
self.assertIn("StampedPost", body)
|
||||
# Latest line surfaces in the row body, not the older line
|
||||
self.assertIn("latest line text", body)
|
||||
# A <time> ts element renders on the row
|
||||
self.assertRegex(body, r'<time[^>]+class="[^"]*row-ts')
|
||||
|
||||
def test_my_buds_applet_row_shows_active_title_and_since_ts(self):
|
||||
bud = User.objects.create(email="alice@test.io", username="alice")
|
||||
note = Note.objects.create(
|
||||
user=bud, slug="stargazer", earned_at=timezone.now(),
|
||||
)
|
||||
bud.active_title = note
|
||||
bud.save()
|
||||
self.user.buds.add(bud)
|
||||
response = self.client.get("/billboard/")
|
||||
body = response.content.decode()
|
||||
# Bud handle shows; the active title surfaces as the row body;
|
||||
# the ts column carries a "since " prefix unique to buds
|
||||
self.assertIn("alice", body)
|
||||
self.assertIn("the Stargazer", body)
|
||||
self.assertRegex(body, r'class="[^"]*row-ts[^"]*"[^>]*>\s*since\s+')
|
||||
|
||||
def test_my_buds_applet_row_no_active_title_no_body_or_ts(self):
|
||||
bud = User.objects.create(email="alice@test.io", username="alice")
|
||||
# No active_title donned
|
||||
self.user.buds.add(bud)
|
||||
response = self.client.get("/billboard/")
|
||||
body = response.content.decode()
|
||||
self.assertIn("alice", body)
|
||||
# Without an active title we don't render a "since" line
|
||||
self.assertNotRegex(body, r'class="[^"]*row-ts[^"]*"[^>]*>\s*since\s+')
|
||||
|
||||
def test_my_notes_applet_row_shows_description_and_earned_at(self):
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
)
|
||||
response = self.client.get("/billboard/")
|
||||
body = response.content.decode()
|
||||
self.assertIn("Stargazer", body)
|
||||
# Description prefix from _NOTE_META — full string is 40 chars and
|
||||
# truncates to 32+"..." via the `truncate_title` filter, so assert
|
||||
# against the head only ("You saved your first personal" is 30 chars,
|
||||
# comfortably inside the truncation window).
|
||||
self.assertIn("You saved your first personal", body)
|
||||
# ts column present
|
||||
self.assertRegex(body, r'<time[^>]+class="[^"]*row-ts')
|
||||
|
||||
def test_my_scrolls_applet_row_shows_latest_event_and_ts(self):
|
||||
room = Room.objects.create(name="StampedRoom", 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/")
|
||||
body = response.content.decode()
|
||||
self.assertIn("StampedRoom", body)
|
||||
# The slot-fill prose mentions the token; we just check the room's
|
||||
# scroll row carries a <time> + some body content (event prose)
|
||||
self.assertRegex(body, r'<time[^>]+class="[^"]*row-ts')
|
||||
|
||||
|
||||
class SaveScrollPositionViewTest(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user