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:
33
src/apps/applets/migrations/0005_seed_pronouns_applet.py
Normal file
33
src/apps/applets/migrations/0005_seed_pronouns_applet.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Seed the Pronouns applet on the Game Kit page (3x3, default visible)."""
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
SLUG = "pronouns"
|
||||
NAME = "Pronouns"
|
||||
CONTEXT = "game-kit"
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
Applet = apps.get_model("applets", "Applet")
|
||||
Applet.objects.get_or_create(
|
||||
slug=SLUG,
|
||||
defaults={
|
||||
"name": NAME,
|
||||
"context": CONTEXT,
|
||||
"default_visible": True,
|
||||
"grid_cols": 3,
|
||||
"grid_rows": 3,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def backward(apps, schema_editor):
|
||||
Applet = apps.get_model("applets", "Applet")
|
||||
Applet.objects.filter(slug=SLUG).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("applets", "0004_rename_billboard_applet_slugs"),
|
||||
]
|
||||
operations = [migrations.RunPython(forward, backward)]
|
||||
@@ -651,3 +651,39 @@ class SkyNatusDataViewTest(TestCase):
|
||||
self.client.logout()
|
||||
response = self.client.get("/dashboard/sky/data")
|
||||
self.assertRedirects(response, "/?next=/dashboard/sky/data", fetch_redirect_response=False)
|
||||
|
||||
|
||||
class SetPronounsViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="they@test.io")
|
||||
self.client.force_login(self.user)
|
||||
self.url = reverse("set_pronouns")
|
||||
|
||||
def test_post_valid_choice_persists_on_user(self):
|
||||
response = self.client.post(self.url, data={"pronouns": "misogyny"})
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.pronouns, "misogyny")
|
||||
|
||||
def test_post_each_valid_choice(self):
|
||||
for key in ("pluralism", "bawlmorese", "misogyny", "misandry", "misanthropy"):
|
||||
with self.subTest(key=key):
|
||||
self.client.post(self.url, data={"pronouns": key})
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.pronouns, key)
|
||||
|
||||
def test_post_invalid_choice_returns_400_and_does_not_change_user(self):
|
||||
original = self.user.pronouns
|
||||
response = self.client.post(self.url, data={"pronouns": "bogus"})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.pronouns, original)
|
||||
|
||||
def test_get_returns_405(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
def test_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(self.url, data={"pronouns": "misogyny"})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
@@ -18,4 +18,5 @@ urlpatterns = [
|
||||
path('sky/preview', views.sky_preview, name='sky_preview'),
|
||||
path('sky/save', views.sky_save, name='sky_save'),
|
||||
path('sky/data', views.sky_natus_data, name='sky_natus_data'),
|
||||
path('set-pronouns', views.set_pronouns, name='set_pronouns'),
|
||||
]
|
||||
|
||||
@@ -179,6 +179,20 @@ def set_profile(request):
|
||||
request.user.save(update_fields=["username"])
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
def set_pronouns(request):
|
||||
from django.http import HttpResponseNotAllowed
|
||||
from apps.lyric.models import PRONOUN_TABLE
|
||||
if request.method != "POST":
|
||||
return HttpResponseNotAllowed(["POST"])
|
||||
choice = request.POST.get("pronouns", "")
|
||||
if choice not in PRONOUN_TABLE:
|
||||
return HttpResponse(status=400)
|
||||
request.user.pronouns = choice
|
||||
request.user.save(update_fields=["pronouns"])
|
||||
return HttpResponse(status=204)
|
||||
|
||||
@login_required(login_url="/")
|
||||
def toggle_applets(request):
|
||||
checked = request.POST.getlist("applets")
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
# ── Default gender-neutral pronouns (Baltimore original) ──────────────────────
|
||||
# Later: replace with per-actor lookup when User model gains a pronouns field.
|
||||
PRONOUN_SUBJ = "yo"
|
||||
PRONOUN_OBJ = "yo"
|
||||
PRONOUN_POSS = "yos"
|
||||
from apps.lyric.models import resolve_pronouns
|
||||
|
||||
|
||||
def _actor_pronouns(actor):
|
||||
"""Return (subj, obj, poss) for an event actor; default = pluralism when None."""
|
||||
return resolve_pronouns(getattr(actor, "pronouns", None) if actor else None)
|
||||
|
||||
|
||||
class GameEvent(models.Model):
|
||||
@@ -90,7 +91,8 @@ class GameEvent(models.Model):
|
||||
ordinal = _ordinals[_chair_order.index(code)]
|
||||
except ValueError:
|
||||
ordinal = "?"
|
||||
return f"assumes {ordinal} Chair; yo will start the game as the {role}."
|
||||
subj, _, _ = _actor_pronouns(self.actor)
|
||||
return f"assumes {ordinal} Chair; {subj} will start the game as the {role}."
|
||||
if self.verb == self.ROLES_REVEALED:
|
||||
return "All roles assigned"
|
||||
if self.verb == self.SIG_READY:
|
||||
@@ -102,9 +104,16 @@ class GameEvent(models.Model):
|
||||
abbrev = f" ({corner_rank}{icon_html})"
|
||||
else:
|
||||
abbrev = ""
|
||||
return f"embodies as {PRONOUN_POSS} Significator the {card_name}{abbrev}."
|
||||
# Trump cards ("The Schizo", "The Nomad", "The Wanderer") drop
|
||||
# their "The " in this rendering: the prose template already
|
||||
# supplies "the", and a levity/gravity qualifier (e.g. "Engraven"
|
||||
# in "Engraven The Nomad") needs to butt up against the proper name.
|
||||
card_name = card_name.replace("The ", "", 1)
|
||||
_, _, poss = _actor_pronouns(self.actor)
|
||||
return f"embodies as {poss} Significator the {card_name}{abbrev}."
|
||||
if self.verb == self.SIG_UNREADY:
|
||||
return f"disembodies {PRONOUN_POSS} Significator."
|
||||
_, _, poss = _actor_pronouns(self.actor)
|
||||
return f"disembodies {poss} Significator."
|
||||
return self.verb
|
||||
|
||||
@property
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
18
src/apps/lyric/migrations/0002_user_pronouns.py
Normal file
18
src/apps/lyric/migrations/0002_user_pronouns.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0 on 2026-05-04 04:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('lyric', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='pronouns',
|
||||
field=models.CharField(choices=[('pluralism', 'they/them/their'), ('bawlmorese', 'yo/yo/yos'), ('misogyny', 'he/him/his'), ('misandry', 'she/her/hers'), ('misanthropy', 'it/it/its')], default='pluralism', max_length=16),
|
||||
),
|
||||
]
|
||||
@@ -9,6 +9,34 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
# ── Pronoun preference set ────────────────────────────────────────────────
|
||||
# Drives provenance prose ("embodies as their Significator …") and any other
|
||||
# user-facing referent. Default is pluralism (singular they) so a brand-new
|
||||
# account renders neutrally; bawlmorese (yo/yo/yos) is the original Earthman
|
||||
# default kept available as a Baltimore-flavoured option.
|
||||
|
||||
PRONOUN_CHOICES = [
|
||||
("pluralism", "they/them/their"),
|
||||
("bawlmorese", "yo/yo/yos"),
|
||||
("misogyny", "he/him/his"),
|
||||
("misandry", "she/her/hers"),
|
||||
("misanthropy", "it/it/its"),
|
||||
]
|
||||
PRONOUN_TABLE = {
|
||||
"pluralism": {"subj": "they", "obj": "them", "poss": "their"},
|
||||
"bawlmorese": {"subj": "yo", "obj": "yo", "poss": "yos"},
|
||||
"misogyny": {"subj": "he", "obj": "him", "poss": "his"},
|
||||
"misandry": {"subj": "she", "obj": "her", "poss": "hers"},
|
||||
"misanthropy": {"subj": "it", "obj": "it", "poss": "its"},
|
||||
}
|
||||
|
||||
|
||||
def resolve_pronouns(pronouns_key):
|
||||
"""Return (subj, obj, poss) for a pronouns key, defaulting to pluralism."""
|
||||
row = PRONOUN_TABLE.get(pronouns_key) or PRONOUN_TABLE["pluralism"]
|
||||
return row["subj"], row["obj"], row["poss"]
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
def create_user(self, email):
|
||||
user = self.model(email=email)
|
||||
@@ -63,10 +91,26 @@ class User(AbstractBaseUser):
|
||||
is_staff = models.BooleanField(default=False)
|
||||
is_superuser = models.BooleanField(default=False)
|
||||
|
||||
pronouns = models.CharField(
|
||||
max_length=16, choices=PRONOUN_CHOICES, default="pluralism",
|
||||
)
|
||||
|
||||
objects = UserManager()
|
||||
REQUIRED_FIELDS = []
|
||||
USERNAME_FIELD = "email"
|
||||
|
||||
@property
|
||||
def pronoun_subj(self):
|
||||
return resolve_pronouns(self.pronouns)[0]
|
||||
|
||||
@property
|
||||
def pronoun_obj(self):
|
||||
return resolve_pronouns(self.pronouns)[1]
|
||||
|
||||
@property
|
||||
def pronoun_poss(self):
|
||||
return resolve_pronouns(self.pronouns)[2]
|
||||
|
||||
def ensure_keypair(self):
|
||||
"""Generate and persist an RSA-2048 keypair if not already set."""
|
||||
if self.ap_public_key:
|
||||
|
||||
@@ -79,6 +79,50 @@ class UserPaletteTest(TestCase):
|
||||
user = User.objects.create(email="a@b.cde")
|
||||
self.assertEqual(user.palette, "palette-default")
|
||||
|
||||
|
||||
class UserPronounsTest(TestCase):
|
||||
def test_pronouns_default_to_pluralism(self):
|
||||
user = User.objects.create(email="they@test.io")
|
||||
self.assertEqual(user.pronouns, "pluralism")
|
||||
|
||||
def test_pronouns_choices_include_all_five_options(self):
|
||||
from apps.lyric.models import PRONOUN_CHOICES
|
||||
keys = [k for (k, _) in PRONOUN_CHOICES]
|
||||
self.assertEqual(
|
||||
keys,
|
||||
["pluralism", "bawlmorese", "misogyny", "misandry", "misanthropy"],
|
||||
)
|
||||
|
||||
def test_pluralism_resolves_to_they_them_their(self):
|
||||
user = User.objects.create(email="plural@test.io")
|
||||
self.assertEqual(user.pronoun_subj, "they")
|
||||
self.assertEqual(user.pronoun_obj, "them")
|
||||
self.assertEqual(user.pronoun_poss, "their")
|
||||
|
||||
def test_bawlmorese_resolves_to_yo_yo_yos(self):
|
||||
user = User.objects.create(email="yo@test.io", pronouns="bawlmorese")
|
||||
self.assertEqual(user.pronoun_subj, "yo")
|
||||
self.assertEqual(user.pronoun_obj, "yo")
|
||||
self.assertEqual(user.pronoun_poss, "yos")
|
||||
|
||||
def test_misogyny_resolves_to_he_him_his(self):
|
||||
user = User.objects.create(email="he@test.io", pronouns="misogyny")
|
||||
self.assertEqual(user.pronoun_subj, "he")
|
||||
self.assertEqual(user.pronoun_obj, "him")
|
||||
self.assertEqual(user.pronoun_poss, "his")
|
||||
|
||||
def test_misandry_resolves_to_she_her_hers(self):
|
||||
user = User.objects.create(email="she@test.io", pronouns="misandry")
|
||||
self.assertEqual(user.pronoun_subj, "she")
|
||||
self.assertEqual(user.pronoun_obj, "her")
|
||||
self.assertEqual(user.pronoun_poss, "hers")
|
||||
|
||||
def test_misanthropy_resolves_to_it_it_its(self):
|
||||
user = User.objects.create(email="it@test.io", pronouns="misanthropy")
|
||||
self.assertEqual(user.pronoun_subj, "it")
|
||||
self.assertEqual(user.pronoun_obj, "it")
|
||||
self.assertEqual(user.pronoun_poss, "its")
|
||||
|
||||
class WalletCreationTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="capman@test.io")
|
||||
|
||||
@@ -82,6 +82,7 @@ body.page-billscroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.75rem;
|
||||
|
||||
.scroll-buffer {
|
||||
display: flex;
|
||||
|
||||
@@ -188,7 +188,8 @@
|
||||
|
||||
.gk-deck-card,
|
||||
.gk-trinket-card,
|
||||
.gk-token-card {
|
||||
.gk-token-card,
|
||||
.gk-pronoun-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -208,6 +209,32 @@
|
||||
&:hover { border-color: rgba(var(--secUser), 0.8); }
|
||||
}
|
||||
|
||||
.gk-pronoun-card {
|
||||
// Card shows the ideology slug (pluralism, bawlmorese, …) in italic;
|
||||
// the guard portal previews the actual slash trio above OK|NVM via JS.
|
||||
// Active card is filled with the secondary tint so the user can see at a
|
||||
// glance which preference is currently in effect.
|
||||
.gk-pronoun-label {
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: lowercase;
|
||||
font-style: italic;
|
||||
}
|
||||
&.active {
|
||||
border-color: rgba(var(--secUser), 1);
|
||||
background: rgba(var(--secUser), 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
#id_guard_portal .guard-pronoun-trio {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.75;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gk-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -77,5 +77,18 @@
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.applet.slug == 'pronouns' and entry.visible %}
|
||||
<section id="id_gk_pronouns" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
|
||||
<h2>Pronouns</h2>
|
||||
<div class="gk-items">
|
||||
{% for opt in pronoun_options %}
|
||||
<div class="gk-pronoun-card{% if opt.active %} active{% endif %}" data-pronoun="{{ opt.key }}" data-trio="{{ opt.label }}">
|
||||
<span class="gk-pronoun-label">{{ opt.key }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user