my-sea bud-invite Phase A: SeaInvite model + @mailman log + OK/BYE accept/decline — TDD
Phase A of the my-sea invite → @mailman → spectator → voice blueprint (magical-dancing-quasar.md). Pure Django; no new infra this phase (the coturn droplet lands in Phase C5). Mirrors the @taxman ledger shape throughout. - A1: SeaInvite model (gameboard) — single source of truth for a my-sea invite (owner / invitee / status / timestamps + OneToOne FK to its @mailman Line). is_expired / voice_active / is_present / expires_at properties; 12 UTs. created_at uses default=timezone.now (MySeaDraw precedent) for testable 24h expiry; a token deposit makes the invite non-expiring per spec. - A2: reserved @mailman system user — get_or_create_mailman + "mailman" added to RESERVED_USERNAMES + seed migration lyric/0015. Email domain confirmed w. user as mailman@earthmanrpg.local (matches adman/taxman). - A3: billboard KIND_MAIL_ACCEPTANCE on Post + Brief; extends the post_save unsolicited-line guard (_SYSTEM_AUTHOR_POST_KINDS) + migration billboard/0009. - A4: apps/billboard/mail.py log_sea_invite — appends one interactive Line + invitee Brief on the invitee's "Acceptances & rejections" Post, links the Line back onto the SeaInvite; "Listen!—@owner invites you to {poss} drawing table" prose via at_handle + resolve_pronouns. Unregistered invitee no-ops. - A5: post.html renders OK .btn-confirm / BYE .btn-abandon (PENDING) or a status badge (ACCEPTED / DECLINED / LEFT / EXPIRED) from line.sea_invite.status via new _partials/_invite_actions.html; 'mailman' added to the system-author |safe + read-only-input + bud-panel-suppression branches. - A6: real my_sea_invite (replaces the coming-soon stub) — resolves recipient, dedups outstanding PENDING/ACCEPTED, creates SeaInvite + logs the @mailman line; new my_sea_invite_accept / my_sea_invite_decline endpoints (invitee-only, redirect back to the invite-log Post; accept links invitee FK + stamps accepted_at). 16 ITs. - A7: updated MySeaBudBtnInviteTest (stub→real invite) + new MySeaInviteAcceptanceLogTest FT (invitee opens their log Post, sees the line + OK/BYE). Both green. 457 IT/UT green. Phase B (invitee spectator seat-2 + visitor token gate) + Phase C (WebRTC mesh voice + coturn droplet) to follow. 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:
@@ -1783,12 +1783,12 @@ class MySeaSeatOnePersistenceTest(FunctionalTest):
|
||||
self.assertNotIn("seated", seat1.get_attribute("class"))
|
||||
|
||||
|
||||
class MySeaBudBtnStubTest(FunctionalTest):
|
||||
"""Sprint 6 iter 6c — bud-btn invite panel rendered on the
|
||||
gatekeeper. Panel opens + autocomplete works (reuses billboard:
|
||||
search_buds), but the OK btn is a no-op stub — POSTs return a
|
||||
'Multiplayer my-sea coming soon' Brief banner. Async invite is
|
||||
deferred to a future sprint."""
|
||||
class MySeaBudBtnInviteTest(FunctionalTest):
|
||||
"""bud-btn invite panel on the my-sea gatekeeper. Panel opens +
|
||||
autocomplete works (reuses billboard:search_buds); the OK btn now sends a
|
||||
REAL invite (Phase A of [[my-sea-invite-voice-blueprint]]) — POSTs create
|
||||
a SeaInvite + return an "invite sent" Brief banner. (Was a coming-soon
|
||||
stub through iter 6c.)"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -1820,9 +1820,10 @@ class MySeaBudBtnStubTest(FunctionalTest):
|
||||
)
|
||||
self.browser.find_element(By.ID, "id_recipient")
|
||||
|
||||
def test_bud_btn_ok_renders_coming_soon_brief(self):
|
||||
def test_bud_btn_ok_sends_real_invite(self):
|
||||
from apps.lyric.models import User as _U
|
||||
# Seed a friend so the OK click has a recipient to "invite".
|
||||
from apps.gameboard.models import SeaInvite
|
||||
# Seed a friend so the OK click has a recipient to invite.
|
||||
_U.objects.create(email="friend@test.io")
|
||||
self.create_pre_authenticated_session(self.email)
|
||||
self.browser.get(self.live_server_url + "/gameboard/my-sea/gate/")
|
||||
@@ -1847,11 +1848,47 @@ class MySeaBudBtnStubTest(FunctionalTest):
|
||||
self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm"
|
||||
).click()
|
||||
# Brief banner appears w. coming-soon copy.
|
||||
# Brief banner confirms the invite is on its way.
|
||||
brief = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-banner")
|
||||
)
|
||||
self.assertIn("coming soon", brief.text.lower())
|
||||
self.assertIn("on its way", brief.text.lower())
|
||||
# A real SeaInvite row now exists for the invited friend.
|
||||
self.assertTrue(
|
||||
SeaInvite.objects.filter(invitee_email="friend@test.io").exists()
|
||||
)
|
||||
|
||||
|
||||
class MySeaInviteAcceptanceLogTest(FunctionalTest):
|
||||
"""Phase A of [[my-sea-invite-voice-blueprint]] — an invited bud opens
|
||||
their @mailman "Acceptances & rejections" Post and sees the invite line
|
||||
with interactive OK / BYE buttons."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.email = "invited_bud@test.io"
|
||||
self.invitee = User.objects.create(email=self.email)
|
||||
|
||||
def test_invitee_sees_invite_line_with_ok_bye(self):
|
||||
from django.urls import reverse
|
||||
from apps.billboard.mail import log_sea_invite
|
||||
from apps.gameboard.models import SeaInvite
|
||||
owner = User.objects.create(email="owner@test.io", username="discoman")
|
||||
invite = SeaInvite.objects.create(
|
||||
owner=owner, invitee=self.invitee, invitee_email=self.email,
|
||||
)
|
||||
post, _, _ = log_sea_invite(invite)
|
||||
self.create_pre_authenticated_session(self.email)
|
||||
self.browser.get(
|
||||
self.live_server_url + reverse("billboard:view_post", args=[post.id])
|
||||
)
|
||||
body = self.browser.find_element(By.TAG_NAME, "body")
|
||||
# @mailman prose + interactive OK/BYE both render on the invite line.
|
||||
self.wait_for(lambda: self.assertIn("invites you to", body.text))
|
||||
ok = self.browser.find_element(By.CSS_SELECTOR, ".invite-ok-btn")
|
||||
bye = self.browser.find_element(By.CSS_SELECTOR, ".invite-bye-btn")
|
||||
self.assertEqual(ok.text.upper(), "OK")
|
||||
self.assertEqual(bye.text.upper(), "BYE")
|
||||
|
||||
|
||||
class MySeaGearBtnTest(FunctionalTest):
|
||||
|
||||
Reference in New Issue
Block a user