diff --git a/src/apps/gameboard/tests/integrated/test_sea_visit.py b/src/apps/gameboard/tests/integrated/test_sea_visit.py index 175c622..4a74662 100644 --- a/src/apps/gameboard/tests/integrated/test_sea_visit.py +++ b/src/apps/gameboard/tests/integrated/test_sea_visit.py @@ -313,10 +313,11 @@ class MySeaOwnerNavbarGateUnaffectedTest(TestCase): class MySeaVisitOwnerSeatedTest(TestCase): - """Phase 2 — the owner shows seated in 1C on the spectator hex whenever - she's committed to a draw cycle (drawn OR paid), not only once a card - lands. Previously a paid-but-undrawn owner read as the empty .fa-ban - default on the visitor's view.""" + """Phase 2 (window rule 2026-05-31) — the owner shows seated in 1C on the + spectator hex for the FULL 24h window after her most recent free/paid + draw, i.e. as long as `active_draw_for` returns a row. Seating tracks the + row's existence, not the hand/deposit/paid state — so a DEL'd row keeps + the owner seated on the visitor's view too.""" def setUp(self): self.owner = _owner_with_sig() @@ -336,6 +337,15 @@ class MySeaVisitOwnerSeatedTest(TestCase): ) self.assertTrue(self.client.get(self.url).context["seat1_present"]) + def test_owner_seated_with_empty_hand_no_payment(self): + # DEL'd row (empty hand, no deposit/paid) — still seated for the 24h + # window on the spectator's view (user-spec 2026-05-31). + MySeaDraw.objects.create( + user=self.owner, spread="situation-action-outcome", + significator_id=1, hand=[], + ) + self.assertTrue(self.client.get(self.url).context["seat1_present"]) + def test_owner_not_seated_without_draw_or_payment(self): self.assertFalse(self.client.get(self.url).context["seat1_present"]) diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index 19aa90c..d42e8f9 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -1347,12 +1347,13 @@ class MySeaDrawSeaLandingViewTest(TestCase): self.assertEqual(html.count('class="position-status-icon fa-solid fa-ban"'), 6) self.assertEqual(html.count('class="seat-position-label"'), 6) - # ── Owner 1C seating (Phase 2, 2026-05-29) ─────────────────────────── - # The owner is "seated" in 1C whenever she's committed to a draw cycle — - # paid for one OR partially/completely drawn (w/o DEL). Previously only a - # non-empty hand seated 1C, so a paid-but-undrawn owner (the PAID DRAW - # landing) showed the semi-opaque .fa-ban default. Seat 1C now carries - # `.seated` + `.fa-circle-check` so it persists on refresh (sync). + # ── Owner 1C seating (Phase 2, 2026-05-29; window rule 2026-05-31) ──── + # The owner is "seated" in 1C for the FULL 24h window after her most + # recent free/paid draw — i.e. as long as `active_draw_for` returns a + # row. Seating tracks the row's existence, NOT the hand/deposit/paid + # state (user-spec 2026-05-31): a DEL'd row (empty hand, no paid credit) + # keeps 1C seated until the 24h row expires. Seat 1C carries `.seated` + + # `.fa-circle-check` so it persists on refresh (sync). _SEATED_1C = 'class="table-seat seated" data-slot="1"' _STATUS_CHECK = 'class="position-status-icon fa-solid fa-circle-check"' _STATUS_BAN = 'class="position-status-icon fa-solid fa-ban"' @@ -1392,15 +1393,24 @@ class MySeaDrawSeaLandingViewTest(TestCase): self.assertIn(self._SEATED_1C, html) self.assertEqual(html.count(self._STATUS_CHECK), 1) - def test_seat_1c_not_seated_at_gate_view_after_del(self): + def test_seat_1c_seated_at_gate_view_after_del(self): # Cooldown row, hand cleared (DEL'd), no deposit/paid credit → GATE - # VIEW. The owner has unseated herself; 1C falls back to .fa-ban. + # VIEW in the center, BUT the owner stays seated in 1C for the full + # 24h window (user-spec 2026-05-31): seating tracks the draw row's + # existence (active_draw_for), not the hand/deposit/paid state. self._make_draw(hand=[]) html = self.client.get(reverse("my_sea")).content.decode() - self.assertNotIn(self._SEATED_1C, html) + self.assertIn(self._SEATED_1C, html) self.assertIn("GATE", html) - self.assertEqual(html.count(self._STATUS_CHECK), 0) - self.assertEqual(html.count(self._STATUS_BAN), 6) + self.assertEqual(html.count(self._STATUS_CHECK), 1) # 1C seated + self.assertEqual(html.count(self._STATUS_BAN), 5) # 2C–6C empty + + def test_seat1_seated_context_key_removed(self): + # DRY: the dead `seat1_seated` context var (the `seats` ring drives + # 1C now) is gone — guard against re-introduction. + self._make_draw(hand=[]) + response = self.client.get(reverse("my_sea")) + self.assertNotIn("seat1_seated", response.context) def test_landing_not_rendered_when_user_has_no_sig(self): # Sprint 4b gate still wins precedence — FREE DRAW must not render diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index b140c7f..b3c6807 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -382,14 +382,11 @@ def my_sea(request): "saved_by_position": saved_by_position, "next_free_draw_at": next_free_draw_at, "hand_complete": hand_complete, - # Owner seated in 1C whenever committed to a draw cycle — paid for one - # (deposit reserved OR paid-through credit, via show_paid_draw) OR has - # cards down (hand_non_empty). Drives the persistent `.seated` + - # `.fa-circle-check` on the landing's 1C chair (user-spec 2026-05-29); - # a DEL'd row at GATE VIEW (empty hand, no paid credit) reads unseated. - "seat1_seated": hand_non_empty or show_paid_draw, # The owner's landing hex shows present visitors too (2C-6C), not just # 1C — the same `_my_sea_seats` ring the spectator + broadcast use. + # 1C seating is driven entirely by this ring's `seat.present`; the dead + # `seat1_seated` flag was removed 2026-05-31 (the template never read + # it — `_my_sea_seats` already seats 1C for the full 24h window). "seats": _my_sea_seats(request.user), "show_picker": show_picker, "show_cont_draw": show_cont_draw, @@ -507,11 +504,12 @@ def _my_sea_seats(owner): seat broadcast (`sea_seats`).""" from .models import SeaInvite, MY_SEA_MAX_VISITORS owner_draw = active_draw_for(owner) - owner_seated = owner_draw is not None and ( - bool(owner_draw.hand) - or owner_draw.deposit_token_id is not None - or owner_draw.paid_through_at is not None - ) + # Seated in 1C for the FULL 24h window — as long as a draw row exists + # (free or paid; PAID DRAW resets created_at so the window runs from the + # later of the two). Decoupled from hand/deposit/paid state per user-spec + # 2026-05-31: a DEL'd row (empty hand, no paid credit) keeps the owner + # seated until the row expires at 24h, not unseated the instant she DELs. + owner_seated = owner_draw is not None owner_token = (f"owner-{owner.id}-{owner_draw.id}" if owner_draw is not None else "") seats = [{"n": 1, "label": "1C", "present": owner_seated, "token": owner_token}] @@ -1022,15 +1020,11 @@ def my_sea_visit(request, owner_id): owner_draw = active_draw_for(owner) sig_card, sig_reversed = _resolve_sig(owner, owner_draw) owner_hand_non_empty = owner_draw is not None and bool(owner_draw.hand) - # Owner seated in 1C (as the visitor sees it) whenever she's committed to a - # draw cycle — drawn OR paid (deposit reserved / paid-through credit). Keeps - # the owner's 1C chair persistently `.seated` on the spectator hex even - # before a card lands (user-spec 2026-05-29); mirrors my_sea's seat1_seated. - owner_paid = owner_draw is not None and ( - owner_draw.deposit_token_id is not None - or owner_draw.paid_through_at is not None - ) - owner_seated = owner_hand_non_empty or owner_paid + # Owner seated in 1C (as the visitor sees it) for the FULL 24h window — + # as long as a draw row exists (user-spec 2026-05-31), not only once a + # card lands or a deposit/paid credit is set. Mirrors `_my_sea_seats`' + # `owner_seated` so the spectator hex and the owner's own landing agree. + owner_seated = owner_draw is not None # Multi-seat hex (2026-05-29): every present member shows on the ring — # owner 1C + present invitees 2C-6C by deposit order (the same seats # everyone sees), built by the shared `_my_sea_seats` helper that the live diff --git a/src/functional_tests/test_game_my_sea.py b/src/functional_tests/test_game_my_sea.py index 8ced546..4768a0a 100644 --- a/src/functional_tests/test_game_my_sea.py +++ b/src/functional_tests/test_game_my_sea.py @@ -1744,9 +1744,11 @@ class MySeaNavbarGateViewTest(FunctionalTest): class MySeaSeatOnePersistenceTest(FunctionalTest): - """Sprint 6 iter 6b — seat 1 (the owner's reserved chair) renders - `.seated` whenever the user's hand is non-empty (mid-draw or - complete). DEL empties the hand → seat 1 reverts to `.fa-ban`.""" + """Sprint 6 iter 6b (window rule 2026-05-31) — seat 1 (the owner's + reserved chair) renders `.seated` for the full 24h window after her most + recent draw, i.e. as long as a `MySeaDraw` row exists. DEL empties the + hand but keeps the row → seat 1 STAYS seated; only a fresh user with no + row (or a fully-expired window) shows `.fa-ban`.""" def setUp(self): super().setUp() @@ -1767,10 +1769,10 @@ class MySeaSeatOnePersistenceTest(FunctionalTest): self.assertNotIn("seated", seat1.get_attribute("class")) seat1.find_element(By.CSS_SELECTOR, ".fa-ban") - def test_seat_1_banned_when_active_draw_has_empty_hand(self): - """DEL leaves the row but wipes the hand; seat 1 reverts to - banned (per user spec 2026-05-20: seat 1 tied to hand non-empty, - NOT to active_draw existence).""" + def test_seat_1_seated_when_active_draw_has_empty_hand(self): + """DEL leaves the row but wipes the hand; seat 1 STAYS seated for + the rest of the 24h window (user-spec 2026-05-31: seating tied to + `active_draw` row existence, NOT to the hand being non-empty).""" from apps.gameboard.models import MySeaDraw MySeaDraw.objects.create( user=self.gamer, spread="situation-action-outcome", @@ -1783,7 +1785,8 @@ class MySeaSeatOnePersistenceTest(FunctionalTest): By.CSS_SELECTOR, ".table-seat[data-slot='1']" ) ) - self.assertNotIn("seated", seat1.get_attribute("class")) + self.assertIn("seated", seat1.get_attribute("class")) + seat1.find_element(By.CSS_SELECTOR, ".fa-circle-check") class MySeaBudBtnInviteTest(FunctionalTest):