Skip to content

Commit aaca527

Browse files
authored
Add courts functionality (#256)
1 parent 3d2942d commit aaca527

39 files changed

+630
-67
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""create courts table
2+
3+
Revision ID: 3469289a7e06
4+
Revises: 6458e0bc3e9d
5+
Create Date: 2023-09-11 13:36:28.464161
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
11+
from alembic import op
12+
13+
# revision identifiers, used by Alembic.
14+
revision: str | None = '3469289a7e06'
15+
down_revision: str | None = '6458e0bc3e9d'
16+
branch_labels: str | None = None
17+
depends_on: str | None = None
18+
19+
20+
def upgrade() -> None:
21+
op.create_table(
22+
'courts',
23+
sa.Column('id', sa.BigInteger(), nullable=False),
24+
sa.Column('name', sa.Text(), nullable=False),
25+
sa.Column('created', sa.DateTime(timezone=True), nullable=False),
26+
sa.Column('tournament_id', sa.BigInteger(), nullable=False),
27+
sa.ForeignKeyConstraint(
28+
['tournament_id'],
29+
['tournaments.id'],
30+
),
31+
sa.PrimaryKeyConstraint('id'),
32+
)
33+
op.create_index(op.f('ix_courts_id'), 'courts', ['id'], unique=False)
34+
op.add_column('matches', sa.Column('court_id', sa.BigInteger(), nullable=True))
35+
op.create_foreign_key('matches_courts_fkey', 'matches', 'courts', ['court_id'], ['id'])
36+
op.create_index(op.f('ix_courts_tournament_id'), 'courts', ['tournament_id'], unique=False)
37+
op.add_column(
38+
'tournaments',
39+
sa.Column('auto_assign_courts', sa.Boolean(), server_default='f', nullable=False),
40+
)
41+
op.drop_column('matches', 'label')
42+
43+
44+
def downgrade() -> None:
45+
op.add_column('matches', sa.Column('label', sa.Text(), nullable=True, server_default=''))
46+
op.alter_column('matches', 'label', server_default=None)
47+
op.drop_column('tournaments', 'auto_assign_courts')
48+
op.drop_index(op.f('ix_courts_tournament_id'), table_name='courts')
49+
op.drop_constraint('matches_courts_fkey', 'matches', type_='foreignkey')
50+
op.drop_column('matches', 'court_id')
51+
op.drop_index(op.f('ix_courts_id'), table_name='courts')
52+
op.drop_table('courts')

backend/bracket/app.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,18 @@
77

88
from bracket.config import Environment, config, environment, init_sentry
99
from bracket.database import database, init_db_when_empty
10-
from bracket.routes import auth, clubs, matches, players, rounds, stages, teams, tournaments, users
10+
from bracket.routes import (
11+
auth,
12+
clubs,
13+
courts,
14+
matches,
15+
players,
16+
rounds,
17+
stages,
18+
teams,
19+
tournaments,
20+
users,
21+
)
1122

1223
init_sentry()
1324

@@ -68,4 +79,5 @@ async def generic_exception_handler(request: Request, exc: Exception) -> JSONRes
6879
app.include_router(matches.router, tags=['matches'])
6980
app.include_router(stages.router, tags=['stages'])
7081
app.include_router(teams.router, tags=['teams'])
82+
app.include_router(courts.router, tags=['courts'])
7183
app.include_router(users.router, tags=['users'])

backend/bracket/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class Config:
6666
env_file = 'demo.env'
6767

6868

69-
environment = Environment(os.getenv('ENVIRONMENT', 'CI'))
69+
environment = Environment(os.getenv('ENVIRONMENT', 'CI').upper())
7070
config: Config
7171

7272
match environment:

backend/bracket/models/db/court.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from heliclockter import datetime_utc
2+
3+
from bracket.models.db.shared import BaseModelORM
4+
5+
6+
class Court(BaseModelORM):
7+
id: int | None = None
8+
name: str
9+
created: datetime_utc
10+
tournament_id: int
11+
12+
13+
class CourtInDB(Court):
14+
id: int
15+
16+
17+
class CourtBody(BaseModelORM):
18+
name: str
19+
20+
21+
class CourtToInsert(CourtBody):
22+
created: datetime_utc
23+
tournament_id: int

backend/bracket/models/db/match.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from heliclockter import datetime_utc
44
from pydantic import BaseModel
55

6+
from bracket.models.db.court import Court
67
from bracket.models.db.shared import BaseModelORM
78
from bracket.models.db.team import FullTeamWithPlayers, TeamWithPlayers
89
from bracket.utils.types import assert_some
@@ -16,17 +17,18 @@ class Match(BaseModelORM):
1617
team2_id: int
1718
team1_score: int
1819
team2_score: int
19-
label: str
20+
court_id: int | None
2021

2122

2223
class UpcomingMatch(BaseModel):
2324
team1_id: int
2425
team2_id: int
2526

2627

27-
class MatchWithTeamDetails(Match):
28+
class MatchWithDetails(Match):
2829
team1: FullTeamWithPlayers
2930
team2: FullTeamWithPlayers
31+
court: Court | None
3032

3133
@property
3234
def teams(self) -> list[FullTeamWithPlayers]:
@@ -45,14 +47,14 @@ class MatchBody(BaseModelORM):
4547
round_id: int
4648
team1_score: int = 0
4749
team2_score: int = 0
48-
label: str
50+
court_id: int | None
4951

5052

5153
class MatchCreateBody(BaseModelORM):
5254
round_id: int
5355
team1_id: int
5456
team2_id: int
55-
label: str
57+
court_id: int | None
5658

5759

5860
class MatchToInsert(MatchCreateBody):

backend/bracket/models/db/round.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from heliclockter import datetime_utc
55
from pydantic import root_validator, validator
66

7-
from bracket.models.db.match import Match, MatchWithTeamDetails
7+
from bracket.models.db.match import Match, MatchWithDetails
88
from bracket.models.db.shared import BaseModelORM
99
from bracket.models.db.stage import Stage, StageType
1010
from bracket.utils.types import assert_some
@@ -20,7 +20,7 @@ class Round(BaseModelORM):
2020

2121

2222
class RoundWithMatches(Round):
23-
matches: list[MatchWithTeamDetails]
23+
matches: list[MatchWithDetails]
2424

2525
@validator('matches', pre=True)
2626
def handle_matches(values: list[Match]) -> list[Match]: # type: ignore[misc]

backend/bracket/models/db/tournament.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ class Tournament(BaseModelORM):
1111
dashboard_public: bool
1212
logo_path: str | None
1313
players_can_be_in_multiple_teams: bool
14+
auto_assign_courts: bool
1415

1516

1617
class TournamentUpdateBody(BaseModelORM):
1718
name: str
1819
dashboard_public: bool
1920
players_can_be_in_multiple_teams: bool
21+
auto_assign_courts: bool
2022

2123

2224
class TournamentBody(TournamentUpdateBody):

backend/bracket/routes/courts.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from fastapi import APIRouter, Depends
2+
from heliclockter import datetime_utc
3+
4+
from bracket.database import database
5+
from bracket.models.db.court import Court, CourtBody, CourtToInsert
6+
from bracket.models.db.user import UserPublic
7+
from bracket.routes.auth import user_authenticated_for_tournament
8+
from bracket.routes.models import CourtsResponse, SingleCourtResponse, SuccessResponse
9+
from bracket.schema import courts
10+
from bracket.sql.courts import get_all_courts_in_tournament, update_court
11+
from bracket.utils.db import fetch_one_parsed
12+
from bracket.utils.types import assert_some
13+
14+
router = APIRouter()
15+
16+
17+
@router.get("/tournaments/{tournament_id}/courts", response_model=CourtsResponse)
18+
async def get_courts(
19+
tournament_id: int,
20+
_: UserPublic = Depends(user_authenticated_for_tournament),
21+
) -> CourtsResponse:
22+
return CourtsResponse(data=await get_all_courts_in_tournament(tournament_id))
23+
24+
25+
@router.patch("/tournaments/{tournament_id}/courts/{court_id}", response_model=SingleCourtResponse)
26+
async def update_court_by_id(
27+
tournament_id: int,
28+
court_id: int,
29+
court_body: CourtBody,
30+
_: UserPublic = Depends(user_authenticated_for_tournament),
31+
) -> SingleCourtResponse:
32+
await update_court(
33+
tournament_id=tournament_id,
34+
court_id=court_id,
35+
court_body=court_body,
36+
)
37+
return SingleCourtResponse(
38+
data=assert_some(
39+
await fetch_one_parsed(
40+
database,
41+
Court,
42+
courts.select().where(
43+
(courts.c.id == court_id) & (courts.c.tournament_id == tournament_id)
44+
),
45+
)
46+
)
47+
)
48+
49+
50+
@router.delete("/tournaments/{tournament_id}/courts/{court_id}", response_model=SuccessResponse)
51+
async def delete_court(
52+
tournament_id: int, court_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
53+
) -> SuccessResponse:
54+
await database.execute(
55+
query=courts.delete().where(
56+
courts.c.id == court_id and courts.c.tournament_id == tournament_id
57+
),
58+
)
59+
return SuccessResponse()
60+
61+
62+
@router.post("/tournaments/{tournament_id}/courts", response_model=SingleCourtResponse)
63+
async def create_court(
64+
court_body: CourtBody,
65+
tournament_id: int,
66+
_: UserPublic = Depends(user_authenticated_for_tournament),
67+
) -> SingleCourtResponse:
68+
last_record_id = await database.execute(
69+
query=courts.insert(),
70+
values=CourtToInsert(
71+
**court_body.dict(),
72+
created=datetime_utc.now(),
73+
tournament_id=tournament_id,
74+
).dict(),
75+
)
76+
return SingleCourtResponse(
77+
data=assert_some(
78+
await fetch_one_parsed(
79+
database,
80+
Court,
81+
courts.select().where(
82+
courts.c.id == last_record_id and courts.c.tournament_id == tournament_id
83+
),
84+
)
85+
)
86+
)

backend/bracket/routes/matches.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from fastapi import APIRouter, Depends, HTTPException
22

3-
from bracket.database import database
43
from bracket.logic.elo import recalculate_elo_for_tournament_id
54
from bracket.logic.scheduling.ladder_players_iter import get_possible_upcoming_matches_for_players
65
from bracket.logic.scheduling.round_robin import get_possible_upcoming_matches_round_robin
@@ -11,8 +10,7 @@
1110
from bracket.routes.auth import user_authenticated_for_tournament
1211
from bracket.routes.models import SingleMatchResponse, SuccessResponse, UpcomingMatchesResponse
1312
from bracket.routes.util import match_dependency, round_dependency
14-
from bracket.schema import matches
15-
from bracket.sql.matches import sql_create_match, sql_delete_match
13+
from bracket.sql.matches import sql_create_match, sql_delete_match, sql_update_match
1614
from bracket.sql.stages import get_stages_with_rounds_and_matches
1715
from bracket.utils.types import assert_some
1816

@@ -87,9 +85,7 @@ async def update_match_by_id(
8785
_: UserPublic = Depends(user_authenticated_for_tournament),
8886
match: Match = Depends(match_dependency),
8987
) -> SuccessResponse:
90-
await database.execute(
91-
query=matches.update().where(matches.c.id == match.id),
92-
values=match_body.dict(),
93-
)
88+
assert match.id
89+
await sql_update_match(match.id, match_body)
9490
await recalculate_elo_for_tournament_id(tournament_id)
9591
return SuccessResponse()

backend/bracket/routes/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pydantic.generics import GenericModel
55

66
from bracket.models.db.club import Club
7+
from bracket.models.db.court import Court
78
from bracket.models.db.match import Match, SuggestedMatch
89
from bracket.models.db.player import Player
910
from bracket.models.db.round import Round, StageWithRounds
@@ -81,3 +82,11 @@ class UserPublicResponse(DataResponse[UserPublic]):
8182

8283
class TokenResponse(DataResponse[Token]):
8384
pass
85+
86+
87+
class CourtsResponse(DataResponse[list[Court]]):
88+
pass
89+
90+
91+
class SingleCourtResponse(DataResponse[Court]):
92+
pass

0 commit comments

Comments
 (0)