From 6ad736413bf366d03c92dd1786ca930441cfb69e Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 28 Apr 2026 01:30:02 -0400 Subject: [PATCH] =?UTF-8?q?super-schizo=20+=20super-nomad=20Notes:=20auto-?= =?UTF-8?q?grant=20to=20superusers;=20sig=20unlock;=20navbar=20titles=20?= =?UTF-8?q?=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - drama/models.py: _NOTE_DISPLAY dict; Note.display_title / .display_greeting properties; super-schizo → "21st Century" + "Schizoid Man"; super-nomad → "Howdy," + "Stranger" - billboard/views.py: _NOTE_META super-schizo/nomad entries with mark_safe HTML descriptions ("card-ref"-styled card names), swatch_label "I"/"0", no palette_options; swatch_label added to note_items context - lyric/models.py post_save: new superusers get super-schizo + super-nomad Notes automatically; setup_sig_session grants them explicitly too - epic/models.py _filter_major_unlocks: accepts super-nomad / super-schizo as valid unlocks alongside their plain counterparts - _navbar.html: display_greeting|safe + display_title replace slug|capfirst - my_notes.html: note-item__image-box--label branch for swatch_label - _note.scss: .note-item__image-box--label modifier (bold italic, solid border) - _base.scss: .ord global ordinal superscript class (21st etc.) - ITs: SuperuserNoteGrantTest (3); SigSelectRenderingTest +2 (super- variants) Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Sonnet 4.6 --- src/apps/billboard/views.py | 17 +++++++++++++++ src/apps/drama/models.py | 17 +++++++++++++++ src/apps/epic/models.py | 4 ++-- src/apps/epic/tests/integrated/test_views.py | 10 +++++++++ src/apps/lyric/models.py | 4 ++++ .../lyric/tests/integrated/test_models.py | 21 +++++++++++++++++++ .../management/commands/setup_sig_session.py | 3 +++ src/static_src/scss/_base.scss | 11 +++++++++- src/static_src/scss/_note.scss | 11 ++++++++++ src/templates/apps/billboard/my_notes.html | 2 ++ src/templates/core/_partials/_navbar.html | 2 +- 11 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/apps/billboard/views.py b/src/apps/billboard/views.py index a44bc20..9606088 100644 --- a/src/apps/billboard/views.py +++ b/src/apps/billboard/views.py @@ -1,6 +1,7 @@ import json from django.contrib.auth.decorators import login_required +from django.utils.html import mark_safe from django.db.models import Max, Q from django.http import JsonResponse from django.shortcuts import redirect, render @@ -100,16 +101,31 @@ _NOTE_META = { "title": "Stargazer", "description": "You saved your first personal sky chart.", "palette_options": _palette_opts(["palette-bardo", "palette-sheol"]), + "swatch_label": None, }, "schizo": { "title": "Schizo", "description": "The socius recognizes the line of flight.", "palette_options": [], + "swatch_label": None, }, "nomad": { "title": "Nomad", "description": "The socius recognizes the smooth space.", "palette_options": [], + "swatch_label": None, + }, + "super-schizo": { + "title": "Schizoid Man", + "description": mark_safe('Admin access granted to I. The Schizo as Significator'), + "palette_options": [], + "swatch_label": "I", + }, + "super-nomad": { + "title": "Stranger", + "description": mark_safe('Admin access granted to 0. The Nomad as Significator'), + "palette_options": [], + "swatch_label": "0", }, } @@ -144,6 +160,7 @@ def my_notes(request): "title": _NOTE_META.get(n.slug, {}).get("title", n.slug), "description": _NOTE_META.get(n.slug, {}).get("description", ""), "palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []), + "swatch_label": _NOTE_META.get(n.slug, {}).get("swatch_label"), "palette_label": _PALETTE_LABELS.get(n.palette, "") if n.palette else "", "is_equipped": active_title is not None and active_title.pk == n.pk, } diff --git a/src/apps/drama/models.py b/src/apps/drama/models.py index ac5294a..8241fc9 100644 --- a/src/apps/drama/models.py +++ b/src/apps/drama/models.py @@ -170,6 +170,15 @@ def record(room, verb, actor=None, **data): return GameEvent.objects.create(room=room, actor=actor, verb=verb, data=data) +_NOTE_DISPLAY = { + "stargazer": {"greeting": "Welcome,", "title": "Stargazer"}, + "schizo": {"greeting": "Welcome,", "title": "Schizo"}, + "nomad": {"greeting": "Welcome,", "title": "Nomad"}, + "super-schizo": {"greeting": "21st Century", "title": "Schizoid Man"}, + "super-nomad": {"greeting": "Howdy,", "title": "Stranger"}, +} + + class Note(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, @@ -186,6 +195,14 @@ class Note(models.Model): def __str__(self): return f"{self.user.email} — {self.slug}" + @property + def display_title(self): + return _NOTE_DISPLAY.get(self.slug, {}).get("title", self.slug.replace("-", " ").title()) + + @property + def display_greeting(self): + return _NOTE_DISPLAY.get(self.slug, {}).get("greeting", "Welcome,") + @classmethod def grant_if_new(cls, user, slug): from django.utils import timezone diff --git a/src/apps/epic/models.py b/src/apps/epic/models.py index 6565203..84794cb 100644 --- a/src/apps/epic/models.py +++ b/src/apps/epic/models.py @@ -499,8 +499,8 @@ def _filter_major_unlocks(cards, user): return [ c for c in cards if c.arcana != TarotCard.MAJOR - or (c.number == 0 and "nomad" in earned) - or (c.number == 1 and "schizo" in earned) + or (c.number == 0 and earned & {"nomad", "super-nomad"}) + or (c.number == 1 and earned & {"schizo", "super-schizo"}) ] diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 33b8a71..0e9b332 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -1071,6 +1071,16 @@ class SigSelectRenderingTest(TestCase): response = self.client.get(self.url) self.assertEqual(response.content.decode().count('data-card-id='), 17) + def test_super_nomad_note_also_unlocks_nomad(self): + Note.objects.create(user=self.gamers[0], slug="super-nomad", earned_at=timezone.now()) + response = self.client.get(self.url) + self.assertEqual(response.content.decode().count('data-card-id='), 17) + + def test_super_schizo_note_also_unlocks_schizo(self): + Note.objects.create(user=self.gamers[0], slug="super-schizo", earned_at=timezone.now()) + response = self.client.get(self.url) + self.assertEqual(response.content.decode().count('data-card-id='), 17) + def test_both_notes_gives_18_sig_cards(self): Note.objects.create(user=self.gamers[0], slug="nomad", earned_at=timezone.now()) Note.objects.create(user=self.gamers[0], slug="schizo", earned_at=timezone.now()) diff --git a/src/apps/lyric/models.py b/src/apps/lyric/models.py index 7fa5951..6bf460b 100644 --- a/src/apps/lyric/models.py +++ b/src/apps/lyric/models.py @@ -213,3 +213,7 @@ def create_wallet_and_tokens(sender, instance, created, **kwargs): instance.save(update_fields=['equipped_trinket', 'equipped_deck']) if earthman: instance.unlocked_decks.add(earthman) + if instance.is_superuser: + from apps.drama.models import Note + Note.grant_if_new(instance, "super-schizo") + Note.grant_if_new(instance, "super-nomad") diff --git a/src/apps/lyric/tests/integrated/test_models.py b/src/apps/lyric/tests/integrated/test_models.py index 59548e0..8543b32 100644 --- a/src/apps/lyric/tests/integrated/test_models.py +++ b/src/apps/lyric/tests/integrated/test_models.py @@ -144,6 +144,27 @@ class SuperuserTokenCreationTest(TestCase): ) +class SuperuserNoteGrantTest(TestCase): + def setUp(self): + self.user = User.objects.create_superuser( + email="admin@test.io", password="secret" + ) + + def test_superuser_gets_super_schizo_note(self): + from apps.drama.models import Note + self.assertTrue(Note.objects.filter(user=self.user, slug="super-schizo").exists()) + + def test_superuser_gets_super_nomad_note(self): + from apps.drama.models import Note + self.assertTrue(Note.objects.filter(user=self.user, slug="super-nomad").exists()) + + def test_regular_user_does_not_get_super_notes(self): + from apps.drama.models import Note + regular = User.objects.create(email="regular@test.io") + self.assertFalse(Note.objects.filter(user=regular, slug="super-schizo").exists()) + self.assertFalse(Note.objects.filter(user=regular, slug="super-nomad").exists()) + + class WalletTooltipTest(TestCase): def setUp(self): self.user = User.objects.create(email="wallet@test.io") diff --git a/src/functional_tests/management/commands/setup_sig_session.py b/src/functional_tests/management/commands/setup_sig_session.py index 634f1f7..1d5cf99 100644 --- a/src/functional_tests/management/commands/setup_sig_session.py +++ b/src/functional_tests/management/commands/setup_sig_session.py @@ -20,6 +20,7 @@ from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_K from django.contrib.sessions.backends.db import SessionStore from django.core.management.base import BaseCommand +from apps.drama.models import Note from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat from apps.lyric.models import User @@ -67,6 +68,8 @@ class Command(BaseCommand): user.equipped_deck = None user.save() user.unlocked_decks.add(earthman) + Note.grant_if_new(user, "super-schizo") + Note.grant_if_new(user, "super-nomad") users.append(user) # ── Room ───────────────────────────────────────────────────────────── diff --git a/src/static_src/scss/_base.scss b/src/static_src/scss/_base.scss index 7d206a2..61d0f53 100644 --- a/src/static_src/scss/_base.scss +++ b/src/static_src/scss/_base.scss @@ -557,6 +557,15 @@ body { opacity: 0.6; } +// Ordinal superscript: 21st, 2nd, 3rd etc. — matches .tt-ord but globally available. +.ord { + font-size: 0.6em; + vertical-align: 0.25em; + line-height: 0; + margin-left: -0.1em; + letter-spacing: 0; +} + #id_guard_portal { display: none; position: fixed; @@ -586,4 +595,4 @@ body { display: flex; gap: 0.5rem; } -} \ No newline at end of file +} diff --git a/src/static_src/scss/_note.scss b/src/static_src/scss/_note.scss index 8a4ad8b..0f1f027 100644 --- a/src/static_src/scss/_note.scss +++ b/src/static_src/scss/_note.scss @@ -164,6 +164,17 @@ opacity: 0.6; &:hover { opacity: 1; } + + &--label { + font-size: 1.1rem; + font-weight: bold; + font-style: italic; + color: rgba(var(--terUser), 1); + opacity: 1; + cursor: default; + border-style: solid; + &:hover { opacity: 1; } + } } // Confirmed palette swatch — right-side thumbnail, same gradient as .note-swatch-body. diff --git a/src/templates/apps/billboard/my_notes.html b/src/templates/apps/billboard/my_notes.html index 15bdbec..93bd7ae 100644 --- a/src/templates/apps/billboard/my_notes.html +++ b/src/templates/apps/billboard/my_notes.html @@ -39,6 +39,8 @@ {% if item.obj.palette %}
+ {% elif item.swatch_label %} +
{{ item.swatch_label }}
{% else %}
?
{% endif %} diff --git a/src/templates/core/_partials/_navbar.html b/src/templates/core/_partials/_navbar.html index 99daf07..6eb505f 100644 --- a/src/templates/core/_partials/_navbar.html +++ b/src/templates/core/_partials/_navbar.html @@ -2,7 +2,7 @@