'theme_switcher,' 'theme-picker' & 'theme' renamed everywhere to simply 'palette'; new urls & views & their corresponding ITs ensure applet menu checkbox functionality

This commit is contained in:
Disco DeDisco
2026-03-05 14:45:55 -05:00
parent ca835059c2
commit c099479740
16 changed files with 154 additions and 85 deletions

View File

@@ -82,7 +82,7 @@ class AppletModelTest(TestCase):
class UserAppletModelTest(TestCase): class UserAppletModelTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create(email="a@b.cde") self.user = User.objects.create(email="a@b.cde")
self.applet = Applet.objects.create(slug="username", name="Username") self.applet, _ = Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
def test_user_applet_links_user_to_applet(self): def test_user_applet_links_user_to_applet(self):
ua = UserApplet.objects.create(user=self.user, applet=self.applet, visible=True) ua = UserApplet.objects.create(user=self.user, applet=self.applet, visible=True)

View File

@@ -9,7 +9,7 @@ from apps.dashboard.forms import (
DUPLICATE_ITEM_ERROR, DUPLICATE_ITEM_ERROR,
EMPTY_ITEM_ERROR, EMPTY_ITEM_ERROR,
) )
from apps.dashboard.models import Item, List from apps.dashboard.models import Applet, Item, List, UserApplet
from apps.lyric.models import User from apps.lyric.models import User
@@ -256,45 +256,45 @@ class ViewAuthListTest(TestCase):
response = self.client.get(reverse("view_list", args=[self.our_list.id])) response = self.client.get(reverse("view_list", args=[self.our_list.id]))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class SetThemeTest(TestCase): class SetPaletteTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create(email="a@b.cde") self.user = User.objects.create(email="a@b.cde")
self.client.force_login(self.user) self.client.force_login(self.user)
self.url = reverse("home") self.url = reverse("home")
def test_anonymous_user_is_redirected_home(self): def test_anonymous_user_is_redirected_home(self):
response = self.client.post("/dashboard/set_theme") response = self.client.post("/dashboard/set_palette")
self.assertRedirects(response, "/") self.assertRedirects(response, "/")
def test_set_theme_updates_user_theme(self): def test_set_palette_updates_user_palette(self):
User.objects.filter(pk=self.user.pk).update(theme="theme-sheol") User.objects.filter(pk=self.user.pk).update(palette="palette-sheol")
self.client.post("/dashboard/set_theme", data={"theme": "theme-default"}) self.client.post("/dashboard/set_palette", data={"palette": "palette-default"})
self.user.refresh_from_db() self.user.refresh_from_db()
self.assertEqual(self.user.theme, "theme-default") self.assertEqual(self.user.palette, "palette-default")
def test_locked_theme_is_rejected(self): def test_locked_palette_is_rejected(self):
response = self.client.post("/dashboard/set_theme", data={"theme": "theme-nirvana"}) response = self.client.post("/dashboard/set_palette", data={"palette": "palette-nirvana"})
self.user.refresh_from_db() self.user.refresh_from_db()
self.assertEqual(self.user.theme, "theme-default") self.assertEqual(self.user.palette, "palette-default")
self.assertRedirects(response, "/") self.assertRedirects(response, "/")
def test_set_theme_redirects_home(self): def test_set_palette_redirects_home(self):
response = self.client.post("/dashboard/set_theme", data={"theme": "theme-default"}) response = self.client.post("/dashboard/set_palette", data={"palette": "palette-default"})
self.assertRedirects(response, "/") self.assertRedirects(response, "/")
def test_my_lists_contains_set_theme_form(self): def test_my_lists_contains_set_palette_form(self):
response = self.client.get(self.url) response = self.client.get(self.url)
parsed = lxml.html.fromstring(response.content) parsed = lxml.html.fromstring(response.content)
forms = parsed.cssselect('form[action="/dashboard/set_theme"]') forms = parsed.cssselect('form[action="/dashboard/set_palette"]')
self.assertEqual(len(forms), 1) self.assertEqual(len(forms), 1)
def test_active_theme_swatch_has_active_class(self): def test_active_palette_swatch_has_active_class(self):
response = self.client.get(self.url) response = self.client.get(self.url)
parsed = lxml.html.fromstring(response.content) parsed = lxml.html.fromstring(response.content)
[active] = parsed.cssselect(".swatch.active") [active] = parsed.cssselect(".swatch.active")
self.assertIn("theme-default", active.classes) self.assertIn("palette-default", active.classes)
def test_locked_themes_are_not_forms(self): def test_locked_palettes_are_not_forms(self):
response = self.client.get(self.url) response = self.client.get(self.url)
parsed = lxml.html.fromstring(response.content) parsed = lxml.html.fromstring(response.content)
locked = parsed.cssselect(".swatch.locked") locked = parsed.cssselect(".swatch.locked")
@@ -303,11 +303,11 @@ class SetThemeTest(TestCase):
for swatch in locked: for swatch in locked:
self.assertNotEqual(swatch.tag, "button") self.assertNotEqual(swatch.tag, "button")
def test_theme_picker_count_matches_context(self): def test_palette_picker_count_matches_context(self):
response = self.client.get(self.url) response = self.client.get(self.url)
parsed = lxml.html.fromstring(response.content) parsed = lxml.html.fromstring(response.content)
swatches = parsed.cssselect(".swatch") swatches = parsed.cssselect(".swatch")
self.assertEqual(len(swatches), len(response.context["themes"])) self.assertEqual(len(swatches), len(response.context["palettes"]))
class ProfileViewTest(TestCase): class ProfileViewTest(TestCase):
def setUp(self): def setUp(self):
@@ -341,3 +341,36 @@ class ProfileViewTest(TestCase):
[username_input] = parsed.cssselect("#id_new_username") [username_input] = parsed.cssselect("#id_new_username")
self.assertEqual("discoman", username_input.get("value")) self.assertEqual("discoman", username_input.get("value"))
class ToggleAppletsViewTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="disco@test.io")
self.client.force_login(self.user)
self.username_applet, _ = Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
self.palette_applet, _ = Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
self.url = reverse("toggle_applets")
def test_unauthenticated_user_is_redirected(self):
self.client.logout()
response = self.client.post(self.url)
self.assertRedirects(
response, f"/?next={self.url}", fetch_redirect_response=False
)
def test_unchecked_applet_gets_user_applet_with_visible_false(self):
self.client.post(self.url, {"applets": ["username"]})
ua = UserApplet.objects.get(user=self.user, applet=self.palette_applet)
self.assertFalse(ua.visible)
def test_redirects_on_normal_post(self):
response = self.client.post(
self.url, {"applets": ["username", "palette"]}
)
self.assertRedirects(response, reverse("home"), fetch_redirect_response=False)
def test_returns_200_on_htmx_post(self):
response = self.client.post(
self.url,
{"applets": ["username", "palette"]},
HTTP_HX_REQUEST="true",
)
self.assertEqual(response.status_code, 200)

View File

@@ -5,7 +5,8 @@ urlpatterns = [
path('new_list', views.new_list, name='new_list'), path('new_list', views.new_list, name='new_list'),
path('list/<uuid:list_id>/', views.view_list, name='view_list'), path('list/<uuid:list_id>/', views.view_list, name='view_list'),
path('list/<uuid:list_id>/share_list', views.share_list, name="share_list"), path('list/<uuid:list_id>/share_list', views.share_list, name="share_list"),
path('set_theme', views.set_theme, name='set_theme'), path('set_palette', views.set_palette, name='set_palette'),
path('set_profile', views.set_profile, name='set_profile'), path('set_profile', views.set_profile, name='set_profile'),
path('users/<uuid:user_id>/', views.my_lists, name='my_lists'), path('users/<uuid:user_id>/', views.my_lists, name='my_lists'),
path('toggle_applets', views.toggle_applets, name="toggle_applets"),
] ]

View File

@@ -1,18 +1,18 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponseForbidden from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from .forms import ExistingListItemForm, ItemForm from apps.dashboard.forms import ExistingListItemForm, ItemForm
from .models import Item, List from apps.dashboard.models import Applet, Item, List, UserApplet
from apps.lyric.models import User from apps.lyric.models import User
UNLOCKED_THEMES = frozenset(["theme-default"]) UNLOCKED_PALETTES = frozenset(["palette-default"])
THEMES = [ PALETTES = [
{"name": "theme-default", "label": "Earthman", "locked": False}, {"name": "palette-default", "label": "Earthman", "locked": False},
{"name": "theme-nirvana", "label": "Nirvana", "locked": True}, {"name": "palette-nirvana", "label": "Nirvana", "locked": True},
{"name": "theme-sheol", "label": "Sheol", "locked": True}, {"name": "palette-sheol", "label": "Sheol", "locked": True},
] ]
@@ -20,7 +20,7 @@ def home_page(request):
return render( return render(
request, "apps/dashboard/home.html", { request, "apps/dashboard/home.html", {
"form": ItemForm(), "form": ItemForm(),
"themes": THEMES, "palettes": PALETTES,
}) })
def new_list(request): def new_list(request):
@@ -74,12 +74,12 @@ def share_list(request, list_id):
return redirect(our_list) return redirect(our_list)
@login_required(login_url="/") @login_required(login_url="/")
def set_theme(request): def set_palette(request):
if request.method == "POST": if request.method == "POST":
theme = request.POST.get("theme", "") palette = request.POST.get("palette", "")
if theme in UNLOCKED_THEMES: if palette in UNLOCKED_PALETTES:
request.user.theme = theme request.user.palette = palette
request.user.save(update_fields=["theme"]) request.user.save(update_fields=["palette"])
return redirect("home") return redirect("home")
@login_required(login_url="/") @login_required(login_url="/")
@@ -89,3 +89,16 @@ def set_profile(request):
request.user.username = username request.user.username = username
request.user.save(update_fields=["username"]) request.user.save(update_fields=["username"])
return redirect("/") return redirect("/")
@login_required(login_url="/")
def toggle_applets(request):
checked = request.POST.getlist("applets")
for applet in Applet.objects.all():
UserApplet.objects.update_or_create(
user=request.user,
applet=applet,
defaults={"visible": applet.slug in checked},
)
if request.headers.get("HX-Request"):
return HttpResponse("")
return redirect("home")

View File

@@ -0,0 +1,22 @@
# Generated by Django 6.0 on 2026-03-05 19:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0003_user_theme'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='theme',
),
migrations.AddField(
model_name='user',
name='palette',
field=models.CharField(default='palette-default', max_length=32),
),
]

View File

@@ -26,7 +26,7 @@ class User(AbstractBaseUser):
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
username = models.CharField(max_length=35, unique=True, null=True, blank=True) username = models.CharField(max_length=35, unique=True, null=True, blank=True)
searchable = models.BooleanField(default=False) searchable = models.BooleanField(default=False)
theme = models.CharField(max_length=32, default="theme-default") palette = models.CharField(max_length=32, default="palette-default")
is_staff = models.BooleanField(default=False) is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False)

View File

@@ -51,7 +51,7 @@ class UserManagerTest(TestCase):
) )
self.assertTrue(user.check_password("correct-password")) self.assertTrue(user.check_password("correct-password"))
class UserThemeTest(TestCase): class UserPaletteTest(TestCase):
def test_theme_field_defaults_to_theme_default(self): def test_palette_field_defaults_to_palette_default(self):
user = User.objects.create(email="a@b.cde") user = User.objects.create(email="a@b.cde")
self.assertEqual(user.theme, "theme-default") self.assertEqual(user.palette, "palette-default")

View File

@@ -1,4 +1,4 @@
def user_theme(request): def user_palette(request):
if request.user.is_authenticated: if request.user.is_authenticated:
return {"user_theme": request.user.theme} return {"user_palette": request.user.palette}
return {"user_theme": "theme-default"} return {"user_palette": "palette-default"}

View File

@@ -92,7 +92,7 @@ TEMPLATES = [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'core.context_processors.user_theme', 'core.context_processors.user_palette',
], ],
}, },
}, },

View File

@@ -4,7 +4,7 @@ from .base import FunctionalTest
class SiteThemeTest(FunctionalTest): class SiteThemeTest(FunctionalTest):
def test_page_renders_with_earthman_theme(self): def test_page_renders_with_earthman_palette(self):
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
body = self.browser.find_element(By.TAG_NAME, "body") body = self.browser.find_element(By.TAG_NAME, "body")
self.assertIn("theme-default", body.get_attribute("class")) self.assertIn("palette-default", body.get_attribute("class"))

View File

@@ -43,9 +43,9 @@ class DashboardMaintenanceTest(FunctionalTest):
# 1. Auth as discoman@example.com, navigate home # 1. Auth as discoman@example.com, navigate home
self.create_pre_authenticated_session("discoman@example.com") self.create_pre_authenticated_session("discoman@example.com")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
# 2. Assert both applets present on page (id_applet_username, id_applet_theme_switcher) # 2. Assert both applets present on page (id_applet_username, id_applet_palette)
self.browser.find_element(By.ID, "id_applet_username") self.browser.find_element(By.ID, "id_applet_username")
self.browser.find_element(By.ID, "id_applet_theme_switcher") self.browser.find_element(By.ID, "id_applet_palette")
# 3. Click el w. id="id_dash_gear" # 3. Click el w. id="id_dash_gear"
dash_gear = self.browser.find_element(By.ID, "id_dash_gear") dash_gear = self.browser.find_element(By.ID, "id_dash_gear")
dash_gear.click() dash_gear.click()
@@ -53,15 +53,15 @@ class DashboardMaintenanceTest(FunctionalTest):
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.ID, "id_applet_menu") lambda: self.browser.find_element(By.ID, "id_applet_menu")
) )
# 5. Find two checkboxes in menu, name="username" & name="theme-switcher"; assert both .is_selected() # 5. Find two checkboxes in menu, name="username" & name="palette"; assert both .is_selected()
menu = self.browser.find_element(By.ID, "id_applet_menu") menu = self.browser.find_element(By.ID, "id_applet_menu")
username_cb = menu.find_element(By.NAME, "username") username_cb = menu.find_element(By.NAME, "username")
theme_cb = menu.find_element(By.NAME, "theme-switcher") palette_cb = menu.find_element(By.NAME, "palette")
self.assertTrue(username_cb.is_selected()) self.assertTrue(username_cb.is_selected())
self.assertTrue(theme_cb.is_selected()) self.assertTrue(palette_cb.is_selected())
# 6. Click theme-switcher box to uncheck it # 6. Click palette box to uncheck it
theme_cb.click() palette_cb.click()
self.assertFalse(theme_cb.is_selected()) self.assertFalse(palette_cb.is_selected())
self.browser.execute_script("window.__no_reload_marker = True") self.browser.execute_script("window.__no_reload_marker = True")
# 7. Submit the menu form via [type="submit"] btn inside menu # 7. Submit the menu form via [type="submit"] btn inside menu
menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click() menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click()
@@ -70,24 +70,24 @@ class DashboardMaintenanceTest(FunctionalTest):
self.browser.find_element(By.ID, "id_applet_menu").is_displayed() self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
) )
) )
# 8. wait_for theme-switcher applet to be gone # 8. wait_for palette applet to be gone
self.wait_for( self.wait_for(
lambda: self.assertRaises( lambda: self.assertRaises(
NoSuchElementException, NoSuchElementException,
self.browser.find_element, self.browser.find_element,
By.ID, "id_applet_theme_switcher" By.ID, "id_applet_palette"
) )
) )
# 9. assert id_applet_username remains # 9. assert id_applet_username remains
self.browser.find_element(By.ID, "id_applet_username") self.browser.find_element(By.ID, "id_applet_username")
# 10. Click gear again, find menu, find theme-switcher checkbox; assert now NOT selected # 10. Click gear again, find menu, find palette checkbox; assert now NOT selected
dash_gear.click() dash_gear.click()
self.assertFalse(theme_cb.is_selected()) self.assertFalse(palette_cb.is_selected())
# 11. Click it to re-check box; submit # 11. Click it to re-check box; submit
theme_cb.click() palette_cb.click()
self.assertTrue(theme_cb.is_selected()) self.assertTrue(palette_cb.is_selected())
menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click() menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click()
# 12. wait_for id_applet_theme_switcher to reappear # 12. wait_for id_applet_palette to reappear
self.wait_for( self.wait_for(
lambda: self.assertFalse( lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_applet_menu").is_displayed() self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
@@ -95,7 +95,7 @@ class DashboardMaintenanceTest(FunctionalTest):
) )
self.wait_for( self.wait_for(
lambda: self.assertTrue( lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_applet_theme_switcher") self.browser.find_element(By.ID, "id_applet_palette")
) )
) )
self.assertTrue(self.browser.execute_script("return window.__no_reload_marker === true")) self.assertTrue(self.browser.execute_script("return window.__no_reload_marker === true"))

View File

@@ -1,9 +1,9 @@
.theme-picker { .palette-picker {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
} }
.theme-picker-item { .palette-picker-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View File

@@ -1,7 +1,7 @@
@import 'rootvars'; @import 'rootvars';
@import 'base'; @import 'base';
@import 'button-pad'; @import 'button-pad';
@import 'theme-picker'; @import 'palette-picker';
input, input,

View File

@@ -301,8 +301,8 @@
--decClh: 255, 174, 0; --decClh: 255, 174, 0;
} }
/* Default Earthman Theme */ /* Default Earthman Palette */
.theme-default { .palette-default {
--priUser: var(--terBrk); --priUser: var(--terBrk);
--secUser: var(--priKhk); --secUser: var(--priKhk);
--terUser: var(--priMze); --terUser: var(--priMze);
@@ -314,8 +314,8 @@
--ninUser: var(--priCtn); --ninUser: var(--priCtn);
--decUser: var(--terCtn); --decUser: var(--terCtn);
} }
/* Grave Sheol Theme */ /* Grave Sheol Palette */
.theme-sheol { .palette-sheol {
--priUser: var(--priPu); --priUser: var(--priPu);
--secUser: var(--quiPu); --secUser: var(--quiPu);
--terUser: var(--terFs); --terUser: var(--terFs);
@@ -327,8 +327,8 @@
--ninUser: var(--sixPu); --ninUser: var(--sixPu);
--decUser: var(--terPu); --decUser: var(--terPu);
} }
/* Blissful Nirvana Theme */ /* Blissful Nirvana Palette */
.theme-nirvana { .palette-nirvana {
--priUser: var(--priU); --priUser: var(--priU);
--secUser: var(--quiU); --secUser: var(--quiU);
--terUser: var(--terMe); --terUser: var(--terMe);
@@ -340,8 +340,8 @@
--ninUser: var(--sixCu); --ninUser: var(--sixCu);
--decUser: var(--terU); --decUser: var(--terU);
} }
/* Disco Inferno Theme */ /* Disco Inferno Palette */
.theme-inferno { .palette-inferno {
--priUser: var(--quaSwp); --priUser: var(--quaSwp);
--secUser: var(--priSwp); --secUser: var(--priSwp);
--terUser: var(--terBld); --terUser: var(--terBld);
@@ -353,8 +353,8 @@
--ninUser: var(--priMst); --ninUser: var(--priMst);
--decUser: var(--terMst); --decUser: var(--terMst);
} }
/* Torre Terrestre Theme */ /* Torre Terrestre Palette */
.theme-terrestre { .palette-terrestre {
--priUser: var(--priAdm); --priUser: var(--priAdm);
--secUser: var(--quaAdm); --secUser: var(--quaAdm);
--terUser: var(--sixAdm); --terUser: var(--sixAdm);
@@ -366,8 +366,8 @@
--ninUser: var(--sixPer); --ninUser: var(--sixPer);
--decUser: var(--terMrb); --decUser: var(--terMrb);
} }
/* Fantastia Celestia Theme */ /* Fantastia Celestia Palette */
.theme-celestia { .palette-celestia {
--priUser: var(--octClh); --priUser: var(--octClh);
--secUser: var(--sixClh); --secUser: var(--sixClh);
--terUser: var(--quaClh); --terUser: var(--quaClh);
@@ -380,7 +380,7 @@
--decUser: var(--quiClh); --decUser: var(--quiClh);
} }
/* Theme Classes */ /* Palette Classes */
.priUser { .priUser {
color: rgba(var(--priUser), 1); color: rgba(var(--priUser), 1);
} }

View File

@@ -15,14 +15,14 @@
{% block content %} {% block content %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<section id="id_applet_theme_switcher" class="theme-picker"> <section id="id_applet_palette" class="palette">
{% for theme in themes %} {% for palette in palettes %}
<div class="theme-picker-item"> <div class="palette-item">
<div class="swatch {{ theme.name }}{% if user_theme == theme.name %} active{% endif %}{% if theme.locked %} locked{% endif %}"></div> <div class="swatch {{ palette.name }}{% if user_palette == palette.name %} active{% endif %}{% if palette.locked %} locked{% endif %}"></div>
{% if not theme.locked %} {% if not palette.locked %}
<form method="POST" action="{% url 'set_theme' %}"> <form method="POST" action="{% url 'set_palette' %}">
{% csrf_token %} {% csrf_token %}
<button type="submit" name="theme" value="{{ theme.name }}" class="btn btn-confirm">OK</button> <button type="submit" name="palette" value="{{ palette.name }}" class="btn btn-confirm">OK</button>
</form> </form>
{% else %} {% else %}
<span class="btn btn-disabled">&times;</span> <span class="btn btn-disabled">&times;</span>

View File

@@ -16,7 +16,7 @@
{% endcompress %} {% endcompress %}
</head> </head>
<body class="{{ user_theme }}"> <body class="{{ user_palette }}">
<div class="container"> <div class="container">
<nav class="navbar"> <nav class="navbar">
<div class="container-fluid"> <div class="container-fluid">