Compare commits

...

4 Commits

23 changed files with 445 additions and 95 deletions

View File

@@ -8,6 +8,7 @@ cssselect==1.3.0
dj-database-url
Django==6.0
django-compressor
django-htmx
django-libsass
django-stubs==5.2.8
django-stubs-ext==5.2.8

View File

@@ -3,6 +3,7 @@ cssselect==1.3.0
Django==6.0
dj-database-url
django-compressor
django-htmx
django-libsass
django-stubs==5.2.8
django-stubs-ext==5.2.8

View File

@@ -0,0 +1,37 @@
# Generated by Django 6.0 on 2026-03-04 20:34
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Applet',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(unique=True)),
('name', models.CharField(max_length=100)),
('default_visible', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='UserApplet',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('visible', models.BooleanField(default=True)),
('applet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dashboard.applet')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_applets', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'applet')},
},
),
]

View File

@@ -0,0 +1,17 @@
from django.db import migrations
def seed_applets(apps, schema_editor):
Applet = apps.get_model("dashboard", "Applet")
Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
class Migration(migrations.Migration):
dependencies = [
("dashboard", "0002_applet_userapplet"),
]
operations = [
migrations.RunPython(seed_applets, migrations.RunPython.noop),
]

View File

@@ -38,3 +38,26 @@ class Item(models.Model):
def __str__(self):
return self.text
class Applet(models.Model):
slug = models.SlugField(unique=True)
name = models.CharField(max_length=100)
default_visible = models.BooleanField(default=True)
def __str__(self):
return self.name
class UserApplet(models.Model):
user = models.ForeignKey(
"lyric.User",
related_name="user_applets",
on_delete=models.CASCADE,
)
applet = models.ForeignKey(
Applet,
on_delete=models.CASCADE,
)
visible = models.BooleanField(default=True)
class Meta:
unique_together = ("user", "applet")

View File

@@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
from django.test import TestCase
from apps.dashboard.models import Item, List
from apps.dashboard.models import Applet, Item, List, UserApplet
from apps.lyric.models import User
@@ -68,3 +68,27 @@ class ListModelTest(TestCase):
Item.objects.create(list=list_, text="first item")
Item.objects.create(list=list_, text="second item")
self.assertEqual(list_.name, "first item")
class AppletModelTest(TestCase):
def test_applet_can_be_created(self):
applet = Applet.objects.create(slug="my-applet", name="My Applet", default_visible=True)
self.assertEqual(Applet.objects.get(slug="my-applet"), applet)
def test_applet_slug_is_unique(self):
Applet.objects.create(slug="my-applet", name="First")
with self.assertRaises(IntegrityError):
Applet.objects.create(slug="my-applet", name="Second")
class UserAppletModelTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="a@b.cde")
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)
self.assertIn(ua, self.user.user_applets.all())
def test_user_applet_unique_per_user_and_applet(self):
UserApplet.objects.create(user=self.user, applet=self.applet, visible=True)
with self.assertRaises(IntegrityError):
UserApplet.objects.create(user=self.user, applet=self.applet, visible=False)

View File

@@ -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
@@ -210,7 +210,11 @@ class ShareListTest(TestCase):
f"/dashboard/list/{our_list.id}/share_list",
data={"recipient": "nobody@example.com"},
)
self.assertRedirects(response, f"/dashboard/list/{our_list.id}/")
self.assertRedirects(
response,
f"/dashboard/list/{our_list.id}/",
fetch_redirect_response=False,
)
def test_share_list_does_not_add_owner_as_recipient(self):
owner = User.objects.create(email="owner@example.com")
@@ -256,45 +260,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 +307,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 +345,60 @@ 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)
def test_htmx_post_renders_visible_applets_only(self):
response = self.client.post(
self.url,
{"applets": ["username"]},
HTTP_HX_REQUEST="true",
)
parsed = lxml.html.fromstring(response.content)
self.assertEqual(len(parsed.cssselect("#id_applet_username")), 1)
self.assertEqual(len(parsed.cssselect("#id_applet_palette")), 0)
class AppletVisibilityContextTest(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"})
UserApplet.objects.create(user=self.user, applet=self.palette_applet, visible=False)
def test_dash_reflects_user_applet_visibility(self):
response = self.client.get("/")
applet_map = {entry["applet"].slug: entry["visible"] for entry in response.context["applets"]}
self.assertFalse(applet_map["palette"])
self.assertTrue(applet_map["username"])

View File

@@ -5,7 +5,8 @@ urlpatterns = [
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>/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/<uuid:user_id>/', views.my_lists, name='my_lists'),
path('toggle_applets', views.toggle_applets, name="toggle_applets"),
]

View File

@@ -1,27 +1,33 @@
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},
]
def _applet_context(user):
ua_map = {ua.applet_id: ua.visible for ua in user.user_applets.all()}
return [
{"applet": applet, "visible": ua_map.get(applet.pk, applet.default_visible)}
for applet in Applet.objects.all()
]
def home_page(request):
return render(
request, "apps/dashboard/home.html", {
"form": ItemForm(),
"themes": THEMES,
})
context = {"form": ItemForm(), "palettes": PALETTES}
if request.user.is_authenticated:
context["applets"] = _applet_context(request.user)
return render(request, "apps/dashboard/home.html", context)
def new_list(request):
form = ItemForm(data=request.POST)
@@ -74,12 +80,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 +95,19 @@ 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 render(request, "apps/dashboard/_partials/_applets.html", {
"applets": _applet_context(request.user),
"palettes": PALETTES,
})
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)
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)

View File

@@ -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")

View File

@@ -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"}
return {"user_palette": request.user.palette}
return {"user_palette": "palette-default"}

View File

@@ -77,6 +77,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_htmx.middleware.HtmxMiddleware',
]
ROOT_URLCONF = 'core.urls'
@@ -91,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',
],
},
},

View File

@@ -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"))

View File

@@ -1,10 +1,17 @@
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from .base import FunctionalTest
from apps.dashboard.models import Applet
class DashboardMaintenanceTest(FunctionalTest):
def setUp(self):
super().setUp()
Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
def test_user_without_username_can_claim_unclaimed_username(self):
# 1. Create a pre-authenticated session for discoman@example.com
self.create_pre_authenticated_session("discoman@example.com")
@@ -37,3 +44,73 @@ class DashboardMaintenanceTest(FunctionalTest):
self.browser.find_element(By.CSS_SELECTOR, "#id_new_username").get_attribute("value")
)
)
def test_user_can_toggle_applet_visibility_via_gear_menu(self):
# 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_palette)
self.browser.find_element(By.ID, "id_applet_username")
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()
# 4. A menu appears; wait_for el w. id="id_applet_menu"
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
)
)
# 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.CSS_SELECTOR, '[name="applets"][value="username"]')
palette_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="palette"]')
self.assertTrue(username_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()
self.wait_for(
lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
)
)
# 8. wait_for palette applet to be gone
self.wait_for(
lambda: self.assertRaises(
NoSuchElementException,
self.browser.find_element,
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 palette checkbox; assert now NOT selected
dash_gear.click()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
)
)
menu = self.browser.find_element(By.ID, "id_applet_menu")
palette_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="palette"]')
self.assertFalse(palette_cb.is_selected())
# 11. Click it to re-check box; submit
palette_cb.click()
self.assertTrue(palette_cb.is_selected())
menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click()
# 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()
)
)
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_applet_palette")
)
)
self.assertTrue(self.browser.execute_script("return window.__no_reload_marker === true"))

View File

@@ -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;

View File

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

View File

@@ -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);
}

1
src/static_src/vendor/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,61 @@
{% load lyric_extras %}
<div id="id_applets_container">
<div id="id_applet_menu" style="display:none;">
<form
hx-post="{% url "toggle_applets" %}"
hx-target="#id_applets_container"
hx-swap="outerHTML"
>
{% csrf_token %}
{% for entry in applets %}
<label>
<input
type="checkbox"
name="applets"
value="{{ entry.applet.slug }}"
{% if entry.visible %}checked{% endif %}
>
{{ entry.applet.name }}
</label>
{% endfor %}
<button type="submit" class="btn btn-confirm">OK</button>
</form>
</div>
{% for entry in applets %}
{% if entry.visible %}
{% if entry.applet.slug == "username" %}
<section id="id_applet_username">
<h1>{{ user|display_name }}</h1>
<div class="form-container">
<form method="POST" action="{% url "set_profile" %}">
{% csrf_token %}
<input
id="id_new_username"
name="username"
required
value="{{ user.username|default:'' }}"
>
</form>
</div>
</section>
{% elif entry.applet.slug == "palette" %}
<section id="id_applet_palette" class="palette">
{% for palette in palettes %}
<div class="palette-item">
<div class="swatch {{ palette.name }}{% if user_palette == palette.name %} active{% endif %}{% if palette.locked %} locked{% endif %}"></div>
{% if not palette.locked %}
<form method="POST" action="{% url "set_palette" %}">
{% csrf_token %}
<button type="submit" name="palette" value="{{ palette.name }}" class="btn btn-confirm">OK</button>
</form>
{% else %}
<span class="btn btn-disabled">&times;</span>
{% endif %}
</div>
{% endfor %}
</section>
{% endif %}
{% endif %}
{% endfor %}
</div>

View File

@@ -15,29 +15,12 @@
{% block content %}
{% if user.is_authenticated %}
<section class="theme-picker">
{% for theme in themes %}
<div class="theme-picker-item">
<div class="swatch {{ theme.name }}{% if user_theme == theme.name %} active{% endif %}{% if theme.locked %} locked{% endif %}"></div>
{% if not theme.locked %}
<form method="POST" action="{% url 'set_theme' %}">
{% csrf_token %}
<button type="submit" name="theme" value="{{ theme.name }}" class="btn btn-confirm">OK</button>
</form>
{% else %}
<span class="btn btn-disabled">&times;</span>
{% endif %}
</div>
{% endfor %}
</section>
<section id="id_applet_username">
<h1>{{ user|display_name }}</h1>
<div class="form-container">
<form method="POST" action="{% url "set_profile" %}">
{% csrf_token %}
<input id="id_new_username" name="username" required value="{{ user.username|default:'' }}">
</form>
</div>
</section>
<button
id="id_dash_gear"
onclick="document.getElementById('id_applet_menu').style.display='block'"
>
&#9881;
</button>
{% include "apps/dashboard/_partials/_applets.html" %}
{% endif %}
{% endblock content %}

View File

@@ -16,7 +16,7 @@
{% endcompress %}
</head>
<body class="{{ user_theme }}">
<body class="{{ user_palette }}">
<div class="container">
<nav class="navbar">
<div class="container-fluid">
@@ -77,9 +77,27 @@
{% endblock content %}
</div>
</body>
{% block scripts %}
{% endblock scripts %}
<script src="{% static "vendor/htmx.min.js" %}"></script>
<script>
document.body.addEventListener('htmx:configRequest', function(evt) {
evt.detail.headers['X-CSRFToken'] = getCookie('csrftoken');
});
function getCookie(name) {
let val = null;
if (document.cookie) {
for (let c of document.cookie.split(';')) {
c = c.trim();
if (c.startsWith(name + '=')) {
val = decodeURIComponent(c.substring(name.length + 1));
break;
}
}
}
return val;
}
</script>
</body>
</html>