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
145 changes: 145 additions & 0 deletions backend/tournaments/management/commands/load_live_tournament_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from datetime import date
from tournaments.models import Tournament, Team, Player, Room, Round

User = get_user_model()


class Command(BaseCommand):
help = 'Load sample data for a live tournament with 16 teams, players, rooms, and rounds'

def handle(self, *args, **kwargs):
# Team names - creative science-themed names
team_names = [
("Quantum Quizzers", "MIT"),
("Atomic Aces", "Stanford"),
("Neural Networks", "UC Berkeley"),
("Periodic Panthers", "Caltech"),
("Genomic Giants", "Harvard"),
("Photon Phasers", "Princeton"),
("Molecular Mavericks", "Yale"),
("Cosmic Crusaders", "Cornell"),
("Electron Eagles", "Columbia"),
("Particle Prodigies", "Carnegie Mellon"),
("Galactic Geniuses", "Duke"),
("Chemical Champions", "Northwestern"),
("Velocity Victors", "UChicago"),
("Thermal Titans", "Georgia Tech"),
("Nucleus Knights", "Rice"),
("Plasma Pioneers", "Johns Hopkins"),
]

# Get or create tournament director
director, _ = User.objects.get_or_create(
username='tournament_director',
defaults={
'first_name': 'Sarah',
'last_name': 'Chen',
'email': '[email protected]',
'bio': 'Tournament director for Stanford Science Bowl.',
'school': 'Stanford University',
}
)

# Get the Stanford 2026 High School Tournament and set it to live
try:
tournament = Tournament.objects.get(name='Stanford 2026 High School Tournament')
# Update tournament to be live
tournament.status = 'IN_PROGRESS'
tournament.format = 'ROUND_ROBIN'
tournament.max_teams = 16
tournament.current_teams = 16
tournament.save()

self.stdout.write(self.style.SUCCESS(f'Found tournament: {tournament.name}'))
self.stdout.write(self.style.SUCCESS(f'Set status to: IN_PROGRESS'))

# Clear existing data for this tournament
tournament.teams.all().delete()
tournament.rooms.all().delete()
tournament.rounds.all().delete()
self.stdout.write(self.style.WARNING('Cleared existing tournament data'))
except Tournament.DoesNotExist:
self.stdout.write(self.style.ERROR('Stanford 2026 High School Tournament not found!'))
self.stdout.write(self.style.ERROR('Please run: python manage.py load_sample_tournaments first'))
return

# Create 16 teams with 5 players each, assigned to 4 pools (A, B, C, D)
teams = []
pools = ['A', 'B', 'C', 'D']
for idx, (team_name, school) in enumerate(team_names, 1):
# Assign teams to pools: seeds 1,5,9,13 to Pool A, 2,6,10,14 to Pool B, etc.
pool = pools[(idx - 1) % 4]
team = Team.objects.create(
tournament=tournament,
name=team_name,
school=school,
seed=idx,
pool=pool,
)
teams.append(team)

# Create 5 players for each team
positions = ['Captain', 'Vice Captain', 'Member', 'Member', 'Alternate']
for player_idx in range(5):
Player.objects.create(
team=team,
name=f"{team_name.split()[0]} Player {player_idx + 1}",
grade_level=str(11 if player_idx < 3 else 10),
total_points=0,
tossups_heard=0,
correct_buzzes=0,
incorrect_buzzes=0,
)

self.stdout.write(self.style.SUCCESS(f'Created team: {team_name} with 5 players'))

# Create 8 rooms
room_names = [
"Physics Lab A",
"Chemistry Lab B",
"Biology Lab C",
"Math Room 201",
"Earth Science 305",
"Energy Lab D",
"Auditorium East",
"Auditorium West",
]

rooms = []
for room_name in room_names:
room = Room.objects.create(
tournament=tournament,
name=room_name,
status='IN_PROGRESS',
current_round=2, # Currently in round 2
)
rooms.append(room)
self.stdout.write(self.style.SUCCESS(f'Created room: {room_name}'))

# Create 5 rounds with packet assignments
rounds_data = [
(1, "Round 1 - Pool Play", "NSB 2024 Regionals Set 1"),
(2, "Round 2 - Pool Play", "NSB 2024 Regionals Set 2"),
(3, "Round 3 - Pool Play", "NSB 2024 Nationals Prelim"),
(4, "Round 4 - Semifinals", "NSB 2025 Regionals Set 1"),
(5, "Round 5 - Finals", "NSB 2025 Nationals Finals"),
]

for round_num, round_name, packet_name in rounds_data:
Round.objects.create(
tournament=tournament,
round_number=round_num,
name=round_name,
packet_name=packet_name,
)
self.stdout.write(self.style.SUCCESS(f'Created round: {round_name}'))

self.stdout.write(self.style.SUCCESS(
f'\n✅ Live tournament data loaded successfully!'
f'\n - 16 teams created'
f'\n - 80 players created (5 per team)'
f'\n - 8 rooms created'
f'\n - 5 rounds created'
))
18 changes: 18 additions & 0 deletions backend/tournaments/migrations/0003_team_pool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-12-20 23:20

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('tournaments', '0002_tournament_registration_url_tournament_website_url'),
]

operations = [
migrations.AddField(
model_name='team',
name='pool',
field=models.CharField(blank=True, help_text="Pool/Group assignment (e.g., 'A', 'B', 'C', 'D')", max_length=10),
),
]
1 change: 1 addition & 0 deletions backend/tournaments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class Team(models.Model):
name = models.CharField(max_length=255)
school = models.CharField(max_length=255)
seed = models.IntegerField(null=True, blank=True)
pool = models.CharField(max_length=10, blank=True, help_text="Pool/Group assignment (e.g., 'A', 'B', 'C', 'D')")

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
Expand Down
6 changes: 3 additions & 3 deletions backend/tournaments/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ def get_director(self, obj):
class TeamSerializer(serializers.ModelSerializer):
"""Serializer for teams."""
players_count = serializers.SerializerMethodField()

class Meta:
model = Team
fields = ['id', 'name', 'school', 'seed', 'players_count']
fields = ['id', 'name', 'school', 'seed', 'pool', 'players_count']

def get_players_count(self, obj):
return obj.players.count()

Expand Down
28 changes: 13 additions & 15 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,19 @@ services:
db:
condition: service_healthy

frontend:
image: node:20
working_dir: /app
command: sh -c "npm install && npm run dev -- --host --force"
volumes:
- ./frontend:/app
- /app/node_modules
ports:
- "5173:5173"
environment:
- VITE_API_URL=http://localhost:8000
- CHOKIDAR_USEPOLLING=true
- CHOKIDAR_INTERVAL=100
stdin_open: true
tty: true
frontend:
image: node:20
working_dir: /app
command: sh -c "npm install && npm run dev -- --host"
volumes:
- ./frontend:/app
- /app/node_modules
ports:
- "5173:5173"
environment:
- VITE_API_URL=http://localhost:8000
stdin_open: true
tty: true

volumes:
postgres_data:
53 changes: 52 additions & 1 deletion frontend/src/features/tournaments/pages/TournamentDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function TournamentDetailPage() {
const [teams, setTeams] = useState<Team[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'overview' | 'teams' | 'contact'>('overview');
const [activeTab, setActiveTab] = useState<'overview' | 'teams' | 'pools' | 'contact'>('overview');

useEffect(() => {
if (id) {
Expand Down Expand Up @@ -170,6 +170,18 @@ export function TournamentDetailPage() {
>
Teams ({teams.length})
</button>
{tournament.format === 'ROUND_ROBIN' && tournament.status === 'IN_PROGRESS' && (
<button
onClick={() => setActiveTab('pools')}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === 'pools'
? 'text-purple-400 border-b-2 border-purple-400'
: 'text-slate-400 hover:text-slate-200'
}`}
>
Pools
</button>
)}
<button
onClick={() => setActiveTab('contact')}
className={`px-4 py-2 font-medium transition-colors ${
Expand Down Expand Up @@ -236,6 +248,45 @@ export function TournamentDetailPage() {
</div>
)}

{activeTab === 'pools' && (
<div className="space-y-6">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold text-white mb-2">Round Robin Pools</h2>
<p className="text-slate-400">4 Groups of 4 Teams</p>
</div>
<div className="grid md:grid-cols-2 gap-6">
{['A', 'B', 'C', 'D'].map((pool) => {
const poolTeams = teams.filter((team) => team.pool === pool);
return (
<div key={pool} className="bg-slate-800/50 border border-slate-700 rounded-lg p-6">
<h3 className="text-xl font-bold text-purple-400 mb-4 text-center">Pool {pool}</h3>
<div className="space-y-3">
{poolTeams.length === 0 ? (
<p className="text-slate-400 text-center">No teams assigned</p>
) : (
poolTeams.map((team) => (
<div key={team.id} className="bg-slate-900/50 border border-slate-700 rounded-lg p-4 hover:border-purple-500/30 transition-colors">
<div className="flex justify-between items-center">
<div>
<div className="text-white font-semibold">{team.name}</div>
<div className="text-slate-400 text-sm">{team.school}</div>
</div>
<div className="text-right">
<div className="text-purple-400 text-sm font-medium">Seed #{team.seed}</div>
<div className="text-slate-500 text-xs">{team.players_count} players</div>
</div>
</div>
</div>
))
)}
</div>
</div>
);
})}
</div>
</div>
)}

{activeTab === 'contact' && (
<div className="bg-slate-800/50 border border-slate-700 rounded-lg p-6">
<h2 className="text-xl font-bold text-white mb-4">Tournament Director</h2>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/shared/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export interface Team {
name: string;
school: string;
seed: number | null;
pool: string;
players_count: number;
}

Expand Down