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
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:
@@ -699,36 +699,62 @@ def invite_gamer(request, room_id):
|
||||
# username-typed invite doesn't store the raw username as if it were
|
||||
# an email); falls back to the raw input for unregistered addresses.
|
||||
invitee_email = candidate.email if candidate else raw
|
||||
RoomInvite.objects.get_or_create(
|
||||
room=room,
|
||||
inviter=request.user,
|
||||
invitee_email=invitee_email,
|
||||
defaults={"status": RoomInvite.PENDING},
|
||||
)
|
||||
|
||||
# Buds graph: symmetric auto-add on registered recipients (mirrors
|
||||
# share_post). Idempotent on M2M; no-op on unregistered recipients.
|
||||
if candidate is not None:
|
||||
request.user.buds.add(candidate)
|
||||
candidate.buds.add(request.user)
|
||||
|
||||
# Brief: confirmation banner for the inviter. Brief.post stays null;
|
||||
# banner FYI navigates to the room's gatekeeper page via Brief.room.
|
||||
brief = Brief.objects.create(
|
||||
owner=request.user,
|
||||
post=None,
|
||||
room=room,
|
||||
kind=Brief.KIND_GAME_INVITE,
|
||||
title="Invite sent",
|
||||
# Duplicate-invite guard: "already present" = recipient is either
|
||||
# already seated in the room OR has a (pending/accepted) RoomInvite.
|
||||
# During gatekeeper phase the visible `.gate-slot.filled` cells are
|
||||
# GateSlot-driven (TableSeats spin up later at SIG SELECT), so check
|
||||
# both — GateSlot.FILLED catches the in-phase case, TableSeat catches
|
||||
# the post-phase case. Seated recipients carry recipient_user_id so
|
||||
# the client can find the .gate-slot.filled[data-user-id="X"]
|
||||
# highlight target; pending invitees have no visible slot, so
|
||||
# recipient_user_id stays null.
|
||||
already_seated = candidate is not None and (
|
||||
GateSlot.objects.filter(
|
||||
room=room, gamer=candidate, status=GateSlot.FILLED,
|
||||
).exists()
|
||||
or TableSeat.objects.filter(room=room, gamer=candidate).exists()
|
||||
)
|
||||
already_invited = RoomInvite.objects.filter(
|
||||
room=room, invitee_email=invitee_email,
|
||||
).exists()
|
||||
already_present = already_seated or already_invited
|
||||
|
||||
brief = None
|
||||
if not already_present:
|
||||
RoomInvite.objects.create(
|
||||
room=room,
|
||||
inviter=request.user,
|
||||
invitee_email=invitee_email,
|
||||
status=RoomInvite.PENDING,
|
||||
)
|
||||
# Buds graph: symmetric auto-add on registered recipients (mirrors
|
||||
# share_post). Idempotent on M2M; no-op on unregistered recipients.
|
||||
if candidate is not None:
|
||||
request.user.buds.add(candidate)
|
||||
candidate.buds.add(request.user)
|
||||
# Brief: confirmation banner for the inviter. Brief.post stays
|
||||
# null; banner FYI navigates to the room's gatekeeper page via
|
||||
# Brief.room.
|
||||
brief = Brief.objects.create(
|
||||
owner=request.user,
|
||||
post=None,
|
||||
room=room,
|
||||
kind=Brief.KIND_GAME_INVITE,
|
||||
title="Invite sent",
|
||||
)
|
||||
|
||||
recipient_user_id = str(candidate.id) if already_seated else None
|
||||
|
||||
if is_ajax:
|
||||
recipient_display = None
|
||||
if candidate is not None:
|
||||
recipient_display = candidate.username or candidate.email
|
||||
return JsonResponse({
|
||||
"brief": brief.to_banner_dict(),
|
||||
"brief": brief.to_banner_dict() if brief is not None else None,
|
||||
"recipient_display": recipient_display,
|
||||
"recipient_user_id": recipient_user_id,
|
||||
"already_present": already_present,
|
||||
})
|
||||
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
Reference in New Issue
Block a user