Game Sign picker @ /billboard/my-sign/ + billboard applet — Sprint 4a of My Sea roadmap — TDD
User scope (per design conv this session): split the room's sig-select responsibility off into a standalone billboard-context "My Significator" applet — branded "Game Sign" on the surface. Same 18-card pile as room sig-select (16 middle arcana + Major 0 & 1 filtered by Note unlocks); polarity collapses to a single FLIP choice (the FLIP btn in the picker carousel toggles User.significator_reversed). Selection persists globally on the User model + propagates to the billboard's Game Sign applet ; **naming convention locked**: "significator" stays at storage (User.significator FK + User.significator_reversed) + room sig-select context (DRY w. existing template/JS); "Sign" / "Game Sign" is the billboard-surface branding (file my_sign.html, URL /billboard/my-sign/, URL names my_sign + save_sign, applet name "Game Sign", page wordmark "Game Sign", btn label SAVE SIGN). Action URLs don't carry a trailing slash per project convention (/billboard/my-sign/save vs the page's /billboard/my-sign/) ; **schema**: User gains 2 fields — `significator: FK → epic.TarotCard (nullable, on_delete=SET_NULL)` + `significator_reversed: BooleanField(default=False)`. Migration lyric/0006_user_significator_user_significator_reversed.py auto-generated; reversible. Applet seed in applets/0009_seed_my_sig_applet.py adds the row (slug='my-sign', name='Game Sign', context='billboard', default_visible=True, grid_cols=4, grid_rows=6), idempotent update_or_create, reversible unseed() ; **picker page** (my_sign.html): solo lift of `_sig_select_overlay.html` — sig-stage-card scaffold + sig-stat-block + 18-card grid + SAVE SIGN form. Stripped: countdown / WebSocket / polarity / multi-user / reservations. Empty-state branch covers no-equipped-deck (link back to Game Kit; full Brief-redirect + Earthman-Backup fallback deferred to a follow-up sub-sprint). Minimal inline JS: click .sig-card → mark .sig-focused + set hidden card_id + enable SAVE SIGN; FLIP btn toggles .is-reversed + the hidden reversed input. Stage-card preview (name/qualifier population + keyword swap on FLIP) deferred — Sprint 4a follow-up will lift stage-card.js's populator into a non-room context ; **applet partial** (_applet-my-sign.html): renders user.significator's corner-rank + suit-icon + name_title if set; `.my-sign-applet-empty` "No sign chosen yet." otherwise. Header `<h2><a href="{% url 'billboard:my_sign' %}">Game Sign</a></h2>` links to the picker ; **helper refactor** (epic/models.py): extracted `_sig_unique_cards_for_deck(deck_variant)` from `_sig_unique_cards(room)`. New public `personal_sig_cards(user)` parallels `levity_sig_cards / gravity_sig_cards` but pulls from `user.equipped_deck` instead of `room.deck_variant`. Same Note-unlock filtering. No behavior change to existing room callers (3-line wrapper preserves the room signature) ; **TDD trail** — user called out mid-sprint that I'd skipped FTs; pivoted to FT-first. test_bill_my_sign.py (new, 3 FTs): T1 picker renders w. wordmark + target card present in grid; T2 click card → SAVE SIGN enables → POST persists → applet shows the card; T3 fresh user → applet renders empty-state. Initial reds — (a) setUp's `personal_sig_cards(user)` returned [] because StaticLiveServerTestCase → TransactionTestCase flushes migration-seeded DeckVariant + TarotCard between tests; fixed w. `serialized_rollback = True` on the test class (per [[feedback_transactiontestcase_flush]]); (b) h2 wordmark assertion against `MYSIGNIFICATOR` failed against the renamed "Game Sign" + the letter-splitter spreading chars across <span> children — switched to whitespace-stripped substring check `GAMESIGN`; (c) `.fan-corner-rank` text is CSS-hidden so Selenium returns "" — replaced corner-rank assertions w. data-card-id selectors (already-proven reliable from the parent .sig-card lookup) ; ITs (+12, in apps.billboard.tests.integrated.test_views): MySignViewTest (6 — login redirect, 200 + template, 16-card pile, save persists, invalid card_id → 403, GET save redirects); BillboardAppletMySignTest (3 — applet rendered, empty-state w/o sig, card+reversed class w. sig). PersonalSigCardsTest in apps.epic.tests.integrated.test_models (3 — happy path 16 cards, no-equipped-deck → [], schizo Note unlocks Major 1) ; pre-existing change picked up by the commit: my_sea.html branding "Game Sea" (user-modified mid-session; was "My Sea" in Sprint 3 — divergence captured in MEMORY.md follow-up) ; 1020 IT/UT green (+12) in 46s; 3 FTs green in 24s. Sprint 4a unblocks Sprint 4b (My Sea gating w. --terUser link to /billboard/my-sign/) + Sprint 4c (FT helper for mocking the sig choice across other FTs)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:23:24 -04:00
|
|
|
{% extends "core/base.html" %}
|
|
|
|
|
{% load static %}
|
|
|
|
|
|
|
|
|
|
{% block title_text %}Game Sign{% endblock title_text %}
|
|
|
|
|
{% block header_text %}<span>Game</span><span>Sign</span>{% endblock header_text %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
{# Solo lift of `_sig_select_overlay.html`. Same card-grid + stage-card #}
|
|
|
|
|
{# choreography as the room sig-select, minus countdown / WebSocket / #}
|
|
|
|
|
{# polarity / multi-user. FLIP btn (.spin-btn) lets the user choose the #}
|
|
|
|
|
{# card's orientation; SAVE SIG persists it on the User model. #}
|
|
|
|
|
{# "Significator" is preserved at the storage layer (User.significator) + #}
|
|
|
|
|
{# game-room context; this billboard surface re-brands to "Sign". #}
|
|
|
|
|
<div class="my-sign-page"
|
|
|
|
|
data-save-url="{% url 'billboard:save_sign' %}"
|
|
|
|
|
{% if current_significator %}data-current-card-id="{{ current_significator.id }}"{% endif %}
|
|
|
|
|
data-current-reversed="{{ current_significator_reversed|yesno:'true,false' }}">
|
|
|
|
|
|
|
|
|
|
<div class="my-sign-stage">
|
|
|
|
|
<div class="sig-stage-card" style="display:none">
|
|
|
|
|
<div class="fan-card-corner fan-card-corner--tl">
|
|
|
|
|
<span class="fan-corner-rank"></span>
|
|
|
|
|
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="fan-card-face">
|
|
|
|
|
<div class="fan-card-face-upright">
|
|
|
|
|
<p class="fan-card-name-group"></p>
|
|
|
|
|
<p class="sig-qualifier-above"></p>
|
|
|
|
|
<p class="fan-card-name"></p>
|
|
|
|
|
<p class="sig-qualifier-below"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="fan-card-arcana"></p>
|
|
|
|
|
<div class="fan-card-face-reversal">
|
|
|
|
|
<p class="fan-card-reversal-name"></p>
|
|
|
|
|
<p class="fan-card-reversal-qualifier"></p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="fan-card-corner fan-card-corner--br">
|
|
|
|
|
<span class="fan-corner-rank"></span>
|
|
|
|
|
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="sig-stat-block">
|
|
|
|
|
<button class="btn btn-reverse spin-btn" type="button">FLIP</button>
|
|
|
|
|
<div class="stat-face stat-face--upright">
|
|
|
|
|
<p class="stat-face-label">Emanation</p>
|
|
|
|
|
<ul class="stat-keywords"></ul>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-face stat-face--reversed">
|
|
|
|
|
<p class="stat-face-label">Reversal</p>
|
|
|
|
|
<ul class="stat-keywords"></ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="my-sign-deck-grid">
|
|
|
|
|
{% for card in cards %}
|
|
|
|
|
<div class="sig-card"
|
|
|
|
|
data-card-id="{{ card.id }}"
|
|
|
|
|
data-suit-icon="{{ card.suit_icon }}"
|
|
|
|
|
data-corner-rank="{{ card.corner_rank }}"
|
|
|
|
|
data-name-group="{{ card.name_group }}"
|
|
|
|
|
data-name-title="{{ card.name_title }}"
|
|
|
|
|
data-arcana="{{ card.get_arcana_display }}"
|
|
|
|
|
data-keywords-upright="{{ card.keywords_upright|join:',' }}"
|
|
|
|
|
data-keywords-reversed="{{ card.keywords_reversed|join:',' }}"
|
|
|
|
|
data-reversal-qualifier="{{ card.reversal_qualifier }}">
|
|
|
|
|
<div class="fan-card-corner fan-card-corner--tl">
|
|
|
|
|
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
|
|
|
|
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<form id="id_save_sign_form" method="POST" action="{% url 'billboard:save_sign' %}">
|
|
|
|
|
{% csrf_token %}
|
|
|
|
|
<input type="hidden" name="card_id" id="id_save_sign_card_id" value="{{ current_significator.id|default:'' }}">
|
|
|
|
|
<input type="hidden" name="reversed" id="id_save_sign_reversed" value="{{ current_significator_reversed|yesno:'1,0' }}">
|
|
|
|
|
<button type="submit" id="id_save_sign_btn" class="btn btn-primary"{% if not current_significator %} disabled{% endif %}>SAVE SIGN</button>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
{# Minimal picker JS — click .sig-card to pick + enable SAVE SIGN. #}
|
|
|
|
|
{# FLIP-btn integration (reversed toggle) lands w. the stage-card #}
|
|
|
|
|
{# preview JS in a Sprint 4a-follow-up; for now we just record #}
|
|
|
|
|
{# the chosen card_id + the FLIP state. #}
|
|
|
|
|
<script>
|
|
|
|
|
(function () {
|
|
|
|
|
var grid = document.querySelector('.my-sign-deck-grid');
|
|
|
|
|
if (!grid) return;
|
|
|
|
|
var cardIdInput = document.getElementById('id_save_sign_card_id');
|
|
|
|
|
var saveBtn = document.getElementById('id_save_sign_btn');
|
|
|
|
|
var flipBtn = document.querySelector('.my-sign-stage .spin-btn');
|
|
|
|
|
var revInput = document.getElementById('id_save_sign_reversed');
|
|
|
|
|
grid.addEventListener('click', function (e) {
|
|
|
|
|
var card = e.target.closest('.sig-card');
|
|
|
|
|
if (!card) return;
|
|
|
|
|
grid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) {
|
|
|
|
|
c.classList.remove('sig-focused');
|
|
|
|
|
});
|
|
|
|
|
card.classList.add('sig-focused');
|
|
|
|
|
cardIdInput.value = card.dataset.cardId;
|
|
|
|
|
saveBtn.removeAttribute('disabled');
|
|
|
|
|
});
|
|
|
|
|
if (flipBtn) {
|
|
|
|
|
flipBtn.addEventListener('click', function () {
|
|
|
|
|
var current = revInput.value === '1';
|
|
|
|
|
revInput.value = current ? '0' : '1';
|
|
|
|
|
flipBtn.classList.toggle('is-reversed', !current);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}());
|
|
|
|
|
</script>
|
2026-05-18 22:49:49 -04:00
|
|
|
|
|
|
|
|
{# No equipped deck + no saved sig — fire a Brief banner via the #}
|
|
|
|
|
{# shared note.js API so positioning + NVM behavior + h2-overlay #}
|
|
|
|
|
{# styling matches every other Brief on the site (per #}
|
|
|
|
|
{# [[sprint-baltimorean-note-unlock-may18]] portrait h2 measurement). #}
|
|
|
|
|
{# FYI links to /gameboard/ (Game Kit equip); NVM dismisses + the #}
|
|
|
|
|
{# picker proceeds against the Earthman [Shabby Paperboard] fallback. #}
|
|
|
|
|
<script src="{% static 'apps/dashboard/note.js' %}"></script>
|
|
|
|
|
{% if show_backup_intro_banner %}
|
|
|
|
|
<script>
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
|
if (!window.Brief || !Brief.showBanner) return;
|
|
|
|
|
Brief.showBanner({
|
|
|
|
|
title: 'Default deck warning',
|
|
|
|
|
line_text: 'Look!—no deck is equipped. Navigate to the Game Kit to equip one (FYI) or (NVM) proceed with the Earthman [Shabby Cardstock] deck.',
|
|
|
|
|
post_url: '{% url "gameboard" %}',
|
|
|
|
|
created_at: '',
|
|
|
|
|
kind: 'NUDGE',
|
|
|
|
|
});
|
|
|
|
|
// Tag the banner so FTs (and any my-sign-specific styling) can
|
|
|
|
|
// distinguish this intro nudge from other Briefs on the page.
|
|
|
|
|
var banner = document.querySelector('.note-banner');
|
|
|
|
|
if (banner) banner.classList.add('my-sign-intro-banner');
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
{% endif %}
|
Game Sign picker @ /billboard/my-sign/ + billboard applet — Sprint 4a of My Sea roadmap — TDD
User scope (per design conv this session): split the room's sig-select responsibility off into a standalone billboard-context "My Significator" applet — branded "Game Sign" on the surface. Same 18-card pile as room sig-select (16 middle arcana + Major 0 & 1 filtered by Note unlocks); polarity collapses to a single FLIP choice (the FLIP btn in the picker carousel toggles User.significator_reversed). Selection persists globally on the User model + propagates to the billboard's Game Sign applet ; **naming convention locked**: "significator" stays at storage (User.significator FK + User.significator_reversed) + room sig-select context (DRY w. existing template/JS); "Sign" / "Game Sign" is the billboard-surface branding (file my_sign.html, URL /billboard/my-sign/, URL names my_sign + save_sign, applet name "Game Sign", page wordmark "Game Sign", btn label SAVE SIGN). Action URLs don't carry a trailing slash per project convention (/billboard/my-sign/save vs the page's /billboard/my-sign/) ; **schema**: User gains 2 fields — `significator: FK → epic.TarotCard (nullable, on_delete=SET_NULL)` + `significator_reversed: BooleanField(default=False)`. Migration lyric/0006_user_significator_user_significator_reversed.py auto-generated; reversible. Applet seed in applets/0009_seed_my_sig_applet.py adds the row (slug='my-sign', name='Game Sign', context='billboard', default_visible=True, grid_cols=4, grid_rows=6), idempotent update_or_create, reversible unseed() ; **picker page** (my_sign.html): solo lift of `_sig_select_overlay.html` — sig-stage-card scaffold + sig-stat-block + 18-card grid + SAVE SIGN form. Stripped: countdown / WebSocket / polarity / multi-user / reservations. Empty-state branch covers no-equipped-deck (link back to Game Kit; full Brief-redirect + Earthman-Backup fallback deferred to a follow-up sub-sprint). Minimal inline JS: click .sig-card → mark .sig-focused + set hidden card_id + enable SAVE SIGN; FLIP btn toggles .is-reversed + the hidden reversed input. Stage-card preview (name/qualifier population + keyword swap on FLIP) deferred — Sprint 4a follow-up will lift stage-card.js's populator into a non-room context ; **applet partial** (_applet-my-sign.html): renders user.significator's corner-rank + suit-icon + name_title if set; `.my-sign-applet-empty` "No sign chosen yet." otherwise. Header `<h2><a href="{% url 'billboard:my_sign' %}">Game Sign</a></h2>` links to the picker ; **helper refactor** (epic/models.py): extracted `_sig_unique_cards_for_deck(deck_variant)` from `_sig_unique_cards(room)`. New public `personal_sig_cards(user)` parallels `levity_sig_cards / gravity_sig_cards` but pulls from `user.equipped_deck` instead of `room.deck_variant`. Same Note-unlock filtering. No behavior change to existing room callers (3-line wrapper preserves the room signature) ; **TDD trail** — user called out mid-sprint that I'd skipped FTs; pivoted to FT-first. test_bill_my_sign.py (new, 3 FTs): T1 picker renders w. wordmark + target card present in grid; T2 click card → SAVE SIGN enables → POST persists → applet shows the card; T3 fresh user → applet renders empty-state. Initial reds — (a) setUp's `personal_sig_cards(user)` returned [] because StaticLiveServerTestCase → TransactionTestCase flushes migration-seeded DeckVariant + TarotCard between tests; fixed w. `serialized_rollback = True` on the test class (per [[feedback_transactiontestcase_flush]]); (b) h2 wordmark assertion against `MYSIGNIFICATOR` failed against the renamed "Game Sign" + the letter-splitter spreading chars across <span> children — switched to whitespace-stripped substring check `GAMESIGN`; (c) `.fan-corner-rank` text is CSS-hidden so Selenium returns "" — replaced corner-rank assertions w. data-card-id selectors (already-proven reliable from the parent .sig-card lookup) ; ITs (+12, in apps.billboard.tests.integrated.test_views): MySignViewTest (6 — login redirect, 200 + template, 16-card pile, save persists, invalid card_id → 403, GET save redirects); BillboardAppletMySignTest (3 — applet rendered, empty-state w/o sig, card+reversed class w. sig). PersonalSigCardsTest in apps.epic.tests.integrated.test_models (3 — happy path 16 cards, no-equipped-deck → [], schizo Note unlocks Major 1) ; pre-existing change picked up by the commit: my_sea.html branding "Game Sea" (user-modified mid-session; was "My Sea" in Sprint 3 — divergence captured in MEMORY.md follow-up) ; 1020 IT/UT green (+12) in 46s; 3 FTs green in 24s. Sprint 4a unblocks Sprint 4b (My Sea gating w. --terUser link to /billboard/my-sign/) + Sprint 4c (FT helper for mocking the sig choice across other FTs)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:23:24 -04:00
|
|
|
</div>
|
|
|
|
|
{% endblock content %}
|