diff --git a/backend/bracket/logic/ranking/elo.py b/backend/bracket/logic/ranking/elo.py
index 6acad3849..2e816f3a6 100644
--- a/backend/bracket/logic/ranking/elo.py
+++ b/backend/bracket/logic/ranking/elo.py
@@ -35,10 +35,6 @@ def set_statistics_for_stage_item_input(
match.stage_item_input1_score, match.stage_item_input2_score
)
- # Set default for SWISS teams
- if stage_item.type is StageType.SWISS and stage_item_input_id not in stats:
- stats[stage_item_input_id].points = START_ELO
-
if has_won:
stats[stage_item_input_id].wins += 1
swiss_score_diff = ranking.win_points
@@ -73,7 +69,12 @@ def determine_ranking_for_stage_item(
stage_item: StageItemWithRounds,
ranking: Ranking,
) -> defaultdict[StageItemInputId, TeamStatistics]:
- team_x_stats: defaultdict[StageItemInputId, TeamStatistics] = defaultdict(TeamStatistics)
+ input_x_stats: defaultdict[StageItemInputId, TeamStatistics] = defaultdict(TeamStatistics)
+
+ if stage_item.type is StageType.SWISS:
+ for input_ in stage_item.inputs:
+ input_x_stats[input_.id].points = START_ELO
+
matches = [
match
for round_ in stage_item.rounds
@@ -85,14 +86,14 @@ def determine_ranking_for_stage_item(
for team_index, stage_item_input in enumerate(match.stage_item_inputs):
set_statistics_for_stage_item_input(
team_index,
- team_x_stats,
+ input_x_stats,
match,
stage_item_input.id,
ranking,
stage_item,
)
- return team_x_stats
+ return input_x_stats
def determine_team_ranking_for_stage_item(
diff --git a/backend/bracket/logic/scheduling/builder.py b/backend/bracket/logic/scheduling/builder.py
index 28d10ff74..477f8eefa 100644
--- a/backend/bracket/logic/scheduling/builder.py
+++ b/backend/bracket/logic/scheduling/builder.py
@@ -1,5 +1,6 @@
from fastapi import HTTPException
+from bracket.logic.ranking.elo import recalculate_ranking_for_stage_item_id
from bracket.logic.scheduling.elimination import (
build_single_elimination_stage_item,
get_number_of_rounds_to_create_single_elimination,
@@ -71,6 +72,8 @@ async def build_matches_for_stage_item(stage_item: StageItem, tournament_id: Tou
400, f"Cannot automatically create matches for stage type {stage_item.type}"
)
+ await recalculate_ranking_for_stage_item_id(tournament_id, stage_item.id)
+
def determine_available_inputs(
teams: list[FullTeamWithPlayers],
diff --git a/backend/bracket/routes/rankings.py b/backend/bracket/routes/rankings.py
index fa1bb4297..f889a397f 100644
--- a/backend/bracket/routes/rankings.py
+++ b/backend/bracket/routes/rankings.py
@@ -1,5 +1,6 @@
from fastapi import APIRouter, Depends
+from bracket.logic.ranking.elo import recalculate_ranking_for_stage_item_id
from bracket.logic.subscriptions import check_requirement
from bracket.models.db.ranking import RankingBody, RankingCreateBody
from bracket.models.db.user import UserPublic
@@ -17,6 +18,7 @@
sql_delete_ranking,
sql_update_ranking,
)
+from bracket.sql.stage_item_inputs import get_stage_item_input_ids_by_ranking_id
from bracket.utils.id_types import RankingId, TournamentId
router = APIRouter()
@@ -42,6 +44,9 @@ async def update_ranking_by_id(
ranking_id=ranking_id,
ranking_body=ranking_body,
)
+ stage_item_ids = await get_stage_item_input_ids_by_ranking_id(ranking_id)
+ for stage_item_id in stage_item_ids:
+ await recalculate_ranking_for_stage_item_id(tournament_id, stage_item_id)
return SuccessResponse()
diff --git a/backend/bracket/routes/stage_items.py b/backend/bracket/routes/stage_items.py
index b9e4bea4c..8b02f08f3 100644
--- a/backend/bracket/routes/stage_items.py
+++ b/backend/bracket/routes/stage_items.py
@@ -8,6 +8,7 @@
get_draft_round,
schedule_all_matches_for_swiss_round,
)
+from bracket.logic.ranking.elo import recalculate_ranking_for_stage_item_id
from bracket.logic.scheduling.builder import (
build_matches_for_stage_item,
)
@@ -109,6 +110,7 @@ async def update_stage_item(
query=query,
values={"stage_item_id": stage_item_id, "name": stage_item_body.name},
)
+ await recalculate_ranking_for_stage_item_id(tournament_id, stage_item_id)
return SuccessResponse()
diff --git a/backend/bracket/sql/stage_item_inputs.py b/backend/bracket/sql/stage_item_inputs.py
index 9bcf7b15e..2f20c309a 100644
--- a/backend/bracket/sql/stage_item_inputs.py
+++ b/backend/bracket/sql/stage_item_inputs.py
@@ -10,7 +10,7 @@
StageItemInputFinal,
)
from bracket.sql.teams import get_team_by_id
-from bracket.utils.id_types import StageItemId, StageItemInputId, TeamId, TournamentId
+from bracket.utils.id_types import RankingId, StageItemId, StageItemInputId, TeamId, TournamentId
async def get_stage_item_input_by_id(
@@ -37,6 +37,20 @@ async def get_stage_item_input_by_id(
return TypeAdapter(StageItemInput).validate_python(result)
+async def get_stage_item_input_ids_by_ranking_id(ranking_id: RankingId) -> list[StageItemId]:
+ query = """
+ SELECT id
+ FROM stage_items
+ WHERE ranking_id = :ranking_id
+ """
+ results = await database.fetch_all(
+ query=query,
+ values={"ranking_id": ranking_id},
+ )
+
+ return [StageItemId(result["id"]) for result in results]
+
+
async def sql_set_team_id_for_stage_item_input(
tournament_id: TournamentId, stage_item_input_id: StageItemInputId, team_id: TeamId | None
) -> None:
diff --git a/backend/tests/unit_tests/ranking_calculation_test.py b/backend/tests/unit_tests/ranking_calculation_test.py
index b107cf9cd..d5a76e007 100644
--- a/backend/tests/unit_tests/ranking_calculation_test.py
+++ b/backend/tests/unit_tests/ranking_calculation_test.py
@@ -247,7 +247,7 @@ def test_determine_ranking_for_stage_item_swiss_no_matches() -> None:
],
inputs=[stage_item_input1, stage_item_input2],
type_name="Swiss",
- team_count=4,
+ team_count=2,
ranking_id=None,
id=StageItemId(-1),
stage_id=StageId(-1),
@@ -267,4 +267,7 @@ def test_determine_ranking_for_stage_item_swiss_no_matches() -> None:
),
)
- assert not ranking
+ assert ranking == {
+ -2: TeamStatistics(wins=0, draws=0, losses=0, points=Decimal("1200")),
+ -1: TeamStatistics(wins=0, draws=0, losses=0, points=Decimal("1200")),
+ }
diff --git a/frontend/src/components/brackets/courts_large.tsx b/frontend/src/components/brackets/courts_large.tsx
index b474eb99c..e34f4a057 100644
--- a/frontend/src/components/brackets/courts_large.tsx
+++ b/frontend/src/components/brackets/courts_large.tsx
@@ -23,21 +23,16 @@ export function CourtBadge({ name, color }: { name: string; color: MantineColor
);
}
-function getRoundsGridCols(match: MatchInterface | null) {
- if (match == null) {
- return null;
- }
- return ;
-}
-
export default function CourtsLarge({
court,
activeMatch,
nextMatch,
+ stageItemsLookup,
}: {
court: Court;
activeMatch: MatchInterface | null;
nextMatch: MatchInterface | null;
+ stageItemsLookup: any;
}) {
return (
@@ -45,10 +40,22 @@ export default function CourtsLarge({
- {getRoundsGridCols(activeMatch)}
+
+ {activeMatch != null && (
+
+ )}
+
- {getRoundsGridCols(nextMatch)}
+
+ {nextMatch != null && (
+
+ )}
+
);
diff --git a/frontend/src/components/brackets/match_large.tsx b/frontend/src/components/brackets/match_large.tsx
index 82fb610f8..54310e4c8 100644
--- a/frontend/src/components/brackets/match_large.tsx
+++ b/frontend/src/components/brackets/match_large.tsx
@@ -1,21 +1,28 @@
import { Card, Center, Grid, Text } from '@mantine/core';
-import assert from 'assert';
import React from 'react';
import { MatchInterface } from '../../interfaces/match';
+import { formatStageItemInput } from '../../interfaces/stage_item_input';
import { Time } from '../utils/datetime';
-export default function MatchLarge({ match }: { match: MatchInterface }) {
- assert(match.stage_item_input1?.team != null);
- assert(match.stage_item_input2?.team != null);
-
+export default function MatchLarge({
+ match,
+ stageItemsLookup,
+}: {
+ match: MatchInterface;
+ stageItemsLookup: any;
+}) {
const bracket = (
- {match.stage_item_input1.team.name}
- {match.stage_item_input2.team.name}
+
+ {formatStageItemInput(match.stage_item_input1, stageItemsLookup) || N/A}
+
+
+ {formatStageItemInput(match.stage_item_input2, stageItemsLookup) || N/A}
+
diff --git a/frontend/src/components/info/player_list.tsx b/frontend/src/components/info/player_list.tsx
index 23ddab42d..e1e325d1d 100644
--- a/frontend/src/components/info/player_list.tsx
+++ b/frontend/src/components/info/player_list.tsx
@@ -1,5 +1,7 @@
+import { Text } from '@mantine/core';
+
import { TeamInterface } from '../../interfaces/team';
export default function PlayerList({ team }: { team: TeamInterface }) {
- return {team.name};
+ return {team.name};
}
diff --git a/frontend/src/components/info/player_score.tsx b/frontend/src/components/info/player_score.tsx
index ad1c9694c..76ffd8628 100644
--- a/frontend/src/components/info/player_score.tsx
+++ b/frontend/src/components/info/player_score.tsx
@@ -5,16 +5,25 @@ interface ScoreProps {
min_score: number;
max_score: number;
decimals: number;
+ fontSizeInPixels: number;
}
-export function PlayerScore({ score, min_score, max_score, decimals }: ScoreProps) {
+export function PlayerScore({
+ score,
+ min_score,
+ max_score,
+ decimals,
+ fontSizeInPixels,
+}: ScoreProps) {
const percentageScale = 100.0 / (max_score - min_score);
const empty = max_score - min_score === 0;
return (
-
+
- {Number(score).toFixed(decimals)}
+
+ {Number(score).toFixed(decimals)}
+
);
diff --git a/frontend/src/components/info/player_statistics.tsx b/frontend/src/components/info/player_statistics.tsx
index d98e7ca41..dc97e182f 100644
--- a/frontend/src/components/info/player_statistics.tsx
+++ b/frontend/src/components/info/player_statistics.tsx
@@ -4,23 +4,30 @@ interface PlayerStatisticsProps {
wins: number;
draws: number;
losses: number;
+ fontSizeInPixels: number;
}
-export function WinDistribution({ wins, draws, losses }: PlayerStatisticsProps) {
+export function WinDistribution({ wins, draws, losses, fontSizeInPixels }: PlayerStatisticsProps) {
const percentageScale = 100.0 / (wins + draws + losses);
const empty = wins + draws + losses === 0;
return (
<>
-
+
- {`${wins.toFixed(0)}`}
+
+ {`${wins.toFixed(0)}`}
+
- {`${draws.toFixed(0)}`}
+
+ {`${draws.toFixed(0)}`}
+
- {`${losses.toFixed(0)}`}
+
+ {`${losses.toFixed(0)}`}
+
>
diff --git a/frontend/src/components/no_content/empty_table_info.tsx b/frontend/src/components/no_content/empty_table_info.tsx
index 40c7c6a01..1e96724d9 100644
--- a/frontend/src/components/no_content/empty_table_info.tsx
+++ b/frontend/src/components/no_content/empty_table_info.tsx
@@ -41,7 +41,7 @@ export function NoContent({
{icon || }
{title}
-
+
{description}
diff --git a/frontend/src/components/tables/standings.tsx b/frontend/src/components/tables/standings.tsx
index 298f7faee..836eaa42e 100644
--- a/frontend/src/components/tables/standings.tsx
+++ b/frontend/src/components/tables/standings.tsx
@@ -3,9 +3,7 @@ import { useTranslation } from 'next-i18next';
import React from 'react';
import { StageItemWithRounds } from '../../interfaces/stage_item';
-import { StageItemInputFinal } from '../../interfaces/stage_item_input';
-import { TeamInterface } from '../../interfaces/team';
-import PlayerList from '../info/player_list';
+import { StageItemInputFinal, formatStageItemInput } from '../../interfaces/stage_item_input';
import { PlayerScore } from '../info/player_score';
import { WinDistribution } from '../info/player_statistics';
import { EmptyTableInfo } from '../no_content/empty_table_info';
@@ -13,71 +11,16 @@ import { WinDistributionTitle } from './players';
import { ThNotSortable, ThSortable, getTableState, sortTableEntries } from './table';
import TableLayoutLarge from './table_large';
-export default function StandingsTable({ teams }: { teams: TeamInterface[] }) {
- const { t } = useTranslation();
- const tableState = getTableState('elo_score', false);
-
- const minELOScore = Math.min(...teams.map((team) => team.elo_score));
- const maxELOScore = Math.max(...teams.map((team) => team.elo_score));
-
- const rows = teams
- .sort((p1: TeamInterface, p2: TeamInterface) => (p1.name < p2.name ? 1 : -1))
- .sort((p1: TeamInterface, p2: TeamInterface) => (p1.draws > p2.draws ? 1 : -1))
- .sort((p1: TeamInterface, p2: TeamInterface) => (p1.wins > p2.wins ? 1 : -1))
- .sort((p1: TeamInterface, p2: TeamInterface) => sortTableEntries(p1, p2, tableState))
- .map((team, index) => (
-
- {index + 1}
-
- {team.name}
-
-
-
-
-
-
-
-
-
-
-
- ));
-
- if (rows.length < 1) return ;
-
- return (
-
-
-
- #
-
- {t('name_table_header')}
-
- {t('members_table_header')}
-
- {t('elo_score')}
-
-
-
-
-
-
- {rows}
-
- );
-}
-
export function StandingsTableForStageItem({
teams_with_inputs,
stageItem,
+ fontSizeInPixels,
+ stageItemsLookup,
}: {
teams_with_inputs: StageItemInputFinal[];
stageItem: StageItemWithRounds;
+ fontSizeInPixels: number;
+ stageItemsLookup: any;
}) {
const { t } = useTranslation();
const tableState = getTableState('points', false);
@@ -91,15 +34,15 @@ export function StandingsTableForStageItem({
sortTableEntries(p1, p2, tableState)
)
.map((team_with_input, index) => (
-
+
{index + 1}
-
- {team_with_input.team.name}
+
+ {formatStageItemInput(team_with_input, stageItemsLookup)}
-
+
{team_with_input.points}
@@ -110,6 +53,7 @@ export function StandingsTableForStageItem({
min_score={minPoints}
max_score={maxPoints}
decimals={0}
+ fontSizeInPixels={fontSizeInPixels}
/>
) : (
@@ -118,6 +62,7 @@ export function StandingsTableForStageItem({
wins={team_with_input.wins}
draws={team_with_input.draws}
losses={team_with_input.losses}
+ fontSizeInPixels={fontSizeInPixels}
/>
)}
diff --git a/frontend/src/components/tables/table.tsx b/frontend/src/components/tables/table.tsx
index fb10f842a..7a1c176e2 100644
--- a/frontend/src/components/tables/table.tsx
+++ b/frontend/src/components/tables/table.tsx
@@ -76,9 +76,9 @@ export function ThSortable({ children, field, visibleFrom, state }: ThProps) {
const onSort = () => setSorting(state, field);
return (
-
+
-
+
{children}
{getSortIcon(sorted, state.reversed)}
@@ -98,7 +98,7 @@ export function ThNotSortable({
return (
-
+
{children}
diff --git a/frontend/src/components/tables/table_large.tsx b/frontend/src/components/tables/table_large.tsx
index 1d08d0726..6945fb6a9 100644
--- a/frontend/src/components/tables/table_large.tsx
+++ b/frontend/src/components/tables/table_large.tsx
@@ -5,7 +5,13 @@ export default function TableLayoutLarge({ children }: any) {
return (
<>
-
+
diff --git a/frontend/src/interfaces/stage_item_input.tsx b/frontend/src/interfaces/stage_item_input.tsx
index aec27cb31..2b4fd5b23 100644
--- a/frontend/src/interfaces/stage_item_input.tsx
+++ b/frontend/src/interfaces/stage_item_input.tsx
@@ -7,23 +7,29 @@ export interface StageItemInput {
slot: number;
tournament_id: number;
stage_item_id: number;
- team_id: number | null;
- winner_from_stage_item_id: number | null;
- winner_position: number | null;
wins: number;
draws: number;
losses: number;
points: number;
+ team_id: number | null;
team: TeamInterface | null;
+ winner_from_stage_item_id: number | null;
+ winner_position: number | null;
}
export interface StageItemInputFinal {
id: number;
- team: TeamInterface;
+ slot: number;
+ tournament_id: number;
+ stage_item_id: number;
wins: number;
draws: number;
losses: number;
points: number;
+ team_id: number;
+ team: TeamInterface;
+ winner_from_stage_item_id: number | null;
+ winner_position: number | null;
}
export interface StageItemInputCreateBody {
diff --git a/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx b/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx
index 01185e0e2..35c4c7a7a 100644
--- a/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx
+++ b/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx
@@ -22,7 +22,7 @@ import {
isMatchInTheFuture,
} from '../../../../../interfaces/match';
import { getCourtsLive, getStagesLive } from '../../../../../services/adapter';
-import { getMatchLookupByCourt } from '../../../../../services/lookups';
+import { getMatchLookupByCourt, getStageItemLookup } from '../../../../../services/lookups';
import { getTournamentResponseByEndpointName } from '../../../../../services/tournament';
export default function CourtsPage() {
@@ -43,6 +43,7 @@ export default function CourtsPage() {
if (notFound) {
return ;
}
+ const stageItemsLookup = getStageItemLookup(swrStagesResponse);
const tournamentDataFull = tournamentResponse != null ? tournamentResponse[0] : null;
const courts = responseIsValid(swrCourtsResponse) ? swrCourtsResponse.data.data : [];
@@ -53,23 +54,22 @@ export default function CourtsPage() {
const rows = courts.map((court: Court) => {
const matchesForCourt = matchesByCourtId[court.id] || [];
const activeMatch = matchesForCourt.filter((m: MatchInterface) => isMatchHappening(m))[0];
- const futureMatch = matchesForCourt.filter((m: MatchInterface) => isMatchInTheFuture(m))[0];
+ const futureMatch = matchesForCourt
+ .filter((m: MatchInterface) => isMatchInTheFuture(m))
+ .sort((m1: MatchInterface, m2: MatchInterface) =>
+ m1.start_time > m2.start_time ? 1 : -1
+ )[0];
return (
-
+
);
});
- const header = (
-
-
-
-
-
-
-
-
-
- );
return (
<>
@@ -83,7 +83,15 @@ export default function CourtsPage() {
- {header}
+
+
+
+
+
+
+
+
+
{rows}
diff --git a/frontend/src/pages/tournaments/[id]/dashboard/present/standings.tsx b/frontend/src/pages/tournaments/[id]/dashboard/present/standings.tsx
index 6c67c1ece..49393fd77 100644
--- a/frontend/src/pages/tournaments/[id]/dashboard/present/standings.tsx
+++ b/frontend/src/pages/tournaments/[id]/dashboard/present/standings.tsx
@@ -11,11 +11,11 @@ import {
TournamentQRCode,
TournamentTitle,
} from '../../../../../components/dashboard/layout';
-import StandingsTable from '../../../../../components/tables/standings';
import RequestErrorAlert from '../../../../../components/utils/error_alert';
import { TableSkeletonTwoColumns } from '../../../../../components/utils/skeletons';
-import { getTeamsLive } from '../../../../../services/adapter';
+import { getStagesLive, getTeamsLive } from '../../../../../services/adapter';
import { getTournamentResponseByEndpointName } from '../../../../../services/tournament';
+import { StandingsContent } from '../standings';
export default function Standings() {
const tournamentResponse = getTournamentResponseByEndpointName();
@@ -25,6 +25,7 @@ export default function Standings() {
const tournamentId = !notFound ? tournamentResponse[0].id : null;
const swrTeamsResponse: SWRResponse = getTeamsLive(tournamentId);
+ const swrStagesResponse = getStagesLive(tournamentId);
if (swrTeamsResponse.isLoading) {
return ;
@@ -38,6 +39,7 @@ export default function Standings() {
if (swrTeamsResponse.error) return ;
+ const fontSizeInPixels = 28;
return (
<>
@@ -49,8 +51,11 @@ export default function Standings() {
-
-
+
+
>
diff --git a/frontend/src/pages/tournaments/[id]/dashboard/standings.tsx b/frontend/src/pages/tournaments/[id]/dashboard/standings.tsx
index 45ac42e05..9e2a25985 100644
--- a/frontend/src/pages/tournaments/[id]/dashboard/standings.tsx
+++ b/frontend/src/pages/tournaments/[id]/dashboard/standings.tsx
@@ -17,7 +17,13 @@ import { getStagesLive } from '../../../../services/adapter';
import { getStageItemLookup, getStageItemTeamsLookup } from '../../../../services/lookups';
import { getTournamentResponseByEndpointName } from '../../../../services/tournament';
-function StandingsContent({ swrStagesResponse }: { swrStagesResponse: SWRResponse }) {
+export function StandingsContent({
+ swrStagesResponse,
+ fontSizeInPixels,
+}: {
+ swrStagesResponse: SWRResponse;
+ fontSizeInPixels: number;
+}) {
const { t } = useTranslation();
const stageItemsLookup = getStageItemLookup(swrStagesResponse);
@@ -32,12 +38,14 @@ function StandingsContent({ swrStagesResponse }: { swrStagesResponse: SWRRespons
)
.map((stageItemId) => (
-
+
{stageItemsLookup[stageItemId].name}
));
@@ -84,7 +92,7 @@ export default function Standings() {
-
+
diff --git a/frontend/src/services/lookups.tsx b/frontend/src/services/lookups.tsx
index f0d94f3a1..32bacfbc2 100644
--- a/frontend/src/services/lookups.tsx
+++ b/frontend/src/services/lookups.tsx
@@ -20,6 +20,7 @@ export function getTeamsLookup(tournamentId: number) {
export function getStageItemLookup(swrStagesResponse: SWRResponse) {
let result: any[] = [];
+ if (swrStagesResponse?.data == null) return Object.fromEntries(result);
swrStagesResponse.data.data.map((stage: StageWithStageItems) =>
stage.stage_items.forEach((stage_item) => {