slot/token tooltips: 'expires <relative>' (lowercase, .row-ts timescale) + per-slot token_cost on GateSlot + '+ <Token>' deposited-token list — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

Three pieces of housekeeping on the token tooltips:

1. Expiry format. relative_ts is now distance-based (abs gap from now), so it formats FUTURE expiries with the same timescale rules it already uses for past .row-ts timestamps (<24h time, <7d weekday, <1y 'dd Mon', else +year) — past behaviour unchanged (abs is a no-op for past). The FREE-token wallet tooltip (Token.tooltip_expiry) and the position-circle .tt-expiry both read 'expires <when>' (lowercase, no majuscule); the position tooltip is server-formatted via the filter (JS just copies the attr — no JS date logic).

2. Per-slot token count moved off the user's CARTE token onto the slot model. New GateSlot.token_cost (PositiveSmallIntegerField default 1) — the per-seat expenditure count. _gate_positions reads slot.token_cost instead of the CARTE Token.slots_claimed high-water mark, which wrongly showed '6' on every CARTE-covered seat. Every slot now reads 1 (a CARTE covers each seat at cost 1, like any token); the field only rises above 1 when the rising-game-cost feature lands.

3. Per-slot deposited-token list. Under the '<n> Token(s) deposited' header the tooltip now lists a '+ <Token name>' bulleted <ul> — one entry today (a slot ejects its token on any re-deposit, so combinations aren't yet possible). Derived from the slot's debited_token_type (e.g. 'carte' -> 'Carte Blanche', 'Free' -> 'Free Token'); a CARTE across all six seats shows '+ Carte Blanche' on each. token_types is a list, future-ready for token combinations + elevated per-slot cost.

Rising-game-cost is NOT built (recon-confirmed), so the per-slot count is always 1 and the 2-token-slot FT is intentionally skipped per user.

Tests: relative_ts future-date unit tests; FreeTokenTooltipTest rewritten for the relative format (real datetime, no MagicMock/strftime); wallet FT + the two CARTE token-count tests updated to per-slot semantics (1 + 'Carte Blanche'); FREE-slot IT asserts the token-list + 'expires '. Full suite 1606 green; 11 position-tooltip FTs + wallet tooltip FT green.

[[project-position-circle-tooltips]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-01 17:39:15 -04:00
parent cbc4f4f323
commit 5229b9f96a
14 changed files with 128 additions and 37 deletions

View File

@@ -228,13 +228,9 @@ def _gate_positions(room, user=None, current_slot=None):
bn.bud_id: bn.shoptalk
for bn in BudshipNote.objects.filter(user=user)
}
# One CARTE-token-per-gamer map, hoisted out of the loop — a CARTE gamer
# owns ONE token (carrying slots_claimed) but claims many slots, so the
# per-slot lookup was up to 6 identical queries.
carte_claims = {
t.user_id: t.slots_claimed
for t in Token.objects.filter(token_type=Token.CARTE, current_room=room)
}
# value → display ("carte" → "Carte Blanche", "Free" → "Free Token") for
# the per-slot deposited-token `<ul>`.
token_display = dict(Token.TOKEN_TYPE_CHOICES)
positions = []
for slot in room.gate_slots.select_related("gamer").order_by("slot_number"):
gamer = slot.gamer
@@ -253,11 +249,14 @@ def _gate_positions(room, user=None, current_slot=None):
state_class = "tt-pos-gamer tt-pos-bud"
else:
state_class = "tt-pos-gamer"
# Deposited-token count — CARTE claims many slots on one token.
if gamer is not None and slot.debited_token_type == Token.CARTE:
tokens = carte_claims.get(gamer.id, 1)
else:
tokens = 1
# Ordered display names of the token(s) deposited in THIS slot — one
# today (each slot costs exactly 1 token; a CARTE covers each seat at
# cost 1, so a CARTE seat reads "Carte Blanche"). Becomes a multi-entry
# list when the rising-game-cost feature lands.
token_types = (
[token_display.get(slot.debited_token_type, slot.debited_token_type)]
if gamer is not None and slot.debited_token_type else []
)
sig = seat.significator if seat else None
positions.append({
"slot": slot,
@@ -266,7 +265,9 @@ def _gate_positions(room, user=None, current_slot=None):
"state_class": state_class,
"is_me_also": is_me_also,
"shoptalk": shoptalk_map.get(gamer.id, "") if is_bud else "",
"tokens": tokens,
# Per-slot expenditure count (GateSlot.token_cost) — 1 normally.
"tokens": slot.token_cost,
"token_types": token_types,
"expiry": slot.cost_current_until,
"sign_rank": sig.corner_rank if sig else "",
"sign_suit_icon": sig.suit_icon if sig else "",