recognition: page, palette modal, & dashboard palette unlock — TDD

- billboard/recognition/ view + template; recognition/<slug>/set-palette endpoint (no trailing slash)
- recognition.html: <template>-based modal (clone on open, remove on close — Selenium find_elements compatible)
- recognition-page.js: image-box → modal → swatch preview → body-click restore → OK → confirm → POST set-palette
- _palettes_for_user() replaces static PALETTES; Recognition.palette unlocks swatch + populates data-shoptalk
- _unlocked_palettes_for_user() wires dynamic unlock check into set_palette view
- _applet-palette.html: data-shoptalk from context instead of hard-coded "Placeholder"
- _recognition.scss: banner, recog-list/item, image-box, modal, palette-confirm; :not([hidden]) pattern avoids display override
- FT T2 split into T2a (banner → FYI → recog page), T2b (palette modal flow), T2c (dashboard palette applet)
- 684 ITs green; 7 FTs green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-22 04:02:14 -04:00
parent 565f727aa6
commit 6d9d3d4f54
11 changed files with 683 additions and 53 deletions

View File

@@ -5,7 +5,7 @@ from unittest.mock import patch, MagicMock
from django.contrib.messages import get_messages
from django.test import override_settings, TestCase
from django.urls import reverse
from django.utils import html
from django.utils import html, timezone
from apps.applets.models import Applet, UserApplet
from apps.dashboard.forms import (
@@ -13,6 +13,7 @@ from apps.dashboard.forms import (
EMPTY_ITEM_ERROR,
)
from apps.dashboard.models import Item, Note
from apps.drama.models import Recognition
from apps.lyric.models import User
@@ -348,6 +349,52 @@ class SetPaletteTest(TestCase):
swatches = parsed.cssselect(".swatch")
self.assertEqual(len(swatches), len(response.context["palettes"]))
class RecognitionPaletteContextTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="recog_palette@test.io")
self.client.force_login(self.user)
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
def test_recognition_palette_unlocks_swatch_in_context(self):
Recognition.objects.create(
user=self.user, slug="stargazer", earned_at=timezone.now(),
palette="palette-bardo",
)
response = self.client.get("/")
palettes = response.context["palettes"]
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
self.assertFalse(bardo["locked"])
def test_recognition_palette_shoptalk_contains_recognition_title(self):
Recognition.objects.create(
user=self.user, slug="stargazer", earned_at=timezone.now(),
palette="palette-bardo",
)
response = self.client.get("/")
palettes = response.context["palettes"]
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
self.assertIn("Stargazer", bardo["shoptalk"])
def test_recognition_without_palette_field_keeps_swatch_locked(self):
Recognition.objects.create(
user=self.user, slug="stargazer", earned_at=timezone.now(),
palette=None,
)
response = self.client.get("/")
palettes = response.context["palettes"]
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
self.assertTrue(bardo["locked"])
def test_recognition_palette_allows_set_palette_via_view(self):
Recognition.objects.create(
user=self.user, slug="stargazer", earned_at=timezone.now(),
palette="palette-bardo",
)
self.client.post("/dashboard/set_palette", data={"palette": "palette-bardo"})
self.user.refresh_from_db()
self.assertEqual(self.user.palette, "palette-bardo")
@override_settings(COMPRESS_ENABLED=False)
class ProfileViewTest(TestCase):
def setUp(self):