Files
J621/backend/apps/accounts/serializers.py
T
JakeBreath bb87f563a9 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/.
2026-09-17 22:34:38 -05:00

134 lines
4.1 KiB
Python

from pathlib import Path
from django.contrib.auth.password_validation import validate_password
from django.core import signing
from rest_framework import serializers
from apps.library.services import MEDIA_FILE_SALT, VIDEO_EXTENSIONS
from .models import User
def signed_media_url(request, item):
"""A signed media URL an <img> tag can load for an authenticated user."""
if item is None:
return None
user = getattr(request, "user", None)
if user is None or not getattr(user, "is_authenticated", False):
return None
location = item.locations.first()
is_video = (
location is not None
and Path(location.rel_path).suffix.lower() in VIDEO_EXTENSIONS
)
action = "thumbnail" if is_video else "raw"
signature = signing.dumps(
{"item": item.id, "user": user.id, "action": action},
salt=MEDIA_FILE_SALT,
)
return f"/api/files/J-{item.id}/{action}/?sig={signature}"
class UserSerializer(serializers.ModelSerializer):
e621_configured = serializers.BooleanField(read_only=True)
can_upload = serializers.BooleanField(read_only=True)
avatar_j_id = serializers.SerializerMethodField()
avatar_url = serializers.SerializerMethodField()
class Meta:
model = User
fields = [
"id",
"username",
"email",
"is_staff",
"is_superuser",
"date_joined",
"role",
"can_upload",
"avatar_j_id",
"avatar_url",
"e621_username",
"e621_configured",
"preferences",
]
read_only_fields = fields
def get_avatar_j_id(self, obj):
return f"J-{obj.avatar_id}" if obj.avatar_id else None
def get_avatar_url(self, obj):
return signed_media_url(self.context.get("request"), obj.avatar)
class UserListSerializer(serializers.ModelSerializer):
avatar_j_id = serializers.SerializerMethodField()
avatar_url = serializers.SerializerMethodField()
uploads_count = serializers.IntegerField(read_only=True)
class Meta:
model = User
fields = [
"id",
"username",
"email",
"role",
"is_staff",
"is_superuser",
"date_joined",
"avatar_j_id",
"avatar_url",
"uploads_count",
]
read_only_fields = fields
def get_avatar_j_id(self, obj):
return f"J-{obj.avatar_id}" if obj.avatar_id else None
def get_avatar_url(self, obj):
return signed_media_url(self.context.get("request"), obj.avatar)
class UserUpdateSerializer(serializers.Serializer):
avatar_j_id = serializers.CharField(
required=False, allow_blank=True, allow_null=True
)
role = serializers.ChoiceField(choices=User.ROLE_CHOICES, required=False)
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, validators=[validate_password])
class Meta:
model = User
fields = ["username", "email", "password"]
def create(self, validated_data):
return User.objects.create_user(**validated_data)
class E621CredentialsSerializer(serializers.Serializer):
username = serializers.CharField(max_length=100, allow_blank=True)
api_key = serializers.CharField(
max_length=100, allow_blank=True, trim_whitespace=True
)
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)