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")
|
|
|
|
|
|
|
|
|
|
|
My Sea applet shell — Sprint 3 of the My Sea roadmap
User roadmap step (Sprint 3 of cluster): scaffold the My Sea applet on the gameboard + the standalone /gameboard/my-sea/ page where later sprints will host the gatekeeper / sig-select / sea-select reskin for solo-user draws. Shell-only — no draw flow yet; latest-draw rendering, mid-progress save, daily quota land in Sprints 4-9 ; **migration**: `applets/migrations/0008_seed_my_sea_applet.py` — RunPython that `update_or_create`s Applet(`slug='my-sea'`, name='My Sea', context='gameboard', default_visible=True, grid_cols=12, grid_rows=4). 12×4 wide horizontal banner so the Celtic Cross spread's 10 cards can render left-to-right in the applet aperture, scrollable like My Palette (per user spec). Reverse migration (`unseed`) deletes the row so the migration is reversible for staging rollbacks ; **applet partial**: `templates/apps/gameboard/_partials/_applet-my-sea.html` — same `{% applet_context %}` auto-discovery shape every other applet uses (`<section id="id_applet_my_sea" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">`). Header is a `<h2><a href="{% url 'my_sea' %}">My Sea</a></h2>` link (gold via global `body a` rule); body is a `.my-sea-scroll` container that either renders `.my-sea-card` cells from a `latest_draw_cards` context (TBD in Sprint 4-7) or a `.my-sea-empty` placeholder line "No draws yet." for fresh users ; **standalone page**: new `gameboard/views.py:my_sea` view + url at `/gameboard/my-sea/` (URL name `my_sea`) rendering `apps/gameboard/my_sea.html` — `{% extends "core/base.html" %}` shell w. letter-spread `<span>My</span><span>Sea</span>` h2 wordmark + `.my-sea-page__empty` placeholder paragraph "Your sea is calm. Draws will appear here." `page_class` doubled to `page-gameboard page-my-sea` so the body inherits the gameboard's landscape aperture treatment AND any future my-sea-specific styles can target a single class. Login-required like the rest of gameboard ; **tests (+6 ITs)**: GameboardViewTest gains 3 — `test_gameboard_shows_my_sea_applet` (cssselect pins #id_applet_my_sea), `test_my_sea_applet_renders_empty_state_for_new_user` (asserts ".my-sea-empty" text + no ".my-sea-card" rows), `test_my_sea_applet_header_links_to_my_sea_page` (h2 a href == reverse('my_sea')); new MySeaViewTest class — `test_my_sea_requires_login` (redirect to /?next=...), `test_my_sea_renders_200`, `test_my_sea_uses_gameboard_page_class` (page-gameboard + page-my-sea both in body class). Existing GameboardViewTest setUp already does `get_or_create` per-applet so no fixture change needed for the migration-driven my-sea row ; 1005 IT/UT green (+6 from 999) in 45s; visual verified in Claudezilla at iPhone-14 portrait — applet renders w. rotated "MY SEA" vertical label + "No draws yet." body; /gameboard/my-sea/ standalone page renders w. letter-spread wordmark + placeholder ; **next**: Sprint 4 — My Sea sig-select phase (single-significator pick for solo user, w. the parameterized hex CSS from Sprint 1 hosting the chair-less or single-chair variant)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 19:45:57 -04:00
|
|
|
@login_required(login_url="/")
|
|
|
|
|
def my_sea(request):
|
|
|
|
|
"""Shell view for the My Sea standalone page.
|
|
|
|
|
|
|
|
|
|
Sprint 3 scaffolding only — the page will host the gatekeeper /
|
|
|
|
|
sig-select / sea-select phase reskin for solo-user draws in later
|
|
|
|
|
sprints. For now it just renders the empty skeleton.
|
|
|
|
|
"""
|
|
|
|
|
return render(request, "apps/gameboard/my_sea.html", {
|
|
|
|
|
"page_class": "page-gameboard page-my-sea",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
})
|