From 2cbc1bf29226097c93466dac1d12c57ffb5f12d0 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Fri, 29 May 2026 22:01:23 -0400 Subject: [PATCH] =?UTF-8?q?my-sea=20spectator:=20render=20all=20present=20?= =?UTF-8?q?members=20on=20the=20hex=20(2C-6C),=20not=20just=20the=20viewer?= =?UTF-8?q?=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spectator hex showed only owner 1C + the viewer in 2C; other present visitors were invisible. The view now builds a list — owner 1C + each present invitee in 2C-6C by deposit order (capped at MY_SEA_MAX_VISITORS) — so every viewer sees the same absolute seating, with their own seat marked .table-seat--self (a subtle --terUser tint). - my_sea_visit: context (present/empty + token + label + is_self). - my_sea_visit.html: seat ring loops instead of a hardcoded 1C/2C. - _room.scss: .table-seat--self chair tint. - +1 IT (3 present visitors → 2C-4C seated, viewer is the --self one); the both-seated IT updated for the --self marker. 292 gameboard ITs green. Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tests/integrated/test_sea_visit.py | 25 +++++++++++++++++-- src/apps/gameboard/views.py | 25 ++++++++++++++++++- src/static_src/scss/_room.scss | 5 ++++ .../apps/gameboard/my_sea_visit.html | 24 ++++++++---------- 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/apps/gameboard/tests/integrated/test_sea_visit.py b/src/apps/gameboard/tests/integrated/test_sea_visit.py index c014311..35880b3 100644 --- a/src/apps/gameboard/tests/integrated/test_sea_visit.py +++ b/src/apps/gameboard/tests/integrated/test_sea_visit.py @@ -164,8 +164,9 @@ class MySeaVisitContextTest(TestCase): self.assertTrue(ctx["seat1_present"]) # owner drawn self.assertTrue(ctx["seat2_present"]) # visitor present html = self.client.get(self.url).content.decode() - self.assertIn('class="table-seat seated" data-slot="1"', html) - self.assertIn('class="table-seat seated" data-slot="2"', html) + self.assertIn('class="table-seat seated" data-slot="1"', html) # owner + # The viewer's own seat (2C) carries the --self marker. + self.assertIn('table-seat--self" data-slot="2"', html) self.assertEqual( html.count('class="position-status-icon fa-solid fa-circle-check"'), 2) @@ -335,6 +336,26 @@ class MySeaVisitCapacityTest(TestCase): reverse("my_sea_visit_gate", args=[self.owner.id])).context self.assertTrue(ctx["table_full"]) + def test_other_present_visitors_render_in_3c_onward(self): + # The viewer + 2 other present visitors → owner 1C, viewer 2C, + # the two others 3C/4C; the viewer's seat is the --self one. + viewer, vinv = self._accepted_invitee(0, present=True) + self._accepted_invitee(1, present=True) + self._accepted_invitee(2, present=True) + self.client.force_login(viewer) + resp = self.client.get(reverse("my_sea_visit", args=[self.owner.id])) + seats = resp.context["seats"] + # Owner has no draw here (1C empty); 3 present visitors fill 2C–4C. + self.assertEqual(sum(1 for s in seats if s["present"]), 3) + # Visitors fill 2C, 3C, 4C; exactly one is the viewer (is_self). + selves = [s for s in seats if s.get("is_self")] + self.assertEqual(len(selves), 1) + html = resp.content.decode() + self.assertIn('data-slot="3"', html) + self.assertIn("table-seat--self", html) + # Owner isn't seated (no draw) in this fixture → 1C is the empty seat. + self.assertFalse(seats[0]["present"]) + def test_already_present_visitor_is_not_blocked_by_a_full_table(self): # The 5th present visitor re-POSTing insert is a no-op (already seated), # never a "full" bounce. diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index e9457e6..e674fe5 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -867,7 +867,7 @@ def my_sea_visit(request, owner_id): sea-btn cascade (+ the @mailman post-attribution anchor) both land here, and the click IS the acceptance. A stranger with no invite still 403s.""" from apps.lyric.models import User - from .models import SeaInvite + from .models import SeaInvite, MY_SEA_MAX_VISITORS owner = get_object_or_404(User, id=owner_id) if owner == request.user: return redirect("my_sea") @@ -895,6 +895,28 @@ def my_sea_visit(request, owner_id): 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 — + # owner in 1C, then each present invitee in 2C–6C by deposit order (the + # same seats everyone sees). `seats` drives the table-seat loop; an empty + # seat renders the .fa-ban default. Capped at MY_SEA_MAX_VISITORS visitors. + 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}] + present_invitees = ( + SeaInvite.objects + .filter(owner=owner, status=SeaInvite.ACCEPTED, + token_deposited_at__isnull=False, left_at__isnull=True) + .order_by("token_deposited_at")[:MY_SEA_MAX_VISITORS] + ) + for idx, inv in enumerate(present_invitees): + seats.append({ + "n": idx + 2, "label": f"{idx + 2}C", "present": True, + "token": f"visit-{inv.id}", + "is_self": inv.invitee_id == request.user.id, + }) + while len(seats) < 6: + n = len(seats) + 1 + seats.append({"n": n, "label": f"{n}C", "present": False, "token": ""}) # Read-only spectator render parity (Phase 1, 2026-05-29): the visitor's # VIEW DRAW renders the SAME `.my-sea-cross` picker + `_sea_stage` the # owner sees, populated from the owner's draw. `saved_by_position` fills @@ -910,6 +932,7 @@ def my_sea_visit(request, owner_id): "sea_invite": invite, "seat1_present": owner_seated, "seat2_present": invite.is_present, + "seats": seats, "owner_draw_id": owner_draw.id if owner_draw is not None else "", "voice_active": invite.voice_active, "voice_room_id": f"mysea-{owner.id}", diff --git a/src/static_src/scss/_room.scss b/src/static_src/scss/_room.scss index 1186566..a69f61c 100644 --- a/src/static_src/scss/_room.scss +++ b/src/static_src/scss/_room.scss @@ -640,6 +640,11 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut &.seat-just-seated .fa-chair { animation: my-sea-seat-flare 2s ease forwards; } + // The viewer's own occupied seat on the multi-seat spectator hex — a + // subtle --terUser tint so they can pick themselves out of 2C–6C. + &.table-seat--self .fa-chair { + color: rgba(var(--terUser), 1); + } .seat-portrait { width: 36px; diff --git a/src/templates/apps/gameboard/my_sea_visit.html b/src/templates/apps/gameboard/my_sea_visit.html index c84e7bf..0b8c70a 100644 --- a/src/templates/apps/gameboard/my_sea_visit.html +++ b/src/templates/apps/gameboard/my_sea_visit.html @@ -38,25 +38,21 @@ - {% for n in "123456" %} - {% if n == '1' and seat1_present %} -
+ {# Every present member shows on the ring — owner 1C + #} + {# present invitees 2C–6C by deposit order (the viewer's #} + {# own seat carries `--self`). Built server-side as #} + {# `seats` so all viewers see identical absolute seating. #} + {% for seat in seats %} + {% if seat.present %} +
- 1C - -
- {% elif n == '2' and seat2_present %} -
- - 2C + {{ seat.label }}
{% else %} -
+
- {{ n }}C + {{ seat.label }}
{% endif %}