From 5772ab3e6811e80f298151f87e6b29da53babd0a Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 12 Feb 2025 17:31:59 +0700 Subject: [PATCH] serialize user, add account endpoint --- backend/user/serializers.py | 44 ++++++++++++ backend/user/src/user_config.py | 10 +++ backend/user/urls.py | 1 + backend/user/views.py | 119 ++++++++++++++++++++------------ backend/video/src/index.py | 4 +- 5 files changed, 132 insertions(+), 46 deletions(-) diff --git a/backend/user/serializers.py b/backend/user/serializers.py index e87a10c0..4db2603a 100644 --- a/backend/user/serializers.py +++ b/backend/user/serializers.py @@ -1,5 +1,7 @@ """serializer for account model""" +# pylint: disable=abstract-method + from rest_framework import serializers from user.models import Account @@ -18,3 +20,45 @@ class AccountSerializer(serializers.ModelSerializer): "user_permissions", "last_login", ) + + +class UserMeConfigSerializer(serializers.Serializer): + """serialize user me config""" + + stylesheet = serializers.CharField() + page_size = serializers.IntegerField() + sort_by = serializers.ChoiceField( + choices=[ + "published", + "downloaded", + "views", + "likes", + "duration", + "filesize", + ] + ) + sort_order = serializers.ChoiceField(choices=["asc", "desc"]) + view_style_home = serializers.ChoiceField(choices=["grid", "list"]) + view_style_channel = serializers.ChoiceField(choices=["grid", "list"]) + view_style_downloads = serializers.ChoiceField(choices=["grid", "list"]) + view_style_playlist = serializers.ChoiceField(choices=["grid", "list"]) + grid_items = serializers.IntegerField() + hide_watched = serializers.BooleanField() + show_ignored_only = serializers.BooleanField() + show_subed_only = serializers.BooleanField() + show_help_text = serializers.BooleanField() + + def __init__(self, *args, required=True, **kwargs): + super().__init__(*args, **kwargs) + + # Override 'required' for all fields if provided + for field in self.fields.values(): + field.required = required + + +class LoginSerializer(serializers.Serializer): + """serialize login""" + + username = serializers.CharField() + password = serializers.CharField() + remember_me = serializers.ChoiceField(choices=["on", "off"], default="off") diff --git a/backend/user/src/user_config.py b/backend/user/src/user_config.py index 42dfa2ef..47cbaf80 100644 --- a/backend/user/src/user_config.py +++ b/backend/user/src/user_config.py @@ -143,6 +143,16 @@ class UserConfig: return config + def update_config(self, to_update: dict) -> None: + """update config object""" + data = {"doc": {"config": to_update}} + response, status = ElasticWrap(self.es_update_url).post(data) + if status < 200 or status > 299: + raise ValueError(f"Failed storing user value {status}: {response}") + + for key, value in to_update.items(): + print(f"User {self._user_id} value '{key}' change: to {value}") + def sync_defaults(self): """set initial defaults on 404""" response, _ = ElasticWrap(self.es_url).post( diff --git a/backend/user/urls.py b/backend/user/urls.py index 437f66b7..319a5ccb 100644 --- a/backend/user/urls.py +++ b/backend/user/urls.py @@ -6,5 +6,6 @@ from user import views urlpatterns = [ path("login/", views.LoginApiView.as_view(), name="api-user-login"), path("logout/", views.LogoutApiView.as_view(), name="api-user-logout"), + path("account/", views.UserAccountView.as_view(), name="api-user-account"), path("me/", views.UserConfigView.as_view(), name="api-user-me"), ] diff --git a/backend/user/views.py b/backend/user/views.py index 4cbb4d71..2ef5ce33 100644 --- a/backend/user/views.py +++ b/backend/user/views.py @@ -1,63 +1,75 @@ """all user api views""" +from common.serializers import ErrorResponseSerializer from common.views import ApiBaseView from django.contrib.auth import authenticate, login, logout from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView from user.models import Account -from user.serializers import AccountSerializer +from user.serializers import ( + AccountSerializer, + LoginSerializer, + UserMeConfigSerializer, +) from user.src.user_config import UserConfig +class UserAccountView(ApiBaseView): + """resolves to /api/user/account/ + GET: return current user account + """ + + @extend_schema( + responses=AccountSerializer(), + ) + def get(self, request): + """get user account""" + user_id = request.user.id + account = Account.objects.get(id=user_id) + account_serializer = AccountSerializer(account) + return Response(account_serializer.data) + + class UserConfigView(ApiBaseView): """resolves to /api/user/me/ GET: return current user config POST: update user config """ + @extend_schema(responses=UserMeConfigSerializer()) def get(self, request): - """get config""" - user_id = request.user.id - account = Account.objects.get(id=user_id) - serializer = AccountSerializer(account) - response = serializer.data.copy() + """get user config""" + config = UserConfig(request.user.id).get_config() + serializer = UserMeConfigSerializer(config) - config = UserConfig(user_id).get_config() - response.update({"config": config}) - - return Response(response) + return Response(serializer.data) + @extend_schema( + request=UserMeConfigSerializer(required=False), + responses={ + 200: UserMeConfigSerializer(), + 400: OpenApiResponse( + ErrorResponseSerializer(), description="Bad request" + ), + }, + ) def post(self, request): - """update config""" - user_id = request.user.id - data = request.data + """update config, allows partial update""" - data_config = data.get("config") - if not data_config: - message = { - "status": "Bad Request", - "message": "missing config key", - } - return Response(message, status=400) + data_serializer = UserMeConfigSerializer( + data=request.data, required=False + ) + data_serializer.is_valid(raise_exception=True) + validated_data = data_serializer.validated_data + UserConfig(request.user.id).update_config(to_update=validated_data) + config = UserConfig(request.user.id).get_config() + serializer = UserMeConfigSerializer(config) - user_conf = UserConfig(user_id) - for key, value in data_config.items(): - try: - user_conf.set_value(key, value) - except ValueError as err: - message = { - "status": "Bad Request", - "message": f"failed updating {key} to '{value}', {err}", - } - return Response(message, status=400) - - response = user_conf.get_config() - response.update({"user_id": user_id}) - - return Response(response) + return Response(serializer.data) @method_decorator(csrf_exempt, name="dispatch") @@ -67,21 +79,37 @@ class LoginApiView(APIView): """ permission_classes = [AllowAny] + SEC_IN_DAY = 60 * 60 * 24 + @extend_schema( + request=LoginSerializer(), + responses={204: OpenApiResponse(description="login successful")}, + ) def post(self, request, *args, **kwargs): - """post data""" + """login with username and password""" # pylint: disable=no-member + data_serializer = LoginSerializer(data=request.data) + data_serializer.is_valid(raise_exception=True) + validated_data = data_serializer.validated_data - username = request.data.get("username") - password = request.data.get("password") + username = validated_data["username"] + password = validated_data["password"] + remember_me = validated_data.get("remember_me") user = authenticate(request, username=username, password=password) + if user is None: + error = ErrorResponseSerializer({"error": "Invalid credentials"}) + return Response(error.data, status=400) - if user is not None: - login(request, user) # Creates a session for the user - return Response({"message": "Login successful"}, status=200) + if remember_me == "on": + request.session.set_expiry(self.SEC_IN_DAY * 365) + else: + request.session.set_expiry(self.SEC_IN_DAY * 2) - return Response({"message": "Invalid credentials"}, status=400) + print(f"expire session in {request.session.get_expiry_age()} secs") + + login(request, user) # Creates a session for the user + return Response(status=204) class LogoutApiView(ApiBaseView): @@ -89,7 +117,10 @@ class LogoutApiView(ApiBaseView): POST: handle logout """ + @extend_schema( + responses={204: OpenApiResponse(description="logout successful")} + ) def post(self, request, *args, **kwargs): - """logout on post request""" + """logout user from session""" logout(request) - return Response({"message": "Successfully logged out."}, status=200) + return Response(status=204) diff --git a/backend/video/src/index.py b/backend/video/src/index.py index ff90b142..42ce02ac 100644 --- a/backend/video/src/index.py +++ b/backend/video/src/index.py @@ -53,8 +53,8 @@ class SponsorBlock: print(f"{youtube_id}: get sponsorblock timestamps") try: response = requests.get(url, headers=headers, timeout=10) - except requests.ReadTimeout: - print(f"{youtube_id}: sponsorblock API timeout") + except (requests.ReadTimeout, requests.ConnectionError) as err: + print(f"{youtube_id}: sponsorblock API error: {str(err)}") return False if not response.ok: