From 632f6e5a9ddeef12221ed2029302901f89e3c5e2 Mon Sep 17 00:00:00 2001 From: Isaac George Date: Tue, 28 May 2024 16:42:39 +0100 Subject: [PATCH 01/14] feat: rank by points basic functionality --- ...f_add_multi_ranking_calculation_support.py | 42 ++ backend/bracket/logic/ranking/elo.py | 10 +- .../scheduling/handle_stage_activation.py | 10 +- backend/bracket/models/db/players.py | 1 + backend/bracket/models/db/stage_item.py | 6 + .../bracket/models/db/stage_item_inputs.py | 7 + backend/bracket/schema.py | 3 + backend/bracket/sql/players.py | 4 +- backend/bracket/sql/stage_items.py | 5 +- backend/bracket/sql/teams.py | 4 +- frontend/public/locales/en/common.json | 473 +++++++++--------- .../components/modals/create_stage_item.tsx | 37 +- frontend/src/services/stage_item.tsx | 11 +- 13 files changed, 366 insertions(+), 247 deletions(-) create mode 100644 backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py diff --git a/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py b/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py new file mode 100644 index 000000000..b7000273e --- /dev/null +++ b/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py @@ -0,0 +1,42 @@ +"""add multi ranking calculation support + +Revision ID: 450cddf36bef +Revises: 1961954c0320 +Create Date: 2024-05-27 15:04:16.583628 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import ENUM + + +# revision identifiers, used by Alembic. +revision: str | None = '450cddf36bef' +down_revision: str | None = '1961954c0320' +branch_labels: str | None = None +depends_on: str | None = None + + +ranking_mode = ENUM("HIGHEST_ELO", "HIGHEST_POINTS", name="ranking_mode", create_type=True) + +def upgrade() -> None: + print("upgrade") + ranking_mode.create(op.get_bind(), checkfirst=False) + op.add_column( + "stage_items", sa.Column("ranking_mode", ranking_mode, server_default=None, nullable=True) + ) + op.add_column( + "players", sa.Column("game_points", sa.Integer(), server_default="0", nullable=False) + ) + op.add_column( + "teams", sa.Column("game_points", sa.Integer(), server_default="0", nullable=False) + ) + + +def downgrade() -> None: + print("downgrade") + op.drop_column("stage_items", "ranking_mode") + ranking_mode.drop(op.get_bind(), checkfirst=False) + op.drop_column("players", "game_points") + op.drop_column("teams", "game_points") \ No newline at end of file diff --git a/backend/bracket/logic/ranking/elo.py b/backend/bracket/logic/ranking/elo.py index 8abb1e669..ebdb99785 100644 --- a/backend/bracket/logic/ranking/elo.py +++ b/backend/bracket/logic/ranking/elo.py @@ -3,6 +3,7 @@ from decimal import Decimal from typing import TypeVar +from bracket.models.db.stage_item import RankingMode from bracket.database import database from bracket.models.db.match import MatchWithDetailsDefinitive from bracket.models.db.players import START_ELO, PlayerStatistics @@ -44,6 +45,7 @@ def set_statistics_for_player_or_team( stats[team_or_player_id].losses += 1 swiss_score_diff = Decimal("0.00") + stats[team_or_player_id].game_points += match.team1_score if is_team1 else match.team2_score stats[team_or_player_id].swiss_score += swiss_score_diff rating_diff = (rating_team2_before - rating_team1_before) * (1 if is_team1 else -1) @@ -105,9 +107,15 @@ def determine_ranking_for_stage_items( def determine_team_ranking_for_stage_item( stage_item: StageItemWithRounds, + ranking_mode: RankingMode | None = None, ) -> list[tuple[TeamId, PlayerStatistics]]: _, team_ranking = determine_ranking_for_stage_items([stage_item]) - return sorted(team_ranking.items(), key=lambda x: x[1].elo_score, reverse=True) + + match ranking_mode: + case RankingMode.HIGHEST_POINTS: + return sorted(team_ranking.items(), key=lambda x: x[1].game_points, reverse=True) + case _: + return sorted(team_ranking.items(), key=lambda x: x[1].elo_score, reverse=True) async def recalculate_ranking_for_tournament_id(tournament_id: TournamentId) -> None: diff --git a/backend/bracket/logic/scheduling/handle_stage_activation.py b/backend/bracket/logic/scheduling/handle_stage_activation.py index 5b6fafb6c..ed91c0d83 100644 --- a/backend/bracket/logic/scheduling/handle_stage_activation.py +++ b/backend/bracket/logic/scheduling/handle_stage_activation.py @@ -1,3 +1,4 @@ +from bracket.models.db.stage_item import RankingMode from bracket.logic.ranking.elo import ( determine_team_ranking_for_stage_item, ) @@ -14,12 +15,13 @@ async def determine_team_id( winner_from_stage_item_id: StageItemId | None, winner_position: int | None, winner_from_match_id: MatchId | None, + ranking_mode: RankingMode | None, ) -> TeamId | None: if winner_from_stage_item_id is not None and winner_position is not None: stage_item = await get_stage_item(tournament_id, winner_from_stage_item_id) assert stage_item is not None - team_ranking = determine_team_ranking_for_stage_item(stage_item) + team_ranking = determine_team_ranking_for_stage_item(stage_item, ranking_mode) if len(team_ranking) >= winner_position: return team_ranking[winner_position - 1][0] @@ -38,18 +40,20 @@ async def determine_team_id( raise ValueError("Unexpected match type") -async def set_team_ids_for_match(tournament_id: TournamentId, match: MatchWithDetails) -> None: +async def set_team_ids_for_match(tournament_id: TournamentId, match: MatchWithDetails, ranking_mode: RankingMode | None) -> None: team1_id = await determine_team_id( tournament_id, match.team1_winner_from_stage_item_id, match.team1_winner_position, match.team1_winner_from_match_id, + ranking_mode, ) team2_id = await determine_team_id( tournament_id, match.team2_winner_from_stage_item_id, match.team2_winner_position, match.team2_winner_from_match_id, + ranking_mode, ) await sql_update_team_ids_for_match(assert_some(match.id), team1_id, team2_id) @@ -62,4 +66,4 @@ async def update_matches_in_activated_stage(tournament_id: TournamentId, stage_i for round_ in stage_item.rounds: for match in round_.matches: if isinstance(match, MatchWithDetails): - await set_team_ids_for_match(tournament_id, match) + await set_team_ids_for_match(tournament_id, match, stage_item.ranking_mode) diff --git a/backend/bracket/models/db/players.py b/backend/bracket/models/db/players.py index 831d3a874..6c828f755 100644 --- a/backend/bracket/models/db/players.py +++ b/backend/bracket/models/db/players.py @@ -11,3 +11,4 @@ class PlayerStatistics(BaseModel): losses: int = 0 elo_score: int = START_ELO swiss_score: Decimal = Decimal("0.00") + game_points: int = 0 diff --git a/backend/bracket/models/db/stage_item.py b/backend/bracket/models/db/stage_item.py index c2b28057c..16ca7ea5d 100644 --- a/backend/bracket/models/db/stage_item.py +++ b/backend/bracket/models/db/stage_item.py @@ -19,6 +19,10 @@ class StageType(EnumAutoStr): def supports_dynamic_number_of_rounds(self) -> bool: return self in [StageType.SWISS] +class RankingMode(EnumAutoStr): + HIGHEST_ELO = auto() + HIGHEST_POINTS = auto() + class StageItemToInsert(BaseModelORM): id: StageItemId | None = None @@ -27,6 +31,7 @@ class StageItemToInsert(BaseModelORM): created: datetime_utc type: StageType team_count: int = Field(ge=2, le=64) + ranking_mode: RankingMode | None = None class StageItem(StageItemToInsert): @@ -47,6 +52,7 @@ class StageItemCreateBody(BaseModelORM): type: StageType team_count: int = Field(ge=2, le=64) inputs: list[StageItemInputCreateBody] + ranking_mode: RankingMode | None = None def get_name_or_default_name(self) -> str: return self.name if self.name is not None else self.type.value.replace("_", " ").title() diff --git a/backend/bracket/models/db/stage_item_inputs.py b/backend/bracket/models/db/stage_item_inputs.py index da03a9b0f..3f0adee2f 100644 --- a/backend/bracket/models/db/stage_item_inputs.py +++ b/backend/bracket/models/db/stage_item_inputs.py @@ -1,9 +1,15 @@ +from enum import auto from pydantic import BaseModel, Field from bracket.models.db.shared import BaseModelORM from bracket.utils.id_types import MatchId, StageItemId, StageItemInputId, TeamId, TournamentId +from bracket.utils.types import EnumAutoStr +class RankingMode(EnumAutoStr): + HIGHEST_ELO = auto() + HIGHEST_POINTS = auto() + class StageItemInputBase(BaseModelORM): id: StageItemInputId | None = None slot: int @@ -16,6 +22,7 @@ class StageItemInputGeneric(BaseModel): winner_from_stage_item_id: StageItemId | None = None winner_position: int | None = None winner_from_match_id: MatchId | None = None + ranking_mode: RankingMode = RankingMode.HIGHEST_ELO def __hash__(self) -> int: return ( diff --git a/backend/bracket/schema.py b/backend/bracket/schema.py index 5364f3ab2..8fc09169d 100644 --- a/backend/bracket/schema.py +++ b/backend/bracket/schema.py @@ -59,6 +59,7 @@ ), nullable=False, ), + Column("ranking_mode", Enum("HIGHEST_POINTS", "HIGHEST_ELO", name="ranking_mode"), nullable=True), ) stage_item_inputs = Table( @@ -134,6 +135,7 @@ Column("draws", Integer, nullable=False, server_default="0"), Column("losses", Integer, nullable=False, server_default="0"), Column("logo_path", String, nullable=True), + Column("game_points", Integer, nullable=False, server_default="0"), ) players = Table( @@ -149,6 +151,7 @@ Column("draws", Integer, nullable=False), Column("losses", Integer, nullable=False), Column("active", Boolean, nullable=False, index=True, server_default="t"), + Column("game_points", Integer, nullable=False, server_default="0"), ) users = Table( diff --git a/backend/bracket/sql/players.py b/backend/bracket/sql/players.py index 5fae5f2e1..3b71752b4 100644 --- a/backend/bracket/sql/players.py +++ b/backend/bracket/sql/players.py @@ -87,7 +87,8 @@ async def update_player_stats( draws = :draws, losses = :losses, elo_score = :elo_score, - swiss_score = :swiss_score + swiss_score = :swiss_score, + game_points = :game_points WHERE players.tournament_id = :tournament_id AND players.id = :player_id """ @@ -101,6 +102,7 @@ async def update_player_stats( "losses": player_statistics.losses, "elo_score": player_statistics.elo_score, "swiss_score": float(player_statistics.swiss_score), + "game_points": player_statistics.game_points, }, ) diff --git a/backend/bracket/sql/stage_items.py b/backend/bracket/sql/stage_items.py index 4e5127b9d..4f353a396 100644 --- a/backend/bracket/sql/stage_items.py +++ b/backend/bracket/sql/stage_items.py @@ -11,8 +11,8 @@ async def sql_create_stage_item( ) -> StageItem: async with database.transaction(): query = """ - INSERT INTO stage_items (type, stage_id, name, team_count) - VALUES (:stage_item_type, :stage_id, :name, :team_count) + INSERT INTO stage_items (type, stage_id, name, team_count, ranking_mode) + VALUES (:stage_item_type, :stage_id, :name, :team_count, :ranking_mode) RETURNING * """ result = await database.fetch_one( @@ -22,6 +22,7 @@ async def sql_create_stage_item( "stage_id": stage_item.stage_id, "name": stage_item.get_name_or_default_name(), "team_count": stage_item.team_count, + "ranking_mode": stage_item.ranking_mode.value if stage_item.ranking_mode else None, }, ) diff --git a/backend/bracket/sql/teams.py b/backend/bracket/sql/teams.py index 3902630ff..b7eb02268 100644 --- a/backend/bracket/sql/teams.py +++ b/backend/bracket/sql/teams.py @@ -118,7 +118,8 @@ async def update_team_stats( draws = :draws, losses = :losses, elo_score = :elo_score, - swiss_score = :swiss_score + swiss_score = :swiss_score, + game_points = :game_points WHERE teams.tournament_id = :tournament_id AND teams.id = :team_id """ @@ -132,6 +133,7 @@ async def update_team_stats( "losses": team_statistics.losses, "elo_score": team_statistics.elo_score, "swiss_score": float(team_statistics.swiss_score), + "game_points": team_statistics.game_points, }, ) diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 6172466ad..b77caa84a 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -1,236 +1,239 @@ { - "8_characters_required": "Has at least 8 characters", - "accept_policy_checkbox": "I have read the policy above", - "active": "Active", - "active_badge_label": "Active", - "active_next_round_modal_choose_description": "You can choose to either (check the checkbox or not):", - "active_next_round_modal_choose_option_checked": "Adjust the start times of the next matches to start immediately(now). This will be done by modifying the margin times of the matches in the previous round.", - "active_next_round_modal_choose_option_unchecked": "Use default timing (the next matches will be planned tightly after the matches of the active round end, taking margin into account)", - "active_next_round_modal_description": "This will assign times and courts to matches of next round, which is the round after the current activated (green) round.", - "active_next_round_modal_title": "Assign times and courts to matches of next round", - "active_player_checkbox_label": "This player is active", - "active_players_checkbox_label": "These players are active", - "active_round_checkbox_label": "This round is active", - "active_team_checkbox_label": "This team is active", - "active_teams_checkbox_label": "These teams are active", - "add_court_title": "Add Court", - "add_player_button": "Add Player", - "add_round_button": "Add Round", - "add_stage_button": "Add Stage", - "add_stage_item_modal_title": "Add Stage Item", - "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", - "api_docs_title": "API docs", - "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": "Add new matches automatically", - "back_home_nav": "Take me back to home page", - "back_to_login_nav": "Back to login page", - "checkbox_status_checked": "Checked", - "checkbox_status_unchecked": "Unchecked", - "club_choose_title": "Please choose a club", - "club_name_input_placeholder": "Best Club Ever", - "club_select_label": "Club", - "club_select_placeholder": "Pick a club for this tournament", - "clubs_spotlight_description": "View, add or delete clubs", - "clubs_title": "clubs", - "copied_dashboard_url_button": "Copied Dashboard URL", - "copy_dashboard_url_button": "Copy Dashboard URL", - "could_not_find_any_alert": "Could not find any", - "court_name_input_placeholder": "Best Court Ever", - "court_spotlight_description": "View, add or delete courts", - "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", - "create_account_button": "Create Account", - "create_account_title": "Create a new account", - "create_club_button": "Create Club", - "create_court_button": "Create Court", - "create_demo_account_title": "Create demo account", - "create_player_modal_title": "Create Player", - "create_stage_item_button": "Create Stage Item", - "create_tournament_button": "Create Tournament", - "created": "Created", - "current_matches_badge": "Current matches", - "custom_match_duration_label": "Custom match duration", - "custom_match_margin_label": "Custom match margin", - "customize_checkbox_label": "Customize", - "dashboard_link_label": "Dashboard link", - "dashboard_link_placeholder": "best_tournament", - "dashboard_public_description": "Allow anyone to see the dashboard of rounds and matches", - "dashboard_settings_title": "Dashboard Settings", - "delete_button": "Delete", - "delete_club_button": "Delete Club", - "delete_court_button": "Delete Court", - "delete_player_button": "Delete Player", - "delete_round_button": "Delete Round", - "delete_team_button": "Delete Team", - "delete_tournament_button": "Delete Tournament", - "demo_description": "To test Bracket, you can start a demo. A demo will last for 30 minutes, after which your demo account be deleted. Please make fair use of it.", - "demo_policy_title": "Demo policy", - "draft_round_checkbox_label": "This round is a draft round", - "drop_match_alert_title": "Drop a match here", - "dropzone_accept_text": "Drop files here", - "dropzone_idle_text": "Upload Logo", - "dropzone_reject_text": "Image must be less than 5MB.", - "duration_minutes_choose_title": "Please choose a duration of the matches", - "edit_club_button": "Edit Club", - "edit_details_tab_title": "Edit details", - "edit_language_tab_title": "Change language", - "edit_match_modal_title": "Edit Match", - "edit_name_button": "Edit Name", - "edit_password_tab_title": "Edit password", - "edit_player": "Edit Player", - "edit_profile_title": "Edit Profile", - "edit_round": "Edit Round", - "edit_team_title": "Edit Team", - "edit_tournament_button": "Edit Tournament", - "elo_difference": "ELO Difference", - "elo_input_label": "Max ELO difference", - "elo_score": "ELO score", - "email_input_label": "Email Address", - "email_input_placeholder": "Your email", - "empty_email_validation": "Invalid email", - "empty_name_validation": "Name cannot be empty", - "empty_password_validation": "Password cannot be empty", - "filter_stage_item_label": "Filter on stage item", - "filter_stage_item_placeholder": "No filter", - "forgot_password_button": "Forgot password?", - "github_title": "Github", - "handle_swiss_system": "Handle Swiss System", - "home_spotlight_description": "Get to home page", - "home_title": "Home", - "inactive": "Inactive", - "invalid_email_validation": "Invalid email", - "invalid_password_validation": "Invalid password", - "iterations_input_label": "Iterations", - "language": "Language", - "login_success_title": "Login successful", - "logo_settings_title": "Logo Settings", - "logout_success_title": "Logout successful", - "logout_title": "Logout", - "lowercase_required": "Includes lowercase letter", - "margin_minutes_choose_title": "Please choose a margin between matches", - "match_duration_label": "Match duration (minutes)", - "match_filter_option_all": "All matches", - "match_filter_option_current": "Current matches", - "match_filter_option_past": "Hide past matches", - "max_results_input_label": "Max results", - "members_table_header": "Members", - "minutes": "minutes", - "miscellaneous_label": "Allow players to be in multiple teams", - "miscellaneous_title": "Miscellaneous", - "more_title": "More", - "multiple_players_input_label": "Add multiple players. Put every player on a separate line", - "multiple_players_input_placeholder": "Player 1", - "multiple_players_title": "Multiple Players", - "multiple_teams": "Multiple Teams", - "multiple_teams_input_label": "Add multiple teams. Put every team on a separate line", - "multiple_teams_input_placeholder": "Team 1", - "name_field_text": "name", - "name_filter_options_player": "Player names", - "name_filter_options_team": "Team names", - "name_input_label": "Name", - "name_input_placeholder": "Your name", - "name_table_header": "Name", - "negative_match_duration_validation": "Match duration cannot be negative", - "negative_match_margin_validation": "Match margin cannot be negative", - "negative_score_validation": "Score cannot be negative", - "next_matches_badge": "Next matches", - "next_stage_button": "Next Stage", - "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_players_title": "No players yet", - "no_teams_title": "No teams 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.", - "no_round_found_in_stage_description": "There are no rounds in this stage yet", - "no_round_found_title": "No rounds found", - "no_round_title": "No round", - "no_team_members_description": "No members", - "none": "None", - "not_found_description": "Unfortunately, this is only a 404 page. You may have mistyped the address, or the page has been moved to another URL.", - "not_found_title": "You have found a secret place.", - "nothing_found_placeholder": "Nothing found ...", - "now_button": "NOW", - "number_required": "Includes number", - "only_recommended_input_group_label": "Only show teams that played less matches", - "only_recommended_radio_label": "Only recommended", - "password_input_label": "Password", - "password_input_placeholder": "Your password", - "plan_next_round_button": "Plan next round", - "planning_of_matches_description": "Start of the tournament", - "planning_of_matches_legend": "Planning of matches", - "planning_spotlight_description": "Change planning of matches", - "planning_title": "Planning", - "player_name_input_placeholder": "Best Player Ever", - "players_spotlight_description": "View, add or delete players", - "players_title": "players", - "policy_not_accepted": "Please indicate that you have read the policy", - "previous_stage_button": "Previous Stage", - "recommended_badge_title": "Recommended", - "remove_logo": "Remove logo", - "remove_match_button": "Remove Match", - "results_spotlight_description": "Enter scores of matches", - "results_title": "Results", - "round_name_input_placeholder": "Best Round Ever", - "round_robin_label": "Round Robin", - "save_button": "Save", - "save_players_button": "Save players", - "schedule_description": "Schedule All Unscheduled Matches", - "schedule_title": "Schedule", - "score_of_label": "Score of", - "search_placeholder": "Search ...", - "set_to_new_button": "Set To Now", - "sign_in_title": "Sign in", - "single_elimination_label": "Single Elimination", - "single_player_title": "Single Player", - "single_team": "Single Team", - "special_character_required": "Includes special character", - "stage_spotlight_description": "Change the layout of the tournament", - "stage_title": "Stages", - "stage_type_select_label": "Stage type", - "start_demo_button": "Start demo", - "start_time": "Start time", - "start_time_choose_title": "Please choose a start time", - "status": "Status", - "swiss_difference": "Swiss Difference", - "swiss_label": "Swiss", - "swiss_score": "Swiss score", - "team_count_input_round_robin_label": "Number of teams advancing from the previous stage", - "team_count_select_elimination_label": "Number of teams advancing from the previous stage", - "team_count_select_elimination_placeholder": "2, 4, 8 etc.", - "team_member_select_title": "Team members", - "team_name_input_placeholder": "Best Team Ever", - "team_title": "Team", - "teams_spotlight_description": "View, add or delete teams", - "teams_title": "teams", - "time_between_matches_label": "Time between matches (minutes)", - "title": "Title", - "too_short_dashboard_link_validation": "Dashboard link is short", - "too_short_name_validation": "Name is too short", - "too_short_password_validation": "Password is too short", - "tournament_name_input_placeholder": "Best Tournament Ever", - "tournament_not_started_description": "Please wait for the tournament to start.", - "tournament_not_started_title": "Tournament has not started yet", - "tournament_setting_spotlight_description": "Change the settings of the tournament", - "tournament_setting_title": "Tournament Settings", - "tournament_title": "tournament", - "tournaments_title": "tournaments", - "upcoming_matches_empty_table_info": "upcoming 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", - "user_settings_spotlight_description": "Change name, email, password etc.", - "user_settings_title": "User Settings", - "user_title": "User", - "view_dashboard_button": "View Dashboard", - "website_title": "Website", - "welcome_title": "Welcome to", - "win_distribution_text_draws": "draws", - "win_distribution_text_losses": "losses", - "win_distribution_text_win": "wins" -} \ No newline at end of file + "8_characters_required": "Has at least 8 characters", + "accept_policy_checkbox": "I have read the policy above", + "active": "Active", + "active_badge_label": "Active", + "active_next_round_modal_choose_description": "You can choose to either (check the checkbox or not):", + "active_next_round_modal_choose_option_checked": "Adjust the start times of the next matches to start immediately(now). This will be done by modifying the margin times of the matches in the previous round.", + "active_next_round_modal_choose_option_unchecked": "Use default timing (the next matches will be planned tightly after the matches of the active round end, taking margin into account)", + "active_next_round_modal_description": "This will assign times and courts to matches of next round, which is the round after the current activated (green) round.", + "active_next_round_modal_title": "Assign times and courts to matches of next round", + "active_player_checkbox_label": "This player is active", + "active_players_checkbox_label": "These players are active", + "active_round_checkbox_label": "This round is active", + "active_team_checkbox_label": "This team is active", + "active_teams_checkbox_label": "These teams are active", + "add_court_title": "Add Court", + "add_player_button": "Add Player", + "add_round_button": "Add Round", + "add_stage_button": "Add Stage", + "add_stage_item_modal_title": "Add Stage Item", + "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", + "api_docs_title": "API docs", + "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": "Add new matches automatically", + "back_home_nav": "Take me back to home page", + "back_to_login_nav": "Back to login page", + "checkbox_status_checked": "Checked", + "checkbox_status_unchecked": "Unchecked", + "club_choose_title": "Please choose a club", + "club_name_input_placeholder": "Best Club Ever", + "club_select_label": "Club", + "club_select_placeholder": "Pick a club for this tournament", + "clubs_spotlight_description": "View, add or delete clubs", + "clubs_title": "clubs", + "copied_dashboard_url_button": "Copied Dashboard URL", + "copy_dashboard_url_button": "Copy Dashboard URL", + "could_not_find_any_alert": "Could not find any", + "court_name_input_placeholder": "Best Court Ever", + "court_spotlight_description": "View, add or delete courts", + "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", + "create_account_button": "Create Account", + "create_account_title": "Create a new account", + "create_club_button": "Create Club", + "create_court_button": "Create Court", + "create_demo_account_title": "Create demo account", + "create_player_modal_title": "Create Player", + "create_stage_item_button": "Create Stage Item", + "create_tournament_button": "Create Tournament", + "created": "Created", + "current_matches_badge": "Current matches", + "custom_match_duration_label": "Custom match duration", + "custom_match_margin_label": "Custom match margin", + "customize_checkbox_label": "Customize", + "dashboard_link_label": "Dashboard link", + "dashboard_link_placeholder": "best_tournament", + "dashboard_public_description": "Allow anyone to see the dashboard of rounds and matches", + "dashboard_settings_title": "Dashboard Settings", + "delete_button": "Delete", + "delete_club_button": "Delete Club", + "delete_court_button": "Delete Court", + "delete_player_button": "Delete Player", + "delete_round_button": "Delete Round", + "delete_team_button": "Delete Team", + "delete_tournament_button": "Delete Tournament", + "demo_description": "To test Bracket, you can start a demo. A demo will last for 30 minutes, after which your demo account be deleted. Please make fair use of it.", + "demo_policy_title": "Demo policy", + "draft_round_checkbox_label": "This round is a draft round", + "drop_match_alert_title": "Drop a match here", + "dropzone_accept_text": "Drop files here", + "dropzone_idle_text": "Upload Logo", + "dropzone_reject_text": "Image must be less than 5MB.", + "duration_minutes_choose_title": "Please choose a duration of the matches", + "edit_club_button": "Edit Club", + "edit_details_tab_title": "Edit details", + "edit_language_tab_title": "Change language", + "edit_match_modal_title": "Edit Match", + "edit_name_button": "Edit Name", + "edit_password_tab_title": "Edit password", + "edit_player": "Edit Player", + "edit_profile_title": "Edit Profile", + "edit_round": "Edit Round", + "edit_team_title": "Edit Team", + "edit_tournament_button": "Edit Tournament", + "elo_difference": "ELO Difference", + "elo_input_label": "Max ELO difference", + "elo_score": "ELO score", + "email_input_label": "Email Address", + "email_input_placeholder": "Your email", + "empty_email_validation": "Invalid email", + "empty_name_validation": "Name cannot be empty", + "empty_password_validation": "Password cannot be empty", + "filter_stage_item_label": "Filter on stage item", + "filter_stage_item_placeholder": "No filter", + "forgot_password_button": "Forgot password?", + "github_title": "Github", + "handle_swiss_system": "Handle Swiss System", + "highest_elo_label": "Highest ELO", + "highest_points_label": "Highest Points", + "home_spotlight_description": "Get to home page", + "home_title": "Home", + "inactive": "Inactive", + "invalid_email_validation": "Invalid email", + "invalid_password_validation": "Invalid password", + "iterations_input_label": "Iterations", + "language": "Language", + "login_success_title": "Login successful", + "logo_settings_title": "Logo Settings", + "logout_success_title": "Logout successful", + "logout_title": "Logout", + "lowercase_required": "Includes lowercase letter", + "margin_minutes_choose_title": "Please choose a margin between matches", + "match_duration_label": "Match duration (minutes)", + "match_filter_option_all": "All matches", + "match_filter_option_current": "Current matches", + "match_filter_option_past": "Hide past matches", + "max_results_input_label": "Max results", + "members_table_header": "Members", + "minutes": "minutes", + "miscellaneous_label": "Allow players to be in multiple teams", + "miscellaneous_title": "Miscellaneous", + "more_title": "More", + "multiple_players_input_label": "Add multiple players. Put every player on a separate line", + "multiple_players_input_placeholder": "Player 1", + "multiple_players_title": "Multiple Players", + "multiple_teams": "Multiple Teams", + "multiple_teams_input_label": "Add multiple teams. Put every team on a separate line", + "multiple_teams_input_placeholder": "Team 1", + "name_field_text": "name", + "name_filter_options_player": "Player names", + "name_filter_options_team": "Team names", + "name_input_label": "Name", + "name_input_placeholder": "Your name", + "name_table_header": "Name", + "negative_match_duration_validation": "Match duration cannot be negative", + "negative_match_margin_validation": "Match margin cannot be negative", + "negative_score_validation": "Score cannot be negative", + "next_matches_badge": "Next matches", + "next_stage_button": "Next Stage", + "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_players_title": "No players yet", + "no_teams_title": "No teams 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.", + "no_round_found_in_stage_description": "There are no rounds in this stage yet", + "no_round_found_title": "No rounds found", + "no_round_title": "No round", + "no_team_members_description": "No members", + "none": "None", + "not_found_description": "Unfortunately, this is only a 404 page. You may have mistyped the address, or the page has been moved to another URL.", + "not_found_title": "You have found a secret place.", + "nothing_found_placeholder": "Nothing found ...", + "now_button": "NOW", + "number_required": "Includes number", + "only_recommended_input_group_label": "Only show teams that played less matches", + "only_recommended_radio_label": "Only recommended", + "password_input_label": "Password", + "password_input_placeholder": "Your password", + "plan_next_round_button": "Plan next round", + "planning_of_matches_description": "Start of the tournament", + "planning_of_matches_legend": "Planning of matches", + "planning_spotlight_description": "Change planning of matches", + "planning_title": "Planning", + "player_name_input_placeholder": "Best Player Ever", + "players_spotlight_description": "View, add or delete players", + "players_title": "players", + "policy_not_accepted": "Please indicate that you have read the policy", + "previous_stage_button": "Previous Stage", + "rank_mode_select_label": "Ranking method for previous stage", + "recommended_badge_title": "Recommended", + "remove_logo": "Remove logo", + "remove_match_button": "Remove Match", + "results_spotlight_description": "Enter scores of matches", + "results_title": "Results", + "round_name_input_placeholder": "Best Round Ever", + "round_robin_label": "Round Robin", + "save_button": "Save", + "save_players_button": "Save players", + "schedule_description": "Schedule All Unscheduled Matches", + "schedule_title": "Schedule", + "score_of_label": "Score of", + "search_placeholder": "Search ...", + "set_to_new_button": "Set To Now", + "sign_in_title": "Sign in", + "single_elimination_label": "Single Elimination", + "single_player_title": "Single Player", + "single_team": "Single Team", + "special_character_required": "Includes special character", + "stage_spotlight_description": "Change the layout of the tournament", + "stage_title": "Stages", + "stage_type_select_label": "Stage type", + "start_demo_button": "Start demo", + "start_time": "Start time", + "start_time_choose_title": "Please choose a start time", + "status": "Status", + "swiss_difference": "Swiss Difference", + "swiss_label": "Swiss", + "swiss_score": "Swiss score", + "team_count_input_round_robin_label": "Number of teams advancing from the previous stage", + "team_count_select_elimination_label": "Number of teams advancing from the previous stage", + "team_count_select_elimination_placeholder": "2, 4, 8 etc.", + "team_member_select_title": "Team members", + "team_name_input_placeholder": "Best Team Ever", + "team_title": "Team", + "teams_spotlight_description": "View, add or delete teams", + "teams_title": "teams", + "time_between_matches_label": "Time between matches (minutes)", + "title": "Title", + "too_short_dashboard_link_validation": "Dashboard link is short", + "too_short_name_validation": "Name is too short", + "too_short_password_validation": "Password is too short", + "tournament_name_input_placeholder": "Best Tournament Ever", + "tournament_not_started_description": "Please wait for the tournament to start.", + "tournament_not_started_title": "Tournament has not started yet", + "tournament_setting_spotlight_description": "Change the settings of the tournament", + "tournament_setting_title": "Tournament Settings", + "tournament_title": "tournament", + "tournaments_title": "tournaments", + "upcoming_matches_empty_table_info": "upcoming 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", + "user_settings_spotlight_description": "Change name, email, password etc.", + "user_settings_title": "User Settings", + "user_title": "User", + "view_dashboard_button": "View Dashboard", + "website_title": "Website", + "welcome_title": "Welcome to", + "win_distribution_text_draws": "draws", + "win_distribution_text_losses": "losses", + "win_distribution_text_win": "wins" +} diff --git a/frontend/src/components/modals/create_stage_item.tsx b/frontend/src/components/modals/create_stage_item.tsx index e840526c2..717ee0c33 100644 --- a/frontend/src/components/modals/create_stage_item.tsx +++ b/frontend/src/components/modals/create_stage_item.tsx @@ -68,6 +68,7 @@ function StageItemInput({ const { t } = useTranslation(); return ( + ) : null} diff --git a/frontend/src/services/stage_item.tsx b/frontend/src/services/stage_item.tsx index d1cf79253..01ac31230 100644 --- a/frontend/src/services/stage_item.tsx +++ b/frontend/src/services/stage_item.tsx @@ -6,10 +6,17 @@ export async function createStageItem( stage_id: number, type: string, team_count: number, - inputs: StageItemInputCreateBody[] + inputs: StageItemInputCreateBody[], + ranking_mode?: string ) { return createAxios() - .post(`tournaments/${tournament_id}/stage_items`, { stage_id, type, team_count, inputs }) + .post(`tournaments/${tournament_id}/stage_items`, { + stage_id, + type, + team_count, + inputs, + ranking_mode, + }) .catch((response: any) => handleRequestError(response)); } From 3cccbdfea788e2b52240d358642029baeaf428eb Mon Sep 17 00:00:00 2001 From: Isaac George Date: Tue, 28 May 2024 16:47:52 +0100 Subject: [PATCH 02/14] chore: fix test --- backend/tests/unit_tests/elo_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/tests/unit_tests/elo_test.py b/backend/tests/unit_tests/elo_test.py index 32387c4e6..94b38032a 100644 --- a/backend/tests/unit_tests/elo_test.py +++ b/backend/tests/unit_tests/elo_test.py @@ -82,10 +82,10 @@ def test_elo_calculation() -> None: ) player_stats, team_stats = determine_ranking_for_stage_items([stage_item]) assert player_stats == { - 1: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00")), - 2: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00")), + 1: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3), + 2: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00"),game_points=4), } assert team_stats == { - 3: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00")), - 4: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00")), + 3: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3), + 4: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00"), game_points=4), } From a511a7c6185eceac4970c523a74edeadf0ccfce8 Mon Sep 17 00:00:00 2001 From: Isaac George Date: Tue, 28 May 2024 16:53:55 +0100 Subject: [PATCH 03/14] chore: remove prints --- .../450cddf36bef_add_multi_ranking_calculation_support.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py b/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py index b7000273e..064bbc9eb 100644 --- a/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py +++ b/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py @@ -21,7 +21,6 @@ ranking_mode = ENUM("HIGHEST_ELO", "HIGHEST_POINTS", name="ranking_mode", create_type=True) def upgrade() -> None: - print("upgrade") ranking_mode.create(op.get_bind(), checkfirst=False) op.add_column( "stage_items", sa.Column("ranking_mode", ranking_mode, server_default=None, nullable=True) @@ -35,7 +34,6 @@ def upgrade() -> None: def downgrade() -> None: - print("downgrade") op.drop_column("stage_items", "ranking_mode") ranking_mode.drop(op.get_bind(), checkfirst=False) op.drop_column("players", "game_points") From ec96767a7c3f00e4ad8f0b9b6af6c4608ff5a7f4 Mon Sep 17 00:00:00 2001 From: Isaac George Date: Tue, 28 May 2024 17:04:14 +0100 Subject: [PATCH 04/14] chore: fix test --- backend/tests/integration_tests/api/stages_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/tests/integration_tests/api/stages_test.py b/backend/tests/integration_tests/api/stages_test.py index c879f67db..d72117a87 100644 --- a/backend/tests/integration_tests/api/stages_test.py +++ b/backend/tests/integration_tests/api/stages_test.py @@ -66,6 +66,7 @@ async def test_stages_endpoint( "created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"), "type": "ROUND_ROBIN", "team_count": 4, + "ranking_mode": None, "rounds": [ { "id": round_inserted.id, From ddd031ae1477a34d5a53feb0e248cafb78eec66b Mon Sep 17 00:00:00 2001 From: Isaac George Date: Tue, 28 May 2024 17:16:21 +0100 Subject: [PATCH 05/14] fix: linting errors --- backend/bracket/schema.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/bracket/schema.py b/backend/bracket/schema.py index 8fc09169d..9ed736a29 100644 --- a/backend/bracket/schema.py +++ b/backend/bracket/schema.py @@ -59,7 +59,15 @@ ), nullable=False, ), - Column("ranking_mode", Enum("HIGHEST_POINTS", "HIGHEST_ELO", name="ranking_mode"), nullable=True), + Column( + "ranking_mode", + Enum( + "HIGHEST_POINTS", + "HIGHEST_ELO", + name="ranking_mode" + ), + nullable=True + ), ) stage_item_inputs = Table( From a9a7aa2df95def02b56fb26e21e90cbc74384371 Mon Sep 17 00:00:00 2001 From: Isaac George Date: Tue, 28 May 2024 17:22:49 +0100 Subject: [PATCH 06/14] fix: linting errors --- backend/bracket/logic/ranking/elo.py | 2 +- backend/bracket/logic/scheduling/handle_stage_activation.py | 6 +++++- backend/bracket/schema.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/bracket/logic/ranking/elo.py b/backend/bracket/logic/ranking/elo.py index ebdb99785..1ba6e6ed6 100644 --- a/backend/bracket/logic/ranking/elo.py +++ b/backend/bracket/logic/ranking/elo.py @@ -112,7 +112,7 @@ def determine_team_ranking_for_stage_item( _, team_ranking = determine_ranking_for_stage_items([stage_item]) match ranking_mode: - case RankingMode.HIGHEST_POINTS: + case RankingMode.HIGHEST_POINTS: return sorted(team_ranking.items(), key=lambda x: x[1].game_points, reverse=True) case _: return sorted(team_ranking.items(), key=lambda x: x[1].elo_score, reverse=True) diff --git a/backend/bracket/logic/scheduling/handle_stage_activation.py b/backend/bracket/logic/scheduling/handle_stage_activation.py index ed91c0d83..7d925b769 100644 --- a/backend/bracket/logic/scheduling/handle_stage_activation.py +++ b/backend/bracket/logic/scheduling/handle_stage_activation.py @@ -40,7 +40,11 @@ async def determine_team_id( raise ValueError("Unexpected match type") -async def set_team_ids_for_match(tournament_id: TournamentId, match: MatchWithDetails, ranking_mode: RankingMode | None) -> None: +async def set_team_ids_for_match( + tournament_id: TournamentId, + match: MatchWithDetails, + ranking_mode: RankingMode | None +) -> None: team1_id = await determine_team_id( tournament_id, match.team1_winner_from_stage_item_id, diff --git a/backend/bracket/schema.py b/backend/bracket/schema.py index 9ed736a29..393728a8d 100644 --- a/backend/bracket/schema.py +++ b/backend/bracket/schema.py @@ -65,7 +65,7 @@ "HIGHEST_POINTS", "HIGHEST_ELO", name="ranking_mode" - ), + ), nullable=True ), ) From e59adfb0f635946463db178323472fa05b924a9a Mon Sep 17 00:00:00 2001 From: Isaac George Date: Tue, 28 May 2024 17:26:59 +0100 Subject: [PATCH 07/14] chore: run reformatter --- ...cddf36bef_add_multi_ranking_calculation_support.py | 11 ++++++----- .../logic/scheduling/handle_stage_activation.py | 4 +--- backend/bracket/models/db/stage_item.py | 1 + backend/bracket/models/db/stage_item_inputs.py | 3 ++- backend/bracket/schema.py | 8 +------- backend/tests/unit_tests/elo_test.py | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) diff --git a/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py b/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py index 064bbc9eb..f9eab3768 100644 --- a/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py +++ b/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py @@ -12,14 +12,15 @@ # revision identifiers, used by Alembic. -revision: str | None = '450cddf36bef' -down_revision: str | None = '1961954c0320' +revision: str | None = "450cddf36bef" +down_revision: str | None = "1961954c0320" branch_labels: str | None = None depends_on: str | None = None ranking_mode = ENUM("HIGHEST_ELO", "HIGHEST_POINTS", name="ranking_mode", create_type=True) + def upgrade() -> None: ranking_mode.create(op.get_bind(), checkfirst=False) op.add_column( @@ -31,10 +32,10 @@ def upgrade() -> None: op.add_column( "teams", sa.Column("game_points", sa.Integer(), server_default="0", nullable=False) ) - + def downgrade() -> None: op.drop_column("stage_items", "ranking_mode") - ranking_mode.drop(op.get_bind(), checkfirst=False) + ranking_mode.drop(op.get_bind(), checkfirst=False) op.drop_column("players", "game_points") - op.drop_column("teams", "game_points") \ No newline at end of file + op.drop_column("teams", "game_points") diff --git a/backend/bracket/logic/scheduling/handle_stage_activation.py b/backend/bracket/logic/scheduling/handle_stage_activation.py index 7d925b769..25dc95edf 100644 --- a/backend/bracket/logic/scheduling/handle_stage_activation.py +++ b/backend/bracket/logic/scheduling/handle_stage_activation.py @@ -41,9 +41,7 @@ async def determine_team_id( async def set_team_ids_for_match( - tournament_id: TournamentId, - match: MatchWithDetails, - ranking_mode: RankingMode | None + tournament_id: TournamentId, match: MatchWithDetails, ranking_mode: RankingMode | None ) -> None: team1_id = await determine_team_id( tournament_id, diff --git a/backend/bracket/models/db/stage_item.py b/backend/bracket/models/db/stage_item.py index 16ca7ea5d..b19fdf595 100644 --- a/backend/bracket/models/db/stage_item.py +++ b/backend/bracket/models/db/stage_item.py @@ -19,6 +19,7 @@ class StageType(EnumAutoStr): def supports_dynamic_number_of_rounds(self) -> bool: return self in [StageType.SWISS] + class RankingMode(EnumAutoStr): HIGHEST_ELO = auto() HIGHEST_POINTS = auto() diff --git a/backend/bracket/models/db/stage_item_inputs.py b/backend/bracket/models/db/stage_item_inputs.py index 3f0adee2f..5ae8a3f3b 100644 --- a/backend/bracket/models/db/stage_item_inputs.py +++ b/backend/bracket/models/db/stage_item_inputs.py @@ -9,7 +9,8 @@ class RankingMode(EnumAutoStr): HIGHEST_ELO = auto() HIGHEST_POINTS = auto() - + + class StageItemInputBase(BaseModelORM): id: StageItemInputId | None = None slot: int diff --git a/backend/bracket/schema.py b/backend/bracket/schema.py index 393728a8d..5769ccc53 100644 --- a/backend/bracket/schema.py +++ b/backend/bracket/schema.py @@ -60,13 +60,7 @@ nullable=False, ), Column( - "ranking_mode", - Enum( - "HIGHEST_POINTS", - "HIGHEST_ELO", - name="ranking_mode" - ), - nullable=True + "ranking_mode", Enum("HIGHEST_POINTS", "HIGHEST_ELO", name="ranking_mode"), nullable=True ), ) diff --git a/backend/tests/unit_tests/elo_test.py b/backend/tests/unit_tests/elo_test.py index 94b38032a..5d601c4a0 100644 --- a/backend/tests/unit_tests/elo_test.py +++ b/backend/tests/unit_tests/elo_test.py @@ -83,7 +83,7 @@ def test_elo_calculation() -> None: player_stats, team_stats = determine_ranking_for_stage_items([stage_item]) assert player_stats == { 1: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3), - 2: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00"),game_points=4), + 2: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00"), game_points=4), } assert team_stats == { 3: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3), From a8eb560a46be5e20bd168cff5de1593efa8b9783 Mon Sep 17 00:00:00 2001 From: Isaac George Date: Tue, 28 May 2024 17:30:54 +0100 Subject: [PATCH 08/14] chore: sort imports --- .../450cddf36bef_add_multi_ranking_calculation_support.py | 2 +- backend/bracket/logic/ranking/elo.py | 2 +- backend/bracket/logic/scheduling/handle_stage_activation.py | 2 +- backend/bracket/models/db/stage_item_inputs.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py b/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py index f9eab3768..026f1c737 100644 --- a/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py +++ b/backend/alembic/versions/450cddf36bef_add_multi_ranking_calculation_support.py @@ -7,9 +7,9 @@ """ import sqlalchemy as sa -from alembic import op from sqlalchemy.dialects.postgresql import ENUM +from alembic import op # revision identifiers, used by Alembic. revision: str | None = "450cddf36bef" diff --git a/backend/bracket/logic/ranking/elo.py b/backend/bracket/logic/ranking/elo.py index 1ba6e6ed6..d8a525471 100644 --- a/backend/bracket/logic/ranking/elo.py +++ b/backend/bracket/logic/ranking/elo.py @@ -3,10 +3,10 @@ from decimal import Decimal from typing import TypeVar -from bracket.models.db.stage_item import RankingMode from bracket.database import database from bracket.models.db.match import MatchWithDetailsDefinitive from bracket.models.db.players import START_ELO, PlayerStatistics +from bracket.models.db.stage_item import RankingMode from bracket.models.db.util import StageItemWithRounds from bracket.schema import players, teams from bracket.sql.players import get_all_players_in_tournament, update_player_stats diff --git a/backend/bracket/logic/scheduling/handle_stage_activation.py b/backend/bracket/logic/scheduling/handle_stage_activation.py index 25dc95edf..595387031 100644 --- a/backend/bracket/logic/scheduling/handle_stage_activation.py +++ b/backend/bracket/logic/scheduling/handle_stage_activation.py @@ -1,8 +1,8 @@ -from bracket.models.db.stage_item import RankingMode from bracket.logic.ranking.elo import ( determine_team_ranking_for_stage_item, ) from bracket.models.db.match import MatchWithDetails +from bracket.models.db.stage_item import RankingMode from bracket.sql.matches import sql_get_match, sql_update_team_ids_for_match from bracket.sql.stage_items import get_stage_item from bracket.sql.stages import get_full_tournament_details diff --git a/backend/bracket/models/db/stage_item_inputs.py b/backend/bracket/models/db/stage_item_inputs.py index 5ae8a3f3b..4597c81e3 100644 --- a/backend/bracket/models/db/stage_item_inputs.py +++ b/backend/bracket/models/db/stage_item_inputs.py @@ -1,4 +1,5 @@ from enum import auto + from pydantic import BaseModel, Field from bracket.models.db.shared import BaseModelORM From 4a804d9219b9728d85808da78cb0ca7019d6f7ea Mon Sep 17 00:00:00 2001 From: Isaac George Date: Wed, 29 May 2024 17:01:42 +0100 Subject: [PATCH 09/14] fix: remove old code and imporve test coverage --- .../bracket/models/db/stage_item_inputs.py | 9 -- backend/tests/unit_tests/elo_test.py | 117 +++++++++++++++++- 2 files changed, 113 insertions(+), 13 deletions(-) diff --git a/backend/bracket/models/db/stage_item_inputs.py b/backend/bracket/models/db/stage_item_inputs.py index 4597c81e3..da03a9b0f 100644 --- a/backend/bracket/models/db/stage_item_inputs.py +++ b/backend/bracket/models/db/stage_item_inputs.py @@ -1,15 +1,7 @@ -from enum import auto - from pydantic import BaseModel, Field from bracket.models.db.shared import BaseModelORM from bracket.utils.id_types import MatchId, StageItemId, StageItemInputId, TeamId, TournamentId -from bracket.utils.types import EnumAutoStr - - -class RankingMode(EnumAutoStr): - HIGHEST_ELO = auto() - HIGHEST_POINTS = auto() class StageItemInputBase(BaseModelORM): @@ -24,7 +16,6 @@ class StageItemInputGeneric(BaseModel): winner_from_stage_item_id: StageItemId | None = None winner_position: int | None = None winner_from_match_id: MatchId | None = None - ranking_mode: RankingMode = RankingMode.HIGHEST_ELO def __hash__(self) -> int: return ( diff --git a/backend/tests/unit_tests/elo_test.py b/backend/tests/unit_tests/elo_test.py index 5d601c4a0..16353e605 100644 --- a/backend/tests/unit_tests/elo_test.py +++ b/backend/tests/unit_tests/elo_test.py @@ -2,9 +2,11 @@ from bracket.logic.ranking.elo import ( determine_ranking_for_stage_items, + determine_team_ranking_for_stage_item, ) from bracket.models.db.match import MatchWithDetailsDefinitive from bracket.models.db.players import PlayerStatistics +from bracket.models.db.stage_item import RankingMode from bracket.models.db.team import FullTeamWithPlayers from bracket.models.db.util import RoundWithMatches, StageItemWithRounds from bracket.utils.dummy_records import ( @@ -82,10 +84,117 @@ def test_elo_calculation() -> None: ) player_stats, team_stats = determine_ranking_for_stage_items([stage_item]) assert player_stats == { - 1: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3), - 2: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00"), game_points=4), + 1: PlayerStatistics( + losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3 + ), + 2: PlayerStatistics( + wins=1, elo_score=1216, swiss_score=Decimal("1.00"), game_points=4 + ), } assert team_stats == { - 3: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3), - 4: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00"), game_points=4), + 3: PlayerStatistics( + losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3 + ), + 4: PlayerStatistics( + wins=1, elo_score=1216, swiss_score=Decimal("1.00"), game_points=4 + ), } + + +def test_ranking_by_points(): + stage_item = StageItemWithRounds( + id=StageItemId(1), + stage_id=1, + name="Some stage item", + team_count=2, + type="SINGLE_ELIMINATION", + inputs=[], + created=DUMMY_MOCK_TIME, + rounds=[ + RoundWithMatches( + stage_item_id=StageItemId(1), + created=DUMMY_MOCK_TIME, + is_draft=False, + is_active=False, + name="Some round", + matches=[ + MatchWithDetailsDefinitive( + created=DUMMY_MOCK_TIME, + start_time=DUMMY_MOCK_TIME, + team1_id=TeamId(1), + team2_id=TeamId(1), + team1_winner_from_stage_item_id=None, + team1_winner_position=None, + team1_winner_from_match_id=None, + team2_winner_from_stage_item_id=None, + team2_winner_position=None, + team2_winner_from_match_id=None, + team1_score=3, + team2_score=4, + round_id=RoundId(1), + court_id=None, + court=None, + duration_minutes=10, + margin_minutes=5, + custom_duration_minutes=None, + custom_margin_minutes=None, + position_in_schedule=0, + team1=FullTeamWithPlayers( + id=TeamId(3), + name="Dummy team 1", + tournament_id=TournamentId(1), + active=True, + created=DUMMY_MOCK_TIME, + players=[DUMMY_PLAYER1.model_copy(update={"id": 1})], + elo_score=DUMMY_PLAYER1.elo_score, + swiss_score=DUMMY_PLAYER1.swiss_score, + wins=DUMMY_PLAYER1.wins, + draws=DUMMY_PLAYER1.draws, + losses=DUMMY_PLAYER1.losses, + ), + team2=FullTeamWithPlayers( + id=TeamId(4), + name="Dummy team 2", + tournament_id=TournamentId(1), + active=True, + created=DUMMY_MOCK_TIME, + players=[DUMMY_PLAYER2.model_copy(update={"id": 2})], + elo_score=DUMMY_PLAYER2.elo_score, + swiss_score=DUMMY_PLAYER2.swiss_score, + wins=DUMMY_PLAYER2.wins, + draws=DUMMY_PLAYER2.draws, + losses=DUMMY_PLAYER2.losses, + ), + ) + ], + ) + ], + ) + ranking = determine_team_ranking_for_stage_item( + stage_item, RankingMode.HIGHEST_POINTS + ) + print(ranking) + assert ranking == [ + ( + 4, + PlayerStatistics( + wins=1, + draws=0, + losses=0, + elo_score=1216, + swiss_score=Decimal("1.00"), + game_points=4, + ), + ), + ( + 3, + PlayerStatistics( + wins=0, + draws=0, + losses=1, + elo_score=1184, + swiss_score=Decimal("0.00"), + game_points=3, + ), + ), + ] From be4ba812bbc202223fa06ce74e8ec99d0416f152 Mon Sep 17 00:00:00 2001 From: Isaac George Date: Wed, 29 May 2024 17:14:23 +0100 Subject: [PATCH 10/14] fix: formating and linting issues --- backend/tests/unit_tests/elo_test.py | 32 ++++++++++------------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/backend/tests/unit_tests/elo_test.py b/backend/tests/unit_tests/elo_test.py index 16353e605..2e750a142 100644 --- a/backend/tests/unit_tests/elo_test.py +++ b/backend/tests/unit_tests/elo_test.py @@ -6,7 +6,7 @@ ) from bracket.models.db.match import MatchWithDetailsDefinitive from bracket.models.db.players import PlayerStatistics -from bracket.models.db.stage_item import RankingMode +from bracket.models.db.stage_item import RankingMode, StageType from bracket.models.db.team import FullTeamWithPlayers from bracket.models.db.util import RoundWithMatches, StageItemWithRounds from bracket.utils.dummy_records import ( @@ -15,7 +15,7 @@ DUMMY_PLAYER2, DUMMY_STAGE_ITEM1, ) -from bracket.utils.id_types import RoundId, StageItemId, TeamId, TournamentId +from bracket.utils.id_types import RoundId, StageId, StageItemId, TeamId, TournamentId def test_elo_calculation() -> None: @@ -84,30 +84,23 @@ def test_elo_calculation() -> None: ) player_stats, team_stats = determine_ranking_for_stage_items([stage_item]) assert player_stats == { - 1: PlayerStatistics( - losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3 - ), - 2: PlayerStatistics( - wins=1, elo_score=1216, swiss_score=Decimal("1.00"), game_points=4 - ), + 1: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3), + 2: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00"), game_points=4), } assert team_stats == { - 3: PlayerStatistics( - losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3 - ), - 4: PlayerStatistics( - wins=1, elo_score=1216, swiss_score=Decimal("1.00"), game_points=4 - ), + 3: PlayerStatistics(losses=1, elo_score=1184, swiss_score=Decimal("0.00"), game_points=3), + 4: PlayerStatistics(wins=1, elo_score=1216, swiss_score=Decimal("1.00"), game_points=4), } -def test_ranking_by_points(): +def test_ranking_by_points() -> None: stage_item = StageItemWithRounds( id=StageItemId(1), - stage_id=1, + type_name="Single Elimination", + stage_id=StageId(1), name="Some stage item", team_count=2, - type="SINGLE_ELIMINATION", + type=StageType.SINGLE_ELIMINATION, inputs=[], created=DUMMY_MOCK_TIME, rounds=[ @@ -170,10 +163,7 @@ def test_ranking_by_points(): ) ], ) - ranking = determine_team_ranking_for_stage_item( - stage_item, RankingMode.HIGHEST_POINTS - ) - print(ranking) + ranking = determine_team_ranking_for_stage_item(stage_item, RankingMode.HIGHEST_POINTS) assert ranking == [ ( 4, From 01463cea2efb5634797888171923f6d0cf81ae36 Mon Sep 17 00:00:00 2001 From: Isaac George Date: Thu, 30 May 2024 13:12:24 +0100 Subject: [PATCH 11/14] feat: show game points on leaderboards --- backend/bracket/models/db/player.py | 2 ++ backend/bracket/models/db/team.py | 2 ++ backend/bracket/utils/pagination.py | 20 ++++++++++++++++++-- frontend/public/locales/en/common.json | 1 + frontend/src/components/tables/players.tsx | 12 ++++++++++++ frontend/src/components/tables/standings.tsx | 12 ++++++++++++ frontend/src/components/tables/teams.tsx | 4 ++++ frontend/src/interfaces/player.tsx | 1 + frontend/src/interfaces/team.tsx | 1 + 9 files changed, 53 insertions(+), 2 deletions(-) diff --git a/backend/bracket/models/db/player.py b/backend/bracket/models/db/player.py index cbf3189e9..cbeb90d35 100644 --- a/backend/bracket/models/db/player.py +++ b/backend/bracket/models/db/player.py @@ -18,6 +18,7 @@ class Player(BaseModelORM): wins: int = 0 draws: int = 0 losses: int = 0 + game_points: int = 0 def __hash__(self) -> int: return self.id if self.id is not None else int(self.created.timestamp()) @@ -41,3 +42,4 @@ class PlayerToInsert(PlayerBody): wins: int = 0 draws: int = 0 losses: int = 0 + game_points: int = 0 diff --git a/backend/bracket/models/db/team.py b/backend/bracket/models/db/team.py index 96338d738..10bec4315 100644 --- a/backend/bracket/models/db/team.py +++ b/backend/bracket/models/db/team.py @@ -39,6 +39,7 @@ class TeamWithPlayers(BaseModel): losses: int = 0 name: str logo_path: str | None = None + game_points: int = 0 @property def player_ids(self) -> list[PlayerId]: @@ -100,3 +101,4 @@ class TeamToInsert(BaseModelORM): wins: int = 0 draws: int = 0 losses: int = 0 + game_points: int = 0 diff --git a/backend/bracket/utils/pagination.py b/backend/bracket/utils/pagination.py index 246de3af7..870d21080 100644 --- a/backend/bracket/utils/pagination.py +++ b/backend/bracket/utils/pagination.py @@ -14,12 +14,28 @@ class Pagination: @dataclass class PaginationPlayers(Pagination): sort_by: Literal[ - "name", "elo_score", "swiss_score", "wins", "draws", "losses", "active", "created" + "name", + "elo_score", + "swiss_score", + "wins", + "draws", + "losses", + "active", + "created", + "game_points", ] = "name" @dataclass class PaginationTeams(Pagination): sort_by: Literal[ - "name", "elo_score", "swiss_score", "wins", "draws", "losses", "active", "created" + "name", + "elo_score", + "swiss_score", + "wins", + "draws", + "losses", + "active", + "created", + "game_points", ] = "name" diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index b77caa84a..31c15fdf5 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -99,6 +99,7 @@ "filter_stage_item_label": "Filter on stage item", "filter_stage_item_placeholder": "No filter", "forgot_password_button": "Forgot password?", + "game_points": "Game points", "github_title": "Github", "handle_swiss_system": "Handle Swiss System", "highest_elo_label": "Highest ELO", diff --git a/frontend/src/components/tables/players.tsx b/frontend/src/components/tables/players.tsx index 6068bb249..9390c715c 100644 --- a/frontend/src/components/tables/players.tsx +++ b/frontend/src/components/tables/players.tsx @@ -53,6 +53,7 @@ export default function PlayersTable({ const minELOScore = Math.min(...players.map((player) => Number(player.elo_score))); const maxELOScore = Math.max(...players.map((player) => Number(player.elo_score))); const maxSwissScore = Math.max(...players.map((player) => Number(player.swiss_score))); + const maxGamePoints = Math.max(...players.map((player) => Number(player.game_points))); if (swrPlayersResponse.error) return ; @@ -96,6 +97,14 @@ export default function PlayersTable({ decimals={1} /> + + + {t('swiss_score')} + + {t('game_points')} + {null} diff --git a/frontend/src/components/tables/standings.tsx b/frontend/src/components/tables/standings.tsx index f80ea866a..dba635dd0 100644 --- a/frontend/src/components/tables/standings.tsx +++ b/frontend/src/components/tables/standings.tsx @@ -83,6 +83,7 @@ export function StandingsTableForStageItem({ const minELOScore = Math.min(...teams.map((team) => team.elo_score)); const maxELOScore = Math.max(...teams.map((team) => team.elo_score)); + const maxGamePoints = Math.max(...teams.map((team) => team.game_points)); const rows = teams .sort((p1: TeamInterface, p2: TeamInterface) => (p1.name < p2.name ? 1 : -1)) @@ -114,6 +115,14 @@ export function StandingsTableForStageItem({ )} + + + )); @@ -137,6 +146,9 @@ export function StandingsTableForStageItem({ )} + + {t('game_points')} + {rows} diff --git a/frontend/src/components/tables/teams.tsx b/frontend/src/components/tables/teams.tsx index 3fe259e96..2764156ac 100644 --- a/frontend/src/components/tables/teams.tsx +++ b/frontend/src/components/tables/teams.tsx @@ -55,6 +55,7 @@ export default function TeamsTable({ {Number(team.swiss_score).toFixed(1)} {Number(team.elo_score).toFixed(0)} + {Number(team.game_points)} {t('elo_score')} + + {t('game_points')} + {null} diff --git a/frontend/src/interfaces/player.tsx b/frontend/src/interfaces/player.tsx index 4940d0439..b917d646e 100644 --- a/frontend/src/interfaces/player.tsx +++ b/frontend/src/interfaces/player.tsx @@ -9,4 +9,5 @@ export interface Player { wins: number; draws: number; losses: number; + game_points: number; } diff --git a/frontend/src/interfaces/team.tsx b/frontend/src/interfaces/team.tsx index 97639d885..3d0ad6906 100644 --- a/frontend/src/interfaces/team.tsx +++ b/frontend/src/interfaces/team.tsx @@ -12,4 +12,5 @@ export interface TeamInterface { draws: number; losses: number; logo_path: string; + game_points: number; } From 035710fb63ad8b32844ce2bef7c8cbb309c4a765 Mon Sep 17 00:00:00 2001 From: Isaac George Date: Thu, 30 May 2024 13:23:08 +0100 Subject: [PATCH 12/14] fix: tests --- backend/tests/integration_tests/api/matches_test.py | 7 +++++++ backend/tests/integration_tests/api/players_test.py | 1 + backend/tests/integration_tests/api/teams_test.py | 1 + 3 files changed, 9 insertions(+) diff --git a/backend/tests/integration_tests/api/matches_test.py b/backend/tests/integration_tests/api/matches_test.py index b3e861535..ef0b726ed 100644 --- a/backend/tests/integration_tests/api/matches_test.py +++ b/backend/tests/integration_tests/api/matches_test.py @@ -255,6 +255,7 @@ async def test_upcoming_matches_endpoint( "wins": 0, "draws": 0, "losses": 0, + "game_points": 0, }, { "id": player_inserted_3.id, @@ -267,6 +268,7 @@ async def test_upcoming_matches_endpoint( "wins": 0, "draws": 0, "losses": 0, + "game_points": 0, }, ], "swiss_score": "0.0", @@ -275,6 +277,8 @@ async def test_upcoming_matches_endpoint( "draws": 0, "losses": 0, "logo_path": None, + "game_points": 0, + }, "team2": { "id": team2_inserted.id, @@ -291,6 +295,7 @@ async def test_upcoming_matches_endpoint( "wins": 0, "draws": 0, "losses": 0, + "game_points": 0, }, { "id": player_inserted_4.id, @@ -303,6 +308,7 @@ async def test_upcoming_matches_endpoint( "wins": 0, "draws": 0, "losses": 0, + "game_points": 0, }, ], "swiss_score": "0.0", @@ -311,6 +317,7 @@ async def test_upcoming_matches_endpoint( "draws": 0, "losses": 0, "logo_path": None, + "game_points": 0, }, "elo_diff": "200", "swiss_diff": "0", diff --git a/backend/tests/integration_tests/api/players_test.py b/backend/tests/integration_tests/api/players_test.py index 98190aed9..e3ec4b3ef 100644 --- a/backend/tests/integration_tests/api/players_test.py +++ b/backend/tests/integration_tests/api/players_test.py @@ -32,6 +32,7 @@ async def test_players_endpoint( "losses": 0, "name": "Player 01", "tournament_id": auth_context.tournament.id, + "game_points": 0, } ], "count": 1, diff --git a/backend/tests/integration_tests/api/teams_test.py b/backend/tests/integration_tests/api/teams_test.py index e1f3a5aa0..55f504fd6 100644 --- a/backend/tests/integration_tests/api/teams_test.py +++ b/backend/tests/integration_tests/api/teams_test.py @@ -34,6 +34,7 @@ async def test_teams_endpoint( "draws": 0, "losses": 0, "logo_path": None, + "game_points": 0, } ], "count": 1, From 1c027fc646d2e0ccd64a6a5330811b7a8b8c2346 Mon Sep 17 00:00:00 2001 From: Isaac George Date: Thu, 30 May 2024 13:27:42 +0100 Subject: [PATCH 13/14] fix: remoe white space --- backend/tests/integration_tests/api/matches_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/tests/integration_tests/api/matches_test.py b/backend/tests/integration_tests/api/matches_test.py index ef0b726ed..65586b527 100644 --- a/backend/tests/integration_tests/api/matches_test.py +++ b/backend/tests/integration_tests/api/matches_test.py @@ -277,8 +277,7 @@ async def test_upcoming_matches_endpoint( "draws": 0, "losses": 0, "logo_path": None, - "game_points": 0, - + "game_points": 0, }, "team2": { "id": team2_inserted.id, From 4a3b5ed5084e52fc7745a999aae9f7803f5d14e5 Mon Sep 17 00:00:00 2001 From: Isaac George Date: Thu, 30 May 2024 13:31:24 +0100 Subject: [PATCH 14/14] fix: remoe white space --- backend/tests/integration_tests/api/matches_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/integration_tests/api/matches_test.py b/backend/tests/integration_tests/api/matches_test.py index 65586b527..0d8bb116c 100644 --- a/backend/tests/integration_tests/api/matches_test.py +++ b/backend/tests/integration_tests/api/matches_test.py @@ -277,7 +277,7 @@ async def test_upcoming_matches_endpoint( "draws": 0, "losses": 0, "logo_path": None, - "game_points": 0, + "game_points": 0, }, "team2": { "id": team2_inserted.id,