diff --git a/backend/alembic/versions/3469289a7e06_create_courts_table.py b/backend/alembic/versions/3469289a7e06_create_courts_table.py new file mode 100644 index 00000000..1f498ae5 --- /dev/null +++ b/backend/alembic/versions/3469289a7e06_create_courts_table.py @@ -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') diff --git a/backend/bracket/app.py b/backend/bracket/app.py index 20cc3b19..39f80b7e 100644 --- a/backend/bracket/app.py +++ b/backend/bracket/app.py @@ -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']) diff --git a/backend/bracket/config.py b/backend/bracket/config.py index 5ddf9fde..b22392b5 100644 --- a/backend/bracket/config.py +++ b/backend/bracket/config.py @@ -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: diff --git a/backend/bracket/models/db/court.py b/backend/bracket/models/db/court.py new file mode 100644 index 00000000..0100de36 --- /dev/null +++ b/backend/bracket/models/db/court.py @@ -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 diff --git a/backend/bracket/models/db/match.py b/backend/bracket/models/db/match.py index bc11b0e8..32bbb516 100644 --- a/backend/bracket/models/db/match.py +++ b/backend/bracket/models/db/match.py @@ -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): diff --git a/backend/bracket/models/db/round.py b/backend/bracket/models/db/round.py index 9ca0d325..2de05c21 100644 --- a/backend/bracket/models/db/round.py +++ b/backend/bracket/models/db/round.py @@ -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] diff --git a/backend/bracket/models/db/tournament.py b/backend/bracket/models/db/tournament.py index 019e23a2..a7bae671 100644 --- a/backend/bracket/models/db/tournament.py +++ b/backend/bracket/models/db/tournament.py @@ -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): diff --git a/backend/bracket/routes/courts.py b/backend/bracket/routes/courts.py new file mode 100644 index 00000000..662ed773 --- /dev/null +++ b/backend/bracket/routes/courts.py @@ -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 + ), + ) + ) + ) diff --git a/backend/bracket/routes/matches.py b/backend/bracket/routes/matches.py index abfd9667..f02b34ee 100644 --- a/backend/bracket/routes/matches.py +++ b/backend/bracket/routes/matches.py @@ -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() diff --git a/backend/bracket/routes/models.py b/backend/bracket/routes/models.py index b9c99115..146c6b0a 100644 --- a/backend/bracket/routes/models.py +++ b/backend/bracket/routes/models.py @@ -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 diff --git a/backend/bracket/schema.py b/backend/bracket/schema.py index 864f6865..d89287c9 100644 --- a/backend/bracket/schema.py +++ b/backend/bracket/schema.py @@ -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), +) diff --git a/backend/bracket/sql/courts.py b/backend/bracket/sql/courts.py new file mode 100644 index 00000000..69d290da --- /dev/null +++ b/backend/bracket/sql/courts.py @@ -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] diff --git a/backend/bracket/sql/matches.py b/backend/bracket/sql/matches.py index 4c3f00d6..bcb47711 100644 --- a/backend/bracket/sql/matches.py +++ b/backend/bracket/sql/matches.py @@ -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()}) diff --git a/backend/bracket/sql/stages.py b/backend/bracket/sql/stages.py index 4547a571..1d2aff36 100644 --- a/backend/bracket/sql/stages.py +++ b/backend/bracket/sql/stages.py @@ -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) diff --git a/backend/bracket/utils/dummy_records.py b/backend/bracket/utils/dummy_records.py index 6cd54785..27006e55 100644 --- a/backend/bracket/utils/dummy_records.py +++ b/backend/bracket/utils/dummy_records.py @@ -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, +) diff --git a/backend/cli.py b/backend/cli.py index 26a9c46e..e378cb61 100755 --- a/backend/cli.py +++ b/backend/cli.py @@ -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( diff --git a/backend/tests/integration_tests/api/courts_test.py b/backend/tests/integration_tests/api/courts_test.py new file mode 100644 index 00000000..74775437 --- /dev/null +++ b/backend/tests/integration_tests/api/courts_test.py @@ -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) diff --git a/backend/tests/integration_tests/api/matches_test.py b/backend/tests/integration_tests/api/matches_test.py index 96974580..710bb06f 100644 --- a/backend/tests/integration_tests/api/matches_test.py +++ b/backend/tests/integration_tests/api/matches_test.py @@ -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) diff --git a/backend/tests/integration_tests/api/tournaments_test.py b/backend/tests/integration_tests/api/tournaments_test.py index f3358a9d..7ae8b381 100644 --- a/backend/tests/integration_tests/api/tournaments_test.py +++ b/backend/tests/integration_tests/api/tournaments_test.py @@ -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) diff --git a/backend/tests/integration_tests/sql.py b/backend/tests/integration_tests/sql.py index d9e0d13d..409cd8fd 100644 --- a/backend/tests/integration_tests/sql.py +++ b/backend/tests/integration_tests/sql.py @@ -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: diff --git a/backend/tests/unit_tests/elo_test.py b/backend/tests/unit_tests/elo_test.py index 0497d661..53a679cd 100644 --- a/backend/tests/unit_tests/elo_test.py +++ b/backend/tests/unit_tests/elo_test.py @@ -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, diff --git a/frontend/package.json b/frontend/package.json index 932eb2eb..81a2d7a5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/brackets/brackets.tsx b/frontend/src/components/brackets/brackets.tsx index d6db9429..e04cd7fe 100644 --- a/frontend/src/components/brackets/brackets.tsx +++ b/frontend/src/components/brackets/brackets.tsx @@ -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 ) diff --git a/frontend/src/components/brackets/match.tsx b/frontend/src/components/brackets/match.tsx index a1f32672..85aa3c46 100644 --- a/frontend/src/components/brackets/match.tsx +++ b/frontend/src/components/brackets/match.tsx @@ -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 (
@@ -47,7 +47,7 @@ function MatchBadge({ match, theme }: { match: MatchInterface; theme: any }) { }} >
- {match.label} + {match.court?.name}
@@ -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({ (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) => ( ({ value: court.id, label: court.name })) + : []; + return ( +