super-schizo + super-nomad Notes: auto-grant to superusers; sig unlock; navbar titles — TDD
- 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 <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.utils.html import mark_safe
|
||||||
from django.db.models import Max, Q
|
from django.db.models import Max, Q
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
@@ -100,16 +101,31 @@ _NOTE_META = {
|
|||||||
"title": "Stargazer",
|
"title": "Stargazer",
|
||||||
"description": "You saved your first personal sky chart.",
|
"description": "You saved your first personal sky chart.",
|
||||||
"palette_options": _palette_opts(["palette-bardo", "palette-sheol"]),
|
"palette_options": _palette_opts(["palette-bardo", "palette-sheol"]),
|
||||||
|
"swatch_label": None,
|
||||||
},
|
},
|
||||||
"schizo": {
|
"schizo": {
|
||||||
"title": "Schizo",
|
"title": "Schizo",
|
||||||
"description": "The socius recognizes the line of flight.",
|
"description": "The socius recognizes the line of flight.",
|
||||||
"palette_options": [],
|
"palette_options": [],
|
||||||
|
"swatch_label": None,
|
||||||
},
|
},
|
||||||
"nomad": {
|
"nomad": {
|
||||||
"title": "Nomad",
|
"title": "Nomad",
|
||||||
"description": "The socius recognizes the smooth space.",
|
"description": "The socius recognizes the smooth space.",
|
||||||
"palette_options": [],
|
"palette_options": [],
|
||||||
|
"swatch_label": None,
|
||||||
|
},
|
||||||
|
"super-schizo": {
|
||||||
|
"title": "Schizoid Man",
|
||||||
|
"description": mark_safe('Admin access granted to <span class="card-ref">I. The Schizo</span> as Significator'),
|
||||||
|
"palette_options": [],
|
||||||
|
"swatch_label": "I",
|
||||||
|
},
|
||||||
|
"super-nomad": {
|
||||||
|
"title": "Stranger",
|
||||||
|
"description": mark_safe('Admin access granted to <span class="card-ref">0. The Nomad</span> 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),
|
"title": _NOTE_META.get(n.slug, {}).get("title", n.slug),
|
||||||
"description": _NOTE_META.get(n.slug, {}).get("description", ""),
|
"description": _NOTE_META.get(n.slug, {}).get("description", ""),
|
||||||
"palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []),
|
"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 "",
|
"palette_label": _PALETTE_LABELS.get(n.palette, "") if n.palette else "",
|
||||||
"is_equipped": active_title is not None and active_title.pk == n.pk,
|
"is_equipped": active_title is not None and active_title.pk == n.pk,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,15 @@ def record(room, verb, actor=None, **data):
|
|||||||
return GameEvent.objects.create(room=room, actor=actor, verb=verb, data=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": "21<span class='ord'>st</span> Century", "title": "Schizoid Man"},
|
||||||
|
"super-nomad": {"greeting": "Howdy,", "title": "Stranger"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Note(models.Model):
|
class Note(models.Model):
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||||
@@ -186,6 +195,14 @@ class Note(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.email} — {self.slug}"
|
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
|
@classmethod
|
||||||
def grant_if_new(cls, user, slug):
|
def grant_if_new(cls, user, slug):
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|||||||
@@ -499,8 +499,8 @@ def _filter_major_unlocks(cards, user):
|
|||||||
return [
|
return [
|
||||||
c for c in cards
|
c for c in cards
|
||||||
if c.arcana != TarotCard.MAJOR
|
if c.arcana != TarotCard.MAJOR
|
||||||
or (c.number == 0 and "nomad" in earned)
|
or (c.number == 0 and earned & {"nomad", "super-nomad"})
|
||||||
or (c.number == 1 and "schizo" in earned)
|
or (c.number == 1 and earned & {"schizo", "super-schizo"})
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1071,6 +1071,16 @@ class SigSelectRenderingTest(TestCase):
|
|||||||
response = self.client.get(self.url)
|
response = self.client.get(self.url)
|
||||||
self.assertEqual(response.content.decode().count('data-card-id='), 17)
|
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):
|
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="nomad", earned_at=timezone.now())
|
||||||
Note.objects.create(user=self.gamers[0], slug="schizo", earned_at=timezone.now())
|
Note.objects.create(user=self.gamers[0], slug="schizo", earned_at=timezone.now())
|
||||||
|
|||||||
@@ -213,3 +213,7 @@ def create_wallet_and_tokens(sender, instance, created, **kwargs):
|
|||||||
instance.save(update_fields=['equipped_trinket', 'equipped_deck'])
|
instance.save(update_fields=['equipped_trinket', 'equipped_deck'])
|
||||||
if earthman:
|
if earthman:
|
||||||
instance.unlocked_decks.add(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")
|
||||||
|
|||||||
@@ -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):
|
class WalletTooltipTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = User.objects.create(email="wallet@test.io")
|
self.user = User.objects.create(email="wallet@test.io")
|
||||||
|
|||||||
@@ -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.contrib.sessions.backends.db import SessionStore
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from apps.drama.models import Note
|
||||||
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat
|
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
@@ -67,6 +68,8 @@ class Command(BaseCommand):
|
|||||||
user.equipped_deck = None
|
user.equipped_deck = None
|
||||||
user.save()
|
user.save()
|
||||||
user.unlocked_decks.add(earthman)
|
user.unlocked_decks.add(earthman)
|
||||||
|
Note.grant_if_new(user, "super-schizo")
|
||||||
|
Note.grant_if_new(user, "super-nomad")
|
||||||
users.append(user)
|
users.append(user)
|
||||||
|
|
||||||
# ── Room ─────────────────────────────────────────────────────────────
|
# ── Room ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -557,6 +557,15 @@ body {
|
|||||||
opacity: 0.6;
|
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 {
|
#id_guard_portal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -586,4 +595,4 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,17 @@
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
|
||||||
&:hover { opacity: 1; }
|
&: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.
|
// Confirmed palette swatch — right-side thumbnail, same gradient as .note-swatch-body.
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
|
|
||||||
{% if item.obj.palette %}
|
{% if item.obj.palette %}
|
||||||
<div class="note-item__palette {{ item.obj.palette }}"></div>
|
<div class="note-item__palette {{ item.obj.palette }}"></div>
|
||||||
|
{% elif item.swatch_label %}
|
||||||
|
<div class="note-item__image-box note-item__image-box--label">{{ item.swatch_label }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="note-item__image-box">?</div>
|
<div class="note-item__image-box">?</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a href="/" class="navbar-brand">
|
<a href="/" class="navbar-brand">
|
||||||
<h1>Welcome,<br><span id="id_greeting_name">{% if user.active_title %}{{ user.active_title.slug|capfirst }}{% else %}Earthman{% endif %}</span></h1>
|
<h1>{% if user.active_title %}{{ user.active_title.display_greeting|safe }}{% else %}Welcome,{% endif %}<br><span id="id_greeting_name">{% if user.active_title %}{{ user.active_title.display_title }}{% else %}Earthman{% endif %}</span></h1>
|
||||||
</a>
|
</a>
|
||||||
{% if user.email %}
|
{% if user.email %}
|
||||||
<div class="navbar-user">
|
<div class="navbar-user">
|
||||||
|
|||||||
Reference in New Issue
Block a user