room gate-view: mid-game renewal area (room_gate) + renew_token endpoint + _room_gear nvm_url — TDD
Phase 4 of the room GATE VIEW + seat-renewal sprint. The 3rd-person mirror of my_sea_gate: a gate-view a seated gamer can open at any time to check token TIME REMAINING or RENEW, reachable even mid-game (the gatekeeper redirects to the table once table_status is set — this view does not). - `room_gate` view + `room/<uuid>/gate/view/` URL — renders the viewer's own seat/position circle, a live time-remaining ticker (counts down to cost_current_until, then to grace_expires_at in renewal grace), and a RENEW affordance. page_class carries `page-room` (drives the navbar GATE VIEW in Phase 0). No seat → "no seat" copy, no RENEW btn. - `renew_token` view + `room/<uuid>/gate/renew` URL — re-deposits a token into the viewer's already-FILLED slot via the existing `debit_token` (resets filled_at=now → restarts the cost-current window). Reuses select_token / debit_token wholesale; distinct from confirm_token, which needs a RESERVED slot. 402 when token-depleted; no-op redirect when the user holds no filled slot (already auto-BYE'd). - `room_gate.html` — reuses the gatekeeper's .gate-overlay/.gate-modal chrome (hand-rolled like my_sea_gate, inner content differs) + an inline countdown ticker mirroring the status-dots IIFE. - DRY: `_room_gear.html` now takes an `nvm_url` param (default the gameboard listing — room.html's own gear unchanged); the gate-view passes the table-hex URL so NVM returns to the hex, mirroring _my_sea_gear's contract. Tests: RoomGateViewTest (7) + RoomRenewTokenTest (6) — renders mid-game, own seat circle, data-cost-until, RENEW posts to renew_token, NVM→hex, page-room marker, no-seat render; renew resets filled_at + consumes FREE + records SLOT_FILLED, no-slot/GET redirects, 402 when depleted. 504 epic tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -456,6 +456,62 @@ def room_view(request, room_id):
|
||||
return render(request, "apps/gameboard/room.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def room_gate(request, room_id):
|
||||
"""Room renewal gate-view — reachable mid-game (unlike `gatekeeper`,
|
||||
which redirects to the table once `table_status` is set). GATE VIEW
|
||||
(navbar + center supersession) routes here. Shows the viewer's own
|
||||
seat/position circle, their token time-remaining, and a RENEW
|
||||
affordance; the gear-menu NVM returns to the table hex, not /gameboard/.
|
||||
Mirrors the my-sea gate-view (`my_sea_gate`) for the 3rd-person table."""
|
||||
room = Room.objects.get(id=room_id)
|
||||
user_slot = room.gate_slots.filter(
|
||||
gamer=request.user, status=GateSlot.FILLED
|
||||
).first()
|
||||
return render(request, "apps/gameboard/room_gate.html", {
|
||||
"room": room,
|
||||
"user_filled_slot": user_slot,
|
||||
"cost_current": user_slot.cost_current if user_slot else True,
|
||||
"cost_current_until": user_slot.cost_current_until if user_slot else None,
|
||||
"grace_expires_at": user_slot.grace_expires_at if user_slot else None,
|
||||
"in_renewal_grace": user_slot.in_renewal_grace if user_slot else False,
|
||||
"slot_role_label": (
|
||||
SLOT_ROLE_LABELS.get(user_slot.slot_number, "") if user_slot else ""
|
||||
),
|
||||
"page_class": "page-gameboard page-room page-room-gate",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def renew_token(request, room_id):
|
||||
"""Renew the viewer's seat — re-deposit a token into their already-FILLED
|
||||
slot, resetting `filled_at=now` (via `debit_token`) so the cost-current
|
||||
window restarts. Distinct from `confirm_token` (which transitions a
|
||||
RESERVED slot); renewal is a FILLED→FILLED refresh of an occupied seat.
|
||||
402 when the user is token-depleted; no-op redirect when the user holds
|
||||
no filled slot (e.g. already auto-BYE'd out of the room)."""
|
||||
if request.method != "POST":
|
||||
return redirect("epic:room_gate", room_id=room_id)
|
||||
room = Room.objects.get(id=room_id)
|
||||
slot = room.gate_slots.filter(
|
||||
gamer=request.user, status=GateSlot.FILLED
|
||||
).first()
|
||||
if slot is None:
|
||||
return redirect("epic:room_gate", room_id=room_id)
|
||||
token_id = request.POST.get("token_id")
|
||||
token = (request.user.tokens.filter(id=token_id).first()
|
||||
if token_id else select_token(request.user))
|
||||
if token is None:
|
||||
return HttpResponse(status=402)
|
||||
debit_token(request.user, slot, token) # resets filled_at=now → A=now
|
||||
record(room, GameEvent.SLOT_FILLED, actor=request.user,
|
||||
slot_number=slot.slot_number, token_type=token.token_type,
|
||||
token_display=token.get_token_type_display(),
|
||||
renewal_days=(room.renewal_period.days if room.renewal_period else 7))
|
||||
_notify_gate_update(room_id)
|
||||
return redirect("epic:room_gate", room_id=room_id)
|
||||
|
||||
|
||||
@login_required
|
||||
def drop_token(request, room_id):
|
||||
if request.method == "POST":
|
||||
|
||||
Reference in New Issue
Block a user