From bedc489d7bef916b7613279b5b6ec498354d6a9a Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Fri, 5 Jun 2026 01:29:37 -0400 Subject: [PATCH] =?UTF-8?q?Gate-view=20pos-1=20lockout:=20thread=20the=20a?= =?UTF-8?q?cting=20=3Fseat=20through=20the=20GATE=20VIEW=20nav=20=E2=80=94?= =?UTF-8?q?=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GATE VIEW buttons (navbar + room.html lapsed-cost) linked to room_gate with no ?seat, so _viewer_current_slot fell back to owned[0] (pos 1). On the gate view a CARTE multi-seat gamer acting at pos 2+ saw pos 1 wrongly flagged me-current AND href-less — the one circle you could never switch back to. Fix (option a): _role_select_context + _gate_context now both expose current_slot; both GATE VIEW buttons append ?seat={{ current_slot }} on page-room. The gate view's current now matches the table → pos 1 becomes me-also (switch href) when acting elsewhere, the occupied seat correctly carries no href. _gate_context computes current_slot once and reuses it for gate_positions. 3 ITs in CarteTrayFollowsSelectedSeatTest (button carries acting seat; default targets lowest owned; gate page re-carries seat + pos 1 is me-also). 911 epic+gameboard ITs green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/apps/epic/tests/integrated/test_views.py | 35 ++++++++++++++++++++ src/apps/epic/views.py | 13 ++++++-- src/templates/apps/gameboard/room.html | 2 +- src/templates/core/_partials/_navbar.html | 2 +- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 34c0538..c13969f 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -884,6 +884,41 @@ class CarteTrayFollowsSelectedSeatTest(TestCase): response = self.client.get(self.room_url + "?seat=6") self.assertIsNone(response.context["my_tray_sig"]) + def test_gate_view_button_carries_acting_seat(self): + # Regression (gate-view pos-1 lockout): the GATE VIEW buttons linked + # to room_gate with NO ?seat, so the gate view fell back to owned[0] + # (pos 1) → pos 1 was always me-current + never got a switch href: + # the one seat you could never return to. The acting ?seat must ride + # the gate-view nav so the gate view's current matches the table. + gate_url = reverse("epic:room_gate", kwargs={"room_id": self.room.id}) + response = self.client.get(self.room_url + "?seat=4") + self.assertEqual(response.context["current_slot"], 4) + self.assertContains(response, f"{gate_url}?seat=4") + + def test_gate_view_button_default_targets_lowest_owned(self): + # No ?seat on the table → the gate button carries the lowest owned + # slot (current_slot), keeping the table + gate views in agreement. + gate_url = reverse("epic:room_gate", kwargs={"room_id": self.room.id}) + response = self.client.get(self.room_url) + self.assertEqual(response.context["current_slot"], 1) + self.assertContains(response, f"{gate_url}?seat=1") + + def test_gate_page_exposes_and_re_carries_acting_seat(self): + # The gate page's own navbar GATE VIEW re-click must keep the acting + # seat → _gate_context exposes current_slot too, and pos 1 is now + # me-also (switchable) while pos 4 is me-current — no lockout. + gate_url = reverse("epic:room_gate", kwargs={"room_id": self.room.id}) + response = self.client.get(gate_url + "?seat=4") + self.assertEqual(response.context["current_slot"], 4) + self.assertContains(response, f"{gate_url}?seat=4") + by_slot = { + p["slot"].slot_number: p + for p in response.context["gate_positions"] + } + self.assertEqual(by_slot[4]["state_class"], "tt-pos-me-current") + self.assertTrue(by_slot[1]["is_me_also"]) + self.assertEqual(by_slot[1]["state_class"], "tt-pos-me-also") + class PickRolesViewTest(TestCase): def setUp(self): diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py index c691db9..10a0ef3 100644 --- a/src/apps/epic/views.py +++ b/src/apps/epic/views.py @@ -336,6 +336,7 @@ def _expire_lapsed_seats(room): def _gate_context(room, user, seat_param=None): _expire_reserved_slots(room) + current_slot = _viewer_current_slot(room, user, seat_param) slots = room.gate_slots.order_by("slot_number") pending_slot = slots.filter(status=GateSlot.RESERVED).first() user_reserved_slot = None @@ -390,9 +391,10 @@ def _gate_context(room, user, seat_param=None): "carte_slots_claimed": carte_slots_claimed, "carte_nvm_slot_number": carte_nvm_slot_number, "carte_next_slot_number": carte_next_slot_number, - "gate_positions": _gate_positions( - room, user, _viewer_current_slot(room, user, seat_param) - ), + # Acting seat — exposed so the gate page's own GATE VIEW re-click + # re-carries ?seat (keeps the seat-switch context across reloads). + "current_slot": current_slot, + "gate_positions": _gate_positions(room, user, current_slot), "starter_roles": [], } @@ -486,6 +488,11 @@ def _role_select_context(room, user, seat_param=None): role_select_deck_id = seat_w_deck.deck_variant_id ctx = { "card_stack_state": card_stack_state, + # The viewer's acting seat (?seat slot, else lowest owned). Threaded + # onto the GATE VIEW nav so the gate view's current matches the table + # — without it the gate view falls back to owned[0] (pos 1), locking + # pos 1 as a never-switchable me-current circle. + "current_slot": current_slot, "equipped_deck_id": role_select_deck_id, "starter_roles": starter_roles, "assigned_seats": assigned_seats, diff --git a/src/templates/apps/gameboard/room.html b/src/templates/apps/gameboard/room.html index 54b1529..f4ca32c 100644 --- a/src/templates/apps/gameboard/room.html +++ b/src/templates/apps/gameboard/room.html @@ -80,7 +80,7 @@ {# family]] #} + onclick="window.location.href='{% url 'epic:room_gate' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}'">GATE
VIEW {% else %} {% if room.table_status == "SKY_SELECT" %} {% if sky_confirmed %} diff --git a/src/templates/core/_partials/_navbar.html b/src/templates/core/_partials/_navbar.html index 0960238..4704bef 100644 --- a/src/templates/core/_partials/_navbar.html +++ b/src/templates/core/_partials/_navbar.html @@ -45,7 +45,7 @@ id="id_navbar_gate_view_btn" class="btn btn-primary" type="button" - onclick="window.location.href='{% if 'page-room' in page_class %}{% url 'epic:room_gate' room.id %}{% elif 'page-my-sea-visit' in page_class %}{% url 'my_sea_visit_gate' owner.id %}{% else %}{% url 'my_sea_gate' %}{% endif %}'" + onclick="window.location.href='{% if 'page-room' in page_class %}{% url 'epic:room_gate' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}{% elif 'page-my-sea-visit' in page_class %}{% url 'my_sea_visit_gate' owner.id %}{% else %}{% url 'my_sea_gate' %}{% endif %}'" > GATE
VIEW