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

Friend list (S7) #44

Closed davidcr01 closed 1 year ago

davidcr01 commented 1 year ago

Description

Is necessary to implement a new page to display the friends of the player.

Tasks

davidcr01 commented 1 year ago

Update Report - DRF

Model and serializer

The model to store the friend relationships is the following:

class FriendList(models.Model):
    sender = models.ForeignKey(Player, on_delete=models.CASCADE, related_name='friend_requests_sent')
    receiver = models.ForeignKey(Player, on_delete=models.CASCADE, related_name='friend_requests_received')
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['sender', 'receiver'], name='unique_friendship')        ]

    def clean(self):
        if self.sender == self.receiver:
            raise ValidationError('You can not be friend of yourself.')
        if FriendList.objects.filter(sender=self.receiver, receiver=self.sender).exists():
            raise ValidationError('This friend relation already exists.')

    def __str__(self):
        return f"{self.sender.user.username} - {self.receiver.user.username}"

Notice that:

The serializer has been a little modified compared to the rest. It only returns the information of the friends and excludes the information of the player who is making the request.

class FriendListSerializer(serializers.ModelSerializer):
    friend = serializers.SerializerMethodField()

    class Meta:
        model = FriendList
        fields = ['friend']

    def get_friend(self, obj):
        request = self.context.get('request')
        player = request.user.player
        if obj.sender == player:
            friend = obj.receiver
        else:
            friend = obj.sender
        return {'username': friend.user.username, 'id': friend.user.id}

View

The view follows the same line of the previous serializer. We check if the player who makes the request is actually a player, and we filter the information either by sender or receiver using the Q function that allows complex queries. As we are only interested in the list of the friends, we only define the list method:

class FriendListViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = FriendListSerializer
    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 = FriendList.objects.filter(Q(sender=player) | Q(receiver=player))
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

Results

With this, if we use a token of an administrator: image

If we use the token of player: image

If we use the token of player3, who is a friend of player: image

davidcr01 commented 1 year ago

Update Report - Frontend

The mockup for this view is the following: image

HTML

In the frontend, a new page friendlist has been generated. This page is loaded when the user clicks on the 1vs1 button of the main page.

The HTML code is divided into two parts: the ion-toolbar to change the view and display the current friends or the friend request; and the cards containing the friend username and the related actions: play, view profile and delete friend:

<ion-toolbar>
    <ion-segment mode="ios" value="friends">
      <ion-segment-button value="friends">
        <ion-label>Friends</ion-label>
      </ion-segment-button>
      <ion-segment-button value="requests">
        <ion-label>Requests</ion-label>
      </ion-segment-button>
    </ion-segment>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div *ngIf="friendList && friendList.length > 0">
  <ion-card *ngFor="let friend of friendList">
    <ion-card-header>
      {{ friend.username }}
    </ion-card-header>
    <ion-card-content>
      <ion-list>
        <ion-item>
          <ion-icon name="people" slot="start"></ion-icon>
          <ion-label>Play Wordle</ion-label>
          <ion-button fill="clear" (click)="playWithFriend(friend.id)">
            <ion-icon name="play" slot="icon-only"></ion-icon>
          </ion-button>
        </ion-item>
        <ion-item>
          <ion-icon name="information-circle" slot="start"></ion-icon>
          <ion-label>View profile</ion-label>
          <ion-button fill="clear" (click)="viewFriendInfo(friend.id)">
            <ion-icon name="information-circle-outline" slot="icon-only"></ion-icon>
          </ion-button>
        </ion-item>
        <ion-item>
          <ion-icon name="trash" slot="start"></ion-icon>
          <ion-label>Delete friend</ion-label>
          <ion-button fill="clear" (click)="removeFriend(friend.id)">
            <ion-icon name="close" slot="icon-only"></ion-icon>
          </ion-button>
        </ion-item>
      </ion-list>
    </ion-card-content>
  </ion-card>
</div>

TS logic

About the logic of the page, is necessary to fetch the friends using the API, using the endpoint defined in the previous comment. To do this, a new method in the apiService has been added:

async getFriendlist(): Promise<any[]> {
        let url = `${this.baseURL}/api/friendlist/`;
        const accessToken = this.storageService.getAccessToken();
        if (!accessToken) {
            return throwError('Access token not found') as any;
        }
        const decryptedToken = this.encryptionService.decryptData(await accessToken);
        const headers = new HttpHeaders({
            Authorization: `Token ${decryptedToken}`,
            'Content-Type': 'application/json'
        });

        return this.http.get<any[]>(url, { headers }).toPromise();
      }

And this method is used in the ts file of the friendlist page:

export class FriendlistPage implements OnInit {

  friendList: any[];

  constructor(private apiService: ApiService, private toastController: ToastController) {}

  ngOnInit() {
    this.loadFriendList();
  }

  async loadFriendList() {
    try {
      const friends = await this.apiService.getFriendlist() as any;
      this.friendList = friends.map(item => item.friend);
      console.log(this.friendList)
    } catch (error) {
      console.error('Error loading friend list:', error);
      const toast = await this.toastController.create({
        message: 'Error loading friend list',
        duration: 2000,
        color: 'danger'
      });
      toast.present();
    }
  }
}

Notice that we have to map the items of the returned information. In this case, we have to get the 'friend' items of the list, and save them in the friendList array. If an error is generated, a toast is displayed.

Result

In case of the logged player has friends added, the page looks like this:

image

In case of the player does not have friends yet, this information is displayed: image