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/.
134 lines
4.1 KiB
Python
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)
|