Closed davidcr01 closed 1 year ago
A new model, serializer and view have been added to manage the friend requests. The model is very similar to the FriendList:
class FriendRequest(models.Model):
sender = models.ForeignKey(Player, on_delete=models.CASCADE, related_name='requests_sent')
receiver = models.ForeignKey(Player, on_delete=models.CASCADE, related_name='requests_received')
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
constraints = [
models.UniqueConstraint(fields=['sender', 'receiver'], name='unique_friendrequest') ]
def clean(self):
if self.sender == self.receiver:
raise ValidationError('You can not be send a request to yourself.')
if FriendRequest.objects.filter(sender=self.receiver, receiver=self.sender).exists():
raise ValidationError('This friend request already exists.')
def __str__(self):
return f"{self.sender.user.username} - {self.receiver.user.username}"
About the serializer, is a little bit different from the FriendList serializer. In this case, we do not return the receiver field. This is done to get the friend request correctly and ignoring the receiver, who is the player itself. Also, the ID of the request is added in the serializer:
class FriendRequestSerializer(serializers.ModelSerializer):
sender = serializers.SerializerMethodField()
class Meta:
model = FriendRequest
fields = ['id', 'sender']
def get_sender(self, obj):
return {'username': obj.sender.user.username, 'id': obj.sender.user.id}
As the friend requests manage the friend list and the notification, and relates two players, the view is a little complex:
The GET method lists all the requests where the receiver is the identified player.
class FriendRequestViewSet(viewsets.ReadOnlyModelViewSet):
queryset = FriendRequest.objects.all()
serializer_class = FriendRequestSerializer
permission_classes = [permissions.IsAuthenticated]
def list(self, request, *args, **kwargs):
player = getattr(request.user, 'player', None)
if not player:
return Response({'error': 'Player not found'}, status=404)
queryset = FriendRequest.objects.filter(receiver=player).order_by('timestamp')
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
About the POST method to create new requests (the identified player is the sender), some checks have to be done:
def create(self, request, *args, **kwargs):
sender = getattr(request.user, 'player', None)
if not sender:
return Response({'error': 'Player not found'}, status=404)
receiver_id = request.data.get('receiver_id')
if not receiver_id:
return Response({'error': 'receiver_id is required'}, status=status.HTTP_400_BAD_REQUEST)
try:
receiver = Player.objects.get(id=receiver_id)
except Player.DoesNotExist:
return Response({'error': 'Receiver not found'}, status=status.HTTP_404_NOT_FOUND)
if sender == receiver:
return Response({'error': 'Cannot send friend request to yourself'}, status=status.HTTP_400_BAD_REQUEST)
# Check if there is an existing request
existing_request = FriendRequest.objects.filter(sender=sender, receiver=receiver)
if existing_request.exists():
return Response({'error': 'Friend request already sent'}, status=status.HTTP_400_BAD_REQUEST)
# Check if there is an existing friendship
existing_friendship1 = FriendList.objects.filter(sender=sender, receiver=receiver)
existing_friendship2 = FriendList.objects.filter(sender=receiver, receiver=sender)
if existing_friendship1.exists() or existing_friendship2.exists():
return Response({'error': 'Friendship already exists'}, status=status.HTTP_400_BAD_REQUEST)
# Create the request and notify it
friend_request = FriendRequest.objects.create(sender=sender, receiver=receiver)
Notification.objects.create(
player=receiver,
text='You have a new friend request!',
link='http://localhost:8100/friendlist'
)
serializer = FriendRequestSerializer(friend_request)
return Response(serializer.data, status=status.HTTP_201_CREATED)
To accept or decline requests, a decorator is used in the POST request. This allows the POST request to add a sub-url, (/{idRequest}/[accept/reject])
In this case:
@action(detail=True, methods=['post'])
def accept(self, request, *args, **kwargs):
instance = self.get_object()
receiver = getattr(request.user, 'player', None)
if not receiver:
return Response({'error': 'Player not found'}, status=404)
if instance.receiver != receiver:
return Response({'error': 'Permission denied'}, status=403)
# Create the friendship
FriendList.objects.create(sender=instance.sender, receiver=receiver)
# Create notification for both players
Notification.objects.create(
player=instance.sender,
text=f"You are now friends with {receiver.user.username}.",
link='http://localhost:8100/friendlist'
)
Notification.objects.create(
player=receiver,
text=f"You are now friends with {instance.sender.user.username}.",
link='http://localhost:8100/friendlist'
)
instance.delete()
return Response({'message': 'Friend request accepted'}, status=200)
@action(detail=True, methods=['post'])
def reject(self, request, *args, **kwargs):
instance = self.get_object()
receiver = request.user.player
if instance.receiver != receiver:
return Response({'error': 'Permission denied'}, status=403)
instance.delete()
return Response({'message': 'Friend request rejected'}, status=200)
The URL is added to the router:
router.register('api/friendrequest', FriendRequestViewSet, basename='friendrequest')
The following screenshots show the whole process.
Creating the request as player:
As player2, we list the pending requests: The ID of the request is 8.
The player2 rejects the request, and no requests are left:
The request is created again, and accepted. The friendship and notifications are created:
To search for the player to be friend, is necessary to add a new serializer and view to get the usernames of all the players:
Serializer:
class PlayerListSerializer(serializers.ModelSerializer):
username = serializers.ReadOnlyField(source='user.username')
class Meta:
model = Player
fields = ['username']
View:
class PlayerListAPIView(generics.ListAPIView):
queryset = Player.objects.all()
serializer_class = PlayerListSerializer
permission_classes = [permissions.IsAuthenticated]
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
usernames = serializer.data
return Response(usernames)
The path is api/list-players
The mockup for this view is:
In the frontend, we use the request
tab of the segment created in #44.
When clicking this tab, this HTMl code is displayed:
<div *ngIf="selectedSegment === 'requests'">
<ion-searchbar color="dark" placeholder="Search players" (ionChange)="searchPlayers($event)"></ion-searchbar>
<div *ngIf="showResults">
<!-- Show filtered players if there are filtered players -->
<ion-card *ngIf="filteredPlayers && filteredPlayers.length > 0">
<ion-card-header>Search results</ion-card-header>
<ion-card-content>
<ion-list>
<ion-item *ngFor="let player of filteredPlayers">
{{ player.username }}
<ion-button fill="clear" slot="end" (click)="sendFriendRequest(player.id)">
<ion-icon name="add"></ion-icon>
</ion-button>
</ion-item>
</ion-list>
</ion-card-content>
</ion-card>
</div>
<!-- Show message if there are no filtered players -->
<ion-card *ngIf="filteredPlayers && filteredPlayers.length === 0">
<ion-card-header>Search results</ion-card-header>
<ion-card-content>No results!</ion-card-content>
</ion-card>
<!-- Show requests if there are requests -->
<div *ngIf="friendRequests && friendRequests.length > 0">
<ion-card *ngFor="let request of friendRequests">
<ion-card-header class="request-container">
<ion-icon name="people" color="success"></ion-icon>
<div class="username">
{{ request.sender.username }}
</div>
<div class="buttons">
<ion-button fill="clear" (click)="acceptFriendRequest(request.id)">
<ion-icon name="checkmark"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="rejectFriendRequest(request.id)">
<ion-icon name="close" color="danger"></ion-icon>
</ion-button>
</div>
</ion-card-header>
</ion-card>
</div>
<!-- Show messages if there are no requests -->
<ion-card *ngIf="selectedSegment === 'requests' && friendRequests && friendRequests.length === 0">
<ion-card-content>
No friend requests here!
</ion-card-content>
</ion-card>
</div>
This code is divided into several parts:
The logic of the search of players corresponds to this methods:
searchPlayers
: it captures the introduced value and search in the list of usernames of all the players of the platform. When the results are fetched, they are displayed in a list in the HTML.
searchPlayers(event: any) {
const username = event.target.value;
this.showResults = true;
if (!username) {
this.filteredPlayers = [];
return;
}
if (typeof username === 'string') {
const filteredPlayers = this.playerUsernames.filter(player => {
return player['username'].toLowerCase().includes(username.toLowerCase());
});
this.filteredPlayers = filteredPlayers;
}
}
getAllPlayers
: gets list of usernames of all the players of the platform, using the PlayerListAPIView
view defined previously. This list is only fetched once in the ngOnInit function. It uses a new function with the same name of the apiService.
async getAllPlayers() {
(await this.apiService.getAllPlayers()).subscribe(
(players: string[]) => {
this.playerUsernames = players;
},
(error) => {
console.error('Error retrieving players:', error);
}
);
}
sendFriendRequest
: it uses the method with the same name of the apiservice, and shows a toast depending on the result. The apiService method uses the /accept
URL defined previously:
async sendFriendRequest(playerId: number) {
this.showResults = false;
console.log(playerId);
(await this.apiService.sendFriendRequest(playerId)).subscribe(
async (response) => {
console.log('Friend request sent successfully', response);
this.toastService.showToast("Request was sent successfully!", 2000, 'top', 'success');
},
async (error) => {
console.error('Error sending friend request:', error);
let message = "Error sending request";
if (error.error.error === 'Friend request already sent') {
message = error.error.error
}
this.toastService.showToast(message, 2000, 'top', 'danger');
}
);
}
loadFriendRequests
that simply uses the apiService to get all the current requests.This is the result of searching for a player:
When clicking to the add icon, the results are removed and a message is displayed:
If we try to send the request again:
Now, if we log as the testing2
player, we see this:
If we accept, the request is removed and the friend and notification is created:
Description
As a player, is necessary to implement a new logic to search for another player by its username and send a friend request.