2026-03-09 01:07:16 -04:00
|
|
|
from django.contrib.auth.decorators import login_required
|
2026-03-16 00:07:52 -04:00
|
|
|
from django.http import HttpResponse
|
|
|
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
2026-03-15 16:08:34 -04:00
|
|
|
from django.utils import timezone
|
2026-03-09 01:07:16 -04:00
|
|
|
|
2026-04-21 15:46:30 -04:00
|
|
|
from apps.applets.utils import applet_context, apply_applet_toggle
|
2026-04-27 23:24:43 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _annotate_deck_in_use(decks, user):
|
|
|
|
|
"""Attach .in_use_room_name to each deck — the name of the active room using it, or None."""
|
|
|
|
|
active = {
|
|
|
|
|
ts.deck_variant_id: ts.room.name
|
|
|
|
|
for ts in TableSeat.objects.filter(
|
|
|
|
|
gamer=user, deck_variant__isnull=False,
|
|
|
|
|
).select_related("room")
|
|
|
|
|
}
|
|
|
|
|
for deck in decks:
|
|
|
|
|
deck.in_use_room_name = active.get(deck.pk)
|
|
|
|
|
return decks
|
|
|
|
|
from apps.epic.models import DeckVariant, Room, TableSeat
|
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>
2026-05-12 23:06:55 -04:00
|
|
|
from apps.epic.utils import annotate_latest_event, rooms_for_user
|
2026-03-09 01:07:16 -04:00
|
|
|
from apps.lyric.models import Token
|
|
|
|
|
|
|
|
|
|
|
2026-03-09 21:13:35 -04:00
|
|
|
GAMEBOARD_APPLET_ORDER = [
|
|
|
|
|
"new-game",
|
|
|
|
|
"my-games",
|
|
|
|
|
"game-kit",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-03-09 01:07:16 -04:00
|
|
|
@login_required(login_url="/")
|
|
|
|
|
def gameboard(request):
|
2026-03-15 01:17:09 -04:00
|
|
|
pass_token = request.user.tokens.filter(token_type=Token.PASS).first() if request.user.is_staff else None
|
2026-03-09 01:07:16 -04:00
|
|
|
coin = request.user.tokens.filter(token_type=Token.COIN).first()
|
2026-03-16 00:07:52 -04:00
|
|
|
carte = request.user.tokens.filter(token_type=Token.CARTE).first()
|
2026-03-15 16:08:34 -04:00
|
|
|
free_tokens = list(request.user.tokens.filter(
|
|
|
|
|
token_type=Token.FREE, expires_at__gt=timezone.now()
|
|
|
|
|
).order_by("expires_at"))
|
2026-03-09 01:07:16 -04:00
|
|
|
return render(
|
|
|
|
|
request, "apps/gameboard/gameboard.html", {
|
2026-03-15 01:17:09 -04:00
|
|
|
"pass_token": pass_token,
|
2026-03-09 01:07:16 -04:00
|
|
|
"coin": coin,
|
2026-03-16 00:07:52 -04:00
|
|
|
"carte": carte,
|
2026-04-16 00:14:47 -04:00
|
|
|
"equipped_trinket_id": request.user.equipped_trinket_id,
|
|
|
|
|
"equipped_deck_id": request.user.equipped_deck_id,
|
2026-04-27 23:24:43 -04:00
|
|
|
"deck_variants": _annotate_deck_in_use(list(request.user.unlocked_decks.all()), request.user),
|
2026-03-09 01:07:16 -04:00
|
|
|
"free_tokens": free_tokens,
|
2026-03-15 16:08:34 -04:00
|
|
|
"free_count": len(free_tokens),
|
2026-03-09 21:13:35 -04:00
|
|
|
"applets": applet_context(request.user, "gameboard"),
|
2026-03-09 21:52:54 -04:00
|
|
|
"page_class": "page-gameboard",
|
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>
2026-05-12 23:06:55 -04:00
|
|
|
"my_games": annotate_latest_event(rooms_for_user(request.user)),
|
2026-03-09 01:07:16 -04:00
|
|
|
}
|
|
|
|
|
)
|
2026-03-09 21:13:35 -04:00
|
|
|
|
|
|
|
|
@login_required(login_url="/")
|
|
|
|
|
def toggle_game_applets(request):
|
|
|
|
|
checked = request.POST.getlist("applets")
|
2026-04-21 15:46:30 -04:00
|
|
|
apply_applet_toggle(request.user, "gameboard", checked)
|
2026-03-09 21:13:35 -04:00
|
|
|
if request.headers.get("HX-Request"):
|
2026-04-21 15:46:30 -04:00
|
|
|
free_tokens = list(request.user.tokens.filter(
|
|
|
|
|
token_type=Token.FREE, expires_at__gt=timezone.now()
|
|
|
|
|
).order_by("expires_at"))
|
2026-03-09 21:13:35 -04:00
|
|
|
return render(request, "apps/gameboard/_partials/_applets.html", {
|
|
|
|
|
"applets": applet_context(request.user, "gameboard"),
|
2026-03-15 01:17:09 -04:00
|
|
|
"pass_token": request.user.tokens.filter(token_type=Token.PASS).first() if request.user.is_staff else None,
|
2026-03-09 21:13:35 -04:00
|
|
|
"coin": request.user.tokens.filter(token_type=Token.COIN).first(),
|
2026-03-16 00:07:52 -04:00
|
|
|
"carte": request.user.tokens.filter(token_type=Token.CARTE).first(),
|
2026-04-16 00:14:47 -04:00
|
|
|
"equipped_trinket_id": request.user.equipped_trinket_id,
|
|
|
|
|
"equipped_deck_id": request.user.equipped_deck_id,
|
2026-04-27 23:24:43 -04:00
|
|
|
"deck_variants": _annotate_deck_in_use(list(request.user.unlocked_decks.all()), request.user),
|
2026-04-21 15:46:30 -04:00
|
|
|
"free_tokens": free_tokens,
|
|
|
|
|
"free_count": len(free_tokens),
|
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>
2026-05-12 23:06:55 -04:00
|
|
|
"my_games": annotate_latest_event(rooms_for_user(request.user)),
|
2026-03-09 21:13:35 -04:00
|
|
|
})
|
|
|
|
|
return redirect("gameboard")
|
2026-03-16 00:07:52 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@login_required(login_url="/")
|
|
|
|
|
def equip_trinket(request, token_id):
|
|
|
|
|
token = get_object_or_404(Token, pk=token_id, user=request.user)
|
|
|
|
|
if request.method == "POST":
|
|
|
|
|
request.user.equipped_trinket = token
|
|
|
|
|
request.user.save(update_fields=["equipped_trinket"])
|
|
|
|
|
return HttpResponse(status=204)
|
|
|
|
|
return render(
|
|
|
|
|
request,
|
|
|
|
|
"apps/gameboard/_partials/_equip_trinket_btn.html",
|
|
|
|
|
{"token": token},
|
|
|
|
|
)
|
2026-03-24 21:52:57 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@login_required(login_url="/")
|
|
|
|
|
def equip_deck(request, deck_id):
|
|
|
|
|
deck = get_object_or_404(DeckVariant, pk=deck_id)
|
|
|
|
|
if request.method == "POST":
|
|
|
|
|
request.user.equipped_deck = deck
|
|
|
|
|
request.user.save(update_fields=["equipped_deck"])
|
|
|
|
|
return HttpResponse(status=204)
|
|
|
|
|
return HttpResponse(status=405)
|
2026-03-24 22:57:12 -04:00
|
|
|
|
|
|
|
|
|
2026-04-16 00:14:47 -04:00
|
|
|
@login_required(login_url="/")
|
|
|
|
|
def unequip_trinket(request, token_id):
|
|
|
|
|
token = get_object_or_404(Token, pk=token_id, user=request.user)
|
|
|
|
|
if request.method == "POST":
|
|
|
|
|
if request.user.equipped_trinket_id == token.pk:
|
|
|
|
|
request.user.equipped_trinket = None
|
|
|
|
|
request.user.save(update_fields=["equipped_trinket"])
|
|
|
|
|
return HttpResponse(status=204)
|
|
|
|
|
return HttpResponse(status=405)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@login_required(login_url="/")
|
|
|
|
|
def unequip_deck(request, deck_id):
|
|
|
|
|
get_object_or_404(DeckVariant, pk=deck_id)
|
|
|
|
|
if request.method == "POST":
|
|
|
|
|
if request.user.equipped_deck_id == deck_id:
|
|
|
|
|
request.user.equipped_deck = None
|
|
|
|
|
request.user.save(update_fields=["equipped_deck"])
|
|
|
|
|
return HttpResponse(status=204)
|
|
|
|
|
return HttpResponse(status=405)
|
|
|
|
|
|
|
|
|
|
|
2026-04-04 13:49:48 -04:00
|
|
|
def _game_kit_context(user):
|
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>
2026-05-04 01:11:40 -04:00
|
|
|
from apps.lyric.models import PRONOUN_CHOICES
|
2026-04-04 13:49:48 -04:00
|
|
|
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()
|
|
|
|
|
free_tokens = list(user.tokens.filter(
|
2026-03-24 22:57:12 -04:00
|
|
|
token_type=Token.FREE, expires_at__gt=timezone.now()
|
|
|
|
|
).order_by("expires_at"))
|
2026-04-04 13:49:48 -04:00
|
|
|
tithe_tokens = list(user.tokens.filter(token_type=Token.TITHE))
|
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>
2026-05-04 01:11:40 -04:00
|
|
|
pronoun_options = [
|
|
|
|
|
{"key": k, "label": label, "active": (k == user.pronouns)}
|
|
|
|
|
for (k, label) in PRONOUN_CHOICES
|
|
|
|
|
]
|
2026-04-04 13:49:48 -04:00
|
|
|
return {
|
2026-03-24 22:57:12 -04:00
|
|
|
"coin": coin,
|
|
|
|
|
"pass_token": pass_token,
|
|
|
|
|
"carte": carte,
|
|
|
|
|
"free_tokens": free_tokens,
|
|
|
|
|
"tithe_tokens": tithe_tokens,
|
2026-04-04 13:49:48 -04:00
|
|
|
"unlocked_decks": list(user.unlocked_decks.all()),
|
|
|
|
|
"applets": applet_context(user, "game-kit"),
|
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>
2026-05-04 01:11:40 -04:00
|
|
|
"pronoun_options": pronoun_options,
|
|
|
|
|
"current_pronouns": user.pronouns,
|
2026-04-04 13:49:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@login_required(login_url="/")
|
|
|
|
|
def game_kit(request):
|
|
|
|
|
return render(request, "apps/gameboard/game_kit.html", {
|
|
|
|
|
**_game_kit_context(request.user),
|
2026-03-24 22:57:12 -04:00
|
|
|
"page_class": "page-gameboard",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
2026-04-04 13:49:48 -04:00
|
|
|
@login_required(login_url="/")
|
|
|
|
|
def toggle_game_kit_sections(request):
|
|
|
|
|
checked = request.POST.getlist("applets")
|
2026-04-21 15:46:30 -04:00
|
|
|
apply_applet_toggle(request.user, "game-kit", checked)
|
2026-04-04 13:49:48 -04:00
|
|
|
if request.headers.get("HX-Request"):
|
|
|
|
|
return render(request, "apps/gameboard/_partials/_game_kit_sections.html",
|
|
|
|
|
_game_kit_context(request.user))
|
|
|
|
|
return redirect("game_kit")
|
|
|
|
|
|
|
|
|
|
|
2026-03-24 22:57:12 -04:00
|
|
|
@login_required(login_url="/")
|
|
|
|
|
def tarot_fan(request, deck_id):
|
|
|
|
|
from apps.epic.models import TarotCard
|
|
|
|
|
deck = get_object_or_404(DeckVariant, pk=deck_id)
|
|
|
|
|
if not request.user.unlocked_decks.filter(pk=deck_id).exists():
|
|
|
|
|
return HttpResponse(status=403)
|
2026-04-27 20:48:12 -04:00
|
|
|
_suit_order = {"BRANDS": 0, "GRAILS": 1, "BLADES": 2, "CROWNS": 3,
|
|
|
|
|
"WANDS": 0, "CUPS": 1, "SWORDS": 2, "PENTACLES": 3}
|
2026-03-25 00:24:26 -04:00
|
|
|
cards = sorted(
|
|
|
|
|
TarotCard.objects.filter(deck_variant=deck),
|
|
|
|
|
key=lambda c: (0 if c.arcana == "MAJOR" else 1, _suit_order.get(c.suit or "", 9), c.number),
|
|
|
|
|
)
|
2026-03-24 22:57:12 -04:00
|
|
|
return render(request, "apps/gameboard/_partials/_tarot_fan.html", {
|
|
|
|
|
"deck": deck,
|
|
|
|
|
"cards": cards,
|
|
|
|
|
})
|