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:
@@ -420,3 +420,70 @@ var GameKit = (function () {
|
||||
_testNavigate: navigate,
|
||||
};
|
||||
}());
|
||||
|
||||
// ── Pronouns applet — guard-portal-confirmed preference flip ─────────────
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function csrfFromCookie() {
|
||||
var name = 'csrftoken';
|
||||
if (!document.cookie) return '';
|
||||
var found = '';
|
||||
document.cookie.split(';').forEach(function (c) {
|
||||
c = c.trim();
|
||||
if (c.indexOf(name + '=') === 0) {
|
||||
found = decodeURIComponent(c.substring(name.length + 1));
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
function commit(card) {
|
||||
var key = card.getAttribute('data-pronoun');
|
||||
var body = new URLSearchParams({ pronouns: key });
|
||||
fetch('/dashboard/set-pronouns', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRFToken': csrfFromCookie(),
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: body.toString(),
|
||||
}).then(function (resp) {
|
||||
if (resp.status === 204) {
|
||||
// Reload so any provenance prose currently on the page renders
|
||||
// with the new pronouns and the .active class moves to the new card.
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
function bindCards() {
|
||||
var cards = document.querySelectorAll('.gk-pronoun-card');
|
||||
if (!cards.length) return;
|
||||
cards.forEach(function (card) {
|
||||
card.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
if (typeof window.showGuard !== 'function') return;
|
||||
var trio = card.getAttribute('data-trio') || '';
|
||||
var msg = 'Set pronoun preference?'
|
||||
+ '<span class="guard-pronoun-trio">' + escapeHtml(trio) + '</span>';
|
||||
window.showGuard(card, msg, function () {
|
||||
commit(card);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bindCards);
|
||||
} else {
|
||||
bindCards();
|
||||
}
|
||||
}());
|
||||
|
||||
@@ -212,6 +212,7 @@ class GameKitViewTest(TestCase):
|
||||
Applet.objects.get_or_create(slug="gk-tokens", defaults={"name": "Tokens", "context": "game-kit"})
|
||||
Applet.objects.get_or_create(slug="gk-decks", defaults={"name": "Card Decks", "context": "game-kit"})
|
||||
Applet.objects.get_or_create(slug="gk-dice", defaults={"name": "Dice Sets", "context": "game-kit"})
|
||||
Applet.objects.get_or_create(slug="pronouns", defaults={"name": "Pronouns", "context": "game-kit"})
|
||||
response = self.client.get("/gameboard/game-kit/")
|
||||
self.parsed = lxml.html.fromstring(response.content)
|
||||
|
||||
@@ -247,7 +248,27 @@ class GameKitViewTest(TestCase):
|
||||
|
||||
def test_all_sections_visible_by_default(self):
|
||||
sections = self.parsed.cssselect("#id_gk_sections_container section")
|
||||
self.assertEqual(len(sections), 4)
|
||||
# Trinkets, Tokens, Card Decks, Dice Sets, Pronouns
|
||||
self.assertEqual(len(sections), 5)
|
||||
|
||||
def test_pronouns_section_renders_five_cards(self):
|
||||
[section] = self.parsed.cssselect("#id_gk_pronouns")
|
||||
cards = section.cssselect(".gk-pronoun-card")
|
||||
self.assertEqual(len(cards), 5)
|
||||
slugs = [c.get("data-pronoun") for c in cards]
|
||||
self.assertEqual(
|
||||
slugs,
|
||||
["pluralism", "bawlmorese", "misogyny", "misandry", "misanthropy"],
|
||||
)
|
||||
|
||||
def test_pronouns_section_marks_current_choice_active(self):
|
||||
# Default user pronouns = "pluralism" — that card should carry .active.
|
||||
[active] = self.parsed.cssselect("#id_gk_pronouns .gk-pronoun-card.active")
|
||||
self.assertEqual(active.get("data-pronoun"), "pluralism")
|
||||
|
||||
def test_game_kit_applet_menu_has_pronouns_checkbox(self):
|
||||
[inp] = self.parsed.cssselect("#id_game_kit_menu input[value='pronouns']")
|
||||
self.assertEqual(inp.get("type"), "checkbox")
|
||||
|
||||
|
||||
class ToggleGameKitSectionsViewTest(TestCase):
|
||||
|
||||
@@ -123,6 +123,7 @@ def unequip_deck(request, deck_id):
|
||||
|
||||
|
||||
def _game_kit_context(user):
|
||||
from apps.lyric.models import PRONOUN_CHOICES
|
||||
coin = user.tokens.filter(token_type=Token.COIN).first()
|
||||
pass_token = user.tokens.filter(token_type=Token.PASS).first() if user.is_staff else None
|
||||
carte = user.tokens.filter(token_type=Token.CARTE).first()
|
||||
@@ -130,6 +131,10 @@ def _game_kit_context(user):
|
||||
token_type=Token.FREE, expires_at__gt=timezone.now()
|
||||
).order_by("expires_at"))
|
||||
tithe_tokens = list(user.tokens.filter(token_type=Token.TITHE))
|
||||
pronoun_options = [
|
||||
{"key": k, "label": label, "active": (k == user.pronouns)}
|
||||
for (k, label) in PRONOUN_CHOICES
|
||||
]
|
||||
return {
|
||||
"coin": coin,
|
||||
"pass_token": pass_token,
|
||||
@@ -138,6 +143,8 @@ def _game_kit_context(user):
|
||||
"tithe_tokens": tithe_tokens,
|
||||
"unlocked_decks": list(user.unlocked_decks.all()),
|
||||
"applets": applet_context(user, "game-kit"),
|
||||
"pronoun_options": pronoun_options,
|
||||
"current_pronouns": user.pronouns,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user