bud page sea-btn cascade: live-invite window + accept-on-GET + glow handoff; my-buds tooltip clamp + row hover/lock — TDD
Fixes the Bill Bud invite cascade so the sea sub-btn actually lights + leads to the bud's my_sea, and gives the My Buds row tooltip viewport clamping + hover/lock styling. SeaInvite.invitee_access_open (gameboard/models.py): new invitee-facing access window — a non-terminal invite (PENDING/ACCEPTED) within 24h of being proffered OR within 24h of the invitee's last gate token deposit. Re-arms on each deposit; DECLINED/LEFT/EXPIRED stay shut. Distinct from is_expired (which only models the PENDING lapse). 8 UTs. bud_page (billboard/views.py): sea_btn_active / sea_first_draw_pending now key on invitee_access_open across PENDING + ACCEPTED, not PENDING-only. Old design darkened the btn the instant the user accepted, so they could never reach the bud's sea from here post-accept — that was the red .fa-ban the user saw. ITs updated: accepted-within-window now lights; added stale-accepted-dark + recent-deposit-relights cases. my_sea_visit (gameboard/views.py): accept-on-GET — a still-pending, non-expired invite from the owner to the visitor is accepted implicitly on arrival (the sea-btn cascade + @mailman post-attribution anchor both land here, so the click IS the acceptance). Previously PENDING → 403, so the cascade dead-ended. ITs: pending-invitee now auto-accepts (200); expired-pending still 403s; stranger still 403s. bud.html: burger → sea_btn glow-handoff machine (the my_sea.html cascade minus the spread-modal stage) so the glow rides the affordance chain to the click target; active sea click clears glow, preserves .active, navigates. my-buds-tooltip.js: clamp the position:fixed #id_tooltip_portal to the viewport on row-lock — same 1rem-inset shape as game-kit.js / sky-wheel.js / wallet.js (measure after .active, clamp left, prefer above / flip below). Reset on clear. _billboard.scss: .bud-entry hover + .row-locked highlight (rows aren't .row-3col so the existing rule missed them) — fill --secUser, flip the --terUser handle to --quiUser, trailing title to readable --priUser. 520 billboard+gameboard ITs/UTs green; affected sea-btn-cascade FT 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:
@@ -1251,10 +1251,13 @@ class BudPageAutoAddOnFirstVisitTest(TestCase):
|
||||
|
||||
|
||||
class BudPagePendingInviteCascadeTest(TestCase):
|
||||
"""`sea_btn_active` + `sea_first_draw_pending` fire iff a non-expired
|
||||
PENDING SeaInvite exists from this bud (owner) to the viewer (invitee).
|
||||
Reuses the same template flags `_burger.html` already reads on my_sea
|
||||
+ room — no new template plumbing on bud.html."""
|
||||
"""`sea_btn_active` + `sea_first_draw_pending` fire iff a *live* SeaInvite
|
||||
exists from this bud (owner) to the viewer (invitee) — non-terminal
|
||||
(PENDING or ACCEPTED) AND inside its 24h-from-proffer window OR within 24h
|
||||
of the viewer's last gate token deposit (user-spec 2026-05-29, via
|
||||
`SeaInvite.invitee_access_open`). Reuses the same template flags
|
||||
`_burger.html` already reads on my_sea + room — no new template plumbing
|
||||
on bud.html."""
|
||||
|
||||
def setUp(self):
|
||||
from apps.gameboard.models import SeaInvite
|
||||
@@ -1286,7 +1289,10 @@ class BudPagePendingInviteCascadeTest(TestCase):
|
||||
self.assertTrue(response.context["sea_btn_active"])
|
||||
self.assertTrue(response.context["sea_first_draw_pending"])
|
||||
|
||||
def test_accepted_invite_does_not_cascade(self):
|
||||
def test_accepted_invite_within_window_lights_cascade(self):
|
||||
# New spec: an ACCEPTED invite still inside its 24h window keeps the
|
||||
# cascade lit (the old design went dark the instant it accepted, so
|
||||
# the user could never reach the bud's sea from here post-accept).
|
||||
self.SeaInvite.objects.create(
|
||||
owner=self.alice,
|
||||
invitee=self.user,
|
||||
@@ -1296,8 +1302,9 @@ class BudPagePendingInviteCascadeTest(TestCase):
|
||||
response = self.client.get(
|
||||
reverse("billboard:bud_page", args=[self.alice.id])
|
||||
)
|
||||
self.assertIsNone(response.context["pending_invite"])
|
||||
self.assertFalse(response.context["sea_btn_active"])
|
||||
self.assertIsNotNone(response.context["pending_invite"])
|
||||
self.assertTrue(response.context["sea_btn_active"])
|
||||
self.assertTrue(response.context["sea_first_draw_pending"])
|
||||
|
||||
def test_expired_pending_invite_does_not_cascade(self):
|
||||
inv = self.SeaInvite.objects.create(
|
||||
@@ -1315,6 +1322,41 @@ class BudPagePendingInviteCascadeTest(TestCase):
|
||||
self.assertIsNone(response.context["pending_invite"])
|
||||
self.assertFalse(response.context["sea_btn_active"])
|
||||
|
||||
def test_stale_accepted_without_deposit_does_not_cascade(self):
|
||||
inv = self.SeaInvite.objects.create(
|
||||
owner=self.alice,
|
||||
invitee=self.user,
|
||||
invitee_email=self.user.email,
|
||||
status=self.SeaInvite.ACCEPTED,
|
||||
)
|
||||
self.SeaInvite.objects.filter(pk=inv.pk).update(
|
||||
created_at=timezone.now() - timezone.timedelta(hours=48),
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("billboard:bud_page", args=[self.alice.id])
|
||||
)
|
||||
self.assertIsNone(response.context["pending_invite"])
|
||||
self.assertFalse(response.context["sea_btn_active"])
|
||||
|
||||
def test_recent_deposit_relights_cascade_past_invite_window(self):
|
||||
# Invite proffered 3 days ago but a gate token deposit 5h ago re-arms
|
||||
# the 24h window — the "OR 24h since last token deposit" clause.
|
||||
inv = self.SeaInvite.objects.create(
|
||||
owner=self.alice,
|
||||
invitee=self.user,
|
||||
invitee_email=self.user.email,
|
||||
status=self.SeaInvite.ACCEPTED,
|
||||
token_deposited_at=timezone.now() - timezone.timedelta(hours=5),
|
||||
)
|
||||
self.SeaInvite.objects.filter(pk=inv.pk).update(
|
||||
created_at=timezone.now() - timezone.timedelta(hours=72),
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("billboard:bud_page", args=[self.alice.id])
|
||||
)
|
||||
self.assertIsNotNone(response.context["pending_invite"])
|
||||
self.assertTrue(response.context["sea_btn_active"])
|
||||
|
||||
def test_invite_for_other_invitee_ignored(self):
|
||||
# Pending invite from alice → some other user is irrelevant to ME.
|
||||
other = User.objects.create(email="other@inv.io", username="other")
|
||||
|
||||
Reference in New Issue
Block a user