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

@@ -690,7 +690,10 @@ class PositionTooltipRenderTest(TestCase):
def test_deposit_count_and_expiry_present(self):
slot2 = _circle_tag(self._gate_content(), 2)
self.assertIn('data-tt-tokens="1"', slot2)
self.assertIn("data-tt-expiry=", slot2)
# FREE-funded slot → the deposited-token list reads "Free Token".
self.assertIn('data-tt-token-types="Free Token"', slot2)
# Lowercase "expires <when>" (relative timescale), not an ISO/locale date.
self.assertIn('data-tt-expiry="expires ', slot2)
def test_seat_significator_rank_rides_the_circle(self):
sig = TarotCard.objects.create(
@@ -741,9 +744,18 @@ class PositionTooltipCarteRenderTest(TestCase):
self.client.force_login(self.viewer)
self.room_url = reverse("epic:room", kwargs={"room_id": self.room.id})
def test_tokens_reflects_carte_slots_claimed(self):
def test_each_carte_slot_costs_one_token(self):
# A CARTE covers each seat at cost 1 — the per-slot expenditure count
# is 1 (GateSlot.token_cost), NOT the CARTE slots_claimed high-water
# mark. The deposited-token list reads "Carte Blanche".
content = self.client.get(self.room_url).content.decode()
self.assertIn('data-tt-tokens="6"', _circle_tag(content, 1))
# One CARTE covers ALL six seats — every slot reads cost 1 + "Carte
# Blanche" (token combinations per slot aren't possible: a re-deposit
# ejects the slot's token), so there is no mixed CARTE+FREE spread.
for n in (1, 4, 6):
tag = _circle_tag(content, n)
self.assertIn('data-tt-tokens="1"', tag)
self.assertIn('data-tt-token-types="Carte Blanche"', tag)
def test_own_other_seat_is_me_also_with_switch_href(self):
content = self.client.get(self.room_url).content.decode()