Per-user browse preferences

Adds a preferences JSON field on the user plus GET/POST
/api/auth/preferences/ (merge semantics, validated keys), surfaced in
/auth/me/ and typed on the frontend.

The Account page gains a Browsing preferences card: landing page,
default rating filter, default sort, items per page and thumbnail size.
Signed-in users also sync these while browsing (the Library sidebar's
rating/sort/per-page controls and the new thumbnail slider), debounced;
on load the account's values seed the local UI state, so settings follow
the user across browsers. Guests keep the existing localStorage
behaviour. The thumbnail size drives the media grids (Library, Online,
pool detail) between 140 and 320px columns.

Verified the API against the dev server: merge keeps untouched keys,
invalid values 400, values round-trip through /auth/me/.
This commit is contained in:
2026-09-17 22:34:38 -05:00
parent 3c49d2be2e
commit bb87f563a9
17 changed files with 432 additions and 43 deletions
@@ -0,0 +1,18 @@
# Generated by Django 6.1.1 on 2026-09-18 03:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_user_avatar'),
]
operations = [
migrations.AddField(
model_name='user',
name='preferences',
field=models.JSONField(blank=True, default=dict),
),
]
+2
View File
@@ -25,6 +25,8 @@ class User(AbstractUser):
e621_username = models.CharField(max_length=100, blank=True, default="")
e621_api_key = models.CharField(max_length=100, blank=True, default="")
e621_base_url = models.CharField(max_length=200, default="https://e621.net")
# Per-user browse preferences (landing page, default filters, grid size).
preferences = models.JSONField(default=dict, blank=True)
@property
def e621_configured(self):
+17
View File
@@ -50,6 +50,7 @@ class UserSerializer(serializers.ModelSerializer):
"avatar_url",
"e621_username",
"e621_configured",
"preferences",
]
read_only_fields = fields
@@ -114,3 +115,19 @@ class E621CredentialsSerializer(serializers.Serializer):
base_url = serializers.URLField(
max_length=200, required=False, allow_blank=True
)
class PreferencesSerializer(serializers.Serializer):
"""Per-user browse preferences; unknown keys are ignored."""
LANDING_PAGES = ["library", "online", "pools", "followed"]
landing_page = serializers.ChoiceField(choices=LANDING_PAGES, required=False)
ratings = serializers.ListField(
child=serializers.ChoiceField(choices=["s", "q", "e"]),
required=False,
max_length=3,
)
ordering = serializers.CharField(max_length=30, required=False)
per_page = serializers.IntegerField(min_value=12, max_value=200, required=False)
zoom = serializers.IntegerField(min_value=120, max_value=400, required=False)
+9 -1
View File
@@ -1,7 +1,14 @@
from django.urls import path
from rest_framework.authtoken.views import obtain_auth_token
from .views import AvatarView, E621CredentialsView, LogoutView, MeView, RegisterView
from .views import (
AvatarView,
E621CredentialsView,
LogoutView,
MeView,
PreferencesView,
RegisterView,
)
urlpatterns = [
path("register/", RegisterView.as_view(), name="register"),
@@ -9,5 +16,6 @@ urlpatterns = [
path("logout/", LogoutView.as_view(), name="logout"),
path("me/", MeView.as_view(), name="me"),
path("avatar/", AvatarView.as_view(), name="avatar"),
path("preferences/", PreferencesView.as_view(), name="preferences"),
path("e621/", E621CredentialsView.as_view(), name="e621_credentials"),
]
+19
View File
@@ -10,6 +10,7 @@ from apps.library.models import MediaItem
from .models import User
from .serializers import (
E621CredentialsSerializer,
PreferencesSerializer,
RegisterSerializer,
UserListSerializer,
UserSerializer,
@@ -137,6 +138,24 @@ class AvatarView(APIView):
return Response(UserSerializer(user, context={"request": request}).data)
class PreferencesView(APIView):
"""Per-user browse preferences, merged so callers can send single keys."""
permission_classes = [IsAuthenticated]
def get(self, request):
return Response(request.user.preferences or {})
def post(self, request):
serializer = PreferencesSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
preferences = dict(request.user.preferences or {})
preferences.update(serializer.validated_data)
request.user.preferences = preferences
request.user.save(update_fields=["preferences"])
return Response(preferences)
class UserViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,