my-sea voice: cap visitors at 5 (6 seats) + free a seat on leave — TDD
Phase 5a of the my-sea voice batch (user-spec 2026-05-29). The owner holds 1C; at most 5 visitors fill 2C–6C, which also caps the voice mesh (voice requires a deposited seat, so seat-capping caps membership). - SeaInvite: MY_SEA_MAX_VISITORS=5 + present_count(owner) / table_has_room(owner) classmethods (present = ACCEPTED + deposited + not LEFT). - my_sea_visit_insert_token: a fresh deposit into a full table is bounced (?full=1, no token spent, no seat); a visitor who BYEs frees their seat (is_present → False) for the next visitor. - my_sea_visit_gate: context → the gate shows 'TABLE FULL' + inert rails instead of INSERT TOKEN for a not-yet-present visitor. - 6 capacity ITs (count/room, full-table bounce, leave-frees-seat, gate flag, already-seated not blocked). 291 gameboard ITs green. Remaining Phase 5 (live-verify / needs a spec call): disconnect visuals (--priRd/.fa-ban, item 7) + the true Web-Audio equalizer (item 5) + consumer- level voice-member enforcement + multi-seat (3C–6C) spectator viz. 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:
@@ -940,11 +940,15 @@ def my_sea_visit_gate(request, owner_id):
|
||||
voice window. Reuses my_sea_gate.html with spectator=True (INSERT TOKEN
|
||||
only; no refund / PAID-DRAW two-step)."""
|
||||
from apps.lyric.models import User
|
||||
from .models import SeaInvite
|
||||
owner = get_object_or_404(User, id=owner_id)
|
||||
invite = _accepted_visit_invite(owner, request.user)
|
||||
if invite is None:
|
||||
return HttpResponseForbidden()
|
||||
sig_card, sig_reversed = _resolve_sig(owner, active_draw_for(owner))
|
||||
# Seat cap — a not-yet-present visitor can't deposit into a full table
|
||||
# (owner + 5). The gate renders a "table full" notice instead of the rails.
|
||||
table_full = not invite.is_present and not SeaInvite.table_has_room(owner)
|
||||
return render(request, "apps/gameboard/my_sea_gate.html", {
|
||||
"spectator": True,
|
||||
"owner": owner,
|
||||
@@ -957,6 +961,7 @@ def my_sea_visit_gate(request, owner_id):
|
||||
# PAID DRAW blocks (gated on `deposit_reserved`) never render.
|
||||
"deposit_reserved": False,
|
||||
"hand_non_empty": False,
|
||||
"table_full": table_full,
|
||||
"page_class": "page-gameboard page-my-sea page-my-sea-gate page-my-sea-visit-gate",
|
||||
})
|
||||
|
||||
@@ -969,12 +974,21 @@ def my_sea_visit_insert_token(request, owner_id):
|
||||
`token_deposited_at` + a 24h `voice_until` on the SeaInvite (NOT on the
|
||||
owner's MySeaDraw), marking seat 2C present + opening the voice window."""
|
||||
from datetime import timedelta
|
||||
from django.urls import reverse
|
||||
from apps.lyric.models import User
|
||||
from .models import SeaInvite
|
||||
owner = get_object_or_404(User, id=owner_id)
|
||||
invite = _accepted_visit_invite(owner, request.user)
|
||||
if invite is None:
|
||||
return HttpResponseForbidden()
|
||||
if invite.token_deposited_at is None:
|
||||
# Seat cap (user-spec 2026-05-29): owner (1C) + up to 5 visitors. A
|
||||
# fresh deposit can only take a seat while one is free; a full table
|
||||
# bounces back w. ?full=1 so the gate can say so. A visitor who LEFT
|
||||
# frees their seat for someone else (is_present → False).
|
||||
if not SeaInvite.table_has_room(owner):
|
||||
return redirect(
|
||||
reverse("my_sea_visit", args=[owner.id]) + "?full=1")
|
||||
token = _select_my_sea_token(request.user)
|
||||
if token is not None:
|
||||
debit_my_sea_token(request.user, token)
|
||||
|
||||
Reference in New Issue
Block a user