Skip to content

feat: profile page apis #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions backend/apps/api/serializers/user/profile/downvoted.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions backend/apps/api/serializers/user/profile/overview.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions backend/apps/api/serializers/user/profile/upvoted.py
Original file line number Diff line number Diff line change
@@ -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
120 changes: 112 additions & 8 deletions backend/apps/api/viewsets/user/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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)
1 change: 1 addition & 0 deletions backend/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down