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):
|
class MySeaVisitOwnerSeatedTest(TestCase):
|
||||||
"""Phase 2 — the owner shows seated in 1C on the spectator hex whenever
|
"""Phase 2 (window rule 2026-05-31) — the owner shows seated in 1C on the
|
||||||
she's committed to a draw cycle (drawn OR paid), not only once a card
|
spectator hex for the FULL 24h window after her most recent free/paid
|
||||||
lands. Previously a paid-but-undrawn owner read as the empty .fa-ban
|
draw, i.e. as long as `active_draw_for` returns a row. Seating tracks the
|
||||||
default on the visitor's view."""
|
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):
|
def setUp(self):
|
||||||
self.owner = _owner_with_sig()
|
self.owner = _owner_with_sig()
|
||||||
@@ -336,6 +337,15 @@ class MySeaVisitOwnerSeatedTest(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(self.client.get(self.url).context["seat1_present"])
|
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):
|
def test_owner_not_seated_without_draw_or_payment(self):
|
||||||
self.assertFalse(self.client.get(self.url).context["seat1_present"])
|
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="position-status-icon fa-solid fa-ban"'), 6)
|
||||||
self.assertEqual(html.count('class="seat-position-label"'), 6)
|
self.assertEqual(html.count('class="seat-position-label"'), 6)
|
||||||
|
|
||||||
# ── Owner 1C seating (Phase 2, 2026-05-29) ───────────────────────────
|
# ── Owner 1C seating (Phase 2, 2026-05-29; window rule 2026-05-31) ────
|
||||||
# The owner is "seated" in 1C whenever she's committed to a draw cycle —
|
# The owner is "seated" in 1C for the FULL 24h window after her most
|
||||||
# paid for one OR partially/completely drawn (w/o DEL). Previously only a
|
# recent free/paid draw — i.e. as long as `active_draw_for` returns a
|
||||||
# non-empty hand seated 1C, so a paid-but-undrawn owner (the PAID DRAW
|
# row. Seating tracks the row's existence, NOT the hand/deposit/paid
|
||||||
# landing) showed the semi-opaque .fa-ban default. Seat 1C now carries
|
# state (user-spec 2026-05-31): a DEL'd row (empty hand, no paid credit)
|
||||||
# `.seated` + `.fa-circle-check` so it persists on refresh (sync).
|
# 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"'
|
_SEATED_1C = 'class="table-seat seated" data-slot="1"'
|
||||||
_STATUS_CHECK = 'class="position-status-icon fa-solid fa-circle-check"'
|
_STATUS_CHECK = 'class="position-status-icon fa-solid fa-circle-check"'
|
||||||
_STATUS_BAN = 'class="position-status-icon fa-solid fa-ban"'
|
_STATUS_BAN = 'class="position-status-icon fa-solid fa-ban"'
|
||||||
@@ -1392,15 +1393,24 @@ class MySeaDrawSeaLandingViewTest(TestCase):
|
|||||||
self.assertIn(self._SEATED_1C, html)
|
self.assertIn(self._SEATED_1C, html)
|
||||||
self.assertEqual(html.count(self._STATUS_CHECK), 1)
|
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
|
# 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=[])
|
self._make_draw(hand=[])
|
||||||
html = self.client.get(reverse("my_sea")).content.decode()
|
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.assertIn("GATE", html)
|
||||||
self.assertEqual(html.count(self._STATUS_CHECK), 0)
|
self.assertEqual(html.count(self._STATUS_CHECK), 1) # 1C seated
|
||||||
self.assertEqual(html.count(self._STATUS_BAN), 6)
|
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):
|
def test_landing_not_rendered_when_user_has_no_sig(self):
|
||||||
# Sprint 4b gate still wins precedence — FREE DRAW must not render
|
# 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,
|
"saved_by_position": saved_by_position,
|
||||||
"next_free_draw_at": next_free_draw_at,
|
"next_free_draw_at": next_free_draw_at,
|
||||||
"hand_complete": hand_complete,
|
"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
|
# 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 — 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),
|
"seats": _my_sea_seats(request.user),
|
||||||
"show_picker": show_picker,
|
"show_picker": show_picker,
|
||||||
"show_cont_draw": show_cont_draw,
|
"show_cont_draw": show_cont_draw,
|
||||||
@@ -507,11 +504,12 @@ def _my_sea_seats(owner):
|
|||||||
seat broadcast (`sea_seats`)."""
|
seat broadcast (`sea_seats`)."""
|
||||||
from .models import SeaInvite, MY_SEA_MAX_VISITORS
|
from .models import SeaInvite, MY_SEA_MAX_VISITORS
|
||||||
owner_draw = active_draw_for(owner)
|
owner_draw = active_draw_for(owner)
|
||||||
owner_seated = owner_draw is not None and (
|
# Seated in 1C for the FULL 24h window — as long as a draw row exists
|
||||||
bool(owner_draw.hand)
|
# (free or paid; PAID DRAW resets created_at so the window runs from the
|
||||||
or owner_draw.deposit_token_id is not None
|
# later of the two). Decoupled from hand/deposit/paid state per user-spec
|
||||||
or owner_draw.paid_through_at is not None
|
# 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}"
|
owner_token = (f"owner-{owner.id}-{owner_draw.id}"
|
||||||
if owner_draw is not None else "")
|
if owner_draw is not None else "")
|
||||||
seats = [{"n": 1, "label": "1C", "present": owner_seated, "token": owner_token}]
|
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)
|
owner_draw = active_draw_for(owner)
|
||||||
sig_card, sig_reversed = _resolve_sig(owner, owner_draw)
|
sig_card, sig_reversed = _resolve_sig(owner, owner_draw)
|
||||||
owner_hand_non_empty = owner_draw is not None and bool(owner_draw.hand)
|
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
|
# Owner seated in 1C (as the visitor sees it) for the FULL 24h window —
|
||||||
# draw cycle — drawn OR paid (deposit reserved / paid-through credit). Keeps
|
# as long as a draw row exists (user-spec 2026-05-31), not only once a
|
||||||
# the owner's 1C chair persistently `.seated` on the spectator hex even
|
# card lands or a deposit/paid credit is set. Mirrors `_my_sea_seats`'
|
||||||
# before a card lands (user-spec 2026-05-29); mirrors my_sea's seat1_seated.
|
# `owner_seated` so the spectator hex and the owner's own landing agree.
|
||||||
owner_paid = owner_draw is not None and (
|
owner_seated = owner_draw is not None
|
||||||
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
|
|
||||||
# Multi-seat hex (2026-05-29): every present member shows on the ring —
|
# 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
|
# 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
|
# everyone sees), built by the shared `_my_sea_seats` helper that the live
|
||||||
|
|||||||
@@ -1744,9 +1744,11 @@ class MySeaNavbarGateViewTest(FunctionalTest):
|
|||||||
|
|
||||||
|
|
||||||
class MySeaSeatOnePersistenceTest(FunctionalTest):
|
class MySeaSeatOnePersistenceTest(FunctionalTest):
|
||||||
"""Sprint 6 iter 6b — seat 1 (the owner's reserved chair) renders
|
"""Sprint 6 iter 6b (window rule 2026-05-31) — seat 1 (the owner's
|
||||||
`.seated` whenever the user's hand is non-empty (mid-draw or
|
reserved chair) renders `.seated` for the full 24h window after her most
|
||||||
complete). DEL empties the hand → seat 1 reverts to `.fa-ban`."""
|
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):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@@ -1767,10 +1769,10 @@ class MySeaSeatOnePersistenceTest(FunctionalTest):
|
|||||||
self.assertNotIn("seated", seat1.get_attribute("class"))
|
self.assertNotIn("seated", seat1.get_attribute("class"))
|
||||||
seat1.find_element(By.CSS_SELECTOR, ".fa-ban")
|
seat1.find_element(By.CSS_SELECTOR, ".fa-ban")
|
||||||
|
|
||||||
def test_seat_1_banned_when_active_draw_has_empty_hand(self):
|
def test_seat_1_seated_when_active_draw_has_empty_hand(self):
|
||||||
"""DEL leaves the row but wipes the hand; seat 1 reverts to
|
"""DEL leaves the row but wipes the hand; seat 1 STAYS seated for
|
||||||
banned (per user spec 2026-05-20: seat 1 tied to hand non-empty,
|
the rest of the 24h window (user-spec 2026-05-31: seating tied to
|
||||||
NOT to active_draw existence)."""
|
`active_draw` row existence, NOT to the hand being non-empty)."""
|
||||||
from apps.gameboard.models import MySeaDraw
|
from apps.gameboard.models import MySeaDraw
|
||||||
MySeaDraw.objects.create(
|
MySeaDraw.objects.create(
|
||||||
user=self.gamer, spread="situation-action-outcome",
|
user=self.gamer, spread="situation-action-outcome",
|
||||||
@@ -1783,7 +1785,8 @@ class MySeaSeatOnePersistenceTest(FunctionalTest):
|
|||||||
By.CSS_SELECTOR, ".table-seat[data-slot='1']"
|
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):
|
class MySeaBudBtnInviteTest(FunctionalTest):
|
||||||
|
|||||||
Reference in New Issue
Block a user