Added seperate courts
This commit is contained in:
+28
-29
@@ -28,7 +28,7 @@ def create_tournament(db: Session, data: schemas.TournamentCreate):
|
||||
type=data.type,
|
||||
)
|
||||
new_t.teams = [models.Team(name=n) for n in data.teams]
|
||||
new_t.courts = [models.Court(name=n) for n in data.courts]
|
||||
new_t.courts = db.query(models.Court).filter(models.Court.id.in_(data.courts)).all()
|
||||
|
||||
db.add(new_t)
|
||||
db.commit()
|
||||
@@ -141,53 +141,52 @@ def delete_team(db: Session, tournament_id: str, team_id: int):
|
||||
return True
|
||||
|
||||
|
||||
def get_courts(db: Session, tournament_id: str):
|
||||
return (
|
||||
db.query(models.Court).filter(models.Court.tournament_id == tournament_id).all()
|
||||
)
|
||||
def get_courts(db: Session):
|
||||
return db.query(models.Court).all()
|
||||
|
||||
|
||||
def create_court(db: Session, tournament_id: str, court_data: schemas.CourtCreate):
|
||||
t = get_tournament(db, tournament_id)
|
||||
if not t:
|
||||
return None
|
||||
|
||||
new_court = models.Court(name=court_data.name, tournament_id=tournament_id)
|
||||
def create_court(db: Session, court_data: schemas.CourtCreate):
|
||||
new_court = models.Court(name=court_data.name)
|
||||
db.add(new_court)
|
||||
db.flush()
|
||||
|
||||
logic.update_schedule_times(db, t)
|
||||
|
||||
db.commit()
|
||||
db.refresh(new_court)
|
||||
return new_court
|
||||
|
||||
|
||||
def delete_court(db: Session, tournament_id: str, court_id: int):
|
||||
t = get_tournament(db, tournament_id)
|
||||
if not t:
|
||||
return None
|
||||
def delete_court(db: Session, court_id: int):
|
||||
court = db.get(models.Court, court_id)
|
||||
if not court or court.tournament_id != tournament_id:
|
||||
return None
|
||||
if not court:
|
||||
return False
|
||||
|
||||
affected_tournaments = list(court.tournaments)
|
||||
|
||||
matches = db.query(models.Match).filter(models.Match.court_id == court_id).all()
|
||||
for m in matches:
|
||||
if m.status == models.MatchStatus.FINISHED:
|
||||
m.court_id = None
|
||||
else:
|
||||
m.court_id = None
|
||||
m.start_time = None
|
||||
|
||||
db.delete(court)
|
||||
db.flush()
|
||||
|
||||
logic.update_schedule_times(db, t)
|
||||
|
||||
db.commit()
|
||||
|
||||
processed_days = set()
|
||||
for t in affected_tournaments:
|
||||
day = t.timestamp.date() if t.timestamp else None
|
||||
if day and day not in processed_days:
|
||||
logic.update_schedule_times(db, t)
|
||||
processed_days.add(day)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def update_tournament_courts(
|
||||
db: Session, tournament_id: str, new_court_names: list[str]
|
||||
):
|
||||
def update_tournament_courts(db: Session, tournament_id: str, new_court_ids: list[int]):
|
||||
t = get_tournament(db, tournament_id)
|
||||
if not t:
|
||||
return None
|
||||
|
||||
t.courts = [models.Court(name=c, tournament_id=t.id) for c in new_court_names]
|
||||
t.courts = db.query(models.Court).filter(models.Court.id.in_(new_court_ids)).all()
|
||||
db.flush()
|
||||
logic.update_schedule_times(db, t)
|
||||
|
||||
|
||||
+130
-55
@@ -1,6 +1,6 @@
|
||||
# backend/app/logic.py
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -79,59 +79,108 @@ def generate_bracket(db: Session, t: models.Tournament):
|
||||
|
||||
|
||||
def update_schedule_times(db: Session, t: models.Tournament):
|
||||
if not t.courts or not t.matches:
|
||||
if not t.timestamp:
|
||||
return
|
||||
|
||||
matches = t.matches
|
||||
match_map = {m.id: m for m in matches}
|
||||
# 1. Grab the entire day of tournaments
|
||||
target_date = t.timestamp.date()
|
||||
start_of_day = datetime.combine(target_date, datetime.min.time())
|
||||
end_of_day = start_of_day + timedelta(days=1)
|
||||
|
||||
tournaments = (
|
||||
db.query(models.Tournament)
|
||||
.filter(
|
||||
models.Tournament.timestamp >= start_of_day,
|
||||
models.Tournament.timestamp < end_of_day,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
if not tournaments:
|
||||
return
|
||||
|
||||
all_matches: list[models.Match] = []
|
||||
match_map: dict[str, models.Match] = {}
|
||||
prereqs = defaultdict(list)
|
||||
for m in matches:
|
||||
if m.winner_next_match_id:
|
||||
prereqs[m.winner_next_match_id].append(m.id)
|
||||
if m.loser_next_match_id:
|
||||
prereqs[m.loser_next_match_id].append(m.id)
|
||||
|
||||
depth_cache = {}
|
||||
|
||||
# 2. Gather all matches and prereqs globally
|
||||
for t_item in tournaments:
|
||||
for m in t_item.matches:
|
||||
all_matches.append(m)
|
||||
match_map[m.id] = m
|
||||
if m.winner_next_match_id:
|
||||
prereqs[m.winner_next_match_id].append(m.id)
|
||||
if m.loser_next_match_id:
|
||||
prereqs[m.loser_next_match_id].append(m.id)
|
||||
|
||||
# 3. Calculate depths
|
||||
def get_depth(m_id):
|
||||
if m_id not in match_map:
|
||||
return 0
|
||||
if m_id in depth_cache:
|
||||
return depth_cache[m_id]
|
||||
|
||||
m = match_map[m_id]
|
||||
child_depths = [0]
|
||||
if m.winner_next_match_id:
|
||||
child_depths.append(get_depth(m.winner_next_match_id))
|
||||
if m.loser_next_match_id:
|
||||
child_depths.append(get_depth(m.loser_next_match_id))
|
||||
|
||||
depth = 1 + max(child_depths)
|
||||
depth_cache[m_id] = depth
|
||||
return depth
|
||||
|
||||
for m in matches:
|
||||
for m in all_matches:
|
||||
get_depth(m.id)
|
||||
|
||||
tournament_start = (
|
||||
t.timestamp.replace(tzinfo=None) if t.timestamp.tzinfo else t.timestamp
|
||||
)
|
||||
court_timers = {c.id: tournament_start for c in t.courts}
|
||||
# 4. Initialize timers & lock finished matches
|
||||
all_courts: set[models.Court] = set()
|
||||
court_sharing_count = defaultdict(
|
||||
int
|
||||
) # NEW: Track how highly contested each court is
|
||||
|
||||
for t_item in tournaments:
|
||||
for c in t_item.courts:
|
||||
all_courts.add(c)
|
||||
court_sharing_count[c.id] += 1 # NEW
|
||||
|
||||
court_timers = {c.id: start_of_day for c in all_courts}
|
||||
planned_finish_times = {}
|
||||
unscheduled = list(matches)
|
||||
unscheduled: list[models.Match] = []
|
||||
|
||||
loop_limit = len(matches) * 2
|
||||
for m in all_matches:
|
||||
if m.status == MatchStatus.FINISHED and m.start_time and m.court_id:
|
||||
fin = m.start_time + timedelta(minutes=m.tournament.duration)
|
||||
planned_finish_times[m.id] = fin
|
||||
if court_timers.get(m.court_id, start_of_day) < fin:
|
||||
court_timers[m.court_id] = fin
|
||||
else:
|
||||
unscheduled.append(m)
|
||||
|
||||
tournament_match_counts = {t_item.id: 0 for t_item in tournaments}
|
||||
|
||||
# 5. Global Interleaving Schedule Loop
|
||||
loop_limit = len(unscheduled) * 3
|
||||
while unscheduled and loop_limit > 0:
|
||||
loop_limit -= 1
|
||||
best_court_id = min(court_timers, key=lambda k: court_timers[k])
|
||||
|
||||
best_court_id = min(
|
||||
court_timers.keys(), key=lambda k: (court_timers[k], court_sharing_count[k])
|
||||
)
|
||||
current_time = court_timers[best_court_id]
|
||||
|
||||
ready: list[models.Match] = []
|
||||
for m in unscheduled:
|
||||
if best_court_id not in [c.id for c in m.tournament.courts]:
|
||||
continue
|
||||
|
||||
is_ready = True
|
||||
max_prereq_time = tournament_start
|
||||
max_prereq_time = (
|
||||
m.tournament.timestamp.replace(tzinfo=None)
|
||||
if m.tournament.timestamp.tzinfo
|
||||
else m.tournament.timestamp
|
||||
)
|
||||
|
||||
for p_id in prereqs[m.id]:
|
||||
if p_id not in planned_finish_times:
|
||||
is_ready = False
|
||||
@@ -143,27 +192,41 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
||||
|
||||
if ready:
|
||||
ready.sort(
|
||||
key=lambda x: (-depth_cache[x.id], x.round_number, x.match_number)
|
||||
key=lambda x: (
|
||||
len(x.tournament.courts),
|
||||
tournament_match_counts[x.tournament_id],
|
||||
-depth_cache[x.id],
|
||||
x.round_number,
|
||||
x.match_number,
|
||||
)
|
||||
)
|
||||
cand = ready[0]
|
||||
|
||||
cand.court_id = best_court_id
|
||||
cand.start_time = current_time
|
||||
|
||||
fin = current_time + timedelta(minutes=t.duration)
|
||||
fin = current_time + timedelta(minutes=cand.tournament.duration)
|
||||
planned_finish_times[cand.id] = fin
|
||||
court_timers[best_court_id] = fin
|
||||
tournament_match_counts[cand.tournament_id] += 1
|
||||
unscheduled.remove(cand)
|
||||
else:
|
||||
next_wake = None
|
||||
for m in unscheduled:
|
||||
if best_court_id not in [c.id for c in m.tournament.courts]:
|
||||
continue
|
||||
is_ready = True
|
||||
max_prereq_time = tournament_start
|
||||
max_prereq_time = (
|
||||
m.tournament.timestamp.replace(tzinfo=None)
|
||||
if m.tournament.timestamp.tzinfo
|
||||
else m.tournament.timestamp
|
||||
)
|
||||
for p_id in prereqs[m.id]:
|
||||
if p_id not in planned_finish_times:
|
||||
is_ready = False
|
||||
break
|
||||
max_prereq_time = max(max_prereq_time, planned_finish_times[p_id])
|
||||
|
||||
if is_ready and max_prereq_time > current_time:
|
||||
if next_wake is None or max_prereq_time < next_wake:
|
||||
next_wake = max_prereq_time
|
||||
@@ -171,22 +234,26 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
||||
if next_wake:
|
||||
court_timers[best_court_id] = next_wake
|
||||
else:
|
||||
break
|
||||
court_timers[best_court_id] += timedelta(minutes=5)
|
||||
|
||||
# ==========================================
|
||||
# --- DYNAMIC REFEREE ASSIGNMENT ---
|
||||
# ==========================================
|
||||
all_matches = sorted(list(matches), key=lambda x: (x.start_time, x.court_id))
|
||||
# 6. Dynamic Referee Assignment (Runs globally across all interleaved matches!)
|
||||
all_matches = sorted(
|
||||
all_matches, key=lambda x: (x.start_time or start_of_day, x.court_id or 0)
|
||||
)
|
||||
duty_counts = defaultdict(int)
|
||||
active_refs = []
|
||||
|
||||
def is_busy(outcome_tuple, source_m, start, end):
|
||||
next_m_id = source_m.winner_next_match_id if outcome_tuple[0] == "W" else source_m.loser_next_match_id
|
||||
def is_busy(outcome_tuple, source_m: models.Match, start, end):
|
||||
next_m_id = (
|
||||
source_m.winner_next_match_id
|
||||
if outcome_tuple[0] == "W"
|
||||
else source_m.loser_next_match_id
|
||||
)
|
||||
if next_m_id:
|
||||
next_m = match_map[next_m_id]
|
||||
next_start = next_m.start_time
|
||||
if next_start:
|
||||
next_end = next_start + timedelta(minutes=t.duration)
|
||||
next_end = next_start + timedelta(minutes=next_m.tournament.duration)
|
||||
if not (next_end <= start or next_start >= end):
|
||||
return True
|
||||
for r_outcome, r_start, r_end in active_refs:
|
||||
@@ -198,29 +265,37 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
||||
for m in all_matches:
|
||||
if not m.start_time:
|
||||
continue
|
||||
|
||||
|
||||
m_start = m.start_time
|
||||
m_end = m_start + timedelta(minutes=t.duration)
|
||||
m_end = m_start + timedelta(minutes=m.tournament.duration)
|
||||
|
||||
if m.bracket_type == BracketTypes.FINALS:
|
||||
prev_final = next((p for p in all_matches if p.bracket_type == BracketTypes.FINALS and p.winner_next_match_id == m.id), None)
|
||||
|
||||
prev_final = next(
|
||||
(
|
||||
p
|
||||
for p in all_matches
|
||||
if p.bracket_type == BracketTypes.FINALS
|
||||
and p.winner_next_match_id == m.id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if prev_final:
|
||||
m.ref_team_id = prev_final.ref_team_id
|
||||
m.ref_label = prev_final.ref_label
|
||||
|
||||
identifier = f"TEAM_{m.ref_team_id}" if m.ref_team_id else prev_final.ref_label
|
||||
identifier = (
|
||||
f"TEAM_{m.ref_team_id}" if m.ref_team_id else prev_final.ref_label
|
||||
)
|
||||
active_refs.append((identifier, m_start, m_end))
|
||||
continue
|
||||
|
||||
|
||||
best_outcome = None
|
||||
best_score = float('inf')
|
||||
|
||||
best_score = float("inf")
|
||||
|
||||
for prev_m in all_matches:
|
||||
if not prev_m.start_time:
|
||||
continue
|
||||
prev_end = prev_m.start_time + timedelta(minutes=t.duration)
|
||||
|
||||
prev_end = prev_m.start_time + timedelta(minutes=prev_m.tournament.duration)
|
||||
|
||||
if prev_end <= m_start:
|
||||
l_outcome = ("L", prev_m.id)
|
||||
if not is_busy(l_outcome, prev_m, m_start, m_end):
|
||||
@@ -231,17 +306,17 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
best_outcome = (l_outcome, prev_m)
|
||||
|
||||
|
||||
w_outcome = ("W", prev_m.id)
|
||||
if not is_busy(w_outcome, prev_m, m_start, m_end):
|
||||
wait_mins = (m_start - prev_end).total_seconds() / 60.0
|
||||
score = (duty_counts[w_outcome] * 120) + wait_mins + 60
|
||||
score = (duty_counts[w_outcome] * 120) + wait_mins + 60
|
||||
if prev_m.court_id == m.court_id:
|
||||
score -= 30
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
best_outcome = (w_outcome, prev_m)
|
||||
|
||||
|
||||
if best_outcome:
|
||||
outcome_tuple, prev_m = best_outcome
|
||||
duty_counts[outcome_tuple] += 1
|
||||
@@ -249,20 +324,18 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
||||
role = "Winner" if outcome_tuple[0] == "W" else "Loser"
|
||||
m.ref_label = f"{role} of #{prev_m.match_number}"
|
||||
else:
|
||||
m.ref_label = "Staff / Volunteers"
|
||||
m.ref_label = "Staff / Volunteers"
|
||||
for future_m in all_matches:
|
||||
if future_m.start_time and future_m.start_time >= m_end:
|
||||
assigned = False
|
||||
|
||||
for t_id in [future_m.p1_team_id, future_m.p2_team_id]:
|
||||
if t_id:
|
||||
team_identifier = f"TEAM_{t_id}"
|
||||
|
||||
is_team_busy = any(
|
||||
r_outcome == team_identifier and not (r_end <= m_start or r_start >= m_end)
|
||||
r_outcome == team_identifier
|
||||
and not (r_end <= m_start or r_start >= m_end)
|
||||
for r_outcome, r_start, r_end in active_refs
|
||||
)
|
||||
|
||||
if not is_team_busy:
|
||||
m.ref_team_id = t_id
|
||||
m.ref_label = None
|
||||
@@ -271,7 +344,6 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
||||
break
|
||||
if assigned:
|
||||
break
|
||||
# ----------------------------------
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -355,7 +427,10 @@ def undo_advancement(db: Session, match: models.Match):
|
||||
return
|
||||
|
||||
for m in match.tournament.matches:
|
||||
if m.ref_label == f"Loser of #{match.match_number}" or m.ref_label == f"Winner of #{match.match_number}":
|
||||
if (
|
||||
m.ref_label == f"Loser of #{match.match_number}"
|
||||
or m.ref_label == f"Winner of #{match.match_number}"
|
||||
):
|
||||
m.ref_team_id = None
|
||||
db.add(m)
|
||||
|
||||
@@ -385,5 +460,5 @@ def undo_advancement(db: Session, match: models.Match):
|
||||
clear_from_next(match.winner_next_match, match.winner_next_match_slot)
|
||||
if match.loser_next_match and loser_id:
|
||||
clear_from_next(match.loser_next_match, match.loser_next_match_slot)
|
||||
|
||||
db.commit()
|
||||
|
||||
db.commit()
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@ from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from .database import Base, engine
|
||||
from .routes import auth, tournaments, websocket
|
||||
from .routes import auth, tournaments, websocket, courts
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -15,6 +15,7 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
app = FastAPI(title="Tournament Bracket API", lifespan=lifespan, root_path="/api")
|
||||
app.include_router(tournaments.router)
|
||||
app.include_router(courts.router)
|
||||
app.include_router(auth.router)
|
||||
app.include_router(websocket.router)
|
||||
app.add_middleware(
|
||||
|
||||
+68
-26
@@ -2,7 +2,7 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import JSON, DateTime
|
||||
from sqlalchemy import JSON, DateTime, Table, Column
|
||||
from sqlalchemy import Enum as SqlEnum
|
||||
from sqlalchemy import ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -10,6 +10,33 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from .constants import MatchStatus, TournamentTypes, BracketTypes
|
||||
from .database import Base
|
||||
|
||||
tournament_courts = Table(
|
||||
"tournament_courts",
|
||||
Base.metadata,
|
||||
Column(
|
||||
"tournament_id",
|
||||
String(8),
|
||||
ForeignKey("tournaments.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
Column(
|
||||
"court_id",
|
||||
Integer,
|
||||
ForeignKey("courts.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Court(Base):
|
||||
__tablename__ = "courts"
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False, unique=True)
|
||||
|
||||
tournaments: Mapped[list["Tournament"]] = relationship(
|
||||
"Tournament", secondary=tournament_courts, back_populates="courts"
|
||||
)
|
||||
|
||||
|
||||
class Tournament(Base):
|
||||
__tablename__ = "tournaments"
|
||||
@@ -25,7 +52,7 @@ class Tournament(Base):
|
||||
"Team", back_populates="tournament", cascade="all, delete-orphan"
|
||||
)
|
||||
courts: Mapped[list["Court"]] = relationship(
|
||||
"Court", back_populates="tournament", cascade="all, delete-orphan"
|
||||
"Court", secondary=tournament_courts, back_populates="tournaments"
|
||||
)
|
||||
|
||||
matches: Mapped[list["Match"]] = relationship(
|
||||
@@ -50,15 +77,6 @@ class Team(Base):
|
||||
tournament: Mapped["Tournament"] = relationship(back_populates="teams")
|
||||
|
||||
|
||||
class Court(Base):
|
||||
__tablename__ = "courts"
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
tournament_id: Mapped[str] = mapped_column(ForeignKey("tournaments.id"))
|
||||
|
||||
tournament: Mapped["Tournament"] = relationship(back_populates="courts")
|
||||
|
||||
|
||||
class Match(Base):
|
||||
__tablename__ = "matches"
|
||||
|
||||
@@ -70,32 +88,56 @@ class Match(Base):
|
||||
|
||||
bracket_type: Mapped[BracketTypes] = mapped_column(SqlEnum(BracketTypes))
|
||||
|
||||
court_id: Mapped[Optional[int]] = mapped_column(ForeignKey("courts.id"), nullable=True)
|
||||
court_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("courts.id"), nullable=True
|
||||
)
|
||||
start_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
status: Mapped[MatchStatus] = mapped_column(SqlEnum(MatchStatus), default=MatchStatus.SCHEDULED)
|
||||
status: Mapped[MatchStatus] = mapped_column(
|
||||
SqlEnum(MatchStatus), default=MatchStatus.SCHEDULED
|
||||
)
|
||||
|
||||
p1_team_id: Mapped[Optional[int]] = mapped_column(ForeignKey("teams.id"), nullable=True)
|
||||
p2_team_id: Mapped[Optional[int]] = mapped_column(ForeignKey("teams.id"), nullable=True)
|
||||
|
||||
ref_team_id: Mapped[Optional[int]] = mapped_column(ForeignKey("teams.id"), nullable=True)
|
||||
p1_team_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("teams.id"), nullable=True
|
||||
)
|
||||
p2_team_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("teams.id"), nullable=True
|
||||
)
|
||||
|
||||
ref_team_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("teams.id"), nullable=True
|
||||
)
|
||||
ref_label: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
||||
|
||||
sets: Mapped[list[dict]] = mapped_column(JSON, default=list)
|
||||
winner_team_id: Mapped[Optional[int]] = mapped_column(ForeignKey("teams.id"), nullable=True)
|
||||
|
||||
winner_next_match_id: Mapped[Optional[str]] = mapped_column(ForeignKey("matches.id"), nullable=True)
|
||||
winner_next_match_slot: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
winner_team_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("teams.id"), nullable=True
|
||||
)
|
||||
|
||||
loser_next_match_id: Mapped[Optional[str]] = mapped_column(ForeignKey("matches.id"), nullable=True)
|
||||
winner_next_match_id: Mapped[Optional[str]] = mapped_column(
|
||||
ForeignKey("matches.id"), nullable=True
|
||||
)
|
||||
winner_next_match_slot: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True
|
||||
)
|
||||
|
||||
loser_next_match_id: Mapped[Optional[str]] = mapped_column(
|
||||
ForeignKey("matches.id"), nullable=True
|
||||
)
|
||||
loser_next_match_slot: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
|
||||
tournament: Mapped["Tournament"] = relationship(back_populates="matches")
|
||||
court: Mapped[Optional["Court"]] = relationship()
|
||||
p1_team: Mapped["Team"] = relationship("Team", foreign_keys=[p1_team_id])
|
||||
p2_team: Mapped["Team"] = relationship("Team", foreign_keys=[p2_team_id])
|
||||
|
||||
ref_team: Mapped[Optional["Team"]] = relationship("Team", foreign_keys=[ref_team_id])
|
||||
|
||||
winner_next_match: Mapped["Match"] = relationship("Match", remote_side=[id], foreign_keys=[winner_next_match_id])
|
||||
loser_next_match: Mapped["Match"] = relationship("Match", remote_side=[id], foreign_keys=[loser_next_match_id])
|
||||
ref_team: Mapped[Optional["Team"]] = relationship(
|
||||
"Team", foreign_keys=[ref_team_id]
|
||||
)
|
||||
|
||||
winner_next_match: Mapped["Match"] = relationship(
|
||||
"Match", remote_side=[id], foreign_keys=[winner_next_match_id]
|
||||
)
|
||||
loser_next_match: Mapped["Match"] = relationship(
|
||||
"Match", remote_side=[id], foreign_keys=[loser_next_match_id]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# backend/app/routes/courts.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, date
|
||||
|
||||
from .. import crud, schemas, models
|
||||
from ..constants import SUCCESS
|
||||
from ..core.auth import get_admin_user
|
||||
from ..database import get_db
|
||||
|
||||
router = APIRouter(prefix="/courts", tags=["Courts"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[schemas.CourtSchema])
|
||||
def get_all_courts(db: Session = Depends(get_db)):
|
||||
"""Public route to list all global courts"""
|
||||
return db.query(models.Court).all()
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.CourtSchema)
|
||||
def create_global_court(
|
||||
data: schemas.CourtCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(get_admin_user),
|
||||
):
|
||||
"""Admin route to register a new physical court"""
|
||||
return crud.create_court(db, data)
|
||||
|
||||
|
||||
@router.delete("/{court_id}")
|
||||
def delete_global_court(
|
||||
court_id: int, db: Session = Depends(get_db), user: dict = Depends(get_admin_user)
|
||||
):
|
||||
success = crud.delete_court(db, court_id)
|
||||
if not success:
|
||||
raise HTTPException(404, "Court not found")
|
||||
return SUCCESS
|
||||
|
||||
|
||||
@router.get("/{court_id}/schedule")
|
||||
def get_court_schedule(court_id: int, db: Session = Depends(get_db)):
|
||||
court = db.query(models.Court).filter(models.Court.id == court_id).first()
|
||||
if not court:
|
||||
raise HTTPException(404, "Court not found")
|
||||
|
||||
today = date.today()
|
||||
|
||||
matches = (
|
||||
db.query(models.Match)
|
||||
.join(models.Tournament)
|
||||
.filter(models.Match.court_id == court_id)
|
||||
.filter(
|
||||
models.Tournament.timestamp >= datetime.combine(today, datetime.min.time())
|
||||
)
|
||||
.filter(
|
||||
models.Tournament.timestamp < datetime.combine(today, datetime.max.time())
|
||||
)
|
||||
.order_by(models.Match.start_time)
|
||||
.all()
|
||||
)
|
||||
|
||||
schedule = []
|
||||
for m in matches:
|
||||
schedule.append(
|
||||
{
|
||||
"id": m.id,
|
||||
"tournament_id": m.tournament_id,
|
||||
"tournament_name": m.tournament.name,
|
||||
"time": m.start_time.strftime("%H:%M") if m.start_time else "TBD",
|
||||
"status": m.status,
|
||||
"match_number": m.match_number,
|
||||
"p1": m.p1_team.name if m.p1_team else "TBD",
|
||||
"p2": m.p2_team.name if m.p2_team else "TBD",
|
||||
"p1_sets": sum(1 for s in m.sets if s["p1"] > s["p2"]) if m.sets else 0,
|
||||
"p2_sets": sum(1 for s in m.sets if s["p2"] > s["p1"]) if m.sets else 0,
|
||||
"ref_name": m.ref_team.name if m.ref_team else m.ref_label,
|
||||
}
|
||||
)
|
||||
|
||||
return {"court": court.name, "matches": schedule}
|
||||
@@ -3,4 +3,4 @@ from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix="/tournaments", tags=["Tournaments"])
|
||||
|
||||
from . import tournaments, settings, teams, courts, matches, report
|
||||
from . import tournaments, settings, teams, matches, report
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# backend/app/routes/tournaments/courts.py
|
||||
from fastapi import Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ... import crud, schemas
|
||||
from ...constants import SUCCESS
|
||||
from ...core.auth import get_admin_user
|
||||
from ...core.websocket_manager import send_ws_update
|
||||
from ...database import get_db
|
||||
from . import router
|
||||
|
||||
|
||||
@router.get("/{id}/courts", response_model=list[schemas.CourtSchema])
|
||||
def get_courts(id: str, db: Session = Depends(get_db)):
|
||||
return crud.get_courts(db, id)
|
||||
|
||||
|
||||
@router.post("/{id}/courts", response_model=schemas.CourtSchema)
|
||||
async def create_court(
|
||||
id: str,
|
||||
court: schemas.CourtCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(get_admin_user),
|
||||
):
|
||||
new_court = crud.create_court(db, id, court)
|
||||
if not new_court:
|
||||
raise HTTPException(404, "Tournament not found")
|
||||
|
||||
await send_ws_update(id)
|
||||
return new_court
|
||||
|
||||
|
||||
@router.patch("/{id}/courts", response_model=schemas.TournamentDetail)
|
||||
async def update_courts(
|
||||
id: str,
|
||||
courts: list[str],
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(get_admin_user),
|
||||
):
|
||||
t = crud.update_tournament_courts(db, id, courts)
|
||||
if not t:
|
||||
raise HTTPException(404, "Not found")
|
||||
|
||||
await send_ws_update(id)
|
||||
return t
|
||||
|
||||
|
||||
@router.delete("/{id}/courts/{court_id}")
|
||||
async def delete_court(
|
||||
id: str,
|
||||
court_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(get_admin_user),
|
||||
):
|
||||
success = crud.delete_court(db, id, court_id)
|
||||
if not success:
|
||||
raise HTTPException(404, "Court or Tournament not found")
|
||||
|
||||
await send_ws_update(id)
|
||||
return SUCCESS
|
||||
@@ -60,3 +60,17 @@ async def delete_tournament(
|
||||
await send_ws_update(id)
|
||||
|
||||
return SUCCESS
|
||||
|
||||
|
||||
@router.patch("/{id}/courts", response_model=schemas.TournamentOut)
|
||||
def update_tournament_courts(
|
||||
id: str,
|
||||
court_ids: list[int],
|
||||
db: Session = Depends(get_db),
|
||||
user: dict = Depends(get_admin_user),
|
||||
):
|
||||
"""Updates the global courts assigned to a specific tournament"""
|
||||
t = crud.update_tournament_courts(db, id, court_ids)
|
||||
if not t:
|
||||
raise HTTPException(404, "Tournament not found")
|
||||
return t
|
||||
|
||||
@@ -49,7 +49,7 @@ class MatchOut(BaseModel):
|
||||
p1_team_id: int | None = None
|
||||
p2_team_id: int | None = None
|
||||
winner_team_id: int | None = None
|
||||
|
||||
|
||||
ref_team_id: int | None = None
|
||||
ref_label: str | None = None
|
||||
ref_team: TeamSchema | None = None
|
||||
@@ -71,7 +71,7 @@ class TournamentCreate(BaseModel):
|
||||
timestamp: datetime
|
||||
duration: int
|
||||
teams: list[str]
|
||||
courts: list[str]
|
||||
courts: list[int]
|
||||
|
||||
|
||||
class TournamentUpdate(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user