Self-service avatar picker in Account

Users can now set their own profile picture instead of asking staff:
POST /api/auth/avatar/ accepts a J-ID (or blank to clear) and reuses the
same item resolution as the staff endpoint. The Account page gains a
profile picture card with a searchable, paginated library grid — any
item works (the thumbnail is used), the current avatar is marked, and
the choice is confirmed before saving. Refreshing the signed-in user
updates the shell avatar immediately.

Verified against the dev server: set, clear, unknown J-ID -> 400,
anonymous -> 401.
This commit is contained in:
2026-09-17 22:31:05 -05:00
parent d8ba442e72
commit 3c49d2be2e
5 changed files with 313 additions and 19 deletions
+45 -15
View File
@@ -98,6 +98,45 @@ class IsStaffUser(permissions.BasePermission):
)
def resolve_avatar_item(value):
"""Turn a "J-42" / "42" string into a MediaItem.
Returns ``(item, error)``: an empty value clears the avatar (``None``,
``None``), an unknown item returns an error message.
"""
value = str(value or "").strip()
if not value:
return None, None
numeric = value[2:] if value.upper().startswith("J-") else value
item = (
MediaItem.objects.filter(pk=int(numeric)).first()
if numeric.isdigit()
else None
)
if item is None:
return None, f"No library item {value}."
return item, None
class AvatarView(APIView):
"""Self-service profile picture chosen from the library."""
permission_classes = [IsAuthenticated]
def post(self, request):
serializer = UserUpdateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
item, error = resolve_avatar_item(
serializer.validated_data.get("avatar_j_id")
)
if error:
return Response({"detail": error}, status=status.HTTP_400_BAD_REQUEST)
user = request.user
user.avatar = item
user.save(update_fields=["avatar"])
return Response(UserSerializer(user, context={"request": request}).data)
class UserViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
@@ -133,22 +172,13 @@ class UserViewSet(
user.role = data["role"]
update_fields.append("role")
if "avatar_j_id" in data:
value = str(data.get("avatar_j_id") or "").strip()
if not value:
user.avatar = None
else:
numeric = value[2:] if value.upper().startswith("J-") else value
item = (
MediaItem.objects.filter(pk=int(numeric)).first()
if numeric.isdigit()
else None
item, error = resolve_avatar_item(data.get("avatar_j_id"))
if error:
return Response(
{"detail": error},
status=status.HTTP_400_BAD_REQUEST,
)
if item is None:
return Response(
{"detail": f"No library item {value}."},
status=status.HTTP_400_BAD_REQUEST,
)
user.avatar = item
user.avatar = item
update_fields.append("avatar")
if update_fields:
user.save(update_fields=update_fields)