diff --git a/backend/tournaments/models.py b/backend/tournaments/models.py index a99bf2a..df00d24 100644 --- a/backend/tournaments/models.py +++ b/backend/tournaments/models.py @@ -104,6 +104,27 @@ def __str__(self): return f"{self.name} ({self.school})" +class Coach(models.Model): + """ + Represents a coach for a team. + """ + team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name='coaches') + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='coached_teams') + + name = models.CharField(max_length=255) + email = models.EmailField(blank=True) + phone = models.CharField(max_length=50, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['name'] + + def __str__(self): + return f"{self.name} ({self.team.name})" + + class Player(models.Model): """ Represents an individual player on a team. diff --git a/backend/tournaments/serializers.py b/backend/tournaments/serializers.py index ce92f2d..f8b92ab 100644 --- a/backend/tournaments/serializers.py +++ b/backend/tournaments/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Tournament, Team, Player, Room, Round, Game +from .models import Tournament, Team, Coach, Player, Room, Round, Game class TournamentDirectorSerializer(serializers.Serializer): @@ -55,14 +55,27 @@ def get_director(self, obj): class TeamSerializer(serializers.ModelSerializer): """Serializer for teams.""" players_count = serializers.SerializerMethodField() + coaches_count = serializers.SerializerMethodField() class Meta: model = Team - fields = ['id', 'name', 'school', 'seed', 'pool', 'players_count'] + fields = ['id', 'name', 'school', 'seed', 'pool', 'players_count', 'coaches_count'] def get_players_count(self, obj): return obj.players.count() + def get_coaches_count(self, obj): + return obj.coaches.count() + + +class CoachSerializer(serializers.ModelSerializer): + """Serializer for coaches.""" + team_name = serializers.CharField(source='team.name', read_only=True) + + class Meta: + model = Coach + fields = ['id', 'name', 'email', 'phone', 'team', 'team_name'] + class PlayerSerializer(serializers.ModelSerializer): """Serializer for players.""" diff --git a/backend/tournaments/urls.py b/backend/tournaments/urls.py index b5179ee..b340a36 100644 --- a/backend/tournaments/urls.py +++ b/backend/tournaments/urls.py @@ -1,10 +1,12 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import TournamentViewSet, TeamViewSet, RoomViewSet, GameViewSet +from .views import TournamentViewSet, TeamViewSet, CoachViewSet, PlayerViewSet, RoomViewSet, GameViewSet router = DefaultRouter() router.register(r'tournaments', TournamentViewSet, basename='tournament') router.register(r'teams', TeamViewSet, basename='team') +router.register(r'coaches', CoachViewSet, basename='coach') +router.register(r'players', PlayerViewSet, basename='player') router.register(r'rooms', RoomViewSet, basename='room') router.register(r'games', GameViewSet, basename='game') diff --git a/backend/tournaments/views.py b/backend/tournaments/views.py index 62932d0..dc58d49 100644 --- a/backend/tournaments/views.py +++ b/backend/tournaments/views.py @@ -1,10 +1,12 @@ -from rest_framework import viewsets, permissions +from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.response import Response -from .models import Tournament, Team, Player, Room, Round, Game +from django.db import transaction +from itertools import combinations +from .models import Tournament, Team, Coach, Player, Room, Round, Game from .serializers import ( TournamentListSerializer, TournamentDetailSerializer, - TeamSerializer, PlayerSerializer, RoomSerializer, + TeamSerializer, CoachSerializer, PlayerSerializer, RoomSerializer, RoundSerializer, GameSerializer ) @@ -70,13 +72,105 @@ def games(self, request, pk=None): serializer = GameSerializer(games, many=True) return Response(serializer.data) + @action(detail=True, methods=['post']) + def generate_schedule(self, request, pk=None): + """ + Generate round-robin matches for all pools in the tournament. + Creates Game objects for all teams in each pool to play each other once. + """ + tournament = self.get_object() + + # Get all teams grouped by pool + teams = tournament.teams.all() + pools = {} + for team in teams: + pool_name = team.pool or 'Unassigned' + if pool_name not in pools: + pools[pool_name] = [] + pools[pool_name].append(team) + + # Check if any games already exist + existing_games_count = tournament.games.count() + if existing_games_count > 0: + return Response( + {'error': f'Tournament already has {existing_games_count} games. Delete existing games first.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Check if rooms exist + rooms = list(tournament.rooms.all()) + if not rooms: + return Response( + {'error': 'No rooms configured. Please add rooms before generating schedule.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + generated_games = [] + + with transaction.atomic(): + round_number = 1 + + # For each pool, generate round-robin matchups + for pool_name, pool_teams in pools.items(): + if pool_name == 'Unassigned' or len(pool_teams) < 2: + continue + + # Generate all possible matchups (combinations of 2) + matchups = list(combinations(pool_teams, 2)) -class TeamViewSet(viewsets.ReadOnlyModelViewSet): - """ViewSet for viewing teams.""" + # Create games for each matchup + for idx, (team1, team2) in enumerate(matchups): + # Get or create round + round_obj, _ = Round.objects.get_or_create( + tournament=tournament, + round_number=round_number, + defaults={'name': f'Pool {pool_name} - Round {round_number}'} + ) + + # Assign to room (rotate through available rooms) + room = rooms[idx % len(rooms)] + + # Create game + game = Game.objects.create( + tournament=tournament, + round=round_obj, + room=room, + team1=team1, + team2=team2 + ) + + generated_games.append({ + 'id': game.id, + 'pool': pool_name, + 'round_number': round_number, + 'room_name': room.name, + 'team1_name': team1.name, + 'team2_name': team2.name, + }) + + # Move to next round after filling all rooms + if (idx + 1) % len(rooms) == 0: + round_number += 1 + + # Ensure next pool starts on a new round + round_number += 1 + + return Response({ + 'message': f'Successfully generated {len(generated_games)} games across {len(pools) - (1 if "Unassigned" in pools else 0)} pools', + 'games': generated_games, + 'pools': {pool: len(teams) for pool, teams in pools.items() if pool != 'Unassigned'}, + }, status=status.HTTP_201_CREATED) + + +class TeamViewSet(viewsets.ModelViewSet): + """ + ViewSet for managing teams. + Supports full CRUD operations. + """ queryset = Team.objects.all() serializer_class = TeamSerializer permission_classes = [permissions.AllowAny] - + @action(detail=True, methods=['get']) def players(self, request, pk=None): """Get all players for a team.""" @@ -85,6 +179,34 @@ def players(self, request, pk=None): serializer = PlayerSerializer(players, many=True) return Response(serializer.data) + @action(detail=True, methods=['get']) + def coaches(self, request, pk=None): + """Get all coaches for a team.""" + team = self.get_object() + coaches = team.coaches.all() + serializer = CoachSerializer(coaches, many=True) + return Response(serializer.data) + + +class CoachViewSet(viewsets.ModelViewSet): + """ + ViewSet for managing coaches. + Supports full CRUD operations. + """ + queryset = Coach.objects.all() + serializer_class = CoachSerializer + permission_classes = [permissions.AllowAny] + + +class PlayerViewSet(viewsets.ModelViewSet): + """ + ViewSet for managing players. + Supports full CRUD operations. + """ + queryset = Player.objects.all() + serializer_class = PlayerSerializer + permission_classes = [permissions.AllowAny] + class RoomViewSet(viewsets.ReadOnlyModelViewSet): """ViewSet for viewing rooms."""