mirror of
https://github.com/evroon/bracket.git
synced 2026-03-04 15:22:34 -05:00
Various bugfixes (#77)
This commit is contained in:
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -9,13 +9,19 @@ updates:
|
||||
directory: "/backend"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
ignore:
|
||||
- update-types: ["version-update:semver-patch"]
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/frontend"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
ignore:
|
||||
- update-types: ["version-update:semver-patch"]
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- update-types: ["version-update:semver-patch"]
|
||||
|
||||
@@ -18,7 +18,7 @@ sudo pg_createcluster -u postgres -p 5532 13 bracket
|
||||
pg_ctlcluster 13 bracket start
|
||||
```
|
||||
|
||||
Subsequently, create a new `bracket_dev` database (and `bracket_ci` for tests):
|
||||
Subsequently, create a new `bracket_dev` database:
|
||||
```shell
|
||||
sudo -Hu postgres psql -p 5532
|
||||
CREATE USER bracket_ci WITH PASSWORD 'bracket_ci';
|
||||
|
||||
@@ -4,37 +4,37 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
aiopg = "1.4.0"
|
||||
fastapi = "0.88.0"
|
||||
fastapi-cache2 = "0.2.0"
|
||||
gunicorn = "20.1.0"
|
||||
uvicorn = "0.20.0"
|
||||
starlette = "0.22.0"
|
||||
aiopg = ">=1.4.0"
|
||||
fastapi = ">=0.88.0"
|
||||
fastapi-cache2 = ">=0.2.0"
|
||||
gunicorn = ">=20.1.0"
|
||||
uvicorn = ">=0.20.0"
|
||||
starlette = ">=0.22.0"
|
||||
sqlalchemy = "<2.0"
|
||||
sqlalchemy-stubs = "0.4"
|
||||
pydantic = "1.10.4"
|
||||
heliclockter = "1.0.4"
|
||||
alembic = "1.9.1"
|
||||
types-simplejson = "3.18.0"
|
||||
python-dotenv = "0.21.0"
|
||||
databases = {extras = ["asyncpg"], version = "0.7.0"}
|
||||
passlib = "1.7.4"
|
||||
sqlalchemy-stubs = ">=0.4"
|
||||
pydantic = ">=1.10.4"
|
||||
heliclockter = ">=1.0.4"
|
||||
alembic = ">=1.9.1"
|
||||
types-simplejson = ">=3.18.0"
|
||||
python-dotenv = ">=0.21.0"
|
||||
databases = {extras = ["asyncpg"], version = ">=0.7.0"}
|
||||
passlib = ">=1.7.4"
|
||||
types-passlib = "*"
|
||||
pyjwt = "2.6.0"
|
||||
click = "8.1.3"
|
||||
python-multipart = "*"
|
||||
parameterized = "*"
|
||||
pyjwt = ">=2.6.0"
|
||||
click = ">=8.1.3"
|
||||
python-multipart = ">=0.0.5"
|
||||
parameterized = ">=0.8.1"
|
||||
|
||||
[dev-packages]
|
||||
mypy = "0.991"
|
||||
black = "22.12.0"
|
||||
isort = "5.11.4"
|
||||
pylint = "2.15.10"
|
||||
pytest = "7.2.0"
|
||||
pytest-cov = "4.0.0"
|
||||
pytest-asyncio = "0.20.3"
|
||||
aiohttp = "3.8.3"
|
||||
aioresponses = "0.7.4"
|
||||
mypy = ">=0.991"
|
||||
black = ">=22.12.0"
|
||||
isort = ">=5.11.4"
|
||||
pylint = ">=2.15.10"
|
||||
pytest = ">=7.2.0"
|
||||
pytest-cov = ">=4.0.0"
|
||||
pytest-asyncio = ">=0.20.3"
|
||||
aiohttp = ">=3.8.3"
|
||||
aioresponses = ">=0.7.4"
|
||||
|
||||
[requires]
|
||||
python_version = "3.10"
|
||||
|
||||
@@ -22,32 +22,33 @@ def calculate_elo_per_player(rounds: list[RoundWithMatches]) -> defaultdict[int,
|
||||
player_x_elo: defaultdict[int, PlayerStatistics] = defaultdict(PlayerStatistics)
|
||||
|
||||
for round in rounds:
|
||||
for match in round.matches:
|
||||
for team_index, team in enumerate(match.teams):
|
||||
for player in team.players:
|
||||
team_score = match.team1_score if team_index == 0 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
|
||||
)
|
||||
if not round.is_draft:
|
||||
for match in round.matches:
|
||||
for team_index, team in enumerate(match.teams):
|
||||
for player in team.players:
|
||||
team_score = match.team1_score if team_index == 0 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
|
||||
)
|
||||
|
||||
if has_won:
|
||||
player_x_elo[assert_some(player.id)].wins += 1
|
||||
player_x_elo[assert_some(player.id)].swiss_score += Decimal('1.00')
|
||||
elif was_draw:
|
||||
player_x_elo[assert_some(player.id)].draws += 1
|
||||
player_x_elo[assert_some(player.id)].swiss_score += Decimal('0.50')
|
||||
else:
|
||||
player_x_elo[assert_some(player.id)].losses += 1
|
||||
if has_won:
|
||||
player_x_elo[assert_some(player.id)].wins += 1
|
||||
player_x_elo[assert_some(player.id)].swiss_score += Decimal('1.00')
|
||||
elif was_draw:
|
||||
player_x_elo[assert_some(player.id)].draws += 1
|
||||
player_x_elo[assert_some(player.id)].swiss_score += Decimal('0.50')
|
||||
else:
|
||||
player_x_elo[assert_some(player.id)].losses += 1
|
||||
|
||||
player_x_elo[assert_some(player.id)].elo_score += team_score
|
||||
player_x_elo[assert_some(player.id)].elo_score += team_score
|
||||
|
||||
return player_x_elo
|
||||
|
||||
|
||||
async def recalculate_elo_for_tournament_id(tournament_id: int) -> None:
|
||||
rounds_response = await get_rounds_with_matches(tournament_id)
|
||||
elo_per_player = calculate_elo_per_player(rounds_response.data)
|
||||
elo_per_player = calculate_elo_per_player(rounds_response)
|
||||
|
||||
for player_id, statistics in elo_per_player.items():
|
||||
await database.execute(
|
||||
|
||||
@@ -22,14 +22,14 @@ async def get_possible_upcoming_matches(
|
||||
) -> list[SuggestedMatch]:
|
||||
suggestions: list[SuggestedMatch] = []
|
||||
rounds_response = await get_rounds_with_matches(tournament_id)
|
||||
draft_round = next((round for round in rounds_response.data if round.is_draft), None)
|
||||
draft_round = next((round for round in rounds_response if round.is_draft), None)
|
||||
if draft_round is None:
|
||||
raise HTTPException(400, 'There is no draft round, so no matches can be scheduled.')
|
||||
|
||||
teams = await get_teams_with_members(tournament_id, only_active_teams=True)
|
||||
|
||||
for i, team1 in enumerate(teams.data):
|
||||
for j, team2 in enumerate(teams.data[i + 1 :]):
|
||||
for i, team1 in enumerate(teams):
|
||||
for j, team2 in enumerate(teams[i + 1 :]):
|
||||
team_already_scheduled = any(
|
||||
(
|
||||
True
|
||||
|
||||
@@ -16,6 +16,7 @@ class Match(BaseModelORM):
|
||||
team2_id: int
|
||||
team1_score: int
|
||||
team2_score: int
|
||||
label: str
|
||||
|
||||
|
||||
class UpcomingMatch(BaseModel):
|
||||
@@ -40,12 +41,14 @@ class MatchBody(BaseModelORM):
|
||||
round_id: int
|
||||
team1_score: int = 0
|
||||
team2_score: int = 0
|
||||
label: str
|
||||
|
||||
|
||||
class MatchCreateBody(BaseModelORM):
|
||||
round_id: int
|
||||
team1_id: int
|
||||
team2_id: int
|
||||
label: str
|
||||
|
||||
|
||||
class MatchToInsert(MatchCreateBody):
|
||||
|
||||
@@ -5,6 +5,7 @@ from pydantic import validator
|
||||
|
||||
from bracket.models.db.match import Match, MatchWithTeamDetails
|
||||
from bracket.models.db.shared import BaseModelORM
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
|
||||
class Round(BaseModelORM):
|
||||
@@ -29,6 +30,9 @@ class RoundWithMatches(Round):
|
||||
|
||||
return values
|
||||
|
||||
def get_team_ids(self) -> set[int]:
|
||||
return {assert_some(team.id) for match in self.matches for team in match.teams}
|
||||
|
||||
|
||||
class RoundBody(BaseModelORM):
|
||||
name: str
|
||||
|
||||
@@ -4,10 +4,11 @@ from heliclockter import datetime_utc
|
||||
from bracket.database import database
|
||||
from bracket.logic.elo import recalculate_elo_for_tournament_id
|
||||
from bracket.logic.upcoming_matches import get_possible_upcoming_matches
|
||||
from bracket.models.db.match import MatchBody, MatchCreateBody, MatchFilter, MatchToInsert
|
||||
from bracket.models.db.match import Match, MatchBody, MatchCreateBody, MatchFilter, MatchToInsert
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import user_authenticated_for_tournament
|
||||
from bracket.routes.models import SuccessResponse, UpcomingMatchesResponse
|
||||
from bracket.routes.util import match_dependency
|
||||
from bracket.schema import matches
|
||||
|
||||
router = APIRouter()
|
||||
@@ -24,11 +25,13 @@ async def get_matches_to_schedule(
|
||||
|
||||
@router.delete("/tournaments/{tournament_id}/matches/{match_id}", response_model=SuccessResponse)
|
||||
async def delete_match(
|
||||
tournament_id: int, match_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
|
||||
tournament_id: int,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
match: Match = Depends(match_dependency),
|
||||
) -> SuccessResponse:
|
||||
await database.execute(
|
||||
query=matches.delete().where(
|
||||
matches.c.id == match_id and matches.c.tournament_id == tournament_id
|
||||
matches.c.id == match.id and matches.c.tournament_id == tournament_id
|
||||
),
|
||||
)
|
||||
await recalculate_elo_for_tournament_id(tournament_id)
|
||||
@@ -37,7 +40,6 @@ async def delete_match(
|
||||
|
||||
@router.post("/tournaments/{tournament_id}/matches", response_model=SuccessResponse)
|
||||
async def create_match(
|
||||
tournament_id: int,
|
||||
match_body: MatchCreateBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SuccessResponse:
|
||||
@@ -54,12 +56,12 @@ async def create_match(
|
||||
@router.patch("/tournaments/{tournament_id}/matches/{match_id}", response_model=SuccessResponse)
|
||||
async def update_match_by_id(
|
||||
tournament_id: int,
|
||||
match_id: int,
|
||||
match_body: MatchBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
match: Match = Depends(match_dependency),
|
||||
) -> SuccessResponse:
|
||||
await database.execute(
|
||||
query=matches.update().where(matches.c.id == match_id),
|
||||
query=matches.update().where(matches.c.id == match.id),
|
||||
values=match_body.dict(),
|
||||
)
|
||||
await recalculate_elo_for_tournament_id(tournament_id)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from heliclockter import datetime_utc
|
||||
from starlette import status
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.elo import recalculate_elo_for_tournament_id
|
||||
from bracket.models.db.round import RoundBody, RoundToInsert
|
||||
from bracket.models.db.round import Round, RoundBody, RoundToInsert, RoundWithMatches
|
||||
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 RoundsWithMatchesResponse, SuccessResponse
|
||||
from bracket.routes.util import round_dependency, round_with_matches_dependency
|
||||
from bracket.schema import rounds
|
||||
from bracket.utils.sql import get_next_round_name, get_rounds_with_matches
|
||||
|
||||
@@ -26,15 +28,24 @@ async def get_rounds(
|
||||
tournament_id, no_draft_rounds=user is None or no_draft_rounds
|
||||
)
|
||||
if user is not None:
|
||||
return rounds
|
||||
return RoundsWithMatchesResponse(data=rounds)
|
||||
|
||||
return RoundsWithMatchesResponse(data=[round_ for round_ in rounds.data if not round_.is_draft])
|
||||
return RoundsWithMatchesResponse(data=[round_ for round_ in rounds if not round_.is_draft])
|
||||
|
||||
|
||||
@router.delete("/tournaments/{tournament_id}/rounds/{round_id}", response_model=SuccessResponse)
|
||||
async def delete_round(
|
||||
tournament_id: int, round_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
|
||||
tournament_id: int,
|
||||
round_id: int,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
round_with_matches: RoundWithMatches = Depends(round_with_matches_dependency),
|
||||
) -> SuccessResponse:
|
||||
if len(round_with_matches.matches) > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Round contains matches, delete those first",
|
||||
)
|
||||
|
||||
await database.execute(
|
||||
query=rounds.delete().where(
|
||||
rounds.c.id == round_id and rounds.c.tournament_id == tournament_id
|
||||
@@ -65,6 +76,7 @@ async def update_round_by_id(
|
||||
round_id: int,
|
||||
round_body: RoundBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
round: Round = Depends(round_dependency),
|
||||
) -> SuccessResponse:
|
||||
values = {'tournament_id': tournament_id, 'round_id': round_id}
|
||||
query = '''
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from heliclockter import datetime_utc
|
||||
from starlette import status
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.elo import recalculate_elo_for_tournament_id
|
||||
from bracket.models.db.team import Team, TeamBody, TeamToInsert
|
||||
from bracket.models.db.team import Team, TeamBody, TeamToInsert, TeamWithPlayers
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import user_authenticated_for_tournament
|
||||
from bracket.routes.models import SingleTeamResponse, SuccessResponse, TeamsWithPlayersResponse
|
||||
from bracket.routes.util import team_dependency, team_with_players_dependency
|
||||
from bracket.schema import players, teams
|
||||
from bracket.utils.db import fetch_one_parsed
|
||||
from bracket.utils.sql import get_teams_with_members
|
||||
from bracket.utils.sql import get_rounds_with_matches, get_teams_with_members
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -38,30 +41,30 @@ async def update_team_members(team_id: int, tournament_id: int, player_ids: list
|
||||
async def get_teams(
|
||||
tournament_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
|
||||
) -> TeamsWithPlayersResponse:
|
||||
return await get_teams_with_members(tournament_id)
|
||||
return TeamsWithPlayersResponse.parse_obj({'data': await get_teams_with_members(tournament_id)})
|
||||
|
||||
|
||||
@router.patch("/tournaments/{tournament_id}/teams/{team_id}", response_model=SingleTeamResponse)
|
||||
async def update_team_by_id(
|
||||
tournament_id: int,
|
||||
team_id: int,
|
||||
team_body: TeamBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
team: Team = Depends(team_dependency),
|
||||
) -> SingleTeamResponse:
|
||||
await database.execute(
|
||||
query=teams.update().where(
|
||||
(teams.c.id == team_id) & (teams.c.tournament_id == tournament_id)
|
||||
(teams.c.id == team.id) & (teams.c.tournament_id == tournament_id)
|
||||
),
|
||||
values=team_body.dict(exclude={'player_ids'}),
|
||||
)
|
||||
await update_team_members(team_id, tournament_id, team_body.player_ids)
|
||||
await update_team_members(assert_some(team.id), tournament_id, team_body.player_ids)
|
||||
|
||||
return SingleTeamResponse(
|
||||
data=await fetch_one_parsed(
|
||||
database,
|
||||
Team,
|
||||
teams.select().where(
|
||||
(teams.c.id == team_id) & (teams.c.tournament_id == tournament_id)
|
||||
(teams.c.id == team.id) & (teams.c.tournament_id == tournament_id)
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -69,11 +72,27 @@ async def update_team_by_id(
|
||||
|
||||
@router.delete("/tournaments/{tournament_id}/teams/{team_id}", response_model=SuccessResponse)
|
||||
async def delete_team(
|
||||
tournament_id: int, team_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
|
||||
tournament_id: int,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
team: TeamWithPlayers = Depends(team_with_players_dependency),
|
||||
) -> SuccessResponse:
|
||||
rounds = await get_rounds_with_matches(tournament_id, no_draft_rounds=False)
|
||||
for round in rounds:
|
||||
if team.id in round.get_team_ids():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Could not delete team that participates in matches in the tournament",
|
||||
)
|
||||
|
||||
if len(team.players):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Could not delete team that still has players in it",
|
||||
)
|
||||
|
||||
await database.execute(
|
||||
query=teams.delete().where(
|
||||
teams.c.id == team_id and teams.c.tournament_id == tournament_id
|
||||
teams.c.id == team.id and teams.c.tournament_id == tournament_id
|
||||
),
|
||||
)
|
||||
await recalculate_elo_for_tournament_id(tournament_id)
|
||||
@@ -91,7 +110,7 @@ async def create_team(
|
||||
values=TeamToInsert(
|
||||
**team_to_insert.dict(exclude={'player_ids'}),
|
||||
created=datetime_utc.now(),
|
||||
tournament_id=tournament_id
|
||||
tournament_id=tournament_id,
|
||||
).dict(),
|
||||
)
|
||||
await update_team_members(last_record_id, tournament_id, team_to_insert.player_ids)
|
||||
|
||||
84
backend/bracket/routes/util.py
Normal file
84
backend/bracket/routes/util.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from fastapi import HTTPException
|
||||
from starlette import status
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.match import Match
|
||||
from bracket.models.db.round import Round, RoundWithMatches
|
||||
from bracket.models.db.team import Team, TeamWithPlayers
|
||||
from bracket.schema import matches, rounds, teams
|
||||
from bracket.utils.db import fetch_one_parsed
|
||||
from bracket.utils.sql import get_rounds_with_matches, get_teams_with_members
|
||||
|
||||
|
||||
async def round_dependency(tournament_id: int, round_id: int) -> Round:
|
||||
round_ = await fetch_one_parsed(
|
||||
database,
|
||||
Round,
|
||||
rounds.select().where(rounds.c.id == round_id and matches.c.tournament_id == tournament_id),
|
||||
)
|
||||
|
||||
if round_ is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Could not find round with id {round_id}",
|
||||
)
|
||||
|
||||
return round_
|
||||
|
||||
|
||||
async def round_with_matches_dependency(tournament_id: int, round_id: int) -> RoundWithMatches:
|
||||
rounds = await get_rounds_with_matches(tournament_id, no_draft_rounds=False, round_id=round_id)
|
||||
|
||||
if len(rounds) < 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Could not find round with id {round_id}",
|
||||
)
|
||||
|
||||
return rounds[0]
|
||||
|
||||
|
||||
async def match_dependency(tournament_id: int, match_id: int) -> Match:
|
||||
match = await fetch_one_parsed(
|
||||
database,
|
||||
Match,
|
||||
matches.select().where(
|
||||
matches.c.id == match_id and matches.c.tournament_id == tournament_id
|
||||
),
|
||||
)
|
||||
|
||||
if match is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Could not find match with id {match_id}",
|
||||
)
|
||||
|
||||
return match
|
||||
|
||||
|
||||
async def team_dependency(tournament_id: int, team_id: int) -> Team:
|
||||
team = await fetch_one_parsed(
|
||||
database,
|
||||
Team,
|
||||
teams.select().where(teams.c.id == team_id and teams.c.tournament_id == tournament_id),
|
||||
)
|
||||
|
||||
if team is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Could not find team with id {team_id}",
|
||||
)
|
||||
|
||||
return team
|
||||
|
||||
|
||||
async def team_with_players_dependency(tournament_id: int, team_id: int) -> TeamWithPlayers:
|
||||
teams = await get_teams_with_members(tournament_id, team_id=team_id)
|
||||
|
||||
if len(teams) < 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Could not find team with id {team_id}",
|
||||
)
|
||||
|
||||
return teams[0]
|
||||
@@ -47,6 +47,7 @@ matches = Table(
|
||||
Column('team2_id', BigInteger, ForeignKey('teams.id'), nullable=False),
|
||||
Column('team1_score', Integer, nullable=False),
|
||||
Column('team2_score', Integer, nullable=False),
|
||||
Column('label', String, nullable=False),
|
||||
)
|
||||
|
||||
teams = Table(
|
||||
|
||||
@@ -56,6 +56,7 @@ DUMMY_MATCH1 = Match(
|
||||
team2_id=2,
|
||||
team1_score=11,
|
||||
team2_score=22,
|
||||
label='Court 1 | 11:00 - 11:20',
|
||||
)
|
||||
|
||||
DUMMY_MATCH2 = Match(
|
||||
@@ -65,6 +66,7 @@ DUMMY_MATCH2 = Match(
|
||||
team2_id=4,
|
||||
team1_score=9,
|
||||
team2_score=6,
|
||||
label='Court 2 | 11:00 - 11:20',
|
||||
)
|
||||
|
||||
DUMMY_MATCH3 = Match(
|
||||
@@ -74,6 +76,7 @@ DUMMY_MATCH3 = Match(
|
||||
team2_id=4,
|
||||
team1_score=23,
|
||||
team2_score=26,
|
||||
label='Court 1 | 11:30 - 11:50',
|
||||
)
|
||||
|
||||
DUMMY_MATCH4 = Match(
|
||||
@@ -83,6 +86,7 @@ DUMMY_MATCH4 = Match(
|
||||
team2_id=3,
|
||||
team1_score=43,
|
||||
team2_score=45,
|
||||
label='Court 2 | 11:30 - 11:50',
|
||||
)
|
||||
|
||||
DUMMY_USER = User(
|
||||
|
||||
@@ -4,12 +4,16 @@ from bracket.database import database
|
||||
from bracket.models.db.round import RoundWithMatches
|
||||
from bracket.models.db.team import TeamWithPlayers
|
||||
from bracket.routes.models import RoundsWithMatchesResponse, TeamsWithPlayersResponse
|
||||
from bracket.utils.types import dict_without_none
|
||||
|
||||
|
||||
async def get_rounds_with_matches(
|
||||
tournament_id: int, no_draft_rounds: bool = False
|
||||
) -> RoundsWithMatchesResponse:
|
||||
tournament_id: int,
|
||||
no_draft_rounds: bool = False,
|
||||
round_id: int | None = None,
|
||||
) -> list[RoundWithMatches]:
|
||||
draft_filter = 'AND rounds.is_draft IS FALSE' if no_draft_rounds else ''
|
||||
round_filter = 'AND rounds.id = :round_id' if round_id is not None else ''
|
||||
query = f'''
|
||||
WITH teams_with_players AS (
|
||||
SELECT DISTINCT ON (teams.id)
|
||||
@@ -34,12 +38,12 @@ async def get_rounds_with_matches(
|
||||
LEFT JOIN matches_with_teams m on rounds.id = m.round_id
|
||||
WHERE rounds.tournament_id = :tournament_id
|
||||
{draft_filter}
|
||||
{round_filter}
|
||||
GROUP BY rounds.id
|
||||
'''
|
||||
result = await database.fetch_all(query=query, values={'tournament_id': tournament_id})
|
||||
return RoundsWithMatchesResponse.parse_obj(
|
||||
{'data': [RoundWithMatches.parse_obj(x._mapping) for x in result]}
|
||||
)
|
||||
values = dict_without_none({'tournament_id': tournament_id, 'round_id': round_id})
|
||||
result = await database.fetch_all(query=query, values=values)
|
||||
return [RoundWithMatches.parse_obj(x._mapping) for x in result]
|
||||
|
||||
|
||||
async def get_next_round_name(database: Database, tournament_id: int) -> str:
|
||||
@@ -54,21 +58,22 @@ async def get_next_round_name(database: Database, tournament_id: int) -> str:
|
||||
|
||||
|
||||
async def get_teams_with_members(
|
||||
tournament_id: int, *, only_active_teams: bool = False
|
||||
) -> TeamsWithPlayersResponse:
|
||||
teams_filter = 'AND teams.active IS TRUE' if only_active_teams else ''
|
||||
tournament_id: int, *, only_active_teams: bool = False, team_id: int | None = None
|
||||
) -> list[TeamWithPlayers]:
|
||||
active_team_filter = 'AND teams.active IS TRUE' if only_active_teams else ''
|
||||
team_id_filter = 'AND teams.id = :team_id' if team_id is not None else ''
|
||||
query = f'''
|
||||
SELECT teams.*, to_json(array_agg(players.*)) AS players
|
||||
FROM teams
|
||||
LEFT JOIN players ON players.team_id = teams.id
|
||||
WHERE teams.tournament_id = :tournament_id
|
||||
{teams_filter}
|
||||
{active_team_filter}
|
||||
{team_id_filter}
|
||||
GROUP BY teams.id;
|
||||
'''
|
||||
result = await database.fetch_all(query=query, values={'tournament_id': tournament_id})
|
||||
return TeamsWithPlayersResponse.parse_obj(
|
||||
{'data': [TeamWithPlayers.parse_obj(x._mapping) for x in result]}
|
||||
)
|
||||
values = dict_without_none({'tournament_id': tournament_id, 'team_id': team_id})
|
||||
result = await database.fetch_all(query=query, values=values)
|
||||
return [TeamWithPlayers.parse_obj(x._mapping) for x in result]
|
||||
|
||||
|
||||
async def get_user_access_to_tournament(tournament_id: int, user_id: int) -> bool:
|
||||
|
||||
@@ -31,3 +31,7 @@ class EnumAutoStr(EnumValues):
|
||||
def assert_some(result: T | None) -> T:
|
||||
assert result is not None
|
||||
return result
|
||||
|
||||
|
||||
def dict_without_none(input: dict[Any, Any]) -> dict[Any, Any]:
|
||||
return {k: v for k, v in input.items() if v is not None}
|
||||
|
||||
@@ -32,6 +32,7 @@ async def test_create_match(
|
||||
'team1_id': team1_inserted.id,
|
||||
'team2_id': team2_inserted.id,
|
||||
'round_id': round_inserted.id,
|
||||
'label': 'Some label',
|
||||
}
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
@@ -81,7 +82,12 @@ async def test_update_match(
|
||||
}
|
||||
)
|
||||
) as match_inserted:
|
||||
body = {'team1_score': 42, 'team2_score': 24, 'round_id': round_inserted.id}
|
||||
body = {
|
||||
'team1_score': 42,
|
||||
'team2_score': 24,
|
||||
'round_id': round_inserted.id,
|
||||
'label': 'Some label',
|
||||
}
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
HTTPMethod.PATCH,
|
||||
@@ -99,6 +105,7 @@ async def test_update_match(
|
||||
)
|
||||
assert patched_match.team1_score == body['team1_score']
|
||||
assert patched_match.team2_score == body['team2_score']
|
||||
assert patched_match.label == body['label']
|
||||
|
||||
await assert_row_count_and_clear(matches, 1)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ def test_elo_calculation() -> None:
|
||||
round_ = RoundWithMatches(
|
||||
tournament_id=1,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
is_draft=True,
|
||||
is_draft=False,
|
||||
is_active=False,
|
||||
name='Some round',
|
||||
matches=[
|
||||
@@ -22,6 +22,7 @@ def test_elo_calculation() -> None:
|
||||
team1_score=3,
|
||||
team2_score=4,
|
||||
round_id=1,
|
||||
label='Some label',
|
||||
team1=TeamWithPlayers(
|
||||
name='Dummy team 1',
|
||||
tournament_id=1,
|
||||
|
||||
@@ -2,8 +2,6 @@ coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 100%
|
||||
threshold: 100%
|
||||
informational: true
|
||||
wait_for_ci: false
|
||||
require_ci_to_pass: false
|
||||
|
||||
@@ -1 +1,110 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><g fill="none" fill-rule="evenodd"><rect width="500" height="500" fill="#339AF0" rx="250"/><g fill="#FFF"><path fill-rule="nonzero" d="M202.055 135.706c-6.26 8.373-4.494 20.208 3.944 26.42 29.122 21.45 45.824 54.253 45.824 90.005 0 35.752-16.702 68.559-45.824 90.005-8.436 6.215-10.206 18.043-3.944 26.42 6.26 8.378 18.173 10.13 26.611 3.916a153.835 153.835 0 0024.509-22.54h53.93c10.506 0 19.023-8.455 19.023-18.885 0-10.43-8.517-18.886-19.023-18.886h-29.79c8.196-18.594 12.553-38.923 12.553-60.03s-4.357-41.436-12.552-60.03h29.79c10.505 0 19.022-8.455 19.022-18.885 0-10.43-8.517-18.886-19.023-18.886h-53.93a153.835 153.835 0 00-24.509-22.54c-8.438-6.215-20.351-4.46-26.61 3.916z"/><path d="M171.992 246.492c0-15.572 12.624-28.195 28.196-28.195 15.572 0 28.195 12.623 28.195 28.195 0 15.572-12.623 28.196-28.195 28.196-15.572 0-28.196-12.624-28.196-28.196z"/></g></g></svg>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="73.701149mm"
|
||||
height="73.701828mm"
|
||||
viewBox="0 0 73.701149 73.701828"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:export-filename="/home/erik/code/bracket/frontend/public/favicon.png"
|
||||
inkscape:export-xdpi="400.06485"
|
||||
inkscape:export-ydpi="400.06485"
|
||||
sodipodi:docname="favicon.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.2268341"
|
||||
inkscape:cx="118.53724"
|
||||
inkscape:cy="143.63924"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1376"
|
||||
inkscape:window-x="3840"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient37147">
|
||||
<stop
|
||||
style="stop-color:#6948e8;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop37143" />
|
||||
<stop
|
||||
style="stop-color:#9379f5;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop37145" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient37147"
|
||||
id="linearGradient37149"
|
||||
x1="197.95337"
|
||||
y1="200.584"
|
||||
x2="42.395298"
|
||||
y2="31.763771"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.31598688,0,0,0.31598688,66.116499,102.77293)" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-66.116493,-102.77294)">
|
||||
<path
|
||||
id="path33666"
|
||||
style="fill:url(#linearGradient37149);fill-opacity:1;fill-rule:evenodd;stroke-width:0.315986"
|
||||
d="m 102.9672,102.77294 a 36.850776,36.850776 0 0 0 -36.850707,36.85078 36.850776,36.850776 0 0 0 31.797454,36.48845 l -13.091302,-13.09121 0.639774,-19.35673 -1.781779,7.8368 -9.307407,-9.30749 3.464113,-2.45816 7.625073,3.92885 -2.9291,-5.22051 6.111755,-18.921 9.453135,-5.4637 25.546141,25.54614 -2.52109,-23.28549 18.34328,18.34328 a 36.850776,36.850776 0 0 0 -36.49899,-31.89001 z" />
|
||||
<path
|
||||
id="path33606"
|
||||
style="fill:#5337b9;fill-opacity:1;fill-rule:evenodd;stroke-width:0.315986"
|
||||
d="m 139.4662,134.66295 -18.34329,-18.34328 2.52109,23.28549 -25.54614,-25.54614 -9.453134,5.4637 -6.111756,18.921 2.9291,5.22051 -0.639774,19.35673 13.091302,13.09121 a 36.850776,36.850776 0 0 0 5.053252,0.3626 36.850776,36.850776 0 0 0 36.85079,-36.8507 36.850776,36.850776 0 0 0 -0.35214,-4.96077 z" />
|
||||
<path
|
||||
id="path53"
|
||||
style="fill:#4629ae;fill-opacity:1;fill-rule:evenodd;stroke-width:0.315986"
|
||||
d="m 77.836997,139.73538 -3.464112,2.45816 9.307407,9.30749 1.781778,-7.8368 z" />
|
||||
<path
|
||||
id="path33645"
|
||||
style="fill:#7755f5;fill-opacity:1;fill-rule:evenodd;stroke-width:0.315986"
|
||||
d="m 112.88997,117.52931 -5.05334,1.66942 8.04345,8.04407 1.59908,-5.46492 z" />
|
||||
<path
|
||||
id="path33668"
|
||||
style="fill:#4629ae;fill-opacity:1;fill-rule:evenodd;stroke-width:0.315986"
|
||||
d="m 112.88997,117.52931 4.58919,4.24857 -1.59908,5.46492 -8.04345,-8.04407 z" />
|
||||
<g
|
||||
aria-label="{}"
|
||||
id="text1281"
|
||||
style="font-size:46.3909px;line-height:1.25;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;stroke-width:1.15977"
|
||||
transform="matrix(1.1942812,0,0,1.1942812,-20.004416,-27.126378)">
|
||||
<path
|
||||
d="m 98.88979,156.70333 v 4.3265 h -3.397771 q -5.6403,0 -7.565704,-1.67623 -1.902751,-1.67624 -1.902751,-6.68229 v -4.87014 q 0,-3.42042 -1.223198,-4.71157 -1.200546,-1.31381 -4.371799,-1.31381 h -1.404412 v -4.32649 h 1.404412 q 3.171253,0 4.371799,-1.29115 1.223198,-1.29116 1.223198,-4.73423 v -4.87014 q 0,-5.00605 1.902751,-6.65963 1.925404,-1.67623 7.565704,-1.67623 h 3.397771 v 4.30384 h -2.786173 q -2.355787,0 -3.148601,0.95137 -0.792813,0.95138 -0.792813,4.19059 v 4.73423 q 0,3.73754 -1.109938,5.25522 -1.109939,1.51767 -3.964067,1.94805 2.854128,0.47569 3.964067,2.01601 1.109938,1.54032 1.109938,5.25522 v 4.64362 q 0,3.26186 0.792813,4.21324 0.792814,0.97402 3.148601,0.97402 z"
|
||||
style="font-weight:bold;-inkscape-font-specification:'monospace, Bold'"
|
||||
id="path33435" />
|
||||
<path
|
||||
d="m 107.04444,156.70333 h 2.74087 q 2.35579,0 3.17125,-0.97402 0.81547,-0.97403 0.81547,-4.21324 v -4.64362 q 0,-3.7149 1.10994,-5.25522 1.10993,-1.54032 3.94141,-2.01601 -2.85413,-0.43038 -3.96407,-1.94805 -1.08728,-1.51768 -1.08728,-5.25522 v -4.73423 q 0,-3.19391 -0.81547,-4.16793 -0.79281,-0.97403 -3.17125,-0.97403 h -2.74087 v -4.30384 h 3.39777 q 5.61765,0 7.49775,1.67623 1.90275,1.65358 1.90275,6.65963 v 4.87014 q 0,3.42042 1.2232,4.73423 1.24585,1.29115 4.43975,1.29115 h 1.40441 v 4.32649 h -1.40441 q -3.1939,0 -4.43975,1.31381 -1.2232,1.3138 -1.2232,4.71157 v 4.87014 q 0,5.00605 -1.90275,6.68229 -1.8801,1.67623 -7.49775,1.67623 h -3.39777 z"
|
||||
style="font-weight:bold;-inkscape-font-specification:'monospace, Bold'"
|
||||
id="path33437" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 5.6 KiB |
@@ -21,7 +21,7 @@ export default function Brackets({
|
||||
}
|
||||
|
||||
const rounds = swrRoundsResponse.data.data.map((round: RoundInterface) => (
|
||||
<Grid.Col sm={6} lg={3}>
|
||||
<Grid.Col sm={6} lg={4} xl={3}>
|
||||
<Round
|
||||
key={round.id}
|
||||
tournamentData={tournamentData}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { Grid, Tooltip, UnstyledButton, createStyles, useMantineTheme } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Center,
|
||||
Grid,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
createStyles,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { Property } from 'csstype';
|
||||
import React, { useState } from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import { MatchInterface } from '../../interfaces/match';
|
||||
import { TournamentMinimal } from '../../interfaces/tournament';
|
||||
import MatchModal from '../modals/match_modal';
|
||||
|
||||
import Visibility = Property.Visibility;
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
marginTop: '20px',
|
||||
marginTop: '30px',
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: 'darkgray',
|
||||
@@ -29,7 +40,18 @@ const useStyles = createStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export default function Game({
|
||||
function MatchBadge({ match }: { match: MatchInterface }) {
|
||||
const visibility: Visibility = match.label === '' ? 'hidden' : 'visible';
|
||||
return (
|
||||
<Center style={{ transform: 'translateY(50%)', visibility }}>
|
||||
<Badge size="lg" variant="filled">
|
||||
{match.label}
|
||||
</Badge>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Match({
|
||||
swrRoundsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
tournamentData,
|
||||
@@ -53,12 +75,16 @@ export default function Game({
|
||||
const team1_players = match.team1.players.map((player) => player.name).join(', ');
|
||||
const team2_players = match.team2.players.map((player) => player.name).join(', ');
|
||||
|
||||
const team1_players_label = team1_players === '' ? 'No players' : team1_players;
|
||||
const team2_players_label = team2_players === '' ? 'No players' : team2_players;
|
||||
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const bracket = (
|
||||
<>
|
||||
<MatchBadge match={match} />
|
||||
<div className={classes.top} style={team1_style}>
|
||||
<Tooltip label={team1_players} withArrow color="blue">
|
||||
<Tooltip label={team1_players_label} withArrow color="blue">
|
||||
<Grid grow>
|
||||
<Grid.Col span={10}>{match.team1.name}</Grid.Col>
|
||||
<Grid.Col span={2}>{match.team1_score}</Grid.Col>
|
||||
@@ -67,7 +93,7 @@ export default function Game({
|
||||
</div>
|
||||
<div className={classes.divider} />
|
||||
<div className={classes.bottom} style={team2_style}>
|
||||
<Tooltip label={team2_players} position="bottom" withArrow color="blue">
|
||||
<Tooltip label={team2_players_label} position="bottom" withArrow color="blue">
|
||||
<Grid grow>
|
||||
<Grid.Col span={10}>{match.team2.name}</Grid.Col>
|
||||
<Grid.Col span={2}>{match.team2_score}</Grid.Col>
|
||||
@@ -5,7 +5,7 @@ import { SWRResponse } from 'swr';
|
||||
import { RoundInterface } from '../../interfaces/round';
|
||||
import { TournamentMinimal } from '../../interfaces/tournament';
|
||||
import RoundModal from '../modals/round_modal';
|
||||
import Game from './game';
|
||||
import Match from './match';
|
||||
|
||||
export default function Round({
|
||||
tournamentData,
|
||||
@@ -20,16 +20,18 @@ export default function Round({
|
||||
swrUpcomingMatchesResponse: SWRResponse | null;
|
||||
readOnly: boolean;
|
||||
}) {
|
||||
const games = round.matches.map((match) => (
|
||||
<Game
|
||||
key={match.id}
|
||||
tournamentData={tournamentData}
|
||||
swrRoundsResponse={swrRoundsResponse}
|
||||
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
|
||||
match={match}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
));
|
||||
const matches = round.matches
|
||||
.sort((m1, m2) => (m1.label > m2.label ? 1 : 0))
|
||||
.map((match) => (
|
||||
<Match
|
||||
key={match.id}
|
||||
tournamentData={tournamentData}
|
||||
swrRoundsResponse={swrRoundsResponse}
|
||||
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
|
||||
match={match}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
));
|
||||
const active_round_style = round.is_active
|
||||
? {
|
||||
borderStyle: 'solid',
|
||||
@@ -68,7 +70,7 @@ export default function Round({
|
||||
}}
|
||||
>
|
||||
<Center>{modal}</Center>
|
||||
{games}
|
||||
{matches}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Group, Progress, Text, useMantineTheme } from '@mantine/core';
|
||||
|
||||
interface ELOProps {
|
||||
elo_score: number;
|
||||
max_elo_score: number;
|
||||
}
|
||||
|
||||
export function PlayerELOScore({ elo_score, max_elo_score }: ELOProps) {
|
||||
const theme = useMantineTheme();
|
||||
const percentageScale = 100.0 / max_elo_score;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group position="apart">
|
||||
<Text size="xs" color="blue" weight={700}>
|
||||
{elo_score.toFixed(0)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
sections={[
|
||||
{
|
||||
value: percentageScale * elo_score,
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.blue[9] : theme.colors.blue[6],
|
||||
tooltip: `ELO Score (${(percentageScale * elo_score).toFixed(0)}%)`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
34
frontend/src/components/info/player_score.tsx
Normal file
34
frontend/src/components/info/player_score.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Group, Progress, Text, useMantineTheme } from '@mantine/core';
|
||||
import { DefaultMantineColor } from '@mantine/styles/lib/theme/types/MantineColor';
|
||||
|
||||
interface ScoreProps {
|
||||
score: number;
|
||||
max_score: number;
|
||||
color: DefaultMantineColor;
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export function PlayerScore({ score, max_score, color, decimals }: ScoreProps) {
|
||||
const theme = useMantineTheme();
|
||||
const percentageScale = 100.0 / max_score;
|
||||
const base_color = theme.colors[color];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group position="apart">
|
||||
<Text size="xs" color={color} weight={700}>
|
||||
{score.toFixed(0)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
sections={[
|
||||
{
|
||||
value: percentageScale * score,
|
||||
color: theme.colorScheme === 'dark' ? base_color[9] : base_color[6],
|
||||
tooltip: `ELO Score (${(percentageScale * score).toFixed(decimals)}%)`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Modal, NumberInput } from '@mantine/core';
|
||||
import { Button, Modal, NumberInput, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
@@ -27,6 +27,7 @@ export default function MatchModal({
|
||||
initialValues: {
|
||||
team1_score: match != null ? match.team1_score : 0,
|
||||
team2_score: match != null ? match.team2_score : 0,
|
||||
label: match != null ? match.label : '',
|
||||
},
|
||||
|
||||
validate: {
|
||||
@@ -45,6 +46,7 @@ export default function MatchModal({
|
||||
round_id: match.round_id,
|
||||
team1_score: values.team1_score,
|
||||
team2_score: values.team2_score,
|
||||
label: values.label,
|
||||
};
|
||||
await updateMatch(tournamentData.id, match.id, newMatch);
|
||||
await swrRoundsResponse.mutate(null);
|
||||
@@ -65,6 +67,13 @@ export default function MatchModal({
|
||||
placeholder={`Score of ${match.team2.name}`}
|
||||
{...form.getInputProps('team2_score')}
|
||||
/>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
style={{ marginTop: 20 }}
|
||||
label="Label for this match"
|
||||
placeholder="Court 1 | 11:30 - 12:00"
|
||||
{...form.getInputProps('label')}
|
||||
/>
|
||||
<Button fullWidth style={{ marginTop: 20 }} color="green" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
|
||||
@@ -2,11 +2,12 @@ import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Group,
|
||||
Image,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
useMantineColorScheme,
|
||||
} from '@mantine/core';
|
||||
import { IconBrackets, IconMoonStars, IconSun } from '@tabler/icons';
|
||||
import { IconMoonStars, IconSun } from '@tabler/icons';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
@@ -26,15 +27,17 @@ export function Brand() {
|
||||
})}
|
||||
>
|
||||
<Group position="apart">
|
||||
<IconBrackets size={36} style={{ marginBottom: 10 }} />
|
||||
<UnstyledButton>
|
||||
<Title
|
||||
onClick={() => {
|
||||
router.push('/');
|
||||
}}
|
||||
>
|
||||
Bracket
|
||||
</Title>
|
||||
<Group>
|
||||
<Image src="/favicon.svg" width="50px" height="50px" mt="-8px" />
|
||||
<Title
|
||||
onClick={() => {
|
||||
router.push('/');
|
||||
}}
|
||||
>
|
||||
Bracket
|
||||
</Title>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
<ActionIcon variant="default" onClick={() => toggleColorScheme()} size={30}>
|
||||
{colorScheme === 'dark' ? <IconSun size={16} /> : <IconMoonStars size={16} />}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Player } from '../../interfaces/player';
|
||||
import { TournamentMinimal } from '../../interfaces/tournament';
|
||||
import { deletePlayer } from '../../services/player';
|
||||
import DeleteButton from '../buttons/delete';
|
||||
import { PlayerELOScore } from '../info/player_elo_score';
|
||||
import { PlayerScore } from '../info/player_score';
|
||||
import { PlayerStatistics } from '../info/player_statistics';
|
||||
import PlayerModal from '../modals/player_modal';
|
||||
import DateTime from '../utils/datetime';
|
||||
@@ -23,6 +23,7 @@ export default function PlayersTable({
|
||||
const tableState = getTableState('name');
|
||||
|
||||
const maxELOScore = Math.max(...players.map((player) => player.elo_score));
|
||||
const maxSwissScore = Math.max(...players.map((player) => player.swiss_score));
|
||||
|
||||
if (swrPlayersResponse.error) return <RequestErrorAlert error={swrPlayersResponse.error} />;
|
||||
|
||||
@@ -38,7 +39,20 @@ export default function PlayersTable({
|
||||
<PlayerStatistics wins={player.wins} draws={player.draws} losses={player.losses} />
|
||||
</td>
|
||||
<td>
|
||||
<PlayerELOScore elo_score={player.elo_score} max_elo_score={maxELOScore} />
|
||||
<PlayerScore
|
||||
score={player.elo_score}
|
||||
max_score={maxELOScore}
|
||||
color="indigo"
|
||||
decimals={0}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<PlayerScore
|
||||
score={player.swiss_score}
|
||||
max_score={maxSwissScore}
|
||||
color="grape"
|
||||
decimals={1}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<PlayerModal
|
||||
@@ -71,6 +85,9 @@ export default function PlayersTable({
|
||||
<ThSortable state={tableState} field="elo_score">
|
||||
ELO score
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="swiss_score">
|
||||
Swiss score
|
||||
</ThSortable>
|
||||
<ThNotSortable>{null}</ThNotSortable>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -57,9 +57,9 @@ export const setSorting = (state: TableState, newSortField: string) => {
|
||||
|
||||
export const getTableState = (
|
||||
initial_sort_field: string,
|
||||
initial_sort_direection: boolean = true
|
||||
initial_sort_direction: boolean = true
|
||||
) => {
|
||||
const [reversed, setReversed] = useState(initial_sort_direection);
|
||||
const [reversed, setReversed] = useState(initial_sort_direction);
|
||||
const [sortField, setSortField] = useState(initial_sort_field);
|
||||
return {
|
||||
sortField,
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function UpcomingMatchesTable({
|
||||
team1_id: upcoming_match.team1.id,
|
||||
team2_id: upcoming_match.team2.id,
|
||||
round_id,
|
||||
label: '',
|
||||
};
|
||||
await createMatch(tournamentData.id, match_to_schedule);
|
||||
await swrRoundsResponse.mutate(null);
|
||||
@@ -45,7 +46,7 @@ export default function UpcomingMatchesTable({
|
||||
sortTableEntries(m1, m2, tableState)
|
||||
)
|
||||
.map((upcoming_match: UpcomingMatchInterface) => (
|
||||
<tr key={upcoming_match.elo_diff}>
|
||||
<tr key={`${upcoming_match.team1.id} - ${upcoming_match.team2.id}`}>
|
||||
<td>
|
||||
<PlayerList team={upcoming_match.team1} />
|
||||
</td>
|
||||
@@ -74,10 +75,10 @@ export default function UpcomingMatchesTable({
|
||||
<TableLayout>
|
||||
<thead>
|
||||
<tr>
|
||||
<ThSortable state={tableState} field="name">
|
||||
<ThSortable state={tableState} field="team1.name">
|
||||
Team 1
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="name">
|
||||
<ThSortable state={tableState} field="team2.name">
|
||||
Team 2
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="elo_diff">
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface MatchInterface {
|
||||
team2_score: number;
|
||||
team1: TeamInterface;
|
||||
team2: TeamInterface;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface MatchBodyInterface {
|
||||
@@ -15,6 +16,7 @@ export interface MatchBodyInterface {
|
||||
round_id: number;
|
||||
team1_score: number;
|
||||
team2_score: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface UpcomingMatchInterface {
|
||||
@@ -28,4 +30,5 @@ export interface MatchCreateBodyInterface {
|
||||
round_id: number;
|
||||
team1_id: number;
|
||||
team2_id: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface Player {
|
||||
tournament_id: number;
|
||||
team_id: number;
|
||||
elo_score: number;
|
||||
swiss_score: number;
|
||||
wins: number;
|
||||
draws: number;
|
||||
losses: number;
|
||||
|
||||
@@ -12,14 +12,16 @@ export default function Login() {
|
||||
const router = useRouter();
|
||||
|
||||
async function attemptLogin(email: string, password: string) {
|
||||
await performLogin(email, password);
|
||||
showNotification({
|
||||
color: 'green',
|
||||
title: 'Login successful',
|
||||
message: '',
|
||||
});
|
||||
const success = await performLogin(email, password);
|
||||
if (success) {
|
||||
showNotification({
|
||||
color: 'green',
|
||||
title: 'Login successful',
|
||||
message: '',
|
||||
});
|
||||
|
||||
await router.push('/');
|
||||
await router.push('/');
|
||||
}
|
||||
}
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
|
||||
@@ -8,6 +8,12 @@ import { getTournamentIdFromRouter } from '../../../components/utils/util';
|
||||
import { Tournament } from '../../../interfaces/tournament';
|
||||
import { getBaseApiUrl, getRounds, getTournaments } from '../../../services/adapter';
|
||||
|
||||
function TournamentLogo({ tournamentDataFull }: { tournamentDataFull: Tournament }) {
|
||||
return tournamentDataFull.logo_path ? (
|
||||
<Image radius="lg" src={`${getBaseApiUrl()}/static/${tournamentDataFull.logo_path}`} />
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { tournamentData } = getTournamentIdFromRouter();
|
||||
const swrRoundsResponse: SWRResponse = getRounds(tournamentData.id, true);
|
||||
@@ -24,15 +30,12 @@ export default function Dashboard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
grow
|
||||
style={{ marginBottom: '20px', marginTop: '20px', marginLeft: '20px', marginRight: '20px' }}
|
||||
>
|
||||
<Grid.Col span={3}>
|
||||
<Grid grow style={{ margin: '20px' }}>
|
||||
<Grid.Col span={2}>
|
||||
<Title>{tournamentDataFull.name}</Title>
|
||||
<Image radius="lg" src={`${getBaseApiUrl()}/static/${tournamentDataFull.logo_path}`} />
|
||||
<TournamentLogo tournamentDataFull={tournamentDataFull} />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
<Grid.Col span={10}>
|
||||
<Brackets
|
||||
tournamentData={tournamentData}
|
||||
swrRoundsResponse={swrRoundsResponse}
|
||||
|
||||
@@ -4,12 +4,14 @@ import useSWR, { SWRResponse } from 'swr';
|
||||
|
||||
const axios = require('axios').default;
|
||||
|
||||
export function handleRequestError(error: any) {
|
||||
showNotification({
|
||||
color: 'red',
|
||||
title: 'Default notification',
|
||||
message: error.response.data.detail.toString(),
|
||||
});
|
||||
export function handleRequestError(response: any) {
|
||||
if (response.response != null && response.response.data.detail != null) {
|
||||
showNotification({
|
||||
color: 'red',
|
||||
title: 'An error occurred',
|
||||
message: response.response.data.detail.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function checkForAuthError(response: any) {
|
||||
@@ -50,29 +52,23 @@ export function getClubs(): SWRResponse {
|
||||
return useSWR('clubs', fetcher);
|
||||
}
|
||||
|
||||
export function getTournaments(): SWRResponse<any, any> {
|
||||
export function getTournaments(): SWRResponse {
|
||||
return useSWR('tournaments', fetcher);
|
||||
}
|
||||
|
||||
export function getPlayers(
|
||||
tournament_id: number,
|
||||
not_in_team: boolean = false
|
||||
): SWRResponse<any, any> {
|
||||
export function getPlayers(tournament_id: number, not_in_team: boolean = false): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/players?not_in_team=${not_in_team}`, fetcher);
|
||||
}
|
||||
|
||||
export function getTeams(tournament_id: number): SWRResponse<any, any> {
|
||||
export function getTeams(tournament_id: number): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/teams`, fetcher);
|
||||
}
|
||||
|
||||
export function getRounds(
|
||||
tournament_id: number,
|
||||
no_draft_rounds: boolean = false
|
||||
): SWRResponse<any, any> {
|
||||
export function getRounds(tournament_id: number, no_draft_rounds: boolean = false): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/rounds?no_draft_rounds=${no_draft_rounds}`, fetcher);
|
||||
}
|
||||
|
||||
export function getUpcomingMatches(tournament_id: number): SWRResponse<any, any> {
|
||||
export function getUpcomingMatches(tournament_id: number): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/upcoming_matches`, fetcher);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createAxios } from './adapter';
|
||||
|
||||
export async function createTeam(
|
||||
tournament_id: number,
|
||||
name: string,
|
||||
active: boolean,
|
||||
player_ids: number[]
|
||||
) {
|
||||
await createAxios().post(`tournaments/${tournament_id}/teams`, {
|
||||
name,
|
||||
active,
|
||||
player_ids,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteTeam(tournament_id: number, team_id: number) {
|
||||
await createAxios().delete(`tournaments/${tournament_id}/teams/${team_id}`);
|
||||
}
|
||||
|
||||
export async function updateTeam(
|
||||
tournament_id: number,
|
||||
team_id: number,
|
||||
name: string,
|
||||
active: boolean,
|
||||
player_ids: number[]
|
||||
) {
|
||||
await createAxios().patch(`tournaments/${tournament_id}/teams/${team_id}`, {
|
||||
name,
|
||||
active,
|
||||
player_ids,
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import { MatchBodyInterface, MatchCreateBodyInterface } from '../interfaces/match';
|
||||
import { createAxios } from './adapter';
|
||||
import { createAxios, handleRequestError } from './adapter';
|
||||
|
||||
export async function createMatch(tournament_id: number, match: MatchCreateBodyInterface) {
|
||||
return createAxios().post(`tournaments/${tournament_id}/matches`, match);
|
||||
return createAxios()
|
||||
.post(`tournaments/${tournament_id}/matches`, match)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function deleteMatch(tournament_id: number, match_id: number) {
|
||||
return createAxios().delete(`tournaments/${tournament_id}/matches/${match_id}`);
|
||||
return createAxios()
|
||||
.delete(`tournaments/${tournament_id}/matches/${match_id}`)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function updateMatch(
|
||||
@@ -14,5 +18,7 @@ export async function updateMatch(
|
||||
match_id: number,
|
||||
match: MatchBodyInterface
|
||||
) {
|
||||
return createAxios().patch(`tournaments/${tournament_id}/matches/${match_id}`, match);
|
||||
return createAxios()
|
||||
.patch(`tournaments/${tournament_id}/matches/${match_id}`, match)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { createAxios } from './adapter';
|
||||
import { createAxios, handleRequestError } from './adapter';
|
||||
|
||||
export async function createPlayer(tournament_id: number, name: string, team_id: string | null) {
|
||||
return createAxios().post(`tournaments/${tournament_id}/players`, {
|
||||
name,
|
||||
team_id,
|
||||
});
|
||||
return createAxios()
|
||||
.post(`tournaments/${tournament_id}/players`, {
|
||||
name,
|
||||
team_id,
|
||||
})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function deletePlayer(tournament_id: number, player_id: number) {
|
||||
return createAxios().delete(`tournaments/${tournament_id}/players/${player_id}`);
|
||||
return createAxios()
|
||||
.delete(`tournaments/${tournament_id}/players/${player_id}`)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function updatePlayer(
|
||||
@@ -17,8 +21,10 @@ export async function updatePlayer(
|
||||
name: string,
|
||||
team_id: string | null
|
||||
) {
|
||||
return createAxios().patch(`tournaments/${tournament_id}/players/${player_id}`, {
|
||||
name,
|
||||
team_id,
|
||||
});
|
||||
return createAxios()
|
||||
.patch(`tournaments/${tournament_id}/players/${player_id}`, {
|
||||
name,
|
||||
team_id,
|
||||
})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { RoundInterface } from '../interfaces/round';
|
||||
import { createAxios } from './adapter';
|
||||
import { createAxios, handleRequestError } from './adapter';
|
||||
|
||||
export async function createRound(tournament_id: number) {
|
||||
return createAxios().post(`tournaments/${tournament_id}/rounds`);
|
||||
return createAxios()
|
||||
.post(`tournaments/${tournament_id}/rounds`)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function deleteRound(tournament_id: number, round_id: number) {
|
||||
return createAxios().delete(`tournaments/${tournament_id}/rounds/${round_id}`);
|
||||
return createAxios()
|
||||
.delete(`tournaments/${tournament_id}/rounds/${round_id}`)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function updateRound(tournament_id: number, round_id: number, round: RoundInterface) {
|
||||
return createAxios().patch(`tournaments/${tournament_id}/rounds/${round_id}`, round);
|
||||
return createAxios()
|
||||
.patch(`tournaments/${tournament_id}/rounds/${round_id}`, round)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAxios } from './adapter';
|
||||
import { createAxios, handleRequestError } from './adapter';
|
||||
|
||||
export async function createTeam(
|
||||
tournament_id: number,
|
||||
@@ -14,7 +14,9 @@ export async function createTeam(
|
||||
}
|
||||
|
||||
export async function deleteTeam(tournament_id: number, team_id: number) {
|
||||
await createAxios().delete(`tournaments/${tournament_id}/teams/${team_id}`);
|
||||
await createAxios()
|
||||
.delete(`tournaments/${tournament_id}/teams/${team_id}`)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function updateTeam(
|
||||
@@ -24,9 +26,11 @@ export async function updateTeam(
|
||||
active: boolean,
|
||||
player_ids: number[]
|
||||
) {
|
||||
await createAxios().patch(`tournaments/${tournament_id}/teams/${team_id}`, {
|
||||
name,
|
||||
active,
|
||||
player_ids,
|
||||
});
|
||||
await createAxios()
|
||||
.patch(`tournaments/${tournament_id}/teams/${team_id}`, {
|
||||
name,
|
||||
active,
|
||||
player_ids,
|
||||
})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAxios } from './adapter';
|
||||
import { createAxios, handleRequestError } from './adapter';
|
||||
|
||||
export async function performLogin(username: string, password: string) {
|
||||
const bodyFormData = new FormData();
|
||||
@@ -6,12 +6,21 @@ export async function performLogin(username: string, password: string) {
|
||||
bodyFormData.append('username', username);
|
||||
bodyFormData.append('password', password);
|
||||
|
||||
const response = await createAxios().post('token', bodyFormData);
|
||||
const response = await createAxios()
|
||||
.post('token', bodyFormData)
|
||||
.catch((err_response: any) => handleRequestError(err_response));
|
||||
|
||||
if (response == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
localStorage.setItem('login', JSON.stringify(response.data));
|
||||
|
||||
handleRequestError(response);
|
||||
|
||||
// Reload axios object.
|
||||
createAxios();
|
||||
return response;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function performLogout() {
|
||||
|
||||
Reference in New Issue
Block a user