Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions backend/tournaments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 15 additions & 2 deletions backend/tournaments/serializers.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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."""
Expand Down
4 changes: 3 additions & 1 deletion backend/tournaments/urls.py
Original file line number Diff line number Diff line change
@@ -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')

Expand Down
134 changes: 128 additions & 6 deletions backend/tournaments/views.py
Original file line number Diff line number Diff line change
@@ -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
)

Expand Down Expand Up @@ -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."""
Expand All @@ -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."""
Expand Down