davidcr01 / WordlePlus

Repository to store all the documentation, files and structure of my Final Degree Project (TFG in Spanish). The main goal is to develop, as full-stack web developer, a remodel of the Wordle game, including more features and functionalities using Ionic, Django REST Api and PostgreSQL.
1 stars 0 forks source link

Add a friend (S7) #45

Closed davidcr01 closed 1 year ago

davidcr01 commented 1 year ago

Description

As a player, is necessary to implement a new logic to search for another player by its username and send a friend request.

davidcr01 commented 1 year ago

Update Report - DRF

Model and serializer

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}

View

As the friend requests manage the friend list and the notification, and relates two players, the view is a little complex:

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')

Testing

The following screenshots show the whole process.

Creating the request as player: image

As player2, we list the pending requests: image The ID of the request is 8.

The player2 rejects the request, and no requests are left: image image

The request is created again, and accepted. The friendship and notifications are created: image image image

davidcr01 commented 1 year ago

Update Report

DRF

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

davidcr01 commented 1 year ago

Update Report - Frontend

The mockup for this view is: image

HTML

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:

TS logic

The logic of the search of players corresponds to this methods:

Result

This is the result of searching for a player: image

When clicking to the add icon, the results are removed and a message is displayed: image

If we try to send the request again: image

Now, if we log as the testing2 player, we see this: image

If we accept, the request is removed and the friend and notification is created: image image