diff --git a/backend/apps/api/serializers/user/profile.py b/backend/apps/api/serializers/user/profile/__init__.py similarity index 92% rename from backend/apps/api/serializers/user/profile.py rename to backend/apps/api/serializers/user/profile/__init__.py index 9bc0c731..fc7f78f7 100644 --- a/backend/apps/api/serializers/user/profile.py +++ b/backend/apps/api/serializers/user/profile/__init__.py @@ -2,10 +2,9 @@ from rest_framework import serializers +from apps.api.serializers.user import UserSerializer from apps.user.models import Profile -from ...serializers.user import UserSerializer - class ProfileSerializer(serializers.ModelSerializer): user = UserSerializer(read_only=True) diff --git a/backend/apps/api/serializers/user/profile/downvoted.py b/backend/apps/api/serializers/user/profile/downvoted.py new file mode 100644 index 00000000..8b84d5e8 --- /dev/null +++ b/backend/apps/api/serializers/user/profile/downvoted.py @@ -0,0 +1,21 @@ +import copy + +from rest_framework import serializers + +from apps.api.serializers.comment import CommentDetailSerializer +from apps.api.serializers.post import PostSerializer + + +class DownvotedSerializer(serializers.Serializer): + type = serializers.CharField() + data = serializers.SerializerMethodField() + + def get_data(self, obj) -> dict: + obj_type = obj.type + obj_copy = copy.copy(obj) + + delattr(obj_copy, "type") + + if obj_type == "post": + return PostSerializer(obj_copy, context=self.context).data + return CommentDetailSerializer(obj_copy, context=self.context).data diff --git a/backend/apps/api/serializers/user/profile/overview.py b/backend/apps/api/serializers/user/profile/overview.py new file mode 100644 index 00000000..380b5d97 --- /dev/null +++ b/backend/apps/api/serializers/user/profile/overview.py @@ -0,0 +1,21 @@ +import copy + +from rest_framework import serializers + +from apps.api.serializers.comment import CommentDetailSerializer +from apps.api.serializers.post import PostSerializer + + +class OverviewSerializer(serializers.Serializer): + type = serializers.CharField() + data = serializers.SerializerMethodField() + + def get_data(self, obj) -> dict: + obj_type = obj.type + obj_copy = copy.copy(obj) + + delattr(obj_copy, "type") + + if obj_type == "post": + return PostSerializer(obj_copy, context=self.context).data + return CommentDetailSerializer(obj_copy, context=self.context).data diff --git a/backend/apps/api/serializers/user/profile/upvoted.py b/backend/apps/api/serializers/user/profile/upvoted.py new file mode 100644 index 00000000..4a1833f9 --- /dev/null +++ b/backend/apps/api/serializers/user/profile/upvoted.py @@ -0,0 +1,21 @@ +import copy + +from rest_framework import serializers + +from apps.api.serializers.comment import CommentDetailSerializer +from apps.api.serializers.post import PostSerializer + + +class UpvotedSerializer(serializers.Serializer): + type = serializers.CharField() + data = serializers.SerializerMethodField() + + def get_data(self, obj) -> dict: + obj_type = obj.type + obj_copy = copy.copy(obj) + + delattr(obj_copy, "type") + + if obj_type == "post": + return PostSerializer(obj_copy, context=self.context).data + return CommentDetailSerializer(obj_copy, context=self.context).data diff --git a/backend/apps/api/viewsets/user/__init__.py b/backend/apps/api/viewsets/user/__init__.py index c0d7e4b5..952c0ef5 100644 --- a/backend/apps/api/viewsets/user/__init__.py +++ b/backend/apps/api/viewsets/user/__init__.py @@ -1,13 +1,22 @@ +from http import HTTPMethod +from itertools import chain + from django.conf import settings +from django.db.models import CharField, QuerySet, Value from drf_spectacular.utils import extend_schema -from rest_framework import exceptions, filters, permissions, viewsets +from rest_framework import exceptions, filters, permissions, response, viewsets +from rest_framework.decorators import action +from apps.api.serializers.comment import CommentDetailSerializer +from apps.api.serializers.post import PostSerializer +from apps.api.serializers.user.profile import ProfileSerializer +from apps.api.serializers.user.profile.downvoted import DownvotedSerializer +from apps.api.serializers.user.profile.overview import OverviewSerializer +from apps.api.serializers.user.profile.upvoted import UpvotedSerializer from apps.user.models import Profile -from ...serializers.user.profile import ProfileSerializer - -@extend_schema(tags=['user & profiles']) +@extend_schema(tags=["user & profiles"]) class ProfileViewSet(viewsets.ReadOnlyModelViewSet): """ ViewSet for performing read-only operations on the Profile model. @@ -19,10 +28,105 @@ class ProfileViewSet(viewsets.ReadOnlyModelViewSet): queryset = Profile.objects.all() serializer_class = ProfileSerializer filter_backends = (filters.SearchFilter,) - search_fields = ('username',) + search_fields = ("username",) + + serializer_classes = { + "overview": OverviewSerializer, + "posts": PostSerializer, + "comments": CommentDetailSerializer, + "upvoted": UpvotedSerializer, + "downvoted": DownvotedSerializer, + } + + def get_serializer_class(self): + if self.action in self.serializer_classes: + return self.serializer_classes[self.action] + return self.serializer_class + + def get_queryset(self) -> QuerySet[Profile]: + return super().get_queryset() + + def get_object(self) -> Profile: + qs = self.get_queryset() + filter_kwargs = {f"{self.lookup_field}": self.kwargs[self.lookup_field]} + obj = qs.filter(**filter_kwargs).first() + + if not obj: + raise exceptions.NotFound( + f"Profile with ID {self.kwargs[self.lookup_field]} not found." + ) + return obj + + @extend_schema(responses=OverviewSerializer(many=True)) + @action(detail=True, methods=[HTTPMethod.GET]) + def overview(self, request, pk=None): + """Returns a mixed list of posts and comments by the user, ordered by date.""" + profile = self.get_object() + posts = profile.posts.all().annotate(type=Value("post", CharField())) + comments = profile.comments.with_annotated_ratio().annotate( + type=Value("comment", CharField()) + ) + + combined_data = sorted(chain(posts, comments), key=lambda obj: obj.created_at, reverse=True) + serialized_data = self.get_serializer( + combined_data, context={'request': request}, many=True + ).data + return response.Response(serialized_data) + + @extend_schema(responses=PostSerializer(many=True)) + @action(detail=True, methods=[HTTPMethod.GET]) + def posts(self, request, pk=None): + """Returns a list of posts by the user, ordered by date.""" + profile = self.get_object() + posts = profile.posts.all().order_by("-created_at") + serialized_data = self.get_serializer(posts, context={"request": request}, many=True).data + return response.Response(serialized_data) + + @extend_schema(responses=CommentDetailSerializer(many=True)) + @action(detail=True, methods=[HTTPMethod.GET]) + def comments(self, request, pk=None): + """Returns a list of comments by the user, ordered by date.""" + profile = self.get_object() + comments = profile.comments.with_annotated_ratio().order_by("-created_at") + serialized_data = self.get_serializer( + comments, context={"request": request}, many=True + ).data + return response.Response(serialized_data) + + @extend_schema(responses=UpvotedSerializer(many=True)) + @action(detail=True, methods=[HTTPMethod.GET]) + def upvoted(self, request, pk=None): + """Returns a mixed list of upvoted posts and comments by the user, ordered by date.""" + profile = self.get_object() + posts = profile.upvoted_posts.all().annotate(type=Value("post", CharField())) + comments = profile.upvoted_comments.with_annotated_ratio().annotate( + type=Value("comment", CharField()) + ) + + combined_data = sorted(chain(posts, comments), key=lambda obj: obj.created_at, reverse=True) + serialized_data = self.get_serializer( + combined_data, context={"request": request}, many=True + ).data + return response.Response(serialized_data) + + @extend_schema(responses=DownvotedSerializer(many=True)) + @action(detail=True, methods=[HTTPMethod.GET]) + def downvoted(self, request, pk=None): + """Returns a mixed list of downvoted posts and comments by the user, ordered by date.""" + profile = self.get_object() + posts = profile.downvoted_posts.all().annotate(type=Value("post", CharField())) + comments = profile.downvoted_comments.with_annotated_ratio().annotate( + type=Value("comment", CharField()) + ) + + combined_data = sorted(chain(posts, comments), key=lambda obj: obj.created_at, reverse=True) + serialized_data = self.get_serializer( + combined_data, context={"request": request}, many=True + ).data + return response.Response(serialized_data) -@extend_schema(tags=['user & profiles']) +@extend_schema(tags=["user & profiles"]) class MyProfilesViewSet(viewsets.ModelViewSet): """ ViewSet to manage profiles associated with the authenticated user. @@ -39,7 +143,7 @@ def get_queryset(self): # pyright: ignore Restrict queryset to profiles owned by the currently authenticated user. """ # during schema generation - if getattr(self, 'swagger_fake_view', False): + if getattr(self, "swagger_fake_view", False): return Profile.objects.none() user = self.request.user return user.profiles.all() # pyright: ignore @@ -54,6 +158,6 @@ def perform_create(self, serializer): user = self.request.user if user.profiles.count() >= settings.PROFILE_LIMIT: # pyright: ignore raise exceptions.ValidationError( - f'A user cannot have more than {settings.PROFILE_LIMIT} profiles.' + f"A user cannot have more than {settings.PROFILE_LIMIT} profiles." ) serializer.save(user=user) diff --git a/backend/core/settings.py b/backend/core/settings.py index 0d6d0412..31155246 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -102,6 +102,7 @@ 'SERVE_INCLUDE_SCHEMA': False, 'SCHEMA_PATH_PREFIX': r'/api/v[1-9]/', 'SCHEMA_PATH_PREFIX_TRIM': True, + 'SERVERS': [{'url': '/api/v1', 'description': 'v1 API version'}], # sidecar config 'SWAGGER_UI_DIST': 'SIDECAR', 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',