My Sea applet shell — Sprint 3 of the My Sea roadmap
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 (`<section id="id_applet_my_sea" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">`). Header is a `<h2><a href="{% url 'my_sea' %}">My Sea</a></h2>` 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 `<span>My</span><span>Sea</span>` 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 <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
42
src/apps/applets/migrations/0008_seed_my_sea_applet.py
Normal file
42
src/apps/applets/migrations/0008_seed_my_sea_applet.py
Normal file
@@ -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),
|
||||
]
|
||||
@@ -35,6 +35,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())
|
||||
|
||||
@@ -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/<int:deck_id>/', views.tarot_fan, name='tarot_fan'),
|
||||
path('my-sea/', views.my_sea, name='my_sea'),
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
18
src/templates/apps/gameboard/_partials/_applet-my-sea.html
Normal file
18
src/templates/apps/gameboard/_partials/_applet-my-sea.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<section
|
||||
id="id_applet_my_sea"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2><a href="{% url 'my_sea' %}">My Sea</a></h2>
|
||||
<div class="my-sea-scroll">
|
||||
{% if latest_draw_cards %}
|
||||
{% for card in latest_draw_cards %}
|
||||
<div class="my-sea-card" data-position="{{ card.position }}">
|
||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="my-sea-empty">No draws yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
13
src/templates/apps/gameboard/my_sea.html
Normal file
13
src/templates/apps/gameboard/my_sea.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title_text %}My Sea{% endblock title_text %}
|
||||
{% block header_text %}<span>My</span><span>Sea</span>{% endblock header_text %}
|
||||
|
||||
{% block content %}
|
||||
<div class="my-sea-page">
|
||||
{# Sprint 3 shell only — gatekeeper / sig-select / sea-select phases #}
|
||||
{# will land here in later sprints of the My Sea roadmap. #}
|
||||
<p class="my-sea-page__empty">Your sea is calm. Draws will appear here.</p>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
Reference in New Issue
Block a user