From c0994797400fd16f39d470ec7bb95c362d493f4a Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Thu, 5 Mar 2026 14:45:55 -0500 Subject: [PATCH] 'theme_switcher,' 'theme-picker' & 'theme' renamed everywhere to simply 'palette'; new urls & views & their corresponding ITs ensure applet menu checkbox functionality --- .../dashboard/tests/integrated/test_models.py | 2 +- .../dashboard/tests/integrated/test_views.py | 71 ++++++++++++++----- src/apps/dashboard/urls.py | 3 +- src/apps/dashboard/views.py | 41 +++++++---- .../0004_remove_user_theme_user_palette.py | 22 ++++++ src/apps/lyric/models.py | 2 +- .../lyric/tests/integrated/test_models.py | 6 +- src/core/context_processors.py | 6 +- src/core/settings.py | 2 +- .../{test_theme.py => test_applet_palette.py} | 4 +- src/functional_tests/test_dashboard.py | 32 ++++----- ...theme-picker.scss => _palette-picker.scss} | 4 +- src/static_src/scss/core.scss | 2 +- src/static_src/scss/rootvars.scss | 26 +++---- src/templates/apps/dashboard/home.html | 14 ++-- src/templates/core/base.html | 2 +- 16 files changed, 154 insertions(+), 85 deletions(-) create mode 100644 src/apps/lyric/migrations/0004_remove_user_theme_user_palette.py rename src/functional_tests/{test_theme.py => test_applet_palette.py} (64%) rename src/static_src/scss/{_theme-picker.scss => _palette-picker.scss} (94%) diff --git a/src/apps/dashboard/tests/integrated/test_models.py b/src/apps/dashboard/tests/integrated/test_models.py index 72355b3..c4c574f 100644 --- a/src/apps/dashboard/tests/integrated/test_models.py +++ b/src/apps/dashboard/tests/integrated/test_models.py @@ -82,7 +82,7 @@ class AppletModelTest(TestCase): class UserAppletModelTest(TestCase): def setUp(self): 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): ua = UserApplet.objects.create(user=self.user, applet=self.applet, visible=True) diff --git a/src/apps/dashboard/tests/integrated/test_views.py b/src/apps/dashboard/tests/integrated/test_views.py index 87c1529..b24c357 100644 --- a/src/apps/dashboard/tests/integrated/test_views.py +++ b/src/apps/dashboard/tests/integrated/test_views.py @@ -9,7 +9,7 @@ from apps.dashboard.forms import ( DUPLICATE_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 @@ -256,45 +256,45 @@ class ViewAuthListTest(TestCase): response = self.client.get(reverse("view_list", args=[self.our_list.id])) self.assertEqual(response.status_code, 200) -class SetThemeTest(TestCase): +class SetPaletteTest(TestCase): def setUp(self): self.user = User.objects.create(email="a@b.cde") self.client.force_login(self.user) self.url = reverse("home") 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, "/") - def test_set_theme_updates_user_theme(self): - User.objects.filter(pk=self.user.pk).update(theme="theme-sheol") - self.client.post("/dashboard/set_theme", data={"theme": "theme-default"}) + def test_set_palette_updates_user_palette(self): + User.objects.filter(pk=self.user.pk).update(palette="palette-sheol") + self.client.post("/dashboard/set_palette", data={"palette": "palette-default"}) 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): - response = self.client.post("/dashboard/set_theme", data={"theme": "theme-nirvana"}) + def test_locked_palette_is_rejected(self): + response = self.client.post("/dashboard/set_palette", data={"palette": "palette-nirvana"}) self.user.refresh_from_db() - self.assertEqual(self.user.theme, "theme-default") + self.assertEqual(self.user.palette, "palette-default") self.assertRedirects(response, "/") - def test_set_theme_redirects_home(self): - response = self.client.post("/dashboard/set_theme", data={"theme": "theme-default"}) + def test_set_palette_redirects_home(self): + response = self.client.post("/dashboard/set_palette", data={"palette": "palette-default"}) 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) 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) - def test_active_theme_swatch_has_active_class(self): + def test_active_palette_swatch_has_active_class(self): response = self.client.get(self.url) parsed = lxml.html.fromstring(response.content) [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) parsed = lxml.html.fromstring(response.content) locked = parsed.cssselect(".swatch.locked") @@ -303,11 +303,11 @@ class SetThemeTest(TestCase): for swatch in locked: 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) parsed = lxml.html.fromstring(response.content) swatches = parsed.cssselect(".swatch") - self.assertEqual(len(swatches), len(response.context["themes"])) + self.assertEqual(len(swatches), len(response.context["palettes"])) class ProfileViewTest(TestCase): def setUp(self): @@ -341,3 +341,36 @@ class ProfileViewTest(TestCase): [username_input] = parsed.cssselect("#id_new_username") 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) diff --git a/src/apps/dashboard/urls.py b/src/apps/dashboard/urls.py index 24a65dd..9ac721b 100644 --- a/src/apps/dashboard/urls.py +++ b/src/apps/dashboard/urls.py @@ -5,7 +5,8 @@ urlpatterns = [ path('new_list', views.new_list, name='new_list'), path('list//', views.view_list, name='view_list'), path('list//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('users//', views.my_lists, name='my_lists'), + path('toggle_applets', views.toggle_applets, name="toggle_applets"), ] diff --git a/src/apps/dashboard/views.py b/src/apps/dashboard/views.py index d8c3b58..355684a 100644 --- a/src/apps/dashboard/views.py +++ b/src/apps/dashboard/views.py @@ -1,18 +1,18 @@ from django.contrib import messages 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 .forms import ExistingListItemForm, ItemForm -from .models import Item, List +from apps.dashboard.forms import ExistingListItemForm, ItemForm +from apps.dashboard.models import Applet, Item, List, UserApplet from apps.lyric.models import User -UNLOCKED_THEMES = frozenset(["theme-default"]) -THEMES = [ - {"name": "theme-default", "label": "Earthman", "locked": False}, - {"name": "theme-nirvana", "label": "Nirvana", "locked": True}, - {"name": "theme-sheol", "label": "Sheol", "locked": True}, +UNLOCKED_PALETTES = frozenset(["palette-default"]) +PALETTES = [ + {"name": "palette-default", "label": "Earthman", "locked": False}, + {"name": "palette-nirvana", "label": "Nirvana", "locked": True}, + {"name": "palette-sheol", "label": "Sheol", "locked": True}, ] @@ -20,7 +20,7 @@ def home_page(request): return render( request, "apps/dashboard/home.html", { "form": ItemForm(), - "themes": THEMES, + "palettes": PALETTES, }) def new_list(request): @@ -74,12 +74,12 @@ def share_list(request, list_id): return redirect(our_list) @login_required(login_url="/") -def set_theme(request): +def set_palette(request): if request.method == "POST": - theme = request.POST.get("theme", "") - if theme in UNLOCKED_THEMES: - request.user.theme = theme - request.user.save(update_fields=["theme"]) + palette = request.POST.get("palette", "") + if palette in UNLOCKED_PALETTES: + request.user.palette = palette + request.user.save(update_fields=["palette"]) return redirect("home") @login_required(login_url="/") @@ -89,3 +89,16 @@ def set_profile(request): request.user.username = username request.user.save(update_fields=["username"]) 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") diff --git a/src/apps/lyric/migrations/0004_remove_user_theme_user_palette.py b/src/apps/lyric/migrations/0004_remove_user_theme_user_palette.py new file mode 100644 index 0000000..d723d12 --- /dev/null +++ b/src/apps/lyric/migrations/0004_remove_user_theme_user_palette.py @@ -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), + ), + ] diff --git a/src/apps/lyric/models.py b/src/apps/lyric/models.py index efb92da..c9765ed 100644 --- a/src/apps/lyric/models.py +++ b/src/apps/lyric/models.py @@ -26,7 +26,7 @@ class User(AbstractBaseUser): email = models.EmailField(unique=True) username = models.CharField(max_length=35, unique=True, null=True, blank=True) 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_superuser = models.BooleanField(default=False) diff --git a/src/apps/lyric/tests/integrated/test_models.py b/src/apps/lyric/tests/integrated/test_models.py index ce5240d..a3ae0dd 100644 --- a/src/apps/lyric/tests/integrated/test_models.py +++ b/src/apps/lyric/tests/integrated/test_models.py @@ -51,7 +51,7 @@ class UserManagerTest(TestCase): ) self.assertTrue(user.check_password("correct-password")) -class UserThemeTest(TestCase): - def test_theme_field_defaults_to_theme_default(self): +class UserPaletteTest(TestCase): + def test_palette_field_defaults_to_palette_default(self): user = User.objects.create(email="a@b.cde") - self.assertEqual(user.theme, "theme-default") + self.assertEqual(user.palette, "palette-default") diff --git a/src/core/context_processors.py b/src/core/context_processors.py index d7e1fd1..5b629a8 100644 --- a/src/core/context_processors.py +++ b/src/core/context_processors.py @@ -1,4 +1,4 @@ -def user_theme(request): +def user_palette(request): if request.user.is_authenticated: - return {"user_theme": request.user.theme} - return {"user_theme": "theme-default"} \ No newline at end of file + return {"user_palette": request.user.palette} + return {"user_palette": "palette-default"} \ No newline at end of file diff --git a/src/core/settings.py b/src/core/settings.py index 3fcc9c5..def6231 100644 --- a/src/core/settings.py +++ b/src/core/settings.py @@ -92,7 +92,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - 'core.context_processors.user_theme', + 'core.context_processors.user_palette', ], }, }, diff --git a/src/functional_tests/test_theme.py b/src/functional_tests/test_applet_palette.py similarity index 64% rename from src/functional_tests/test_theme.py rename to src/functional_tests/test_applet_palette.py index d12cd70..f3f34d2 100644 --- a/src/functional_tests/test_theme.py +++ b/src/functional_tests/test_applet_palette.py @@ -4,7 +4,7 @@ from .base import 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) 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")) diff --git a/src/functional_tests/test_dashboard.py b/src/functional_tests/test_dashboard.py index 3681a93..220e0e1 100644 --- a/src/functional_tests/test_dashboard.py +++ b/src/functional_tests/test_dashboard.py @@ -43,9 +43,9 @@ class DashboardMaintenanceTest(FunctionalTest): # 1. Auth as discoman@example.com, navigate home self.create_pre_authenticated_session("discoman@example.com") 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_theme_switcher") + self.browser.find_element(By.ID, "id_applet_palette") # 3. Click el w. id="id_dash_gear" dash_gear = self.browser.find_element(By.ID, "id_dash_gear") dash_gear.click() @@ -53,15 +53,15 @@ class DashboardMaintenanceTest(FunctionalTest): self.wait_for( 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") 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(theme_cb.is_selected()) - # 6. Click theme-switcher box to uncheck it - theme_cb.click() - self.assertFalse(theme_cb.is_selected()) + self.assertTrue(palette_cb.is_selected()) + # 6. Click palette box to uncheck it + palette_cb.click() + self.assertFalse(palette_cb.is_selected()) self.browser.execute_script("window.__no_reload_marker = True") # 7. Submit the menu form via [type="submit"] btn inside menu 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() ) ) - # 8. wait_for theme-switcher applet to be gone + # 8. wait_for palette applet to be gone self.wait_for( lambda: self.assertRaises( NoSuchElementException, self.browser.find_element, - By.ID, "id_applet_theme_switcher" + By.ID, "id_applet_palette" ) ) # 9. assert id_applet_username remains 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() - self.assertFalse(theme_cb.is_selected()) + self.assertFalse(palette_cb.is_selected()) # 11. Click it to re-check box; submit - theme_cb.click() - self.assertTrue(theme_cb.is_selected()) + palette_cb.click() + self.assertTrue(palette_cb.is_selected()) 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( lambda: self.assertFalse( self.browser.find_element(By.ID, "id_applet_menu").is_displayed() @@ -95,7 +95,7 @@ class DashboardMaintenanceTest(FunctionalTest): ) self.wait_for( 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")) diff --git a/src/static_src/scss/_theme-picker.scss b/src/static_src/scss/_palette-picker.scss similarity index 94% rename from src/static_src/scss/_theme-picker.scss rename to src/static_src/scss/_palette-picker.scss index 4624532..67cc4dd 100644 --- a/src/static_src/scss/_theme-picker.scss +++ b/src/static_src/scss/_palette-picker.scss @@ -1,9 +1,9 @@ -.theme-picker { +.palette-picker { display: flex; gap: 1rem; } -.theme-picker-item { +.palette-picker-item { display: flex; flex-direction: column; align-items: center; diff --git a/src/static_src/scss/core.scss b/src/static_src/scss/core.scss index 89aadb2..539c5de 100644 --- a/src/static_src/scss/core.scss +++ b/src/static_src/scss/core.scss @@ -1,7 +1,7 @@ @import 'rootvars'; @import 'base'; @import 'button-pad'; -@import 'theme-picker'; +@import 'palette-picker'; input, diff --git a/src/static_src/scss/rootvars.scss b/src/static_src/scss/rootvars.scss index e5c1764..9f78616 100644 --- a/src/static_src/scss/rootvars.scss +++ b/src/static_src/scss/rootvars.scss @@ -301,8 +301,8 @@ --decClh: 255, 174, 0; } -/* Default Earthman Theme */ -.theme-default { +/* Default Earthman Palette */ +.palette-default { --priUser: var(--terBrk); --secUser: var(--priKhk); --terUser: var(--priMze); @@ -314,8 +314,8 @@ --ninUser: var(--priCtn); --decUser: var(--terCtn); } -/* Grave Sheol Theme */ -.theme-sheol { +/* Grave Sheol Palette */ +.palette-sheol { --priUser: var(--priPu); --secUser: var(--quiPu); --terUser: var(--terFs); @@ -327,8 +327,8 @@ --ninUser: var(--sixPu); --decUser: var(--terPu); } -/* Blissful Nirvana Theme */ -.theme-nirvana { +/* Blissful Nirvana Palette */ +.palette-nirvana { --priUser: var(--priU); --secUser: var(--quiU); --terUser: var(--terMe); @@ -340,8 +340,8 @@ --ninUser: var(--sixCu); --decUser: var(--terU); } -/* Disco Inferno Theme */ -.theme-inferno { +/* Disco Inferno Palette */ +.palette-inferno { --priUser: var(--quaSwp); --secUser: var(--priSwp); --terUser: var(--terBld); @@ -353,8 +353,8 @@ --ninUser: var(--priMst); --decUser: var(--terMst); } -/* Torre Terrestre Theme */ -.theme-terrestre { +/* Torre Terrestre Palette */ +.palette-terrestre { --priUser: var(--priAdm); --secUser: var(--quaAdm); --terUser: var(--sixAdm); @@ -366,8 +366,8 @@ --ninUser: var(--sixPer); --decUser: var(--terMrb); } -/* Fantastia Celestia Theme */ -.theme-celestia { +/* Fantastia Celestia Palette */ +.palette-celestia { --priUser: var(--octClh); --secUser: var(--sixClh); --terUser: var(--quaClh); @@ -380,7 +380,7 @@ --decUser: var(--quiClh); } -/* Theme Classes */ +/* Palette Classes */ .priUser { color: rgba(var(--priUser), 1); } diff --git a/src/templates/apps/dashboard/home.html b/src/templates/apps/dashboard/home.html index e55146b..de24dbd 100644 --- a/src/templates/apps/dashboard/home.html +++ b/src/templates/apps/dashboard/home.html @@ -15,14 +15,14 @@ {% block content %} {% if user.is_authenticated %} -
- {% for theme in themes %} -
-
- {% if not theme.locked %} -
+
+ {% for palette in palettes %} +
+
+ {% if not palette.locked %} + {% csrf_token %} - + {% else %} × diff --git a/src/templates/core/base.html b/src/templates/core/base.html index 899f81f..aa5d36c 100644 --- a/src/templates/core/base.html +++ b/src/templates/core/base.html @@ -16,7 +16,7 @@ {% endcompress %} - +