From 3bf35ad539b326a03b5f46c204e17eb4ec06ab06 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Fri, 29 May 2026 11:52:38 -0400 Subject: [PATCH] =?UTF-8?q?remove=20dead=20my-sea=20invite=20accept/declin?= =?UTF-8?q?e=20endpoints=20=E2=80=94=20acceptance=20is=20now=20implicit=20?= =?UTF-8?q?on=20bud-page=20sea-btn=20visit=20(accept-on-GET)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The @mailman Post's OK/BYE block + _invite_actions.html were dropped by the bud-landing-page sprint; with accept-on-GET on my_sea_visit shipped in f5ee83b the explicit endpoints have no trigger left. Removes: - my_sea_invite_accept / my_sea_invite_decline views + the _sea_invite_for_request / _redirect_to_invite_log helpers they alone used (gameboard/views.py) - the my-sea/invite/accept + my-sea/invite/decline URL routes (gameboard/urls.py) - _invite_actions.html partial (already un-included from post.html) - MySeaInviteAcceptDeclineTest (gameboard ITs); MySeaInvitePostRenderTest now asserts the form actions are gone by literal path/class instead of reverse() There is no decline surface now — an un-clicked invite simply lapses after 24h. post.html comment trimmed to match. 515 gameboard+billboard ITs/UTs green. Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tests/integrated/test_sea_invite_views.py | 84 +++---------------- src/apps/gameboard/urls.py | 4 - src/apps/gameboard/views.py | 61 ++------------ .../billboard/_partials/_invite_actions.html | 31 ------- src/templates/apps/billboard/post.html | 11 ++- 5 files changed, 22 insertions(+), 169 deletions(-) delete mode 100644 src/templates/apps/billboard/_partials/_invite_actions.html diff --git a/src/apps/gameboard/tests/integrated/test_sea_invite_views.py b/src/apps/gameboard/tests/integrated/test_sea_invite_views.py index 19811c8..5bd245e 100644 --- a/src/apps/gameboard/tests/integrated/test_sea_invite_views.py +++ b/src/apps/gameboard/tests/integrated/test_sea_invite_views.py @@ -1,14 +1,12 @@ -"""ITs for the my-sea invite endpoints + post.html OK/BYE render — Phase A -(A5/A6) of [[my-sea-invite-voice-blueprint]]. +"""ITs for the my-sea invite send endpoint + the @mailman Post render. -Covers the real `my_sea_invite` (replaces the "coming soon" stub), the -`my_sea_invite_accept` / `my_sea_invite_decline` transitions + auth gating, -and the interactive OK/BYE / status-badge render in the invitee's -"Acceptances & rejections" Post. +Covers the real `my_sea_invite` (replaces the "coming soon" stub) and the +prose-only render of the invitee's "Acceptances & rejections" Post. The +explicit accept/decline endpoints were removed 2026-05-29 — acceptance is now +implicit on the bud-page sea-btn visit (see test_sea_visit.py's accept-on-GET +coverage); an un-clicked invite simply lapses after 24h. """ -from datetime import timedelta - from django.test import TestCase from django.urls import reverse from django.utils import timezone @@ -80,63 +78,6 @@ class MySeaInviteViewTest(TestCase): self.assertIsNone(resp.json()["brief"]) -class MySeaInviteAcceptDeclineTest(TestCase): - def setUp(self): - self.owner = User.objects.create(email="owner@test.io", username="discoman") - self.bud = User.objects.create(email="bud@test.io", username="budster") - self.invite = SeaInvite.objects.create( - owner=self.owner, invitee=self.bud, invitee_email=self.bud.email, - ) - log_sea_invite(self.invite) - self.invite.refresh_from_db() - - def test_invitee_accept_flips_to_accepted(self): - self.client.force_login(self.bud) - resp = self.client.post( - reverse("my_sea_invite_accept", args=[self.invite.id]) - ) - self.invite.refresh_from_db() - self.assertEqual(self.invite.status, SeaInvite.ACCEPTED) - self.assertIsNotNone(self.invite.accepted_at) - self.assertEqual(self.invite.invitee, self.bud) - self.assertEqual(resp.status_code, 302) - - def test_non_invitee_cannot_accept(self): - stranger = User.objects.create(email="x@test.io", username="x") - self.client.force_login(stranger) - resp = self.client.post( - reverse("my_sea_invite_accept", args=[self.invite.id]) - ) - self.invite.refresh_from_db() - self.assertEqual(self.invite.status, SeaInvite.PENDING) - self.assertEqual(resp.status_code, 403) - - def test_invitee_decline_flips_to_declined(self): - self.client.force_login(self.bud) - self.client.post(reverse("my_sea_invite_decline", args=[self.invite.id])) - self.invite.refresh_from_db() - self.assertEqual(self.invite.status, SeaInvite.DECLINED) - - def test_non_invitee_cannot_decline(self): - stranger = User.objects.create(email="x@test.io", username="x") - self.client.force_login(stranger) - resp = self.client.post( - reverse("my_sea_invite_decline", args=[self.invite.id]) - ) - self.invite.refresh_from_db() - self.assertEqual(self.invite.status, SeaInvite.PENDING) - self.assertEqual(resp.status_code, 403) - - def test_expired_invite_cannot_be_accepted(self): - SeaInvite.objects.filter(pk=self.invite.pk).update( - created_at=timezone.now() - timedelta(hours=25) - ) - self.client.force_login(self.bud) - self.client.post(reverse("my_sea_invite_accept", args=[self.invite.id])) - self.invite.refresh_from_db() - self.assertEqual(self.invite.status, SeaInvite.PENDING) # unchanged - - class MySeaInvitePostRenderTest(TestCase): """post.html — the @mailman invite Line carries a post-attribution anchor around the owner's handle whose href routes to the owner's @@ -154,16 +95,15 @@ class MySeaInvitePostRenderTest(TestCase): self.invite.refresh_from_db() self.client.force_login(self.bud) self.post_url = reverse("billboard:view_post", args=[self.post.id]) - self.accept_url = reverse("my_sea_invite_accept", args=[self.invite.id]) - self.decline_url = reverse("my_sea_invite_decline", args=[self.invite.id]) def test_pending_invite_renders_post_attribution_anchor_not_buttons(self): content = self.client.get(self.post_url).content.decode() - # OK/BYE buttons + accept/decline form actions migrated onto bud.html. + # OK/BYE buttons + the accept/decline form actions are gone entirely. self.assertNotIn("invite-ok-btn", content) self.assertNotIn("invite-bye-btn", content) - self.assertNotIn(self.accept_url, content) - self.assertNotIn(self.decline_url, content) + self.assertNotIn("invite-action-form", content) + self.assertNotIn("my-sea/invite/accept", content) + self.assertNotIn("my-sea/invite/decline", content) # Anchor wraps the owner's handle, routing to their bud landing page. self.assertIn('class="post-attribution"', content) self.assertIn(f"/billboard/buds/{self.owner.id}/", content) @@ -175,14 +115,14 @@ class MySeaInvitePostRenderTest(TestCase): content = self.client.get(self.post_url).content.decode() # No "Accepted " badge — the sweep is unconditional. self.assertNotIn("invite-badge--accepted", content) - self.assertNotIn(self.accept_url, content) + self.assertNotIn("invite-action-form", content) def test_declined_invite_renders_no_badge(self): self.invite.status = SeaInvite.DECLINED self.invite.save() content = self.client.get(self.post_url).content.decode() self.assertNotIn("invite-badge--declined", content) - self.assertNotIn(self.accept_url, content) + self.assertNotIn("invite-action-form", content) def test_mailman_line_renders_as_system_with_handles(self): content = self.client.get(self.post_url).content.decode() diff --git a/src/apps/gameboard/urls.py b/src/apps/gameboard/urls.py index 55d5602..fb7631e 100644 --- a/src/apps/gameboard/urls.py +++ b/src/apps/gameboard/urls.py @@ -21,10 +21,6 @@ urlpatterns = [ path('my-sea/refund', views.my_sea_refund_token, name='my_sea_refund_token'), path('my-sea/paid-draw', views.my_sea_paid_draw, name='my_sea_paid_draw'), path('my-sea/invite', views.my_sea_invite, name='my_sea_invite'), - path('my-sea/invite/accept/', views.my_sea_invite_accept, - name='my_sea_invite_accept'), - path('my-sea/invite/decline/', views.my_sea_invite_decline, - name='my_sea_invite_decline'), path('my-sea/visit//', views.my_sea_visit, name='my_sea_visit'), path('my-sea/visit//gate/', views.my_sea_visit_gate, name='my_sea_visit_gate'), diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index 0a96ef9..ae69516 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -818,62 +818,11 @@ def my_sea_invite(request): }) -def _sea_invite_for_request(request, invite_id): - """Fetch a SeaInvite + decide whether the requester is its invitee. Matches - on the invitee FK when set, else on email (handles an invite created - before the recipient registered). Returns ``(invite, is_invitee)``.""" - from .models import SeaInvite - invite = get_object_or_404(SeaInvite, id=invite_id) - if invite.invitee_id is not None: - is_invitee = invite.invitee_id == request.user.id - else: - is_invitee = ( - (invite.invitee_email or "").lower() == (request.user.email or "").lower() - ) - return invite, is_invitee - - -def _redirect_to_invite_log(invite): - """Redirect to the invitee's "Acceptances & rejections" Post (where the - invite Line lives) so the OK/BYE line re-renders post-transition. Falls - back to /gameboard/ if the invite has no linked line/post yet.""" - from django.urls import reverse - if invite.line_id and invite.line.post_id: - return redirect(reverse("billboard:view_post", args=[invite.line.post_id])) - return redirect("gameboard") - - -@login_required(login_url="/") -@require_POST -def my_sea_invite_accept(request, invite_id): - """Invitee accepts a PENDING my-sea invite → ACCEPTED. Links the invitee - FK + stamps accepted_at. Phase B will redirect to the owner's spectator - table (`my_sea_visit`); for now we redirect back to the invite log Post so - the line re-renders with its Accepted badge.""" - from .models import SeaInvite - invite, is_invitee = _sea_invite_for_request(request, invite_id) - if not is_invitee: - return HttpResponseForbidden() - if invite.status == SeaInvite.PENDING and not invite.is_expired: - invite.status = SeaInvite.ACCEPTED - invite.accepted_at = timezone.now() - invite.invitee = request.user - invite.save(update_fields=["status", "accepted_at", "invitee"]) - return _redirect_to_invite_log(invite) - - -@login_required(login_url="/") -@require_POST -def my_sea_invite_decline(request, invite_id): - """Invitee declines a PENDING my-sea invite → DECLINED.""" - from .models import SeaInvite - invite, is_invitee = _sea_invite_for_request(request, invite_id) - if not is_invitee: - return HttpResponseForbidden() - if invite.status == SeaInvite.PENDING: - invite.status = SeaInvite.DECLINED - invite.save(update_fields=["status"]) - return _redirect_to_invite_log(invite) +# Explicit accept/decline endpoints (the @mailman Post's OK/BYE forms + +# `_invite_actions.html`) were removed 2026-05-29: acceptance is now implicit +# — clicking the bud-page sea sub-btn lands on `my_sea_visit`, which accepts a +# still-pending invite on GET. There is no decline surface; an un-clicked +# invite simply lapses after 24h (`SeaInvite.is_expired`). # ── Phase B — my-sea spectator (invitee) surfaces ─────────────────────────── diff --git a/src/templates/apps/billboard/_partials/_invite_actions.html b/src/templates/apps/billboard/_partials/_invite_actions.html deleted file mode 100644 index 2d32fdf..0000000 --- a/src/templates/apps/billboard/_partials/_invite_actions.html +++ /dev/null @@ -1,31 +0,0 @@ -{% 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 %} - -{% if line.sea_invite.status == 'PENDING' and not line.sea_invite.is_expired %} -
- {% csrf_token %} - -
-
- {% csrf_token %} - -
-{% elif line.sea_invite.status == 'ACCEPTED' %} - Accepted {{ line.sea_invite.accepted_at|date:'M j' }} -{% elif line.sea_invite.status == 'DECLINED' %} - Declined -{% elif line.sea_invite.status == 'LEFT' %} - Left {{ line.sea_invite.left_at|date:'M j' }} -{% else %} - Expired -{% endif %} -
diff --git a/src/templates/apps/billboard/post.html b/src/templates/apps/billboard/post.html index d12c5ba..1415c0b 100644 --- a/src/templates/apps/billboard/post.html +++ b/src/templates/apps/billboard/post.html @@ -43,12 +43,11 @@ {{ line.author|at_handle }} {# adman / taxman / mailman-authored Lines (note unlock, share invite, tax ledger, invite cascade) may carry HTML anchors (note-ref / post-attribution). User-typed Lines stay escaped. `display_text` strips the `[] ` 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 %} - {# Pre-sprint @mailman invite Lines carried an in-line OK/BYE #} - {# block via _invite_actions.html. Bud landing page sprint #} - {# 2026-05-27 migrates that interaction onto bud.html — the #} - {# Line's prose now embeds a post-attribution anchor (see #} - {# apps.billboard.mail.INVITE_TEMPLATE) that routes to the #} - {# owner's bud page where accept/decline/spectator live. #} + {# @mailman invite Lines have no in-line OK/BYE block — the #} + {# Line's prose embeds a post-attribution anchor (see #} + {# apps.billboard.mail.INVITE_TEMPLATE) routing to the owner's #} + {# bud page, where clicking the glowing sea sub-btn accepts #} + {# the invite on visit. No explicit accept/decline surface. #} {% endfor %}