mirror of
https://github.com/evroon/bracket.git
synced 2026-06-11 10:15:19 -04:00
Implement custom time per match (#337)
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
"""create custom match duration fields
|
||||
|
||||
Revision ID: 8bae62f80db7
|
||||
Revises: d104afae31e9
|
||||
Create Date: 2023-11-19 15:05:51.284093
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str | None = '8bae62f80db7'
|
||||
down_revision: str | None = 'd104afae31e9'
|
||||
branch_labels: str | None = None
|
||||
depends_on: str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('matches', sa.Column('margin_minutes', sa.Integer(), nullable=True))
|
||||
op.add_column('matches', sa.Column('custom_duration_minutes', sa.Integer(), nullable=True))
|
||||
op.add_column('matches', sa.Column('custom_margin_minutes', sa.Integer(), nullable=True))
|
||||
op.add_column(
|
||||
'tournaments', sa.Column('margin_minutes', sa.Integer(), server_default='5', nullable=False)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('tournaments', 'margin_minutes')
|
||||
op.drop_column('matches', 'custom_margin_minutes')
|
||||
op.drop_column('matches', 'custom_duration_minutes')
|
||||
op.drop_column('matches', 'margin_minutes')
|
||||
@@ -15,7 +15,10 @@ from bracket.models.db.stage_item_inputs import StageItemInputGeneric
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.util import StageWithStageItems
|
||||
from bracket.sql.courts import get_all_courts_in_tournament
|
||||
from bracket.sql.matches import sql_create_match, sql_reschedule_match
|
||||
from bracket.sql.matches import (
|
||||
sql_create_match,
|
||||
sql_reschedule_match_and_determine_duration_and_margin,
|
||||
)
|
||||
from bracket.sql.stages import get_full_tournament_details
|
||||
from bracket.sql.tournaments import sql_get_tournament
|
||||
from bracket.utils.types import assert_some
|
||||
@@ -45,11 +48,16 @@ async def schedule_all_unscheduled_matches(tournament_id: int) -> None:
|
||||
for round_ in stage_item.rounds:
|
||||
for match in round_.matches:
|
||||
if match.start_time is None and match.position_in_schedule is None:
|
||||
await sql_reschedule_match(
|
||||
assert_some(match.id), court.id, start_time, position_in_schedule
|
||||
await sql_reschedule_match_and_determine_duration_and_margin(
|
||||
assert_some(match.id),
|
||||
court.id,
|
||||
start_time,
|
||||
position_in_schedule,
|
||||
match,
|
||||
tournament,
|
||||
)
|
||||
|
||||
start_time += timedelta(minutes=15)
|
||||
start_time += timedelta(minutes=match.duration_minutes)
|
||||
position_in_schedule += 1
|
||||
|
||||
for stage in stages[1:]:
|
||||
@@ -58,12 +66,17 @@ async def schedule_all_unscheduled_matches(tournament_id: int) -> None:
|
||||
for stage_item in stage.stage_items:
|
||||
for round_ in stage_item.rounds:
|
||||
for match in round_.matches:
|
||||
start_time += timedelta(minutes=15)
|
||||
start_time += timedelta(minutes=match.duration_minutes)
|
||||
position_in_schedule += 1
|
||||
|
||||
if match.start_time is None and match.position_in_schedule is None:
|
||||
await sql_reschedule_match(
|
||||
assert_some(match.id), courts[-1].id, start_time, position_in_schedule
|
||||
await sql_reschedule_match_and_determine_duration_and_margin(
|
||||
assert_some(match.id),
|
||||
courts[-1].id,
|
||||
start_time,
|
||||
position_in_schedule,
|
||||
match,
|
||||
tournament,
|
||||
)
|
||||
|
||||
|
||||
@@ -131,13 +144,14 @@ async def iterative_scheduling(
|
||||
winner_from_match_id=match.team2_winner_from_match_id,
|
||||
)
|
||||
team_defs = {match.team1_id, match.team2_id}
|
||||
|
||||
court_id = sorted(match_count_per_court.items(), key=lambda x: x[1])[0][0]
|
||||
|
||||
try:
|
||||
position_in_schedule = len(matches_per_court[court_id])
|
||||
last_match = matches_per_court[court_id][-1]
|
||||
start_time = assert_some(last_match.start_time) + timedelta(minutes=15)
|
||||
start_time = assert_some(last_match.start_time) + timedelta(
|
||||
minutes=match.duration_minutes
|
||||
)
|
||||
except IndexError:
|
||||
start_time = tournament.start_time
|
||||
position_in_schedule = 0
|
||||
@@ -162,8 +176,8 @@ async def iterative_scheduling(
|
||||
attempts_since_last_write = 0
|
||||
random.shuffle(matches_to_schedule)
|
||||
|
||||
await sql_reschedule_match(
|
||||
assert_some(match.id), court_id, start_time, position_in_schedule
|
||||
await sql_reschedule_match_and_determine_duration_and_margin(
|
||||
assert_some(match.id), court_id, start_time, position_in_schedule, match, tournament
|
||||
)
|
||||
|
||||
|
||||
@@ -184,13 +198,17 @@ async def reorder_matches_for_court(
|
||||
|
||||
last_start_time = tournament.start_time
|
||||
for i, match_pos in enumerate(matches_this_court):
|
||||
await sql_reschedule_match(
|
||||
await sql_reschedule_match_and_determine_duration_and_margin(
|
||||
assert_some(match_pos.match.id),
|
||||
court_id,
|
||||
last_start_time,
|
||||
position_in_schedule=i,
|
||||
match=match_pos.match,
|
||||
tournament=tournament,
|
||||
)
|
||||
last_start_time = last_start_time + timedelta(
|
||||
minutes=match_pos.match.duration_minutes + match_pos.match.margin_minutes
|
||||
)
|
||||
last_start_time = last_start_time + timedelta(minutes=15)
|
||||
|
||||
|
||||
async def handle_match_reschedule(
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
from heliclockter import timedelta
|
||||
from heliclockter import datetime_utc, timedelta
|
||||
|
||||
from bracket.logic.planning.matches import get_scheduled_matches_per_court
|
||||
from bracket.models.db.util import RoundWithMatches, StageItemWithRounds
|
||||
from bracket.sql.courts import get_all_courts_in_tournament
|
||||
from bracket.sql.matches import sql_reschedule_match
|
||||
from bracket.sql.matches import (
|
||||
sql_reschedule_match_and_determine_duration_and_margin,
|
||||
)
|
||||
from bracket.sql.stages import get_full_tournament_details
|
||||
from bracket.sql.tournaments import sql_get_tournament
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
|
||||
class MatchTimingAdjustmentInfeasible(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_active_and_next_rounds(
|
||||
stage_item: StageItemWithRounds,
|
||||
) -> tuple[RoundWithMatches | None, RoundWithMatches | None]:
|
||||
@@ -29,11 +36,13 @@ def get_active_and_next_rounds(
|
||||
|
||||
|
||||
async def schedule_all_matches_for_swiss_round(
|
||||
tournament_id: int, active_round: RoundWithMatches
|
||||
tournament_id: int, active_round: RoundWithMatches, adjust_to_time: datetime_utc | None
|
||||
) -> None:
|
||||
courts = await get_all_courts_in_tournament(tournament_id)
|
||||
stages = await get_full_tournament_details(tournament_id)
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
matches_per_court = get_scheduled_matches_per_court(stages)
|
||||
rescheduling_operations = []
|
||||
|
||||
if len(courts) < 1:
|
||||
return
|
||||
@@ -42,11 +51,60 @@ async def schedule_all_matches_for_swiss_round(
|
||||
|
||||
for i, match in enumerate(active_round.matches):
|
||||
court_id = assert_some(courts[i].id)
|
||||
last_match = matches_per_court[court_id][-1]
|
||||
|
||||
await sql_reschedule_match(
|
||||
assert_some(match.id),
|
||||
court_id,
|
||||
assert_some(last_match.match.start_time) + timedelta(minutes=15),
|
||||
assert_some(last_match.match.position_in_schedule) + 1,
|
||||
last_match = (
|
||||
next((m for m in matches_per_court[court_id][::-1] if m.match.id != match.id), None)
|
||||
if court_id in matches_per_court
|
||||
else None
|
||||
)
|
||||
|
||||
if last_match is not None:
|
||||
timing_difference_minutes = 0.0
|
||||
if adjust_to_time is not None:
|
||||
last_match_end = last_match.match.end_time
|
||||
timing_difference_minutes = (adjust_to_time - last_match_end).total_seconds() // 60
|
||||
|
||||
if (
|
||||
timing_difference_minutes < 0
|
||||
and -timing_difference_minutes > last_match.match.margin_minutes
|
||||
):
|
||||
raise MatchTimingAdjustmentInfeasible(
|
||||
"A match from the previous round is still happening"
|
||||
)
|
||||
|
||||
if timing_difference_minutes != 0:
|
||||
last_match_adjusted = last_match.match.copy(
|
||||
update={
|
||||
'custom_margin_minutes': last_match.match.margin_minutes
|
||||
+ timing_difference_minutes
|
||||
}
|
||||
)
|
||||
rescheduling_operations.append(
|
||||
sql_reschedule_match_and_determine_duration_and_margin(
|
||||
assert_some(last_match.match.id),
|
||||
court_id,
|
||||
assert_some(last_match.match.start_time),
|
||||
assert_some(last_match.match.position_in_schedule),
|
||||
last_match_adjusted,
|
||||
tournament,
|
||||
)
|
||||
)
|
||||
|
||||
start_time = assert_some(last_match.match.start_time) + timedelta(
|
||||
minutes=match.duration_minutes
|
||||
+ last_match.match.margin_minutes
|
||||
+ timing_difference_minutes
|
||||
)
|
||||
pos_in_schedule = assert_some(last_match.match.position_in_schedule) + 1
|
||||
else:
|
||||
start_time = tournament.start_time
|
||||
pos_in_schedule = 1
|
||||
|
||||
rescheduling_operations.append(
|
||||
sql_reschedule_match_and_determine_duration_and_margin(
|
||||
assert_some(match.id), court_id, start_time, pos_in_schedule, match, tournament
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: if safe: await asyncio.gather(*rescheduling_operations)
|
||||
for op in rescheduling_operations:
|
||||
await op
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from bracket.logic.planning.matches import create_match_and_assign_free_court
|
||||
from bracket.models.db.match import Match, MatchCreateBody
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.util import RoundWithMatches, StageItemWithRounds
|
||||
from bracket.sql.rounds import get_rounds_for_stage_item
|
||||
from bracket.sql.tournaments import sql_get_tournament
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
|
||||
def determine_matches_first_round(
|
||||
round_: RoundWithMatches, stage_item: StageItemWithRounds
|
||||
round_: RoundWithMatches, stage_item: StageItemWithRounds, tournament: Tournament
|
||||
) -> list[MatchCreateBody]:
|
||||
suggestions: list[MatchCreateBody] = []
|
||||
|
||||
@@ -25,6 +27,10 @@ def determine_matches_first_round(
|
||||
team2_winner_from_stage_item_id=second_input.winner_from_stage_item_id,
|
||||
team2_winner_position=second_input.winner_position,
|
||||
team2_winner_from_match_id=second_input.winner_from_match_id,
|
||||
duration_minutes=tournament.duration_minutes,
|
||||
margin_minutes=tournament.margin_minutes,
|
||||
custom_duration_minutes=None,
|
||||
custom_margin_minutes=None,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -34,6 +40,7 @@ def determine_matches_first_round(
|
||||
def determine_matches_subsequent_round(
|
||||
prev_matches: list[Match],
|
||||
round_: RoundWithMatches,
|
||||
tournament: Tournament,
|
||||
) -> list[MatchCreateBody]:
|
||||
suggestions: list[MatchCreateBody] = []
|
||||
|
||||
@@ -53,6 +60,10 @@ def determine_matches_subsequent_round(
|
||||
team2_winner_position=None,
|
||||
team1_winner_from_match_id=assert_some(first_match.id),
|
||||
team2_winner_from_match_id=assert_some(second_match.id),
|
||||
duration_minutes=tournament.duration_minutes,
|
||||
margin_minutes=tournament.margin_minutes,
|
||||
custom_duration_minutes=None,
|
||||
custom_margin_minutes=None,
|
||||
)
|
||||
)
|
||||
return suggestions
|
||||
@@ -62,18 +73,20 @@ async def build_single_elimination_stage_item(
|
||||
tournament_id: int, stage_item: StageItemWithRounds
|
||||
) -> None:
|
||||
rounds = await get_rounds_for_stage_item(tournament_id, stage_item.id)
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
|
||||
assert len(rounds) > 0
|
||||
first_round = rounds[0]
|
||||
|
||||
prev_matches = [
|
||||
await create_match_and_assign_free_court(tournament_id, match)
|
||||
for match in determine_matches_first_round(first_round, stage_item)
|
||||
for match in determine_matches_first_round(first_round, stage_item, tournament)
|
||||
]
|
||||
|
||||
for round_ in rounds[1:]:
|
||||
prev_matches = [
|
||||
await create_match_and_assign_free_court(tournament_id, match)
|
||||
for match in determine_matches_subsequent_round(prev_matches, round_)
|
||||
for match in determine_matches_subsequent_round(prev_matches, round_, tournament)
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ def get_possible_upcoming_matches_for_swiss(
|
||||
raise HTTPException(400, 'There is no draft round, so no matches can be scheduled.')
|
||||
|
||||
draft_round_team_ids = get_draft_round_team_ids(draft_round)
|
||||
teams_to_schedule = [team for team in teams if team.id not in draft_round_team_ids]
|
||||
teams_to_schedule = [
|
||||
team for team in teams if team.id not in draft_round_team_ids and team.active
|
||||
]
|
||||
|
||||
if len(teams_to_schedule) < 1:
|
||||
return suggestions
|
||||
|
||||
@@ -3,6 +3,7 @@ from bracket.models.db.match import (
|
||||
MatchCreateBody,
|
||||
)
|
||||
from bracket.models.db.util import StageItemWithRounds
|
||||
from bracket.sql.tournaments import sql_get_tournament
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
|
||||
@@ -36,6 +37,7 @@ def get_round_robin_combinations(team_count: int) -> list[list[tuple[int, int]]]
|
||||
|
||||
async def build_round_robin_stage_item(tournament_id: int, stage_item: StageItemWithRounds) -> None:
|
||||
matches = get_round_robin_combinations(len(stage_item.inputs))
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
|
||||
for i, round_ in enumerate(stage_item.rounds):
|
||||
for team_1_id, team_2_id in matches[i]:
|
||||
@@ -53,6 +55,10 @@ async def build_round_robin_stage_item(tournament_id: int, stage_item: StageItem
|
||||
team2_winner_position=team_2.winner_position,
|
||||
team2_winner_from_match_id=team_2.winner_from_match_id,
|
||||
court_id=None,
|
||||
duration_minutes=tournament.duration_minutes,
|
||||
margin_minutes=tournament.margin_minutes,
|
||||
custom_duration_minutes=None,
|
||||
custom_margin_minutes=None,
|
||||
)
|
||||
await create_match_and_assign_free_court(tournament_id, match)
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ class MatchBase(BaseModelORM):
|
||||
id: int | None = None
|
||||
created: datetime_utc
|
||||
start_time: datetime_utc | None
|
||||
duration_minutes: int | None
|
||||
duration_minutes: int
|
||||
margin_minutes: int
|
||||
custom_duration_minutes: int | None
|
||||
custom_margin_minutes: int | None
|
||||
position_in_schedule: int | None
|
||||
round_id: int
|
||||
team1_score: int
|
||||
@@ -21,11 +24,10 @@ class MatchBase(BaseModelORM):
|
||||
court_id: int | None
|
||||
|
||||
@property
|
||||
def end_time(self, default_minutes: int = 15) -> datetime_utc:
|
||||
def end_time(self) -> datetime_utc:
|
||||
assert self.start_time
|
||||
return datetime_utc.from_datetime(
|
||||
self.start_time
|
||||
+ timedelta(minutes=self.duration_minutes if self.duration_minutes else default_minutes)
|
||||
self.start_time + timedelta(minutes=self.duration_minutes + self.margin_minutes)
|
||||
)
|
||||
|
||||
|
||||
@@ -83,9 +85,11 @@ class MatchBody(BaseModelORM):
|
||||
team1_score: int = 0
|
||||
team2_score: int = 0
|
||||
court_id: int | None
|
||||
custom_duration_minutes: int | None
|
||||
custom_margin_minutes: int | None
|
||||
|
||||
|
||||
class MatchCreateBody(BaseModelORM):
|
||||
class MatchCreateBodyFrontend(BaseModelORM):
|
||||
round_id: int
|
||||
court_id: int | None
|
||||
team1_id: int | None
|
||||
@@ -98,6 +102,13 @@ class MatchCreateBody(BaseModelORM):
|
||||
team2_winner_from_match_id: int | None
|
||||
|
||||
|
||||
class MatchCreateBody(MatchCreateBodyFrontend):
|
||||
duration_minutes: int
|
||||
margin_minutes: int
|
||||
custom_duration_minutes: int | None
|
||||
custom_margin_minutes: int | None
|
||||
|
||||
|
||||
class MatchRescheduleBody(BaseModelORM):
|
||||
old_court_id: int
|
||||
old_position: int
|
||||
|
||||
@@ -36,6 +36,10 @@ class StageItemUpdateBody(BaseModelORM):
|
||||
name: str
|
||||
|
||||
|
||||
class StageItemActivateNextBody(BaseModelORM):
|
||||
adjust_to_time: datetime_utc | None
|
||||
|
||||
|
||||
class StageItemCreateBody(BaseModelORM):
|
||||
stage_id: int
|
||||
name: str | None
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from heliclockter import datetime_utc
|
||||
from pydantic import Field
|
||||
|
||||
from bracket.models.db.shared import BaseModelORM
|
||||
|
||||
@@ -9,6 +10,8 @@ class Tournament(BaseModelORM):
|
||||
name: str
|
||||
created: datetime_utc
|
||||
start_time: datetime_utc
|
||||
duration_minutes: int = Field(..., ge=1)
|
||||
margin_minutes: int = Field(..., ge=0)
|
||||
dashboard_public: bool
|
||||
dashboard_endpoint: str | None
|
||||
logo_path: str | None
|
||||
@@ -23,6 +26,8 @@ class TournamentUpdateBody(BaseModelORM):
|
||||
dashboard_endpoint: str | None
|
||||
players_can_be_in_multiple_teams: bool
|
||||
auto_assign_courts: bool
|
||||
duration_minutes: int = Field(..., ge=1)
|
||||
margin_minutes: int = Field(..., ge=0)
|
||||
|
||||
|
||||
class TournamentBody(TournamentUpdateBody):
|
||||
|
||||
@@ -13,6 +13,7 @@ from bracket.models.db.match import (
|
||||
Match,
|
||||
MatchBody,
|
||||
MatchCreateBody,
|
||||
MatchCreateBodyFrontend,
|
||||
MatchFilter,
|
||||
MatchRescheduleBody,
|
||||
SuggestedMatch,
|
||||
@@ -24,7 +25,8 @@ from bracket.routes.auth import user_authenticated_for_tournament
|
||||
from bracket.routes.models import SingleMatchResponse, SuccessResponse, UpcomingMatchesResponse
|
||||
from bracket.routes.util import match_dependency, round_dependency, round_with_matches_dependency
|
||||
from bracket.sql.courts import get_all_courts_in_tournament
|
||||
from bracket.sql.matches import sql_delete_match, sql_update_match
|
||||
from bracket.sql.matches import sql_create_match, sql_delete_match, sql_update_match
|
||||
from bracket.sql.tournaments import sql_get_tournament
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
router = APIRouter()
|
||||
@@ -72,13 +74,18 @@ async def delete_match(
|
||||
@router.post("/tournaments/{tournament_id}/matches", response_model=SingleMatchResponse)
|
||||
async def create_match(
|
||||
tournament_id: int,
|
||||
match_body: MatchCreateBody,
|
||||
match_body: MatchCreateBodyFrontend,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SingleMatchResponse:
|
||||
return SingleMatchResponse(
|
||||
data=await create_match_and_assign_free_court(tournament_id, match_body)
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
body_with_durations = MatchCreateBody(
|
||||
**match_body.dict(),
|
||||
duration_minutes=tournament.duration_minutes,
|
||||
margin_minutes=tournament.margin_minutes,
|
||||
)
|
||||
|
||||
return SingleMatchResponse(data=await sql_create_match(body_with_durations))
|
||||
|
||||
|
||||
@router.post("/tournaments/{tournament_id}/schedule_matches", response_model=SuccessResponse)
|
||||
async def schedule_matches(
|
||||
@@ -123,7 +130,9 @@ async def create_matches_automatically(
|
||||
limit=1,
|
||||
iterations=iterations,
|
||||
)
|
||||
|
||||
courts = await get_all_courts_in_tournament(tournament_id)
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
|
||||
limit = len(courts) - len(round_.matches)
|
||||
for __ in range(limit):
|
||||
@@ -150,6 +159,10 @@ async def create_matches_automatically(
|
||||
team2_winner_from_stage_item_id=None,
|
||||
team2_winner_position=None,
|
||||
team2_winner_from_match_id=None,
|
||||
duration_minutes=tournament.duration_minutes,
|
||||
margin_minutes=tournament.margin_minutes,
|
||||
custom_duration_minutes=None,
|
||||
custom_margin_minutes=None,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -164,6 +177,8 @@ async def update_match_by_id(
|
||||
match: Match = Depends(match_dependency),
|
||||
) -> SuccessResponse:
|
||||
assert match.id
|
||||
await sql_update_match(match.id, match_body)
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
|
||||
await sql_update_match(match.id, match_body, tournament)
|
||||
await recalculate_ranking_for_tournament_id(tournament_id)
|
||||
return SuccessResponse()
|
||||
|
||||
@@ -3,6 +3,7 @@ from starlette import status
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.planning.rounds import (
|
||||
MatchTimingAdjustmentInfeasible,
|
||||
get_active_and_next_rounds,
|
||||
schedule_all_matches_for_swiss_round,
|
||||
)
|
||||
@@ -10,7 +11,11 @@ from bracket.logic.ranking.elo import recalculate_ranking_for_tournament_id
|
||||
from bracket.logic.scheduling.builder import (
|
||||
build_matches_for_stage_item,
|
||||
)
|
||||
from bracket.models.db.stage_item import StageItemCreateBody, StageItemUpdateBody
|
||||
from bracket.models.db.stage_item import (
|
||||
StageItemActivateNextBody,
|
||||
StageItemCreateBody,
|
||||
StageItemUpdateBody,
|
||||
)
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.models.db.util import StageItemWithRounds
|
||||
from bracket.routes.auth import (
|
||||
@@ -87,6 +92,7 @@ async def update_stage_item(
|
||||
async def start_next_round(
|
||||
tournament_id: int,
|
||||
stage_item_id: int,
|
||||
active_next_body: StageItemActivateNextBody,
|
||||
stage_item: StageItemWithRounds = Depends(stage_item_dependency),
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SuccessResponse:
|
||||
@@ -100,6 +106,16 @@ async def start_next_round(
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
await schedule_all_matches_for_swiss_round(
|
||||
tournament_id, next_round, active_next_body.adjust_to_time
|
||||
)
|
||||
except MatchTimingAdjustmentInfeasible as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
if active_round is not None and active_round.id is not None:
|
||||
await set_round_active_or_draft(
|
||||
active_round.id, tournament_id, is_active=False, is_draft=False
|
||||
@@ -108,5 +124,4 @@ async def start_next_round(
|
||||
assert next_round.id is not None
|
||||
await set_round_active_or_draft(next_round.id, tournament_id, is_active=True, is_draft=False)
|
||||
|
||||
await schedule_all_matches_for_swiss_round(tournament_id, next_round)
|
||||
return SuccessResponse()
|
||||
|
||||
@@ -28,6 +28,7 @@ tournaments = Table(
|
||||
Column('players_can_be_in_multiple_teams', Boolean, nullable=False, server_default='f'),
|
||||
Column('auto_assign_courts', Boolean, nullable=False, server_default='f'),
|
||||
Column('duration_minutes', Integer, nullable=False, server_default='15'),
|
||||
Column('margin_minutes', Integer, nullable=False, server_default='5'),
|
||||
)
|
||||
|
||||
stages = Table(
|
||||
@@ -97,6 +98,9 @@ matches = Table(
|
||||
Column('created', DateTimeTZ, nullable=False),
|
||||
Column('start_time', DateTimeTZ, nullable=True),
|
||||
Column('duration_minutes', Integer, nullable=True),
|
||||
Column('margin_minutes', Integer, nullable=True),
|
||||
Column('custom_duration_minutes', Integer, nullable=True),
|
||||
Column('custom_margin_minutes', Integer, nullable=True),
|
||||
Column('round_id', BigInteger, ForeignKey('rounds.id'), nullable=False),
|
||||
Column('team1_id', BigInteger, ForeignKey('teams.id'), nullable=True),
|
||||
Column('team2_id', BigInteger, ForeignKey('teams.id'), nullable=True),
|
||||
|
||||
@@ -4,6 +4,7 @@ from heliclockter import datetime_utc
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.match import Match, MatchBody, MatchCreateBody
|
||||
from bracket.models.db.tournament import Tournament
|
||||
|
||||
|
||||
async def sql_delete_match(match_id: int) -> None:
|
||||
@@ -40,6 +41,10 @@ async def sql_create_match(match: MatchCreateBody) -> Match:
|
||||
team2_winner_position,
|
||||
team1_winner_from_match_id,
|
||||
team2_winner_from_match_id,
|
||||
duration_minutes,
|
||||
custom_duration_minutes,
|
||||
margin_minutes,
|
||||
custom_margin_minutes,
|
||||
team1_score,
|
||||
team2_score,
|
||||
created
|
||||
@@ -55,6 +60,10 @@ async def sql_create_match(match: MatchCreateBody) -> Match:
|
||||
:team2_winner_position,
|
||||
:team1_winner_from_match_id,
|
||||
:team2_winner_from_match_id,
|
||||
:duration_minutes,
|
||||
:custom_duration_minutes,
|
||||
:margin_minutes,
|
||||
:custom_margin_minutes,
|
||||
0,
|
||||
0,
|
||||
NOW()
|
||||
@@ -69,17 +78,40 @@ async def sql_create_match(match: MatchCreateBody) -> Match:
|
||||
return Match.parse_obj(result._mapping)
|
||||
|
||||
|
||||
async def sql_update_match(match_id: int, match: MatchBody) -> None:
|
||||
async def sql_update_match(match_id: int, match: MatchBody, tournament: Tournament) -> None:
|
||||
query = '''
|
||||
UPDATE matches
|
||||
SET round_id = :round_id,
|
||||
team1_score = :team1_score,
|
||||
team2_score = :team2_score,
|
||||
court_id = :court_id
|
||||
court_id = :court_id,
|
||||
custom_duration_minutes = :custom_duration_minutes,
|
||||
custom_margin_minutes = :custom_margin_minutes,
|
||||
duration_minutes = :duration_minutes,
|
||||
margin_minutes = :margin_minutes
|
||||
WHERE matches.id = :match_id
|
||||
RETURNING *
|
||||
'''
|
||||
await database.execute(query=query, values={'match_id': match_id, **match.dict()})
|
||||
|
||||
duration_minutes = (
|
||||
match.custom_duration_minutes
|
||||
if match.custom_duration_minutes is not None
|
||||
else tournament.duration_minutes
|
||||
)
|
||||
margin_minutes = (
|
||||
match.custom_margin_minutes
|
||||
if match.custom_margin_minutes is not None
|
||||
else tournament.margin_minutes
|
||||
)
|
||||
await database.execute(
|
||||
query=query,
|
||||
values={
|
||||
'match_id': match_id,
|
||||
**match.dict(),
|
||||
'duration_minutes': duration_minutes,
|
||||
'margin_minutes': margin_minutes,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def sql_update_team_ids_for_match(
|
||||
@@ -97,13 +129,24 @@ async def sql_update_team_ids_for_match(
|
||||
|
||||
|
||||
async def sql_reschedule_match(
|
||||
match_id: int, court_id: int | None, start_time: datetime_utc, position_in_schedule: int | None
|
||||
match_id: int,
|
||||
court_id: int | None,
|
||||
start_time: datetime_utc,
|
||||
position_in_schedule: int | None,
|
||||
duration_minutes: int,
|
||||
margin_minutes: int,
|
||||
custom_duration_minutes: int | None,
|
||||
custom_margin_minutes: int | None,
|
||||
) -> None:
|
||||
query = '''
|
||||
UPDATE matches
|
||||
SET court_id = :court_id,
|
||||
start_time = :start_time,
|
||||
position_in_schedule = :position_in_schedule
|
||||
position_in_schedule = :position_in_schedule,
|
||||
duration_minutes = :duration_minutes,
|
||||
margin_minutes = :margin_minutes,
|
||||
custom_duration_minutes = :custom_duration_minutes,
|
||||
custom_margin_minutes = :custom_margin_minutes
|
||||
WHERE matches.id = :match_id
|
||||
'''
|
||||
await database.execute(
|
||||
@@ -113,10 +156,44 @@ async def sql_reschedule_match(
|
||||
'match_id': match_id,
|
||||
'position_in_schedule': position_in_schedule,
|
||||
'start_time': datetime.fromisoformat(start_time.isoformat()),
|
||||
'duration_minutes': duration_minutes,
|
||||
'margin_minutes': margin_minutes,
|
||||
'custom_duration_minutes': custom_duration_minutes,
|
||||
'custom_margin_minutes': custom_margin_minutes,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def sql_reschedule_match_and_determine_duration_and_margin(
|
||||
match_id: int,
|
||||
court_id: int | None,
|
||||
start_time: datetime_utc,
|
||||
position_in_schedule: int | None,
|
||||
match: Match,
|
||||
tournament: Tournament,
|
||||
) -> None:
|
||||
duration_minutes = (
|
||||
tournament.duration_minutes
|
||||
if match.custom_duration_minutes is None
|
||||
else match.custom_duration_minutes
|
||||
)
|
||||
margin_minutes = (
|
||||
tournament.margin_minutes
|
||||
if match.custom_margin_minutes is None
|
||||
else match.custom_margin_minutes
|
||||
)
|
||||
await sql_reschedule_match(
|
||||
match_id,
|
||||
court_id,
|
||||
start_time,
|
||||
position_in_schedule,
|
||||
duration_minutes,
|
||||
margin_minutes,
|
||||
match.custom_duration_minutes,
|
||||
match.custom_margin_minutes,
|
||||
)
|
||||
|
||||
|
||||
async def sql_get_match(match_id: int) -> Match:
|
||||
query = '''
|
||||
SELECT *
|
||||
|
||||
@@ -179,7 +179,7 @@ async def sql_create_dev_db() -> None:
|
||||
player_id_8 = await insert_dummy(DUMMY_PLAYER8, {'tournament_id': tournament_id_1})
|
||||
|
||||
player_id_9 = await insert_dummy(
|
||||
DUMMY_PLAYER8, {'name': 'Player 9', 'tournament_id': tournament_id_1}
|
||||
DUMMY_PLAYER8, {'name': 'Player 09', 'tournament_id': tournament_id_1}
|
||||
)
|
||||
player_id_10 = await insert_dummy(
|
||||
DUMMY_PLAYER8, {'name': 'Player 10', 'tournament_id': tournament_id_1}
|
||||
|
||||
@@ -36,6 +36,8 @@ DUMMY_TOURNAMENT = Tournament(
|
||||
logo_path=None,
|
||||
players_can_be_in_multiple_teams=True,
|
||||
auto_assign_courts=True,
|
||||
duration_minutes=10,
|
||||
margin_minutes=5,
|
||||
)
|
||||
|
||||
DUMMY_STAGE1 = Stage(
|
||||
@@ -113,6 +115,9 @@ DUMMY_MATCH1 = Match(
|
||||
team2_winner_position=None,
|
||||
team2_winner_from_match_id=None,
|
||||
duration_minutes=10,
|
||||
margin_minutes=5,
|
||||
custom_duration_minutes=None,
|
||||
custom_margin_minutes=None,
|
||||
position_in_schedule=1,
|
||||
)
|
||||
|
||||
@@ -153,56 +158,56 @@ DUMMY_TEAM4 = Team(
|
||||
|
||||
|
||||
DUMMY_PLAYER1 = Player(
|
||||
name='Player 1',
|
||||
name='Player 01',
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_PLAYER2 = Player(
|
||||
name='Player 2',
|
||||
name='Player 02',
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_PLAYER3 = Player(
|
||||
name='Player 3',
|
||||
name='Player 03',
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_PLAYER4 = Player(
|
||||
name='Player 4',
|
||||
name='Player 04',
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_PLAYER5 = Player(
|
||||
name='Player 5',
|
||||
name='Player 05',
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_PLAYER6 = Player(
|
||||
name='Player 6',
|
||||
name='Player 06',
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_PLAYER7 = Player(
|
||||
name='Player 7',
|
||||
name='Player 07',
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_PLAYER8 = Player(
|
||||
name='Player 8',
|
||||
name='Player 08',
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
|
||||
@@ -111,7 +111,9 @@ async def test_activate_next_stage(
|
||||
assert isinstance(match1, MatchWithDetailsDefinitive)
|
||||
assert match1.team2.id == team_inserted_2.id
|
||||
await sql_update_match(
|
||||
assert_some(match1.id), MatchBody(**match1.copy(update={'team2_score': 42}).dict())
|
||||
assert_some(match1.id),
|
||||
MatchBody(**match1.copy(update={'team2_score': 42}).dict()),
|
||||
auth_context.tournament,
|
||||
)
|
||||
|
||||
response = await send_tournament_request(
|
||||
|
||||
@@ -239,7 +239,7 @@ async def test_upcoming_matches_endpoint(
|
||||
{
|
||||
'id': player_inserted_1.id,
|
||||
'active': True,
|
||||
'name': 'Player 1',
|
||||
'name': 'Player 01',
|
||||
'created': '2022-01-11T04:32:11+00:00',
|
||||
'tournament_id': auth_context.tournament.id,
|
||||
'elo_score': 1100,
|
||||
@@ -251,7 +251,7 @@ async def test_upcoming_matches_endpoint(
|
||||
{
|
||||
'id': player_inserted_3.id,
|
||||
'active': True,
|
||||
'name': 'Player 3',
|
||||
'name': 'Player 03',
|
||||
'created': '2022-01-11T04:32:11+00:00',
|
||||
'tournament_id': auth_context.tournament.id,
|
||||
'elo_score': 1200,
|
||||
@@ -274,7 +274,7 @@ async def test_upcoming_matches_endpoint(
|
||||
{
|
||||
'id': player_inserted_2.id,
|
||||
'active': True,
|
||||
'name': 'Player 2',
|
||||
'name': 'Player 02',
|
||||
'created': '2022-01-11T04:32:11+00:00',
|
||||
'tournament_id': auth_context.tournament.id,
|
||||
'elo_score': 1300,
|
||||
@@ -286,7 +286,7 @@ async def test_upcoming_matches_endpoint(
|
||||
{
|
||||
'id': player_inserted_4.id,
|
||||
'active': True,
|
||||
'name': 'Player 4',
|
||||
'name': 'Player 04',
|
||||
'created': '2022-01-11T04:32:11+00:00',
|
||||
'tournament_id': auth_context.tournament.id,
|
||||
'elo_score': 1400,
|
||||
|
||||
@@ -29,7 +29,7 @@ async def test_players_endpoint(
|
||||
'wins': 0,
|
||||
'draws': 0,
|
||||
'losses': 0,
|
||||
'name': 'Player 1',
|
||||
'name': 'Player 01',
|
||||
'tournament_id': auth_context.tournament.id,
|
||||
}
|
||||
],
|
||||
|
||||
@@ -28,6 +28,8 @@ async def test_tournaments_endpoint(
|
||||
'dashboard_endpoint': 'cool-tournament',
|
||||
'players_can_be_in_multiple_teams': True,
|
||||
'auto_assign_courts': True,
|
||||
'duration_minutes': 10,
|
||||
'margin_minutes': 5,
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -49,6 +51,8 @@ async def test_tournament_endpoint(
|
||||
'dashboard_endpoint': 'cool-tournament',
|
||||
'players_can_be_in_multiple_teams': True,
|
||||
'auto_assign_courts': True,
|
||||
'duration_minutes': 10,
|
||||
'margin_minutes': 5,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -63,6 +67,8 @@ async def test_create_tournament(
|
||||
'dashboard_public': False,
|
||||
'players_can_be_in_multiple_teams': True,
|
||||
'auto_assign_courts': True,
|
||||
'duration_minutes': 12,
|
||||
'margin_minutes': 3,
|
||||
}
|
||||
assert (
|
||||
await send_auth_request(HTTPMethod.POST, 'tournaments', auth_context, json=body)
|
||||
@@ -80,6 +86,8 @@ async def test_update_tournament(
|
||||
'dashboard_public': False,
|
||||
'players_can_be_in_multiple_teams': True,
|
||||
'auto_assign_courts': True,
|
||||
'duration_minutes': 12,
|
||||
'margin_minutes': 3,
|
||||
}
|
||||
assert (
|
||||
await send_tournament_request(HTTPMethod.PUT, '', auth_context, json=body)
|
||||
|
||||
@@ -40,6 +40,9 @@ def test_elo_calculation() -> None:
|
||||
court_id=None,
|
||||
court=None,
|
||||
duration_minutes=10,
|
||||
margin_minutes=5,
|
||||
custom_duration_minutes=None,
|
||||
custom_margin_minutes=None,
|
||||
position_in_schedule=0,
|
||||
team1=FullTeamWithPlayers(
|
||||
id=3,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Alert, Button, Container, Grid, Group, Skeleton } from '@mantine/core';
|
||||
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
|
||||
import { IconAlertCircle, IconSquareArrowRight } from '@tabler/icons-react';
|
||||
import { IconAlertCircle } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
@@ -9,7 +9,8 @@ import { RoundInterface } from '../../interfaces/round';
|
||||
import { StageWithStageItems } from '../../interfaces/stage';
|
||||
import { StageItemWithRounds, stageItemIsHandledAutomatically } from '../../interfaces/stage_item';
|
||||
import { TournamentMinimal } from '../../interfaces/tournament';
|
||||
import { createRound, startNextRound } from '../../services/round';
|
||||
import { createRound } from '../../services/round';
|
||||
import ActivateNextRoundModal from '../modals/activate_next_round_modal';
|
||||
import { responseIsValid } from '../utils/util';
|
||||
import Round from './round';
|
||||
|
||||
@@ -17,7 +18,6 @@ function getRoundsGridCols(
|
||||
stageItem: StageItemWithRounds,
|
||||
tournamentData: TournamentMinimal,
|
||||
swrStagesResponse: SWRResponse,
|
||||
swrCourtsResponse: SWRResponse,
|
||||
swrUpcomingMatchesResponse: SWRResponse | null,
|
||||
readOnly: boolean,
|
||||
displaySettings: BracketDisplaySettings
|
||||
@@ -30,7 +30,6 @@ function getRoundsGridCols(
|
||||
tournamentData={tournamentData}
|
||||
round={round}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
|
||||
readOnly={readOnly}
|
||||
dynamicSchedule={!stageItemIsHandledAutomatically(stageItem)}
|
||||
@@ -46,8 +45,8 @@ function getRoundsGridCols(
|
||||
);
|
||||
}
|
||||
|
||||
const showAddRoundButton =
|
||||
tournamentData != null && (readOnly || stageItemIsHandledAutomatically(stageItem));
|
||||
const hideAddRoundButton =
|
||||
tournamentData == null || readOnly || stageItemIsHandledAutomatically(stageItem);
|
||||
|
||||
return (
|
||||
<React.Fragment key={stageItem.id}>
|
||||
@@ -58,7 +57,7 @@ function getRoundsGridCols(
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Group position="right">
|
||||
{showAddRoundButton ? null : (
|
||||
{hideAddRoundButton ? null : (
|
||||
<Button
|
||||
color="green"
|
||||
size="md"
|
||||
@@ -72,18 +71,12 @@ function getRoundsGridCols(
|
||||
Add Round
|
||||
</Button>
|
||||
)}
|
||||
{showAddRoundButton ? null : (
|
||||
<Button
|
||||
color="indigo"
|
||||
size="md"
|
||||
leftIcon={<IconSquareArrowRight size={24} />}
|
||||
onClick={async () => {
|
||||
await startNextRound(tournamentData.id, stageItem.id);
|
||||
await swrStagesResponse.mutate();
|
||||
}}
|
||||
>
|
||||
Start next round
|
||||
</Button>
|
||||
{hideAddRoundButton ? null : (
|
||||
<ActivateNextRoundModal
|
||||
tournamentId={tournamentData.id}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
stageItem={stageItem}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
@@ -130,21 +123,20 @@ function NotStartedAlert() {
|
||||
|
||||
function LoadingSkeleton() {
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Col sm={6} lg={4} xl={3}>
|
||||
<Group>
|
||||
<div style={{ width: '400px', marginLeft: '1rem' }}>
|
||||
<Skeleton height={500} mb="xl" radius="xl" />
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={6} lg={4} xl={3}>
|
||||
</div>
|
||||
<div style={{ width: '400px', marginLeft: '1rem' }}>
|
||||
<Skeleton height={500} mb="xl" radius="xl" />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Brackets({
|
||||
tournamentData,
|
||||
swrStagesResponse,
|
||||
swrCourtsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
readOnly,
|
||||
selectedStageId,
|
||||
@@ -152,12 +144,14 @@ export default function Brackets({
|
||||
}: {
|
||||
tournamentData: TournamentMinimal;
|
||||
swrStagesResponse: SWRResponse;
|
||||
swrCourtsResponse: SWRResponse;
|
||||
swrUpcomingMatchesResponse: SWRResponse | null;
|
||||
readOnly: boolean;
|
||||
selectedStageId: string | null;
|
||||
displaySettings: BracketDisplaySettings;
|
||||
}) {
|
||||
if (swrStagesResponse.isLoading) {
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
if (selectedStageId == null) {
|
||||
return <NotStartedAlert />;
|
||||
}
|
||||
@@ -182,7 +176,6 @@ export default function Brackets({
|
||||
stageItem,
|
||||
tournamentData,
|
||||
swrStagesResponse,
|
||||
swrCourtsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
readOnly,
|
||||
displaySettings
|
||||
|
||||
@@ -20,7 +20,6 @@ function getRoundsGridCols(
|
||||
key={match.id}
|
||||
tournamentData={tournamentData}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
swrCourtsResponse={null}
|
||||
swrUpcomingMatchesResponse={null}
|
||||
match={match}
|
||||
readOnly
|
||||
|
||||
@@ -68,7 +68,6 @@ export function MatchBadge({ match, theme }: { match: MatchInterface; theme: any
|
||||
|
||||
export default function Match({
|
||||
swrStagesResponse,
|
||||
swrCourtsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
tournamentData,
|
||||
match,
|
||||
@@ -77,7 +76,6 @@ export default function Match({
|
||||
displaySettings,
|
||||
}: {
|
||||
swrStagesResponse: SWRResponse;
|
||||
swrCourtsResponse: SWRResponse | null;
|
||||
swrUpcomingMatchesResponse: SWRResponse | null;
|
||||
tournamentData: TournamentMinimal;
|
||||
match: MatchInterface;
|
||||
@@ -141,7 +139,6 @@ export default function Match({
|
||||
return <div className={classes.root}>{bracket}</div>;
|
||||
}
|
||||
assert(swrStagesResponse != null);
|
||||
assert(swrCourtsResponse != null);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -17,7 +17,6 @@ export default function Round({
|
||||
tournamentData,
|
||||
round,
|
||||
swrStagesResponse,
|
||||
swrCourtsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
readOnly,
|
||||
dynamicSchedule,
|
||||
@@ -26,7 +25,6 @@ export default function Round({
|
||||
tournamentData: TournamentMinimal;
|
||||
round: RoundInterface;
|
||||
swrStagesResponse: SWRResponse;
|
||||
swrCourtsResponse: SWRResponse;
|
||||
swrUpcomingMatchesResponse: SWRResponse | null;
|
||||
readOnly: boolean;
|
||||
dynamicSchedule: boolean;
|
||||
@@ -45,7 +43,6 @@ export default function Round({
|
||||
key={match.id}
|
||||
tournamentData={tournamentData}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
|
||||
match={match}
|
||||
readOnly={readOnly}
|
||||
|
||||
@@ -7,6 +7,9 @@ import { getBaseApiUrl } from '../../services/adapter';
|
||||
import { getBaseURL } from '../utils/util';
|
||||
|
||||
export function TournamentQRCode({ tournamentDataFull }: { tournamentDataFull: Tournament }) {
|
||||
if (tournamentDataFull == null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -6,7 +6,7 @@ export function MultiPlayersInput({ form }: { form: UseFormReturnType<any> }) {
|
||||
return (
|
||||
<Textarea
|
||||
label="Add multiple players. Put every player on a separate line"
|
||||
placeholder="Player 1"
|
||||
placeholder="Player 01"
|
||||
minRows={10}
|
||||
{...form.getInputProps('names')}
|
||||
/>
|
||||
|
||||
69
frontend/src/components/modals/activate_next_round_modal.tsx
Normal file
69
frontend/src/components/modals/activate_next_round_modal.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Button, Checkbox, Modal } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconSquareArrowRight } from '@tabler/icons-react';
|
||||
import React, { useState } from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import { StageItemWithRounds } from '../../interfaces/stage_item';
|
||||
import { startNextRound } from '../../services/round';
|
||||
|
||||
export default function ActivateNextRoundModal({
|
||||
tournamentId,
|
||||
stageItem,
|
||||
swrStagesResponse,
|
||||
}: {
|
||||
tournamentId: number;
|
||||
stageItem: StageItemWithRounds;
|
||||
swrStagesResponse: SWRResponse;
|
||||
}) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
adjust_to_time: false,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={() => setOpened(false)} title="Activate next round">
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
await startNextRound(
|
||||
tournamentId,
|
||||
stageItem.id,
|
||||
values.adjust_to_time ? new Date() : null
|
||||
);
|
||||
await swrStagesResponse.mutate();
|
||||
setOpened(false);
|
||||
})}
|
||||
>
|
||||
<Checkbox
|
||||
mt="lg"
|
||||
label="Adjust start time of matches in this round to the current time"
|
||||
{...form.getInputProps('adjust_to_time', { type: 'checkbox' })}
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
color="indigo"
|
||||
size="md"
|
||||
mt="lg"
|
||||
type="submit"
|
||||
leftIcon={<IconSquareArrowRight size={24} />}
|
||||
>
|
||||
Start next round
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<Button
|
||||
color="indigo"
|
||||
size="md"
|
||||
leftIcon={<IconSquareArrowRight size={24} />}
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
Activate next round
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button, Modal, NumberInput } from '@mantine/core';
|
||||
import { Button, Center, Checkbox, Divider, Grid, Modal, NumberInput, Text } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import {
|
||||
@@ -62,16 +62,29 @@ export default function MatchModal({
|
||||
}) {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
team1_score: match != null ? match.team1_score : 0,
|
||||
team2_score: match != null ? match.team2_score : 0,
|
||||
team1_score: match.team1_score,
|
||||
team2_score: match.team2_score,
|
||||
custom_duration_minutes: match.custom_duration_minutes,
|
||||
custom_margin_minutes: match.custom_margin_minutes,
|
||||
},
|
||||
|
||||
validate: {
|
||||
team1_score: (value) => (value >= 0 ? null : 'Score cannot be negative'),
|
||||
team2_score: (value) => (value >= 0 ? null : 'Score cannot be negative'),
|
||||
custom_duration_minutes: (value) =>
|
||||
value == null || value >= 0 ? null : 'Match duration cannot be negative',
|
||||
custom_margin_minutes: (value) =>
|
||||
value == null || value >= 0 ? null : 'Match margin cannot be negative',
|
||||
},
|
||||
});
|
||||
|
||||
const [customDurationEnabled, setCustomDurationEnabled] = useState(
|
||||
match.custom_duration_minutes != null
|
||||
);
|
||||
const [customMarginEnabled, setCustomMarginEnabled] = useState(
|
||||
match.custom_margin_minutes != null
|
||||
);
|
||||
|
||||
const stageItemsLookup = getStageItemLookup(swrStagesResponse);
|
||||
const matchesLookup = getMatchLookup(swrStagesResponse);
|
||||
|
||||
@@ -89,6 +102,10 @@ export default function MatchModal({
|
||||
team1_score: values.team1_score,
|
||||
team2_score: values.team2_score,
|
||||
court_id: match.court_id,
|
||||
custom_duration_minutes: customDurationEnabled
|
||||
? values.custom_duration_minutes
|
||||
: null,
|
||||
custom_margin_minutes: customMarginEnabled ? values.custom_margin_minutes : null,
|
||||
};
|
||||
await updateMatch(tournamentData.id, match.id, updatedMatch);
|
||||
await swrStagesResponse.mutate(null);
|
||||
@@ -104,11 +121,64 @@ export default function MatchModal({
|
||||
/>
|
||||
<NumberInput
|
||||
withAsterisk
|
||||
style={{ marginTop: 20 }}
|
||||
mt="lg"
|
||||
label={`Score of ${team2Name}`}
|
||||
placeholder={`Score of ${team2Name}`}
|
||||
{...form.getInputProps('team2_score')}
|
||||
/>
|
||||
<Divider mt="lg" />
|
||||
|
||||
<Text size="sm" mt="lg">
|
||||
Custom match duration
|
||||
</Text>
|
||||
<Grid align="center">
|
||||
<Grid.Col sm={8}>
|
||||
<NumberInput
|
||||
disabled={!customDurationEnabled}
|
||||
rightSection={<Text>minutes</Text>}
|
||||
placeholder={`${match.duration_minutes}`}
|
||||
rightSectionWidth={92}
|
||||
{...form.getInputProps('custom_duration_minutes')}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={4}>
|
||||
<Center>
|
||||
<Checkbox
|
||||
checked={customDurationEnabled}
|
||||
label="Customize"
|
||||
onChange={(event) => {
|
||||
setCustomDurationEnabled(event.currentTarget.checked);
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Text size="sm" mt="lg">
|
||||
Custom match margin
|
||||
</Text>
|
||||
<Grid align="center">
|
||||
<Grid.Col sm={8}>
|
||||
<NumberInput
|
||||
disabled={!customMarginEnabled}
|
||||
placeholder={`${match.margin_minutes}`}
|
||||
rightSection={<Text>minutes</Text>}
|
||||
rightSectionWidth={92}
|
||||
{...form.getInputProps('custom_margin_minutes')}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={4}>
|
||||
<Center>
|
||||
<Checkbox
|
||||
checked={customMarginEnabled}
|
||||
label="Customize"
|
||||
onChange={(event) => {
|
||||
setCustomMarginEnabled(event.currentTarget.checked);
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Button fullWidth style={{ marginTop: 20 }} color="green" type="submit">
|
||||
Save
|
||||
|
||||
33
frontend/src/components/utils/skeletons.tsx
Normal file
33
frontend/src/components/utils/skeletons.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Center, Grid, Skeleton } from '@mantine/core';
|
||||
import React from 'react';
|
||||
|
||||
export function GenericSkeleton() {
|
||||
return (
|
||||
<>
|
||||
<Skeleton height={75} radius="lg" mb="xl" />
|
||||
<Skeleton height={75} radius="lg" mb="xl" />
|
||||
<Skeleton height={75} radius="lg" mb="xl" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableSkeletonTwoColumns() {
|
||||
return (
|
||||
<Center>
|
||||
<div style={{ minWidth: '1500px', marginTop: '2rem' }}>
|
||||
<Grid>
|
||||
<Grid.Col sm={6}>
|
||||
<Skeleton height={75} radius="lg" mb="xl" />
|
||||
<Skeleton height={75} radius="lg" mb="xl" />
|
||||
<Skeleton height={75} radius="lg" mb="xl" />
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={6}>
|
||||
<Skeleton height={75} radius="lg" mb="xl" />
|
||||
<Skeleton height={75} radius="lg" mb="xl" />
|
||||
<Skeleton height={75} radius="lg" mb="xl" />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</div>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,10 @@ export interface MatchInterface {
|
||||
court: Court | null;
|
||||
start_time: string;
|
||||
position_in_schedule: number | null;
|
||||
duration_minutes: number | null;
|
||||
duration_minutes: number;
|
||||
margin_minutes: number;
|
||||
custom_duration_minutes: number | null;
|
||||
custom_margin_minutes: number | null;
|
||||
}
|
||||
|
||||
export interface MatchBodyInterface {
|
||||
@@ -31,6 +34,8 @@ export interface MatchBodyInterface {
|
||||
team1_score: number;
|
||||
team2_score: number;
|
||||
court_id: number | null;
|
||||
custom_duration_minutes: number | null;
|
||||
custom_margin_minutes: number | null;
|
||||
}
|
||||
|
||||
export interface MatchRescheduleInterface {
|
||||
@@ -66,15 +71,26 @@ export interface SchedulerSettings {
|
||||
setOnlyRecommended: any;
|
||||
}
|
||||
|
||||
export function isMatchHappening(match: MatchInterface) {
|
||||
return (
|
||||
new Date(match.start_time) < new Date() &&
|
||||
new Date(new Date(match.start_time).getTime() + 60000 * 15) > new Date()
|
||||
export function getMatchStartTime(match: MatchInterface) {
|
||||
return new Date(match.start_time);
|
||||
}
|
||||
|
||||
export function getMatchEndTime(match: MatchInterface) {
|
||||
return new Date(
|
||||
getMatchStartTime(match).getTime() + 60000 * (match.duration_minutes + match.margin_minutes)
|
||||
);
|
||||
}
|
||||
|
||||
export function isMatchHappening(match: MatchInterface) {
|
||||
return getMatchStartTime(match) < new Date() && getMatchEndTime(match) > new Date();
|
||||
}
|
||||
|
||||
export function isMatchInTheFutureOrPresent(match: MatchInterface) {
|
||||
return new Date(new Date(match.start_time).getTime() + 60000 * 15) > new Date();
|
||||
return getMatchEndTime(match) > new Date();
|
||||
}
|
||||
|
||||
export function isMatchInTheFuture(match: MatchInterface) {
|
||||
return getMatchStartTime(match) > new Date();
|
||||
}
|
||||
|
||||
export function formatMatchTeam1(
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface Tournament {
|
||||
players_can_be_in_multiple_teams: boolean;
|
||||
auto_assign_courts: boolean;
|
||||
logo_path: string;
|
||||
duration_minutes: number;
|
||||
margin_minutes: number;
|
||||
}
|
||||
export interface TournamentMinimal {
|
||||
id: number;
|
||||
|
||||
@@ -16,7 +16,6 @@ import { StageItemWithRounds } from '../../interfaces/stage_item';
|
||||
import { Tournament, getTournamentEndpoint } from '../../interfaces/tournament';
|
||||
import {
|
||||
checkForAuthError,
|
||||
getCourts,
|
||||
getStages,
|
||||
getTournaments,
|
||||
getUpcomingMatches,
|
||||
@@ -29,7 +28,6 @@ export default function TournamentPage() {
|
||||
const swrTournamentsResponse = getTournaments();
|
||||
checkForAuthError(swrTournamentsResponse);
|
||||
const swrStagesResponse: SWRResponse = getStages(id);
|
||||
const swrCourtsResponse: SWRResponse = getCourts(id);
|
||||
const [onlyRecommended, setOnlyRecommended] = useState('true');
|
||||
const [eloThreshold, setEloThreshold] = useState(100);
|
||||
const [iterations, setIterations] = useState(200);
|
||||
@@ -90,10 +88,6 @@ export default function TournamentPage() {
|
||||
schedulerSettings
|
||||
);
|
||||
|
||||
if (tournamentDataFull == null) {
|
||||
return <NotFoundTitle />;
|
||||
}
|
||||
|
||||
const scheduler =
|
||||
draftRound != null &&
|
||||
activeStage != null &&
|
||||
@@ -112,11 +106,15 @@ export default function TournamentPage() {
|
||||
</>
|
||||
) : null;
|
||||
|
||||
if (!swrTournamentsResponse.isLoading && tournamentDataFull == null) {
|
||||
return <NotFoundTitle />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TournamentLayout tournament_id={tournamentData.id}>
|
||||
<Grid grow>
|
||||
<Grid.Col span={6}>
|
||||
<Title>{tournamentDataFull.name}</Title>
|
||||
<Title>{tournamentDataFull != null ? tournamentDataFull.name : ''}</Title>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Group position="right">
|
||||
@@ -163,7 +161,6 @@ export default function TournamentPage() {
|
||||
<Brackets
|
||||
tournamentData={tournamentDataFull}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
|
||||
readOnly={false}
|
||||
selectedStageId={selectedStageId}
|
||||
|
||||
@@ -11,13 +11,10 @@ import {
|
||||
TournamentQRCode,
|
||||
TournamentTitle,
|
||||
} from '../../../../components/dashboard/layout';
|
||||
import { TableSkeletonTwoColumns } from '../../../../components/utils/skeletons';
|
||||
import { responseIsValid } from '../../../../components/utils/util';
|
||||
import { Court } from '../../../../interfaces/court';
|
||||
import {
|
||||
MatchInterface,
|
||||
isMatchHappening,
|
||||
isMatchInTheFutureOrPresent,
|
||||
} from '../../../../interfaces/match';
|
||||
import { MatchInterface, isMatchHappening, isMatchInTheFuture } from '../../../../interfaces/match';
|
||||
import { getCourtsLive, getStagesLive } from '../../../../services/adapter';
|
||||
import { getMatchLookupByCourt } from '../../../../services/lookups';
|
||||
import { getTournamentResponseByEndpointName } from '../../../../services/tournament';
|
||||
@@ -32,27 +29,24 @@ export default function CourtsPage() {
|
||||
const swrStagesResponse: SWRResponse = getStagesLive(tournamentId, true);
|
||||
const swrCourtsResponse: SWRResponse = getCourtsLive(tournamentId);
|
||||
|
||||
if (swrStagesResponse.isLoading || swrCourtsResponse.isLoading) {
|
||||
return <TableSkeletonTwoColumns />;
|
||||
}
|
||||
|
||||
if (notFound) {
|
||||
return <NotFoundTitle />;
|
||||
}
|
||||
|
||||
const tournamentDataFull = tournamentResponse[0];
|
||||
const stages = responseIsValid(swrStagesResponse) ? swrStagesResponse.data.data : [];
|
||||
const tournamentDataFull = tournamentResponse != null ? tournamentResponse[0] : null;
|
||||
const courts = responseIsValid(swrCourtsResponse) ? swrCourtsResponse.data.data : [];
|
||||
const matchesByCourtId = responseIsValid(swrStagesResponse)
|
||||
? getMatchLookupByCourt(swrStagesResponse)
|
||||
: [];
|
||||
|
||||
if (courts.length < 1 || stages.length < 1) {
|
||||
return <NotFoundTitle />;
|
||||
}
|
||||
|
||||
const rows = courts.map((court: Court) => {
|
||||
const matchesForCourt = matchesByCourtId[court.id] || [];
|
||||
const activeMatch = matchesForCourt.filter((m: MatchInterface) => isMatchHappening(m))[0];
|
||||
const futureMatch = matchesForCourt.filter((m: MatchInterface) =>
|
||||
isMatchInTheFutureOrPresent(m)
|
||||
)[0];
|
||||
const futureMatch = matchesForCourt.filter((m: MatchInterface) => isMatchInTheFuture(m))[0];
|
||||
|
||||
return (
|
||||
<CourtsLarge key={court.id} court={court} activeMatch={activeMatch} nextMatch={futureMatch} />
|
||||
|
||||
@@ -15,7 +15,7 @@ import StagesTab from '../../../../components/utils/stages_tab';
|
||||
import { responseIsValid } from '../../../../components/utils/util';
|
||||
import { BracketDisplaySettings } from '../../../../interfaces/brackets';
|
||||
import { StageWithStageItems } from '../../../../interfaces/stage';
|
||||
import { getCourts, getStagesLive } from '../../../../services/adapter';
|
||||
import { getStagesLive } from '../../../../services/adapter';
|
||||
import { getTournamentResponseByEndpointName } from '../../../../services/tournament';
|
||||
|
||||
export default function Index() {
|
||||
@@ -26,7 +26,6 @@ export default function Index() {
|
||||
const tournamentId = !notFound ? tournamentResponse[0].id : -1;
|
||||
|
||||
const swrStagesResponse: SWRResponse = getStagesLive(tournamentId, true);
|
||||
const swrCourtsResponse: SWRResponse = getCourts(tournamentId);
|
||||
const [selectedStageId, setSelectedStageId] = useState(null);
|
||||
const [matchVisibility, setMatchVisibility] = useState('all');
|
||||
const [teamNamesDisplay, setTeamNamesDisplay] = useState('team-names');
|
||||
@@ -36,12 +35,11 @@ export default function Index() {
|
||||
teamNamesDisplay,
|
||||
setTeamNamesDisplay,
|
||||
};
|
||||
|
||||
if (notFound) {
|
||||
if (notFound && !swrStagesResponse.isLoading) {
|
||||
return <NotFoundTitle />;
|
||||
}
|
||||
|
||||
const tournamentDataFull = tournamentResponse[0];
|
||||
const tournamentDataFull = tournamentResponse != null ? tournamentResponse[0] : null;
|
||||
|
||||
if (responseIsValid(swrStagesResponse)) {
|
||||
const activeTab = swrStagesResponse.data.data.filter(
|
||||
@@ -75,7 +73,6 @@ export default function Index() {
|
||||
<Brackets
|
||||
tournamentData={tournamentDataFull}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={null}
|
||||
readOnly
|
||||
selectedStageId={selectedStageId}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TournamentTitle,
|
||||
} from '../../../../components/dashboard/layout';
|
||||
import StandingsTable from '../../../../components/tables/standings';
|
||||
import { TableSkeletonTwoColumns } from '../../../../components/utils/skeletons';
|
||||
import { getTeamsLive } from '../../../../services/adapter';
|
||||
import { getTournamentResponseByEndpointName } from '../../../../services/tournament';
|
||||
|
||||
@@ -23,6 +24,10 @@ export default function Standings() {
|
||||
|
||||
const swrTeamsResponse: SWRResponse = getTeamsLive(tournamentId);
|
||||
|
||||
if (swrTeamsResponse.isLoading) {
|
||||
return <TableSkeletonTwoColumns />;
|
||||
}
|
||||
|
||||
if (notFound) {
|
||||
return <NotFoundTitle />;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CopyButton,
|
||||
Grid,
|
||||
Image,
|
||||
NumberInput,
|
||||
Select,
|
||||
Text,
|
||||
TextInput,
|
||||
@@ -17,12 +18,14 @@ import assert from 'assert';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import NotFoundTitle from '../../404';
|
||||
import { DropzoneButton } from '../../../components/utils/file_upload';
|
||||
import { GenericSkeleton } from '../../../components/utils/skeletons';
|
||||
import { getBaseURL, getTournamentIdFromRouter } from '../../../components/utils/util';
|
||||
import { Club } from '../../../interfaces/club';
|
||||
import { Tournament, getTournamentEndpoint } from '../../../interfaces/tournament';
|
||||
import { getBaseApiUrl, getClubs, getTournaments } from '../../../services/adapter';
|
||||
import { createTournament, updateTournament } from '../../../services/tournament';
|
||||
import { updateTournament } from '../../../services/tournament';
|
||||
import TournamentLayout from '../_tournament_layout';
|
||||
|
||||
export function TournamentLogo({ tournament }: { tournament: Tournament | null }) {
|
||||
@@ -31,26 +34,25 @@ export function TournamentLogo({ tournament }: { tournament: Tournament | null }
|
||||
}
|
||||
|
||||
function GeneralTournamentForm({
|
||||
is_create_form,
|
||||
tournament,
|
||||
swrTournamentsResponse,
|
||||
clubs,
|
||||
}: {
|
||||
is_create_form: boolean;
|
||||
tournament: Tournament | null;
|
||||
tournament: Tournament;
|
||||
swrTournamentsResponse: SWRResponse;
|
||||
clubs: Club[];
|
||||
}) {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
start_time: tournament == null ? new Date() : new Date(tournament.start_time),
|
||||
name: tournament == null ? '' : tournament.name,
|
||||
club_id: tournament == null ? null : `${tournament.club_id}`,
|
||||
dashboard_public: tournament == null ? true : tournament.dashboard_public,
|
||||
dashboard_endpoint: tournament == null ? '' : tournament.dashboard_endpoint,
|
||||
players_can_be_in_multiple_teams:
|
||||
tournament == null ? true : tournament.players_can_be_in_multiple_teams,
|
||||
auto_assign_courts: tournament == null ? true : tournament.auto_assign_courts,
|
||||
start_time: new Date(tournament.start_time),
|
||||
name: tournament.name,
|
||||
club_id: `${tournament.club_id}`,
|
||||
dashboard_public: tournament.dashboard_public,
|
||||
dashboard_endpoint: tournament.dashboard_endpoint,
|
||||
players_can_be_in_multiple_teams: tournament.players_can_be_in_multiple_teams,
|
||||
auto_assign_courts: tournament.auto_assign_courts,
|
||||
duration_minutes: tournament.duration_minutes,
|
||||
margin_minutes: tournament.margin_minutes,
|
||||
},
|
||||
|
||||
validate: {
|
||||
@@ -65,28 +67,19 @@ function GeneralTournamentForm({
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
assert(values.club_id != null);
|
||||
if (is_create_form) {
|
||||
await createTournament(
|
||||
parseInt(values.club_id, 10),
|
||||
values.name,
|
||||
values.dashboard_public,
|
||||
values.dashboard_endpoint,
|
||||
values.players_can_be_in_multiple_teams,
|
||||
values.auto_assign_courts,
|
||||
values.start_time.toISOString()
|
||||
);
|
||||
} else {
|
||||
assert(tournament != null);
|
||||
await updateTournament(
|
||||
tournament.id,
|
||||
values.name,
|
||||
values.dashboard_public,
|
||||
values.dashboard_endpoint,
|
||||
values.players_can_be_in_multiple_teams,
|
||||
values.auto_assign_courts,
|
||||
values.start_time.toISOString()
|
||||
);
|
||||
}
|
||||
|
||||
await updateTournament(
|
||||
tournament.id,
|
||||
values.name,
|
||||
values.dashboard_public,
|
||||
values.dashboard_endpoint,
|
||||
values.players_can_be_in_multiple_teams,
|
||||
values.auto_assign_courts,
|
||||
values.start_time.toISOString(),
|
||||
values.duration_minutes,
|
||||
values.margin_minutes
|
||||
);
|
||||
|
||||
await swrTournamentsResponse.mutate(null);
|
||||
})}
|
||||
>
|
||||
@@ -120,7 +113,6 @@ function GeneralTournamentForm({
|
||||
<Grid>
|
||||
<Grid.Col sm={9}>
|
||||
<DateTimePicker
|
||||
label=""
|
||||
icon={<IconCalendar size="1.1rem" stroke={1.5} />}
|
||||
placeholder="Pick date and time"
|
||||
mx="auto"
|
||||
@@ -142,6 +134,25 @@ function GeneralTournamentForm({
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col sm={6}>
|
||||
<NumberInput
|
||||
label="Match duration (minutes)"
|
||||
mt="lg"
|
||||
type="number"
|
||||
{...form.getInputProps('duration_minutes')}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={6}>
|
||||
<NumberInput
|
||||
label="Time between matches (minutes)"
|
||||
mt="lg"
|
||||
type="number"
|
||||
{...form.getInputProps('margin_minutes')}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Checkbox
|
||||
mt="lg"
|
||||
label="Allow anyone to see the dashboard of rounds and matches"
|
||||
@@ -185,28 +196,36 @@ function GeneralTournamentForm({
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { tournamentData } = getTournamentIdFromRouter();
|
||||
|
||||
const swrClubsResponse: SWRResponse = getClubs();
|
||||
const swrTournamentsResponse = getTournaments();
|
||||
|
||||
const tournaments: Tournament[] =
|
||||
swrTournamentsResponse.data != null ? swrTournamentsResponse.data.data : [];
|
||||
const tournamentDataFull = tournaments.filter(
|
||||
(tournament) => tournament.id === tournamentData.id
|
||||
)[0];
|
||||
|
||||
const is_create_form = tournamentDataFull == null;
|
||||
const swrClubsResponse: SWRResponse = getClubs();
|
||||
const clubs: Club[] = swrClubsResponse.data != null ? swrClubsResponse.data.data : [];
|
||||
|
||||
let content = <NotFoundTitle />;
|
||||
|
||||
if (swrTournamentsResponse.isLoading || swrClubsResponse.isLoading) {
|
||||
content = <GenericSkeleton />;
|
||||
}
|
||||
|
||||
if (tournamentDataFull != null) {
|
||||
content = (
|
||||
<GeneralTournamentForm
|
||||
tournament={tournamentDataFull}
|
||||
swrTournamentsResponse={swrTournamentsResponse}
|
||||
clubs={clubs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TournamentLayout tournament_id={tournamentData.id}>
|
||||
<Container>
|
||||
<GeneralTournamentForm
|
||||
is_create_form={is_create_form}
|
||||
tournament={tournamentDataFull}
|
||||
swrTournamentsResponse={swrTournamentsResponse}
|
||||
clubs={clubs}
|
||||
/>
|
||||
</Container>
|
||||
<Container>{content}</Container>
|
||||
</TournamentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,14 @@ import { getLogin, performLogout, tokenPresent } from './local_storage';
|
||||
const axios = require('axios').default;
|
||||
|
||||
export function handleRequestError(response: any) {
|
||||
if (response.code === 'ERR_NETWORK') {
|
||||
showNotification({
|
||||
color: 'red',
|
||||
title: 'An error occurred',
|
||||
message: 'Internal server error',
|
||||
});
|
||||
}
|
||||
|
||||
if (response.response != null && response.response.data.detail != null) {
|
||||
// If the detail contains an array, there is likely a pydantic validation error occurring.
|
||||
const message = Array.isArray(response.response.data.detail)
|
||||
@@ -67,7 +75,7 @@ export function getTeams(tournament_id: number): SWRResponse {
|
||||
|
||||
export function getTeamsLive(tournament_id: number): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/teams`, fetcher, {
|
||||
refreshInterval: 5000,
|
||||
refreshInterval: 5_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,16 +84,27 @@ export function getAvailableStageItemInputs(tournament_id: number, stage_id: num
|
||||
}
|
||||
|
||||
export function getStages(tournament_id: number, no_draft_rounds: boolean = false): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/stages?no_draft_rounds=${no_draft_rounds}`, fetcher);
|
||||
return useSWR(
|
||||
tournament_id === -1
|
||||
? null
|
||||
: `tournaments/${tournament_id}/stages?no_draft_rounds=${no_draft_rounds}`,
|
||||
fetcher
|
||||
);
|
||||
}
|
||||
|
||||
export function getStagesLive(
|
||||
tournament_id: number,
|
||||
no_draft_rounds: boolean = false
|
||||
): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/stages?no_draft_rounds=${no_draft_rounds}`, fetcher, {
|
||||
refreshInterval: 5_000,
|
||||
});
|
||||
return useSWR(
|
||||
tournament_id === -1
|
||||
? null
|
||||
: `tournaments/${tournament_id}/stages?no_draft_rounds=${no_draft_rounds}`,
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: 5_000,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getCourts(tournament_id: number): SWRResponse {
|
||||
@@ -108,7 +127,9 @@ export function getUpcomingMatches(
|
||||
schedulerSettings: SchedulerSettings
|
||||
): SWRResponse {
|
||||
return useSWR(
|
||||
`tournaments/${tournament_id}/rounds/${round_id}/upcoming_matches?elo_diff_threshold=${schedulerSettings.eloThreshold}&only_recommended=${schedulerSettings.onlyRecommended}&limit=${schedulerSettings.limit}&iterations=${schedulerSettings.iterations}`,
|
||||
round_id === -1
|
||||
? null
|
||||
: `tournaments/${tournament_id}/rounds/${round_id}/upcoming_matches?elo_diff_threshold=${schedulerSettings.eloThreshold}&only_recommended=${schedulerSettings.onlyRecommended}&limit=${schedulerSettings.limit}&iterations=${schedulerSettings.iterations}`,
|
||||
fetcher
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,8 +27,14 @@ export async function updateRound(tournament_id: number, round_id: number, round
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function startNextRound(tournament_id: number, stage_item_id: number) {
|
||||
export async function startNextRound(
|
||||
tournament_id: number,
|
||||
stage_item_id: number,
|
||||
adjust_to_time: Date | null
|
||||
) {
|
||||
return createAxios()
|
||||
.post(`tournaments/${tournament_id}/stage_items/${stage_item_id}/start_next_round`)
|
||||
.post(`tournaments/${tournament_id}/stage_items/${stage_item_id}/start_next_round`, {
|
||||
adjust_to_time: adjust_to_time != null ? adjust_to_time?.toISOString() : null,
|
||||
})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ export async function updateTournament(
|
||||
dashboard_endpoint: string,
|
||||
players_can_be_in_multiple_teams: boolean,
|
||||
auto_assign_courts: boolean,
|
||||
start_time: string
|
||||
start_time: string,
|
||||
duration_minutes: number,
|
||||
margin_minutes: number
|
||||
) {
|
||||
return createAxios()
|
||||
.put(`tournaments/${tournament_id}`, {
|
||||
@@ -46,6 +48,8 @@ export async function updateTournament(
|
||||
players_can_be_in_multiple_teams,
|
||||
auto_assign_courts,
|
||||
start_time,
|
||||
duration_minutes,
|
||||
margin_minutes,
|
||||
})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user