My Sea iter 6c: bud-btn invite stub + #id_my_sea_menu gear (NVM-only, %applet-menu-styled, on both /gameboard/my-sea/ and the gatekeeper) + PAID DRAW now deletes the row and redirects to ?phase=picker so the user drops straight into picking cards instead of looping back to GATE VIEW — Sprint 5 iter 6c of My Sea roadmap — TDD
Bundled fix for the PAID-DRAW-loops-to-GATE-VIEW bug surfaced 2026-05-20 in live testing: previously the view reset `created_at = now()` + cleared the hand, but the row's continued existence meant `quota_spent=True` on the next render → landing rendered GATE VIEW → user clicked it → back to gatekeeper → loop. Now PAID DRAW does `active_draw.delete()` after debiting the token + then redirects to `/gameboard/my-sea/?phase=picker`. The my_sea view honors `?phase=picker` (only when no active_draw exists — can't bypass post-DEL GATE VIEW) by forcing `show_picker=True` so the user lands in the picker ready to draw. First card draw creates a fresh row w. fresh `created_at`, starting the new 24h quota cycle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1563,32 +1563,22 @@ class MySeaPaidDrawViewTest(TestCase):
|
||||
self.client.post(self.url)
|
||||
self.assertFalse(Token.objects.filter(pk=self.free_tok.pk).exists())
|
||||
|
||||
def test_paid_draw_clears_deposit_fields_and_resets_created_at(self):
|
||||
old_created = self.draw.created_at
|
||||
# Push old created_at back so reset is observable.
|
||||
from datetime import timedelta
|
||||
def test_paid_draw_deletes_active_draw_row(self):
|
||||
# User-spec 2026-05-20: PAID DRAW commits the token + drops the row
|
||||
# entirely so the user returns to a fresh "able-to-draw-now" state
|
||||
# (instead of the buggy "row preserved → GATE VIEW loop" semantics).
|
||||
from apps.gameboard.models import MySeaDraw
|
||||
MySeaDraw.objects.filter(pk=self.draw.pk).update(
|
||||
created_at=old_created - timedelta(hours=12),
|
||||
)
|
||||
self.client.post(self.url)
|
||||
self.draw.refresh_from_db()
|
||||
self.assertIsNone(self.draw.deposit_token_id)
|
||||
self.assertIsNone(self.draw.deposit_reserved_at)
|
||||
self.assertGreater(self.draw.created_at, old_created - timedelta(hours=12))
|
||||
self.assertFalse(MySeaDraw.objects.filter(pk=self.draw.pk).exists())
|
||||
|
||||
def test_paid_draw_resets_hand_to_empty(self):
|
||||
# Even if hand is non-empty, PAID DRAW wipes it (fresh draw cycle).
|
||||
from apps.epic.models import TarotCard
|
||||
cards = list(TarotCard.objects.exclude(id=self.target.id)[:2])
|
||||
self.draw.hand = [
|
||||
{"position": "lay", "card_id": cards[0].id, "reversed": False, "polarity": "gravity"},
|
||||
{"position": "cover", "card_id": cards[1].id, "reversed": True, "polarity": "levity"},
|
||||
]
|
||||
self.draw.save(update_fields=["hand"])
|
||||
self.client.post(self.url)
|
||||
self.draw.refresh_from_db()
|
||||
self.assertEqual(self.draw.hand, [])
|
||||
def test_paid_draw_redirects_to_my_sea_with_phase_picker(self):
|
||||
# User-spec 2026-05-20: drop the user directly into the picker
|
||||
# after PAID DRAW (no intermediate FREE-DRAW click). Encoded via
|
||||
# `?phase=picker` query param so the my_sea view can short-
|
||||
# circuit `show_picker` even when active_draw is now None.
|
||||
response = self.client.post(self.url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("phase=picker", response["Location"])
|
||||
|
||||
def test_paid_draw_with_coin_sets_24h_cooldown_and_unequips(self):
|
||||
from datetime import timedelta
|
||||
@@ -1621,6 +1611,45 @@ class MySeaPaidDrawViewTest(TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
|
||||
class MySeaPhasePickerQueryParamTest(TestCase):
|
||||
"""Sprint 6 iter 6c — `?phase=picker` query param forces picker phase
|
||||
when no active_draw row exists (the just-after-PAID-DRAW state).
|
||||
Without the param, no-active-draw users default to the FREE DRAW
|
||||
landing. With it, they drop straight into the picker so they can
|
||||
start drawing immediately (the token they just spent earns this)."""
|
||||
|
||||
def setUp(self):
|
||||
from apps.epic.models import personal_sig_cards
|
||||
self.user = User.objects.create(email="phase@test.io")
|
||||
self.client.force_login(self.user)
|
||||
self.target = personal_sig_cards(self.user)[0]
|
||||
self.user.significator = self.target
|
||||
self.user.save(update_fields=["significator"])
|
||||
|
||||
def test_no_param_lands_on_free_draw(self):
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertContains(response, 'data-phase="landing"')
|
||||
self.assertContains(response, 'id="id_draw_sea_btn"')
|
||||
|
||||
def test_phase_picker_param_forces_picker(self):
|
||||
response = self.client.get(reverse("my_sea") + "?phase=picker")
|
||||
self.assertContains(response, 'data-phase="picker"')
|
||||
# Picker IS rendered (no inline style="display:none" on it).
|
||||
self.assertNotContains(response, 'id="id_sea_overlay"' + ' style="display:none"')
|
||||
|
||||
def test_phase_picker_param_ignored_when_active_draw_with_empty_hand(self):
|
||||
# Post-DEL state: active row w. empty hand → quota's spent, the
|
||||
# query param shouldn't bypass GATE VIEW. Landing branch wins.
|
||||
from apps.gameboard.models import MySeaDraw
|
||||
MySeaDraw.objects.create(
|
||||
user=self.user, spread="situation-action-outcome",
|
||||
significator_id=self.target.id, hand=[],
|
||||
)
|
||||
response = self.client.get(reverse("my_sea") + "?phase=picker")
|
||||
self.assertContains(response, 'data-phase="landing"')
|
||||
self.assertContains(response, 'id="id_my_sea_gate_view_btn"')
|
||||
|
||||
|
||||
class SelectMySeaTokenTest(TestCase):
|
||||
"""Sprint 6 iter 6a — `_select_my_sea_token` priority chain w. CARTE
|
||||
excluded + COIN cooldown-respecting."""
|
||||
|
||||
Reference in New Issue
Block a user