From bf446285368afdefd3401358495d10ddfc17e17d Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Mon, 18 May 2026 19:45:57 -0400 Subject: [PATCH] =?UTF-8?q?My=20Sea=20applet=20shell=20=E2=80=94=20Sprint?= =?UTF-8?q?=203=20of=20the=20My=20Sea=20roadmap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User roadmap step (Sprint 3 of cluster): scaffold the My Sea applet on the gameboard + the standalone /gameboard/my-sea/ page where later sprints will host the gatekeeper / sig-select / sea-select reskin for solo-user draws. Shell-only — no draw flow yet; latest-draw rendering, mid-progress save, daily quota land in Sprints 4-9 ; **migration**: `applets/migrations/0008_seed_my_sea_applet.py` — RunPython that `update_or_create`s Applet(`slug='my-sea'`, name='My Sea', context='gameboard', default_visible=True, grid_cols=12, grid_rows=4). 12×4 wide horizontal banner so the Celtic Cross spread's 10 cards can render left-to-right in the applet aperture, scrollable like My Palette (per user spec). Reverse migration (`unseed`) deletes the row so the migration is reversible for staging rollbacks ; **applet partial**: `templates/apps/gameboard/_partials/_applet-my-sea.html` — same `{% applet_context %}` auto-discovery shape every other applet uses (`
`). Header is a `

My Sea

` link (gold via global `body a` rule); body is a `.my-sea-scroll` container that either renders `.my-sea-card` cells from a `latest_draw_cards` context (TBD in Sprint 4-7) or a `.my-sea-empty` placeholder line "No draws yet." for fresh users ; **standalone page**: new `gameboard/views.py:my_sea` view + url at `/gameboard/my-sea/` (URL name `my_sea`) rendering `apps/gameboard/my_sea.html` — `{% extends "core/base.html" %}` shell w. letter-spread `MySea` h2 wordmark + `.my-sea-page__empty` placeholder paragraph "Your sea is calm. Draws will appear here." `page_class` doubled to `page-gameboard page-my-sea` so the body inherits the gameboard's landscape aperture treatment AND any future my-sea-specific styles can target a single class. Login-required like the rest of gameboard ; **tests (+6 ITs)**: GameboardViewTest gains 3 — `test_gameboard_shows_my_sea_applet` (cssselect pins #id_applet_my_sea), `test_my_sea_applet_renders_empty_state_for_new_user` (asserts ".my-sea-empty" text + no ".my-sea-card" rows), `test_my_sea_applet_header_links_to_my_sea_page` (h2 a href == reverse('my_sea')); new MySeaViewTest class — `test_my_sea_requires_login` (redirect to /?next=...), `test_my_sea_renders_200`, `test_my_sea_uses_gameboard_page_class` (page-gameboard + page-my-sea both in body class). Existing GameboardViewTest setUp already does `get_or_create` per-applet so no fixture change needed for the migration-driven my-sea row ; 1005 IT/UT green (+6 from 999) in 45s; visual verified in Claudezilla at iPhone-14 portrait — applet renders w. rotated "MY SEA" vertical label + "No draws yet." body; /gameboard/my-sea/ standalone page renders w. letter-spread wordmark + placeholder ; **next**: Sprint 4 — My Sea sig-select phase (single-significator pick for solo user, w. the parameterized hex CSS from Sprint 1 hosting the chair-less or single-chair variant) Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Sonnet 4.6 --- .../migrations/0008_seed_my_sea_applet.py | 42 +++++++++++++++++ .../gameboard/tests/integrated/test_views.py | 45 +++++++++++++++++++ src/apps/gameboard/urls.py | 1 + src/apps/gameboard/views.py | 13 ++++++ .../gameboard/_partials/_applet-my-sea.html | 18 ++++++++ src/templates/apps/gameboard/my_sea.html | 13 ++++++ 6 files changed, 132 insertions(+) create mode 100644 src/apps/applets/migrations/0008_seed_my_sea_applet.py create mode 100644 src/templates/apps/gameboard/_partials/_applet-my-sea.html create mode 100644 src/templates/apps/gameboard/my_sea.html diff --git a/src/apps/applets/migrations/0008_seed_my_sea_applet.py b/src/apps/applets/migrations/0008_seed_my_sea_applet.py new file mode 100644 index 0000000..305c8eb --- /dev/null +++ b/src/apps/applets/migrations/0008_seed_my_sea_applet.py @@ -0,0 +1,42 @@ +"""Seed the My Sea applet — Sprint 3 of the My Sea roadmap. + +The applet itself is just a shell for now (header + horizontal scroll +container w. empty-state placeholder). Sprints 4+ will fill in the +sig-select / sea-select / gatekeeper phases that render in the dedicated +`my_sea.html` page reachable via the applet's header link. + +Grid: 12 cols × 4 rows — wide horizontal banner so the latest draw's +10-card Celtic Cross spread can render left-to-right in the applet +aperture, scrollable like My Palette. +""" +from django.db import migrations + + +def seed(apps, schema_editor): + Applet = apps.get_model("applets", "Applet") + Applet.objects.update_or_create( + slug="my-sea", + defaults={ + "name": "My Sea", + "context": "gameboard", + "default_visible": True, + "grid_cols": 12, + "grid_rows": 4, + }, + ) + + +def unseed(apps, schema_editor): + Applet = apps.get_model("applets", "Applet") + Applet.objects.filter(slug="my-sea").delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("applets", "0007_rename_my_buddies_to_my_buds"), + ] + + operations = [ + migrations.RunPython(seed, unseed), + ] diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index 3475454..5ce9a9b 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -34,6 +34,23 @@ class GameboardViewTest(TestCase): def test_gameboard_shows_new_game_applet(self): [_] = self.parsed.cssselect("#id_applet_new_game") + + def test_gameboard_shows_my_sea_applet(self): + # Sprint 3 of the My Sea roadmap — applet shell only; sigs/sea/draw + # flow lands in later sprints. Seeded via migration 0008. + [_] = self.parsed.cssselect("#id_applet_my_sea") + + def test_my_sea_applet_renders_empty_state_for_new_user(self): + # A fresh user has no saved draws → the scroll container hosts a + # single placeholder line ("No draws yet."), no card cells. + [empty] = self.parsed.cssselect("#id_applet_my_sea .my-sea-empty") + self.assertIn("No draws yet", empty.text_content()) + cards = self.parsed.cssselect("#id_applet_my_sea .my-sea-card") + self.assertEqual(len(cards), 0) + + def test_my_sea_applet_header_links_to_my_sea_page(self): + [link] = self.parsed.cssselect("#id_applet_my_sea h2 a") + self.assertEqual(link.get("href"), reverse("my_sea")) def test_gameboard_shows_game_kit(self): [_] = self.parsed.cssselect("#id_game_kit") @@ -442,3 +459,31 @@ class TarotFanViewTest(TestCase): def test_returns_403_for_locked_deck(self): response = self.client.get(reverse("tarot_fan", kwargs={"deck_id": self.fiorentine.pk})) self.assertEqual(response.status_code, 403) + + +class MySeaViewTest(TestCase): + """Sprint 3 of the My Sea roadmap — standalone page is a shell only. + Sigs / sea-select / gatekeeper phase content lands in later sprints.""" + + def setUp(self): + self.user = User.objects.create(email="sea@test.io") + self.client.force_login(self.user) + + def test_my_sea_requires_login(self): + self.client.logout() + response = self.client.get(reverse("my_sea")) + self.assertRedirects( + response, "/?next=/gameboard/my-sea/", fetch_redirect_response=False + ) + + def test_my_sea_renders_200(self): + response = self.client.get(reverse("my_sea")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "apps/gameboard/my_sea.html") + + def test_my_sea_uses_gameboard_page_class(self): + # `page_class` drives the body class for the landscape layout aperture + # — My Sea inherits the gameboard's aperture (same nav/footer rails). + response = self.client.get(reverse("my_sea")) + self.assertIn("page-gameboard", response.content.decode()) + self.assertIn("page-my-sea", response.content.decode()) diff --git a/src/apps/gameboard/urls.py b/src/apps/gameboard/urls.py index e96cac9..e4e013b 100644 --- a/src/apps/gameboard/urls.py +++ b/src/apps/gameboard/urls.py @@ -13,5 +13,6 @@ urlpatterns = [ path('game-kit/', views.game_kit, name='game_kit'), path('game-kit/toggle-sections', views.toggle_game_kit_sections, name='toggle_game_kit_sections'), path('game-kit/deck//', views.tarot_fan, name='tarot_fan'), + path('my-sea/', views.my_sea, name='my_sea'), ] diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index 5e7e718..671e303 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -166,6 +166,19 @@ def toggle_game_kit_sections(request): return redirect("game_kit") +@login_required(login_url="/") +def my_sea(request): + """Shell view for the My Sea standalone page. + + Sprint 3 scaffolding only — the page will host the gatekeeper / + sig-select / sea-select phase reskin for solo-user draws in later + sprints. For now it just renders the empty skeleton. + """ + return render(request, "apps/gameboard/my_sea.html", { + "page_class": "page-gameboard page-my-sea", + }) + + @login_required(login_url="/") def tarot_fan(request, deck_id): from apps.epic.models import TarotCard diff --git a/src/templates/apps/gameboard/_partials/_applet-my-sea.html b/src/templates/apps/gameboard/_partials/_applet-my-sea.html new file mode 100644 index 0000000..ea119f7 --- /dev/null +++ b/src/templates/apps/gameboard/_partials/_applet-my-sea.html @@ -0,0 +1,18 @@ +
+

My Sea

+
+ {% if latest_draw_cards %} + {% for card in latest_draw_cards %} +
+ {{ card.corner_rank }} + {% if card.suit_icon %}{% endif %} +
+ {% endfor %} + {% else %} +

No draws yet.

+ {% endif %} +
+
diff --git a/src/templates/apps/gameboard/my_sea.html b/src/templates/apps/gameboard/my_sea.html new file mode 100644 index 0000000..629bce7 --- /dev/null +++ b/src/templates/apps/gameboard/my_sea.html @@ -0,0 +1,13 @@ +{% extends "core/base.html" %} +{% load static %} + +{% block title_text %}My Sea{% endblock title_text %} +{% block header_text %}MySea{% endblock header_text %} + +{% block content %} +
+ {# Sprint 3 shell only — gatekeeper / sig-select / sea-select phases #} + {# will land here in later sprints of the My Sea roadmap. #} +

Your sea is calm. Draws will appear here.

+
+{% endblock content %}