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:
31
src/templates/apps/billboard/_partials/_invite_actions.html
Normal file
31
src/templates/apps/billboard/_partials/_invite_actions.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% comment %}
|
||||
Interactive OK/BYE block for a @mailman invite Line — Phase A of the my-sea
|
||||
invite flow ([[my-sea-invite-voice-blueprint]]). Renders entirely from
|
||||
`line.sea_invite.status`; the {% if line.sea_invite %} guard lives in
|
||||
post.html so this partial is only reached for invite Lines.
|
||||
|
||||
PENDING (not expired) → OK / BYE form buttons (POST accept / decline).
|
||||
ACCEPTED → "Accepted {date}" badge. (VISIT link to the owner's table is
|
||||
added in Phase B once `my_sea_visit` exists.)
|
||||
DECLINED → "Declined" · LEFT → "Left {date}" · else → "Expired".
|
||||
{% endcomment %}
|
||||
<span class="invite-actions invite-actions--{{ line.sea_invite.status|lower }}">
|
||||
{% if line.sea_invite.status == 'PENDING' and not line.sea_invite.is_expired %}
|
||||
<form class="invite-action-form" method="POST" action="{% url 'my_sea_invite_accept' line.sea_invite.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-confirm invite-ok-btn">OK</button>
|
||||
</form>
|
||||
<form class="invite-action-form" method="POST" action="{% url 'my_sea_invite_decline' line.sea_invite.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-abandon invite-bye-btn">BYE</button>
|
||||
</form>
|
||||
{% elif line.sea_invite.status == 'ACCEPTED' %}
|
||||
<span class="invite-badge invite-badge--accepted">Accepted {{ line.sea_invite.accepted_at|date:'M j' }}</span>
|
||||
{% elif line.sea_invite.status == 'DECLINED' %}
|
||||
<span class="invite-badge invite-badge--declined">Declined</span>
|
||||
{% elif line.sea_invite.status == 'LEFT' %}
|
||||
<span class="invite-badge invite-badge--left">Left {{ line.sea_invite.left_at|date:'M j' }}</span>
|
||||
{% else %}
|
||||
<span class="invite-badge invite-badge--expired">Expired</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
@@ -39,10 +39,14 @@
|
||||
|
||||
<ul id="id_post_table" class="post-lines">
|
||||
{% for line in post.lines.all %}
|
||||
<li class="post-line {% if line.author.username == 'adman' or line.author.username == 'taxman' %}post-line--system{% endif %}">
|
||||
<li class="post-line {% if line.author.username == 'adman' or line.author.username == 'taxman' or line.author.username == 'mailman' %}post-line--system{% endif %}">
|
||||
<span class="post-line-author">{{ line.author|at_handle }}</span>
|
||||
<span class="post-line-text">{# adman / taxman-authored Lines (note unlock, share invite, tax ledger system prose) may carry an `<a class="note-ref">` anchor that needs to render as HTML. User-typed Lines stay escaped. `display_text` strips the `[<iso timestamp>] ` prefix that tax-ledger Lines carry to satisfy `unique_together = (post, text)` — the per-line `created_at` timestamp on the right renders the user-facing moment. #}{% if line.author.username == 'adman' or line.author.username == 'taxman' %}{{ line.display_text|safe }}{% else %}{{ line.display_text }}{% endif %}</span>
|
||||
<span class="post-line-text">{# adman / taxman-authored Lines (note unlock, share invite, tax ledger system prose) may carry an `<a class="note-ref">` anchor that needs to render as HTML. User-typed Lines stay escaped. `display_text` strips the `[<iso timestamp>] ` prefix that tax-ledger Lines carry to satisfy `unique_together = (post, text)` — the per-line `created_at` timestamp on the right renders the user-facing moment. #}{% if line.author.username == 'adman' or line.author.username == 'taxman' or line.author.username == 'mailman' %}{{ line.display_text|safe }}{% else %}{{ line.display_text }}{% endif %}</span>
|
||||
<time class="post-line-time" datetime="{{ line.created_at|date:'c' }}">{{ line.created_at|relative_ts }}</time>
|
||||
{# @mailman invite Lines carry an OK/BYE action block driven by #}
|
||||
{# the linked SeaInvite's status (my-sea invite flow). Non-invite #}
|
||||
{# system + user Lines have no `sea_invite`, so this is skipped. #}
|
||||
{% if line.sea_invite %}{% include "apps/billboard/_partials/_invite_actions.html" %}{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="post-line-buffer" aria-hidden="true"></li>
|
||||
@@ -52,7 +56,7 @@
|
||||
{# the user can't respond, and the placeholder calls that out. View_ #}
|
||||
{# post hard-rejects POSTs to these kinds; the post_save Line signal #}
|
||||
{# is the safety net for ORM-level / API writes that bypass the view. #}
|
||||
{% if post.kind == 'note_unlock' or post.kind == 'tax_ledger' %}
|
||||
{% if post.kind == 'note_unlock' or post.kind == 'tax_ledger' or post.kind == 'mail_acceptance' %}
|
||||
<form id="id_post_line_form" class="post-line-form">
|
||||
<input
|
||||
id="id_post_line_text"
|
||||
@@ -85,7 +89,7 @@
|
||||
{# Bud btn (bottom-left) + slide-out recipient field — async share. #}
|
||||
{# Suppressed on system-author Posts (note unlock + tax ledger threads) #}
|
||||
{# since friend-invites don't apply to system-authored threads. #}
|
||||
{% if post.kind != 'note_unlock' and post.kind != 'tax_ledger' %}
|
||||
{% if post.kind != 'note_unlock' and post.kind != 'tax_ledger' and post.kind != 'mail_acceptance' %}
|
||||
{% include "apps/billboard/_partials/_bud_panel.html" %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user