diff --git a/Wordle+/django/djangoproject/settings.py b/Wordle+/django/djangoproject/settings.py index 896371c..04ebb92 100644 --- a/Wordle+/django/djangoproject/settings.py +++ b/Wordle+/django/djangoproject/settings.py @@ -145,7 +145,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' AUTH_USER_MODEL = 'djapi.CustomUser' -TOKEN_EXPIRED_AFTER_SECONDS = 3600 +TOKEN_EXPIRED_AFTER_SECONDS = 3600 # Time of the token expiration in seconds MEDIA_URL = '/avatars/' MEDIA_ROOT = os.path.join(BASE_DIR, 'avatars') @@ -154,6 +154,6 @@ CORS_ALLOWED_ORIGINS = [ 'http://localhost:8100', # Ionic in local (dev) - 'http://localhost:8080', # Ionic in Docker + 'http://localhost', # Ionic in Docker ] CORS_ALLOW_REDIRECTS = False \ No newline at end of file diff --git a/Wordle+/django/djangoproject/urls.py b/Wordle+/django/djangoproject/urls.py index 377f9e0..6ffbf22 100644 --- a/Wordle+/django/djangoproject/urls.py +++ b/Wordle+/django/djangoproject/urls.py @@ -33,7 +33,6 @@ router.register('api/friendrequest', FriendRequestViewSet, basename='friendrequest') # Wire up our API using automatic URL routing. -# Additionally, we include login URLs for the browsable API. urlpatterns = [ path('', include(router.urls)), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), diff --git a/Wordle+/django/djapi/admin.py b/Wordle+/django/djapi/admin.py index 5b9ce28..011dc91 100644 --- a/Wordle+/django/djapi/admin.py +++ b/Wordle+/django/djapi/admin.py @@ -92,7 +92,7 @@ class NotificationAdmin(admin.ModelAdmin): list_display = ('id', 'player', 'text', 'link', 'timestamp') class TournamentsForm(forms.ModelForm): - word_length = forms.ChoiceField(choices=[ + word_length = forms.ChoiceField(choices=[ # Dropdown tab (4, '4'), (5, '5'), (6, '6'), @@ -100,8 +100,8 @@ class TournamentsForm(forms.ModelForm): (8, '8'), ]) - max_players = forms.ChoiceField(choices=[ - (2, '2'), + max_players = forms.ChoiceField(choices=[ # Dropdown tab + (2, '2'), (4, '4'), (8, '8'), (16, '16'), @@ -129,6 +129,8 @@ def save_model(self, request, obj, form, change): tournament.num_players += 1 super().save_model(request, obj, form, change) + + # If the tournament is full, create the rounds and the games of the first round if (tournament.num_players >= tournament.max_players): tournament.is_closed = True rounds = int(math.log2(tournament.max_players)) @@ -147,7 +149,7 @@ def save_model(self, request, obj, form, change): player = obj.player message = f"You were assigned in {tournament.name}. Good luck!" - link = "http://localhost:8100/tabs/tournaments" + link = "http://localhost/tabs/tournaments" notification = Notification.objects.create(player=player, text=message, link=link) notification.save() @@ -196,6 +198,7 @@ class RoundAdmin(admin.ModelAdmin): class RoundGameAdmin(admin.ModelAdmin): list_display = ('id', 'round', 'game',) +# Register all the models admin.site.register(RoundGame, RoundGameAdmin) admin.site.register(Round, RoundAdmin) admin.site.register(Game, GameAdmin) diff --git a/Wordle+/django/djapi/models.py b/Wordle+/django/djapi/models.py index 020af6d..9363add 100644 --- a/Wordle+/django/djapi/models.py +++ b/Wordle+/django/djapi/models.py @@ -103,7 +103,8 @@ def clean(self): def __str__(self): return f"{self.sender.user.username} - {self.receiver.user.username}" - + +# Model to store the friend requests between players. 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') @@ -122,6 +123,7 @@ def clean(self): def __str__(self): return f"{self.sender.user.username} - {self.receiver.user.username}" +# Model to store the multiplayer and tournament games between players. class Game(models.Model): player1 = models.ForeignKey(Player, on_delete=models.CASCADE, related_name='player1_wordle') player2 = models.ForeignKey(Player, on_delete=models.CASCADE, related_name='player2_wordle') @@ -139,6 +141,7 @@ class Game(models.Model): def __str__(self): return f"{self.player1.user.username} - {self.player2.user.username}" +# Model to store the rounds of a tournament. class Round(models.Model): tournament = models.ForeignKey(Tournament, on_delete=models.CASCADE) number = models.PositiveIntegerField() @@ -146,6 +149,7 @@ class Round(models.Model): def __str__(self): return f"Tournament: {self.tournament.name}, Round: {self.number}" +# Model to relate the games to a round. class RoundGame(models.Model): round = models.ForeignKey(Round, on_delete=models.CASCADE) game = models.ForeignKey(Game, on_delete=models.CASCADE) @@ -163,12 +167,20 @@ def assign_permissions(sender, instance, created, **kwargs): # Obtain the necessary permissions to manage CustomUser and Player customuser_content_type = ContentType.objects.get(app_label='djapi', model='customuser') player_content_type = ContentType.objects.get(app_label='djapi', model='player') + tournament_content_type = ContentType.objects.get(app_label='djapi', model='tournament') + round_content_type = ContentType.objects.get(app_label='djapi', model='round') + roundgame_content_type = ContentType.objects.get(app_label='djapi', model='roundgame') + participation_content_type = ContentType.objects.get(app_label='djapi', model='participation') customuser_permissions = Permission.objects.filter(content_type=customuser_content_type) player_permissions = Permission.objects.filter(content_type=player_content_type) + tournament_permissions = Permission.objects.filter(content_type=tournament_content_type) + round_permissions = Permission.objects.filter(content_type=round_content_type) + roundgame_permissions = Permission.objects.filter(content_type=roundgame_content_type) + participation_permissions = Permission.objects.filter(content_type=participation_content_type) # Assign the permissions to the "Staff" group - staff_group.permissions.set(customuser_permissions | player_permissions) + staff_group.permissions.set(customuser_permissions | player_permissions | tournament_permissions | roundgame_permissions | round_permissions | participation_permissions) # Assign the user to the "Staff" group staff_group.user_set.add(instance) \ No newline at end of file diff --git a/Wordle+/django/djapi/serializers.py b/Wordle+/django/djapi/serializers.py index c2274a3..dfc59bd 100644 --- a/Wordle+/django/djapi/serializers.py +++ b/Wordle+/django/djapi/serializers.py @@ -148,6 +148,7 @@ class Meta: model = FriendList fields = ['friend'] + # Return the sender or receiver depending on the friend field. def get_friend(self, obj): request = self.context.get('request') player = request.user.player @@ -183,7 +184,6 @@ def get_player1(self, obj): def get_player2(self, obj): return obj.player2.user.username - class GameCreateSerializer(serializers.ModelSerializer): player2 = serializers.SerializerMethodField() class Meta: diff --git a/Wordle+/django/djapi/signals.py b/Wordle+/django/djapi/signals.py index 6ef1e1c..533381a 100644 --- a/Wordle+/django/djapi/signals.py +++ b/Wordle+/django/djapi/signals.py @@ -3,6 +3,7 @@ from .models import Game, RoundGame, Round import math +# Singal executed every time a tournament game is completed @receiver(post_save, sender=Game) def game_completed(sender, instance, created, **kwargs): if instance.is_tournament_game and instance.winner: diff --git a/Wordle+/django/djapi/token_expire.py b/Wordle+/django/djapi/token_expire.py index bef7ee5..94f9413 100644 --- a/Wordle+/django/djapi/token_expire.py +++ b/Wordle+/django/djapi/token_expire.py @@ -6,6 +6,8 @@ from django.http import JsonResponse from django.conf import settings +# Middleware to check if the token has expired. The middleware +# is executed everytime an API endpoint is used. class TokenExpirationMiddleware(object): def __init__(self, get_response): self.get_response = get_response diff --git a/Wordle+/django/djapi/views.py b/Wordle+/django/djapi/views.py index b54386e..f97601e 100644 --- a/Wordle+/django/djapi/views.py +++ b/Wordle+/django/djapi/views.py @@ -20,9 +20,6 @@ class CustomUserViewSet(viewsets.ModelViewSet): - """ - API endpoint that allows users to be viewed or edited. - """ queryset = CustomUser.objects.all().order_by('-date_joined') serializer_class = CustomUserSerializer @@ -68,9 +65,6 @@ def patch(self, request): return Response(serializer.errors, status=400) class PlayerViewSet(viewsets.ModelViewSet): - """ - API endpoint that allows players to be viewed or edited. - """ queryset = Player.objects.all() serializer_class = PlayerSerializer @@ -111,6 +105,7 @@ def destroy(self, request, *args, **kwargs): self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) + # Method to get the ranking players @action(detail=False, methods=['get']) def ranking(self, request): filter_param = request.GET.get('filter') @@ -140,9 +135,6 @@ def list(self, request, *args, **kwargs): return Response(usernames) class GroupViewSet(viewsets.ModelViewSet): - """ - API endpoint that allows groups to be viewed or edited. - """ queryset = Group.objects.all() serializer_class = GroupSerializer permission_classes = [permissions.IsAuthenticated] @@ -163,7 +155,6 @@ def post(self, request, *args, **kwargs): token.delete() token = Token.objects.create(user=user) - # Serialize the token along with any other data you want to include in the response response_data = { 'token': token.key, 'user_id': user.id, @@ -196,6 +187,7 @@ class ClassicWordleViewSet(viewsets.GenericViewSet): queryset = ClassicWordle.objects.all() serializer_class = ClassicWordleSerializer + # Gets limited number of classic worldes from most recent to oldest def list(self, request): player = getattr(request.user, 'player', None) if not player: @@ -223,6 +215,7 @@ class AvatarView(APIView): """ permission_classes = [permissions.IsAuthenticated] + # Gets the avatar image. Only returned if the requesting player is the owner. def get(self, request, user_id): try: user = get_object_or_404(CustomUser, id=user_id) @@ -237,6 +230,7 @@ def get(self, request, user_id): except CustomUser.DoesNotExist: return Response({'detail': 'The specified user does not exist.'}, status=404) + # Save the player avatar. If there is an existing one, is removed. def post(self, request, user_id): try: user = get_object_or_404(CustomUser, id=user_id) @@ -266,6 +260,7 @@ class NotificationsViewSet(viewsets.ModelViewSet): serializer_class = NotificationSerializer permission_classes = [permissions.IsAuthenticated, IsOwnerPermission] + # Gets limited number of notifications from the most recent to the oldest. def list(self, request): limit = int(request.query_params.get('limit', 10)) player = getattr(request.user, 'player', None) @@ -293,6 +288,7 @@ class TournamentViewSet(viewsets.ReadOnlyModelViewSet): queryset = Tournament.objects.order_by('max_players') serializer_class = TournamentSerializer + # Get a list of all the tournaments filtered by its word length. def get_queryset(self): queryset = super().get_queryset() @@ -305,6 +301,7 @@ def get_queryset(self): return queryset + # Gets the information of a specific tournament @action(detail=True, methods=['get']) def tournament_info(self, request, pk=None): tournament = Tournament.objects.get(pk=pk) @@ -317,7 +314,7 @@ def tournament_info(self, request, pk=None): serializer = TournamentSerializer(tournament) return Response(serializer.data) - + # Gets the tournament that the player is joined in. @action(detail=False, methods=['get']) def player_tournaments(self, request): player = getattr(request.user, 'player', None) @@ -328,6 +325,7 @@ def player_tournaments(self, request): serializer = TournamentSerializer(tournaments, many=True) return Response(serializer.data) + # Gets the rounds of a specific tournament. @action(detail=True, methods=['get']) def tournament_rounds(self, request, pk=None): tournament = self.get_object() @@ -344,6 +342,7 @@ def tournament_rounds(self, request, pk=None): return Response({'error': 'No rounds found for this tournament.'}, status=404) return Response(serializer.data) + # Gets the games of a specific round of a specific tournament. @action(detail=True, methods=['get'], url_path='round_games/(?P\d+)') def round_games(self, request, pk=None, round_number=None): player = getattr(request.user, 'player', None) @@ -437,7 +436,7 @@ def create(self, request, *args, **kwargs): # Create the related notification to the player message = f"You were assigned in {tournament.name}. Good luck!" - link = "http://localhost:8100/tabs/tournaments" + link = "http://localhost/tabs/tournaments" notification = Notification.objects.create(player=player, text=message, link=link) notification.save() @@ -457,6 +456,7 @@ def list(self, request, *args, **kwargs): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) + # Deletes a friend. def destroy(self, request, *args, **kwargs): player = getattr(request.user, 'player', None) friend_id = kwargs.get('pk') @@ -491,6 +491,8 @@ def list(self, request, *args, **kwargs): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) + # Creates a new friend request. It checks if the players exist and if there + # is an existing friend request between them. def create(self, request, *args, **kwargs): sender = getattr(request.user, 'player', None) if not sender: @@ -524,12 +526,13 @@ def create(self, request, *args, **kwargs): Notification.objects.create( player=receiver, text='You have a new friend request!', - link='http://localhost:8100/friendlist' + link='http://localhost/friendlist' ) serializer = FriendRequestSerializer(friend_request) return Response(serializer.data, status=status.HTTP_201_CREATED) + # Accepts a friend request. The friend relationship is created. @action(detail=True, methods=['post']) def accept(self, request, *args, **kwargs): instance = self.get_object() @@ -547,17 +550,18 @@ def accept(self, request, *args, **kwargs): Notification.objects.create( player=instance.sender, text=f"You are now friends with {receiver.user.username}.", - link='http://localhost:8100/friendlist' + link='http://localhost/friendlist' ) Notification.objects.create( player=receiver, text=f"You are now friends with {instance.sender.user.username}.", - link='http://localhost:8100/friendlist' + link='http://localhost/friendlist' ) instance.delete() return Response({'message': 'Friend request accepted'}, status=200) + # Rejects a friend request. It is deleted. @action(detail=True, methods=['post']) def reject(self, request, *args, **kwargs): instance = self.get_object() @@ -683,6 +687,7 @@ def partial_update(self, request, *args, **kwargs): player2_time = request.data.get('player2_time') player1_time = instance.player1_time + # Determine the winner if all data is available. if player2_xp is not None: player1_xp = instance.player1_xp if player2_xp > player1_xp: @@ -707,6 +712,9 @@ def partial_update(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) serializer.save() + player.xp += serializer.validated_data['player2_xp'] + player.save() + return Response({'winner': instance.winner.user.username}) # Patch method to update the tournament game. Executed by both players diff --git a/Wordle+/ionic/.dockerignore b/Wordle+/ionic/.dockerignore index b3b8cdb..b788c81 100644 --- a/Wordle+/ionic/.dockerignore +++ b/Wordle+/ionic/.dockerignore @@ -1,2 +1 @@ ionic-app/node_modules -ionic-app/src/env.json \ No newline at end of file diff --git a/Wordle+/ionic/ionic-app/src/app/components/notifications-popover/notifications-popover.component.html b/Wordle+/ionic/ionic-app/src/app/components/notifications-popover/notifications-popover.component.html index 4d26faa..10b409a 100644 --- a/Wordle+/ionic/ionic-app/src/app/components/notifications-popover/notifications-popover.component.html +++ b/Wordle+/ionic/ionic-app/src/app/components/notifications-popover/notifications-popover.component.html @@ -4,5 +4,8 @@

Notifications

{{ notification.text }} + + No notifications here! + diff --git a/Wordle+/ionic/ionic-app/src/app/pages/game-tournament/game-tournament.page.ts b/Wordle+/ionic/ionic-app/src/app/pages/game-tournament/game-tournament.page.ts index c1f6200..2883de0 100644 --- a/Wordle+/ionic/ionic-app/src/app/pages/game-tournament/game-tournament.page.ts +++ b/Wordle+/ionic/ionic-app/src/app/pages/game-tournament/game-tournament.page.ts @@ -29,6 +29,7 @@ export class GameTournamentPage implements OnInit { this.tournamentId = params['tournamentId']; this.gameId = params['idGame']; this.selfUsername = await this.storageService.getUsername(); + this.loadTournamentInfo(); }); } @@ -66,6 +67,7 @@ export class GameTournamentPage implements OnInit { } }, (error) => { + console.log(error); this.showAlert("Ups!", "You can't play this game!"); } ); @@ -98,7 +100,7 @@ export class GameTournamentPage implements OnInit { } else { if (response.winner === this.selfUsername) { - setTimeout( () => this.showAlert('Congratulations!', 'You won! You will be in the next round!'), 2500); + setTimeout( () => this.showAlert('Congratulations!', 'You won! Amazing!'), 2500); } else { setTimeout( () => this.showAlert('Bad news!', 'You lost. Try next time!'), 2500); } diff --git a/Wordle+/ionic/ionic-app/src/app/pages/respond-game/respond-game.page.ts b/Wordle+/ionic/ionic-app/src/app/pages/respond-game/respond-game.page.ts index a612208..8e01935 100644 --- a/Wordle+/ionic/ionic-app/src/app/pages/respond-game/respond-game.page.ts +++ b/Wordle+/ionic/ionic-app/src/app/pages/respond-game/respond-game.page.ts @@ -70,8 +70,10 @@ export class RespondGamePage implements OnInit { console.log('Game resolved successfully', response); if (response.winner === this.selfUsername) { setTimeout( () => this.showAlert('Congratulations!', 'You won! Amazing!'), 2500); + this.router.navigate(['/tabs/main'], { queryParams: { refresh: 'true' } }); } else { setTimeout( () => this.showAlert('Bad news!', 'You lost. Try next time!'), 2500); + this.router.navigate(['/tabs/main'], { queryParams: { refresh: 'true' } }); } }, (error) => { diff --git a/Wordle+/ionic/ionic-app/src/app/pages/tournamentrounds/tournamentrounds.page.ts b/Wordle+/ionic/ionic-app/src/app/pages/tournamentrounds/tournamentrounds.page.ts index 952f259..8b45092 100644 --- a/Wordle+/ionic/ionic-app/src/app/pages/tournamentrounds/tournamentrounds.page.ts +++ b/Wordle+/ionic/ionic-app/src/app/pages/tournamentrounds/tournamentrounds.page.ts @@ -73,6 +73,8 @@ export class TournamentroundsPage implements OnInit { (await this.apiService.getGamesRound(this.tournamentId, roundNumber)).subscribe( async (data: any) => { this.roundGames = data; + console.log(roundNumber); + console.log(data); if (roundNumber === this.lastRound && this.roundGames[0].winner) { (await this.apiService.getPlayerData(this.roundGames[0].winner)).subscribe( diff --git a/Wordle+/ionic/ionic-app/src/app/tab1/tab1.page.ts b/Wordle+/ionic/ionic-app/src/app/tab1/tab1.page.ts index 47ab809..bcde01b 100644 --- a/Wordle+/ionic/ionic-app/src/app/tab1/tab1.page.ts +++ b/Wordle+/ionic/ionic-app/src/app/tab1/tab1.page.ts @@ -88,6 +88,7 @@ export class Tab1Page implements OnInit{ if (playerId) { (await this.apiService.getPlayerData(playerId)).subscribe( (response: any) => { + this.storageService.setWins(response.wins); this.storageService.setWinsPVP(response.wins_pvp); this.storageService.setXP(response.xp); this.storageService.setWinsTournament(response.wins_tournaments); diff --git a/Wordle+/ionic/ionic-app/src/app/tab2/tab2.page.html b/Wordle+/ionic/ionic-app/src/app/tab2/tab2.page.html index 1fdca0e..28c7d90 100644 --- a/Wordle+/ionic/ionic-app/src/app/tab2/tab2.page.html +++ b/Wordle+/ionic/ionic-app/src/app/tab2/tab2.page.html @@ -41,10 +41,6 @@ - - Description: - {{ tournament.description }} - Players:

{{ tournament.num_players }}/{{ tournament.max_players }}