gatekeeper invite ports to #id_bud_btn slide-out: drop the inline #id_invite_email form, add bud-invite panel for room owner during gate phase, async POST to invite_gamer w. autocomplete + symmetric buds auto-add + slide-down Brief banner — TDD

- Brief schema (billboard/0007): post FK becomes nullable + new room FK to epic.Room + KIND_GAME_INVITE enum value. to_banner_dict resolves post_url to reverse('epic:gatekeeper', room.id) when post is null and room is set.
  - epic.invite_gamer view refactor:
    • Accepts `recipient` (matches bud-panel field; legacy `invitee_email` still works for full backwards compat).
    • Resolves via apps.billboard.views._resolve_recipient (email if "@" present, else username).
    • RoomInvite stores the resolved User's email (or raw input if unregistered).
    • Auto-adds inviter ↔ recipient to each others' buds (symmetric per Phase 2 spec) when recipient is a registered User.
    • Spawns a Brief w. owner=request.user, kind=GAME_INVITE, room=room, post=null.
    • Accept: application/json → {brief, recipient_display}; otherwise redirects to gatekeeper as before.
    • Self-invite + blank recipient: 200 w. brief=null, no RoomInvite, no buds touch.
  - _gatekeeper.html: gate-invite-panel block (lines 62-71) removed.
  - new templates/apps/billboard/_partials/_bud_invite_panel.html: clone of _bud_panel.html w. data-invite-url + autocomplete from request.user.buds. JS posts to invite_gamer + Brief.showBanner. room.html includes it owner-only when not table_status and gate_status != RENEWAL_DUE.
  - room.html scripts block now loads apps/dashboard/note.js so window.Brief is defined for the slide-down banner.
  - Tests: new test_invite_gamer.py (14 ITs) covering ajax + legacy form-submit paths, recipient resolution, RoomInvite creation, Brief w. room FK + GAME_INVITE kind, symmetric buds auto-add, unregistered/self/blank silent no-op cases. New test_gatekeeper_bud_btn.py FT (9 tests) covers presence (owner-only), absence of legacy #id_invite_email, async invite flow end-to-end (RoomInvite, Brief, banner, panel close, username resolve, buds auto-add).
  - test_brief.test_brief_owner_post_required relaxed to test_brief_owner_required (post is now nullable).
  - test_room_gatekeeper.test_second_gamer_drops_token_into_open_slot updated to drive the bud-btn flow (drops the #id_invite_email/#id_invite_btn references).
  - 866 ITs (+14) + 9 gatekeeper FTs + 28 existing room-gatekeeper FTs green.

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-09 00:59:54 -04:00
parent 4010e452a6
commit 419e022140
10 changed files with 549 additions and 32 deletions

View File

@@ -94,10 +94,12 @@ class Brief(models.Model):
KIND_NOTE_UNLOCK = "note_unlock"
KIND_USER_POST = "user_post"
KIND_SHARE_INVITE = "share_invite"
KIND_GAME_INVITE = "game_invite"
KIND_CHOICES = [
(KIND_NOTE_UNLOCK, "Note unlock"),
(KIND_USER_POST, "User post"),
(KIND_SHARE_INVITE, "Share invite"),
(KIND_GAME_INVITE, "Game invite"),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
@@ -106,10 +108,25 @@ class Brief(models.Model):
related_name="briefs",
on_delete=models.CASCADE,
)
# Post is nullable now: KIND_GAME_INVITE briefs ride on a Room FK
# instead of a Post (the gatekeeper invite confirmation has no post
# to navigate to). Post FKs only set for note_unlock / user_post /
# share_invite kinds.
post = models.ForeignKey(
Post,
related_name="briefs",
on_delete=models.CASCADE,
null=True,
blank=True,
)
# Room FK — set only on KIND_GAME_INVITE briefs; FYI navigates to
# the gatekeeper page for that room.
room = models.ForeignKey(
"epic.Room",
related_name="briefs",
on_delete=models.CASCADE,
null=True,
blank=True,
)
# Line is nullable because a share_invite-style Brief can race ahead of its
# async-appended Line write; the post FK alone is enough to navigate.
@@ -142,16 +159,23 @@ class Brief(models.Model):
"""Shape this Brief for the slide-down banner JS. NOTE_UNLOCK kind
carries a square_url pointing at /billboard/my-notes/ so the
thumbnail-square inside the banner jumps direct to the user's Note
collection — other kinds get an empty square_url."""
collection. GAME_INVITE kind has no Post — the FYI link navigates
to the gatekeeper page for the brief's Room instead."""
square_url = ""
if self.kind == self.KIND_NOTE_UNLOCK:
square_url = reverse("billboard:my_notes")
if self.post_id:
post_url = self.post.get_absolute_url()
elif self.room_id:
post_url = reverse("epic:gatekeeper", args=[self.room_id])
else:
post_url = ""
return {
"id": str(self.id),
"kind": self.kind,
"title": self.title,
"line_text": self.line.text if self.line else "",
"post_url": self.post.get_absolute_url(),
"post_url": post_url,
"square_url": square_url,
"created_at": self.created_at.isoformat(),
}