my-sea spectator: render all present members on the hex (2C-6C), not just the viewer — TDD
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 <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -164,8 +164,9 @@ class MySeaVisitContextTest(TestCase):
|
|||||||
self.assertTrue(ctx["seat1_present"]) # owner drawn
|
self.assertTrue(ctx["seat1_present"]) # owner drawn
|
||||||
self.assertTrue(ctx["seat2_present"]) # visitor present
|
self.assertTrue(ctx["seat2_present"]) # visitor present
|
||||||
html = self.client.get(self.url).content.decode()
|
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="1"', html) # owner
|
||||||
self.assertIn('class="table-seat seated" data-slot="2"', html)
|
# The viewer's own seat (2C) carries the --self marker.
|
||||||
|
self.assertIn('table-seat--self" data-slot="2"', html)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
html.count('class="position-status-icon fa-solid fa-circle-check"'), 2)
|
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
|
reverse("my_sea_visit_gate", args=[self.owner.id])).context
|
||||||
self.assertTrue(ctx["table_full"])
|
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):
|
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),
|
# The 5th present visitor re-POSTing insert is a no-op (already seated),
|
||||||
# never a "full" bounce.
|
# never a "full" bounce.
|
||||||
|
|||||||
@@ -867,7 +867,7 @@ def my_sea_visit(request, owner_id):
|
|||||||
sea-btn cascade (+ the @mailman post-attribution anchor) both land here,
|
sea-btn cascade (+ the @mailman post-attribution anchor) both land here,
|
||||||
and the click IS the acceptance. A stranger with no invite still 403s."""
|
and the click IS the acceptance. A stranger with no invite still 403s."""
|
||||||
from apps.lyric.models import User
|
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)
|
owner = get_object_or_404(User, id=owner_id)
|
||||||
if owner == request.user:
|
if owner == request.user:
|
||||||
return redirect("my_sea")
|
return redirect("my_sea")
|
||||||
@@ -895,6 +895,28 @@ def my_sea_visit(request, owner_id):
|
|||||||
or owner_draw.paid_through_at is not None
|
or owner_draw.paid_through_at is not None
|
||||||
)
|
)
|
||||||
owner_seated = owner_hand_non_empty or owner_paid
|
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
|
# 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
|
# VIEW DRAW renders the SAME `.my-sea-cross` picker + `_sea_stage` the
|
||||||
# owner sees, populated from the owner's draw. `saved_by_position` fills
|
# 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,
|
"sea_invite": invite,
|
||||||
"seat1_present": owner_seated,
|
"seat1_present": owner_seated,
|
||||||
"seat2_present": invite.is_present,
|
"seat2_present": invite.is_present,
|
||||||
|
"seats": seats,
|
||||||
"owner_draw_id": owner_draw.id if owner_draw is not None else "",
|
"owner_draw_id": owner_draw.id if owner_draw is not None else "",
|
||||||
"voice_active": invite.voice_active,
|
"voice_active": invite.voice_active,
|
||||||
"voice_room_id": f"mysea-{owner.id}",
|
"voice_room_id": f"mysea-{owner.id}",
|
||||||
|
|||||||
@@ -640,6 +640,11 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut
|
|||||||
&.seat-just-seated .fa-chair {
|
&.seat-just-seated .fa-chair {
|
||||||
animation: my-sea-seat-flare 2s ease forwards;
|
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 {
|
.seat-portrait {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
|
|||||||
@@ -38,25 +38,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for n in "123456" %}
|
{# Every present member shows on the ring — owner 1C + #}
|
||||||
{% if n == '1' and seat1_present %}
|
{# present invitees 2C–6C by deposit order (the viewer's #}
|
||||||
<div class="table-seat seated" data-slot="1"
|
{# own seat carries `--self`). Built server-side as #}
|
||||||
data-seat-token="owner-{{ owner.id }}-{{ owner_draw_id }}">
|
{# `seats` so all viewers see identical absolute seating. #}
|
||||||
|
{% for seat in seats %}
|
||||||
|
{% if seat.present %}
|
||||||
|
<div class="table-seat seated{% if seat.is_self %} table-seat--self{% endif %}" data-slot="{{ seat.n }}" data-seat-token="{{ seat.token }}">
|
||||||
<i class="fa-solid fa-chair"></i>
|
<i class="fa-solid fa-chair"></i>
|
||||||
<span class="seat-position-label">1C</span>
|
<span class="seat-position-label">{{ seat.label }}</span>
|
||||||
<i class="position-status-icon fa-solid fa-circle-check"></i>
|
|
||||||
</div>
|
|
||||||
{% elif n == '2' and seat2_present %}
|
|
||||||
<div class="table-seat seated" data-slot="2"
|
|
||||||
data-seat-token="visit-{{ sea_invite.id }}">
|
|
||||||
<i class="fa-solid fa-chair"></i>
|
|
||||||
<span class="seat-position-label">2C</span>
|
|
||||||
<i class="position-status-icon fa-solid fa-circle-check"></i>
|
<i class="position-status-icon fa-solid fa-circle-check"></i>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="table-seat" data-slot="{{ n }}">
|
<div class="table-seat" data-slot="{{ seat.n }}">
|
||||||
<i class="fa-solid fa-chair"></i>
|
<i class="fa-solid fa-chair"></i>
|
||||||
<span class="seat-position-label">{{ n }}C</span>
|
<span class="seat-position-label">{{ seat.label }}</span>
|
||||||
<i class="position-status-icon fa-solid fa-ban"></i>
|
<i class="position-status-icon fa-solid fa-ban"></i>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user