mirror of
https://github.com/evroon/bracket.git
synced 2026-03-05 23:59:01 -05:00
Feature: archived tournaments (#1112)
fixes https://github.com/evroon/bracket/issues/690
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
"""add tournaments.status
|
||||
|
||||
Revision ID: c1ab44651e79
|
||||
Revises: e6e2718365dc
|
||||
Create Date: 2025-02-09 11:06:32.622324
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import ENUM
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str | None = "c1ab44651e79"
|
||||
down_revision: str | None = "e6e2718365dc"
|
||||
branch_labels: str | None = None
|
||||
depends_on: str | None = None
|
||||
|
||||
enum = ENUM("OPEN", "ARCHIVED", name="tournament_status", create_type=True)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
enum.create(op.get_bind(), checkfirst=True)
|
||||
op.add_column(
|
||||
"tournaments", sa.Column("status", enum, server_default="OPEN", nullable=False, index=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("tournaments", "status")
|
||||
enum.drop(op.get_bind())
|
||||
@@ -1,9 +1,17 @@
|
||||
from enum import auto
|
||||
|
||||
from heliclockter import datetime_utc
|
||||
from pydantic import Field
|
||||
|
||||
from bracket.models.db.shared import BaseModelORM
|
||||
from bracket.utils.id_types import ClubId, TournamentId
|
||||
from bracket.utils.pydantic import EmptyStrToNone
|
||||
from bracket.utils.types import EnumAutoStr
|
||||
|
||||
|
||||
class TournamentStatus(EnumAutoStr):
|
||||
OPEN = auto()
|
||||
ARCHIVED = auto()
|
||||
|
||||
|
||||
class TournamentInsertable(BaseModelORM):
|
||||
@@ -18,6 +26,7 @@ class TournamentInsertable(BaseModelORM):
|
||||
logo_path: str | None = None
|
||||
players_can_be_in_multiple_teams: bool
|
||||
auto_assign_courts: bool
|
||||
status: TournamentStatus = TournamentStatus.OPEN
|
||||
|
||||
|
||||
class Tournament(TournamentInsertable):
|
||||
@@ -35,5 +44,9 @@ class TournamentUpdateBody(BaseModelORM):
|
||||
margin_minutes: int = Field(..., ge=0)
|
||||
|
||||
|
||||
class TournamentChangeStatusBody(BaseModelORM):
|
||||
status: TournamentStatus
|
||||
|
||||
|
||||
class TournamentBody(TournamentUpdateBody):
|
||||
club_id: ClubId
|
||||
|
||||
@@ -5,12 +5,14 @@ from starlette import status
|
||||
from bracket.database import database
|
||||
from bracket.logic.subscriptions import check_requirement
|
||||
from bracket.models.db.court import Court, CourtBody, CourtToInsert
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import (
|
||||
user_authenticated_for_tournament,
|
||||
user_authenticated_or_public_dashboard,
|
||||
)
|
||||
from bracket.routes.models import CourtsResponse, SingleCourtResponse, SuccessResponse
|
||||
from bracket.routes.util import disallow_archived_tournament
|
||||
from bracket.schema import courts
|
||||
from bracket.sql.courts import get_all_courts_in_tournament, sql_delete_court, update_court
|
||||
from bracket.sql.stages import get_full_tournament_details
|
||||
@@ -35,6 +37,7 @@ async def update_court_by_id(
|
||||
court_id: CourtId,
|
||||
court_body: CourtBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SingleCourtResponse:
|
||||
await update_court(
|
||||
tournament_id=tournament_id,
|
||||
@@ -59,6 +62,7 @@ async def delete_court(
|
||||
tournament_id: TournamentId,
|
||||
court_id: CourtId,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
stages = await get_full_tournament_details(tournament_id, no_draft_rounds=False)
|
||||
used_in_matches_count = 0
|
||||
@@ -84,6 +88,7 @@ async def create_court(
|
||||
court_body: CourtBody,
|
||||
tournament_id: TournamentId,
|
||||
user: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
_: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SingleCourtResponse:
|
||||
existing_courts = await get_all_courts_in_tournament(tournament_id)
|
||||
check_requirement(existing_courts, user, "max_courts")
|
||||
|
||||
@@ -25,10 +25,11 @@ from bracket.models.db.match import (
|
||||
MatchRescheduleBody,
|
||||
)
|
||||
from bracket.models.db.stage_item import StageType
|
||||
from bracket.models.db.tournament import Tournament
|
||||
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
|
||||
from bracket.routes.util import disallow_archived_tournament, match_dependency
|
||||
from bracket.sql.courts import get_all_courts_in_tournament
|
||||
from bracket.sql.matches import sql_create_match, sql_delete_match, sql_update_match
|
||||
from bracket.sql.rounds import get_round_by_id
|
||||
@@ -76,6 +77,7 @@ async def get_matches_to_schedule(
|
||||
async def delete_match(
|
||||
tournament_id: TournamentId,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
match: Match = Depends(match_dependency),
|
||||
) -> SuccessResponse:
|
||||
round_ = await get_round_by_id(tournament_id, match.round_id)
|
||||
@@ -100,6 +102,7 @@ async def create_match(
|
||||
tournament_id: TournamentId,
|
||||
match_body: MatchCreateBodyFrontend,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SingleMatchResponse:
|
||||
await check_foreign_keys_belong_to_tournament(match_body, tournament_id)
|
||||
|
||||
@@ -126,6 +129,7 @@ async def create_match(
|
||||
async def schedule_matches(
|
||||
tournament_id: TournamentId,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
stages = await get_full_tournament_details(tournament_id)
|
||||
await schedule_all_unscheduled_matches(tournament_id, stages)
|
||||
@@ -140,6 +144,7 @@ async def reschedule_match(
|
||||
match_id: MatchId,
|
||||
body: MatchRescheduleBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
await check_foreign_keys_belong_to_tournament(body, tournament_id)
|
||||
await handle_match_reschedule(tournament_id, body, match_id)
|
||||
@@ -153,6 +158,7 @@ async def update_match_by_id(
|
||||
match_id: MatchId,
|
||||
match_body: MatchBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
match: Match = Depends(match_dependency),
|
||||
) -> SuccessResponse:
|
||||
await check_foreign_keys_belong_to_tournament(match_body, tournament_id)
|
||||
|
||||
@@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends
|
||||
from bracket.database import database
|
||||
from bracket.logic.subscriptions import check_requirement
|
||||
from bracket.models.db.player import Player, PlayerBody, PlayerMultiBody
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import user_authenticated_for_tournament
|
||||
from bracket.routes.models import (
|
||||
@@ -11,6 +12,7 @@ from bracket.routes.models import (
|
||||
SinglePlayerResponse,
|
||||
SuccessResponse,
|
||||
)
|
||||
from bracket.routes.util import disallow_archived_tournament
|
||||
from bracket.schema import players
|
||||
from bracket.sql.players import (
|
||||
get_all_players_in_tournament,
|
||||
@@ -49,6 +51,7 @@ async def update_player_by_id(
|
||||
player_id: PlayerId,
|
||||
player_body: PlayerBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SinglePlayerResponse:
|
||||
await database.execute(
|
||||
query=players.update().where(
|
||||
@@ -74,6 +77,7 @@ async def delete_player(
|
||||
tournament_id: TournamentId,
|
||||
player_id: PlayerId,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
await sql_delete_player(tournament_id, player_id)
|
||||
return SuccessResponse()
|
||||
@@ -84,6 +88,7 @@ async def create_single_player(
|
||||
player_body: PlayerBody,
|
||||
tournament_id: TournamentId,
|
||||
user: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
_: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
existing_players = await get_all_players_in_tournament(tournament_id)
|
||||
check_requirement(existing_players, user, "max_players")
|
||||
@@ -96,6 +101,7 @@ async def create_multiple_players(
|
||||
player_body: PlayerMultiBody,
|
||||
tournament_id: TournamentId,
|
||||
user: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
_: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
player_names = [player.strip() for player in player_body.names.split("\n") if len(player) > 0]
|
||||
existing_players = await get_all_players_in_tournament(tournament_id)
|
||||
|
||||
@@ -7,6 +7,7 @@ from bracket.logic.ranking.elimination import (
|
||||
from bracket.logic.subscriptions import check_requirement
|
||||
from bracket.models.db.ranking import RankingBody, RankingCreateBody
|
||||
from bracket.models.db.stage_item import StageType
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import (
|
||||
user_authenticated_for_tournament,
|
||||
@@ -16,6 +17,7 @@ from bracket.routes.models import (
|
||||
RankingsResponse,
|
||||
SuccessResponse,
|
||||
)
|
||||
from bracket.routes.util import disallow_archived_tournament
|
||||
from bracket.sql.rankings import (
|
||||
get_all_rankings_in_tournament,
|
||||
sql_create_ranking,
|
||||
@@ -43,6 +45,7 @@ async def update_ranking_by_id(
|
||||
ranking_id: RankingId,
|
||||
ranking_body: RankingBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
await sql_update_ranking(
|
||||
tournament_id=tournament_id,
|
||||
@@ -64,6 +67,7 @@ async def delete_ranking(
|
||||
tournament_id: TournamentId,
|
||||
ranking_id: RankingId,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
await sql_delete_ranking(tournament_id, ranking_id)
|
||||
return SuccessResponse()
|
||||
@@ -74,6 +78,7 @@ async def create_ranking(
|
||||
ranking_body: RankingCreateBody,
|
||||
tournament_id: TournamentId,
|
||||
user: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
_: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
existing_rankings = await get_all_rankings_in_tournament(tournament_id)
|
||||
check_requirement(existing_rankings, user, "max_rankings")
|
||||
|
||||
@@ -12,11 +12,13 @@ from bracket.models.db.round import (
|
||||
RoundInsertable,
|
||||
RoundUpdateBody,
|
||||
)
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.models.db.util import RoundWithMatches
|
||||
from bracket.routes.auth import user_authenticated_for_tournament
|
||||
from bracket.routes.models import SuccessResponse
|
||||
from bracket.routes.util import (
|
||||
disallow_archived_tournament,
|
||||
round_dependency,
|
||||
round_with_matches_dependency,
|
||||
)
|
||||
@@ -41,6 +43,7 @@ async def delete_round(
|
||||
tournament_id: TournamentId,
|
||||
round_id: RoundId,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
round_with_matches: RoundWithMatches = Depends(round_with_matches_dependency),
|
||||
) -> SuccessResponse:
|
||||
for match in round_with_matches.matches:
|
||||
@@ -58,6 +61,7 @@ async def create_round(
|
||||
tournament_id: TournamentId,
|
||||
round_body: RoundCreateBody,
|
||||
user: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
_: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
await check_foreign_keys_belong_to_tournament(round_body, tournament_id)
|
||||
|
||||
@@ -98,6 +102,7 @@ async def update_round_by_id(
|
||||
round_body: RoundUpdateBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Round = Depends(round_dependency),
|
||||
___: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
query = """
|
||||
UPDATE rounds
|
||||
|
||||
@@ -8,13 +8,14 @@ from bracket.models.db.stage_item_inputs import (
|
||||
StageItemInputUpdateBodyFinal,
|
||||
StageItemInputUpdateBodyTentative,
|
||||
)
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.models.db.util import StageItemWithRounds
|
||||
from bracket.routes.auth import (
|
||||
user_authenticated_for_tournament,
|
||||
)
|
||||
from bracket.routes.models import SuccessResponse
|
||||
from bracket.routes.util import stage_item_dependency
|
||||
from bracket.routes.util import disallow_archived_tournament, stage_item_dependency
|
||||
from bracket.sql.stage_item_inputs import get_stage_item_input_by_id
|
||||
from bracket.sql.stages import get_full_tournament_details
|
||||
from bracket.sql.teams import get_team_by_id
|
||||
@@ -72,6 +73,7 @@ async def update_stage_item_input(
|
||||
stage_item_body: StageItemInputUpdateBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: StageItemWithRounds = Depends(stage_item_dependency),
|
||||
___: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
stage_item_input = await get_stage_item_input_by_id(tournament_id, stage_item_input_id)
|
||||
await validate_stage_item_update(stage_item_input, stage_item_body, tournament_id)
|
||||
|
||||
@@ -27,13 +27,14 @@ from bracket.models.db.stage_item import (
|
||||
StageItemUpdateBody,
|
||||
StageType,
|
||||
)
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.models.db.util import StageItemWithRounds
|
||||
from bracket.routes.auth import (
|
||||
user_authenticated_for_tournament,
|
||||
)
|
||||
from bracket.routes.models import SuccessResponse
|
||||
from bracket.routes.util import stage_item_dependency
|
||||
from bracket.routes.util import disallow_archived_tournament, stage_item_dependency
|
||||
from bracket.sql.courts import get_all_courts_in_tournament
|
||||
from bracket.sql.matches import sql_create_match
|
||||
from bracket.sql.rounds import (
|
||||
@@ -101,6 +102,7 @@ async def update_stage_item(
|
||||
stage_item_id: StageItemId,
|
||||
stage_item_body: StageItemUpdateBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
stage_item: StageItemWithRounds = Depends(stage_item_dependency),
|
||||
) -> SuccessResponse:
|
||||
if stage_item is None:
|
||||
@@ -137,6 +139,7 @@ async def start_next_round(
|
||||
elo_diff_threshold: int = 200,
|
||||
iterations: int = 2_000,
|
||||
only_recommended: bool = False,
|
||||
_: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
draft_round = get_draft_round(stage_item)
|
||||
if draft_round is not None:
|
||||
|
||||
@@ -10,6 +10,7 @@ from bracket.logic.scheduling.handle_stage_activation import (
|
||||
)
|
||||
from bracket.logic.subscriptions import check_requirement
|
||||
from bracket.models.db.stage import Stage, StageActivateBody, StageUpdateBody
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.models.db.util import StageWithStageItems
|
||||
from bracket.routes.auth import (
|
||||
@@ -22,7 +23,7 @@ from bracket.routes.models import (
|
||||
StagesWithStageItemsResponse,
|
||||
SuccessResponse,
|
||||
)
|
||||
from bracket.routes.util import stage_dependency
|
||||
from bracket.routes.util import disallow_archived_tournament, stage_dependency
|
||||
from bracket.sql.stages import (
|
||||
get_full_tournament_details,
|
||||
get_next_stage_in_tournament,
|
||||
@@ -57,6 +58,7 @@ async def delete_stage(
|
||||
tournament_id: TournamentId,
|
||||
stage_id: StageId,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
stage: StageWithStageItems = Depends(stage_dependency),
|
||||
) -> SuccessResponse:
|
||||
if len(stage.stage_items) > 0:
|
||||
@@ -80,6 +82,7 @@ async def delete_stage(
|
||||
async def create_stage(
|
||||
tournament_id: TournamentId,
|
||||
user: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
_: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
existing_stages = await get_full_tournament_details(tournament_id)
|
||||
check_requirement(existing_stages, user, "max_stages")
|
||||
@@ -94,6 +97,7 @@ async def update_stage(
|
||||
stage_id: StageId,
|
||||
stage_body: StageUpdateBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
stage: Stage = Depends(stage_dependency), # pylint: disable=redefined-builtin
|
||||
) -> SuccessResponse:
|
||||
values = {"tournament_id": tournament_id, "stage_id": stage_id}
|
||||
@@ -115,6 +119,7 @@ async def activate_next_stage(
|
||||
tournament_id: TournamentId,
|
||||
stage_body: StageActivateBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
new_active_stage_id = await get_next_stage_in_tournament(tournament_id, stage_body.direction)
|
||||
if new_active_stage_id is None:
|
||||
|
||||
@@ -16,6 +16,7 @@ from bracket.models.db.team import (
|
||||
TeamInsertable,
|
||||
TeamMultiBody,
|
||||
)
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import (
|
||||
user_authenticated_for_tournament,
|
||||
@@ -27,7 +28,11 @@ from bracket.routes.models import (
|
||||
SuccessResponse,
|
||||
TeamsWithPlayersResponse,
|
||||
)
|
||||
from bracket.routes.util import team_dependency, team_with_players_dependency
|
||||
from bracket.routes.util import (
|
||||
disallow_archived_tournament,
|
||||
team_dependency,
|
||||
team_with_players_dependency,
|
||||
)
|
||||
from bracket.schema import players_x_teams, teams
|
||||
from bracket.sql.teams import (
|
||||
get_team_by_id,
|
||||
@@ -87,6 +92,7 @@ async def update_team_by_id(
|
||||
tournament_id: TournamentId,
|
||||
team_body: TeamBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
team: Team = Depends(team_dependency),
|
||||
) -> SingleTeamResponse:
|
||||
await check_foreign_keys_belong_to_tournament(team_body, tournament_id)
|
||||
@@ -117,6 +123,7 @@ async def update_team_logo(
|
||||
tournament_id: TournamentId,
|
||||
file: UploadFile | None = None,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
team: Team = Depends(team_dependency),
|
||||
) -> SingleTeamResponse:
|
||||
old_logo_path = await get_team_logo_path(tournament_id, team.id)
|
||||
@@ -153,6 +160,7 @@ async def update_team_logo(
|
||||
async def delete_team(
|
||||
tournament_id: TournamentId,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
team: FullTeamWithPlayers = Depends(team_with_players_dependency),
|
||||
) -> SuccessResponse:
|
||||
with check_foreign_key_violation(
|
||||
@@ -172,6 +180,7 @@ async def create_team(
|
||||
team_to_insert: TeamBody,
|
||||
tournament_id: TournamentId,
|
||||
user: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
_: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SingleTeamResponse:
|
||||
await check_foreign_keys_belong_to_tournament(team_to_insert, tournament_id)
|
||||
|
||||
@@ -198,6 +207,7 @@ async def create_multiple_teams(
|
||||
team_body: TeamMultiBody,
|
||||
tournament_id: TournamentId,
|
||||
user: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
_: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
team_names = [team.strip() for team in team_body.names.split("\n") if len(team) > 0]
|
||||
existing_teams = await get_teams_with_members(tournament_id)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
|
||||
import aiofiles.os
|
||||
@@ -11,7 +12,9 @@ from bracket.logic.subscriptions import check_requirement
|
||||
from bracket.logic.tournaments import get_tournament_logo_path
|
||||
from bracket.models.db.ranking import RankingCreateBody
|
||||
from bracket.models.db.tournament import (
|
||||
Tournament,
|
||||
TournamentBody,
|
||||
TournamentChangeStatusBody,
|
||||
TournamentUpdateBody,
|
||||
)
|
||||
from bracket.models.db.user import UserPublic
|
||||
@@ -22,6 +25,7 @@ from bracket.routes.auth import (
|
||||
user_authenticated_or_public_dashboard_by_endpoint_name,
|
||||
)
|
||||
from bracket.routes.models import SuccessResponse, TournamentResponse, TournamentsResponse
|
||||
from bracket.routes.util import disallow_archived_tournament
|
||||
from bracket.schema import tournaments
|
||||
from bracket.sql.rankings import (
|
||||
get_all_rankings_in_tournament,
|
||||
@@ -35,6 +39,7 @@ from bracket.sql.tournaments import (
|
||||
sql_get_tournament_by_endpoint_name,
|
||||
sql_get_tournaments,
|
||||
sql_update_tournament,
|
||||
sql_update_tournament_status,
|
||||
)
|
||||
from bracket.sql.users import get_user_access_to_club, get_which_clubs_has_user_access_to
|
||||
from bracket.utils.errors import (
|
||||
@@ -69,6 +74,7 @@ async def get_tournament(
|
||||
@router.get("/tournaments", response_model=TournamentsResponse)
|
||||
async def get_tournaments(
|
||||
user: UserPublic | None = Depends(user_authenticated_or_public_dashboard_by_endpoint_name),
|
||||
filter_: Literal["ALL", "OPEN", "ARCHIVED"] = "OPEN",
|
||||
endpoint_name: str | None = None,
|
||||
) -> TournamentsResponse:
|
||||
match user, endpoint_name:
|
||||
@@ -88,7 +94,7 @@ async def get_tournaments(
|
||||
case _, _ if isinstance(user, UserPublic):
|
||||
user_club_ids = await get_which_clubs_has_user_access_to(user.id)
|
||||
return TournamentsResponse(
|
||||
data=await sql_get_tournaments(tuple(user_club_ids), endpoint_name)
|
||||
data=await sql_get_tournaments(tuple(user_club_ids), endpoint_name, filter_)
|
||||
)
|
||||
|
||||
raise RuntimeError()
|
||||
@@ -99,6 +105,7 @@ async def update_tournament_by_id(
|
||||
tournament_id: TournamentId,
|
||||
tournament_body: TournamentUpdateBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> SuccessResponse:
|
||||
with check_unique_constraint_violation({UniqueIndex.ix_tournaments_dashboard_endpoint}):
|
||||
await sql_update_tournament(tournament_id, tournament_body)
|
||||
@@ -127,6 +134,28 @@ async def delete_tournament(
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
@router.post("/tournaments/{tournament_id}/change-status", response_model=SuccessResponse)
|
||||
async def change_status(
|
||||
tournament_id: TournamentId,
|
||||
body: TournamentChangeStatusBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SuccessResponse:
|
||||
"""
|
||||
Make a tournament archived or non-archived.
|
||||
"""
|
||||
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
if tournament.status == body.status:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Tournament already has the requested status",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
await sql_update_tournament_status(tournament_id, body)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
@router.post("/tournaments", response_model=SuccessResponse)
|
||||
async def create_tournament(
|
||||
tournament_to_insert: TournamentBody, user: UserPublic = Depends(user_authenticated)
|
||||
@@ -157,6 +186,7 @@ async def upload_logo(
|
||||
tournament_id: TournamentId,
|
||||
file: UploadFile | None = None,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
__: Tournament = Depends(disallow_archived_tournament),
|
||||
) -> TournamentResponse:
|
||||
old_logo_path = await get_tournament_logo_path(tournament_id)
|
||||
filename: str | None = None
|
||||
|
||||
@@ -5,12 +5,14 @@ from bracket.database import database
|
||||
from bracket.models.db.match import Match
|
||||
from bracket.models.db.round import Round
|
||||
from bracket.models.db.team import FullTeamWithPlayers, Team
|
||||
from bracket.models.db.tournament import Tournament, TournamentStatus
|
||||
from bracket.models.db.util import RoundWithMatches, StageItemWithRounds, StageWithStageItems
|
||||
from bracket.schema import matches, rounds, teams
|
||||
from bracket.sql.rounds import get_round_by_id
|
||||
from bracket.sql.stage_items import get_stage_item
|
||||
from bracket.sql.stages import get_full_tournament_details
|
||||
from bracket.sql.teams import get_teams_with_members
|
||||
from bracket.sql.tournaments import sql_get_tournament
|
||||
from bracket.utils.db import fetch_one_parsed
|
||||
from bracket.utils.id_types import MatchId, RoundId, StageId, StageItemId, TeamId, TournamentId
|
||||
|
||||
@@ -103,3 +105,15 @@ async def team_with_players_dependency(
|
||||
)
|
||||
|
||||
return teams_with_members[0]
|
||||
|
||||
|
||||
async def disallow_archived_tournament(tournament_id: TournamentId) -> Tournament:
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
if tournament.status is TournamentStatus.ARCHIVED:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Can't update archived tournament",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return tournament
|
||||
|
||||
@@ -29,6 +29,17 @@ tournaments = Table(
|
||||
Column("auto_assign_courts", Boolean, nullable=False, server_default="f"),
|
||||
Column("duration_minutes", Integer, nullable=False, server_default="15"),
|
||||
Column("margin_minutes", Integer, nullable=False, server_default="5"),
|
||||
Column(
|
||||
"status",
|
||||
Enum(
|
||||
"OPEN",
|
||||
"ARCHIVED",
|
||||
name="tournament_status",
|
||||
),
|
||||
nullable=False,
|
||||
server_default="OPEN",
|
||||
index=True,
|
||||
),
|
||||
)
|
||||
|
||||
stages = Table(
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.tournament import Tournament, TournamentBody, TournamentUpdateBody
|
||||
from bracket.models.db.tournament import (
|
||||
Tournament,
|
||||
TournamentBody,
|
||||
TournamentChangeStatusBody,
|
||||
TournamentUpdateBody,
|
||||
)
|
||||
from bracket.utils.id_types import TournamentId
|
||||
|
||||
|
||||
@@ -28,7 +33,9 @@ async def sql_get_tournament_by_endpoint_name(endpoint_name: str) -> Tournament
|
||||
|
||||
|
||||
async def sql_get_tournaments(
|
||||
club_ids: tuple[int, ...], endpoint_name: str | None = None
|
||||
club_ids: tuple[int, ...],
|
||||
endpoint_name: str | None = None,
|
||||
filter_: Literal["ALL", "OPEN", "ARCHIVED"] = "ALL",
|
||||
) -> list[Tournament]:
|
||||
query = """
|
||||
SELECT *
|
||||
@@ -42,6 +49,11 @@ async def sql_get_tournaments(
|
||||
query += "AND dashboard_endpoint = :endpoint_name"
|
||||
params = {**params, "endpoint_name": endpoint_name}
|
||||
|
||||
if filter_ == "OPEN":
|
||||
query += "AND status = 'OPEN'"
|
||||
elif filter_ == "ARCHIVED":
|
||||
query += "AND status = 'ARCHIVED'"
|
||||
|
||||
result = await database.fetch_all(query=query, values=params)
|
||||
return [Tournament.model_validate(x) for x in result]
|
||||
|
||||
@@ -76,6 +88,23 @@ async def sql_update_tournament(
|
||||
)
|
||||
|
||||
|
||||
async def sql_update_tournament_status(
|
||||
tournament_id: TournamentId, body: TournamentChangeStatusBody
|
||||
) -> None:
|
||||
query = """
|
||||
UPDATE tournaments
|
||||
SET
|
||||
status = :state,
|
||||
dashboard_public = :dashboard_public
|
||||
WHERE tournaments.id = :tournament_id
|
||||
"""
|
||||
|
||||
# Make dashboard non-public when archiving.
|
||||
# When tournament is archived, setting dashboard_public to False shouldn't have an effect.
|
||||
params = {"tournament_id": tournament_id, "state": body.status.value, "dashboard_public": False}
|
||||
await database.execute(query=query, values=params)
|
||||
|
||||
|
||||
async def sql_create_tournament(tournament: TournamentBody) -> TournamentId:
|
||||
query = """
|
||||
INSERT INTO tournaments (
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.logic.tournaments import sql_delete_tournament_completely
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.tournament import Tournament, TournamentStatus
|
||||
from bracket.schema import tournaments
|
||||
from bracket.sql.tournaments import sql_delete_tournament, sql_get_tournament_by_endpoint_name
|
||||
from bracket.utils.db import fetch_one_parsed_certain
|
||||
@@ -40,6 +40,7 @@ async def test_tournaments_endpoint(
|
||||
"auto_assign_courts": True,
|
||||
"duration_minutes": 10,
|
||||
"margin_minutes": 5,
|
||||
"status": "OPEN",
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -65,6 +66,7 @@ async def test_tournament_endpoint(
|
||||
"auto_assign_courts": True,
|
||||
"duration_minutes": 10,
|
||||
"margin_minutes": 5,
|
||||
"status": "OPEN",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -141,6 +143,36 @@ async def test_update_tournament(
|
||||
assert updated_tournament.dashboard_public == body["dashboard_public"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_archive_and_unarchive_tournament(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
query = tournaments.select().where(tournaments.c.id == auth_context.tournament.id)
|
||||
body = {"status": "ARCHIVED"}
|
||||
assert (
|
||||
await send_tournament_request(HTTPMethod.POST, "change-status", auth_context, json=body)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
updated_tournament = await fetch_one_parsed_certain(database, Tournament, query)
|
||||
assert updated_tournament.status is TournamentStatus.ARCHIVED
|
||||
assert updated_tournament.dashboard_public is False
|
||||
|
||||
# Archiving twice is not allowed
|
||||
assert await send_tournament_request(
|
||||
HTTPMethod.POST, "change-status", auth_context, json=body
|
||||
) == {"detail": "Tournament already has the requested status"}
|
||||
|
||||
# Unarchive the tournament
|
||||
body = {"status": "OPEN"}
|
||||
assert (
|
||||
await send_tournament_request(HTTPMethod.POST, "change-status", auth_context, json=body)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
updated_tournament = await fetch_one_parsed_certain(database, Tournament, query)
|
||||
assert updated_tournament.status is TournamentStatus.OPEN
|
||||
assert updated_tournament.dashboard_public is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_delete_tournament(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
@@ -182,7 +214,7 @@ async def test_tournament_upload_and_remove_logo(
|
||||
body=data,
|
||||
)
|
||||
|
||||
assert response["data"]["logo_path"], f"Response: {response}"
|
||||
assert response.get("data", {}).get("logo_path"), f"Response: {response}"
|
||||
assert await aiofiles.os.path.exists(f"static/tournament-logos/{response['data']['logo_path']}")
|
||||
|
||||
response = await send_tournament_request(
|
||||
|
||||
@@ -28,15 +28,16 @@
|
||||
"add_team_button": "Add Team",
|
||||
"adjust_start_times_checkbox_label": "Adjust start time of matches in this round to the current time",
|
||||
"all_matches_radio_label": "All matches",
|
||||
"all_matches_scheduled_description": "Matches have been scheduled on all courts in this round. Add a new round or add a new court for more matches.",
|
||||
"api_docs_title": "API docs",
|
||||
"mark_round_as_non_draft": "Mark this round as ready",
|
||||
"mark_round_as_draft": "Mark this round as draft",
|
||||
"archive_tournament_button": "Archive Tournament",
|
||||
"archived_label": "Archived",
|
||||
"archived_header_label": "This tournament is archived. It is now read-only.",
|
||||
"at_least_one_player_validation": "Enter at least one player",
|
||||
"at_least_one_team_validation": "Enter at least one team",
|
||||
"at_least_two_team_validation": "Need at least two teams",
|
||||
"auto_assign_courts_label": "Automatically assign courts to matches",
|
||||
"auto_create_matches_button": "Plan new round automatically",
|
||||
"courts_filled_badge": "courts filled",
|
||||
"back_home_nav": "Take me back to home page",
|
||||
"back_to_login_nav": "Back to login page",
|
||||
"checkbox_status_checked": "Checked",
|
||||
@@ -53,6 +54,7 @@
|
||||
"copy_url_button": "Copy URL",
|
||||
"could_not_find_any_alert": "Could not find any",
|
||||
"court_name_input_placeholder": "Best Court Ever",
|
||||
"courts_filled_badge": "courts filled",
|
||||
"courts_title": "courts",
|
||||
"create_account_alert_description": "Account creation is disabled on this domain for now since bracket is still in beta phase",
|
||||
"create_account_alert_title": "Unavailable",
|
||||
@@ -118,6 +120,7 @@
|
||||
"filter_stage_item_placeholder": "No filter",
|
||||
"forgot_password_button": "Forgot password?",
|
||||
"github_title": "Github",
|
||||
"go_to_courts_page": "Go to courts page",
|
||||
"handle_swiss_system": "Handle Swiss System",
|
||||
"home_spotlight_description": "Get to home page",
|
||||
"home_title": "Home",
|
||||
@@ -133,6 +136,8 @@
|
||||
"loss_points_input_label": "Points for a loss",
|
||||
"lowercase_required": "Includes lowercase letter",
|
||||
"margin_minutes_choose_title": "Please choose a margin between matches",
|
||||
"mark_round_as_draft": "Mark this round as draft",
|
||||
"mark_round_as_non_draft": "Mark this round as ready",
|
||||
"match_duration_label": "Match duration (minutes)",
|
||||
"match_filter_option_all": "All matches",
|
||||
"match_filter_option_current": "Current matches",
|
||||
@@ -161,9 +166,9 @@
|
||||
"no_courts_description": "No courts have been created yet. First, create the tournament structure by adding stages and stage items. Then, create courts here and schedule matches on these courts.",
|
||||
"no_courts_description_swiss": "No courts have been created yet. First add courts before managing a Swiss stage item.",
|
||||
"no_courts_title": "No courts yet",
|
||||
"go_to_courts_page": "Go to courts page",
|
||||
"no_matches_description": "First, add matches by creating stages and stage items. Then, schedule them using the button in the topright corner.",
|
||||
"no_matches_title": "No matches scheduled yet",
|
||||
"no_more_matches_title": "No more matches to schedule",
|
||||
"no_players_title": "No players yet",
|
||||
"no_round_description": "There are no rounds in this stage item yet",
|
||||
"no_round_found_description": "Please wait for the organiser to add them.",
|
||||
@@ -248,9 +253,8 @@
|
||||
"tournament_setting_title": "Tournament Settings",
|
||||
"tournament_title": "tournament",
|
||||
"tournaments_title": "tournaments",
|
||||
"unarchive_tournament_button": "Unarchive Tournament",
|
||||
"upcoming_matches_empty_table_info": "upcoming matches",
|
||||
"no_more_matches_title": "No more matches to schedule",
|
||||
"all_matches_scheduled_description": "Matches have been scheduled on all courts in this round. Add a new round or add a new court for more matches.",
|
||||
"upload_placeholder_team": "Drop a file here to upload as team logo.",
|
||||
"upload_placeholder_tournament": "Drop a file here to upload as tournament logo.",
|
||||
"uppercase_required": "Includes uppercase letter",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Card, Group, Image, Text, UnstyledButton } from '@mantine/core';
|
||||
import { Badge, Button, Card, Group, Image, Text, UnstyledButton } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
@@ -65,8 +65,9 @@ export default function TournamentsCardTable({
|
||||
</Card.Section>
|
||||
|
||||
<Group justify="space-between" mt="md" mb="xs">
|
||||
<Text fw={500}>{tournament.name}</Text>
|
||||
{/*<Badge color="pink">Archived</Badge>*/}
|
||||
<Text fw={500} lineClamp={1}>
|
||||
{tournament.name}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Card.Section className={classes.section}>
|
||||
@@ -74,15 +75,26 @@ export default function TournamentsCardTable({
|
||||
</Card.Section>
|
||||
|
||||
<Card.Section className={classes.section}>
|
||||
<Button
|
||||
component={Link}
|
||||
color="blue"
|
||||
fullWidth
|
||||
radius="md"
|
||||
href={`/tournaments/${tournament.id}/stages`}
|
||||
>
|
||||
OPEN
|
||||
</Button>
|
||||
<Group w="100%">
|
||||
<Badge
|
||||
fullWidth
|
||||
color="yellow"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
style={{ visibility: tournament.status === 'ARCHIVED' ? 'visible' : 'hidden' }}
|
||||
>
|
||||
{t('archived_label')}
|
||||
</Badge>
|
||||
<Button
|
||||
component={Link}
|
||||
color="blue"
|
||||
fullWidth
|
||||
radius="md"
|
||||
href={`/tournaments/${tournament.id}/stages`}
|
||||
>
|
||||
OPEN
|
||||
</Button>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</UnstyledButton>
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
|
||||
export function Brand() {
|
||||
return (
|
||||
<Center mr="1rem">
|
||||
<Center mr="1rem" miw="12rem">
|
||||
<UnstyledButton component={Link} href="/">
|
||||
<Group>
|
||||
<Image
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export type TournamentStatus = 'OPEN' | 'ARCHIVED';
|
||||
|
||||
export type TournamentFilter = 'ALL' | TournamentStatus;
|
||||
|
||||
export interface Tournament {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -11,6 +15,7 @@ export interface Tournament {
|
||||
logo_path: string;
|
||||
duration_minutes: number;
|
||||
margin_minutes: number;
|
||||
status: TournamentStatus;
|
||||
}
|
||||
export interface TournamentMinimal {
|
||||
id: number;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.fullWithMobile {
|
||||
@media (max-width: $mantine-breakpoint-xs) {
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
padding-right: 1rem;
|
||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,45 @@
|
||||
import { Grid, Title } from '@mantine/core';
|
||||
import { Grid, Select, Title } from '@mantine/core';
|
||||
import { GetStaticProps } from 'next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useState } from 'react';
|
||||
|
||||
import TournamentsCardTable from '../components/card_tables/tournaments';
|
||||
import TournamentModal from '../components/modals/tournament_modal';
|
||||
import { capitalize } from '../components/utils/util';
|
||||
import { TournamentFilter } from '../interfaces/tournament';
|
||||
import { checkForAuthError, getTournaments } from '../services/adapter';
|
||||
import Layout from './_layout';
|
||||
import classes from './index.module.css';
|
||||
|
||||
export default function HomePage() {
|
||||
const swrTournamentsResponse = getTournaments();
|
||||
checkForAuthError(swrTournamentsResponse);
|
||||
const { t } = useTranslation();
|
||||
const [filter, setFilter] = useState<TournamentFilter>('OPEN');
|
||||
|
||||
const swrTournamentsResponse = getTournaments(filter);
|
||||
checkForAuthError(swrTournamentsResponse);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Grid justify="space-between">
|
||||
<Grid>
|
||||
<Grid.Col span="auto">
|
||||
<Title>{capitalize(t('tournaments_title'))}</Title>
|
||||
</Grid.Col>
|
||||
<Grid.Col span="content" className={classes.fullWithMobile}>
|
||||
<Select
|
||||
size="md"
|
||||
placeholder="Pick value"
|
||||
data={[
|
||||
{ label: 'All', value: 'ALL' },
|
||||
{ label: 'Archived', value: 'ARCHIVED' },
|
||||
{ label: 'Open', value: 'OPEN' },
|
||||
]}
|
||||
allowDeselect={false}
|
||||
value={filter}
|
||||
// @ts-ignore
|
||||
onChange={(f: TournamentFilter) => setFilter(f)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span="content" className={classes.fullWithMobile}>
|
||||
<TournamentModal swrTournamentsResponse={swrTournamentsResponse} />
|
||||
</Grid.Col>
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
Checkbox,
|
||||
Container,
|
||||
CopyButton,
|
||||
Divider,
|
||||
Fieldset,
|
||||
Grid,
|
||||
Group,
|
||||
Image,
|
||||
NumberInput,
|
||||
Select,
|
||||
@@ -15,12 +17,14 @@ import {
|
||||
import { DateTimePicker } from '@mantine/dates';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { MdDelete } from '@react-icons/all-files/md/MdDelete';
|
||||
import { IconCalendar, IconCalendarTime, IconCopy } from '@tabler/icons-react';
|
||||
import { MdUnarchive } from '@react-icons/all-files/md/MdUnarchive';
|
||||
import { IconCalendar, IconCalendarTime, IconCopy, IconPencil } from '@tabler/icons-react';
|
||||
import assert from 'assert';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { MdArchive } from 'react-icons/md';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import NotFoundTitle from '../../404';
|
||||
@@ -36,7 +40,12 @@ import {
|
||||
handleRequestError,
|
||||
removeTournamentLogo,
|
||||
} from '../../../services/adapter';
|
||||
import { deleteTournament, updateTournament } from '../../../services/tournament';
|
||||
import {
|
||||
archiveTournament,
|
||||
deleteTournament,
|
||||
unarchiveTournament,
|
||||
updateTournament,
|
||||
} from '../../../services/tournament';
|
||||
import TournamentLayout from '../_tournament_layout';
|
||||
|
||||
export function TournamentLogo({ tournament }: { tournament: Tournament | null }) {
|
||||
@@ -50,6 +59,62 @@ export function TournamentLogo({ tournament }: { tournament: Tournament | null }
|
||||
);
|
||||
}
|
||||
|
||||
function ArchiveTournamentButton({
|
||||
t,
|
||||
tournament,
|
||||
swrTournamentResponse,
|
||||
}: {
|
||||
t: any;
|
||||
tournament: Tournament;
|
||||
swrTournamentResponse: SWRResponse;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
mt="sm"
|
||||
color="orange"
|
||||
size="lg"
|
||||
leftSection={<MdArchive size={36} />}
|
||||
onClick={async () => {
|
||||
await archiveTournament(tournament.id).catch((response: any) =>
|
||||
handleRequestError(response)
|
||||
);
|
||||
await swrTournamentResponse.mutate();
|
||||
}}
|
||||
>
|
||||
{t('archive_tournament_button')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function UnarchiveTournamentButton({
|
||||
t,
|
||||
tournament,
|
||||
swrTournamentResponse,
|
||||
}: {
|
||||
t: any;
|
||||
tournament: Tournament;
|
||||
swrTournamentResponse: SWRResponse;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
mt="sm"
|
||||
color="orange"
|
||||
size="lg"
|
||||
leftSection={<MdUnarchive size={36} />}
|
||||
onClick={async () => {
|
||||
await unarchiveTournament(tournament.id).catch((response: any) =>
|
||||
handleRequestError(response)
|
||||
);
|
||||
await swrTournamentResponse.mutate();
|
||||
}}
|
||||
>
|
||||
{t('unarchive_tournament_button')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function GeneralTournamentForm({
|
||||
tournament,
|
||||
swrTournamentResponse,
|
||||
@@ -233,27 +298,50 @@ function GeneralTournamentForm({
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
<Button fullWidth mt={24} color="green" type="submit">
|
||||
<Button
|
||||
fullWidth
|
||||
mt={24}
|
||||
size="md"
|
||||
color="green"
|
||||
type="submit"
|
||||
leftSection={<IconPencil size={36} />}
|
||||
>
|
||||
{t('save_button')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outline"
|
||||
mt="sm"
|
||||
color="red"
|
||||
size="sm"
|
||||
leftSection={<MdDelete size={20} />}
|
||||
onClick={async () => {
|
||||
await deleteTournament(tournament.id)
|
||||
.then(async () => {
|
||||
await router.push('/');
|
||||
})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}}
|
||||
>
|
||||
{t('delete_tournament_button')}
|
||||
</Button>
|
||||
<Divider mt="2rem" mb="1rem" size="2px" />
|
||||
<Group grow>
|
||||
<Button
|
||||
variant="outline"
|
||||
mt="sm"
|
||||
color="red"
|
||||
size="lg"
|
||||
leftSection={<MdDelete size={36} />}
|
||||
onClick={async () => {
|
||||
await deleteTournament(tournament.id)
|
||||
.then(async () => {
|
||||
await router.push('/');
|
||||
})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}}
|
||||
>
|
||||
{t('delete_tournament_button')}
|
||||
</Button>
|
||||
|
||||
{tournament.status === 'OPEN' ? (
|
||||
<ArchiveTournamentButton
|
||||
tournament={tournament}
|
||||
t={t}
|
||||
swrTournamentResponse={swrTournamentResponse}
|
||||
/>
|
||||
) : (
|
||||
<UnarchiveTournamentButton
|
||||
tournament={tournament}
|
||||
t={t}
|
||||
swrTournamentResponse={swrTournamentResponse}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Group, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React from 'react';
|
||||
import { HiArchiveBoxArrowDown } from 'react-icons/hi2';
|
||||
|
||||
import { TournamentLinks } from '../../components/navbar/_main_links';
|
||||
import { responseIsValid } from '../../components/utils/util';
|
||||
@@ -6,12 +9,33 @@ import { checkForAuthError, getTournamentById } from '../../services/adapter';
|
||||
import Layout from '../_layout';
|
||||
|
||||
export default function TournamentLayout({ children, tournament_id }: any) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tournamentResponse = getTournamentById(tournament_id);
|
||||
checkForAuthError(tournamentResponse);
|
||||
|
||||
const tournamentLinks = <TournamentLinks tournament_id={tournament_id} />;
|
||||
const breadcrumbs = responseIsValid(tournamentResponse) ? (
|
||||
<h2>/ {tournamentResponse.data.data.name}</h2>
|
||||
<Group gap="xs" miw="25rem">
|
||||
<Title order={2} maw="20rem">
|
||||
/
|
||||
</Title>
|
||||
<Title order={2} maw="20rem" lineClamp={1}>
|
||||
{tournamentResponse.data.data.name}
|
||||
</Title>
|
||||
|
||||
<Tooltip label={`${t('archived_header_label')}`}>
|
||||
<ThemeIcon
|
||||
color="yellow"
|
||||
variant="light"
|
||||
style={{
|
||||
visibility: tournamentResponse.data.data.status === 'ARCHIVED' ? 'visible' : 'hidden',
|
||||
}}
|
||||
>
|
||||
<HiArchiveBoxArrowDown />
|
||||
</ThemeIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,7 @@ import useSWR, { SWRResponse } from 'swr';
|
||||
import { Pagination } from '../components/utils/util';
|
||||
import { SchedulerSettings } from '../interfaces/match';
|
||||
import { RoundInterface } from '../interfaces/round';
|
||||
import { TournamentFilter } from '../interfaces/tournament';
|
||||
import { getLogin, performLogout, tokenPresent } from './local_storage';
|
||||
|
||||
// TODO: This is a workaround for the fact that axios is not properly typed.
|
||||
@@ -114,8 +115,8 @@ export function getTournamentById(tournament_id: number): SWRResponse {
|
||||
return useSWR(`tournaments/${tournament_id}`, fetcher);
|
||||
}
|
||||
|
||||
export function getTournaments(): SWRResponse {
|
||||
return useSWR('tournaments', fetcher);
|
||||
export function getTournaments(filter: TournamentFilter): SWRResponse {
|
||||
return useSWR(`tournaments?filter_=${filter}`, fetcher);
|
||||
}
|
||||
|
||||
export function getPlayers(tournament_id: number, not_in_team: boolean = false): SWRResponse {
|
||||
|
||||
@@ -6,11 +6,13 @@ export async function createTeam(
|
||||
active: boolean,
|
||||
player_ids: string[]
|
||||
) {
|
||||
return createAxios().post(`tournaments/${tournament_id}/teams`, {
|
||||
name,
|
||||
active,
|
||||
player_ids,
|
||||
});
|
||||
return createAxios()
|
||||
.post(`tournaments/${tournament_id}/teams`, {
|
||||
name,
|
||||
active,
|
||||
player_ids,
|
||||
})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function createTeams(tournament_id: number, names: string, active: boolean) {
|
||||
|
||||
@@ -31,6 +31,14 @@ export async function deleteTournament(tournament_id: number) {
|
||||
return createAxios().delete(`tournaments/${tournament_id}`);
|
||||
}
|
||||
|
||||
export async function archiveTournament(tournament_id: number) {
|
||||
return createAxios().post(`tournaments/${tournament_id}/change-status`, { status: 'ARCHIVED' });
|
||||
}
|
||||
|
||||
export async function unarchiveTournament(tournament_id: number) {
|
||||
return createAxios().post(`tournaments/${tournament_id}/change-status`, { status: 'OPEN' });
|
||||
}
|
||||
|
||||
export async function updateTournament(
|
||||
tournament_id: number,
|
||||
name: string,
|
||||
|
||||
Reference in New Issue
Block a user