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:
Disco DeDisco
2026-05-31 22:45:21 -04:00
parent 86a349b64e
commit 1e70ffabd6
4 changed files with 60 additions and 43 deletions

View File

@@ -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"])

View File

@@ -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) # 2C6C 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

View File

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

View File

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