From b2f1511c2db770cc38360f126727731caac7d0ce Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Wed, 13 May 2026 00:13:33 -0400 Subject: [PATCH] =?UTF-8?q?my-scrolls=20/=20my-games=20applet=20rows:=20pr?= =?UTF-8?q?epend=20actor=20`display=5Fname`=20to=20the=20body=20cell=20?= =?UTF-8?q?=E2=80=94=20the=20latest=20event's=20`to=5Fprose`=20returns=20t?= =?UTF-8?q?he=20action=20alone=20("deposits=20a=20Carte=20Blanche=E2=80=A6?= =?UTF-8?q?")=20because=20scroll.html=20splits=20the=20row=20across=20`{{=20event.actor|display=5Fname=20}}`=20+=20adjac?= =?UTF-8?q?ent=20`{{=20to=5Fprose|safe=20}}`;=20the=20applet=20rows=20have?= =?UTF-8?q?=20a=20single=20middle=20column=20(`=20|=20<body>=20|=20?= =?UTF-8?q?<ts>`)=20so=20they=20need=20both=20halves=20concatenated=20into?= =?UTF-8?q?=20`.row-body`;=20ROOM=5FCREATED=20welcome=20events=20(actor=3D?= =?UTF-8?q?None)=20keep=20rendering=20prose=20alone=20since=20`to=5Fprose`?= =?UTF-8?q?=20already=20reads=20"Welcome=20to=20<name>!"=20=E2=80=94=20the?= =?UTF-8?q?=20`{%=20if=20item.latest=5Fevent.actor=20%}`=20guard=20skips?= =?UTF-8?q?=20the=20prefix,=20mirroring=20the=20same=20actor-guarded=20`<s?= =?UTF-8?q?trong>`=20we=20added=20to=20`=5Fpartials/=5Fscroll.html`=20+=20?= =?UTF-8?q?`=5Fapplet-most-recent-scroll.html`=20on=20c03fb2b=20so=20welco?= =?UTF-8?q?me=20lines=20don't=20carry=20a=20bogus=20empty=20actor;=202=20I?= =?UTF-8?q?Ts=20added=20=E2=80=94=20BillboardViewTest.test=5Fmy=5Fscrolls?= =?UTF-8?q?=5Fapplet=5Frow=5Fbody=5Fincludes=5Factor=5Fdisplay=5Fname=20+?= =?UTF-8?q?=20GameboardViewTest.test=5Fmy=5Fgames=5Frow=5Fbody=5Fincludes?= =?UTF-8?q?=5Factor=5Fdisplay=5Fname=20=E2=80=94=20scoped=20to=20`<span=20?= =?UTF-8?q?class=3D"row-body">...stuart...deposits...</span>`=20(regex=20m?= =?UTF-8?q?atch=20on=20the=20.row-body=20cell=20content)=20so=20the=20asse?= =?UTF-8?q?rtion=20can't=20pass=20on=20actor=20renders=20outside=20the=20r?= =?UTF-8?q?ow=20(the=20Most=20Recent=20Scroll=20applet=20on=20/billboard/?= =?UTF-8?q?=20renders=20the=20same=20actor=20too,=20separately=20=E2=80=94?= =?UTF-8?q?=20initial=20pass=20missed=20this=20and=20`assertIn("acto",=20b?= =?UTF-8?q?ody)`=20matched=20there=20instead,=20hiding=20the=20bug);=20Bil?= =?UTF-8?q?lboardViewTest=20also=20gains=20test=5Fmy=5Fscrolls=5Fapplet=5F?= =?UTF-8?q?row=5Fbody=5Fno=5Factor=5Fprefix=5Ffor=5Fwelcome=20to=20lock=20?= =?UTF-8?q?in=20the=20no-empty-prefix=20contract=20for=20ROOM=5FCREATED=20?= =?UTF-8?q?welcome=20events;=20931=20ITs=20green;=20settings.local.json=20?= =?UTF-8?q?fix-up=20=E2=80=94=20`Bash(git=20add=20*)`=20(literal=20`*`=20w?= =?UTF-8?q?ould=20only=20match=20the=20exact=20string=20"git=20add=20*",?= =?UTF-8?q?=20not=20`git=20add=20-u`)=20=E2=86=92=20`Bash(git=20add:*)`=20?= =?UTF-8?q?+=20companion=20read-only=20git=20patterns=20(status=20/=20diff?= =?UTF-8?q?=20/=20log=20/=20show)=20so=20the=20in-session=20commit=20flow?= =?UTF-8?q?=20stops=20prompting=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --- .../billboard/tests/integrated/test_views.py | 37 +++++++++++++++++++ .../gameboard/tests/integrated/test_views.py | 22 +++++++++++ .../billboard/_partials/_my_scrolls_item.html | 6 ++- .../gameboard/_partials/_my_games_item.html | 5 ++- 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/apps/billboard/tests/integrated/test_views.py b/src/apps/billboard/tests/integrated/test_views.py index 89a56c9..67d86fc 100644 --- a/src/apps/billboard/tests/integrated/test_views.py +++ b/src/apps/billboard/tests/integrated/test_views.py @@ -230,6 +230,43 @@ class BillboardViewTest(TestCase): # scroll row carries a <time> + some body content (event prose) self.assertRegex(body, r'<time[^>]+class="[^"]*row-ts') + def test_my_scrolls_applet_row_body_includes_actor_display_name(self): + """Latest event prose w.o. the actor name is meaningless — `deposits + a Carte Blanche` should read `<actor> deposits a Carte Blanche`. + scroll.html renders actor via a separate `<strong>` adjacent to + prose; the applet row has a single body cell so we concatenate. + Scoped to the `.row-body` span (vs. a loose substring match) so + we don't pass on the Most Recent Scroll applet's actor render + — which renders the same actor too, separately.""" + actor = User.objects.create(email="stuart@test.io", username="stuart") + room = Room.objects.create(name="ScrollRoom", owner=self.user) + record( + room, GameEvent.SLOT_FILLED, actor=actor, + slot_number=1, token_type="coin", + token_display="Coin-on-a-String", renewal_days=7, + ) + response = self.client.get("/billboard/") + body = response.content.decode() + # The `.row-body` cell inside the My Scrolls applet must carry + # both the actor handle AND the event prose, in that order. + self.assertRegex( + body, + r'<span class="row-body">[^<]*stuart[^<]*deposits a Coin-on-a-String', + ) + + def test_my_scrolls_applet_row_body_no_actor_prefix_for_welcome(self): + """Welcome events (actor=None) must not render an empty `<strong></strong>` + prefix before the prose — same shape the scroll.html template adopted.""" + room = Room.objects.create(name="GreenroomTwo", owner=self.user) + record(room, GameEvent.ROOM_CREATED) + response = self.client.get("/billboard/") + body = response.content.decode() + self.assertIn("Welcome to GreenroomTwo!", body) + # No empty <strong> before the welcome line + self.assertNotRegex( + body, r'<strong>\s*</strong>\s*Welcome to GreenroomTwo!' + ) + class SaveScrollPositionViewTest(TestCase): def setUp(self): diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index 12fef24..253a096 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -64,6 +64,28 @@ class GameboardViewTest(TestCase): # A .row-body cell carries some event prose self.assertRegex(body, r'<time[^>]+class="[^"]*row-ts') + def test_my_games_row_body_includes_actor_display_name(self): + """Mirror of My Scrolls — the My Games row body must include the + event actor's display_name so `<actor> deposits a Coin-on-a-String` + reads as a complete sentence. Scoped to the `.row-body` span (vs. + a loose substring) so the assertion can't pass on actor renders + outside the row (no Most Recent Scroll on gameboard, but the + navbar etc. could shadow a substring match).""" + from apps.drama.models import GameEvent, record + actor = User.objects.create(email="stuart@test.io", username="stuart") + room = Room.objects.create(name="GameRoom", owner=self.user) + record( + room, GameEvent.SLOT_FILLED, actor=actor, + slot_number=2, token_type="coin", + token_display="Coin-on-a-String", renewal_days=7, + ) + response = self.client.get("/gameboard/") + body = response.content.decode() + self.assertRegex( + body, + r'<span class="row-body">[^<]*stuart[^<]*deposits a Coin-on-a-String', + ) + def test_game_kit_has_coin_on_a_string(self): [_] = self.parsed.cssselect("#id_game_kit #id_kit_coin_on_a_string") diff --git a/src/templates/apps/billboard/_partials/_my_scrolls_item.html b/src/templates/apps/billboard/_partials/_my_scrolls_item.html index 538e324..5ed5ae5 100644 --- a/src/templates/apps/billboard/_partials/_my_scrolls_item.html +++ b/src/templates/apps/billboard/_partials/_my_scrolls_item.html @@ -1,10 +1,14 @@ {% load lyric_extras %} {# My Scrolls applet row — room name | latest event prose | event ts. #} {# `item` is a Room w. `latest_event` attached by `annotate_latest_event`. #} +{# Body cell concatenates actor + prose since the row has a single #} +{# middle column (scroll.html splits them across <strong> + prose, but #} +{# the applet row has no room for that). Welcome events (actor=None) #} +{# render prose alone — `to_prose` already reads "Welcome to <name>!". #} <li class="applet-list-entry row-3col"> <a href="{% url 'billboard:scroll' item.id %}" class="row-title">{{ item.name|truncate_title }}</a> {% if item.latest_event %} - <span class="row-body">{{ item.latest_event.to_prose|striptags }}</span> + <span class="row-body">{% if item.latest_event.actor %}{{ item.latest_event.actor|display_name }} {% endif %}{{ item.latest_event.to_prose|striptags }}</span> <time class="row-ts" datetime="{{ item.latest_event.timestamp|date:'c' }}">{{ item.latest_event.timestamp|relative_ts }}</time> {% endif %} </li> diff --git a/src/templates/apps/gameboard/_partials/_my_games_item.html b/src/templates/apps/gameboard/_partials/_my_games_item.html index 4838fff..62d2f7d 100644 --- a/src/templates/apps/gameboard/_partials/_my_games_item.html +++ b/src/templates/apps/gameboard/_partials/_my_games_item.html @@ -1,10 +1,13 @@ {% load lyric_extras %} {# My Games applet row — room name | latest event prose | event ts. #} {# `item` is a Room w. `latest_event` attached by `annotate_latest_event`. #} +{# Body cell concatenates actor + prose since the row has a single #} +{# middle column. Welcome events (actor=None) render prose alone — #} +{# `to_prose` already reads "Welcome to <name>!". #} <li class="applet-list-entry row-3col"> <a href="{% url 'epic:gatekeeper' item.id %}" class="row-title">{{ item.name|truncate_title }}</a> {% if item.latest_event %} - <span class="row-body">{{ item.latest_event.to_prose|striptags }}</span> + <span class="row-body">{% if item.latest_event.actor %}{{ item.latest_event.actor|display_name }} {% endif %}{{ item.latest_event.to_prose|striptags }}</span> <time class="row-ts" datetime="{{ item.latest_event.timestamp|date:'c' }}">{{ item.latest_event.timestamp|relative_ts }}</time> {% endif %} </li>