diff --git a/README.md b/README.md index 1246346..33b7e39 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,5 @@ python manage.py runserver 0.0.0.0:8000 coc nvim ``` pip install django-stubs +pip install django-type ``` diff --git a/frontend/static/js/api/MyProfile.js b/frontend/static/js/api/MyProfile.js index 93fc799..3724f34 100644 --- a/frontend/static/js/api/MyProfile.js +++ b/frontend/static/js/api/MyProfile.js @@ -19,12 +19,22 @@ class MyProfile extends Profile * @type {[Profile]} */ this.friends = []; + /** + * @type {[Profile]} + */ + this.incomingFriendRequests = []; + /** + * @type {[Profile]} + */ + this.outgoingFriendRequests = []; } async init() { await super.init(); await this.getBlockedUsers(); await this.getFriends(); + await this.getIncomingFriendRequests() + await this.getOutgoingFriendRequests() } async getBlockedUsers() { @@ -38,6 +48,20 @@ class MyProfile extends Profile const data = await response.json(); data.forEach(profileData => this.friends.push(new Profile(this.client, profileData.username, profileData.user_id, profileData.avatar))); } + async getIncomingFriendRequests() { + const response = await this.client._get('/api/profiles/incoming_friend_requests'); + const data = await response.json(); + data.forEach(profileData => this.incomingFriendRequests.push( + new Profile(this.client, profileData.username, profileData.user_id, profileData.avatar) + )); + } + async getOutgoingFriendRequests() { + const response = await this.client._get('/api/profiles/outgoing_friend_requests'); + const data = await response.json(); + data.forEach(profileData => this.outgoingFriendRequests.push( + new Profile(this.client, profileData.username, profileData.user_id, profileData.avatar) + )); + } /** * * @param {File} selectedFile diff --git a/profiles/models.py b/profiles/models.py index 8de9356..bf034a4 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -2,7 +2,7 @@ from __future__ import annotations from os.path import splitext from django.contrib.auth.models import User -from django.db.models import Q, Model, CASCADE, ForeignKey, ImageField +from django.db.models import Q, Model, CASCADE, ForeignKey, ImageField, OneToOneField from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver @@ -10,8 +10,9 @@ from django.dispatch import receiver def upload_to(instance, filename: str): return f"./profiles/static/avatars/{instance.pk}{splitext(filename)[1]}" + class ProfileModel(Model): - user = ForeignKey(User, on_delete=CASCADE) + user = OneToOneField(User, on_delete=CASCADE) avatar = ImageField(upload_to=upload_to, default="./profiles/static/avatars/default.avif") def get_game(self) -> int: @@ -43,7 +44,7 @@ class ProfileModel(Model): ).delete() def is_friend_requested_by(self, profile): - return self.get_received_friend_request_from(profile) is None + return FriendRequestModel.objects.filter(author=profile, target=self).exists() def get_received_friend_request_from(self, profile): return FriendRequestModel.objects.filter(author=profile, target=self).first() @@ -51,10 +52,13 @@ class ProfileModel(Model): def is_friend_requesting(self, profile): return FriendRequestModel.objects.filter(author=self, target=profile).exists() - def get_sent_friend_requests(self) -> list[ProfileModel]: + def get_outgoing_friend_request_to(self, profile): + return FriendRequestModel.objects.filter(author=self, target=profile).first() + + def get_outgoing_friend_requests(self) -> list[ProfileModel]: return FriendRequestModel.objects.filter(author=self) - def get_received_friend_requests(self) -> list[ProfileModel]: + def get_incoming_friend_requests(self) -> list[ProfileModel]: return FriendRequestModel.objects.filter(target=self) diff --git a/profiles/urls.py b/profiles/urls.py index dec31aa..a202666 100644 --- a/profiles/urls.py +++ b/profiles/urls.py @@ -3,7 +3,10 @@ from django.urls import path from .viewsets.ProfileViewSet import ProfileViewSet from .viewsets.MyProfileViewSet import MyProfileViewSet from .views.blocks import GetBlocksView, EditBlocksView -from .views.friends import GetFriendsView, EditFriendView +from .views.friends import (GetFriendsView, + EditFriendView, + GetIncomingFriendRequestView, + GetOutgoingFriendRequestView) urlpatterns = [ path("settings", MyProfileViewSet.as_view({'patch': 'partial_update', 'delete': 'delete_avatar'}), name="my_profile_page"), @@ -13,6 +16,8 @@ urlpatterns = [ path("block/", EditBlocksView.as_view(), name="block_page"), path("friends", GetFriendsView.as_view(), name="friends_list_page"), path("friends/", EditFriendView.as_view(), name="friends_edit_page"), + path("incoming_friend_requests", GetIncomingFriendRequestView.as_view(), name="incoming_friend_requests"), + path("outgoing_friend_requests", GetOutgoingFriendRequestView.as_view(), name="outgoing_friend_requests"), path("user/", ProfileViewSet.as_view({'get': 'retrieve'}), name="profile_page"), path("id/", ProfileViewSet.as_view({'get': 'retrieve_id'}), name="profile_page"), ] diff --git a/profiles/views/blocks.py b/profiles/views/blocks.py index 7f5dc02..6907f1c 100644 --- a/profiles/views/blocks.py +++ b/profiles/views/blocks.py @@ -4,8 +4,8 @@ from rest_framework import permissions, status from rest_framework.authentication import SessionAuthentication from rest_framework.request import Request -from django.contrib.auth.models import User from django.utils.translation import gettext as _ +from django.shortcuts import get_object_or_404 from ..models import BlockModel, ProfileModel from ..serializers.ProfileSerializer import ProfileSerializer @@ -16,7 +16,7 @@ class GetBlocksView(APIView): authentication_classes = (SessionAuthentication,) def get(self, request: Request): - blocks = BlockModel.objects.filter(blocker=ProfileModel.objects.filter(user=request.user).first()) + blocks = BlockModel.objects.filter(blocker=request.user.profilemodel) bloked_profiles = [block.blocked for block in blocks] return Response(ProfileSerializer(bloked_profiles, many=True).data) @@ -26,32 +26,32 @@ class EditBlocksView(APIView): permission_classes = (permissions.IsAuthenticated,) authentication_classes = (SessionAuthentication,) + def get_object(self): + return self.request.user.profilemodel + def post(self, request, pk=None): - if (pk == request.user.pk): + user_profile = self.get_object() + blocked_profile = get_object_or_404(ProfileModel, pk=pk) + + if user_profile.pk == pk: return Response(_('You can\'t block yourself.'), status.HTTP_400_BAD_REQUEST) - blocked = User.objects.filter(pk=pk) - if (not blocked.exists()): - return Response(_('This user doesn\'t exist.'), status.HTTP_404_NOT_FOUND) + if BlockModel.objects.filter(blocker=user_profile, blocked=blocked_profile): + return Response(_('You already blocked this user.'), status.HTTP_409_CONFLICT) - if (BlockModel.objects.filter(blocker=request.user, blocked=pk)): - return Response(_('You already blocked this user.'), status=status.HTTP_409_CONFLICT) - - BlockModel(blocker=request.user, blocked=blocked[0]).save() - - return Response(status=status.HTTP_201_CREATED) + BlockModel(blocker=user_profile, blocked=blocked_profile).save() + return Response(_('User successfully blocked.'), status.HTTP_201_CREATED) def delete(self, request, pk=None): - if (pk == request.user.pk): + user_profile = self.get_object() + blocked_profile = get_object_or_404(ProfileModel, pk=pk) + + if user_profile.pk == pk: return Response(_('You can\'t unblock yourself.'), status.HTTP_400_BAD_REQUEST) - blocked = User.objects.filter(pk=pk) - if (not blocked.exists()): - return Response(_('This user doesn\'t exist.'), status.HTTP_404_NOT_FOUND) + block_record = BlockModel.objects.filter(blocker=user_profile, blocked=blocked_profile).first() + if not block_record: + return Response(_('This user is not blocked.'), status.HTTP_400_BAD_REQUEST) - block = BlockModel.objects.filter(blocker=request.user, blocked=blocked[0]) - if (not block): - return Response(_('This user isn\'t blocked.'), status.HTTP_400_NOT_FOUND) - - block.delete() - return Response(status=status.HTTP_200_OK) + block_record.delete() + return Response(_('User successfully unblocked.'), status.HTTP_200_OK) diff --git a/profiles/views/friends.py b/profiles/views/friends.py index 6cf7513..00d76b3 100644 --- a/profiles/views/friends.py +++ b/profiles/views/friends.py @@ -4,10 +4,9 @@ from rest_framework import permissions, status from rest_framework.authentication import SessionAuthentication from django.utils.translation import gettext as _ -from django.db.models import Q from django.shortcuts import get_object_or_404 -from ..models import ProfileModel, FriendModel +from ..models import ProfileModel, FriendRequestModel from ..serializers.ProfileSerializer import ProfileSerializer @@ -16,11 +15,7 @@ class GetFriendsView(APIView): authentication_classes = (SessionAuthentication,) def get(self, request): - query = ProfileModel.objects.filter(user=request.user) - if not query.exists(): - return Response(status=status.HTTP_400_BAD_REQUEST) - friends = query[0].get_friends() - return Response(ProfileSerializer(friends, many=True).data) + return Response(ProfileSerializer(request.user.profilemodel.get_friends(), many=True).data) class EditFriendView(APIView): @@ -28,10 +23,10 @@ class EditFriendView(APIView): authentication_classes = (SessionAuthentication,) def get_object(self): - return ProfileModel.objects.get(pk=self.request.user.pk) + return self.request.user.profilemodel def post(self, request, pk=None): - user_profile = self.get_object() + user_profile: ProfileModel = self.get_object() friend_profile = get_object_or_404(ProfileModel, pk=pk) if user_profile.pk == pk: @@ -40,8 +35,16 @@ class EditFriendView(APIView): if user_profile.is_friend(friend_profile): return Response(_('You are already friend with this user.'), status.HTTP_400_BAD_REQUEST) - FriendModel(friend1=user_profile, friend2=friend_profile).save() - return Response(_('Friendship succssfully created.'), status.HTTP_201_CREATED) + if user_profile.is_friend_requesting(friend_profile): + return Response(_('You already sent a request to this user.'), status.HTTP_400_BAD_REQUEST) + + incoming_request = user_profile.get_received_friend_request_from(friend_profile) + if incoming_request: + incoming_request.accept() + return Response(_('Friendship succssfully created.'), status.HTTP_201_CREATED) + + FriendRequestModel(author=user_profile, target=friend_profile).save() + return Response(_('Friend request sent.'), status.HTTP_200_OK) def delete(self, request, pk=None): user_profile = self.get_object() @@ -50,5 +53,30 @@ class EditFriendView(APIView): if not user_profile.is_friend(friend_profile): return Response(_('You are not friend with this user.'), status.HTTP_400_BAD_REQUEST) + outgoing_request = user_profile.get_outgoing_friend_request_to(friend_profile) + if outgoing_request: + outgoing_request.delete() + return Response(_('Friend request cancelled.')) + user_profile.delete_friend(friend_profile) return Response(_('Friendship succssfully deleted.')) + + +class GetIncomingFriendRequestView(APIView): + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (SessionAuthentication,) + + def get(self, request): + requests = request.user.profilemodel.get_incoming_friend_requests() + profiles = [request.author for request in requests] + return Response(ProfileSerializer(profiles, many=True).data) + + +class GetOutgoingFriendRequestView(APIView): + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (SessionAuthentication,) + + def get(self, request): + requests = request.user.profilemodel.get_outgoing_friend_requests() + profiles = [request.target for request in requests] + return Response(ProfileSerializer(profiles, many=True).data) diff --git a/profiles/viewsets/MyProfileViewSet.py b/profiles/viewsets/MyProfileViewSet.py index 6bcfd30..fbc034a 100644 --- a/profiles/viewsets/MyProfileViewSet.py +++ b/profiles/viewsets/MyProfileViewSet.py @@ -14,8 +14,7 @@ class MyProfileViewSet(viewsets.ModelViewSet): queryset = ProfileModel.objects.all() def get_object(self): - obj = self.get_queryset().get(pk=self.request.user.pk) - return obj + return self.request.user.profilemodel def perform_update(self, serializer: ProfileSerializer, pk=None): serializer.is_valid(raise_exception=True) diff --git a/profiles/viewsets/ProfileViewSet.py b/profiles/viewsets/ProfileViewSet.py index 5a82d2a..b476d71 100644 --- a/profiles/viewsets/ProfileViewSet.py +++ b/profiles/viewsets/ProfileViewSet.py @@ -1,7 +1,7 @@ -from django.utils.translation import gettext as _ from django.contrib.auth.models import User +from django.shortcuts import get_object_or_404 -from rest_framework import permissions, status +from rest_framework import permissions from rest_framework import viewsets from rest_framework.response import Response @@ -15,21 +15,12 @@ class ProfileViewSet(viewsets.ModelViewSet): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) def retrieve(self, request, username=None): - query = User.objects.filter(username=username) - if (not query): - return Response({"detail": _("Profile not found.")}, status.HTTP_404_NOT_FOUND) - query = self.get_queryset().filter(pk=query[0].pk) - if (not query): - return Response({"detail": _("Profile not found.")}, status.HTTP_404_NOT_FOUND) - instance = query[0] - return Response(self.serializer_class(instance).data) + user = get_object_or_404(User, username=username) + return Response(self.serializer_class(user.profilemodel).data) def retrieve_id(self, request, pk=None): - query = self.get_queryset().filter(pk=pk) - if (not query): - return Response({"detail": _("Profile not found.")}, status.HTTP_404_NOT_FOUND) - instance = query[0] - return Response(self.serializer_class(instance).data) + user = get_object_or_404(User, pk=pk) + return Response(self.serializer_class(user.profilemodel).data) def list(self, request): serializer = ProfileSerializer(self.get_queryset(), many=True) diff --git a/transcendence/settings.py b/transcendence/settings.py index 44bdff8..8d55720 100644 --- a/transcendence/settings.py +++ b/transcendence/settings.py @@ -72,15 +72,14 @@ CHANNEL_LAYERS = { MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.locale.LocaleMiddleware', ] ROOT_URLCONF = 'transcendence.urls'