note title equip: DON/DOFF buttons outside top-left corner; User.active_title FK; don/doff views; greeting updated via JS — TDD
- lyric/models.py: active_title = ForeignKey('drama.Note', null=True, SET_NULL)
- lyric migration 0020_add_active_title
- billboard/views.py: don_title + doff_title views; is_equipped per note_item context
- billboard/urls.py: note/<slug>/don + note/<slug>/doff routes
- _navbar.html: id_greeting_name span; shows active_title.slug|capfirst when set
- my_notes.html: .note-don-doff buttons (DON/DOFF, × when disabled); data-don-url/doff-url/title attrs
- note-page.js: _bindDonDoff() — DON POST sets greeting + swaps btn state; DOFF restores Earthman
- _note.scss: .note-don-doff position:absolute left:-1rem top:0; flex-direction:column gap:1.25rem
- ITs: NoteEquipTitleViewTest (5 tests); UserModelTest.test_active_title_* (3 tests)
- FT: NoteEquipTitleTest.test_don_equips_title_greeting_and_doff_restores
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:
@@ -173,6 +173,50 @@
|
|||||||
|
|
||||||
// ── init ──────────────────────────────────────────────────────────────────
|
// ── init ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function _setGreeting(name) {
|
||||||
|
var el = document.getElementById('id_greeting_name');
|
||||||
|
if (el) el.textContent = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bindDonDoff(item) {
|
||||||
|
var donBtn = item.querySelector('.note-don-btn');
|
||||||
|
var doffBtn = item.querySelector('.note-doff-btn');
|
||||||
|
if (!donBtn || !doffBtn) return;
|
||||||
|
|
||||||
|
donBtn.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (donBtn.classList.contains('btn-disabled')) return;
|
||||||
|
fetch(item.dataset.donUrl, {
|
||||||
|
method: 'POST', credentials: 'same-origin',
|
||||||
|
headers: { 'X-CSRFToken': _getCsrf() },
|
||||||
|
})
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
_setGreeting(data.title);
|
||||||
|
donBtn.classList.add('btn-disabled');
|
||||||
|
donBtn.textContent = '×';
|
||||||
|
doffBtn.classList.remove('btn-disabled');
|
||||||
|
doffBtn.textContent = 'DOFF';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
doffBtn.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (doffBtn.classList.contains('btn-disabled')) return;
|
||||||
|
fetch(item.dataset.doffUrl, {
|
||||||
|
method: 'POST', credentials: 'same-origin',
|
||||||
|
headers: { 'X-CSRFToken': _getCsrf() },
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
_setGreeting('Earthman');
|
||||||
|
doffBtn.classList.add('btn-disabled');
|
||||||
|
doffBtn.textContent = '×';
|
||||||
|
donBtn.classList.remove('btn-disabled');
|
||||||
|
donBtn.textContent = 'DON';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function _init() {
|
function _init() {
|
||||||
document.querySelectorAll('.note-item__image-box').forEach(function (box) {
|
document.querySelectorAll('.note-item__image-box').forEach(function (box) {
|
||||||
box.addEventListener('click', function (e) {
|
box.addEventListener('click', function (e) {
|
||||||
@@ -182,6 +226,10 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.note-item').forEach(function (item) {
|
||||||
|
_bindDonDoff(item);
|
||||||
|
});
|
||||||
|
|
||||||
// Body click → dismiss modal and revert any preview
|
// Body click → dismiss modal and revert any preview
|
||||||
document.body.addEventListener('click', function () {
|
document.body.addEventListener('click', function () {
|
||||||
if (_selectedPalette) _revertPreview();
|
if (_selectedPalette) _revertPreview();
|
||||||
|
|||||||
@@ -282,6 +282,51 @@ class NoteSetPaletteViewTest(TestCase):
|
|||||||
self.assertEqual(self.user.palette, "palette-bardo")
|
self.assertEqual(self.user.palette, "palette-bardo")
|
||||||
|
|
||||||
|
|
||||||
|
class NoteEquipTitleViewTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create(email="don@test.io")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.note = Note.objects.create(
|
||||||
|
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_don_sets_active_title(self):
|
||||||
|
self.client.post("/billboard/note/stargazer/don")
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.assertEqual(self.user.active_title, self.note)
|
||||||
|
|
||||||
|
def test_doff_clears_active_title(self):
|
||||||
|
self.user.active_title = self.note
|
||||||
|
self.user.save(update_fields=["active_title"])
|
||||||
|
self.client.post("/billboard/note/stargazer/doff")
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.assertIsNone(self.user.active_title)
|
||||||
|
|
||||||
|
def test_don_returns_200_with_title(self):
|
||||||
|
response = self.client.post("/billboard/note/stargazer/don")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()["title"], "Stargazer")
|
||||||
|
|
||||||
|
def test_doff_returns_200(self):
|
||||||
|
response = self.client.post("/billboard/note/stargazer/doff")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"ok": True})
|
||||||
|
|
||||||
|
def test_don_requires_login(self):
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.post("/billboard/note/stargazer/don")
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
def test_don_returns_404_for_unowned_note(self):
|
||||||
|
other = User.objects.create(email="other@test.io")
|
||||||
|
Note.objects.create(user=other, slug="stargazer", earned_at=timezone.now())
|
||||||
|
self.client.logout()
|
||||||
|
self.client.force_login(other)
|
||||||
|
response = self.client.post("/billboard/note/stargazer/don")
|
||||||
|
# other user's own note — should work
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class SaveScrollPositionTest(TestCase):
|
class SaveScrollPositionTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = User.objects.create(email="test@savescroll.io")
|
self.user = User.objects.create(email="test@savescroll.io")
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ urlpatterns = [
|
|||||||
path("toggle-applets", views.toggle_billboard_applets, name="toggle_applets"),
|
path("toggle-applets", views.toggle_billboard_applets, name="toggle_applets"),
|
||||||
path("my-notes/", views.my_notes, name="my_notes"),
|
path("my-notes/", views.my_notes, name="my_notes"),
|
||||||
path("note/<slug:slug>/set-palette", views.note_set_palette, name="note_set_palette"),
|
path("note/<slug:slug>/set-palette", views.note_set_palette, name="note_set_palette"),
|
||||||
|
path("note/<slug:slug>/don", views.don_title, name="don_title"),
|
||||||
|
path("note/<slug:slug>/doff", views.doff_title, name="doff_title"),
|
||||||
path("room/<uuid:room_id>/scroll/", views.room_scroll, name="scroll"),
|
path("room/<uuid:room_id>/scroll/", views.room_scroll, name="scroll"),
|
||||||
path("room/<uuid:room_id>/scroll-position/", views.save_scroll_position, name="save_scroll_position"),
|
path("room/<uuid:room_id>/scroll-position/", views.save_scroll_position, name="save_scroll_position"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ def note_set_palette(request, slug):
|
|||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
def my_notes(request):
|
def my_notes(request):
|
||||||
qs = Note.objects.filter(user=request.user)
|
qs = Note.objects.filter(user=request.user)
|
||||||
|
active_title = request.user.active_title
|
||||||
note_items = [
|
note_items = [
|
||||||
{
|
{
|
||||||
"obj": n,
|
"obj": n,
|
||||||
@@ -144,6 +145,7 @@ def my_notes(request):
|
|||||||
"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", []),
|
||||||
"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,
|
||||||
}
|
}
|
||||||
for n in qs
|
for n in qs
|
||||||
]
|
]
|
||||||
@@ -154,6 +156,28 @@ def my_notes(request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url="/")
|
||||||
|
def don_title(request, slug):
|
||||||
|
from django.http import Http404
|
||||||
|
try:
|
||||||
|
note = Note.objects.get(user=request.user, slug=slug)
|
||||||
|
except Note.DoesNotExist:
|
||||||
|
raise Http404
|
||||||
|
if request.method == "POST":
|
||||||
|
request.user.active_title = note
|
||||||
|
request.user.save(update_fields=["active_title"])
|
||||||
|
title = _NOTE_META.get(slug, {}).get("title", slug.capitalize())
|
||||||
|
return JsonResponse({"title": title})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url="/")
|
||||||
|
def doff_title(request, slug):
|
||||||
|
if request.method == "POST":
|
||||||
|
request.user.active_title = None
|
||||||
|
request.user.save(update_fields=["active_title"])
|
||||||
|
return JsonResponse({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
def save_scroll_position(request, room_id):
|
def save_scroll_position(request, room_id):
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-04-23 05:42
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dashboard', '0004_rename_note_to_post'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='line',
|
||||||
|
name='post',
|
||||||
|
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='dashboard.post'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='post',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='post',
|
||||||
|
name='shared_with',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='shared_posts', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
src/apps/lyric/migrations/0020_add_active_title.py
Normal file
20
src/apps/lyric/migrations/0020_add_active_title.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-04-23 05:37
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('drama', '0005_rename_recognition_to_note'),
|
||||||
|
('lyric', '0019_sky_birth_tz'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='active_title',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='drama.note'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -44,6 +44,10 @@ class User(AbstractBaseUser):
|
|||||||
unlocked_decks = models.ManyToManyField(
|
unlocked_decks = models.ManyToManyField(
|
||||||
"epic.DeckVariant", blank=True, related_name="unlocked_by",
|
"epic.DeckVariant", blank=True, related_name="unlocked_by",
|
||||||
)
|
)
|
||||||
|
active_title = models.ForeignKey(
|
||||||
|
"drama.Note", null=True, blank=True,
|
||||||
|
on_delete=models.SET_NULL, related_name="+",
|
||||||
|
)
|
||||||
ap_public_key = models.TextField(blank=True, default="")
|
ap_public_key = models.TextField(blank=True, default="")
|
||||||
ap_private_key = models.TextField(blank=True, default="")
|
ap_private_key = models.TextField(blank=True, default="")
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.contrib import auth
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from apps.drama.models import Note
|
||||||
from apps.lyric.models import LoginToken, PaymentMethod, Token, User, Wallet
|
from apps.lyric.models import LoginToken, PaymentMethod, Token, User, Wallet
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +30,27 @@ class UserModelTest(TestCase):
|
|||||||
user = User.objects.create(email="a@b.cde")
|
user = User.objects.create(email="a@b.cde")
|
||||||
self.assertFalse(user.searchable)
|
self.assertFalse(user.searchable)
|
||||||
|
|
||||||
|
def test_active_title_is_null_by_default(self):
|
||||||
|
user = User.objects.create(email="a@b.cde")
|
||||||
|
self.assertIsNone(user.active_title)
|
||||||
|
|
||||||
|
def test_active_title_can_be_set_to_a_note(self):
|
||||||
|
user = User.objects.create(email="a@b.cde")
|
||||||
|
note = Note.objects.create(user=user, slug="stargazer", earned_at=timezone.now())
|
||||||
|
user.active_title = note
|
||||||
|
user.save(update_fields=["active_title"])
|
||||||
|
user.refresh_from_db()
|
||||||
|
self.assertEqual(user.active_title, note)
|
||||||
|
|
||||||
|
def test_clearing_note_nullifies_active_title(self):
|
||||||
|
user = User.objects.create(email="a@b.cde")
|
||||||
|
note = Note.objects.create(user=user, slug="stargazer", earned_at=timezone.now())
|
||||||
|
user.active_title = note
|
||||||
|
user.save(update_fields=["active_title"])
|
||||||
|
note.delete()
|
||||||
|
user.refresh_from_db()
|
||||||
|
self.assertIsNone(user.active_title)
|
||||||
|
|
||||||
class LoginTokenModelTest(TestCase):
|
class LoginTokenModelTest(TestCase):
|
||||||
def test_links_user_with_autogen_uid(self):
|
def test_links_user_with_autogen_uid(self):
|
||||||
login_token1 = LoginToken.objects.create(email="a@b.cde")
|
login_token1 = LoginToken.objects.create(email="a@b.cde")
|
||||||
|
|||||||
@@ -398,3 +398,68 @@ class StargazerNoteFromSkyPageTest(FunctionalTest):
|
|||||||
self.browser.find_elements(By.CSS_SELECTOR, ".nw-root")
|
self.browser.find_elements(By.CSS_SELECTOR, ".nw-root")
|
||||||
))
|
))
|
||||||
self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".note-banner"))
|
self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".note-banner"))
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Title equip — DON/DOFF buttons on the note item
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class NoteEquipTitleTest(FunctionalTest):
|
||||||
|
"""DON button equips the note title as the sitewide greeting; DOFF restores it."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.browser.set_window_size(800, 1200)
|
||||||
|
Applet.objects.get_or_create(
|
||||||
|
slug="billboard-notes",
|
||||||
|
defaults={"name": "My Notes", "grid_cols": 4, "grid_rows": 4, "context": "billboard"},
|
||||||
|
)
|
||||||
|
self.gamer = User.objects.create(email="stargazer@test.io")
|
||||||
|
|
||||||
|
def test_don_equips_title_greeting_and_doff_restores(self):
|
||||||
|
"""DON replaces 'Earthman' with the note title; DOFF restores 'Earthman'.
|
||||||
|
DON/DOFF follow the game-kit equip button pattern: the active btn is normal,
|
||||||
|
the inactive btn is × + btn-disabled."""
|
||||||
|
Note.objects.create(
|
||||||
|
user=self.gamer, slug="stargazer", earned_at=timezone.now(),
|
||||||
|
)
|
||||||
|
self.create_pre_authenticated_session("stargazer@test.io")
|
||||||
|
self.browser.get(self.live_server_url + "/billboard/my-notes/")
|
||||||
|
|
||||||
|
note_item = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-item")
|
||||||
|
)
|
||||||
|
don_btn = note_item.find_element(By.CSS_SELECTOR, ".btn-equip")
|
||||||
|
doff_btn = note_item.find_element(By.CSS_SELECTOR, ".btn-unequip")
|
||||||
|
|
||||||
|
# Initial state: DON active ("DON"), DOFF disabled ("×")
|
||||||
|
self.assertNotIn("btn-disabled", don_btn.get_attribute("class"))
|
||||||
|
self.assertEqual(don_btn.text, "DON")
|
||||||
|
self.assertIn("btn-disabled", doff_btn.get_attribute("class"))
|
||||||
|
self.assertEqual(doff_btn.text, "×")
|
||||||
|
|
||||||
|
# Click DON → greeting changes to the note title
|
||||||
|
don_btn.click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
"Stargazer",
|
||||||
|
self.browser.find_element(By.ID, "id_greeting_name").text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIn("btn-disabled", don_btn.get_attribute("class"))
|
||||||
|
self.assertEqual(don_btn.text, "×")
|
||||||
|
self.assertNotIn("btn-disabled", doff_btn.get_attribute("class"))
|
||||||
|
self.assertEqual(doff_btn.text, "DOFF")
|
||||||
|
|
||||||
|
# Click DOFF → greeting restored to "Earthman"
|
||||||
|
doff_btn.click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
"Earthman",
|
||||||
|
self.browser.find_element(By.ID, "id_greeting_name").text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertNotIn("btn-disabled", don_btn.get_attribute("class"))
|
||||||
|
self.assertEqual(don_btn.text, "DON")
|
||||||
|
self.assertIn("btn-disabled", doff_btn.get_attribute("class"))
|
||||||
|
self.assertEqual(doff_btn.text, "×")
|
||||||
|
|||||||
@@ -60,6 +60,20 @@
|
|||||||
@media (min-width: 1200px) { grid-template-columns: repeat(4, 1fr); }
|
@media (min-width: 1200px) { grid-template-columns: repeat(4, 1fr); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DON/DOFF equip buttons — positioned outside the top-left corner,
|
||||||
|
// matching the .tt-equip-btns pattern from the game kit tooltip.
|
||||||
|
.note-don-doff {
|
||||||
|
position: absolute;
|
||||||
|
left: -1rem;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.btn { margin: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
.note-item {
|
.note-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -10,7 +10,19 @@
|
|||||||
<ul class="note-list">
|
<ul class="note-list">
|
||||||
{% for item in note_items %}
|
{% for item in note_items %}
|
||||||
<li class="note-item" data-slug="{{ item.obj.slug }}"
|
<li class="note-item" data-slug="{{ item.obj.slug }}"
|
||||||
data-set-palette-url="{% url 'billboard:note_set_palette' item.obj.slug %}">
|
data-set-palette-url="{% url 'billboard:note_set_palette' item.obj.slug %}"
|
||||||
|
data-don-url="{% url 'billboard:don_title' item.obj.slug %}"
|
||||||
|
data-doff-url="{% url 'billboard:doff_title' item.obj.slug %}"
|
||||||
|
data-title="{{ item.title }}">
|
||||||
|
|
||||||
|
<div class="note-don-doff">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-equip note-don-btn{% if item.is_equipped %} btn-disabled{% endif %}"
|
||||||
|
>{% if item.is_equipped %}×{% else %}DON{% endif %}</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-unequip note-doff-btn{% if not item.is_equipped %} btn-disabled{% endif %}"
|
||||||
|
>{% if not item.is_equipped %}×{% else %}DOFF{% endif %}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="note-item__body">
|
<div class="note-item__body">
|
||||||
<p class="note-item__title">{{ item.title }}</p>
|
<p class="note-item__title">{{ item.title }}</p>
|
||||||
|
|||||||
@@ -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>Earthman</h1>
|
<h1>Welcome,<br><span id="id_greeting_name">{% if user.active_title %}{{ user.active_title.slug|capfirst }}{% 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