mirror of
https://github.com/evroon/bracket.git
synced 2026-04-23 16:57:06 -04:00
Customize rankings (#797)
Allows you to add rankings that specify how the ranking per stage item is calculated. Points are now stored per stage item input.
This commit is contained in:
131
backend/alembic/versions/77de1c773dba_create_rankings_table.py
Normal file
131
backend/alembic/versions/77de1c773dba_create_rankings_table.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""create rankings table
|
||||
|
||||
Revision ID: 77de1c773dba
|
||||
Revises: 1961954c0320
|
||||
Create Date: 2024-06-29 14:13:18.278876
|
||||
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str | None = "77de1c773dba"
|
||||
down_revision: str | None = "1961954c0320"
|
||||
branch_labels: str | None = None
|
||||
depends_on: str | None = None
|
||||
|
||||
|
||||
def add_missing_rankings(tournaments_without_ranking: list[Any]) -> None:
|
||||
for tournament in tournaments_without_ranking:
|
||||
ranking_id = (
|
||||
op.get_bind()
|
||||
.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO rankings (
|
||||
tournament_id,
|
||||
position,
|
||||
win_points,
|
||||
draw_points,
|
||||
loss_points,
|
||||
add_score_points
|
||||
)
|
||||
VALUES (:tournament_id, 0, 1, 0.5, 0, false)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
tournament_id=tournament.id,
|
||||
)
|
||||
.scalar_one()
|
||||
)
|
||||
|
||||
op.get_bind().execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE stage_items
|
||||
SET ranking_id = :ranking_id
|
||||
WHERE stage_items.ranking_id IS NULL AND stage_items.stage_id IN (
|
||||
SELECT id FROM stages
|
||||
WHERE stages.tournament_id = :tournament_id
|
||||
)
|
||||
"""
|
||||
),
|
||||
tournament_id=tournament.id,
|
||||
ranking_id=ranking_id,
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"rankings",
|
||||
sa.Column("id", sa.BigInteger(), nullable=False),
|
||||
sa.Column(
|
||||
"created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False
|
||||
),
|
||||
sa.Column("tournament_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("position", sa.Integer(), nullable=False),
|
||||
sa.Column("win_points", sa.Float(), nullable=False),
|
||||
sa.Column("draw_points", sa.Float(), nullable=False),
|
||||
sa.Column("loss_points", sa.Float(), nullable=False),
|
||||
sa.Column("add_score_points", sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["tournament_id"],
|
||||
["tournaments.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_rankings_id"), "rankings", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_rankings_tournament_id"), "rankings", ["tournament_id"], unique=False)
|
||||
|
||||
tournaments_without_ranking = (
|
||||
op.get_bind()
|
||||
.execute(
|
||||
"""
|
||||
SELECT * FROM tournaments WHERE (
|
||||
SELECT NOT EXISTS (
|
||||
SELECT 1 FROM rankings WHERE rankings.tournament_id = tournaments.id
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
.fetchall()
|
||||
)
|
||||
|
||||
op.add_column("stage_items", sa.Column("ranking_id", sa.BigInteger(), nullable=True))
|
||||
|
||||
add_missing_rankings(tournaments_without_ranking)
|
||||
|
||||
op.alter_column("stage_items", "ranking_id", nullable=False)
|
||||
op.create_foreign_key(
|
||||
"stage_items_x_rankings_id_fkey", "stage_items", "rankings", ["ranking_id"], ["id"]
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
"stage_item_inputs", sa.Column("points", sa.Float(), server_default="0", nullable=False)
|
||||
)
|
||||
op.add_column(
|
||||
"stage_item_inputs", sa.Column("wins", sa.Integer(), server_default="0", nullable=False)
|
||||
)
|
||||
op.add_column(
|
||||
"stage_item_inputs", sa.Column("draws", sa.Integer(), server_default="0", nullable=False)
|
||||
)
|
||||
op.add_column(
|
||||
"stage_item_inputs", sa.Column("losses", sa.Integer(), server_default="0", nullable=False)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("stage_item_inputs", "losses")
|
||||
op.drop_column("stage_item_inputs", "draws")
|
||||
op.drop_column("stage_item_inputs", "wins")
|
||||
op.drop_column("stage_item_inputs", "points")
|
||||
|
||||
op.drop_constraint("stage_items_x_rankings_id_fkey", "stage_items", type_="foreignkey")
|
||||
op.drop_column("stage_items", "ranking_id")
|
||||
op.drop_index(op.f("ix_rankings_tournament_id"), table_name="rankings")
|
||||
op.drop_index(op.f("ix_rankings_id"), table_name="rankings")
|
||||
op.drop_table("rankings")
|
||||
@@ -20,6 +20,7 @@ from bracket.routes import (
|
||||
internals,
|
||||
matches,
|
||||
players,
|
||||
rankings,
|
||||
rounds,
|
||||
stage_items,
|
||||
stages,
|
||||
@@ -58,12 +59,13 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||
|
||||
|
||||
routers = {
|
||||
"Internals": internals.router,
|
||||
"Auth": auth.router,
|
||||
"Clubs": clubs.router,
|
||||
"Courts": courts.router,
|
||||
"Internals": internals.router,
|
||||
"Matches": matches.router,
|
||||
"Players": players.router,
|
||||
"Rankings": rankings.router,
|
||||
"Rounds": rounds.router,
|
||||
"Stage Items": stage_items.router,
|
||||
"Stages": stages.router,
|
||||
|
||||
@@ -3,15 +3,15 @@ from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from typing import TypeVar
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.ranking.statistics import START_ELO, TeamStatistics
|
||||
from bracket.models.db.match import MatchWithDetailsDefinitive
|
||||
from bracket.models.db.players import START_ELO, PlayerStatistics
|
||||
from bracket.models.db.ranking import Ranking
|
||||
from bracket.models.db.stage_item import StageType
|
||||
from bracket.models.db.util import StageItemWithRounds
|
||||
from bracket.schema import players, teams
|
||||
from bracket.sql.players import get_all_players_in_tournament, update_player_stats
|
||||
from bracket.sql.stages import get_full_tournament_details
|
||||
from bracket.sql.teams import get_teams_in_tournament, update_team_stats
|
||||
from bracket.utils.id_types import PlayerId, TeamId, TournamentId
|
||||
from bracket.sql.rankings import get_ranking_for_stage_item
|
||||
from bracket.sql.stage_items import get_stage_item
|
||||
from bracket.sql.teams import update_team_stats
|
||||
from bracket.utils.id_types import PlayerId, StageItemId, TeamId, TournamentId
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
K = 32
|
||||
@@ -21,44 +21,58 @@ D = 400
|
||||
TeamIdOrPlayerId = TypeVar("TeamIdOrPlayerId", bound=PlayerId | TeamId)
|
||||
|
||||
|
||||
def set_statistics_for_player_or_team(
|
||||
def set_statistics_for_team(
|
||||
team_index: int,
|
||||
stats: defaultdict[TeamIdOrPlayerId, PlayerStatistics],
|
||||
stats: defaultdict[TeamId, TeamStatistics],
|
||||
match: MatchWithDetailsDefinitive,
|
||||
team_or_player_id: TeamIdOrPlayerId,
|
||||
rating_team1_before: float,
|
||||
rating_team2_before: float,
|
||||
team_id: TeamId,
|
||||
rating_team1_before: Decimal,
|
||||
rating_team2_before: Decimal,
|
||||
ranking: Ranking,
|
||||
stage_item: StageItemWithRounds,
|
||||
) -> None:
|
||||
is_team1 = team_index == 0
|
||||
team_score = match.team1_score if is_team1 else match.team2_score
|
||||
was_draw = match.team1_score == match.team2_score
|
||||
has_won = not was_draw and team_score == max(match.team1_score, match.team2_score)
|
||||
|
||||
# Set default for SWISS teams
|
||||
if stage_item.type is StageType.SWISS and team_id not in stats:
|
||||
stats[team_id].points = START_ELO
|
||||
|
||||
if has_won:
|
||||
stats[team_or_player_id].wins += 1
|
||||
swiss_score_diff = Decimal("1.00")
|
||||
stats[team_id].wins += 1
|
||||
swiss_score_diff = ranking.win_points
|
||||
elif was_draw:
|
||||
stats[team_or_player_id].draws += 1
|
||||
swiss_score_diff = Decimal("0.50")
|
||||
stats[team_id].draws += 1
|
||||
swiss_score_diff = ranking.draw_points
|
||||
else:
|
||||
stats[team_or_player_id].losses += 1
|
||||
swiss_score_diff = Decimal("0.00")
|
||||
stats[team_id].losses += 1
|
||||
swiss_score_diff = ranking.loss_points
|
||||
|
||||
stats[team_or_player_id].swiss_score += swiss_score_diff
|
||||
if ranking.add_score_points:
|
||||
swiss_score_diff += match.team1_score if is_team1 else match.team2_score
|
||||
|
||||
rating_diff = (rating_team2_before - rating_team1_before) * (1 if is_team1 else -1)
|
||||
expected_score = Decimal(1.0 / (1.0 + math.pow(10.0, rating_diff / D)))
|
||||
stats[team_or_player_id].elo_score += int(K * (swiss_score_diff - expected_score))
|
||||
match stage_item.type:
|
||||
case StageType.ROUND_ROBIN | StageType.SINGLE_ELIMINATION:
|
||||
stats[team_id].points += swiss_score_diff
|
||||
|
||||
case StageType.SWISS:
|
||||
rating_diff = (rating_team2_before - rating_team1_before) * (1 if is_team1 else -1)
|
||||
expected_score = Decimal(1.0 / (1.0 + math.pow(10.0, rating_diff / D)))
|
||||
stats[team_id].points += int(K * (swiss_score_diff - expected_score))
|
||||
|
||||
case _:
|
||||
raise ValueError(f"Unsupported stage type: {stage_item.type}")
|
||||
|
||||
|
||||
def determine_ranking_for_stage_items(
|
||||
stage_items: list[StageItemWithRounds],
|
||||
) -> tuple[defaultdict[PlayerId, PlayerStatistics], defaultdict[TeamId, PlayerStatistics]]:
|
||||
player_x_stats: defaultdict[PlayerId, PlayerStatistics] = defaultdict(PlayerStatistics)
|
||||
team_x_stats: defaultdict[TeamId, PlayerStatistics] = defaultdict(PlayerStatistics)
|
||||
def determine_ranking_for_stage_item(
|
||||
stage_item: StageItemWithRounds,
|
||||
ranking: Ranking,
|
||||
) -> defaultdict[TeamId, TeamStatistics]:
|
||||
team_x_stats: defaultdict[TeamId, TeamStatistics] = defaultdict(TeamStatistics)
|
||||
matches = [
|
||||
match
|
||||
for stage_item in stage_items
|
||||
for round_ in stage_item.rounds
|
||||
if not round_.is_draft
|
||||
for match in round_.matches
|
||||
@@ -66,83 +80,46 @@ def determine_ranking_for_stage_items(
|
||||
if match.team1_score != 0 or match.team2_score != 0
|
||||
]
|
||||
for match in matches:
|
||||
rating_team1_before = (
|
||||
sum(player_x_stats[player_id].elo_score for player_id in match.team1.player_ids)
|
||||
/ len(match.team1.player_ids)
|
||||
if len(match.team1.player_ids) > 0
|
||||
else START_ELO
|
||||
)
|
||||
rating_team2_before = (
|
||||
sum(player_x_stats[player_id].elo_score for player_id in match.team2.player_ids)
|
||||
/ len(match.team2.player_ids)
|
||||
if len(match.team2.player_ids) > 0
|
||||
else START_ELO
|
||||
)
|
||||
|
||||
for team_index, team in enumerate(match.teams):
|
||||
if team.id is not None:
|
||||
set_statistics_for_player_or_team(
|
||||
set_statistics_for_team(
|
||||
team_index,
|
||||
team_x_stats,
|
||||
match,
|
||||
team.id,
|
||||
rating_team1_before,
|
||||
rating_team2_before,
|
||||
match.team1.elo_score,
|
||||
match.team2.elo_score,
|
||||
ranking,
|
||||
stage_item,
|
||||
)
|
||||
|
||||
for player in team.players:
|
||||
set_statistics_for_player_or_team(
|
||||
team_index,
|
||||
player_x_stats,
|
||||
match,
|
||||
assert_some(player.id),
|
||||
rating_team1_before,
|
||||
rating_team2_before,
|
||||
)
|
||||
|
||||
return player_x_stats, team_x_stats
|
||||
return team_x_stats
|
||||
|
||||
|
||||
def determine_team_ranking_for_stage_item(
|
||||
async def determine_team_ranking_for_stage_item(
|
||||
stage_item: StageItemWithRounds,
|
||||
) -> list[tuple[TeamId, PlayerStatistics]]:
|
||||
_, team_ranking = determine_ranking_for_stage_items([stage_item])
|
||||
return sorted(team_ranking.items(), key=lambda x: x[1].elo_score, reverse=True)
|
||||
ranking: Ranking,
|
||||
) -> list[tuple[TeamId, TeamStatistics]]:
|
||||
team_ranking = determine_ranking_for_stage_item(stage_item, ranking)
|
||||
return sorted(team_ranking.items(), key=lambda x: x[1].points, reverse=True)
|
||||
|
||||
|
||||
async def recalculate_ranking_for_tournament_id(tournament_id: TournamentId) -> None:
|
||||
stages = await get_full_tournament_details(tournament_id)
|
||||
stage_items = [stage_item for stage in stages for stage_item in stage.stage_items]
|
||||
await recalculate_ranking_for_stage_items(tournament_id, stage_items)
|
||||
|
||||
|
||||
async def recalculate_ranking_for_stage_items(
|
||||
tournament_id: TournamentId, stage_items: list[StageItemWithRounds]
|
||||
async def recalculate_ranking_for_stage_item_id(
|
||||
tournament_id: TournamentId,
|
||||
stage_item_id: StageItemId,
|
||||
) -> None:
|
||||
elo_per_player, elo_per_team = determine_ranking_for_stage_items(stage_items)
|
||||
stage_item = await get_stage_item(tournament_id, stage_item_id)
|
||||
ranking = await get_ranking_for_stage_item(tournament_id, stage_item_id)
|
||||
assert stage_item, "Stage item not found"
|
||||
assert ranking, "Ranking not found"
|
||||
|
||||
for player_id, statistics in elo_per_player.items():
|
||||
await update_player_stats(tournament_id, player_id, statistics)
|
||||
team_x_stage_item_input_lookup = {
|
||||
stage_item_input.team_id: assert_some(stage_item_input.id)
|
||||
for stage_item_input in stage_item.inputs
|
||||
if stage_item_input.team_id is not None
|
||||
}
|
||||
|
||||
for team_id, statistics in elo_per_team.items():
|
||||
await update_team_stats(tournament_id, team_id, statistics)
|
||||
elo_per_team = determine_ranking_for_stage_item(stage_item, ranking)
|
||||
|
||||
all_players = await get_all_players_in_tournament(tournament_id)
|
||||
for player in all_players:
|
||||
if player.id not in elo_per_player:
|
||||
await database.execute(
|
||||
query=players.update().where(
|
||||
(players.c.id == player.id) & (players.c.tournament_id == tournament_id)
|
||||
),
|
||||
values=PlayerStatistics().model_dump(),
|
||||
)
|
||||
|
||||
all_teams = await get_teams_in_tournament(tournament_id)
|
||||
for team in all_teams:
|
||||
if team.id not in elo_per_team:
|
||||
await database.execute(
|
||||
query=teams.update().where(
|
||||
(teams.c.id == team.id) & (teams.c.tournament_id == tournament_id)
|
||||
),
|
||||
values=PlayerStatistics().model_dump(),
|
||||
)
|
||||
for team_id, stage_item_input_id in team_x_stage_item_input_lookup.items():
|
||||
await update_team_stats(tournament_id, stage_item_input_id, elo_per_team[team_id])
|
||||
|
||||
12
backend/bracket/logic/ranking/statistics.py
Normal file
12
backend/bracket/logic/ranking/statistics.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
START_ELO = Decimal("1200")
|
||||
|
||||
|
||||
class TeamStatistics(BaseModel):
|
||||
wins: int = 0
|
||||
draws: int = 0
|
||||
losses: int = 0
|
||||
points: Decimal = Decimal("0.00")
|
||||
@@ -1,25 +1,34 @@
|
||||
from bracket.logic.ranking.elo import (
|
||||
determine_team_ranking_for_stage_item,
|
||||
)
|
||||
from bracket.logic.ranking.statistics import TeamStatistics
|
||||
from bracket.models.db.match import MatchWithDetails
|
||||
from bracket.sql.matches import sql_get_match, sql_update_team_ids_for_match
|
||||
from bracket.sql.rankings import get_ranking_for_stage_item
|
||||
from bracket.sql.stage_items import get_stage_item
|
||||
from bracket.sql.stages import get_full_tournament_details
|
||||
from bracket.utils.id_types import MatchId, StageId, StageItemId, TeamId, TournamentId
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
StageItemXTeamRanking = dict[StageItemId, list[tuple[TeamId, TeamStatistics]]]
|
||||
|
||||
|
||||
async def determine_team_id(
|
||||
tournament_id: TournamentId,
|
||||
winner_from_stage_item_id: StageItemId | None,
|
||||
winner_position: int | None,
|
||||
winner_from_match_id: MatchId | None,
|
||||
stage_item_x_team_rankings: StageItemXTeamRanking,
|
||||
) -> TeamId | None:
|
||||
if winner_from_stage_item_id is not None and winner_position is not None:
|
||||
stage_item = await get_stage_item(tournament_id, winner_from_stage_item_id)
|
||||
assert stage_item is not None
|
||||
"""
|
||||
Determine the team ID for a match that didn't have a team assigned yet.
|
||||
|
||||
team_ranking = determine_team_ranking_for_stage_item(stage_item)
|
||||
Either return:
|
||||
- A team that was chosen from a previous stage item ranking, or
|
||||
- A team that was chosen from a previous match
|
||||
"""
|
||||
|
||||
if winner_from_stage_item_id is not None and winner_position is not None:
|
||||
team_ranking = stage_item_x_team_rankings[winner_from_stage_item_id]
|
||||
if len(team_ranking) >= winner_position:
|
||||
return team_ranking[winner_position - 1][0]
|
||||
|
||||
@@ -28,38 +37,57 @@ async def determine_team_id(
|
||||
if winner_from_match_id is not None:
|
||||
match = await sql_get_match(winner_from_match_id)
|
||||
winner_index = match.get_winner_index()
|
||||
|
||||
if winner_index is not None:
|
||||
team_id = match.team1_id if match.get_winner_index() == 1 else match.team2_id
|
||||
assert team_id is not None
|
||||
return team_id
|
||||
return match.team1_id if winner_index == 1 else match.team2_id
|
||||
|
||||
return None
|
||||
|
||||
raise ValueError("Unexpected match type")
|
||||
|
||||
|
||||
async def set_team_ids_for_match(tournament_id: TournamentId, match: MatchWithDetails) -> None:
|
||||
async def set_team_ids_for_match(
|
||||
match: MatchWithDetails, stage_item_x_team_rankings: StageItemXTeamRanking
|
||||
) -> None:
|
||||
team1_id = await determine_team_id(
|
||||
tournament_id,
|
||||
match.team1_winner_from_stage_item_id,
|
||||
match.team1_winner_position,
|
||||
match.team1_winner_from_match_id,
|
||||
stage_item_x_team_rankings,
|
||||
)
|
||||
team2_id = await determine_team_id(
|
||||
tournament_id,
|
||||
match.team2_winner_from_stage_item_id,
|
||||
match.team2_winner_position,
|
||||
match.team2_winner_from_match_id,
|
||||
stage_item_x_team_rankings,
|
||||
)
|
||||
|
||||
await sql_update_team_ids_for_match(assert_some(match.id), team1_id, team2_id)
|
||||
|
||||
|
||||
async def get_team_rankings_lookup(tournament_id: TournamentId) -> StageItemXTeamRanking:
|
||||
stages = await get_full_tournament_details(tournament_id)
|
||||
|
||||
stage_items = {
|
||||
stage_item.id: assert_some(await get_stage_item(tournament_id, stage_item.id))
|
||||
for stage in stages
|
||||
for stage_item in stage.stage_items
|
||||
}
|
||||
return {
|
||||
stage_item_id: await determine_team_ranking_for_stage_item(
|
||||
stage_item,
|
||||
assert_some(await get_ranking_for_stage_item(tournament_id, stage_item.id)),
|
||||
)
|
||||
for stage_item_id, stage_item in stage_items.items()
|
||||
}
|
||||
|
||||
|
||||
async def update_matches_in_activated_stage(tournament_id: TournamentId, stage_id: StageId) -> None:
|
||||
[stage] = await get_full_tournament_details(tournament_id, stage_id=stage_id)
|
||||
stage_item_x_team_rankings = await get_team_rankings_lookup(tournament_id)
|
||||
|
||||
for stage_item in stage.stage_items:
|
||||
for round_ in stage_item.rounds:
|
||||
for match in round_.matches:
|
||||
if isinstance(match, MatchWithDetails):
|
||||
await set_team_ids_for_match(tournament_id, match)
|
||||
await set_team_ids_for_match(match, stage_item_x_team_rankings)
|
||||
|
||||
@@ -27,6 +27,7 @@ class Subscription(BaseModel):
|
||||
max_stages: int
|
||||
max_stage_items: int
|
||||
max_rounds: int
|
||||
max_rankings: int
|
||||
|
||||
|
||||
demo_subscription = Subscription(
|
||||
@@ -38,6 +39,7 @@ demo_subscription = Subscription(
|
||||
max_stages=4,
|
||||
max_stage_items=6,
|
||||
max_rounds=6,
|
||||
max_rankings=2,
|
||||
)
|
||||
|
||||
|
||||
@@ -50,6 +52,7 @@ regular_subscription = Subscription(
|
||||
max_stages=16,
|
||||
max_stage_items=64,
|
||||
max_rounds=64,
|
||||
max_rankings=16,
|
||||
)
|
||||
|
||||
subscription_lookup = {
|
||||
|
||||
@@ -2,6 +2,7 @@ import aiofiles.os
|
||||
|
||||
from bracket.sql.courts import sql_delete_courts_of_tournament
|
||||
from bracket.sql.players import sql_delete_players_of_tournament
|
||||
from bracket.sql.rankings import get_all_rankings_in_tournament, sql_delete_ranking
|
||||
from bracket.sql.shared import sql_delete_stage_item_relations
|
||||
from bracket.sql.stage_items import sql_delete_stage_item
|
||||
from bracket.sql.stages import get_full_tournament_details, sql_delete_stage
|
||||
@@ -37,6 +38,9 @@ async def sql_delete_tournament_completely(tournament_id: TournamentId) -> None:
|
||||
|
||||
await sql_delete_stage(tournament_id, assert_some(stage.id))
|
||||
|
||||
for ranking in await get_all_rankings_in_tournament(tournament_id):
|
||||
await sql_delete_ranking(tournament_id, ranking.id)
|
||||
|
||||
await sql_delete_players_of_tournament(tournament_id)
|
||||
await sql_delete_courts_of_tournament(tournament_id)
|
||||
await sql_delete_teams_of_tournament(tournament_id)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
START_ELO: int = 1200
|
||||
|
||||
|
||||
class PlayerStatistics(BaseModel):
|
||||
wins: int = 0
|
||||
draws: int = 0
|
||||
losses: int = 0
|
||||
elo_score: int = START_ELO
|
||||
swiss_score: Decimal = Decimal("0.00")
|
||||
36
backend/bracket/models/db/ranking.py
Normal file
36
backend/bracket/models/db/ranking.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from heliclockter import datetime_utc
|
||||
from pydantic import BaseModel
|
||||
|
||||
from bracket.models.db.shared import BaseModelORM
|
||||
from bracket.utils.id_types import RankingId, TournamentId
|
||||
|
||||
|
||||
class RankingInsertable(BaseModel):
|
||||
tournament_id: TournamentId
|
||||
win_points: Decimal
|
||||
draw_points: Decimal
|
||||
loss_points: Decimal
|
||||
add_score_points: bool
|
||||
position: int
|
||||
|
||||
|
||||
class Ranking(BaseModelORM, RankingInsertable):
|
||||
id: RankingId
|
||||
created: datetime_utc
|
||||
|
||||
|
||||
class RankingBody(BaseModel):
|
||||
win_points: Decimal
|
||||
draw_points: Decimal
|
||||
loss_points: Decimal
|
||||
add_score_points: bool
|
||||
position: int
|
||||
|
||||
|
||||
class RankingCreateBody(BaseModel):
|
||||
win_points: Decimal = Decimal("3.0")
|
||||
draw_points: Decimal = Decimal("1.0")
|
||||
loss_points: Decimal = Decimal("0.0")
|
||||
add_score_points: bool = False
|
||||
@@ -6,7 +6,7 @@ from pydantic import Field, model_validator
|
||||
|
||||
from bracket.models.db.shared import BaseModelORM
|
||||
from bracket.models.db.stage_item_inputs import StageItemInputCreateBody
|
||||
from bracket.utils.id_types import StageId, StageItemId
|
||||
from bracket.utils.id_types import RankingId, StageId, StageItemId
|
||||
from bracket.utils.types import EnumAutoStr
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class StageItemToInsert(BaseModelORM):
|
||||
created: datetime_utc
|
||||
type: StageType
|
||||
team_count: int = Field(ge=2, le=64)
|
||||
ranking_id: RankingId | None = None
|
||||
|
||||
|
||||
class StageItem(StageItemToInsert):
|
||||
@@ -35,6 +36,7 @@ class StageItem(StageItemToInsert):
|
||||
|
||||
class StageItemUpdateBody(BaseModelORM):
|
||||
name: str
|
||||
ranking_id: RankingId
|
||||
|
||||
|
||||
class StageItemActivateNextBody(BaseModelORM):
|
||||
@@ -47,6 +49,7 @@ class StageItemCreateBody(BaseModelORM):
|
||||
type: StageType
|
||||
team_count: int = Field(ge=2, le=64)
|
||||
inputs: list[StageItemInputCreateBody]
|
||||
ranking_id: RankingId | None = None
|
||||
|
||||
def get_name_or_default_name(self) -> str:
|
||||
return self.name if self.name is not None else self.type.value.replace("_", " ").title()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from bracket.models.db.shared import BaseModelORM
|
||||
@@ -16,6 +18,10 @@ class StageItemInputGeneric(BaseModel):
|
||||
winner_from_stage_item_id: StageItemId | None = None
|
||||
winner_position: int | None = None
|
||||
winner_from_match_id: MatchId | None = None
|
||||
points: Decimal = Decimal("0.0")
|
||||
wins: int = 0
|
||||
draws: int = 0
|
||||
losses: int = 0
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return (
|
||||
|
||||
@@ -8,8 +8,8 @@ from typing import Annotated
|
||||
from heliclockter import datetime_utc
|
||||
from pydantic import BaseModel, Field, StringConstraints, field_validator
|
||||
|
||||
from bracket.logic.ranking.statistics import START_ELO
|
||||
from bracket.models.db.player import Player
|
||||
from bracket.models.db.players import START_ELO
|
||||
from bracket.models.db.shared import BaseModelORM
|
||||
from bracket.utils.id_types import PlayerId, TeamId, TournamentId
|
||||
from bracket.utils.types import assert_some
|
||||
@@ -21,7 +21,7 @@ class Team(BaseModelORM):
|
||||
name: str
|
||||
tournament_id: TournamentId
|
||||
active: bool
|
||||
elo_score: Decimal = Decimal(START_ELO)
|
||||
elo_score: Decimal = START_ELO
|
||||
swiss_score: Decimal = Decimal("0.0")
|
||||
wins: int = 0
|
||||
draws: int = 0
|
||||
@@ -32,7 +32,7 @@ class Team(BaseModelORM):
|
||||
class TeamWithPlayers(BaseModel):
|
||||
id: TeamId | None = None
|
||||
players: list[Player]
|
||||
elo_score: Decimal = Decimal(START_ELO)
|
||||
elo_score: Decimal = START_ELO
|
||||
swiss_score: Decimal = Decimal("0.0")
|
||||
wins: int = 0
|
||||
draws: int = 0
|
||||
|
||||
@@ -6,7 +6,9 @@ from bracket.logic.planning.matches import (
|
||||
reorder_matches_for_court,
|
||||
schedule_all_unscheduled_matches,
|
||||
)
|
||||
from bracket.logic.ranking.elo import recalculate_ranking_for_tournament_id
|
||||
from bracket.logic.ranking.elo import (
|
||||
recalculate_ranking_for_stage_item_id,
|
||||
)
|
||||
from bracket.logic.scheduling.upcoming_matches import (
|
||||
get_upcoming_matches_for_swiss_round,
|
||||
)
|
||||
@@ -27,6 +29,7 @@ from bracket.routes.models import SingleMatchResponse, SuccessResponse, Upcoming
|
||||
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_create_match, sql_delete_match, sql_update_match
|
||||
from bracket.sql.rounds import get_round_by_id
|
||||
from bracket.sql.stages import get_full_tournament_details
|
||||
from bracket.sql.tournaments import sql_get_tournament
|
||||
from bracket.sql.validation import check_foreign_keys_belong_to_tournament
|
||||
@@ -70,8 +73,11 @@ async def delete_match(
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
match: Match = Depends(match_dependency),
|
||||
) -> SuccessResponse:
|
||||
round_ = await get_round_by_id(tournament_id, match.round_id)
|
||||
|
||||
await sql_delete_match(assert_some(match.id))
|
||||
await recalculate_ranking_for_tournament_id(tournament_id)
|
||||
|
||||
await recalculate_ranking_for_stage_item_id(tournament_id, assert_some(round_).stage_item_id)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
@@ -178,6 +184,7 @@ async def create_matches_automatically(
|
||||
@router.put("/tournaments/{tournament_id}/matches/{match_id}", response_model=SuccessResponse)
|
||||
async def update_match_by_id(
|
||||
tournament_id: TournamentId,
|
||||
match_id: MatchId,
|
||||
match_body: MatchBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
match: Match = Depends(match_dependency),
|
||||
@@ -185,8 +192,11 @@ async def update_match_by_id(
|
||||
await check_foreign_keys_belong_to_tournament(match_body, tournament_id)
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
|
||||
await sql_update_match(assert_some(match.id), match_body, tournament)
|
||||
await recalculate_ranking_for_tournament_id(tournament_id)
|
||||
await sql_update_match(match_id, match_body, tournament)
|
||||
|
||||
round_ = await get_round_by_id(tournament_id, match.round_id)
|
||||
await recalculate_ranking_for_stage_item_id(tournament_id, assert_some(round_).stage_item_id)
|
||||
|
||||
if (
|
||||
match_body.custom_duration_minutes != match.custom_duration_minutes
|
||||
or match_body.custom_margin_minutes != match.custom_margin_minutes
|
||||
@@ -194,4 +204,5 @@ async def update_match_by_id(
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
scheduled_matches = get_scheduled_matches(await get_full_tournament_details(tournament_id))
|
||||
await reorder_matches_for_court(tournament, scheduled_matches, assert_some(match.court_id))
|
||||
|
||||
return SuccessResponse()
|
||||
|
||||
@@ -2,10 +2,12 @@ from typing import Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from bracket.logic.ranking.statistics import TeamStatistics
|
||||
from bracket.models.db.club import Club
|
||||
from bracket.models.db.court import Court
|
||||
from bracket.models.db.match import Match, SuggestedMatch
|
||||
from bracket.models.db.player import Player
|
||||
from bracket.models.db.ranking import Ranking
|
||||
from bracket.models.db.stage_item_inputs import (
|
||||
StageItemInputOptionFinal,
|
||||
StageItemInputOptionTentative,
|
||||
@@ -15,6 +17,7 @@ from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.models.db.util import StageWithStageItems
|
||||
from bracket.routes.auth import Token
|
||||
from bracket.utils.id_types import StageItemId, TeamId
|
||||
|
||||
DataT = TypeVar("DataT")
|
||||
|
||||
@@ -97,7 +100,15 @@ class SingleCourtResponse(DataResponse[Court]):
|
||||
pass
|
||||
|
||||
|
||||
class RankingsResponse(DataResponse[list[Ranking]]):
|
||||
pass
|
||||
|
||||
|
||||
class StageItemInputOptionsResponse(
|
||||
DataResponse[list[StageItemInputOptionTentative | StageItemInputOptionFinal]]
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class StageRankingResponse(DataResponse[dict[StageItemId, list[tuple[TeamId, TeamStatistics]]]]):
|
||||
pass
|
||||
|
||||
71
backend/bracket/routes/rankings.py
Normal file
71
backend/bracket/routes/rankings.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from bracket.logic.subscriptions import check_requirement
|
||||
from bracket.models.db.ranking import RankingBody, RankingCreateBody
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import (
|
||||
user_authenticated_for_tournament,
|
||||
user_authenticated_or_public_dashboard,
|
||||
)
|
||||
from bracket.routes.models import (
|
||||
RankingsResponse,
|
||||
SuccessResponse,
|
||||
)
|
||||
from bracket.sql.rankings import (
|
||||
get_all_rankings_in_tournament,
|
||||
sql_create_ranking,
|
||||
sql_delete_ranking,
|
||||
sql_update_ranking,
|
||||
)
|
||||
from bracket.utils.id_types import RankingId, TournamentId
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/tournaments/{tournament_id}/rankings")
|
||||
async def get_rankings(
|
||||
tournament_id: TournamentId,
|
||||
_: UserPublic = Depends(user_authenticated_or_public_dashboard),
|
||||
) -> RankingsResponse:
|
||||
return RankingsResponse(data=await get_all_rankings_in_tournament(tournament_id))
|
||||
|
||||
|
||||
@router.put("/tournaments/{tournament_id}/rankings/{ranking_id}")
|
||||
async def update_ranking_by_id(
|
||||
tournament_id: TournamentId,
|
||||
ranking_id: RankingId,
|
||||
ranking_body: RankingBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SuccessResponse:
|
||||
await sql_update_ranking(
|
||||
tournament_id=tournament_id,
|
||||
ranking_id=ranking_id,
|
||||
ranking_body=ranking_body,
|
||||
)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
@router.delete("/tournaments/{tournament_id}/rankings/{ranking_id}")
|
||||
async def delete_ranking(
|
||||
tournament_id: TournamentId,
|
||||
ranking_id: RankingId,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SuccessResponse:
|
||||
await sql_delete_ranking(tournament_id, ranking_id)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
@router.post("/tournaments/{tournament_id}/rankings")
|
||||
async def create_ranking(
|
||||
ranking_body: RankingCreateBody,
|
||||
tournament_id: TournamentId,
|
||||
user: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SuccessResponse:
|
||||
existing_rankings = await get_all_rankings_in_tournament(tournament_id)
|
||||
check_requirement(existing_rankings, user, "max_rankings")
|
||||
|
||||
highest_position = (
|
||||
max(x.position for x in existing_rankings) if len(existing_rankings) > 0 else -1
|
||||
)
|
||||
await sql_create_ranking(tournament_id, ranking_body, highest_position + 1)
|
||||
return SuccessResponse()
|
||||
@@ -2,7 +2,9 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from starlette import status
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.ranking.elo import recalculate_ranking_for_tournament_id
|
||||
from bracket.logic.ranking.elo import (
|
||||
recalculate_ranking_for_stage_item_id,
|
||||
)
|
||||
from bracket.logic.subscriptions import check_requirement
|
||||
from bracket.models.db.round import (
|
||||
Round,
|
||||
@@ -46,7 +48,7 @@ async def delete_round(
|
||||
rounds.c.id == round_id and rounds.c.tournament_id == tournament_id
|
||||
),
|
||||
)
|
||||
await recalculate_ranking_for_tournament_id(tournament_id)
|
||||
await recalculate_ranking_for_stage_item_id(tournament_id, round_with_matches.stage_item_id)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from bracket.logic.planning.rounds import (
|
||||
get_active_and_next_rounds,
|
||||
schedule_all_matches_for_swiss_round,
|
||||
)
|
||||
from bracket.logic.ranking.elo import recalculate_ranking_for_tournament_id
|
||||
from bracket.logic.scheduling.builder import (
|
||||
build_matches_for_stage_item,
|
||||
)
|
||||
@@ -53,7 +52,6 @@ async def delete_stage_item(
|
||||
}
|
||||
):
|
||||
await sql_delete_stage_item_with_foreign_keys(stage_item_id)
|
||||
await recalculate_ranking_for_tournament_id(tournament_id)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from starlette import status
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.ranking.elo import recalculate_ranking_for_tournament_id
|
||||
from bracket.logic.ranking.elo import (
|
||||
determine_team_ranking_for_stage_item,
|
||||
)
|
||||
from bracket.logic.scheduling.builder import determine_available_inputs
|
||||
from bracket.logic.scheduling.handle_stage_activation import update_matches_in_activated_stage
|
||||
from bracket.logic.subscriptions import check_requirement
|
||||
@@ -15,10 +17,13 @@ from bracket.routes.auth import (
|
||||
)
|
||||
from bracket.routes.models import (
|
||||
StageItemInputOptionsResponse,
|
||||
StageRankingResponse,
|
||||
StagesWithStageItemsResponse,
|
||||
SuccessResponse,
|
||||
)
|
||||
from bracket.routes.util import stage_dependency
|
||||
from bracket.sql.rankings import get_ranking_for_stage_item
|
||||
from bracket.sql.stage_items import get_stage_item
|
||||
from bracket.sql.stages import (
|
||||
get_full_tournament_details,
|
||||
get_next_stage_in_tournament,
|
||||
@@ -28,6 +33,7 @@ from bracket.sql.stages import (
|
||||
)
|
||||
from bracket.sql.teams import get_teams_with_members
|
||||
from bracket.utils.id_types import StageId, TournamentId
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -69,7 +75,6 @@ async def delete_stage(
|
||||
|
||||
await sql_delete_stage(tournament_id, stage_id)
|
||||
|
||||
await recalculate_ranking_for_tournament_id(tournament_id)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
@@ -140,3 +145,30 @@ async def get_available_inputs(
|
||||
teams = await get_teams_with_members(tournament_id)
|
||||
available_inputs = determine_available_inputs(stage_id, teams, stages)
|
||||
return StageItemInputOptionsResponse(data=available_inputs)
|
||||
|
||||
|
||||
@router.get("/tournaments/{tournament_id}/stages/{stage_id}/rankings")
|
||||
async def get_rankings(
|
||||
tournament_id: TournamentId,
|
||||
stage_id: StageId,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
stage_without_details: Stage = Depends(stage_dependency),
|
||||
) -> StageRankingResponse:
|
||||
"""
|
||||
Get the rankings for the stage items in this stage.
|
||||
"""
|
||||
[stage] = await get_full_tournament_details(tournament_id, stage_id=stage_id)
|
||||
|
||||
stage_items = {
|
||||
stage_item.id: assert_some(await get_stage_item(tournament_id, stage_item.id))
|
||||
for stage_item in stage.stage_items
|
||||
}
|
||||
stage_item_x_ranking = {
|
||||
stage_item_id: await determine_team_ranking_for_stage_item(
|
||||
stage_item,
|
||||
assert_some(await get_ranking_for_stage_item(tournament_id, stage_item.id)),
|
||||
)
|
||||
for stage_item_id, stage_item in stage_items.items()
|
||||
}
|
||||
|
||||
return StageRankingResponse(data=stage_item_x_ranking)
|
||||
|
||||
@@ -7,7 +7,6 @@ from fastapi import APIRouter, Depends, UploadFile
|
||||
from heliclockter import datetime_utc
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.ranking.elo import recalculate_ranking_for_tournament_id
|
||||
from bracket.logic.subscriptions import check_requirement
|
||||
from bracket.logic.teams import get_team_logo_path
|
||||
from bracket.models.db.team import FullTeamWithPlayers, Team, TeamBody, TeamMultiBody, TeamToInsert
|
||||
@@ -61,7 +60,6 @@ async def update_team_members(
|
||||
& (players_x_teams.c.team_id == team_id)
|
||||
),
|
||||
)
|
||||
await recalculate_ranking_for_tournament_id(tournament_id)
|
||||
|
||||
|
||||
@router.get("/tournaments/{tournament_id}/teams", response_model=TeamsWithPlayersResponse)
|
||||
@@ -94,7 +92,6 @@ async def update_team_by_id(
|
||||
values=team_body.model_dump(exclude={"player_ids"}),
|
||||
)
|
||||
await update_team_members(assert_some(team.id), tournament_id, team_body.player_ids)
|
||||
await recalculate_ranking_for_tournament_id(tournament_id)
|
||||
|
||||
return SingleTeamResponse(
|
||||
data=assert_some(
|
||||
@@ -162,7 +159,6 @@ async def delete_team(
|
||||
):
|
||||
await sql_delete_team(tournament_id, assert_some(team.id))
|
||||
|
||||
await recalculate_ranking_for_tournament_id(tournament_id)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from bracket.database import database
|
||||
from bracket.logic.planning.matches import update_start_times_of_matches
|
||||
from bracket.logic.subscriptions import check_requirement
|
||||
from bracket.logic.tournaments import get_tournament_logo_path
|
||||
from bracket.models.db.ranking import RankingCreateBody
|
||||
from bracket.models.db.tournament import (
|
||||
TournamentBody,
|
||||
TournamentUpdateBody,
|
||||
@@ -23,6 +24,7 @@ from bracket.routes.auth import (
|
||||
)
|
||||
from bracket.routes.models import SuccessResponse, TournamentResponse, TournamentsResponse
|
||||
from bracket.schema import tournaments
|
||||
from bracket.sql.rankings import sql_create_ranking
|
||||
from bracket.sql.tournaments import (
|
||||
sql_create_tournament,
|
||||
sql_delete_tournament,
|
||||
@@ -139,10 +141,14 @@ async def create_tournament(
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
await sql_create_tournament(tournament_to_insert)
|
||||
except asyncpg.exceptions.UniqueViolationError as exc:
|
||||
check_unique_constraint_violation(exc, {UniqueIndex.ix_tournaments_dashboard_endpoint})
|
||||
async with database.transaction():
|
||||
try:
|
||||
tournament_id = await sql_create_tournament(tournament_to_insert)
|
||||
except asyncpg.exceptions.UniqueViolationError as exc:
|
||||
check_unique_constraint_violation(exc, {UniqueIndex.ix_tournaments_dashboard_endpoint})
|
||||
|
||||
ranking = RankingCreateBody()
|
||||
await sql_create_ranking(tournament_id, ranking, position=0)
|
||||
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ stage_items = Table(
|
||||
Column("created", DateTimeTZ, nullable=False, server_default=func.now()),
|
||||
Column("stage_id", BigInteger, ForeignKey("stages.id"), index=True, nullable=False),
|
||||
Column("team_count", Integer, nullable=False),
|
||||
Column("ranking_id", BigInteger, ForeignKey("rankings.id"), nullable=False),
|
||||
Column(
|
||||
"type",
|
||||
Enum(
|
||||
@@ -77,6 +78,10 @@ stage_item_inputs = Table(
|
||||
Column("team_id", BigInteger, ForeignKey("teams.id"), nullable=True),
|
||||
Column("winner_from_stage_item_id", BigInteger, ForeignKey("stage_items.id"), nullable=True),
|
||||
Column("winner_position", Integer, nullable=True),
|
||||
Column("points", Float, nullable=False, server_default="0"),
|
||||
Column("wins", Integer, nullable=False, server_default="0"),
|
||||
Column("draws", Integer, nullable=False, server_default="0"),
|
||||
Column("losses", Integer, nullable=False, server_default="0"),
|
||||
)
|
||||
|
||||
rounds = Table(
|
||||
@@ -204,3 +209,16 @@ courts = Table(
|
||||
Column("created", DateTimeTZ, nullable=False, server_default=func.now()),
|
||||
Column("tournament_id", BigInteger, ForeignKey("tournaments.id"), nullable=False, index=True),
|
||||
)
|
||||
|
||||
rankings = Table(
|
||||
"rankings",
|
||||
metadata,
|
||||
Column("id", BigInteger, primary_key=True, index=True),
|
||||
Column("created", DateTimeTZ, nullable=False, server_default=func.now()),
|
||||
Column("tournament_id", BigInteger, ForeignKey("tournaments.id"), nullable=False, index=True),
|
||||
Column("position", Integer, nullable=False),
|
||||
Column("win_points", Float, nullable=False),
|
||||
Column("draw_points", Float, nullable=False),
|
||||
Column("loss_points", Float, nullable=False),
|
||||
Column("add_score_points", Boolean, nullable=False),
|
||||
)
|
||||
|
||||
@@ -4,8 +4,8 @@ from typing import cast
|
||||
from heliclockter import datetime_utc
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.ranking.statistics import START_ELO
|
||||
from bracket.models.db.player import Player, PlayerBody, PlayerToInsert
|
||||
from bracket.models.db.players import START_ELO, PlayerStatistics
|
||||
from bracket.schema import players
|
||||
from bracket.utils.id_types import PlayerId, TournamentId
|
||||
from bracket.utils.pagination import PaginationPlayers
|
||||
@@ -77,34 +77,6 @@ async def get_player_count(
|
||||
return cast(int, await database.fetch_val(query=query, values={"tournament_id": tournament_id}))
|
||||
|
||||
|
||||
async def update_player_stats(
|
||||
tournament_id: TournamentId, player_id: PlayerId, player_statistics: PlayerStatistics
|
||||
) -> None:
|
||||
query = """
|
||||
UPDATE players
|
||||
SET
|
||||
wins = :wins,
|
||||
draws = :draws,
|
||||
losses = :losses,
|
||||
elo_score = :elo_score,
|
||||
swiss_score = :swiss_score
|
||||
WHERE players.tournament_id = :tournament_id
|
||||
AND players.id = :player_id
|
||||
"""
|
||||
await database.execute(
|
||||
query=query,
|
||||
values={
|
||||
"tournament_id": tournament_id,
|
||||
"player_id": player_id,
|
||||
"wins": player_statistics.wins,
|
||||
"draws": player_statistics.draws,
|
||||
"losses": player_statistics.losses,
|
||||
"elo_score": player_statistics.elo_score,
|
||||
"swiss_score": float(player_statistics.swiss_score),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def sql_delete_player(tournament_id: TournamentId, player_id: PlayerId) -> None:
|
||||
query = "DELETE FROM players WHERE id = :player_id AND tournament_id = :tournament_id"
|
||||
await database.fetch_one(
|
||||
@@ -124,7 +96,7 @@ async def insert_player(player_body: PlayerBody, tournament_id: TournamentId) ->
|
||||
**player_body.model_dump(),
|
||||
created=datetime_utc.now(),
|
||||
tournament_id=tournament_id,
|
||||
elo_score=Decimal(START_ELO),
|
||||
elo_score=START_ELO,
|
||||
swiss_score=Decimal("0.0"),
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
107
backend/bracket/sql/rankings.py
Normal file
107
backend/bracket/sql/rankings.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from bracket.database import database
|
||||
from bracket.models.db.ranking import Ranking, RankingBody, RankingCreateBody
|
||||
from bracket.utils.id_types import RankingId, StageItemId, TournamentId
|
||||
|
||||
|
||||
async def get_all_rankings_in_tournament(tournament_id: TournamentId) -> list[Ranking]:
|
||||
query = """
|
||||
SELECT *
|
||||
FROM rankings
|
||||
WHERE rankings.tournament_id = :tournament_id
|
||||
ORDER BY position
|
||||
"""
|
||||
result = await database.fetch_all(query=query, values={"tournament_id": tournament_id})
|
||||
return [Ranking.model_validate(dict(x._mapping)) for x in result]
|
||||
|
||||
|
||||
async def get_default_rankings_in_tournament(tournament_id: TournamentId) -> Ranking:
|
||||
query = """
|
||||
SELECT *
|
||||
FROM rankings
|
||||
WHERE rankings.tournament_id = :tournament_id
|
||||
ORDER BY position
|
||||
LIMIT 1
|
||||
"""
|
||||
result = await database.fetch_one(query=query, values={"tournament_id": tournament_id})
|
||||
assert result is not None, "No default ranking found"
|
||||
return Ranking.model_validate(dict(result._mapping))
|
||||
|
||||
|
||||
async def get_ranking_for_stage_item(
|
||||
tournament_id: TournamentId, stage_item_id: StageItemId
|
||||
) -> Ranking | None:
|
||||
query = """
|
||||
SELECT rankings.*
|
||||
FROM rankings
|
||||
JOIN stage_items ON stage_items.ranking_id = rankings.id
|
||||
WHERE rankings.tournament_id = :tournament_id
|
||||
AND stage_items.id = :stage_item_id
|
||||
"""
|
||||
result = await database.fetch_one(
|
||||
query=query, values={"tournament_id": tournament_id, "stage_item_id": stage_item_id}
|
||||
)
|
||||
return Ranking.model_validate(dict(result._mapping)) if result else None
|
||||
|
||||
|
||||
async def sql_update_ranking(
|
||||
tournament_id: TournamentId, ranking_id: RankingId, ranking_body: RankingBody
|
||||
) -> list[Ranking]:
|
||||
query = """
|
||||
UPDATE rankings
|
||||
SET position = :position,
|
||||
win_points = :win_points,
|
||||
draw_points = :draw_points,
|
||||
loss_points = :loss_points,
|
||||
add_score_points = :add_score_points
|
||||
WHERE rankings.tournament_id = :tournament_id
|
||||
AND rankings.id = :ranking_id
|
||||
"""
|
||||
result = await database.fetch_all(
|
||||
query=query,
|
||||
values={
|
||||
"ranking_id": ranking_id,
|
||||
"tournament_id": tournament_id,
|
||||
"win_points": float(ranking_body.win_points),
|
||||
"draw_points": float(ranking_body.draw_points),
|
||||
"loss_points": float(ranking_body.loss_points),
|
||||
"add_score_points": ranking_body.add_score_points,
|
||||
"position": ranking_body.position,
|
||||
},
|
||||
)
|
||||
return [Ranking.model_validate(dict(x._mapping)) for x in result]
|
||||
|
||||
|
||||
async def sql_delete_ranking(tournament_id: TournamentId, ranking_id: RankingId) -> None:
|
||||
query = "DELETE FROM rankings WHERE id = :ranking_id AND tournament_id = :tournament_id"
|
||||
await database.fetch_one(
|
||||
query=query, values={"ranking_id": ranking_id, "tournament_id": tournament_id}
|
||||
)
|
||||
|
||||
|
||||
async def sql_create_ranking(
|
||||
tournament_id: TournamentId, ranking_body: RankingCreateBody, position: int
|
||||
) -> None:
|
||||
query = """
|
||||
INSERT INTO rankings
|
||||
(tournament_id, position, win_points, draw_points, loss_points, add_score_points)
|
||||
VALUES (
|
||||
:tournament_id,
|
||||
:position,
|
||||
:win_points,
|
||||
:draw_points,
|
||||
:loss_points,
|
||||
:add_score_points
|
||||
)
|
||||
"""
|
||||
|
||||
await database.execute(
|
||||
query=query,
|
||||
values={
|
||||
"tournament_id": tournament_id,
|
||||
"win_points": float(ranking_body.win_points),
|
||||
"draw_points": float(ranking_body.draw_points),
|
||||
"loss_points": float(ranking_body.loss_points),
|
||||
"add_score_points": ranking_body.add_score_points,
|
||||
"position": position,
|
||||
},
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
from bracket.database import database
|
||||
from bracket.models.db.stage_item import StageItem, StageItemCreateBody
|
||||
from bracket.models.db.util import StageItemWithRounds
|
||||
from bracket.sql.rankings import get_default_rankings_in_tournament
|
||||
from bracket.sql.stage_item_inputs import sql_create_stage_item_input
|
||||
from bracket.sql.stages import get_full_tournament_details
|
||||
from bracket.utils.id_types import StageItemId, TournamentId
|
||||
@@ -11,8 +12,8 @@ async def sql_create_stage_item(
|
||||
) -> StageItem:
|
||||
async with database.transaction():
|
||||
query = """
|
||||
INSERT INTO stage_items (type, stage_id, name, team_count)
|
||||
VALUES (:stage_item_type, :stage_id, :name, :team_count)
|
||||
INSERT INTO stage_items (type, stage_id, name, team_count, ranking_id)
|
||||
VALUES (:stage_item_type, :stage_id, :name, :team_count, :ranking_id)
|
||||
RETURNING *
|
||||
"""
|
||||
result = await database.fetch_one(
|
||||
@@ -22,6 +23,9 @@ async def sql_create_stage_item(
|
||||
"stage_id": stage_item.stage_id,
|
||||
"name": stage_item.get_name_or_default_name(),
|
||||
"team_count": stage_item.team_count,
|
||||
"ranking_id": stage_item.ranking_id
|
||||
if stage_item.ranking_id
|
||||
else (await get_default_rankings_in_tournament(tournament_id)).id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import cast
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.players import PlayerStatistics
|
||||
from bracket.logic.ranking.statistics import TeamStatistics
|
||||
from bracket.models.db.team import FullTeamWithPlayers, Team
|
||||
from bracket.utils.id_types import TeamId, TournamentId
|
||||
from bracket.utils.id_types import StageItemInputId, TeamId, TournamentId
|
||||
from bracket.utils.pagination import PaginationTeams
|
||||
from bracket.utils.types import dict_without_none
|
||||
|
||||
@@ -74,24 +74,6 @@ async def get_teams_with_members(
|
||||
return [FullTeamWithPlayers.model_validate(x) for x in result]
|
||||
|
||||
|
||||
async def get_teams_in_tournament(
|
||||
tournament_id: TournamentId,
|
||||
) -> list[Team]:
|
||||
query = """
|
||||
SELECT *
|
||||
FROM teams
|
||||
WHERE teams.tournament_id = :tournament_id
|
||||
ORDER BY name ASC
|
||||
"""
|
||||
values = dict_without_none(
|
||||
{
|
||||
"tournament_id": tournament_id,
|
||||
}
|
||||
)
|
||||
result = await database.fetch_all(query=query, values=values)
|
||||
return [Team.model_validate(x) for x in result]
|
||||
|
||||
|
||||
async def get_team_count(
|
||||
tournament_id: TournamentId,
|
||||
*,
|
||||
@@ -109,29 +91,29 @@ async def get_team_count(
|
||||
|
||||
|
||||
async def update_team_stats(
|
||||
tournament_id: TournamentId, team_id: TeamId, team_statistics: PlayerStatistics
|
||||
tournament_id: TournamentId,
|
||||
stage_item_input_id: StageItemInputId,
|
||||
team_statistics: TeamStatistics,
|
||||
) -> None:
|
||||
query = """
|
||||
UPDATE teams
|
||||
UPDATE stage_item_inputs
|
||||
SET
|
||||
wins = :wins,
|
||||
draws = :draws,
|
||||
losses = :losses,
|
||||
elo_score = :elo_score,
|
||||
swiss_score = :swiss_score
|
||||
WHERE teams.tournament_id = :tournament_id
|
||||
AND teams.id = :team_id
|
||||
points = :points
|
||||
WHERE stage_item_inputs.tournament_id = :tournament_id
|
||||
AND stage_item_inputs.id = :stage_item_input_id
|
||||
"""
|
||||
await database.execute(
|
||||
query=query,
|
||||
values={
|
||||
"tournament_id": tournament_id,
|
||||
"team_id": team_id,
|
||||
"stage_item_input_id": stage_item_input_id,
|
||||
"wins": team_statistics.wins,
|
||||
"draws": team_statistics.draws,
|
||||
"losses": team_statistics.losses,
|
||||
"elo_score": team_statistics.elo_score,
|
||||
"swiss_score": float(team_statistics.swiss_score),
|
||||
"points": float(team_statistics.points),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ async def sql_update_tournament(
|
||||
)
|
||||
|
||||
|
||||
async def sql_create_tournament(tournament: TournamentBody) -> None:
|
||||
async def sql_create_tournament(tournament: TournamentBody) -> TournamentId:
|
||||
query = """
|
||||
INSERT INTO tournaments (
|
||||
name,
|
||||
@@ -102,8 +102,7 @@ async def sql_create_tournament(tournament: TournamentBody) -> None:
|
||||
:duration_minutes,
|
||||
:margin_minutes
|
||||
)
|
||||
RETURNING id
|
||||
"""
|
||||
await database.execute(
|
||||
query=query,
|
||||
values=tournament.model_dump(),
|
||||
)
|
||||
new_id = await database.fetch_val(query=query, values=tournament.model_dump())
|
||||
return TournamentId(new_id)
|
||||
|
||||
@@ -5,7 +5,9 @@ from heliclockter import datetime_utc
|
||||
|
||||
from bracket.config import Environment, config, environment
|
||||
from bracket.database import database, engine
|
||||
from bracket.logic.ranking.elo import recalculate_ranking_for_tournament_id
|
||||
from bracket.logic.ranking.elo import (
|
||||
recalculate_ranking_for_stage_item_id,
|
||||
)
|
||||
from bracket.logic.scheduling.builder import build_matches_for_stage_item
|
||||
from bracket.models.db.account import UserAccountType
|
||||
from bracket.models.db.club import Club
|
||||
@@ -13,6 +15,7 @@ from bracket.models.db.court import Court
|
||||
from bracket.models.db.match import Match, MatchBody
|
||||
from bracket.models.db.player import Player
|
||||
from bracket.models.db.player_x_team import PlayerXTeam
|
||||
from bracket.models.db.ranking import RankingInsertable
|
||||
from bracket.models.db.round import Round
|
||||
from bracket.models.db.stage import Stage
|
||||
from bracket.models.db.stage_item import StageItem, StageItemCreateBody
|
||||
@@ -31,6 +34,7 @@ from bracket.schema import (
|
||||
metadata,
|
||||
players,
|
||||
players_x_teams,
|
||||
rankings,
|
||||
rounds,
|
||||
stage_items,
|
||||
stages,
|
||||
@@ -59,6 +63,7 @@ from bracket.utils.dummy_records import (
|
||||
DUMMY_PLAYER7,
|
||||
DUMMY_PLAYER8,
|
||||
DUMMY_PLAYER_X_TEAM,
|
||||
DUMMY_RANKING1,
|
||||
DUMMY_STAGE1,
|
||||
DUMMY_STAGE2,
|
||||
DUMMY_STAGE_ITEM1,
|
||||
@@ -76,6 +81,7 @@ from bracket.utils.id_types import (
|
||||
CourtId,
|
||||
PlayerId,
|
||||
PlayerXTeamId,
|
||||
RankingId,
|
||||
StageId,
|
||||
TeamId,
|
||||
TournamentId,
|
||||
@@ -150,6 +156,7 @@ async def sql_create_dev_db() -> UserId:
|
||||
Tournament: tournaments,
|
||||
Court: courts,
|
||||
StageItem: stage_items,
|
||||
RankingInsertable: rankings,
|
||||
}
|
||||
|
||||
async def insert_dummy(
|
||||
@@ -180,6 +187,8 @@ async def sql_create_dev_db() -> UserId:
|
||||
stage_id_1 = await insert_dummy(DUMMY_STAGE1, StageId, {"tournament_id": tournament_id_1})
|
||||
stage_id_2 = await insert_dummy(DUMMY_STAGE2, StageId, {"tournament_id": tournament_id_1})
|
||||
|
||||
await insert_dummy(DUMMY_RANKING1, RankingId, {"tournament_id": tournament_id_1})
|
||||
|
||||
team_id_1 = await insert_dummy(DUMMY_TEAM1, TeamId, {"tournament_id": tournament_id_1})
|
||||
team_id_2 = await insert_dummy(DUMMY_TEAM2, TeamId, {"tournament_id": tournament_id_1})
|
||||
team_id_3 = await insert_dummy(DUMMY_TEAM3, TeamId, {"tournament_id": tournament_id_1})
|
||||
@@ -391,7 +400,7 @@ async def sql_create_dev_db() -> UserId:
|
||||
tournament=tournament_details,
|
||||
)
|
||||
|
||||
for tournament in await database.fetch_all(tournaments.select()):
|
||||
await recalculate_ranking_for_tournament_id(tournament.id) # type: ignore[attr-defined]
|
||||
for _stage_item in (stage_item_1, stage_item_2, stage_item_3):
|
||||
await recalculate_ranking_for_stage_item_id(tournament_id_1, _stage_item.id)
|
||||
|
||||
return user_id_1
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from decimal import Decimal
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from heliclockter import datetime_utc
|
||||
@@ -8,6 +9,7 @@ from bracket.models.db.court import Court
|
||||
from bracket.models.db.match import Match
|
||||
from bracket.models.db.player import Player
|
||||
from bracket.models.db.player_x_team import PlayerXTeam
|
||||
from bracket.models.db.ranking import RankingInsertable
|
||||
from bracket.models.db.round import Round
|
||||
from bracket.models.db.stage import Stage
|
||||
from bracket.models.db.stage_item import StageItemToInsert, StageType
|
||||
@@ -18,6 +20,7 @@ from bracket.utils.id_types import (
|
||||
ClubId,
|
||||
CourtId,
|
||||
PlayerId,
|
||||
RankingId,
|
||||
RoundId,
|
||||
StageId,
|
||||
StageItemId,
|
||||
@@ -66,6 +69,7 @@ DUMMY_STAGE2 = Stage(
|
||||
|
||||
DUMMY_STAGE_ITEM1 = StageItemToInsert(
|
||||
stage_id=StageId(DB_PLACEHOLDER_ID),
|
||||
ranking_id=RankingId(DB_PLACEHOLDER_ID),
|
||||
created=DUMMY_MOCK_TIME,
|
||||
type=StageType.ROUND_ROBIN,
|
||||
team_count=4,
|
||||
@@ -74,6 +78,7 @@ DUMMY_STAGE_ITEM1 = StageItemToInsert(
|
||||
|
||||
DUMMY_STAGE_ITEM2 = StageItemToInsert(
|
||||
stage_id=StageId(DB_PLACEHOLDER_ID),
|
||||
ranking_id=RankingId(DB_PLACEHOLDER_ID),
|
||||
created=DUMMY_MOCK_TIME,
|
||||
type=StageType.ROUND_ROBIN,
|
||||
team_count=4,
|
||||
@@ -82,6 +87,7 @@ DUMMY_STAGE_ITEM2 = StageItemToInsert(
|
||||
|
||||
DUMMY_STAGE_ITEM3 = StageItemToInsert(
|
||||
stage_id=StageId(DB_PLACEHOLDER_ID),
|
||||
ranking_id=RankingId(DB_PLACEHOLDER_ID),
|
||||
created=DUMMY_MOCK_TIME,
|
||||
type=StageType.SINGLE_ELIMINATION,
|
||||
team_count=4,
|
||||
@@ -240,3 +246,12 @@ DUMMY_COURT2 = Court(
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=TournamentId(DB_PLACEHOLDER_ID),
|
||||
)
|
||||
|
||||
DUMMY_RANKING1 = RankingInsertable(
|
||||
tournament_id=TournamentId(DB_PLACEHOLDER_ID),
|
||||
win_points=Decimal("1.0"),
|
||||
draw_points=Decimal("0.5"),
|
||||
loss_points=Decimal("0.0"),
|
||||
add_score_points=False,
|
||||
position=0,
|
||||
)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from typing import NewType
|
||||
|
||||
ClubId = NewType("ClubId", int)
|
||||
TournamentId = NewType("TournamentId", int)
|
||||
CourtId = NewType("CourtId", int)
|
||||
MatchId = NewType("MatchId", int)
|
||||
PlayerId = NewType("PlayerId", int)
|
||||
PlayerXTeamId = NewType("PlayerXTeamId", int)
|
||||
RankingId = NewType("RankingId", int)
|
||||
RoundId = NewType("RoundId", int)
|
||||
StageId = NewType("StageId", int)
|
||||
StageItemId = NewType("StageItemId", int)
|
||||
StageItemInputId = NewType("StageItemInputId", int)
|
||||
MatchId = NewType("MatchId", int)
|
||||
RoundId = NewType("RoundId", int)
|
||||
TeamId = NewType("TeamId", int)
|
||||
PlayerId = NewType("PlayerId", int)
|
||||
CourtId = NewType("CourtId", int)
|
||||
TournamentId = NewType("TournamentId", int)
|
||||
UserId = NewType("UserId", int)
|
||||
PlayerXTeamId = NewType("PlayerXTeamId", int)
|
||||
UserXClubId = NewType("UserXClubId", int)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -evo pipefail
|
||||
|
||||
ruff format .
|
||||
ruff --fix .
|
||||
ruff check --fix .
|
||||
! vulture | grep "unused function\|unused class\|unused method"
|
||||
dmypy run -- --follow-imports=normal --junit-xml= .
|
||||
ENVIRONMENT=CI pytest --cov --cov-report=xml . -vvv
|
||||
|
||||
@@ -28,7 +28,11 @@ async def test_available_inputs(
|
||||
# inserted_stage(
|
||||
# DUMMY_STAGE2.model_copy(update={'tournament_id': auth_context.tournament.id})
|
||||
# ) as stage_inserted_2,
|
||||
inserted_stage_item(DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted_1.id})),
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted_1.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
),
|
||||
):
|
||||
response = await send_tournament_request(
|
||||
HTTPMethod.GET, f"stages/{stage_inserted_1.id}/available_inputs", auth_context
|
||||
|
||||
@@ -42,7 +42,9 @@ async def test_create_match(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
inserted_round(
|
||||
DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id})
|
||||
@@ -79,7 +81,9 @@ async def test_delete_match(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
inserted_round(
|
||||
DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id})
|
||||
@@ -121,7 +125,9 @@ async def test_update_match(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
inserted_round(
|
||||
DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id})
|
||||
@@ -182,7 +188,9 @@ async def test_update_endpoint_custom_duration_margin(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
inserted_round(
|
||||
DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id})
|
||||
@@ -249,7 +257,11 @@ async def test_upcoming_matches_endpoint(
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "type": StageType.SWISS}
|
||||
update={
|
||||
"stage_id": stage_inserted.id,
|
||||
"type": StageType.SWISS,
|
||||
"ranking_id": auth_context.ranking.id,
|
||||
}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
inserted_round(
|
||||
|
||||
96
backend/tests/integration_tests/api/rankings_test.py
Normal file
96
backend/tests/integration_tests/api/rankings_test.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from decimal import Decimal
|
||||
from unittest.mock import ANY
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.ranking import Ranking
|
||||
from bracket.schema import rankings
|
||||
from bracket.sql.rankings import (
|
||||
get_all_rankings_in_tournament,
|
||||
sql_delete_ranking,
|
||||
)
|
||||
from bracket.utils.db import fetch_one_parsed_certain
|
||||
from bracket.utils.dummy_records import DUMMY_RANKING1, DUMMY_TEAM1
|
||||
from bracket.utils.http import HTTPMethod
|
||||
from bracket.utils.types import assert_some
|
||||
from tests.integration_tests.api.shared import SUCCESS_RESPONSE, send_tournament_request
|
||||
from tests.integration_tests.models import AuthContext
|
||||
from tests.integration_tests.sql import inserted_ranking, inserted_team
|
||||
|
||||
|
||||
async def test_rankings_endpoint(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
async with inserted_team(
|
||||
DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
):
|
||||
assert await send_tournament_request(HTTPMethod.GET, "rankings", auth_context, {}) == {
|
||||
"data": [
|
||||
{
|
||||
"created": ANY,
|
||||
"id": auth_context.ranking.id,
|
||||
"position": 0,
|
||||
"win_points": "1.0",
|
||||
"draw_points": "0.5",
|
||||
"loss_points": "0.0",
|
||||
"add_score_points": False,
|
||||
"tournament_id": auth_context.tournament.id,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def test_create_ranking(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
response = await send_tournament_request(HTTPMethod.POST, "rankings", auth_context, json={})
|
||||
assert response.get("success") is True, response
|
||||
|
||||
tournament_id = assert_some(auth_context.tournament.id)
|
||||
for ranking in await get_all_rankings_in_tournament(tournament_id):
|
||||
if ranking.position != 0:
|
||||
await sql_delete_ranking(tournament_id, ranking.id)
|
||||
|
||||
|
||||
async def test_delete_ranking(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
async with inserted_team(
|
||||
DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
):
|
||||
async with inserted_ranking(
|
||||
DUMMY_RANKING1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as ranking_inserted:
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
HTTPMethod.DELETE, f"rankings/{ranking_inserted.id}", auth_context
|
||||
)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
|
||||
|
||||
async def test_update_ranking(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
body = {
|
||||
"win_points": "7.5",
|
||||
"draw_points": "2.5",
|
||||
"loss_points": "6.0",
|
||||
"add_score_points": True,
|
||||
"position": 42,
|
||||
}
|
||||
async with inserted_team(
|
||||
DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
):
|
||||
async with inserted_ranking(
|
||||
DUMMY_RANKING1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as ranking_inserted:
|
||||
response = await send_tournament_request(
|
||||
HTTPMethod.PUT, f"rankings/{ranking_inserted.id}", auth_context, json=body
|
||||
)
|
||||
updated_ranking = await fetch_one_parsed_certain(
|
||||
database,
|
||||
Ranking,
|
||||
query=rankings.select().where(rankings.c.id == ranking_inserted.id),
|
||||
)
|
||||
assert response["success"] is True
|
||||
assert updated_ranking.win_points == Decimal("7.5")
|
||||
@@ -34,7 +34,9 @@ async def test_reschedule_match(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
inserted_round(
|
||||
DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id})
|
||||
|
||||
@@ -26,7 +26,11 @@ async def test_create_round(
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "type": StageType.SWISS}
|
||||
update={
|
||||
"stage_id": stage_inserted.id,
|
||||
"type": StageType.SWISS,
|
||||
"ranking_id": auth_context.ranking.id,
|
||||
}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
):
|
||||
@@ -51,7 +55,9 @@ async def test_delete_round(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
inserted_round(
|
||||
DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id})
|
||||
@@ -76,7 +82,9 @@ async def test_update_round(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
inserted_round(
|
||||
DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id})
|
||||
|
||||
@@ -70,7 +70,9 @@ async def test_delete_stage_item(
|
||||
DUMMY_STAGE2.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted_1,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted_1.id})
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted_1.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
):
|
||||
assert (
|
||||
@@ -85,13 +87,15 @@ async def test_delete_stage_item(
|
||||
async def test_update_stage_item(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
body = {"name": "Optimus"}
|
||||
body = {"name": "Optimus", "ranking_id": auth_context.ranking.id}
|
||||
async with (
|
||||
inserted_stage(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
):
|
||||
assert (
|
||||
|
||||
@@ -37,7 +37,9 @@ async def test_stages_endpoint(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
inserted_round(
|
||||
DUMMY_ROUND1.model_copy(update={"stage_item_id": stage_item_inserted.id})
|
||||
@@ -62,6 +64,7 @@ async def test_stages_endpoint(
|
||||
{
|
||||
"id": stage_item_inserted.id,
|
||||
"stage_id": stage_inserted.id,
|
||||
"ranking_id": auth_context.ranking.id,
|
||||
"name": "Group A",
|
||||
"created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"),
|
||||
"type": "ROUND_ROBIN",
|
||||
@@ -132,7 +135,11 @@ async def test_update_stage(
|
||||
inserted_stage(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(DUMMY_STAGE_ITEM1.model_copy(update={"stage_id": stage_inserted.id})),
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
),
|
||||
):
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
@@ -174,3 +181,24 @@ async def test_activate_stage(
|
||||
|
||||
await assert_row_count_and_clear(stage_items, 1)
|
||||
await assert_row_count_and_clear(stages, 1)
|
||||
|
||||
|
||||
async def test_get_rankings(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
async with (
|
||||
inserted_team(DUMMY_TEAM1.model_copy(update={"tournament_id": auth_context.tournament.id})),
|
||||
inserted_stage(
|
||||
DUMMY_STAGE1.model_copy(update={"tournament_id": auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
inserted_stage_item(
|
||||
DUMMY_STAGE_ITEM1.model_copy(
|
||||
update={"stage_id": stage_inserted.id, "ranking_id": auth_context.ranking.id}
|
||||
)
|
||||
) as stage_item_inserted,
|
||||
):
|
||||
response = await send_tournament_request(
|
||||
HTTPMethod.GET, f"stages/{stage_inserted.id}/rankings", auth_context
|
||||
)
|
||||
|
||||
assert response == {"data": {f"{stage_item_inserted.id}": []}}
|
||||
|
||||
@@ -3,9 +3,10 @@ import aiofiles.os
|
||||
import aiohttp
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.tournaments import sql_delete_tournament_completely
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.schema import tournaments
|
||||
from bracket.sql.tournaments import sql_delete_tournament
|
||||
from bracket.sql.tournaments import sql_delete_tournament, sql_get_tournament_by_endpoint_name
|
||||
from bracket.utils.db import fetch_one_parsed_certain
|
||||
from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_TOURNAMENT
|
||||
from bracket.utils.http import HTTPMethod
|
||||
@@ -68,11 +69,13 @@ async def test_tournament_endpoint(
|
||||
async def test_create_tournament(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
dashboard_endpoint = "some-new-endpoint"
|
||||
body = {
|
||||
"name": "Some new name",
|
||||
"start_time": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"),
|
||||
"club_id": auth_context.club.id,
|
||||
"dashboard_public": False,
|
||||
"dashboard_public": True,
|
||||
"dashboard_endpoint": dashboard_endpoint,
|
||||
"players_can_be_in_multiple_teams": True,
|
||||
"auto_assign_courts": True,
|
||||
"duration_minutes": 12,
|
||||
@@ -82,7 +85,10 @@ async def test_create_tournament(
|
||||
await send_auth_request(HTTPMethod.POST, "tournaments", auth_context, json=body)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
await database.execute(tournaments.delete().where(tournaments.c.name == body["name"]))
|
||||
|
||||
# Cleanup
|
||||
tournament = assert_some(await sql_get_tournament_by_endpoint_name(dashboard_endpoint))
|
||||
await sql_delete_tournament_completely(assert_some(tournament.id))
|
||||
|
||||
|
||||
async def test_update_tournament(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from bracket.models.db.club import Club
|
||||
from bracket.models.db.ranking import Ranking
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import User
|
||||
from bracket.models.db.user_x_club import UserXClub
|
||||
@@ -12,3 +13,4 @@ class AuthContext(BaseModel):
|
||||
user: User
|
||||
user_x_club: UserXClub
|
||||
headers: dict[str, str]
|
||||
ranking: Ranking
|
||||
|
||||
@@ -10,6 +10,7 @@ from bracket.models.db.court import Court
|
||||
from bracket.models.db.match import Match
|
||||
from bracket.models.db.player import Player
|
||||
from bracket.models.db.player_x_team import PlayerXTeam
|
||||
from bracket.models.db.ranking import Ranking, RankingInsertable
|
||||
from bracket.models.db.round import Round
|
||||
from bracket.models.db.stage import Stage
|
||||
from bracket.models.db.stage_item import StageItem, StageItemToInsert
|
||||
@@ -23,6 +24,7 @@ from bracket.schema import (
|
||||
matches,
|
||||
players,
|
||||
players_x_teams,
|
||||
rankings,
|
||||
rounds,
|
||||
stage_items,
|
||||
stages,
|
||||
@@ -32,7 +34,7 @@ from bracket.schema import (
|
||||
users_x_clubs,
|
||||
)
|
||||
from bracket.utils.db import insert_generic
|
||||
from bracket.utils.dummy_records import DUMMY_CLUB, DUMMY_TOURNAMENT
|
||||
from bracket.utils.dummy_records import DUMMY_CLUB, DUMMY_RANKING1, DUMMY_TOURNAMENT
|
||||
from bracket.utils.id_types import TeamId
|
||||
from bracket.utils.types import BaseModelT, assert_some
|
||||
from tests.integration_tests.mocks import get_mock_token, get_mock_user
|
||||
@@ -86,6 +88,12 @@ async def inserted_court(court: Court) -> AsyncIterator[Court]:
|
||||
yield row_inserted
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def inserted_ranking(ranking: RankingInsertable) -> AsyncIterator[Ranking]:
|
||||
async with inserted_generic(ranking, rankings, Ranking) as row_inserted:
|
||||
yield cast(Ranking, row_inserted)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def inserted_player(player: Player) -> AsyncIterator[Player]:
|
||||
async with inserted_generic(player, players, Player) as row_inserted:
|
||||
@@ -143,6 +151,9 @@ async def inserted_auth_context() -> AsyncIterator[AuthContext]:
|
||||
inserted_tournament(
|
||||
DUMMY_TOURNAMENT.model_copy(update={"club_id": club_inserted.id})
|
||||
) as tournament_inserted,
|
||||
inserted_ranking(
|
||||
DUMMY_RANKING1.model_copy(update={"tournament_id": tournament_inserted.id})
|
||||
) as ranking_inserted,
|
||||
inserted_user_x_club(
|
||||
UserXClub(
|
||||
user_id=user_inserted.id,
|
||||
@@ -157,4 +168,5 @@ async def inserted_auth_context() -> AsyncIterator[AuthContext]:
|
||||
club=club_inserted,
|
||||
tournament=tournament_inserted,
|
||||
user_x_club=user_x_club_inserted,
|
||||
ranking=ranking_inserted,
|
||||
)
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from bracket.logic.ranking.elo import (
|
||||
determine_ranking_for_stage_items,
|
||||
)
|
||||
from bracket.models.db.match import MatchWithDetailsDefinitive
|
||||
from bracket.models.db.players import PlayerStatistics
|
||||
from bracket.models.db.team import FullTeamWithPlayers
|
||||
from bracket.models.db.util import RoundWithMatches, StageItemWithRounds
|
||||
from bracket.utils.dummy_records import (
|
||||
DUMMY_MOCK_TIME,
|
||||
DUMMY_PLAYER1,
|
||||
DUMMY_PLAYER2,
|
||||
DUMMY_STAGE_ITEM1,
|
||||
)
|
||||
from bracket.utils.id_types import RoundId, StageItemId, TeamId, TournamentId
|
||||
|
||||
|
||||
def test_elo_calculation() -> None:
|
||||
round_ = RoundWithMatches(
|
||||
stage_item_id=StageItemId(1),
|
||||
created=DUMMY_MOCK_TIME,
|
||||
is_draft=False,
|
||||
is_active=False,
|
||||
name="Some round",
|
||||
matches=[
|
||||
MatchWithDetailsDefinitive(
|
||||
created=DUMMY_MOCK_TIME,
|
||||
start_time=DUMMY_MOCK_TIME,
|
||||
team1_id=TeamId(1),
|
||||
team2_id=TeamId(1),
|
||||
team1_winner_from_stage_item_id=None,
|
||||
team1_winner_position=None,
|
||||
team1_winner_from_match_id=None,
|
||||
team2_winner_from_stage_item_id=None,
|
||||
team2_winner_position=None,
|
||||
team2_winner_from_match_id=None,
|
||||
team1_score=3,
|
||||
team2_score=4,
|
||||
round_id=RoundId(1),
|
||||
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=TeamId(3),
|
||||
name="Dummy team 1",
|
||||
tournament_id=TournamentId(1),
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
players=[DUMMY_PLAYER1.model_copy(update={"id": 1})],
|
||||
elo_score=DUMMY_PLAYER1.elo_score,
|
||||
swiss_score=DUMMY_PLAYER1.swiss_score,
|
||||
wins=DUMMY_PLAYER1.wins,
|
||||
draws=DUMMY_PLAYER1.draws,
|
||||
losses=DUMMY_PLAYER1.losses,
|
||||
),
|
||||
team2=FullTeamWithPlayers(
|
||||
id=TeamId(4),
|
||||
name="Dummy team 2",
|
||||
tournament_id=TournamentId(1),
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
players=[DUMMY_PLAYER2.model_copy(update={"id": 2})],
|
||||
elo_score=DUMMY_PLAYER2.elo_score,
|
||||
swiss_score=DUMMY_PLAYER2.swiss_score,
|
||||
wins=DUMMY_PLAYER2.wins,
|
||||
draws=DUMMY_PLAYER2.draws,
|
||||
losses=DUMMY_PLAYER2.losses,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
stage_item = StageItemWithRounds(
|
||||
**DUMMY_STAGE_ITEM1.model_dump(exclude={"id"}),
|
||||
id=StageItemId(-1),
|
||||
inputs=[],
|
||||
rounds=[round_],
|
||||
)
|
||||
player_stats, team_stats = determine_ranking_for_stage_items([stage_item])
|
||||
assert player_stats == {
|
||||
1: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00")),
|
||||
2: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00")),
|
||||
}
|
||||
assert team_stats == {
|
||||
3: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00")),
|
||||
4: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00")),
|
||||
}
|
||||
220
backend/tests/unit_tests/ranking_calculation_test.py
Normal file
220
backend/tests/unit_tests/ranking_calculation_test.py
Normal file
@@ -0,0 +1,220 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from heliclockter import datetime_utc
|
||||
|
||||
from bracket.logic.ranking.elo import determine_ranking_for_stage_item
|
||||
from bracket.logic.ranking.statistics import TeamStatistics
|
||||
from bracket.models.db.match import MatchWithDetails, MatchWithDetailsDefinitive
|
||||
from bracket.models.db.ranking import Ranking
|
||||
from bracket.models.db.stage_item import StageType
|
||||
from bracket.models.db.stage_item_inputs import StageItemInputFinal
|
||||
from bracket.models.db.team import FullTeamWithPlayers
|
||||
from bracket.models.db.util import RoundWithMatches, StageItemWithRounds
|
||||
from bracket.utils.dummy_records import DUMMY_TEAM1, DUMMY_TEAM2
|
||||
from bracket.utils.id_types import RankingId, RoundId, StageId, StageItemId, TeamId, TournamentId
|
||||
|
||||
|
||||
def test_determine_ranking_for_stage_item_elimination() -> None:
|
||||
tournament_id = TournamentId(-1)
|
||||
now = datetime_utc.now()
|
||||
ranking = determine_ranking_for_stage_item(
|
||||
StageItemWithRounds(
|
||||
rounds=[
|
||||
RoundWithMatches(
|
||||
matches=[
|
||||
MatchWithDetailsDefinitive(
|
||||
team1=FullTeamWithPlayers(
|
||||
**DUMMY_TEAM1.model_dump(), players=[], id=TeamId(-1)
|
||||
),
|
||||
team2=FullTeamWithPlayers(
|
||||
**DUMMY_TEAM2.model_dump(), players=[], id=TeamId(-2)
|
||||
),
|
||||
created=now,
|
||||
duration_minutes=90,
|
||||
margin_minutes=15,
|
||||
round_id=RoundId(-1),
|
||||
team1_score=2,
|
||||
team2_score=0,
|
||||
),
|
||||
MatchWithDetailsDefinitive(
|
||||
team1=FullTeamWithPlayers(
|
||||
**DUMMY_TEAM1.model_dump(), players=[], id=TeamId(-1)
|
||||
),
|
||||
team2=FullTeamWithPlayers(
|
||||
**DUMMY_TEAM2.model_dump(), players=[], id=TeamId(-2)
|
||||
),
|
||||
created=now,
|
||||
duration_minutes=90,
|
||||
margin_minutes=15,
|
||||
round_id=RoundId(-1),
|
||||
team1_score=2,
|
||||
team2_score=2,
|
||||
),
|
||||
MatchWithDetails( # This gets ignored in ranking calculation
|
||||
created=now,
|
||||
duration_minutes=90,
|
||||
margin_minutes=15,
|
||||
round_id=RoundId(-1),
|
||||
team1_score=3,
|
||||
team2_score=2,
|
||||
),
|
||||
],
|
||||
stage_item_id=StageItemId(-1),
|
||||
created=now,
|
||||
is_draft=False,
|
||||
name="",
|
||||
)
|
||||
],
|
||||
inputs=[
|
||||
StageItemInputFinal(team_id=TeamId(-1), slot=1, tournament_id=tournament_id),
|
||||
StageItemInputFinal(team_id=TeamId(-2), slot=2, tournament_id=tournament_id),
|
||||
],
|
||||
type_name="Single Elimination",
|
||||
team_count=4,
|
||||
ranking_id=None,
|
||||
id=StageItemId(-1),
|
||||
stage_id=StageId(-1),
|
||||
name="",
|
||||
created=now,
|
||||
type=StageType.SINGLE_ELIMINATION,
|
||||
),
|
||||
Ranking(
|
||||
id=RankingId(-1),
|
||||
tournament_id=tournament_id,
|
||||
created=now,
|
||||
win_points=Decimal("3.5"),
|
||||
draw_points=Decimal("1.25"),
|
||||
loss_points=Decimal("0.0"),
|
||||
add_score_points=False,
|
||||
position=0,
|
||||
),
|
||||
)
|
||||
|
||||
assert ranking == {
|
||||
-2: TeamStatistics(wins=0, draws=1, losses=1, points=Decimal("1.25")),
|
||||
-1: TeamStatistics(wins=1, draws=1, losses=0, points=Decimal("4.75")),
|
||||
}
|
||||
|
||||
|
||||
def test_determine_ranking_for_stage_item_swiss() -> None:
|
||||
tournament_id = TournamentId(-1)
|
||||
now = datetime_utc.now()
|
||||
ranking = determine_ranking_for_stage_item(
|
||||
StageItemWithRounds(
|
||||
rounds=[
|
||||
RoundWithMatches(
|
||||
matches=[
|
||||
MatchWithDetailsDefinitive(
|
||||
team1=FullTeamWithPlayers(
|
||||
**DUMMY_TEAM1.model_dump(), players=[], id=TeamId(-1)
|
||||
),
|
||||
team2=FullTeamWithPlayers(
|
||||
**DUMMY_TEAM2.model_dump(), players=[], id=TeamId(-2)
|
||||
),
|
||||
created=now,
|
||||
duration_minutes=90,
|
||||
margin_minutes=15,
|
||||
round_id=RoundId(-1),
|
||||
team1_score=2,
|
||||
team2_score=0,
|
||||
),
|
||||
MatchWithDetailsDefinitive(
|
||||
team1=FullTeamWithPlayers(
|
||||
**DUMMY_TEAM1.model_dump(), players=[], id=TeamId(-1)
|
||||
),
|
||||
team2=FullTeamWithPlayers(
|
||||
**DUMMY_TEAM2.model_dump(), players=[], id=TeamId(-2)
|
||||
),
|
||||
created=now,
|
||||
duration_minutes=90,
|
||||
margin_minutes=15,
|
||||
round_id=RoundId(-1),
|
||||
team1_score=2,
|
||||
team2_score=2,
|
||||
),
|
||||
MatchWithDetails( # This gets ignored in ranking calculation
|
||||
created=now,
|
||||
duration_minutes=90,
|
||||
margin_minutes=15,
|
||||
round_id=RoundId(-1),
|
||||
team1_score=3,
|
||||
team2_score=2,
|
||||
),
|
||||
],
|
||||
stage_item_id=StageItemId(-1),
|
||||
created=now,
|
||||
is_draft=False,
|
||||
name="",
|
||||
)
|
||||
],
|
||||
inputs=[
|
||||
StageItemInputFinal(team_id=TeamId(-1), slot=1, tournament_id=tournament_id),
|
||||
StageItemInputFinal(team_id=TeamId(-2), slot=2, tournament_id=tournament_id),
|
||||
],
|
||||
type_name="Swiss",
|
||||
team_count=4,
|
||||
ranking_id=None,
|
||||
id=StageItemId(-1),
|
||||
stage_id=StageId(-1),
|
||||
name="",
|
||||
created=now,
|
||||
type=StageType.SWISS,
|
||||
),
|
||||
Ranking(
|
||||
id=RankingId(-1),
|
||||
tournament_id=tournament_id,
|
||||
created=now,
|
||||
win_points=Decimal("3.5"),
|
||||
draw_points=Decimal("1.25"),
|
||||
loss_points=Decimal("0.0"),
|
||||
add_score_points=False,
|
||||
position=0,
|
||||
),
|
||||
)
|
||||
|
||||
assert ranking == {
|
||||
-2: TeamStatistics(wins=0, draws=1, losses=1, points=Decimal("1208")),
|
||||
-1: TeamStatistics(wins=1, draws=1, losses=0, points=Decimal("1320")),
|
||||
}
|
||||
|
||||
|
||||
def test_determine_ranking_for_stage_item_swiss_no_matches() -> None:
|
||||
tournament_id = TournamentId(-1)
|
||||
now = datetime_utc.now()
|
||||
ranking = determine_ranking_for_stage_item(
|
||||
StageItemWithRounds(
|
||||
rounds=[
|
||||
RoundWithMatches(
|
||||
matches=[],
|
||||
stage_item_id=StageItemId(-1),
|
||||
created=now,
|
||||
is_draft=False,
|
||||
name="",
|
||||
)
|
||||
],
|
||||
inputs=[
|
||||
StageItemInputFinal(team_id=TeamId(-1), slot=1, tournament_id=tournament_id),
|
||||
StageItemInputFinal(team_id=TeamId(-2), slot=2, tournament_id=tournament_id),
|
||||
],
|
||||
type_name="Swiss",
|
||||
team_count=4,
|
||||
ranking_id=None,
|
||||
id=StageItemId(-1),
|
||||
stage_id=StageId(-1),
|
||||
name="",
|
||||
created=now,
|
||||
type=StageType.SWISS,
|
||||
),
|
||||
Ranking(
|
||||
id=RankingId(-1),
|
||||
tournament_id=tournament_id,
|
||||
created=now,
|
||||
win_points=Decimal("3.5"),
|
||||
draw_points=Decimal("1.25"),
|
||||
loss_points=Decimal("0.0"),
|
||||
add_score_points=False,
|
||||
position=0,
|
||||
),
|
||||
)
|
||||
|
||||
assert not ranking
|
||||
@@ -15,6 +15,8 @@
|
||||
"active_teams_checkbox_label": "These teams are active",
|
||||
"add_court_title": "Add Court",
|
||||
"add_player_button": "Add Player",
|
||||
"add_ranking_button": "Add Ranking",
|
||||
"add_ranking_title": "Add Ranking",
|
||||
"add_round_button": "Add Round",
|
||||
"add_stage_button": "Add Stage",
|
||||
"add_stage_item_modal_title": "Add Stage Item",
|
||||
@@ -65,6 +67,9 @@
|
||||
"delete_button": "Delete",
|
||||
"delete_club_button": "Delete Club",
|
||||
"delete_court_button": "Delete Court",
|
||||
"default_ranking_badge": "Default Ranking",
|
||||
"default_label": "Default",
|
||||
"delete_ranking_button": "Delete Ranking",
|
||||
"delete_player_button": "Delete Player",
|
||||
"delete_round_button": "Delete Round",
|
||||
"delete_team_button": "Delete Team",
|
||||
@@ -72,6 +77,7 @@
|
||||
"demo_description": "To test Bracket, you can start a demo. A demo will last for 30 minutes, after which your demo account be deleted. Please make fair use of it.",
|
||||
"demo_policy_title": "Demo policy",
|
||||
"draft_round_checkbox_label": "This round is a draft round",
|
||||
"draw_points_input_label": "Points for a draw",
|
||||
"drop_match_alert_title": "Drop a match here",
|
||||
"dropzone_accept_text": "Drop files here",
|
||||
"dropzone_idle_text": "Upload Logo",
|
||||
@@ -112,6 +118,8 @@
|
||||
"logo_settings_title": "Logo Settings",
|
||||
"logout_success_title": "Logout successful",
|
||||
"logout_title": "Logout",
|
||||
"loss_points_input_label": "Points for a loss",
|
||||
"add_score_points_label": "Award points for match score",
|
||||
"lowercase_required": "Includes lowercase letter",
|
||||
"margin_minutes_choose_title": "Please choose a margin between matches",
|
||||
"match_duration_label": "Match duration (minutes)",
|
||||
@@ -120,6 +128,7 @@
|
||||
"match_filter_option_past": "Hide past matches",
|
||||
"max_results_input_label": "Max results",
|
||||
"members_table_header": "Members",
|
||||
"points_table_header": "Points",
|
||||
"minutes": "minutes",
|
||||
"miscellaneous_label": "Allow players to be in multiple teams",
|
||||
"miscellaneous_title": "Miscellaneous",
|
||||
@@ -144,13 +153,13 @@
|
||||
"no_matches_description": "First, add matches by creating stages and stage items. Then, schedule them using the button in the topright corner.",
|
||||
"no_matches_title": "No matches scheduled yet",
|
||||
"no_players_title": "No players yet",
|
||||
"no_teams_title": "No teams yet",
|
||||
"no_round_description": "There are no rounds in this stage item yet",
|
||||
"no_round_found_description": "Please wait for the organiser to add them.",
|
||||
"no_round_found_in_stage_description": "There are no rounds in this stage yet",
|
||||
"no_round_found_title": "No rounds found",
|
||||
"no_round_title": "No round",
|
||||
"no_team_members_description": "No members",
|
||||
"no_teams_title": "No teams yet",
|
||||
"none": "None",
|
||||
"not_found_description": "Unfortunately, this is only a 404 page. You may have mistyped the address, or the page has been moved to another URL.",
|
||||
"not_found_title": "You have found a secret place.",
|
||||
@@ -171,6 +180,9 @@
|
||||
"players_title": "players",
|
||||
"policy_not_accepted": "Please indicate that you have read the policy",
|
||||
"previous_stage_button": "Previous Stage",
|
||||
"ranking_title": "Ranking",
|
||||
"rankings_title": "Rankings",
|
||||
"rankings_spotlight_description": "Add/edit rankings",
|
||||
"recommended_badge_title": "Recommended",
|
||||
"remove_logo": "Remove logo",
|
||||
"remove_match_button": "Remove Match",
|
||||
@@ -180,6 +192,7 @@
|
||||
"round_robin_label": "Round Robin",
|
||||
"save_button": "Save",
|
||||
"save_players_button": "Save players",
|
||||
"save_ranking_button": "Save Ranking",
|
||||
"schedule_description": "Schedule All Unscheduled Matches",
|
||||
"schedule_title": "Schedule",
|
||||
"score_of_label": "Score of",
|
||||
@@ -199,6 +212,8 @@
|
||||
"status": "Status",
|
||||
"swiss_difference": "Swiss Difference",
|
||||
"swiss_label": "Swiss",
|
||||
"edit_stage_label": "Edit Stage",
|
||||
"edit_stage_item_label": "Edit Stage Item",
|
||||
"swiss_score": "Swiss score",
|
||||
"team_count_input_round_robin_label": "Number of teams advancing from the previous stage",
|
||||
"team_count_select_elimination_label": "Number of teams advancing from the previous stage",
|
||||
@@ -232,5 +247,6 @@
|
||||
"welcome_title": "Welcome to",
|
||||
"win_distribution_text_draws": "draws",
|
||||
"win_distribution_text_losses": "losses",
|
||||
"win_distribution_text_win": "wins"
|
||||
"win_distribution_text_win": "wins",
|
||||
"win_points_input_label": "Points for a win"
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import React, { useState } from 'react';
|
||||
import { BiSolidWrench } from 'react-icons/bi';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import { Ranking } from '../../interfaces/ranking';
|
||||
import { StageWithStageItems } from '../../interfaces/stage';
|
||||
import { StageItemWithRounds } from '../../interfaces/stage_item';
|
||||
import { StageItemInput, formatStageItemInput } from '../../interfaces/stage_item_input';
|
||||
@@ -52,11 +53,13 @@ function StageItemRow({
|
||||
tournament,
|
||||
stageItem,
|
||||
swrStagesResponse,
|
||||
rankings,
|
||||
}: {
|
||||
teamsMap: any;
|
||||
tournament: Tournament;
|
||||
stageItem: StageItemWithRounds;
|
||||
swrStagesResponse: SWRResponse;
|
||||
rankings: Ranking[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [opened, setOpened] = useState(false);
|
||||
@@ -92,6 +95,7 @@ function StageItemRow({
|
||||
tournament={tournament}
|
||||
opened={opened}
|
||||
setOpened={setOpened}
|
||||
rankings={rankings}
|
||||
/>
|
||||
<Menu withinPortal position="bottom-end" shadow="sm">
|
||||
<Menu.Target>
|
||||
@@ -107,7 +111,7 @@ function StageItemRow({
|
||||
setOpened(true);
|
||||
}}
|
||||
>
|
||||
{t('edit_name_button')}
|
||||
{t('edit_stage_item_label')}
|
||||
</Menu.Item>
|
||||
{stageItem.type === 'SWISS' ? (
|
||||
<Menu.Item
|
||||
@@ -141,10 +145,12 @@ function StageColumn({
|
||||
tournament,
|
||||
stage,
|
||||
swrStagesResponse,
|
||||
rankings,
|
||||
}: {
|
||||
tournament: Tournament;
|
||||
stage: StageWithStageItems;
|
||||
swrStagesResponse: SWRResponse;
|
||||
rankings: Ranking[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [opened, setOpened] = useState(false);
|
||||
@@ -163,6 +169,7 @@ function StageColumn({
|
||||
tournament={tournament}
|
||||
stageItem={stageItem}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
rankings={rankings}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -176,14 +183,10 @@ function StageColumn({
|
||||
setOpened={setOpened}
|
||||
/>
|
||||
<Group justify="space-between">
|
||||
<h4 style={{ marginBottom: '0rem' }}>
|
||||
<Group>
|
||||
{stage.name}
|
||||
{stage.is_active ? (
|
||||
<Badge ml="1rem" color="green">
|
||||
{t('active_badge_label')}
|
||||
</Badge>
|
||||
) : null}
|
||||
</h4>
|
||||
{stage.is_active ? <Badge color="green">{t('active_badge_label')}</Badge> : null}
|
||||
</Group>
|
||||
<Menu withinPortal position="bottom-end" shadow="sm">
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="transparent" color="gray">
|
||||
@@ -198,7 +201,7 @@ function StageColumn({
|
||||
setOpened(true);
|
||||
}}
|
||||
>
|
||||
{t('edit_name_button')}
|
||||
{t('edit_stage_label')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size="1.5rem" />}
|
||||
@@ -227,9 +230,11 @@ function StageColumn({
|
||||
export default function Builder({
|
||||
tournament,
|
||||
swrStagesResponse,
|
||||
rankings,
|
||||
}: {
|
||||
tournament: Tournament;
|
||||
swrStagesResponse: SWRResponse;
|
||||
rankings: Ranking[];
|
||||
}) {
|
||||
const stages: StageWithStageItems[] =
|
||||
swrStagesResponse.data != null ? swrStagesResponse.data.data : [];
|
||||
@@ -244,6 +249,7 @@ export default function Builder({
|
||||
tournament={tournament}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
stage={stage}
|
||||
rankings={rankings}
|
||||
/>
|
||||
));
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ interface ScoreProps {
|
||||
|
||||
export function PlayerScore({ score, min_score, max_score, decimals }: ScoreProps) {
|
||||
const percentageScale = 100.0 / (max_score - min_score);
|
||||
const empty = score - min_score === 0;
|
||||
const empty = max_score - min_score === 0;
|
||||
|
||||
return (
|
||||
<Progress.Root size={20}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
IconBrackets,
|
||||
IconCalendarEvent,
|
||||
IconHome,
|
||||
IconScoreboard,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconSoccerField,
|
||||
@@ -97,6 +98,13 @@ export function BracketSpotlight() {
|
||||
onClick: () => router.push(`/tournaments/${tournamentId}/settings`),
|
||||
leftSection: <IconSettings size="1.2rem" />,
|
||||
},
|
||||
{
|
||||
id: 'rankings',
|
||||
title: t('rankings_title'),
|
||||
description: t('rankings_spotlight_description'),
|
||||
onClick: () => router.push(`/tournaments/${tournamentId}/rankings`),
|
||||
leftSection: <IconScoreboard size="1.2rem" />,
|
||||
},
|
||||
];
|
||||
const allActions = tournamentId >= 0 ? actions.concat(tournamentActions) : actions;
|
||||
return (
|
||||
|
||||
@@ -28,7 +28,7 @@ export function UpdateStageModal({
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={() => setOpened(false)} title="Edit stage">
|
||||
<Modal opened={opened} onClose={() => setOpened(false)} title={t('edit_stage_label')}>
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
await updateStage(tournament.id, stage.id, values.name);
|
||||
|
||||
@@ -4,9 +4,11 @@ import { useTranslation } from 'next-i18next';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import { Ranking } from '../../interfaces/ranking';
|
||||
import { StageItemWithRounds } from '../../interfaces/stage_item';
|
||||
import { Tournament } from '../../interfaces/tournament';
|
||||
import { updateStageItem } from '../../services/stage_item';
|
||||
import { RankingSelect } from '../select/ranking_select';
|
||||
|
||||
export function UpdateStageItemModal({
|
||||
tournament,
|
||||
@@ -14,24 +16,29 @@ export function UpdateStageItemModal({
|
||||
setOpened,
|
||||
stageItem,
|
||||
swrStagesResponse,
|
||||
rankings,
|
||||
}: {
|
||||
tournament: Tournament;
|
||||
opened: boolean;
|
||||
setOpened: any;
|
||||
stageItem: StageItemWithRounds;
|
||||
swrStagesResponse: SWRResponse;
|
||||
rankings: Ranking[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const form = useForm({
|
||||
initialValues: { name: stageItem.name },
|
||||
initialValues: {
|
||||
name: stageItem.name,
|
||||
ranking_id: rankings.filter((ranking) => ranking.position === 0)[0].id.toString(),
|
||||
},
|
||||
validate: {},
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={() => setOpened(false)} title="Edit stage item">
|
||||
<Modal opened={opened} onClose={() => setOpened(false)} title={t('edit_stage_item_label')}>
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
await updateStageItem(tournament.id, stageItem.id, values.name);
|
||||
await updateStageItem(tournament.id, stageItem.id, values.name, values.ranking_id);
|
||||
await swrStagesResponse.mutate();
|
||||
setOpened(false);
|
||||
})}
|
||||
@@ -44,6 +51,7 @@ export function UpdateStageItemModal({
|
||||
type="text"
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<RankingSelect form={form} rankings={rankings} />
|
||||
<Button fullWidth style={{ marginTop: 16 }} color="green" type="submit">
|
||||
{t('save_button')}
|
||||
</Button>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IconCalendar,
|
||||
IconDots,
|
||||
IconHome,
|
||||
IconScoreboard,
|
||||
IconSettings,
|
||||
IconSoccerField,
|
||||
IconTrophy,
|
||||
@@ -144,6 +145,11 @@ export function TournamentLinks({ tournament_id }: any) {
|
||||
label: capitalize(t('results_title')),
|
||||
link: `${tm_prefix}/results`,
|
||||
},
|
||||
{
|
||||
icon: IconScoreboard,
|
||||
label: capitalize(t('rankings_title')),
|
||||
link: `${tm_prefix}/rankings`,
|
||||
},
|
||||
{
|
||||
icon: IconSettings,
|
||||
label: capitalize(t('tournament_setting_title')),
|
||||
|
||||
27
frontend/src/components/select/ranking_select.tsx
Normal file
27
frontend/src/components/select/ranking_select.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Select } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React from 'react';
|
||||
|
||||
import { Ranking } from '../../interfaces/ranking';
|
||||
|
||||
export function RankingSelect({ form, rankings }: { form: any; rankings: Ranking[] }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const data = rankings.map((ranking: Ranking, i: number) => ({
|
||||
value: ranking.id.toString(),
|
||||
label: `${t('ranking_title')} ${ranking.position + 1} ${i === 0 ? `(${t('default_label')})` : ''}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
withAsterisk
|
||||
data={data}
|
||||
label={t('ranking_title')}
|
||||
searchable
|
||||
allowDeselect={false}
|
||||
limit={16}
|
||||
mt={24}
|
||||
{...form.getInputProps('ranking_id')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import { Player } from '../../interfaces/player';
|
||||
import { TournamentMinimal } from '../../interfaces/tournament';
|
||||
import { deletePlayer } from '../../services/player';
|
||||
import DeleteButton from '../buttons/delete';
|
||||
import { PlayerScore } from '../info/player_score';
|
||||
import { WinDistribution } from '../info/player_statistics';
|
||||
import PlayerUpdateModal from '../modals/player_update_modal';
|
||||
import { NoContent } from '../no_content/empty_table_info';
|
||||
import { DateTime } from '../utils/datetime';
|
||||
@@ -50,9 +48,9 @@ export default function PlayersTable({
|
||||
const players: Player[] =
|
||||
swrPlayersResponse.data != null ? swrPlayersResponse.data.data.players : [];
|
||||
|
||||
const minELOScore = Math.min(...players.map((player) => Number(player.elo_score)));
|
||||
const maxELOScore = Math.max(...players.map((player) => Number(player.elo_score)));
|
||||
const maxSwissScore = Math.max(...players.map((player) => Number(player.swiss_score)));
|
||||
// const minELOScore = Math.min(...players.map((player) => Number(player.elo_score)));
|
||||
// const maxELOScore = Math.max(...players.map((player) => Number(player.elo_score)));
|
||||
// const maxSwissScore = Math.max(...players.map((player) => Number(player.swiss_score)));
|
||||
|
||||
if (swrPlayersResponse.error) return <RequestErrorAlert error={swrPlayersResponse.error} />;
|
||||
|
||||
@@ -77,25 +75,6 @@ export default function PlayersTable({
|
||||
<Table.Td>
|
||||
<DateTime datetime={player.created} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<WinDistribution wins={player.wins} draws={player.draws} losses={player.losses} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<PlayerScore
|
||||
score={Number(player.elo_score)}
|
||||
min_score={minELOScore}
|
||||
max_score={maxELOScore}
|
||||
decimals={0}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<PlayerScore
|
||||
score={Number(player.swiss_score)}
|
||||
min_score={0}
|
||||
max_score={maxSwissScore}
|
||||
decimals={1}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<PlayerUpdateModal
|
||||
swrPlayersResponse={swrPlayersResponse}
|
||||
@@ -129,17 +108,6 @@ export default function PlayersTable({
|
||||
<ThSortable state={tableState} field="created">
|
||||
{t('created')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>
|
||||
<>
|
||||
<WinDistributionTitle />
|
||||
</>
|
||||
</ThNotSortable>
|
||||
<ThSortable state={tableState} field="elo_score">
|
||||
{t('elo_score')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="swiss_score">
|
||||
{t('swiss_score')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>{null}</ThNotSortable>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import React from 'react';
|
||||
|
||||
import { StageItemWithRounds } from '../../interfaces/stage_item';
|
||||
import { StageItemInput } from '../../interfaces/stage_item_input';
|
||||
import { TeamInterface } from '../../interfaces/team';
|
||||
import PlayerList from '../info/player_list';
|
||||
import { PlayerScore } from '../info/player_score';
|
||||
@@ -71,47 +72,58 @@ export default function StandingsTable({ teams }: { teams: TeamInterface[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
type TeamWithInput = { team: TeamInterface; input: StageItemInput };
|
||||
|
||||
export function StandingsTableForStageItem({
|
||||
teams,
|
||||
teams_with_inputs,
|
||||
stageItem,
|
||||
}: {
|
||||
teams: TeamInterface[];
|
||||
teams_with_inputs: { team: TeamInterface; input: StageItemInput }[];
|
||||
stageItem: StageItemWithRounds;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tableState = getTableState('elo_score', false);
|
||||
const tableState = getTableState('points', false);
|
||||
|
||||
const minELOScore = Math.min(...teams.map((team) => team.elo_score));
|
||||
const maxELOScore = Math.max(...teams.map((team) => team.elo_score));
|
||||
const minPoints = Math.min(...teams_with_inputs.map((item) => item.input.points));
|
||||
const maxPoints = Math.max(...teams_with_inputs.map((item) => item.input.points));
|
||||
|
||||
const rows = teams
|
||||
.sort((p1: TeamInterface, p2: TeamInterface) => (p1.name < p2.name ? 1 : -1))
|
||||
.sort((p1: TeamInterface, p2: TeamInterface) => (p1.draws > p2.draws ? 1 : -1))
|
||||
.sort((p1: TeamInterface, p2: TeamInterface) => (p1.wins > p2.wins ? 1 : -1))
|
||||
.sort((p1: TeamInterface, p2: TeamInterface) => sortTableEntries(p1, p2, tableState))
|
||||
.map((team, index) => (
|
||||
<Table.Tr key={team.id}>
|
||||
const rows = teams_with_inputs
|
||||
.sort((p1: TeamWithInput, p2: TeamWithInput) => (p1.input.points > p2.input.points ? 1 : -1))
|
||||
.sort((p1: TeamWithInput, p2: TeamWithInput) =>
|
||||
sortTableEntries(p1.input, p2.input, tableState)
|
||||
)
|
||||
.map((team_with_input, index) => (
|
||||
<Table.Tr key={team_with_input.team.id}>
|
||||
<Table.Td style={{ width: '2rem' }}>{index + 1}</Table.Td>
|
||||
<Table.Td style={{ width: '20rem' }}>
|
||||
<Text truncate="end" lineClamp={1}>
|
||||
{team.name}
|
||||
{team_with_input.team.name}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td visibleFrom="sm" style={{ width: '16rem' }}>
|
||||
<PlayerList team={team} />
|
||||
<PlayerList team={team_with_input.team} />
|
||||
</Table.Td>
|
||||
<Table.Td visibleFrom="sm" style={{ width: '6rem' }}>
|
||||
<Text truncate="end" lineClamp={1}>
|
||||
{team_with_input.input.points}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
{stageItem.type === 'SWISS' ? (
|
||||
<Table.Td>
|
||||
<PlayerScore
|
||||
score={team.elo_score}
|
||||
min_score={minELOScore}
|
||||
max_score={maxELOScore}
|
||||
score={team_with_input.input.points}
|
||||
min_score={minPoints}
|
||||
max_score={maxPoints}
|
||||
decimals={0}
|
||||
/>
|
||||
</Table.Td>
|
||||
) : (
|
||||
<Table.Td style={{ minWidth: '10rem' }}>
|
||||
<WinDistribution wins={team.wins} draws={team.draws} losses={team.losses} />
|
||||
<WinDistribution
|
||||
wins={team_with_input.input.wins}
|
||||
draws={team_with_input.input.draws}
|
||||
losses={team_with_input.input.losses}
|
||||
/>
|
||||
</Table.Td>
|
||||
)}
|
||||
</Table.Tr>
|
||||
@@ -128,6 +140,9 @@ export function StandingsTableForStageItem({
|
||||
{t('name_table_header')}
|
||||
</ThSortable>
|
||||
<ThNotSortable visibleFrom="sm">{t('members_table_header')}</ThNotSortable>
|
||||
<ThSortable visibleFrom="sm" state={tableState} field="points">
|
||||
{t('points_table_header')}
|
||||
</ThSortable>
|
||||
{stageItem.type === 'SWISS' ? (
|
||||
<ThSortable state={tableState} field="elo_score">
|
||||
{t('elo_score')}
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface ThProps {
|
||||
children: React.ReactNode;
|
||||
state: TableState;
|
||||
field: string;
|
||||
visibleFrom?: string;
|
||||
}
|
||||
|
||||
export const setSorting = (state: TableState, newSortField: string) => {
|
||||
@@ -70,11 +71,11 @@ export function getSortIcon(sorted: boolean, reversed: boolean) {
|
||||
return <HiSortAscending />;
|
||||
}
|
||||
|
||||
export function ThSortable({ children, field, state }: ThProps) {
|
||||
export function ThSortable({ children, field, visibleFrom, state }: ThProps) {
|
||||
const sorted = state.sortField === field;
|
||||
const onSort = () => setSorting(state, field);
|
||||
return (
|
||||
<Table.Th className={classes.th}>
|
||||
<Table.Th className={classes.th} visibleFrom={visibleFrom}>
|
||||
<UnstyledButton onClick={onSort} className={classes.control}>
|
||||
<Group justify="apart">
|
||||
<Text fw={800} size="sm" ml="0.5rem" my="0.25rem">
|
||||
|
||||
@@ -53,8 +53,6 @@ export default function TeamsTable({
|
||||
<Table.Td>
|
||||
<DateTime datetime={team.created} />
|
||||
</Table.Td>
|
||||
<Table.Td>{Number(team.swiss_score).toFixed(1)}</Table.Td>
|
||||
<Table.Td>{Number(team.elo_score).toFixed(0)}</Table.Td>
|
||||
<Table.Td>
|
||||
<TeamUpdateModal
|
||||
tournament_id={tournamentData.id}
|
||||
@@ -89,12 +87,6 @@ export default function TeamsTable({
|
||||
<ThSortable state={tableState} field="created">
|
||||
{t('created')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="swiss_score">
|
||||
{t('swiss_score')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="elo_score">
|
||||
{t('elo_score')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>{null}</ThNotSortable>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
10
frontend/src/interfaces/ranking.tsx
Normal file
10
frontend/src/interfaces/ranking.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Ranking {
|
||||
id: number;
|
||||
tournament_id: number;
|
||||
created: string;
|
||||
win_points: number;
|
||||
draw_points: number;
|
||||
loss_points: number;
|
||||
add_score_points: boolean;
|
||||
position: number;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { StageItemInput } from './stage_item_input';
|
||||
|
||||
export interface StageItemWithRounds {
|
||||
id: number;
|
||||
tournament_id: number;
|
||||
created: string;
|
||||
type: string;
|
||||
name: string;
|
||||
|
||||
@@ -6,6 +6,10 @@ export interface StageItemInput {
|
||||
team_id: number | null;
|
||||
winner_from_stage_item_id: number | null;
|
||||
winner_position: number | null;
|
||||
wins: number;
|
||||
draws: number;
|
||||
losses: number;
|
||||
points: number;
|
||||
}
|
||||
|
||||
export interface StageItemInputCreateBody {
|
||||
|
||||
@@ -191,9 +191,8 @@ export default function SchedulePage() {
|
||||
const { t } = useTranslation();
|
||||
const tournamentResponse = getTournamentResponseByEndpointName();
|
||||
|
||||
// Hack to avoid unequal number of rendered hooks.
|
||||
const notFound = tournamentResponse == null || tournamentResponse[0] == null;
|
||||
const tournamentId = !notFound ? tournamentResponse[0].id : -1;
|
||||
const tournamentId = !notFound ? tournamentResponse[0].id : null;
|
||||
const tournamentDataFull = tournamentResponse != null ? tournamentResponse[0] : null;
|
||||
|
||||
const swrStagesResponse = getStagesLive(tournamentId);
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function CourtsPage() {
|
||||
|
||||
// Hack to avoid unequal number of rendered hooks.
|
||||
const notFound = tournamentResponse == null || tournamentResponse[0] == null;
|
||||
const tournamentId = !notFound ? tournamentResponse[0].id : -1;
|
||||
const tournamentId = !notFound ? tournamentResponse[0].id : null;
|
||||
|
||||
const swrStagesResponse: SWRResponse = getStagesLive(tournamentId);
|
||||
const swrCourtsResponse: SWRResponse = getCourtsLive(tournamentId);
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function Standings() {
|
||||
|
||||
// Hack to avoid unequal number of rendered hooks.
|
||||
const notFound = tournamentResponse == null || tournamentResponse[0] == null;
|
||||
const tournamentId = !notFound ? tournamentResponse[0].id : -1;
|
||||
const tournamentId = !notFound ? tournamentResponse[0].id : null;
|
||||
|
||||
const swrTeamsResponse: SWRResponse = getTeamsLive(tournamentId);
|
||||
|
||||
|
||||
@@ -38,15 +38,15 @@ function StandingsContent({
|
||||
stageItemsLookup[si1].name > stageItemsLookup[si2].name ? 1 : -1
|
||||
)
|
||||
.map((stageItemId) => (
|
||||
<>
|
||||
<div key={stageItemId}>
|
||||
<Text size="xl" mt="md" mb="xs">
|
||||
{stageItemsLookup[stageItemId].name}
|
||||
</Text>
|
||||
<StandingsTableForStageItem
|
||||
teams={stageItemTeamLookup[stageItemId]}
|
||||
teams_with_inputs={stageItemTeamLookup[stageItemId]}
|
||||
stageItem={stageItemsLookup[stageItemId]}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
));
|
||||
|
||||
if (rows.length < 1) {
|
||||
@@ -62,15 +62,19 @@ function StandingsContent({
|
||||
|
||||
export default function Standings() {
|
||||
const tournamentResponse = getTournamentResponseByEndpointName();
|
||||
const tournamentDataFull = tournamentResponse[0];
|
||||
|
||||
// Hack to avoid unequal number of rendered hooks.
|
||||
const notFound = tournamentResponse == null || tournamentDataFull == null;
|
||||
const tournamentId = !notFound ? tournamentDataFull.id : -1;
|
||||
const tournamentDataFull = tournamentResponse ? tournamentResponse[0] : null;
|
||||
|
||||
const notFound = tournamentDataFull == null;
|
||||
const tournamentId = !notFound ? tournamentDataFull.id : null;
|
||||
|
||||
const swrStagesResponse = getStagesLive(tournamentId);
|
||||
const swrTeamsResponse: SWRResponse = getTeamsLive(tournamentId);
|
||||
|
||||
if (!tournamentResponse) {
|
||||
return <TableSkeletonTwoColumns />;
|
||||
}
|
||||
|
||||
if (swrTeamsResponse.isLoading || swrStagesResponse.isLoading) {
|
||||
return <TableSkeletonTwoColumns />;
|
||||
}
|
||||
@@ -85,7 +89,7 @@ export default function Standings() {
|
||||
<TournamentHeadTitle tournamentDataFull={tournamentDataFull} />
|
||||
</Head>
|
||||
<DoubleHeader tournamentData={tournamentDataFull} />
|
||||
<Container mt="1rem" style={{ overflow: 'scroll' }} px="0rem">
|
||||
<Container mt="1rem" px="0rem">
|
||||
<Container style={{ width: '100%' }} px="sm">
|
||||
<StandingsContent
|
||||
swrTeamsResponse={swrTeamsResponse}
|
||||
|
||||
209
frontend/src/pages/tournaments/[id]/rankings.tsx
Normal file
209
frontend/src/pages/tournaments/[id]/rankings.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { Accordion, Badge, Button, Center, Checkbox, Container, NumberInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import DeleteButton from '../../../components/buttons/delete';
|
||||
import { EmptyTableInfo } from '../../../components/no_content/empty_table_info';
|
||||
import RequestErrorAlert from '../../../components/utils/error_alert';
|
||||
import { TableSkeletonSingleColumn } from '../../../components/utils/skeletons';
|
||||
import { Translator } from '../../../components/utils/types';
|
||||
import { getTournamentIdFromRouter } from '../../../components/utils/util';
|
||||
import { Ranking } from '../../../interfaces/ranking';
|
||||
import { Tournament } from '../../../interfaces/tournament';
|
||||
import { getRankings, getTournamentById } from '../../../services/adapter';
|
||||
import { createRanking, deleteRanking, editRanking } from '../../../services/ranking';
|
||||
import TournamentLayout from '../_tournament_layout';
|
||||
|
||||
function RankingDeleteButton({
|
||||
t,
|
||||
tournament,
|
||||
ranking,
|
||||
swrRankingsResponse,
|
||||
}: {
|
||||
t: Translator;
|
||||
tournament: Tournament;
|
||||
ranking: Ranking;
|
||||
swrRankingsResponse: SWRResponse;
|
||||
}) {
|
||||
if (ranking.position === 0) {
|
||||
return (
|
||||
<Center ml="1rem" miw="10rem">
|
||||
<Badge color="indigo">{t('default_ranking_badge')}</Badge>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DeleteButton
|
||||
onClick={async () => {
|
||||
await deleteRanking(tournament.id, ranking.id);
|
||||
await swrRankingsResponse.mutate();
|
||||
}}
|
||||
title={t('delete_ranking_button')}
|
||||
ml="1rem"
|
||||
variant="outline"
|
||||
miw="10rem"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EditRankingForm({
|
||||
t,
|
||||
tournament,
|
||||
ranking,
|
||||
swrRankingsResponse,
|
||||
}: {
|
||||
t: Translator;
|
||||
tournament: Tournament;
|
||||
ranking: Ranking;
|
||||
swrRankingsResponse: SWRResponse;
|
||||
}) {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
win_points: ranking.win_points,
|
||||
draw_points: ranking.draw_points,
|
||||
loss_points: ranking.loss_points,
|
||||
add_score_points: ranking.add_score_points,
|
||||
position: ranking.position,
|
||||
},
|
||||
validate: {},
|
||||
});
|
||||
const rankingTitle = `${t('ranking_title')} ${ranking.position + 1}`;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
await editRanking(
|
||||
tournament.id,
|
||||
ranking.id,
|
||||
values.win_points,
|
||||
values.draw_points,
|
||||
values.loss_points,
|
||||
values.add_score_points,
|
||||
values.position
|
||||
);
|
||||
await swrRankingsResponse.mutate();
|
||||
})}
|
||||
>
|
||||
<Accordion.Item key={ranking.id} value={`${ranking.position}`}>
|
||||
<Center>
|
||||
<Accordion.Control>{rankingTitle}</Accordion.Control>
|
||||
<Center>
|
||||
<RankingDeleteButton
|
||||
t={t}
|
||||
tournament={tournament}
|
||||
ranking={ranking}
|
||||
swrRankingsResponse={swrRankingsResponse}
|
||||
/>
|
||||
</Center>
|
||||
</Center>
|
||||
<Accordion.Panel>
|
||||
<NumberInput
|
||||
withAsterisk
|
||||
label={t('win_points_input_label')}
|
||||
{...form.getInputProps('win_points')}
|
||||
/>
|
||||
<NumberInput
|
||||
mt="1rem"
|
||||
withAsterisk
|
||||
label={t('draw_points_input_label')}
|
||||
{...form.getInputProps('draw_points')}
|
||||
/>
|
||||
<NumberInput
|
||||
mt="1rem"
|
||||
withAsterisk
|
||||
label={t('loss_points_input_label')}
|
||||
{...form.getInputProps('loss_points')}
|
||||
/>
|
||||
<Checkbox
|
||||
mt="lg"
|
||||
label={t('add_score_points_label')}
|
||||
{...form.getInputProps('add_score_points', { type: 'checkbox' })}
|
||||
/>
|
||||
<Button fullWidth style={{ marginTop: 16 }} color="green" type="submit">
|
||||
{`${t('save_button')} ${rankingTitle}`}
|
||||
</Button>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function RankingForm({
|
||||
t,
|
||||
tournament,
|
||||
swrRankingsResponse,
|
||||
}: {
|
||||
t: Translator;
|
||||
tournament: Tournament;
|
||||
swrRankingsResponse: SWRResponse;
|
||||
}) {
|
||||
const rankings: Ranking[] = swrRankingsResponse.data != null ? swrRankingsResponse.data.data : [];
|
||||
|
||||
const rows = rankings
|
||||
.sort((s1: Ranking, s2: Ranking) => s1.position - s2.position)
|
||||
.map((ranking) => (
|
||||
<EditRankingForm
|
||||
t={t}
|
||||
tournament={tournament}
|
||||
ranking={ranking}
|
||||
swrRankingsResponse={swrRankingsResponse}
|
||||
/>
|
||||
));
|
||||
|
||||
if (swrRankingsResponse.isLoading) {
|
||||
return <TableSkeletonSingleColumn />;
|
||||
}
|
||||
|
||||
if (swrRankingsResponse.error) return <RequestErrorAlert error={swrRankingsResponse.error} />;
|
||||
|
||||
if (rows.length < 1) return <EmptyTableInfo entity_name={t('rankings_title')} />;
|
||||
|
||||
return (
|
||||
<Accordion multiple defaultValue={['0']}>
|
||||
{rows}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RankingsPage() {
|
||||
const { tournamentData } = getTournamentIdFromRouter();
|
||||
const swrRankingsResponse = getRankings(tournamentData.id);
|
||||
|
||||
const swrTournamentResponse = getTournamentById(tournamentData.id);
|
||||
const tournamentDataFull =
|
||||
swrTournamentResponse.data != null ? swrTournamentResponse.data.data : null;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TournamentLayout tournament_id={tournamentData.id}>
|
||||
<Container maw="50rem">
|
||||
<RankingForm
|
||||
t={t}
|
||||
tournament={tournamentDataFull}
|
||||
swrRankingsResponse={swrRankingsResponse}
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
mt="1rem"
|
||||
color="green"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
await createRanking(tournamentDataFull.id);
|
||||
await swrRankingsResponse.mutate();
|
||||
}}
|
||||
>
|
||||
{t('add_ranking_button')}
|
||||
</Button>
|
||||
</Container>
|
||||
</TournamentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common'])),
|
||||
},
|
||||
});
|
||||
@@ -12,17 +12,20 @@ import {
|
||||
import { NoContent } from '../../../components/no_content/empty_table_info';
|
||||
import { TableSkeletonTwoColumnsSmall } from '../../../components/utils/skeletons';
|
||||
import { getTournamentIdFromRouter } from '../../../components/utils/util';
|
||||
import { Ranking } from '../../../interfaces/ranking';
|
||||
import { StageWithStageItems } from '../../../interfaces/stage';
|
||||
import { getStages, getTournamentById } from '../../../services/adapter';
|
||||
import { getRankings, getStages, getTournamentById } from '../../../services/adapter';
|
||||
import TournamentLayout from '../_tournament_layout';
|
||||
|
||||
export default function StagesPage() {
|
||||
const { t } = useTranslation();
|
||||
const { tournamentData } = getTournamentIdFromRouter();
|
||||
const swrStagesResponse = getStages(tournamentData.id);
|
||||
const swrRankingsResponse = getRankings(tournamentData.id);
|
||||
const swrTournamentResponse = getTournamentById(tournamentData.id);
|
||||
const tournamentDataFull =
|
||||
swrTournamentResponse.data != null ? swrTournamentResponse.data.data : null;
|
||||
const rankings: Ranking[] = swrRankingsResponse.data != null ? swrRankingsResponse.data.data : [];
|
||||
|
||||
const stages: StageWithStageItems[] =
|
||||
swrStagesResponse.data != null ? swrStagesResponse.data.data : [];
|
||||
@@ -51,7 +54,11 @@ export default function StagesPage() {
|
||||
<NextStageButton tournamentData={tournamentData} swrStagesResponse={swrStagesResponse} />
|
||||
</Group>
|
||||
<Group mt="1rem" align="top">
|
||||
<Builder tournament={tournamentDataFull} swrStagesResponse={swrStagesResponse} />
|
||||
<Builder
|
||||
tournament={tournamentDataFull}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
rankings={rankings}
|
||||
/>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -131,8 +131,11 @@ export function getPlayersPaginated(tournament_id: number, pagination: Paginatio
|
||||
);
|
||||
}
|
||||
|
||||
export function getTeams(tournament_id: number): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/teams?limit=100`, fetcher);
|
||||
export function getTeams(tournament_id: number | null): SWRResponse {
|
||||
return useSWR(
|
||||
tournament_id == null ? null : `tournaments/${tournament_id}/teams?limit=100`,
|
||||
fetcher
|
||||
);
|
||||
}
|
||||
|
||||
export function getTeamsPaginated(tournament_id: number, pagination: Pagination): SWRResponse {
|
||||
@@ -142,8 +145,8 @@ export function getTeamsPaginated(tournament_id: number, pagination: Pagination)
|
||||
);
|
||||
}
|
||||
|
||||
export function getTeamsLive(tournament_id: number): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/teams`, fetcher, {
|
||||
export function getTeamsLive(tournament_id: number | null): SWRResponse {
|
||||
return useSWR(tournament_id == null ? null : `tournaments/${tournament_id}/teams`, fetcher, {
|
||||
refreshInterval: 5_000,
|
||||
});
|
||||
}
|
||||
@@ -152,18 +155,21 @@ export function getAvailableStageItemInputs(tournament_id: number, stage_id: num
|
||||
return useSWR(`tournaments/${tournament_id}/stages/${stage_id}/available_inputs`, fetcher);
|
||||
}
|
||||
|
||||
export function getStages(tournament_id: number, no_draft_rounds: boolean = false): SWRResponse {
|
||||
export function getStages(
|
||||
tournament_id: number | null,
|
||||
no_draft_rounds: boolean = false
|
||||
): SWRResponse {
|
||||
return useSWR(
|
||||
tournament_id === -1
|
||||
tournament_id == null || tournament_id === -1
|
||||
? null
|
||||
: `tournaments/${tournament_id}/stages?no_draft_rounds=${no_draft_rounds}`,
|
||||
fetcher
|
||||
);
|
||||
}
|
||||
|
||||
export function getStagesLive(tournament_id: number): SWRResponse {
|
||||
export function getStagesLive(tournament_id: number | null): SWRResponse {
|
||||
return useSWR(
|
||||
tournament_id === -1 ? null : `tournaments/${tournament_id}/stages?no_draft_rounds=true`,
|
||||
tournament_id == null ? null : `tournaments/${tournament_id}/stages?no_draft_rounds=true`,
|
||||
fetcherWithTimestamp,
|
||||
{
|
||||
refreshInterval: 5_000,
|
||||
@@ -171,6 +177,10 @@ export function getStagesLive(tournament_id: number): SWRResponse {
|
||||
);
|
||||
}
|
||||
|
||||
export function getRankings(tournament_id: number): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/rankings`, fetcher);
|
||||
}
|
||||
|
||||
export function getCourts(tournament_id: number): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/courts`, fetcher);
|
||||
}
|
||||
|
||||
@@ -65,13 +65,12 @@ export function getStageItemTeamsLookup(
|
||||
stage.stage_items
|
||||
.sort((si1: any, si2: any) => (si1.name > si2.name ? 1 : -1))
|
||||
.forEach((stageItem) => {
|
||||
const teams = stageItem.inputs
|
||||
.map((input) => input.team_id)
|
||||
.map((id) => teamsLookup![id!])
|
||||
.filter((team: TeamInterface) => team != null);
|
||||
const teams_with_inputs = stageItem.inputs
|
||||
.filter((input) => input.team_id != null)
|
||||
.map((input) => ({ team: teamsLookup[input.team_id!], input }));
|
||||
|
||||
if (teams.length > 0) {
|
||||
result = result.concat([[stageItem.id, teams]]);
|
||||
if (teams_with_inputs.length > 0) {
|
||||
result = result.concat([[stageItem.id, teams_with_inputs]]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
33
frontend/src/services/ranking.tsx
Normal file
33
frontend/src/services/ranking.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createAxios, handleRequestError } from './adapter';
|
||||
|
||||
export async function createRanking(tournament_id: number) {
|
||||
return createAxios()
|
||||
.post(`tournaments/${tournament_id}/rankings`, {})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function editRanking(
|
||||
tournament_id: number,
|
||||
ranking_id: number,
|
||||
win_points: number,
|
||||
draw_points: number,
|
||||
loss_points: number,
|
||||
add_score_points: boolean,
|
||||
position: number
|
||||
) {
|
||||
return createAxios()
|
||||
.put(`tournaments/${tournament_id}/rankings/${ranking_id}`, {
|
||||
win_points,
|
||||
draw_points,
|
||||
loss_points,
|
||||
add_score_points,
|
||||
position,
|
||||
})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function deleteRanking(tournament_id: number, ranking_id: number) {
|
||||
return createAxios()
|
||||
.delete(`tournaments/${tournament_id}/rankings/${ranking_id}`)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
@@ -13,9 +13,14 @@ export async function createStageItem(
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function updateStageItem(tournament_id: number, stage_item_id: number, name: string) {
|
||||
export async function updateStageItem(
|
||||
tournament_id: number,
|
||||
stage_item_id: number,
|
||||
name: string,
|
||||
ranking_id: string
|
||||
) {
|
||||
return createAxios()
|
||||
.put(`tournaments/${tournament_id}/stage_items/${stage_item_id}`, { name })
|
||||
.put(`tournaments/${tournament_id}/stage_items/${stage_item_id}`, { name, ranking_id })
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
|
||||
@@ -60,5 +60,5 @@ export function getTournamentResponseByEndpointName() {
|
||||
const endpointName = getTournamentEndpointFromRouter();
|
||||
const swrTournamentsResponse = getTournamentByEndpointName(endpointName);
|
||||
|
||||
return swrTournamentsResponse.data != null ? swrTournamentsResponse.data.data : null;
|
||||
return swrTournamentsResponse.data?.data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user