bud panels duplicate-add guard: server-side already_present flag + client-side error Brief w. FYI flash highlight on the existing entry — for each of the three #id_bud_btn panels (My Buds / post-share / gatekeeper-invite), the JSON response from add_bud / share_post / invite_gamer now carries {already_present, recipient_display, recipient_user_id}; bud-btn.js branches on already_present → calls new Brief.showDuplicateBanner({display_name, target_selector}) instead of the normal onSuccess append; banner title reads @<username> is already present, NVM dismisses, FYI dismisses AND eases in the .bud-duplicate-flash class (color: var(--terUser); text-shadow: 0 0 .5em var(--ninUser); transition: 600ms) onto the existing element (.bud-entry .bud-name / .post-recipient[data-user-id=…] / .gate-slot.filled[data-user-id=…]); gatekeeper "already present" = recipient is either GateSlot.FILLED + gamer OR has TableSeat OR has a pending RoomInvite (highlight target only set when seated — pending invites have no visible slot); .post-recipient chips + .gate-slot.filled cells gain data-user-id so the FYI selector can find them; my_buds.html now loads note.js via the {% block scripts %} pattern (Brief module is required by the duplicate banner path); bonus: latent test_jasmine.py bug fixed — "0 failures" in result.text matched "10 failures" / "20 failures" / etc, silently passing up to 99 failed specs; replaced w. re.search(r"(?<!\d)0 failures\b", …) (caught my new red specs, would've caught any prior Jasmine regression); 18 new ITs + 10 new Jasmine specs + 3 new FTs (one per panel) — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-12 16:40:15 -04:00
parent 264ed5968e
commit be919c7aff
19 changed files with 738 additions and 38 deletions

View File

@@ -141,3 +141,88 @@ class InviteGamerLegacyFormTest(TestCase):
self.assertTrue(RoomInvite.objects.filter(
room=self.room, invitee_email="alice@test.io",
).exists())
class InviteGamerAlreadyPresentTest(TestCase):
"""Duplicate-invite guard: a gatekeeper invite gets `already_present:
true` in its JSON response when the recipient is either (a) already
seated in the room or (b) has a pending RoomInvite. `recipient_user_id`
is set only when seated — pending invitees have no visible
`.gate-slot.filled` DOM element to highlight, so the FYI button just
dismisses without easing in the flash class."""
def setUp(self):
from apps.epic.models import GateSlot, TableSeat
self.GateSlot = GateSlot
self.TableSeat = TableSeat
self.owner = User.objects.create(email="owner@test.io", username="owner")
self.client.force_login(self.owner)
self.alice = User.objects.create(email="alice@test.io", username="alice")
self.room = Room.objects.create(name="Bingo", owner=self.owner)
def _invite(self, recipient):
return self.client.post(
reverse("epic:invite_gamer", args=[self.room.id]),
data={"recipient": recipient},
HTTP_ACCEPT="application/json",
)
def test_already_present_true_when_recipient_is_seated_via_gate_slot(self):
"""Gatekeeper phase: 'seated' means a GateSlot.FILLED w. gamer set."""
self.GateSlot.objects.create(
room=self.room, gamer=self.alice, slot_number=1,
status=self.GateSlot.FILLED,
)
self.assertTrue(self._invite("alice@test.io").json()["already_present"])
def test_already_present_true_when_recipient_has_table_seat(self):
"""Post-gatekeeper phase: TableSeat exists once slots transition."""
self.TableSeat.objects.create(room=self.room, gamer=self.alice, slot_number=1)
self.assertTrue(self._invite("alice@test.io").json()["already_present"])
def test_recipient_user_id_carries_alice_id_when_seated(self):
self.TableSeat.objects.create(room=self.room, gamer=self.alice, slot_number=1)
body = self._invite("alice@test.io").json()
self.assertEqual(body["recipient_user_id"], str(self.alice.id))
def test_already_present_true_when_recipient_has_pending_room_invite(self):
RoomInvite.objects.create(
room=self.room, inviter=self.owner,
invitee_email="alice@test.io", status=RoomInvite.PENDING,
)
self.assertTrue(self._invite("alice@test.io").json()["already_present"])
def test_recipient_user_id_null_when_only_invited_not_seated(self):
"""Pending invites have no visible slot element to highlight."""
RoomInvite.objects.create(
room=self.room, inviter=self.owner,
invitee_email="alice@test.io", status=RoomInvite.PENDING,
)
self.assertIsNone(self._invite("alice@test.io").json()["recipient_user_id"])
def test_already_present_false_for_brand_new_recipient(self):
self.assertFalse(self._invite("alice@test.io").json()["already_present"])
def test_brief_is_null_on_duplicate(self):
"""No new Brief on duplicate — the client renders the error Brief
from the already_present flag instead."""
self.TableSeat.objects.create(room=self.room, gamer=self.alice, slot_number=1)
self.assertIsNone(self._invite("alice@test.io").json()["brief"])
def test_no_new_room_invite_on_duplicate(self):
RoomInvite.objects.create(
room=self.room, inviter=self.owner,
invitee_email="alice@test.io", status=RoomInvite.PENDING,
)
self._invite("alice@test.io")
self.assertEqual(
RoomInvite.objects.filter(room=self.room, invitee_email="alice@test.io").count(),
1,
)
def test_recipient_display_carries_username_on_duplicate(self):
self.TableSeat.objects.create(room=self.room, gamer=self.alice, slot_number=1)
self.assertEqual(
self._invite("alice@test.io").json()["recipient_display"],
"alice",
)