mirror of
https://github.com/evroon/bracket.git
synced 2026-04-27 10:47:05 -04:00
Add courts functionality (#256)
This commit is contained in:
52
backend/alembic/versions/3469289a7e06_create_courts_table.py
Normal file
52
backend/alembic/versions/3469289a7e06_create_courts_table.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""create courts table
|
||||
|
||||
Revision ID: 3469289a7e06
|
||||
Revises: 6458e0bc3e9d
|
||||
Create Date: 2023-09-11 13:36:28.464161
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str | None = '3469289a7e06'
|
||||
down_revision: str | None = '6458e0bc3e9d'
|
||||
branch_labels: str | None = None
|
||||
depends_on: str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
'courts',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('created', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('tournament_id', sa.BigInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
['tournament_id'],
|
||||
['tournaments.id'],
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index(op.f('ix_courts_id'), 'courts', ['id'], unique=False)
|
||||
op.add_column('matches', sa.Column('court_id', sa.BigInteger(), nullable=True))
|
||||
op.create_foreign_key('matches_courts_fkey', 'matches', 'courts', ['court_id'], ['id'])
|
||||
op.create_index(op.f('ix_courts_tournament_id'), 'courts', ['tournament_id'], unique=False)
|
||||
op.add_column(
|
||||
'tournaments',
|
||||
sa.Column('auto_assign_courts', sa.Boolean(), server_default='f', nullable=False),
|
||||
)
|
||||
op.drop_column('matches', 'label')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.add_column('matches', sa.Column('label', sa.Text(), nullable=True, server_default=''))
|
||||
op.alter_column('matches', 'label', server_default=None)
|
||||
op.drop_column('tournaments', 'auto_assign_courts')
|
||||
op.drop_index(op.f('ix_courts_tournament_id'), table_name='courts')
|
||||
op.drop_constraint('matches_courts_fkey', 'matches', type_='foreignkey')
|
||||
op.drop_column('matches', 'court_id')
|
||||
op.drop_index(op.f('ix_courts_id'), table_name='courts')
|
||||
op.drop_table('courts')
|
||||
@@ -7,7 +7,18 @@ from starlette.staticfiles import StaticFiles
|
||||
|
||||
from bracket.config import Environment, config, environment, init_sentry
|
||||
from bracket.database import database, init_db_when_empty
|
||||
from bracket.routes import auth, clubs, matches, players, rounds, stages, teams, tournaments, users
|
||||
from bracket.routes import (
|
||||
auth,
|
||||
clubs,
|
||||
courts,
|
||||
matches,
|
||||
players,
|
||||
rounds,
|
||||
stages,
|
||||
teams,
|
||||
tournaments,
|
||||
users,
|
||||
)
|
||||
|
||||
init_sentry()
|
||||
|
||||
@@ -68,4 +79,5 @@ app.include_router(rounds.router, tags=['rounds'])
|
||||
app.include_router(matches.router, tags=['matches'])
|
||||
app.include_router(stages.router, tags=['stages'])
|
||||
app.include_router(teams.router, tags=['teams'])
|
||||
app.include_router(courts.router, tags=['courts'])
|
||||
app.include_router(users.router, tags=['users'])
|
||||
|
||||
@@ -66,7 +66,7 @@ class DemoConfig(Config):
|
||||
env_file = 'demo.env'
|
||||
|
||||
|
||||
environment = Environment(os.getenv('ENVIRONMENT', 'CI'))
|
||||
environment = Environment(os.getenv('ENVIRONMENT', 'CI').upper())
|
||||
config: Config
|
||||
|
||||
match environment:
|
||||
|
||||
23
backend/bracket/models/db/court.py
Normal file
23
backend/bracket/models/db/court.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from heliclockter import datetime_utc
|
||||
|
||||
from bracket.models.db.shared import BaseModelORM
|
||||
|
||||
|
||||
class Court(BaseModelORM):
|
||||
id: int | None = None
|
||||
name: str
|
||||
created: datetime_utc
|
||||
tournament_id: int
|
||||
|
||||
|
||||
class CourtInDB(Court):
|
||||
id: int
|
||||
|
||||
|
||||
class CourtBody(BaseModelORM):
|
||||
name: str
|
||||
|
||||
|
||||
class CourtToInsert(CourtBody):
|
||||
created: datetime_utc
|
||||
tournament_id: int
|
||||
@@ -3,6 +3,7 @@ from decimal import Decimal
|
||||
from heliclockter import datetime_utc
|
||||
from pydantic import BaseModel
|
||||
|
||||
from bracket.models.db.court import Court
|
||||
from bracket.models.db.shared import BaseModelORM
|
||||
from bracket.models.db.team import FullTeamWithPlayers, TeamWithPlayers
|
||||
from bracket.utils.types import assert_some
|
||||
@@ -16,7 +17,7 @@ class Match(BaseModelORM):
|
||||
team2_id: int
|
||||
team1_score: int
|
||||
team2_score: int
|
||||
label: str
|
||||
court_id: int | None
|
||||
|
||||
|
||||
class UpcomingMatch(BaseModel):
|
||||
@@ -24,9 +25,10 @@ class UpcomingMatch(BaseModel):
|
||||
team2_id: int
|
||||
|
||||
|
||||
class MatchWithTeamDetails(Match):
|
||||
class MatchWithDetails(Match):
|
||||
team1: FullTeamWithPlayers
|
||||
team2: FullTeamWithPlayers
|
||||
court: Court | None
|
||||
|
||||
@property
|
||||
def teams(self) -> list[FullTeamWithPlayers]:
|
||||
@@ -45,14 +47,14 @@ class MatchBody(BaseModelORM):
|
||||
round_id: int
|
||||
team1_score: int = 0
|
||||
team2_score: int = 0
|
||||
label: str
|
||||
court_id: int | None
|
||||
|
||||
|
||||
class MatchCreateBody(BaseModelORM):
|
||||
round_id: int
|
||||
team1_id: int
|
||||
team2_id: int
|
||||
label: str
|
||||
court_id: int | None
|
||||
|
||||
|
||||
class MatchToInsert(MatchCreateBody):
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from heliclockter import datetime_utc
|
||||
from pydantic import root_validator, validator
|
||||
|
||||
from bracket.models.db.match import Match, MatchWithTeamDetails
|
||||
from bracket.models.db.match import Match, MatchWithDetails
|
||||
from bracket.models.db.shared import BaseModelORM
|
||||
from bracket.models.db.stage import Stage, StageType
|
||||
from bracket.utils.types import assert_some
|
||||
@@ -20,7 +20,7 @@ class Round(BaseModelORM):
|
||||
|
||||
|
||||
class RoundWithMatches(Round):
|
||||
matches: list[MatchWithTeamDetails]
|
||||
matches: list[MatchWithDetails]
|
||||
|
||||
@validator('matches', pre=True)
|
||||
def handle_matches(values: list[Match]) -> list[Match]: # type: ignore[misc]
|
||||
|
||||
@@ -11,12 +11,14 @@ class Tournament(BaseModelORM):
|
||||
dashboard_public: bool
|
||||
logo_path: str | None
|
||||
players_can_be_in_multiple_teams: bool
|
||||
auto_assign_courts: bool
|
||||
|
||||
|
||||
class TournamentUpdateBody(BaseModelORM):
|
||||
name: str
|
||||
dashboard_public: bool
|
||||
players_can_be_in_multiple_teams: bool
|
||||
auto_assign_courts: bool
|
||||
|
||||
|
||||
class TournamentBody(TournamentUpdateBody):
|
||||
|
||||
86
backend/bracket/routes/courts.py
Normal file
86
backend/bracket/routes/courts.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from heliclockter import datetime_utc
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.court import Court, CourtBody, CourtToInsert
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import user_authenticated_for_tournament
|
||||
from bracket.routes.models import CourtsResponse, SingleCourtResponse, SuccessResponse
|
||||
from bracket.schema import courts
|
||||
from bracket.sql.courts import get_all_courts_in_tournament, update_court
|
||||
from bracket.utils.db import fetch_one_parsed
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/tournaments/{tournament_id}/courts", response_model=CourtsResponse)
|
||||
async def get_courts(
|
||||
tournament_id: int,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> CourtsResponse:
|
||||
return CourtsResponse(data=await get_all_courts_in_tournament(tournament_id))
|
||||
|
||||
|
||||
@router.patch("/tournaments/{tournament_id}/courts/{court_id}", response_model=SingleCourtResponse)
|
||||
async def update_court_by_id(
|
||||
tournament_id: int,
|
||||
court_id: int,
|
||||
court_body: CourtBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SingleCourtResponse:
|
||||
await update_court(
|
||||
tournament_id=tournament_id,
|
||||
court_id=court_id,
|
||||
court_body=court_body,
|
||||
)
|
||||
return SingleCourtResponse(
|
||||
data=assert_some(
|
||||
await fetch_one_parsed(
|
||||
database,
|
||||
Court,
|
||||
courts.select().where(
|
||||
(courts.c.id == court_id) & (courts.c.tournament_id == tournament_id)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/tournaments/{tournament_id}/courts/{court_id}", response_model=SuccessResponse)
|
||||
async def delete_court(
|
||||
tournament_id: int, court_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
|
||||
) -> SuccessResponse:
|
||||
await database.execute(
|
||||
query=courts.delete().where(
|
||||
courts.c.id == court_id and courts.c.tournament_id == tournament_id
|
||||
),
|
||||
)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
@router.post("/tournaments/{tournament_id}/courts", response_model=SingleCourtResponse)
|
||||
async def create_court(
|
||||
court_body: CourtBody,
|
||||
tournament_id: int,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SingleCourtResponse:
|
||||
last_record_id = await database.execute(
|
||||
query=courts.insert(),
|
||||
values=CourtToInsert(
|
||||
**court_body.dict(),
|
||||
created=datetime_utc.now(),
|
||||
tournament_id=tournament_id,
|
||||
).dict(),
|
||||
)
|
||||
return SingleCourtResponse(
|
||||
data=assert_some(
|
||||
await fetch_one_parsed(
|
||||
database,
|
||||
Court,
|
||||
courts.select().where(
|
||||
courts.c.id == last_record_id and courts.c.tournament_id == tournament_id
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.elo import recalculate_elo_for_tournament_id
|
||||
from bracket.logic.scheduling.ladder_players_iter import get_possible_upcoming_matches_for_players
|
||||
from bracket.logic.scheduling.round_robin import get_possible_upcoming_matches_round_robin
|
||||
@@ -11,8 +10,7 @@ from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import user_authenticated_for_tournament
|
||||
from bracket.routes.models import SingleMatchResponse, SuccessResponse, UpcomingMatchesResponse
|
||||
from bracket.routes.util import match_dependency, round_dependency
|
||||
from bracket.schema import matches
|
||||
from bracket.sql.matches import sql_create_match, sql_delete_match
|
||||
from bracket.sql.matches import sql_create_match, sql_delete_match, sql_update_match
|
||||
from bracket.sql.stages import get_stages_with_rounds_and_matches
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
@@ -87,9 +85,7 @@ async def update_match_by_id(
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
match: Match = Depends(match_dependency),
|
||||
) -> SuccessResponse:
|
||||
await database.execute(
|
||||
query=matches.update().where(matches.c.id == match.id),
|
||||
values=match_body.dict(),
|
||||
)
|
||||
assert match.id
|
||||
await sql_update_match(match.id, match_body)
|
||||
await recalculate_elo_for_tournament_id(tournament_id)
|
||||
return SuccessResponse()
|
||||
|
||||
@@ -4,6 +4,7 @@ from pydantic import BaseModel
|
||||
from pydantic.generics import GenericModel
|
||||
|
||||
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.round import Round, StageWithRounds
|
||||
@@ -81,3 +82,11 @@ class UserPublicResponse(DataResponse[UserPublic]):
|
||||
|
||||
class TokenResponse(DataResponse[Token]):
|
||||
pass
|
||||
|
||||
|
||||
class CourtsResponse(DataResponse[list[Court]]):
|
||||
pass
|
||||
|
||||
|
||||
class SingleCourtResponse(DataResponse[Court]):
|
||||
pass
|
||||
|
||||
@@ -24,6 +24,7 @@ tournaments = Table(
|
||||
Column('dashboard_public', Boolean, nullable=False),
|
||||
Column('logo_path', String, nullable=True),
|
||||
Column('players_can_be_in_multiple_teams', Boolean, nullable=False, server_default='f'),
|
||||
Column('auto_assign_courts', Boolean, nullable=False, server_default='f'),
|
||||
)
|
||||
|
||||
stages = Table(
|
||||
@@ -66,9 +67,9 @@ matches = Table(
|
||||
Column('round_id', BigInteger, ForeignKey('rounds.id'), nullable=False),
|
||||
Column('team1_id', BigInteger, ForeignKey('teams.id'), nullable=False),
|
||||
Column('team2_id', BigInteger, ForeignKey('teams.id'), nullable=False),
|
||||
Column('court_id', BigInteger, ForeignKey('courts.id'), nullable=True),
|
||||
Column('team1_score', Integer, nullable=False),
|
||||
Column('team2_score', Integer, nullable=False),
|
||||
Column('label', String, nullable=False),
|
||||
)
|
||||
|
||||
teams = Table(
|
||||
@@ -96,7 +97,6 @@ players = Table(
|
||||
Column('active', Boolean, nullable=False, index=True, server_default='t'),
|
||||
)
|
||||
|
||||
|
||||
users = Table(
|
||||
'users',
|
||||
metadata,
|
||||
@@ -122,3 +122,12 @@ players_x_teams = Table(
|
||||
Column('player_id', BigInteger, ForeignKey('players.id'), nullable=False),
|
||||
Column('team_id', BigInteger, ForeignKey('teams.id'), nullable=False),
|
||||
)
|
||||
|
||||
courts = Table(
|
||||
'courts',
|
||||
metadata,
|
||||
Column('id', BigInteger, primary_key=True, index=True),
|
||||
Column('name', Text, nullable=False),
|
||||
Column('created', DateTimeTZ, nullable=False),
|
||||
Column('tournament_id', BigInteger, ForeignKey('tournaments.id'), nullable=False, index=True),
|
||||
)
|
||||
|
||||
26
backend/bracket/sql/courts.py
Normal file
26
backend/bracket/sql/courts.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from bracket.database import database
|
||||
from bracket.models.db.court import Court, CourtBody
|
||||
|
||||
|
||||
async def get_all_courts_in_tournament(tournament_id: int) -> list[Court]:
|
||||
query = '''
|
||||
SELECT *
|
||||
FROM courts
|
||||
WHERE courts.tournament_id = :tournament_id
|
||||
'''
|
||||
result = await database.fetch_all(query=query, values={'tournament_id': tournament_id})
|
||||
return [Court.parse_obj(x._mapping) for x in result]
|
||||
|
||||
|
||||
async def update_court(tournament_id: int, court_id: int, court_body: CourtBody) -> list[Court]:
|
||||
query = '''
|
||||
UPDATE courts
|
||||
SET name = :name
|
||||
WHERE courts.tournament_id = :tournament_id
|
||||
AND courts.id = :court_id
|
||||
'''
|
||||
result = await database.fetch_all(
|
||||
query=query,
|
||||
values={'tournament_id': tournament_id, 'court_id': court_id, 'name': court_body.name},
|
||||
)
|
||||
return [Court.parse_obj(x._mapping) for x in result]
|
||||
@@ -1,5 +1,5 @@
|
||||
from bracket.database import database
|
||||
from bracket.models.db.match import Match, MatchCreateBody
|
||||
from bracket.models.db.match import Match, MatchBody, MatchCreateBody
|
||||
|
||||
|
||||
async def sql_delete_match(match_id: int) -> None:
|
||||
@@ -11,15 +11,27 @@ async def sql_delete_match(match_id: int) -> None:
|
||||
|
||||
|
||||
async def sql_create_match(match: MatchCreateBody) -> Match:
|
||||
async with database.transaction():
|
||||
query = '''
|
||||
INSERT INTO matches (round_id, team1_id, team2_id, team1_score, team2_score, label, created)
|
||||
VALUES (:round_id, :team1_id, :team2_id, 0, 0, :label, NOW())
|
||||
RETURNING *
|
||||
'''
|
||||
result = await database.fetch_one(query=query, values=match.dict())
|
||||
query = '''
|
||||
INSERT INTO matches (round_id, team1_id, team2_id, team1_score, team2_score, court_id, created)
|
||||
VALUES (:round_id, :team1_id, :team2_id, 0, 0, :court_id, NOW())
|
||||
RETURNING *
|
||||
'''
|
||||
result = await database.fetch_one(query=query, values=match.dict())
|
||||
|
||||
if result is None:
|
||||
raise ValueError('Could not create stage')
|
||||
|
||||
return Match.parse_obj(result._mapping)
|
||||
|
||||
|
||||
async def sql_update_match(match_id: int, match: MatchBody) -> None:
|
||||
query = '''
|
||||
UPDATE matches
|
||||
SET round_id = :round_id,
|
||||
team1_score = :team1_score,
|
||||
team2_score = :team2_score,
|
||||
court_id = :court_id
|
||||
WHERE matches.id = :match_id
|
||||
RETURNING *
|
||||
'''
|
||||
await database.execute(query=query, values={'match_id': match_id, **match.dict()})
|
||||
|
||||
@@ -28,12 +28,14 @@ async def get_stages_with_rounds_and_matches(
|
||||
SELECT DISTINCT ON (matches.id)
|
||||
matches.*,
|
||||
to_json(t1) as team1,
|
||||
to_json(t2) as team2
|
||||
to_json(t2) as team2,
|
||||
to_json(c) as court
|
||||
FROM matches
|
||||
LEFT JOIN teams_with_players t1 on t1.id = matches.team1_id
|
||||
LEFT JOIN teams_with_players t2 on t2.id = matches.team2_id
|
||||
LEFT JOIN rounds r on matches.round_id = r.id
|
||||
LEFT JOIN stages s2 on r.stage_id = s2.id
|
||||
LEFT JOIN courts c on matches.court_id = c.id
|
||||
WHERE s2.tournament_id = :tournament_id
|
||||
), rounds_with_matches AS (
|
||||
SELECT DISTINCT ON (rounds.id)
|
||||
|
||||
@@ -3,6 +3,7 @@ from zoneinfo import ZoneInfo
|
||||
from heliclockter import datetime_utc
|
||||
|
||||
from bracket.models.db.club import Club
|
||||
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.round import Round
|
||||
@@ -30,6 +31,7 @@ DUMMY_TOURNAMENT = Tournament(
|
||||
dashboard_public=True,
|
||||
logo_path=None,
|
||||
players_can_be_in_multiple_teams=True,
|
||||
auto_assign_courts=True,
|
||||
)
|
||||
|
||||
DUMMY_STAGE1 = Stage(
|
||||
@@ -75,7 +77,7 @@ DUMMY_MATCH1 = Match(
|
||||
team2_id=2,
|
||||
team1_score=11,
|
||||
team2_score=22,
|
||||
label='Court 1 | 11:00 - 11:20',
|
||||
court_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_MATCH2 = Match(
|
||||
@@ -85,7 +87,7 @@ DUMMY_MATCH2 = Match(
|
||||
team2_id=4,
|
||||
team1_score=9,
|
||||
team2_score=6,
|
||||
label='Court 2 | 11:00 - 11:20',
|
||||
court_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_MATCH3 = Match(
|
||||
@@ -95,7 +97,7 @@ DUMMY_MATCH3 = Match(
|
||||
team2_id=4,
|
||||
team1_score=23,
|
||||
team2_score=26,
|
||||
label='Court 1 | 11:30 - 11:50',
|
||||
court_id=None,
|
||||
)
|
||||
|
||||
DUMMY_MATCH4 = Match(
|
||||
@@ -105,7 +107,7 @@ DUMMY_MATCH4 = Match(
|
||||
team2_id=3,
|
||||
team1_score=43,
|
||||
team2_score=45,
|
||||
label='Court 2 | 11:30 - 11:50',
|
||||
court_id=None,
|
||||
)
|
||||
|
||||
DUMMY_USER = User(
|
||||
@@ -211,3 +213,15 @@ DUMMY_USER_X_CLUB = UserXClub(
|
||||
user_id=DB_PLACEHOLDER_ID,
|
||||
club_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_COURT1 = Court(
|
||||
name='Endor',
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_COURT2 = Court(
|
||||
name='Naboo',
|
||||
created=DUMMY_MOCK_TIME,
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ from bracket.database import database, engine, init_db_when_empty
|
||||
from bracket.logger import get_logger
|
||||
from bracket.logic.elo import recalculate_elo_for_tournament_id
|
||||
from bracket.models.db.club import Club
|
||||
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.round import Round
|
||||
@@ -21,6 +22,7 @@ from bracket.models.db.user import User
|
||||
from bracket.models.db.user_x_club import UserXClub
|
||||
from bracket.schema import (
|
||||
clubs,
|
||||
courts,
|
||||
matches,
|
||||
metadata,
|
||||
players,
|
||||
@@ -34,6 +36,8 @@ from bracket.schema import (
|
||||
from bracket.utils.db import insert_generic
|
||||
from bracket.utils.dummy_records import (
|
||||
DUMMY_CLUB,
|
||||
DUMMY_COURT1,
|
||||
DUMMY_COURT2,
|
||||
DUMMY_MATCH1,
|
||||
DUMMY_MATCH2,
|
||||
DUMMY_MATCH3,
|
||||
@@ -112,6 +116,7 @@ async def create_dev_db() -> None:
|
||||
Round: rounds,
|
||||
Match: matches,
|
||||
Tournament: tournaments,
|
||||
Court: courts,
|
||||
}
|
||||
|
||||
async def insert_dummy(obj_to_insert: BaseModelT) -> int:
|
||||
@@ -149,14 +154,27 @@ async def create_dev_db() -> None:
|
||||
round_id_2 = await insert_dummy(DUMMY_ROUND2.copy(update={'stage_id': stage_id_1}))
|
||||
round_id_3 = await insert_dummy(DUMMY_ROUND3.copy(update={'stage_id': stage_id_2}))
|
||||
|
||||
court_id_1 = await insert_dummy(DUMMY_COURT1.copy(update={'tournament_id': tournament_id_1}))
|
||||
court_id_2 = await insert_dummy(DUMMY_COURT2.copy(update={'tournament_id': tournament_id_1}))
|
||||
|
||||
await insert_dummy(
|
||||
DUMMY_MATCH1.copy(
|
||||
update={'round_id': round_id_1, 'team1_id': team_id_1, 'team2_id': team_id_2}
|
||||
update={
|
||||
'round_id': round_id_1,
|
||||
'team1_id': team_id_1,
|
||||
'team2_id': team_id_2,
|
||||
'court_id': court_id_1,
|
||||
}
|
||||
),
|
||||
)
|
||||
await insert_dummy(
|
||||
DUMMY_MATCH2.copy(
|
||||
update={'round_id': round_id_1, 'team1_id': team_id_3, 'team2_id': team_id_4}
|
||||
update={
|
||||
'round_id': round_id_1,
|
||||
'team1_id': team_id_3,
|
||||
'team2_id': team_id_4,
|
||||
'court_id': court_id_2,
|
||||
}
|
||||
),
|
||||
)
|
||||
await insert_dummy(
|
||||
|
||||
79
backend/tests/integration_tests/api/courts_test.py
Normal file
79
backend/tests/integration_tests/api/courts_test.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from bracket.database import database
|
||||
from bracket.models.db.court import Court
|
||||
from bracket.schema import courts
|
||||
from bracket.utils.db import fetch_one_parsed_certain
|
||||
from bracket.utils.dummy_records import DUMMY_COURT1, DUMMY_MOCK_TIME, DUMMY_TEAM1
|
||||
from bracket.utils.http import HTTPMethod
|
||||
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 assert_row_count_and_clear, inserted_court, inserted_team
|
||||
|
||||
|
||||
async def test_courts_endpoint(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
async with inserted_team(
|
||||
DUMMY_TEAM1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
):
|
||||
async with inserted_court(
|
||||
DUMMY_COURT1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as court_inserted:
|
||||
assert await send_tournament_request(HTTPMethod.GET, 'courts', auth_context, {}) == {
|
||||
'data': [
|
||||
{
|
||||
'created': DUMMY_MOCK_TIME.isoformat(),
|
||||
'id': court_inserted.id,
|
||||
'name': 'Endor',
|
||||
'tournament_id': auth_context.tournament.id,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def test_create_court(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
body = {'name': 'Some new name', 'active': True}
|
||||
response = await send_tournament_request(HTTPMethod.POST, 'courts', auth_context, json=body)
|
||||
assert response['data']['name'] == body['name']
|
||||
await assert_row_count_and_clear(courts, 1)
|
||||
|
||||
|
||||
async def test_delete_court(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
async with inserted_team(
|
||||
DUMMY_TEAM1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
):
|
||||
async with inserted_court(
|
||||
DUMMY_COURT1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as court_inserted:
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
HTTPMethod.DELETE, f'courts/{court_inserted.id}', auth_context
|
||||
)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
await assert_row_count_and_clear(courts, 0)
|
||||
|
||||
|
||||
async def test_update_court(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
body = {'name': 'Some new name'}
|
||||
async with inserted_team(
|
||||
DUMMY_TEAM1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
):
|
||||
async with inserted_court(
|
||||
DUMMY_COURT1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as court_inserted:
|
||||
response = await send_tournament_request(
|
||||
HTTPMethod.PATCH, f'courts/{court_inserted.id}', auth_context, json=body
|
||||
)
|
||||
patched_court = await fetch_one_parsed_certain(
|
||||
database, Court, query=courts.select().where(courts.c.id == court_inserted.id)
|
||||
)
|
||||
assert patched_court.name == body['name']
|
||||
assert response['data']['name'] == body['name']
|
||||
|
||||
await assert_row_count_and_clear(courts, 1)
|
||||
@@ -4,6 +4,7 @@ from bracket.models.db.stage import StageType
|
||||
from bracket.schema import matches
|
||||
from bracket.utils.db import fetch_one_parsed_certain
|
||||
from bracket.utils.dummy_records import (
|
||||
DUMMY_COURT1,
|
||||
DUMMY_MATCH1,
|
||||
DUMMY_PLAYER1,
|
||||
DUMMY_PLAYER2,
|
||||
@@ -20,6 +21,7 @@ from tests.integration_tests.api.shared import SUCCESS_RESPONSE, send_tournament
|
||||
from tests.integration_tests.models import AuthContext
|
||||
from tests.integration_tests.sql import (
|
||||
assert_row_count_and_clear,
|
||||
inserted_court,
|
||||
inserted_match,
|
||||
inserted_player_in_team,
|
||||
inserted_round,
|
||||
@@ -39,6 +41,9 @@ async def test_create_match(
|
||||
inserted_team(
|
||||
DUMMY_TEAM1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as team1_inserted,
|
||||
inserted_court(
|
||||
DUMMY_COURT1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as court1_inserted,
|
||||
inserted_team(
|
||||
DUMMY_TEAM2.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as team2_inserted,
|
||||
@@ -47,14 +52,13 @@ async def test_create_match(
|
||||
'team1_id': team1_inserted.id,
|
||||
'team2_id': team2_inserted.id,
|
||||
'round_id': round_inserted.id,
|
||||
'label': 'Some label',
|
||||
'court_id': court1_inserted.id,
|
||||
}
|
||||
response = await send_tournament_request(
|
||||
HTTPMethod.POST, 'matches', auth_context, json=body
|
||||
)
|
||||
assert response['data']['id']
|
||||
|
||||
# await sql_delete_match(response['data']['id'])
|
||||
await assert_row_count_and_clear(matches, 1)
|
||||
|
||||
|
||||
@@ -72,12 +76,16 @@ async def test_delete_match(
|
||||
inserted_team(
|
||||
DUMMY_TEAM2.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as team2_inserted,
|
||||
inserted_court(
|
||||
DUMMY_COURT1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as court1_inserted,
|
||||
inserted_match(
|
||||
DUMMY_MATCH1.copy(
|
||||
update={
|
||||
'round_id': round_inserted.id,
|
||||
'team1_id': team1_inserted.id,
|
||||
'team2_id': team2_inserted.id,
|
||||
'court_id': court1_inserted.id,
|
||||
}
|
||||
)
|
||||
) as match_inserted,
|
||||
@@ -105,12 +113,16 @@ async def test_update_match(
|
||||
inserted_team(
|
||||
DUMMY_TEAM2.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as team2_inserted,
|
||||
inserted_court(
|
||||
DUMMY_COURT1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as court1_inserted,
|
||||
inserted_match(
|
||||
DUMMY_MATCH1.copy(
|
||||
update={
|
||||
'round_id': round_inserted.id,
|
||||
'team1_id': team1_inserted.id,
|
||||
'team2_id': team2_inserted.id,
|
||||
'court_id': court1_inserted.id,
|
||||
}
|
||||
)
|
||||
) as match_inserted,
|
||||
@@ -119,7 +131,7 @@ async def test_update_match(
|
||||
'team1_score': 42,
|
||||
'team2_score': 24,
|
||||
'round_id': round_inserted.id,
|
||||
'label': 'Some label',
|
||||
'court_id': None,
|
||||
}
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
@@ -138,7 +150,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']
|
||||
assert patched_match.court_id == body['court_id']
|
||||
|
||||
await assert_row_count_and_clear(matches, 1)
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ async def test_tournaments_endpoint(
|
||||
'name': 'Some Cool Tournament',
|
||||
'dashboard_public': True,
|
||||
'players_can_be_in_multiple_teams': True,
|
||||
'auto_assign_courts': True,
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -43,6 +44,7 @@ async def test_tournament_endpoint(
|
||||
'name': 'Some Cool Tournament',
|
||||
'dashboard_public': True,
|
||||
'players_can_be_in_multiple_teams': True,
|
||||
'auto_assign_courts': True,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,6 +57,7 @@ async def test_create_tournament(
|
||||
'club_id': auth_context.club.id,
|
||||
'dashboard_public': False,
|
||||
'players_can_be_in_multiple_teams': True,
|
||||
'auto_assign_courts': True,
|
||||
}
|
||||
assert (
|
||||
await send_auth_request(HTTPMethod.POST, 'tournaments', auth_context, json=body)
|
||||
@@ -70,6 +73,7 @@ async def test_update_tournament(
|
||||
'name': 'Some new name',
|
||||
'dashboard_public': False,
|
||||
'players_can_be_in_multiple_teams': True,
|
||||
'auto_assign_courts': True,
|
||||
}
|
||||
assert (
|
||||
await send_tournament_request(HTTPMethod.PATCH, '', auth_context, json=body)
|
||||
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy import Table
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.club import Club
|
||||
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
|
||||
@@ -16,6 +17,7 @@ from bracket.models.db.user import User, UserInDB
|
||||
from bracket.models.db.user_x_club import UserXClub
|
||||
from bracket.schema import (
|
||||
clubs,
|
||||
courts,
|
||||
matches,
|
||||
players,
|
||||
players_x_teams,
|
||||
@@ -74,6 +76,12 @@ async def inserted_team(team: Team) -> AsyncIterator[Team]:
|
||||
yield row_inserted
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def inserted_court(court: Court) -> AsyncIterator[Court]:
|
||||
async with inserted_generic(court, courts, Court) as row_inserted:
|
||||
yield row_inserted
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def inserted_player(player: Player) -> AsyncIterator[Player]:
|
||||
async with inserted_generic(player, players, Player) as row_inserted:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from bracket.logic.elo import PlayerStatistics, calculate_elo_per_player
|
||||
from bracket.models.db.match import MatchWithTeamDetails
|
||||
from bracket.models.db.match import MatchWithDetails
|
||||
from bracket.models.db.round import RoundWithMatches
|
||||
from bracket.models.db.team import FullTeamWithPlayers
|
||||
from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_PLAYER1, DUMMY_PLAYER2
|
||||
@@ -15,14 +15,15 @@ def test_elo_calculation() -> None:
|
||||
is_active=False,
|
||||
name='Some round',
|
||||
matches=[
|
||||
MatchWithTeamDetails(
|
||||
MatchWithDetails(
|
||||
created=DUMMY_MOCK_TIME,
|
||||
team1_id=1,
|
||||
team2_id=1,
|
||||
team1_score=3,
|
||||
team2_score=4,
|
||||
round_id=1,
|
||||
label='Some label',
|
||||
court_id=None,
|
||||
court=None,
|
||||
team1=FullTeamWithPlayers(
|
||||
name='Dummy team 1',
|
||||
tournament_id=1,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"jest:watch": "jest --watch",
|
||||
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
||||
"test": "tsc --noEmit && npm run prettier:check && npm run lint && npm run jest -- --passWithNoTests"
|
||||
"test": "tsc --noEmit && npm run prettier:write && npm run lint && npm run jest -- --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
|
||||
@@ -13,6 +13,7 @@ function getRoundsGridCols(
|
||||
activeStageId: number,
|
||||
tournamentData: TournamentMinimal,
|
||||
swrStagesResponse: SWRResponse,
|
||||
swrCourtsResponse: SWRResponse,
|
||||
swrUpcomingMatchesResponse: SWRResponse | null,
|
||||
readOnly: boolean
|
||||
) {
|
||||
@@ -24,6 +25,7 @@ function getRoundsGridCols(
|
||||
tournamentData={tournamentData}
|
||||
round={round}
|
||||
swrRoundsResponse={swrStagesResponse}
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
@@ -35,12 +37,14 @@ function getRoundsGridCols(
|
||||
export default function Brackets({
|
||||
tournamentData,
|
||||
swrStagesResponse,
|
||||
swrCourtsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
readOnly,
|
||||
activeStageId,
|
||||
}: {
|
||||
tournamentData: TournamentMinimal;
|
||||
swrStagesResponse: SWRResponse;
|
||||
swrCourtsResponse: SWRResponse;
|
||||
swrUpcomingMatchesResponse: SWRResponse | null;
|
||||
readOnly: boolean;
|
||||
activeStageId: number | null;
|
||||
@@ -89,6 +93,7 @@ export default function Brackets({
|
||||
activeStageId,
|
||||
tournamentData,
|
||||
swrStagesResponse,
|
||||
swrCourtsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
readOnly
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ const useStyles = createStyles((theme) => ({
|
||||
}));
|
||||
|
||||
function MatchBadge({ match, theme }: { match: MatchInterface; theme: any }) {
|
||||
const visibility: Visibility = match.label === '' ? 'hidden' : 'visible';
|
||||
const visibility: Visibility = match.court ? 'visible' : 'hidden';
|
||||
const badgeColor = theme.colorScheme === 'dark' ? theme.colors.blue[7] : theme.colors.blue[2];
|
||||
return (
|
||||
<Center style={{ transform: 'translateY(0%)', visibility }}>
|
||||
@@ -47,7 +47,7 @@ function MatchBadge({ match, theme }: { match: MatchInterface; theme: any }) {
|
||||
}}
|
||||
>
|
||||
<Center>
|
||||
<b>{match.label}</b>
|
||||
<b>{match.court?.name}</b>
|
||||
</Center>
|
||||
</div>
|
||||
</Center>
|
||||
@@ -56,12 +56,14 @@ function MatchBadge({ match, theme }: { match: MatchInterface; theme: any }) {
|
||||
|
||||
export default function Match({
|
||||
swrRoundsResponse,
|
||||
swrCourtsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
tournamentData,
|
||||
match,
|
||||
readOnly,
|
||||
}: {
|
||||
swrRoundsResponse: SWRResponse;
|
||||
swrCourtsResponse: SWRResponse;
|
||||
swrUpcomingMatchesResponse: SWRResponse | null;
|
||||
tournamentData: TournamentMinimal;
|
||||
match: MatchInterface;
|
||||
@@ -113,6 +115,7 @@ export default function Match({
|
||||
</UnstyledButton>
|
||||
<MatchModal
|
||||
swrRoundsResponse={swrRoundsResponse}
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
|
||||
tournamentData={tournamentData}
|
||||
match={match}
|
||||
|
||||
@@ -11,22 +11,25 @@ export default function Round({
|
||||
tournamentData,
|
||||
round,
|
||||
swrRoundsResponse,
|
||||
swrCourtsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
readOnly,
|
||||
}: {
|
||||
tournamentData: TournamentMinimal;
|
||||
round: RoundInterface;
|
||||
swrRoundsResponse: SWRResponse;
|
||||
swrCourtsResponse: SWRResponse;
|
||||
swrUpcomingMatchesResponse: SWRResponse | null;
|
||||
readOnly: boolean;
|
||||
}) {
|
||||
const matches = round.matches
|
||||
.sort((m1, m2) => (m1.label > m2.label ? 1 : 0))
|
||||
.sort((m1, m2) => ((m1.court ? m1.court.name : 'y') > (m2.court ? m2.court.name : 'z') ? 1 : 0))
|
||||
.map((match) => (
|
||||
<Match
|
||||
key={match.id}
|
||||
tournamentData={tournamentData}
|
||||
swrRoundsResponse={swrRoundsResponse}
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
|
||||
match={match}
|
||||
readOnly={readOnly}
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
import { Button, Modal, NumberInput, TextInput } from '@mantine/core';
|
||||
import { Button, Modal, NumberInput, Select } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import { Court } from '../../interfaces/court';
|
||||
import { MatchBodyInterface, MatchInterface } from '../../interfaces/match';
|
||||
import { TournamentMinimal } from '../../interfaces/tournament';
|
||||
import { deleteMatch, updateMatch } from '../../services/match';
|
||||
import DeleteButton from '../buttons/delete';
|
||||
import { responseIsValid } from '../utils/util';
|
||||
|
||||
function CourtsSelect({ form, swrCourtsResponse }: { form: any; swrCourtsResponse: SWRResponse }) {
|
||||
const noCourtOption = { value: null, label: 'No Court' };
|
||||
const data = responseIsValid(swrCourtsResponse)
|
||||
? swrCourtsResponse.data.data.map((court: Court) => ({ value: court.id, label: court.name }))
|
||||
: [];
|
||||
return (
|
||||
<Select
|
||||
label="Court"
|
||||
placeholder="Pick a court"
|
||||
data={data.concat(noCourtOption)}
|
||||
searchable
|
||||
maxDropdownHeight={400}
|
||||
style={{ marginTop: 20 }}
|
||||
nothingFound="No courts found"
|
||||
{...form.getInputProps('court_id')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MatchModal({
|
||||
tournamentData,
|
||||
match,
|
||||
swrRoundsResponse,
|
||||
swrCourtsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
opened,
|
||||
setOpened,
|
||||
@@ -19,6 +41,7 @@ export default function MatchModal({
|
||||
tournamentData: TournamentMinimal;
|
||||
match: MatchInterface;
|
||||
swrRoundsResponse: SWRResponse;
|
||||
swrCourtsResponse: SWRResponse;
|
||||
swrUpcomingMatchesResponse: SWRResponse | null;
|
||||
opened: boolean;
|
||||
setOpened: any;
|
||||
@@ -27,7 +50,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 : '',
|
||||
court_id: match != null ? match.court_id : null,
|
||||
},
|
||||
|
||||
validate: {
|
||||
@@ -41,14 +64,14 @@ export default function MatchModal({
|
||||
<Modal opened={opened} onClose={() => setOpened(false)} title="Edit Match">
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
const newMatch: MatchBodyInterface = {
|
||||
const updatedMatch: MatchBodyInterface = {
|
||||
id: match.id,
|
||||
round_id: match.round_id,
|
||||
team1_score: values.team1_score,
|
||||
team2_score: values.team2_score,
|
||||
label: values.label,
|
||||
court_id: values.court_id,
|
||||
};
|
||||
await updateMatch(tournamentData.id, match.id, newMatch);
|
||||
await updateMatch(tournamentData.id, match.id, updatedMatch);
|
||||
await swrRoundsResponse.mutate(null);
|
||||
if (swrUpcomingMatchesResponse != null) await swrUpcomingMatchesResponse.mutate(null);
|
||||
setOpened(false);
|
||||
@@ -68,13 +91,7 @@ export default function MatchModal({
|
||||
{...form.getInputProps('team2_score')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
withAsterisk
|
||||
style={{ marginTop: 20 }}
|
||||
label="Label for this match"
|
||||
placeholder="Court 1 | 11:30 - 12:00"
|
||||
{...form.getInputProps('label')}
|
||||
/>
|
||||
<CourtsSelect form={form} swrCourtsResponse={swrCourtsResponse} />
|
||||
<Button fullWidth style={{ marginTop: 20 }} color="green" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
|
||||
@@ -48,6 +48,7 @@ function GeneralTournamentForm({
|
||||
dashboard_public: tournament == null ? true : tournament.dashboard_public,
|
||||
players_can_be_in_multiple_teams:
|
||||
tournament == null ? true : tournament.players_can_be_in_multiple_teams,
|
||||
auto_assign_courts: tournament == null ? true : tournament.auto_assign_courts,
|
||||
},
|
||||
|
||||
validate: {
|
||||
@@ -65,7 +66,8 @@ function GeneralTournamentForm({
|
||||
parseInt(values.club_id, 10),
|
||||
values.name,
|
||||
values.dashboard_public,
|
||||
values.players_can_be_in_multiple_teams
|
||||
values.players_can_be_in_multiple_teams,
|
||||
values.auto_assign_courts
|
||||
);
|
||||
} else {
|
||||
assert(tournament != null);
|
||||
@@ -73,7 +75,8 @@ function GeneralTournamentForm({
|
||||
tournament.id,
|
||||
values.name,
|
||||
values.dashboard_public,
|
||||
values.players_can_be_in_multiple_teams
|
||||
values.players_can_be_in_multiple_teams,
|
||||
values.auto_assign_courts
|
||||
);
|
||||
}
|
||||
await swrTournamentsResponse.mutate(null);
|
||||
@@ -106,6 +109,11 @@ function GeneralTournamentForm({
|
||||
label="Allow players to be in multiple teams"
|
||||
{...form.getInputProps('players_can_be_in_multiple_teams', { type: 'checkbox' })}
|
||||
/>
|
||||
<Checkbox
|
||||
mt="md"
|
||||
label="Automatically assign courts to matches"
|
||||
{...form.getInputProps('auto_assign_courts', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
{tournament != null ? <DropzoneButton tournament={tournament} /> : null}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tooltip, UnstyledButton } from '@mantine/core';
|
||||
import { Icon, IconTournament, IconUser, IconUsers } from '@tabler/icons-react';
|
||||
import { Icon, IconSoccerField, IconTournament, IconUser, IconUsers } from '@tabler/icons-react';
|
||||
import { NextRouter, useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
@@ -60,6 +60,12 @@ export function MainLinks({ tournament_id }: any) {
|
||||
endpoint: `${tm_prefix}/teams`,
|
||||
router,
|
||||
},
|
||||
{
|
||||
icon: IconSoccerField,
|
||||
label: 'Courts',
|
||||
endpoint: `${tm_prefix}/courts`,
|
||||
router,
|
||||
},
|
||||
];
|
||||
|
||||
const links = data.map((link) => <MainLink key={link.label} item={link} pathName={pathName} />);
|
||||
|
||||
55
frontend/src/components/tables/courts.tsx
Normal file
55
frontend/src/components/tables/courts.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import { Court } from '../../interfaces/court';
|
||||
import { Tournament } from '../../interfaces/tournament';
|
||||
import { deleteCourt } from '../../services/court';
|
||||
import DeleteButton from '../buttons/delete';
|
||||
import { EmptyTableInfo } from '../utils/empty_table_info';
|
||||
import RequestErrorAlert from '../utils/error_alert';
|
||||
import TableLayout, { ThNotSortable, getTableState, sortTableEntries } from './table';
|
||||
|
||||
export default function CourtsTable({
|
||||
tournament,
|
||||
swrCourtsResponse,
|
||||
}: {
|
||||
tournament: Tournament;
|
||||
swrCourtsResponse: SWRResponse;
|
||||
}) {
|
||||
const courts: Court[] = swrCourtsResponse.data != null ? swrCourtsResponse.data.data : [];
|
||||
const tableState = getTableState('id');
|
||||
|
||||
if (swrCourtsResponse.error) return <RequestErrorAlert error={swrCourtsResponse.error} />;
|
||||
|
||||
const rows = courts
|
||||
.sort((s1: Court, s2: Court) => sortTableEntries(s1, s2, tableState))
|
||||
.map((court) => (
|
||||
<tr key={court.name}>
|
||||
<td>{court.name}</td>
|
||||
<td>
|
||||
<DeleteButton
|
||||
onClick={async () => {
|
||||
await deleteCourt(tournament.id, court.id);
|
||||
await swrCourtsResponse.mutate(null);
|
||||
}}
|
||||
title="Delete Court"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
if (rows.length < 1) return <EmptyTableInfo entity_name="courts" />;
|
||||
|
||||
return (
|
||||
<TableLayout>
|
||||
<thead>
|
||||
<tr>
|
||||
<ThNotSortable>Title</ThNotSortable>
|
||||
<ThNotSortable>Status</ThNotSortable>
|
||||
<ThNotSortable>{null}</ThNotSortable>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</TableLayout>
|
||||
);
|
||||
}
|
||||
6
frontend/src/interfaces/court.tsx
Normal file
6
frontend/src/interfaces/court.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Court {
|
||||
id: number;
|
||||
tournament_id: number;
|
||||
created: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Court } from './court';
|
||||
import { TeamInterface } from './team';
|
||||
|
||||
export interface MatchInterface {
|
||||
@@ -8,7 +9,8 @@ export interface MatchInterface {
|
||||
team2_score: number;
|
||||
team1: TeamInterface;
|
||||
team2: TeamInterface;
|
||||
label: string;
|
||||
court_id: number | null;
|
||||
court: Court | null;
|
||||
}
|
||||
|
||||
export interface MatchBodyInterface {
|
||||
@@ -16,7 +18,7 @@ export interface MatchBodyInterface {
|
||||
round_id: number;
|
||||
team1_score: number;
|
||||
team2_score: number;
|
||||
label: string;
|
||||
court_id: number | null;
|
||||
}
|
||||
|
||||
export interface UpcomingMatchInterface {
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface Tournament {
|
||||
club_id: number;
|
||||
dashboard_public: boolean;
|
||||
players_can_be_in_multiple_teams: boolean;
|
||||
auto_assign_courts: boolean;
|
||||
logo_path: string;
|
||||
}
|
||||
export interface TournamentMinimal {
|
||||
|
||||
@@ -33,7 +33,6 @@ export default function App(props: AppProps & { colorScheme: ColorScheme }) {
|
||||
<Analytics />
|
||||
</MantineProvider>
|
||||
</ColorSchemeProvider>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { StageWithRounds } from '../../interfaces/stage';
|
||||
import { Tournament } from '../../interfaces/tournament';
|
||||
import {
|
||||
checkForAuthError,
|
||||
getCourts,
|
||||
getStages,
|
||||
getTournaments,
|
||||
getUpcomingMatches,
|
||||
@@ -31,6 +32,7 @@ export default function TournamentPage() {
|
||||
const swrTournamentsResponse = getTournaments();
|
||||
checkForAuthError(swrTournamentsResponse);
|
||||
const swrStagesResponse: SWRResponse = getStages(id);
|
||||
const swrCourtsResponse: SWRResponse = getCourts(id);
|
||||
const [onlyBehindSchedule, setOnlyBehindSchedule] = useState('true');
|
||||
const [eloThreshold, setEloThreshold] = useState(100);
|
||||
const [iterations, setIterations] = useState(200);
|
||||
@@ -157,6 +159,7 @@ export default function TournamentPage() {
|
||||
<Brackets
|
||||
tournamentData={tournamentDataFull}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
|
||||
readOnly={false}
|
||||
activeStageId={activeStageId}
|
||||
|
||||
61
frontend/src/pages/tournaments/[id]/courts.tsx
Normal file
61
frontend/src/pages/tournaments/[id]/courts.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Button, Container, Divider, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import CourtsTable from '../../../components/tables/courts';
|
||||
import { getTournamentIdFromRouter } from '../../../components/utils/util';
|
||||
import { Tournament } from '../../../interfaces/tournament';
|
||||
import { getCourts, getTournaments } from '../../../services/adapter';
|
||||
import { createCourt } from '../../../services/court';
|
||||
import TournamentLayout from '../_tournament_layout';
|
||||
|
||||
function CreateCourtForm(tournament: Tournament, swrCourtsResponse: SWRResponse) {
|
||||
const form = useForm({
|
||||
initialValues: { name: '' },
|
||||
validate: {
|
||||
name: (value) => (value.length > 0 ? null : 'Name too short'),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
await createCourt(tournament.id, values.name);
|
||||
await swrCourtsResponse.mutate(null);
|
||||
})}
|
||||
>
|
||||
<Divider mt={12} />
|
||||
<h3>Add Court</h3>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label="Name"
|
||||
placeholder="Best Court Ever"
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<Button fullWidth style={{ marginTop: 16 }} color="green" type="submit">
|
||||
Create Court
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CourtsPage() {
|
||||
const { tournamentData } = getTournamentIdFromRouter();
|
||||
const swrCourtsResponse = getCourts(tournamentData.id);
|
||||
|
||||
const swrTournamentsResponse = getTournaments();
|
||||
const tournaments: Tournament[] =
|
||||
swrTournamentsResponse.data != null ? swrTournamentsResponse.data.data : [];
|
||||
const tournamentDataFull = tournaments.filter(
|
||||
(tournament) => tournament.id === tournamentData.id
|
||||
)[0];
|
||||
|
||||
return (
|
||||
<TournamentLayout tournament_id={tournamentData.id}>
|
||||
<Container>
|
||||
<CourtsTable tournament={tournamentDataFull} swrCourtsResponse={swrCourtsResponse} />
|
||||
{CreateCourtForm(tournamentDataFull, swrCourtsResponse)}
|
||||
</Container>
|
||||
</TournamentLayout>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import StagesTab from '../../../components/utils/stages_tab';
|
||||
import { getTournamentIdFromRouter, responseIsValid } from '../../../components/utils/util';
|
||||
import { StageWithRounds } from '../../../interfaces/stage';
|
||||
import { Tournament } from '../../../interfaces/tournament';
|
||||
import { getBaseApiUrl, getStages, getTournament } from '../../../services/adapter';
|
||||
import { getBaseApiUrl, getCourts, getStages, getTournament } from '../../../services/adapter';
|
||||
|
||||
function TournamentLogo({ tournamentDataFull }: { tournamentDataFull: Tournament }) {
|
||||
if (tournamentDataFull == null) {
|
||||
@@ -44,6 +44,8 @@ function TournamentTitle({ tournamentDataFull }: { tournamentDataFull: Tournamen
|
||||
export default function Dashboard() {
|
||||
const { tournamentData } = getTournamentIdFromRouter();
|
||||
const swrStagesResponse: SWRResponse = getStages(tournamentData.id, true);
|
||||
const swrCourtsResponse: SWRResponse = getCourts(tournamentData.id);
|
||||
|
||||
const swrTournamentsResponse = getTournament(tournamentData.id);
|
||||
const [activeStageId, setActiveStageId] = useState(null);
|
||||
|
||||
@@ -85,6 +87,7 @@ export default function Dashboard() {
|
||||
<Brackets
|
||||
tournamentData={tournamentData}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={null}
|
||||
readOnly
|
||||
activeStageId={activeStageId}
|
||||
|
||||
@@ -9,17 +9,20 @@ const axios = require('axios').default;
|
||||
|
||||
export function handleRequestError(response: any) {
|
||||
if (response.response != null && response.response.data.detail != null) {
|
||||
// If the detail contains an array, there is likely a pydantic validation error occurring.
|
||||
const message = Array.isArray(response.response.data.detail)
|
||||
? 'Unknown error'
|
||||
: response.response.data.detail.toString();
|
||||
|
||||
showNotification({
|
||||
color: 'red',
|
||||
title: 'An error occurred',
|
||||
message: response.response.data.detail.toString(),
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function checkForAuthError(response: any) {
|
||||
// if (localStorage)
|
||||
// console.error('asdasd', localStorage.getItem('login'), response.error);
|
||||
if (
|
||||
response.error != null &&
|
||||
response.error.response != null &&
|
||||
@@ -79,6 +82,10 @@ export function getStages(tournament_id: number, no_draft_rounds: boolean = fals
|
||||
});
|
||||
}
|
||||
|
||||
export function getCourts(tournament_id: number): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}/courts`, fetcher);
|
||||
}
|
||||
|
||||
export function getUser(user_id: number): SWRResponse {
|
||||
return useSWR(`users/${user_id}`, fetcher);
|
||||
}
|
||||
|
||||
13
frontend/src/services/court.tsx
Normal file
13
frontend/src/services/court.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createAxios, handleRequestError } from './adapter';
|
||||
|
||||
export async function createCourt(tournament_id: number, name: string) {
|
||||
return createAxios()
|
||||
.post(`tournaments/${tournament_id}/courts`, { name })
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function deleteCourt(tournament_id: number, court_id: number) {
|
||||
return createAxios()
|
||||
.delete(`tournaments/${tournament_id}/courts/${court_id}`)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
@@ -4,13 +4,15 @@ export async function createTournament(
|
||||
club_id: number,
|
||||
name: string,
|
||||
dashboard_public: boolean,
|
||||
players_can_be_in_multiple_teams: boolean
|
||||
players_can_be_in_multiple_teams: boolean,
|
||||
auto_assign_courts: boolean
|
||||
) {
|
||||
return createAxios().post('tournaments', {
|
||||
name,
|
||||
club_id,
|
||||
dashboard_public,
|
||||
players_can_be_in_multiple_teams,
|
||||
auto_assign_courts,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,11 +24,13 @@ export async function updateTournament(
|
||||
tournament_id: number,
|
||||
name: string,
|
||||
dashboard_public: boolean,
|
||||
players_can_be_in_multiple_teams: boolean
|
||||
players_can_be_in_multiple_teams: boolean,
|
||||
auto_assign_courts: boolean
|
||||
) {
|
||||
return createAxios().patch(`tournaments/${tournament_id}`, {
|
||||
name,
|
||||
dashboard_public,
|
||||
players_can_be_in_multiple_teams,
|
||||
auto_assign_courts,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user