Gate-view pos-1 lockout: thread the acting ?seat through the GATE VIEW nav — TDD

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) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-05 01:29:37 -04:00
parent 4c484cf25a
commit bedc489d7b
4 changed files with 47 additions and 5 deletions

View File

@@ -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):

View File

@@ -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,

View File

@@ -80,7 +80,7 @@
{# family]] #}
<button id="id_room_gate_view_btn" type="button"
class="btn btn-primary"
onclick="window.location.href='{% url 'epic:room_gate' room.id %}'">GATE<br>VIEW</button>
onclick="window.location.href='{% url 'epic:room_gate' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}'">GATE<br>VIEW</button>
{% else %}
{% if room.table_status == "SKY_SELECT" %}
{% if sky_confirmed %}

View File

@@ -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<br>VIEW
</button>