diff --git a/src/apps/applets/migrations/0009_seed_my_sig_applet.py b/src/apps/applets/migrations/0009_seed_my_sig_applet.py index 19178c1..42e8c73 100644 --- a/src/apps/applets/migrations/0009_seed_my_sig_applet.py +++ b/src/apps/applets/migrations/0009_seed_my_sig_applet.py @@ -1,10 +1,12 @@ """Seed the My Sign (a.k.a. My Significator) applet on billboard. Sprint 4 of the My Sea roadmap. "Significator" remains the storage-layer -term (User.significator FK, room sig-select) — this billboard surface is -branded "Game Sign". 4×6 (narrow + tall), seeded after all other billboard -applets so it renders at the end of the billboard grid. Shows the user's -saved significator card or a blank state. +term (User.significator FK, room sig-select); the applet itself is "My +Sign" (the "My X" convention for applet names) while the standalone page +wordmark reads "Game Sign" (the "Game/Dash/Bill X" convention for pages). +4×6 (narrow + tall), seeded after all other billboard applets so it +renders at the end of the billboard grid. Shows the user's saved +significator card or a blank state. """ from django.db import migrations @@ -14,7 +16,7 @@ def seed(apps, schema_editor): Applet.objects.update_or_create( slug="my-sign", defaults={ - "name": "Game Sign", + "name": "My Sign", "context": "billboard", "default_visible": True, "grid_cols": 4, diff --git a/src/apps/applets/migrations/0010_rename_my_sign_applet.py b/src/apps/applets/migrations/0010_rename_my_sign_applet.py new file mode 100644 index 0000000..9da4b95 --- /dev/null +++ b/src/apps/applets/migrations/0010_rename_my_sign_applet.py @@ -0,0 +1,31 @@ +"""Update the My Sign applet's display name — "Game Sign" → "My Sign". + +User clarified naming convention 2026-05-18: **applets** use the "My X" +prefix (My Sign, My Sea, My Posts, etc.) while **standalone pages** use +the "Game/Dash/Bill X" prefix (Game Sign page, Game Sea page, Game Kit +page). Sprint 4a's initial migration (0009) set the applet name to +"Game Sign", which the user corrected after seeing the gear-menu toggle +list show the wrong word. Slug stays `my-sign` (URL + selectors stable). +""" +from django.db import migrations + + +def rename(apps, schema_editor): + Applet = apps.get_model("applets", "Applet") + Applet.objects.filter(slug="my-sign").update(name="My Sign") + + +def unrename(apps, schema_editor): + Applet = apps.get_model("applets", "Applet") + Applet.objects.filter(slug="my-sign").update(name="Game Sign") + + +class Migration(migrations.Migration): + + dependencies = [ + ("applets", "0009_seed_my_sig_applet"), + ] + + operations = [ + migrations.RunPython(rename, unrename), + ] diff --git a/src/apps/billboard/tests/integrated/test_views.py b/src/apps/billboard/tests/integrated/test_views.py index e6287e4..e15d9f7 100644 --- a/src/apps/billboard/tests/integrated/test_views.py +++ b/src/apps/billboard/tests/integrated/test_views.py @@ -15,7 +15,7 @@ def _seed_billboard_applets(): ("my-scrolls", "My Scrolls", 4, 3), ("my-buds", "My Buds", 4, 3), ("most-recent-scroll", "Most Recent Scroll", 8, 6), - ("my-sign", "Game Sign", 4, 6), + ("my-sign", "My Sign", 4, 6), ]: Applet.objects.get_or_create( slug=slug, diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index ee99d58..ffa2b5a 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -877,6 +877,68 @@ class SelectRoleMultiSeatTest(TestCase): self.founder.refresh_from_db() self.assertIsNone(self.founder.equipped_deck) # still None, not broken + # ── Return-trip Role-Select bug — CARTE user navigates away + back ───── # + # + # After the FIRST role pick, select_role() clears user.equipped_deck + # ("deck committed to room"). The room view's role-select context then + # passes `equipped_deck_id = user.equipped_deck_id` to the template, + # which sets `data-equipped-deck=""` and JS guards against role-select + # with "Equip card deck before Role select." → blocks a CARTE user from + # continuing to pick roles for their remaining seats. Fix: the context's + # `equipped_deck_id` should also accept the deck_variant of any seat the + # user already holds in this room (the deck IS in play — it's just + # committed to existing seats, not to user.equipped_deck). + + def test_role_select_context_recovers_deck_id_from_existing_seat(self): + """User cleared their equipped_deck after first role pick, but they + still have a seat in this room w. deck_variant set → context should + report that deck's id so the guard doesn't fire on return.""" + TableSeat.objects.create( + room=self.room, gamer=self.founder, slot_number=1, + role="PC", deck_variant=self.earthman, + ) + # Slot 2 still needs role (CARTE user's next seat) + TableSeat.objects.create(room=self.room, gamer=self.founder, slot_number=2) + self.founder.equipped_deck = None + self.founder.save(update_fields=["equipped_deck"]) + # Simulate filled gate slots so the room renders in role-select state + for i in (1, 2): + slot = self.room.gate_slots.get(slot_number=i) + slot.gamer = self.founder + slot.status = GateSlot.FILLED + slot.save() + response = self.client.get( + reverse("epic:room", kwargs={"room_id": self.room.id}) + ) + self.assertEqual( + response.context["equipped_deck_id"], self.earthman.id, + "Returning CARTE user should see their in-play deck reflected in " + "the role-select context so the JS guard doesn't fire.", + ) + + def test_role_select_context_renders_data_equipped_deck_non_empty(self): + """Template-level check — the rendered `data-equipped-deck` attribute + should be non-empty so the JS guard at role-select.js:165 lets the + fan open.""" + TableSeat.objects.create( + room=self.room, gamer=self.founder, slot_number=1, + role="PC", deck_variant=self.earthman, + ) + TableSeat.objects.create(room=self.room, gamer=self.founder, slot_number=2) + self.founder.equipped_deck = None + self.founder.save(update_fields=["equipped_deck"]) + for i in (1, 2): + slot = self.room.gate_slots.get(slot_number=i) + slot.gamer = self.founder + slot.status = GateSlot.FILLED + slot.save() + response = self.client.get( + reverse("epic:room", kwargs={"room_id": self.room.id}) + ) + # Should NOT contain the empty-string version that triggers the guard. + self.assertNotContains(response, 'data-equipped-deck=""') + self.assertContains(response, f'data-equipped-deck="{self.earthman.id}"') + class RoomViewAllRolesFilledTest(TestCase): """Room view in ROLE_SELECT with all seats assigned shows SCAN SIGS button.""" diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py index 337f015..04fceb4 100644 --- a/src/apps/epic/views.py +++ b/src/apps/epic/views.py @@ -281,9 +281,25 @@ def _role_select_context(room, user): ) active_slot = active_seat.slot_number if active_seat else None _my_role = assigned_seats[0].role if assigned_seats else None + # `equipped_deck_id` gates the JS role-select guard (role-select.js L165). + # Falls back to ANY of the user's seats in this room w. deck_variant set + # — covers the CARTE multi-seat return path where the first role-pick + # cleared `user.equipped_deck` (the deck is committed to seats, but still + # "in play"). Without this, a CARTE user navigating away + back gets + # "Equip card deck before Role select" wrongly fired. See [[sprint-carte- + # role-select-return-may18]] / commit-TBD for the bug trail. + role_select_deck_id = ( + user.equipped_deck_id if user.is_authenticated else None + ) + if user.is_authenticated and not role_select_deck_id: + seat_w_deck = room.table_seats.filter( + gamer=user, deck_variant__isnull=False, + ).order_by("slot_number").first() + if seat_w_deck: + role_select_deck_id = seat_w_deck.deck_variant_id ctx = { "card_stack_state": card_stack_state, - "equipped_deck_id": user.equipped_deck_id if user.is_authenticated else None, + "equipped_deck_id": role_select_deck_id, "starter_roles": starter_roles, "assigned_seats": assigned_seats, "my_tray_role": _my_role, diff --git a/src/functional_tests/test_bill_my_sign.py b/src/functional_tests/test_bill_my_sign.py index 5d5d80d..c91ed33 100644 --- a/src/functional_tests/test_bill_my_sign.py +++ b/src/functional_tests/test_bill_my_sign.py @@ -18,7 +18,7 @@ from apps.lyric.models import User def _seed_my_sign_applet(): Applet.objects.get_or_create( slug="my-sign", - defaults={"name": "Game Sign", "context": "billboard", + defaults={"name": "My Sign", "context": "billboard", "default_visible": True, "grid_cols": 4, "grid_rows": 6}, ) diff --git a/src/static_src/scss/rootvars.scss b/src/static_src/scss/rootvars.scss index db39e3c..e528363 100644 --- a/src/static_src/scss/rootvars.scss +++ b/src/static_src/scss/rootvars.scss @@ -443,14 +443,14 @@ /* Baltimore Palette */ .palette-baltimore { --priUser: var(--sepBlt); - --secUser: var(--terBlt); + --secUser: var(--sixBlt); --terUser: var(--ninBlt); --quaUser: var(--priBlt); --quiUser: var(--terMze); --sixUser: var(--quiBlt); --sepUser: var(--quiBlt); --octUser: var(--quiBlt); - --ninUser: var(--sixBlt); + --ninUser: var(--terBlt); --decUser: var(--quiBlt); } diff --git a/src/templates/apps/billboard/_partials/_applet-my-sign.html b/src/templates/apps/billboard/_partials/_applet-my-sign.html index 104dbc5..f32640c 100644 --- a/src/templates/apps/billboard/_partials/_applet-my-sign.html +++ b/src/templates/apps/billboard/_partials/_applet-my-sign.html @@ -2,7 +2,7 @@ id="id_applet_my_sign" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};" > -