my-sea seating: owner stays seated the full 24h window (row-exists), not just while a hand is down — drop dead seat1_seated — TDD
Phase 1 of the room GATE VIEW + seat-renewal sprint. Decouples 1C seating from hand/deposit/paid state: the owner is seated as long as `active_draw_for` returns a row, i.e. for the full 24h after her most recent FREE or PAID draw (PAID DRAW resets created_at, so the window runs from the later of the two). A DEL'd row (empty hand, no paid credit) now keeps 1C seated until the row expires at 24h — previously 1C dropped to .fa-ban the instant she DEL'd, even mid-window. - `_my_sea_seats` `owner_seated` → `owner_draw is not None` (drives the owner's own landing hex + the live `sea_seats` broadcast). - `my_sea_visit` `owner_seated` → same rule, so the spectator hex and the owner's landing agree (was drawn-OR-paid, dropped `owner_paid`). - DRY: removed the dead `seat1_seated` context (the `seats` ring's `seat.present` has driven 1C since the multi-seat hex landed; the flag was never read by the template). Tests — TDD red→green: - flipped `test_seat_1c_not_seated_at_gate_view_after_del` → `..._seated_...`: DEL'd empty-hand row keeps 1C seated, center still GATE VIEW (1 check / 5 ban). - flipped FT `test_seat_1_banned_when_active_draw_has_empty_hand` → `..._seated_...` (asserts .fa-circle-check). - added `test_seat1_seated_context_key_removed` (DRY regression guard). - added spectator `test_owner_seated_with_empty_hand_no_payment`. - `test_owner_not_seated_without_draw_or_payment` unchanged (no row → still unseated). 318 gameboard ITs green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user