pronouns: per-user pronouns ideology + Pronouns applet on Game Kit; provenance prose uses actor.pronouns at render time — TDD

- User.pronouns CharField w. choices=[pluralism (default), bawlmorese, misogyny, misandry, misanthropy] + pronoun_subj/obj/poss properties; PRONOUN_TABLE single source of truth in apps.lyric.models; mig 0002_user_pronouns
- drama.GameEvent.to_prose() drops module-level PRONOUN_* constants; SIG_READY/SIG_UNREADY/ROLE_SELECTED now resolve poss/subj from self.actor.pronouns at render time, so flipping a user's preference rewrites all their existing scroll prose; default actor → "their"
- SIG_READY prose strips a leading "The " from card_name so "the The Wanderer" reads "the Wanderer" and "the Engraven The Nomad" reads "the Engraven Nomad"; minor arcana ("Maid of Brands") untouched
- new applets/0005 seeds 'pronouns' applet (3x3, game-kit, default visible); _game_kit_sections.html grows a #id_gk_pronouns block w. 5 .gk-pronoun-card items labeled by ideology slug (italic) and tagged data-pronoun + data-trio
- card click → window.showGuard(card, "Set pronoun preference?<span class='guard-pronoun-trio'>{trio}</span>", commitCb); on OK fetches POST /dashboard/set-pronouns w. CSRF cookie + reloads so .active class moves and provenance prose re-renders; NVM dismisses
- dashboard.set_pronouns view (POST-only, login_required, 204/400/405) at /dashboard/set-pronouns; rejects choices not in PRONOUN_TABLE
- _game-kit.scss extends shared card rule to .gk-pronoun-card w. .active fill state + italic ideology label; #id_guard_portal .guard-pronoun-trio styled small/dim/centered under the question
- billscroll aperture: padding-right 0.75rem on #id_drama_scroll inside .applet-scroll so the timestamp column no longer sits beneath the scrollbar

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-04 01:11:40 -04:00
parent 599d40decd
commit 29493c4f74
15 changed files with 393 additions and 14 deletions

View File

@@ -56,7 +56,8 @@ class GameEventModelTest(TestCase):
role="PC", role_display="Player")
prose = event.to_prose()
self.assertIn("Player", prose)
self.assertIn("yo will start the game", prose)
# Default user pronouns = pluralism → "they".
self.assertIn("they will start the game", prose)
# ── to_prose — SIG_READY ─────────────────────────────────────────────
@@ -65,7 +66,8 @@ class GameEventModelTest(TestCase):
card_name="Maid of Brands", corner_rank="M",
suit_icon="fa-wand-sparkles")
prose = event.to_prose()
self.assertIn("embodies as yos Significator the Maid of Brands", prose)
# Default user pronouns = pluralism → "their".
self.assertIn("embodies as their Significator the Maid of Brands", prose)
self.assertIn("(M", prose)
self.assertIn("fa-wand-sparkles", prose)
@@ -73,15 +75,57 @@ class GameEventModelTest(TestCase):
event = record(self.room, GameEvent.SIG_READY, actor=self.user,
card_name="The Wanderer", corner_rank="0", suit_icon="")
prose = event.to_prose()
self.assertIn("embodies as yos Significator the The Wanderer (0)", prose)
# Trump card: leading "The " is stripped so qualifier (if any) butts up
# against the proper name and "the The Wanderer" never reads doubled.
self.assertIn("embodies as their Significator the Wanderer (0)", prose)
self.assertNotIn("the The Wanderer", prose)
self.assertNotIn("fa-", prose)
def test_sig_ready_prose_strips_leading_the_on_qualified_trump(self):
# Trump card with a levity/gravity qualifier already pre-pended in
# card_name: must read "the Engraven Nomad", not "the Engraven The Nomad".
# Default actor pronouns = pluralism → "their".
event = record(self.room, GameEvent.SIG_READY, actor=self.user,
card_name="Engraven The Nomad", corner_rank="0", suit_icon="")
prose = event.to_prose()
self.assertIn("embodies as their Significator the Engraven Nomad (0)", prose)
self.assertNotIn("Engraven The Nomad", prose)
def test_sig_ready_prose_uses_actor_pronouns_at_render_time(self):
# Bawlmorese actor → "yos"; default actor → "their"; switching
# the actor's pronouns updates ALL their existing prose on next render.
self.user.pronouns = "bawlmorese"
self.user.save(update_fields=["pronouns"])
event = record(self.room, GameEvent.SIG_READY, actor=self.user,
card_name="Maid of Brands", corner_rank="M",
suit_icon="fa-wand-sparkles")
self.assertIn("embodies as yos Significator the Maid of Brands", event.to_prose())
self.user.pronouns = "misogyny"
self.user.save(update_fields=["pronouns"])
# Re-fetch — the related object cache may hold the stale pronouns.
event.refresh_from_db()
self.assertIn("embodies as his Significator the Maid of Brands", event.to_prose())
def test_sig_unready_prose_uses_actor_pronouns(self):
self.user.pronouns = "misandry"
self.user.save(update_fields=["pronouns"])
event = record(self.room, GameEvent.SIG_UNREADY, actor=self.user)
self.assertIn("disembodies hers Significator", event.to_prose())
def test_role_selected_prose_uses_actor_pronouns(self):
self.user.pronouns = "misanthropy"
self.user.save(update_fields=["pronouns"])
event = record(self.room, GameEvent.ROLE_SELECTED, actor=self.user,
role="PC", role_display="Player")
self.assertIn("it will start the game", event.to_prose())
def test_sig_ready_prose_degrades_without_corner_rank(self):
# Old events recorded before this change have no corner_rank key
event = record(self.room, GameEvent.SIG_READY, actor=self.user,
card_name="Maid of Brands")
prose = event.to_prose()
self.assertIn("embodies as yos Significator the Maid of Brands", prose)
# Default user pronouns = pluralism → "their".
self.assertIn("embodies as their Significator the Maid of Brands", prose)
self.assertNotIn("(", prose)
def test_str_without_actor_shows_system(self):