From 4e616d8d97699684fd5b40804c36b72932b04f6f Mon Sep 17 00:00:00 2001 From: Erik Vroon Date: Tue, 21 Nov 2023 20:07:35 +0100 Subject: [PATCH] Multi users and teams creation (#342) fixes https://github.com/evroon/bracket/issues/292 --- backend/bracket/models/db/player.py | 7 +- backend/bracket/models/db/team.py | 9 +- backend/bracket/routes/players.py | 42 +++-- backend/bracket/routes/teams.py | 23 ++- .../integration_tests/api/players_test.py | 13 +- .../tests/integration_tests/api/teams_test.py | 11 ++ frontend/src/components/buttons/save.tsx | 7 +- .../forms/player_create_csv_input.tsx | 25 +++ .../components/modals/player_create_modal.tsx | 148 ++++++++++++++++ ...ayer_modal.tsx => player_update_modal.tsx} | 35 ++-- frontend/src/components/modals/spotlight.tsx | 2 +- .../components/modals/team_create_modal.tsx | 164 ++++++++++++++++++ .../{team_modal.tsx => team_update_modal.tsx} | 67 +++---- frontend/src/components/tables/players.tsx | 4 +- frontend/src/components/tables/teams.tsx | 4 +- .../src/pages/tournaments/[id]/players.tsx | 5 +- frontend/src/pages/tournaments/[id]/teams.tsx | 5 +- frontend/src/services/player.tsx | 19 +- frontend/src/services/team.tsx | 4 + 19 files changed, 472 insertions(+), 122 deletions(-) create mode 100644 frontend/src/components/forms/player_create_csv_input.tsx create mode 100644 frontend/src/components/modals/player_create_modal.tsx rename frontend/src/components/modals/{player_modal.tsx => player_update_modal.tsx} (59%) create mode 100644 frontend/src/components/modals/team_create_modal.tsx rename frontend/src/components/modals/{team_modal.tsx => team_update_modal.tsx} (55%) diff --git a/backend/bracket/models/db/player.py b/backend/bracket/models/db/player.py index ac009a36..64901c5f 100644 --- a/backend/bracket/models/db/player.py +++ b/backend/bracket/models/db/player.py @@ -23,7 +23,12 @@ class Player(BaseModelORM): class PlayerBody(BaseModelORM): - name: str = Field(..., max_length=30) + name: str = Field(..., min_length=1, max_length=30) + active: bool + + +class PlayerMultiBody(BaseModelORM): + names: str active: bool diff --git a/backend/bracket/models/db/team.py b/backend/bracket/models/db/team.py index 90896d04..f1f2b644 100644 --- a/backend/bracket/models/db/team.py +++ b/backend/bracket/models/db/team.py @@ -70,9 +70,14 @@ class FullTeamWithPlayers(TeamWithPlayers, Team): class TeamBody(BaseModelORM): - name: str = Field(..., max_length=30) + name: str = Field(..., min_length=1, max_length=30) + active: bool + player_ids: list[int] = Field(..., unique_items=True) + + +class TeamMultiBody(BaseModelORM): + names: str = Field(..., min_length=1, max_length=30) active: bool - player_ids: list[int] class TeamToInsert(BaseModelORM): diff --git a/backend/bracket/routes/players.py b/backend/bracket/routes/players.py index f312a3bc..aaf354aa 100644 --- a/backend/bracket/routes/players.py +++ b/backend/bracket/routes/players.py @@ -4,7 +4,8 @@ from fastapi import APIRouter, Depends from heliclockter import datetime_utc from bracket.database import database -from bracket.models.db.player import Player, PlayerBody, PlayerToInsert +from bracket.models.db.player import Player, PlayerBody, PlayerMultiBody, PlayerToInsert +from bracket.models.db.players import START_ELO from bracket.models.db.user import UserPublic from bracket.routes.auth import user_authenticated_for_tournament from bracket.routes.models import PlayersResponse, SinglePlayerResponse, SuccessResponse @@ -66,30 +67,37 @@ async def delete_player( return SuccessResponse() -@router.post("/tournaments/{tournament_id}/players", response_model=SinglePlayerResponse) -async def create_player( +@router.post("/tournaments/{tournament_id}/players", response_model=SuccessResponse) +async def create_single_player( player_body: PlayerBody, tournament_id: int, _: UserPublic = Depends(user_authenticated_for_tournament), -) -> SinglePlayerResponse: - last_record_id = await database.execute( +) -> SuccessResponse: + await insert_player(player_body, tournament_id) + return SuccessResponse() + + +async def insert_player(player_body: PlayerBody, tournament_id: int) -> None: + await database.execute( query=players.insert(), values=PlayerToInsert( **player_body.dict(), created=datetime_utc.now(), tournament_id=tournament_id, - elo_score=Decimal('0.0'), + elo_score=Decimal(START_ELO), swiss_score=Decimal('0.0'), ).dict(), ) - return SinglePlayerResponse( - data=assert_some( - await fetch_one_parsed( - database, - Player, - players.select().where( - players.c.id == last_record_id and players.c.tournament_id == tournament_id - ), - ) - ) - ) + + +@router.post("/tournaments/{tournament_id}/players_multi", response_model=SuccessResponse) +async def create_multiple_players( + player_body: PlayerMultiBody, + tournament_id: int, + _: UserPublic = Depends(user_authenticated_for_tournament), +) -> SuccessResponse: + player_names = [player.strip() for player in player_body.names.split('\n') if len(player) > 0] + for player_name in player_names: + await insert_player(PlayerBody(name=player_name, active=player_body.active), tournament_id) + + return SuccessResponse() diff --git a/backend/bracket/routes/teams.py b/backend/bracket/routes/teams.py index dd77898f..187221a4 100644 --- a/backend/bracket/routes/teams.py +++ b/backend/bracket/routes/teams.py @@ -4,7 +4,7 @@ from starlette import status from bracket.database import database from bracket.logic.ranking.elo import recalculate_ranking_for_tournament_id -from bracket.models.db.team import FullTeamWithPlayers, Team, TeamBody, TeamToInsert +from bracket.models.db.team import FullTeamWithPlayers, Team, TeamBody, TeamMultiBody, TeamToInsert from bracket.models.db.user import UserPublic from bracket.routes.auth import ( user_authenticated_for_tournament, @@ -133,3 +133,24 @@ async def create_team( team_result = await get_team_by_id(last_record_id, tournament_id) assert team_result is not None return SingleTeamResponse(data=team_result) + + +@router.post("/tournaments/{tournament_id}/teams_multi", response_model=SuccessResponse) +async def create_multiple_teams( + team_body: TeamMultiBody, + tournament_id: int, + _: UserPublic = Depends(user_authenticated_for_tournament), +) -> SuccessResponse: + team_names = [team.strip() for team in team_body.names.split('\n') if len(team) > 0] + for team_name in team_names: + await database.execute( + query=teams.insert(), + values=TeamToInsert( + name=team_name, + active=team_body.active, + created=datetime_utc.now(), + tournament_id=tournament_id, + ).dict(), + ) + + return SuccessResponse() diff --git a/backend/tests/integration_tests/api/players_test.py b/backend/tests/integration_tests/api/players_test.py index a6cd2c64..7ec61fef 100644 --- a/backend/tests/integration_tests/api/players_test.py +++ b/backend/tests/integration_tests/api/players_test.py @@ -41,10 +41,21 @@ async def test_create_player( ) -> None: body = {'name': 'Some new name', 'active': True} response = await send_tournament_request(HTTPMethod.POST, 'players', auth_context, json=body) - assert response['data']['name'] == body['name'] + assert response['success'] is True await assert_row_count_and_clear(players, 1) +async def test_create_players( + startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext +) -> None: + body = {'names': 'Player x\nPlayer y', 'active': True} + response = await send_tournament_request( + HTTPMethod.POST, 'players_multi', auth_context, json=body + ) + assert response['success'] is True + await assert_row_count_and_clear(players, 2) + + async def test_delete_player( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: diff --git a/backend/tests/integration_tests/api/teams_test.py b/backend/tests/integration_tests/api/teams_test.py index 6e60dcb1..d018d506 100644 --- a/backend/tests/integration_tests/api/teams_test.py +++ b/backend/tests/integration_tests/api/teams_test.py @@ -43,6 +43,17 @@ async def test_create_team( await assert_row_count_and_clear(teams, 1) +async def test_create_teams( + startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext +) -> None: + body = {'names': 'Team -1\nTeam -2', 'active': True} + response = await send_tournament_request( + HTTPMethod.POST, 'teams_multi', auth_context, None, body + ) + assert response['success'] is True + await assert_row_count_and_clear(teams, 2) + + async def test_delete_team( startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext ) -> None: diff --git a/frontend/src/components/buttons/save.tsx b/frontend/src/components/buttons/save.tsx index 9554a0bd..a92a5635 100644 --- a/frontend/src/components/buttons/save.tsx +++ b/frontend/src/components/buttons/save.tsx @@ -2,12 +2,7 @@ import { Button } from '@mantine/core'; export default function SaveButton(props: any) { return ( - ); diff --git a/frontend/src/components/forms/player_create_csv_input.tsx b/frontend/src/components/forms/player_create_csv_input.tsx new file mode 100644 index 00000000..871d0fe9 --- /dev/null +++ b/frontend/src/components/forms/player_create_csv_input.tsx @@ -0,0 +1,25 @@ +import { Textarea } from '@mantine/core'; +import { UseFormReturnType } from '@mantine/form'; +import React from 'react'; + +export function MultiPlayersInput({ form }: { form: UseFormReturnType }) { + return ( +